@bilig/xlsx 0.164.0
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.
- package/README.md +25 -0
- package/dist/address.d.ts +15 -0
- package/dist/address.js +82 -0
- package/dist/address.js.map +1 -0
- package/dist/external-workbook-types.d.ts +44 -0
- package/dist/external-workbook-types.js +12 -0
- package/dist/external-workbook-types.js.map +1 -0
- package/dist/file-source.d.ts +10 -0
- package/dist/file-source.js +76 -0
- package/dist/file-source.js.map +1 -0
- package/dist/formula-cache-reader.d.ts +30 -0
- package/dist/formula-cache-reader.js +350 -0
- package/dist/formula-cache-reader.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/simple-workbook-writer.d.ts +114 -0
- package/dist/simple-workbook-writer.js +621 -0
- package/dist/simple-workbook-writer.js.map +1 -0
- package/dist/source-preserving-literal-patches.d.ts +43 -0
- package/dist/source-preserving-literal-patches.js +615 -0
- package/dist/source-preserving-literal-patches.js.map +1 -0
- package/dist/source-preserving-zip.d.ts +25 -0
- package/dist/source-preserving-zip.js +341 -0
- package/dist/source-preserving-zip.js.map +1 -0
- package/dist/streaming-native-cell-arena.d.ts +57 -0
- package/dist/streaming-native-cell-arena.js +233 -0
- package/dist/streaming-native-cell-arena.js.map +1 -0
- package/dist/streaming-native-external-cache.d.ts +13 -0
- package/dist/streaming-native-external-cache.js +753 -0
- package/dist/streaming-native-external-cache.js.map +1 -0
- package/dist/streaming-native-inspect.d.ts +42 -0
- package/dist/streaming-native-inspect.js +297 -0
- package/dist/streaming-native-inspect.js.map +1 -0
- package/dist/streaming-native-lookup-wasm.d.ts +13 -0
- package/dist/streaming-native-lookup-wasm.js +250 -0
- package/dist/streaming-native-lookup-wasm.js.map +1 -0
- package/dist/streaming-native-recalc-evaluator.d.ts +17 -0
- package/dist/streaming-native-recalc-evaluator.js +743 -0
- package/dist/streaming-native-recalc-evaluator.js.map +1 -0
- package/dist/streaming-native-recalc.d.ts +97 -0
- package/dist/streaming-native-recalc.js +652 -0
- package/dist/streaming-native-recalc.js.map +1 -0
- package/dist/streaming-native-row-chain-conditionals.d.ts +8 -0
- package/dist/streaming-native-row-chain-conditionals.js +385 -0
- package/dist/streaming-native-row-chain-conditionals.js.map +1 -0
- package/dist/streaming-native-row-chain-dependencies.d.ts +17 -0
- package/dist/streaming-native-row-chain-dependencies.js +365 -0
- package/dist/streaming-native-row-chain-dependencies.js.map +1 -0
- package/dist/streaming-native-row-chain-references.d.ts +3 -0
- package/dist/streaming-native-row-chain-references.js +36 -0
- package/dist/streaming-native-row-chain-references.js.map +1 -0
- package/dist/streaming-native-row-chain-wasm.d.ts +56 -0
- package/dist/streaming-native-row-chain-wasm.js +546 -0
- package/dist/streaming-native-row-chain-wasm.js.map +1 -0
- package/dist/streaming-native-text.d.ts +2 -0
- package/dist/streaming-native-text.js +14 -0
- package/dist/streaming-native-text.js.map +1 -0
- package/dist/streaming-native-workbook-core.d.ts +47 -0
- package/dist/streaming-native-workbook-core.js +110 -0
- package/dist/streaming-native-workbook-core.js.map +1 -0
- package/dist/targeted-cell-reader.d.ts +11 -0
- package/dist/targeted-cell-reader.js +92 -0
- package/dist/targeted-cell-reader.js.map +1 -0
- package/dist/workbook-cell-reader.d.ts +29 -0
- package/dist/workbook-cell-reader.js +200 -0
- package/dist/workbook-cell-reader.js.map +1 -0
- package/dist/workbook-compatibility-report.d.ts +101 -0
- package/dist/workbook-compatibility-report.js +654 -0
- package/dist/workbook-compatibility-report.js.map +1 -0
- package/dist/workbook-sheet-paths.d.ts +8 -0
- package/dist/workbook-sheet-paths.js +79 -0
- package/dist/workbook-sheet-paths.js.map +1 -0
- package/dist/xml-part-patch.d.ts +12 -0
- package/dist/xml-part-patch.js +45 -0
- package/dist/xml-part-patch.js.map +1 -0
- package/dist/xml.d.ts +9 -0
- package/dist/xml.js +42 -0
- package/dist/xml.js.map +1 -0
- package/dist/zip-reader.d.ts +51 -0
- package/dist/zip-reader.js +448 -0
- package/dist/zip-reader.js.map +1 -0
- package/package.json +113 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { readXlsxFormulaCacheCellsFromWorkbookCore } from './formula-cache-reader.js';
|
|
4
|
+
import { writeSimpleXlsxWorkbook } from './simple-workbook-writer.js';
|
|
5
|
+
import { readXlsxWorkbookCells } from './workbook-cell-reader.js';
|
|
6
|
+
import { closeStreamingNativeWorkbookCore, openStreamingNativeWorkbookCore, scanStreamingNativeWorkbookCellStats, scanStreamingNativeWorkbookPackageParts, } from './streaming-native-workbook-core.js';
|
|
7
|
+
import { readXlsxZipEntriesLazy } from './zip-reader.js';
|
|
8
|
+
export const workbookCompatibilityReportSchemaVersion = 'bilig-workbook-compatibility-report.v1';
|
|
9
|
+
const defaultInspectLimit = 'all';
|
|
10
|
+
const defaultFileInspectLimit = 2000;
|
|
11
|
+
const workbookCompatibilityReportBytesApiLimit = 1_000_000;
|
|
12
|
+
const docsUrl = 'https://proompteng.github.io/bilig/workbook-compatibility-report.html';
|
|
13
|
+
const volatileFunctionNames = ['TODAY', 'NOW', 'RAND', 'RANDBETWEEN', 'RANDARRAY', 'OFFSET', 'INDIRECT', 'SUBTOTAL', 'AGGREGATE'];
|
|
14
|
+
const knownUnsupportedRiskFunctions = new Set([
|
|
15
|
+
'CALL',
|
|
16
|
+
'COPILOT',
|
|
17
|
+
'CUBEKPIMEMBER',
|
|
18
|
+
'CUBEMEMBER',
|
|
19
|
+
'CUBEMEMBERPROPERTY',
|
|
20
|
+
'CUBERANKEDMEMBER',
|
|
21
|
+
'CUBESET',
|
|
22
|
+
'CUBESETCOUNT',
|
|
23
|
+
'CUBEVALUE',
|
|
24
|
+
'DDE',
|
|
25
|
+
'DETECTLANGUAGE',
|
|
26
|
+
'FILTERXML',
|
|
27
|
+
'GOOGLEFINANCE',
|
|
28
|
+
'IMAGE',
|
|
29
|
+
'IMPORTDATA',
|
|
30
|
+
'IMPORTFEED',
|
|
31
|
+
'IMPORTHTML',
|
|
32
|
+
'IMPORTRANGE',
|
|
33
|
+
'IMPORTXML',
|
|
34
|
+
'INFO',
|
|
35
|
+
'PY',
|
|
36
|
+
'REGISTER.ID',
|
|
37
|
+
'RTD',
|
|
38
|
+
'SQL.REQUEST',
|
|
39
|
+
'STOCKHISTORY',
|
|
40
|
+
'TRANSLATE',
|
|
41
|
+
'WEBSERVICE',
|
|
42
|
+
]);
|
|
43
|
+
export function buildWorkbookCompatibilityReport(input, options = {}) {
|
|
44
|
+
const bytes = toUint8Array(input);
|
|
45
|
+
assertWorkbookCompatibilityReportBytesApiWithinLimit(bytes, 'buildWorkbookCompatibilityReport');
|
|
46
|
+
const rss = createWorkbookCompatibilityRssRecorder(options.maxRssBytes);
|
|
47
|
+
rss.recordPhase('bytes-api:start');
|
|
48
|
+
const fileName = options.fileName ?? 'workbook.xlsx';
|
|
49
|
+
const externalWorkbooks = options.externalWorkbooks ?? [];
|
|
50
|
+
const zip = readXlsxZipEntriesLazy(bytes);
|
|
51
|
+
rss.recordPhase('bytes-api:zip');
|
|
52
|
+
const workbook = readXlsxWorkbookCells(zip);
|
|
53
|
+
rss.recordPhase('bytes-api:cells');
|
|
54
|
+
const workbookParts = scanStreamingNativeWorkbookPackageParts({ zip });
|
|
55
|
+
rss.recordPhase('bytes-api:package-parts');
|
|
56
|
+
const cacheInspection = inspectWorkbookFormulaCaches(workbook, options.inspectLimit ?? defaultInspectLimit);
|
|
57
|
+
rss.recordPhase('bytes-api:formula-cache');
|
|
58
|
+
return buildWorkbookCompatibilityReportFromScans({
|
|
59
|
+
fileName,
|
|
60
|
+
externalWorkbookCount: externalWorkbooks.length,
|
|
61
|
+
workbook: {
|
|
62
|
+
sheetNames: workbook.sheets.map((sheet) => sheet.name),
|
|
63
|
+
nonEmptyCellCount: workbook.sheets.reduce((sum, sheet) => sum + sheet.cells.length, 0),
|
|
64
|
+
},
|
|
65
|
+
workbookParts,
|
|
66
|
+
cacheInspection,
|
|
67
|
+
diagnostics: {
|
|
68
|
+
inputBytes: bytes.byteLength,
|
|
69
|
+
maxObservedRssBytes: rss.maxObservedRssBytes,
|
|
70
|
+
...(options.maxRssBytes === undefined ? {} : { maxRssBytes: options.maxRssBytes }),
|
|
71
|
+
phaseRssPeaks: rss.phaseRssPeaks,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function buildWorkbookCompatibilityReportFromFile(inputPath, options = {}) {
|
|
76
|
+
const fileName = options.fileName ?? basename(inputPath);
|
|
77
|
+
const externalWorkbooks = options.externalWorkbooks ?? [];
|
|
78
|
+
const inspectLimit = options.inspectLimit ?? defaultFileInspectLimit;
|
|
79
|
+
const rss = createWorkbookCompatibilityRssRecorder(options.maxRssBytes);
|
|
80
|
+
const inputBytes = statSync(inputPath).size;
|
|
81
|
+
rss.recordPhase('file-api:start');
|
|
82
|
+
const core = openStreamingNativeWorkbookCore(inputPath);
|
|
83
|
+
try {
|
|
84
|
+
rss.recordPhase('file-api:open-core');
|
|
85
|
+
const workbook = scanStreamingNativeWorkbookCellStats(core);
|
|
86
|
+
rss.recordPhase('file-api:cell-stats');
|
|
87
|
+
const workbookParts = scanStreamingNativeWorkbookPackageParts(core);
|
|
88
|
+
rss.recordPhase('file-api:package-parts');
|
|
89
|
+
const cacheInspection = inspectWorkbookFormulaCacheScan(readXlsxFormulaCacheCellsFromWorkbookCore(core, {
|
|
90
|
+
inspectLimit,
|
|
91
|
+
}), inspectLimit);
|
|
92
|
+
rss.recordPhase('file-api:formula-cache');
|
|
93
|
+
return buildWorkbookCompatibilityReportFromScans({
|
|
94
|
+
fileName,
|
|
95
|
+
externalWorkbookCount: externalWorkbooks.length,
|
|
96
|
+
workbook,
|
|
97
|
+
workbookParts,
|
|
98
|
+
cacheInspection,
|
|
99
|
+
diagnostics: {
|
|
100
|
+
inputBytes,
|
|
101
|
+
maxObservedRssBytes: rss.maxObservedRssBytes,
|
|
102
|
+
...(options.maxRssBytes === undefined ? {} : { maxRssBytes: options.maxRssBytes }),
|
|
103
|
+
phaseRssPeaks: rss.phaseRssPeaks,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
closeStreamingNativeWorkbookCore(core);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function buildWorkbookCompatibilityReportFromScans(input) {
|
|
112
|
+
const { fileName, externalWorkbookCount, workbook, workbookParts, cacheInspection } = input;
|
|
113
|
+
const unsupportedFunctions = countNamedValues(cacheInspection.formulas.flatMap((entry) => knownUnsupportedFunctionNamesFromFormula(entry.formula)));
|
|
114
|
+
const volatileFunctions = countNamedValues(cacheInspection.formulas.flatMap((entry) => volatileFunctionNamesFromFormula(entry.formula)));
|
|
115
|
+
const diagnostics = buildWorkbookCompatibilityNativeDiagnostics({
|
|
116
|
+
...input.diagnostics,
|
|
117
|
+
sheetCount: workbook.sheetNames.length,
|
|
118
|
+
inspectedFormulaCellCount: cacheInspection.inspectedFormulaCellCount,
|
|
119
|
+
formulaCellCount: cacheInspection.formulaCellCount,
|
|
120
|
+
unsupportedFormulaCellCount: cacheInspection.cacheStatusSummary.unsupportedRecalculation,
|
|
121
|
+
unsupportedFunctions,
|
|
122
|
+
workbookParts,
|
|
123
|
+
});
|
|
124
|
+
const report = {
|
|
125
|
+
schemaVersion: workbookCompatibilityReportSchemaVersion,
|
|
126
|
+
verified: true,
|
|
127
|
+
input: {
|
|
128
|
+
fileName,
|
|
129
|
+
externalWorkbookCount,
|
|
130
|
+
inspectLimit: cacheInspection.inspectionLimit,
|
|
131
|
+
},
|
|
132
|
+
workbook: {
|
|
133
|
+
sheetCount: workbook.sheetNames.length,
|
|
134
|
+
sheetNames: workbook.sheetNames,
|
|
135
|
+
nonEmptyCellCount: workbook.nonEmptyCellCount,
|
|
136
|
+
formulaCellCount: cacheInspection.formulaCellCount,
|
|
137
|
+
definedNameCount: workbookParts.definedNameCount,
|
|
138
|
+
tableCount: workbookParts.tableCount,
|
|
139
|
+
pivotTableCount: workbookParts.pivotTableCount,
|
|
140
|
+
chartCount: workbookParts.chartCount,
|
|
141
|
+
macroModuleCount: workbookParts.macroModuleCount,
|
|
142
|
+
},
|
|
143
|
+
findings: {
|
|
144
|
+
unsupportedFunctions,
|
|
145
|
+
externalLinks: {
|
|
146
|
+
count: workbookParts.externalLinkCount,
|
|
147
|
+
unresolvedCount: 0,
|
|
148
|
+
refreshedCount: 0,
|
|
149
|
+
},
|
|
150
|
+
macroModules: {
|
|
151
|
+
count: workbookParts.macroModuleCount,
|
|
152
|
+
byteLength: workbookParts.macroByteLength,
|
|
153
|
+
},
|
|
154
|
+
volatileFunctions,
|
|
155
|
+
pivotTables: {
|
|
156
|
+
count: workbookParts.pivotTableCount,
|
|
157
|
+
unsupportedCount: 0,
|
|
158
|
+
cacheOnlyCount: 0,
|
|
159
|
+
},
|
|
160
|
+
staleCachedFormulas: {
|
|
161
|
+
count: cacheInspection.staleCachedFormulaCount,
|
|
162
|
+
},
|
|
163
|
+
missingCachedFormulaValues: {
|
|
164
|
+
count: cacheInspection.cacheStatusSummary.missingCache,
|
|
165
|
+
},
|
|
166
|
+
unsupportedRecalculations: {
|
|
167
|
+
count: cacheInspection.cacheStatusSummary.unsupportedRecalculation,
|
|
168
|
+
},
|
|
169
|
+
warnings: cacheInspection.warnings,
|
|
170
|
+
},
|
|
171
|
+
cacheInspection: {
|
|
172
|
+
inspectedFormulaCellCount: cacheInspection.inspectedFormulaCellCount,
|
|
173
|
+
uninspectedFormulaCellCount: cacheInspection.uninspectedFormulaCellCount,
|
|
174
|
+
inspectionLimit: cacheInspection.inspectionLimit,
|
|
175
|
+
suggestedReads: cacheInspection.suggestedReads,
|
|
176
|
+
},
|
|
177
|
+
diagnostics,
|
|
178
|
+
commandSucceeded: true,
|
|
179
|
+
inspectionCompleted: true,
|
|
180
|
+
recalculationCompleted: false,
|
|
181
|
+
excelParity: 'not_proven',
|
|
182
|
+
limitations: [
|
|
183
|
+
'This report identifies workbook features that may require investigation before using Bilig in a service or agent workflow.',
|
|
184
|
+
'It is not an Excel compatibility certification.',
|
|
185
|
+
'It scans workbook package metadata and formula caches; use xlsx-cache-doctor for native recalculation proof.',
|
|
186
|
+
'It does not execute VBA, refresh pivots, refresh external data sources, or prove desktop Excel UI behavior.',
|
|
187
|
+
],
|
|
188
|
+
next: {
|
|
189
|
+
docs: docsUrl,
|
|
190
|
+
command: `workbook-compatibility-report ${shellQuote(fileName)} --json`,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
...report,
|
|
195
|
+
risk: buildRisk(report),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export function runWorkbookCompatibilityReportCli(args, context = {}) {
|
|
199
|
+
const commandName = context.commandName ?? 'workbook-compatibility-report';
|
|
200
|
+
const writeStdout = context.stdout ?? ((text) => process.stdout.write(text));
|
|
201
|
+
const writeStderr = context.stderr ?? ((text) => process.stderr.write(text));
|
|
202
|
+
try {
|
|
203
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
204
|
+
writeStdout(renderHelp(commandName));
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
const options = parseCliArgs(args, commandName);
|
|
208
|
+
const fileName = options.mode === 'demo' ? 'bilig-workbook-compatibility-demo.xlsx' : basename(requireInputPath(options));
|
|
209
|
+
const externalWorkbooks = readExternalWorkbookReferenceInputs(options.externalWorkbooks);
|
|
210
|
+
const report = options.mode === 'demo'
|
|
211
|
+
? buildWorkbookCompatibilityReport(buildWorkbookCompatibilityDemoBytes(), {
|
|
212
|
+
fileName,
|
|
213
|
+
...(externalWorkbooks.length > 0 ? { externalWorkbooks } : {}),
|
|
214
|
+
...(options.inspectLimit === undefined ? {} : { inspectLimit: options.inspectLimit }),
|
|
215
|
+
})
|
|
216
|
+
: buildWorkbookCompatibilityReportFromFile(requireInputPath(options), {
|
|
217
|
+
fileName,
|
|
218
|
+
...(externalWorkbooks.length > 0 ? { externalWorkbooks } : {}),
|
|
219
|
+
...(options.inspectLimit === undefined ? {} : { inspectLimit: options.inspectLimit }),
|
|
220
|
+
});
|
|
221
|
+
if (options.json) {
|
|
222
|
+
writeStdout(`${JSON.stringify(report, null, 2)}\n`);
|
|
223
|
+
return report.verified ? 0 : 1;
|
|
224
|
+
}
|
|
225
|
+
writeStdout(renderHumanReport(report));
|
|
226
|
+
return report.verified ? 0 : 1;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
writeStderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export function buildWorkbookCompatibilityDemoBytes() {
|
|
234
|
+
return writeSimpleXlsxWorkbook({
|
|
235
|
+
sheets: [
|
|
236
|
+
{
|
|
237
|
+
name: 'Inputs',
|
|
238
|
+
cells: [
|
|
239
|
+
{ address: 'A1', row: 0, col: 0, value: 'Metric' },
|
|
240
|
+
{ address: 'B1', row: 0, col: 1, value: 'Value' },
|
|
241
|
+
{ address: 'A2', row: 1, col: 0, value: 'Units' },
|
|
242
|
+
{ address: 'B2', row: 1, col: 1, value: 40 },
|
|
243
|
+
{ address: 'A3', row: 2, col: 0, value: 'Price' },
|
|
244
|
+
{ address: 'B3', row: 2, col: 1, value: 1200 },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'Summary',
|
|
249
|
+
cells: [
|
|
250
|
+
{ address: 'A1', row: 0, col: 0, value: 'Metric' },
|
|
251
|
+
{ address: 'B1', row: 0, col: 1, value: 'Value' },
|
|
252
|
+
{ address: 'A2', row: 1, col: 0, value: 'Revenue' },
|
|
253
|
+
{ address: 'B2', row: 1, col: 1, formula: 'Inputs!B2*Inputs!B3', value: 60_000 },
|
|
254
|
+
{ address: 'A3', row: 2, col: 0, value: 'GeneratedAt' },
|
|
255
|
+
{ address: 'B3', row: 2, col: 1, formula: 'NOW()', value: 45_123 },
|
|
256
|
+
{ address: 'A4', row: 3, col: 0, value: 'CubeSales' },
|
|
257
|
+
{ address: 'B4', row: 3, col: 1, formula: 'CUBEVALUE("ThisWorkbookDataModel","[Measures].[Sales]")' },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function createWorkbookCompatibilityRssRecorder(maxRssBytes) {
|
|
264
|
+
const recorder = {
|
|
265
|
+
phaseRssPeaks: [],
|
|
266
|
+
maxObservedRssBytes: 0,
|
|
267
|
+
recordPhase(phase) {
|
|
268
|
+
const rssBytes = process.memoryUsage().rss;
|
|
269
|
+
recorder.maxObservedRssBytes = Math.max(recorder.maxObservedRssBytes, rssBytes);
|
|
270
|
+
recorder.phaseRssPeaks.push({ phase, rssBytes });
|
|
271
|
+
if (maxRssBytes !== undefined && rssBytes > maxRssBytes) {
|
|
272
|
+
throw new Error(`workbook compatibility report exceeded maxRssBytes during ${phase}: ${rssBytes} > ${maxRssBytes}`);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
return recorder;
|
|
277
|
+
}
|
|
278
|
+
function buildWorkbookCompatibilityNativeDiagnostics(args) {
|
|
279
|
+
const unsupportedReason = workbookCompatibilityUnsupportedReason(args);
|
|
280
|
+
return {
|
|
281
|
+
engineMode: 'streaming-native',
|
|
282
|
+
fallbackUsed: false,
|
|
283
|
+
inputBytes: args.inputBytes,
|
|
284
|
+
phaseRssPeaks: args.phaseRssPeaks,
|
|
285
|
+
maxObservedRssBytes: args.maxObservedRssBytes,
|
|
286
|
+
...(args.maxRssBytes === undefined ? {} : { maxRssBytes: args.maxRssBytes }),
|
|
287
|
+
sheetCount: args.sheetCount,
|
|
288
|
+
targetRowCount: args.inspectedFormulaCellCount,
|
|
289
|
+
editCount: 0,
|
|
290
|
+
readCount: args.inspectedFormulaCellCount,
|
|
291
|
+
formulaCounts: {
|
|
292
|
+
scannedFormulaCellCount: args.formulaCellCount,
|
|
293
|
+
targetedFormulaCellCount: args.inspectedFormulaCellCount,
|
|
294
|
+
evaluatedFormulaCellCount: 0,
|
|
295
|
+
patchedFormulaCacheCount: 0,
|
|
296
|
+
unsupportedFormulaCellCount: args.unsupportedFormulaCellCount,
|
|
297
|
+
nativeKernelFormulaCellCount: 0,
|
|
298
|
+
nativeKernelBatchCount: 0,
|
|
299
|
+
},
|
|
300
|
+
patchedCacheCount: 0,
|
|
301
|
+
...(unsupportedReason === undefined ? {} : { unsupportedReason }),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function workbookCompatibilityUnsupportedReason(args) {
|
|
305
|
+
if (args.unsupportedFunctions.length > 0) {
|
|
306
|
+
return `unsupported functions: ${formatNamedCounts(args.unsupportedFunctions)}`;
|
|
307
|
+
}
|
|
308
|
+
if (args.unsupportedFormulaCellCount > 0) {
|
|
309
|
+
return `unsupported formula cache inspections: ${args.unsupportedFormulaCellCount.toString()}`;
|
|
310
|
+
}
|
|
311
|
+
if (args.workbookParts.externalLinkCount > 0) {
|
|
312
|
+
return `external workbook links: ${args.workbookParts.externalLinkCount.toString()}`;
|
|
313
|
+
}
|
|
314
|
+
if (args.workbookParts.macroModuleCount > 0) {
|
|
315
|
+
return `macro modules: ${args.workbookParts.macroModuleCount.toString()}`;
|
|
316
|
+
}
|
|
317
|
+
if (args.workbookParts.pivotTableCount > 0) {
|
|
318
|
+
return `pivot tables: ${args.workbookParts.pivotTableCount.toString()}`;
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
function inspectWorkbookFormulaCaches(workbook, inspectLimit) {
|
|
323
|
+
const formulas = workbook.sheets.flatMap((sheet) => sheet.cells.flatMap((cell) => cell.formula && cell.formula.trim().length > 0
|
|
324
|
+
? [
|
|
325
|
+
{
|
|
326
|
+
target: `${sheet.name}!${cell.address}`,
|
|
327
|
+
formula: cell.formula.startsWith('=') ? cell.formula : `=${cell.formula}`,
|
|
328
|
+
hasCachedValue: cell.hasValue,
|
|
329
|
+
},
|
|
330
|
+
]
|
|
331
|
+
: []));
|
|
332
|
+
const normalizedLimit = normalizeInspectLimit(inspectLimit);
|
|
333
|
+
const inspected = normalizedLimit === 'all' ? formulas : formulas.slice(0, normalizedLimit);
|
|
334
|
+
const missingCache = inspected.filter((entry) => !entry.hasCachedValue).length;
|
|
335
|
+
const unsupportedRecalculation = inspected.filter((entry) => knownUnsupportedFunctionNamesFromFormula(entry.formula).length > 0).length;
|
|
336
|
+
const staleCachedFormulaCount = inspected.filter((entry) => entry.hasCachedValue && volatileFunctionNamesFromFormula(entry.formula).length > 0).length;
|
|
337
|
+
const volatileFormulaCount = inspected.filter((entry) => volatileFunctionNamesFromFormula(entry.formula).length > 0).length;
|
|
338
|
+
const warnings = [
|
|
339
|
+
...(volatileFormulaCount > 0
|
|
340
|
+
? ['Volatile formulas were detected; cached formula values may depend on workbook calculation time.']
|
|
341
|
+
: []),
|
|
342
|
+
...(unsupportedRecalculation > 0
|
|
343
|
+
? ['Unsupported formula families were detected; use xlsx-cache-doctor or an oracle harness before trusting cached values.']
|
|
344
|
+
: []),
|
|
345
|
+
].toSorted();
|
|
346
|
+
return {
|
|
347
|
+
formulaCellCount: formulas.length,
|
|
348
|
+
inspectedFormulaCellCount: inspected.length,
|
|
349
|
+
uninspectedFormulaCellCount: formulas.length - inspected.length,
|
|
350
|
+
inspectionLimit: normalizedLimit,
|
|
351
|
+
suggestedReads: inspected.map((entry) => entry.target),
|
|
352
|
+
staleCachedFormulaCount,
|
|
353
|
+
cacheStatusSummary: {
|
|
354
|
+
missingCache,
|
|
355
|
+
unsupportedRecalculation,
|
|
356
|
+
},
|
|
357
|
+
warnings,
|
|
358
|
+
formulas: inspected,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function inspectWorkbookFormulaCacheScan(scan, inspectLimit) {
|
|
362
|
+
const normalizedLimit = normalizeInspectLimit(inspectLimit);
|
|
363
|
+
const inspected = scan.cells.map((cell) => ({
|
|
364
|
+
target: cell.target,
|
|
365
|
+
formula: cell.formula,
|
|
366
|
+
hasCachedValue: cell.cachedValue !== undefined,
|
|
367
|
+
}));
|
|
368
|
+
const missingCache = inspected.filter((entry) => !entry.hasCachedValue).length;
|
|
369
|
+
const unsupportedRecalculation = inspected.filter((entry) => knownUnsupportedFunctionNamesFromFormula(entry.formula).length > 0).length;
|
|
370
|
+
const staleCachedFormulaCount = inspected.filter((entry) => entry.hasCachedValue && volatileFunctionNamesFromFormula(entry.formula).length > 0).length;
|
|
371
|
+
const volatileFormulaCount = inspected.filter((entry) => volatileFunctionNamesFromFormula(entry.formula).length > 0).length;
|
|
372
|
+
const warnings = [
|
|
373
|
+
...(volatileFormulaCount > 0
|
|
374
|
+
? ['Volatile formulas were detected; cached formula values may depend on workbook calculation time.']
|
|
375
|
+
: []),
|
|
376
|
+
...(unsupportedRecalculation > 0
|
|
377
|
+
? ['Unsupported formula families were detected; use xlsx-cache-doctor or an oracle harness before trusting cached values.']
|
|
378
|
+
: []),
|
|
379
|
+
].toSorted();
|
|
380
|
+
return {
|
|
381
|
+
formulaCellCount: scan.formulaCellCount,
|
|
382
|
+
inspectedFormulaCellCount: inspected.length,
|
|
383
|
+
uninspectedFormulaCellCount: scan.formulaCellCount - inspected.length,
|
|
384
|
+
inspectionLimit: normalizedLimit,
|
|
385
|
+
suggestedReads: inspected.map((entry) => entry.target),
|
|
386
|
+
staleCachedFormulaCount,
|
|
387
|
+
cacheStatusSummary: {
|
|
388
|
+
missingCache,
|
|
389
|
+
unsupportedRecalculation,
|
|
390
|
+
},
|
|
391
|
+
warnings,
|
|
392
|
+
formulas: inspected,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function normalizeInspectLimit(limit) {
|
|
396
|
+
if (limit === 'all') {
|
|
397
|
+
return limit;
|
|
398
|
+
}
|
|
399
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
400
|
+
return defaultInspectLimit;
|
|
401
|
+
}
|
|
402
|
+
return limit;
|
|
403
|
+
}
|
|
404
|
+
function buildRisk(report) {
|
|
405
|
+
const highReasons = [];
|
|
406
|
+
const mediumReasons = [];
|
|
407
|
+
if (report.findings.unsupportedFunctions.length > 0) {
|
|
408
|
+
highReasons.push(`unsupported functions: ${formatNamedCounts(report.findings.unsupportedFunctions)}`);
|
|
409
|
+
}
|
|
410
|
+
if (report.findings.externalLinks.count > 0) {
|
|
411
|
+
highReasons.push(`external workbook links: ${report.findings.externalLinks.count.toString()}`);
|
|
412
|
+
}
|
|
413
|
+
if (report.findings.macroModules.count > 0) {
|
|
414
|
+
highReasons.push(`VBA macro payloads preserved but not executed: ${report.findings.macroModules.count.toString()}`);
|
|
415
|
+
}
|
|
416
|
+
if (report.findings.pivotTables.unsupportedCount > 0) {
|
|
417
|
+
highReasons.push(`unsupported pivot tables: ${report.findings.pivotTables.unsupportedCount.toString()}`);
|
|
418
|
+
}
|
|
419
|
+
if (report.findings.pivotTables.count > 0) {
|
|
420
|
+
mediumReasons.push(`pivot tables require review: ${report.findings.pivotTables.count.toString()}`);
|
|
421
|
+
}
|
|
422
|
+
if (report.findings.volatileFunctions.length > 0) {
|
|
423
|
+
mediumReasons.push(`volatile functions: ${formatNamedCounts(report.findings.volatileFunctions)}`);
|
|
424
|
+
}
|
|
425
|
+
if (report.findings.staleCachedFormulas.count > 0) {
|
|
426
|
+
mediumReasons.push(`stale cached formulas: ${report.findings.staleCachedFormulas.count.toString()}`);
|
|
427
|
+
}
|
|
428
|
+
if (report.findings.missingCachedFormulaValues.count > 0) {
|
|
429
|
+
mediumReasons.push(`missing cached formula values: ${report.findings.missingCachedFormulaValues.count.toString()}`);
|
|
430
|
+
}
|
|
431
|
+
if (report.findings.unsupportedRecalculations.count > 0) {
|
|
432
|
+
mediumReasons.push(`unsupported recalculation results: ${report.findings.unsupportedRecalculations.count.toString()}`);
|
|
433
|
+
}
|
|
434
|
+
if (report.cacheInspection.uninspectedFormulaCellCount > 0) {
|
|
435
|
+
mediumReasons.push(`uninspected formula cells: ${report.cacheInspection.uninspectedFormulaCellCount.toString()}`);
|
|
436
|
+
}
|
|
437
|
+
if (report.findings.warnings.length > 0) {
|
|
438
|
+
mediumReasons.push(`import warnings: ${report.findings.warnings.length.toString()}`);
|
|
439
|
+
}
|
|
440
|
+
const reasons = highReasons.length > 0 ? highReasons : mediumReasons;
|
|
441
|
+
if (reasons.length === 0) {
|
|
442
|
+
return {
|
|
443
|
+
level: 'low',
|
|
444
|
+
reasons: ['No known workbook compatibility risk signals were detected by this report.'],
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
level: highReasons.length > 0 ? 'high' : 'medium',
|
|
449
|
+
reasons,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function parseCliArgs(args, commandName) {
|
|
453
|
+
const demo = args.includes('--demo');
|
|
454
|
+
const inputPath = demo ? undefined : args[0];
|
|
455
|
+
if (!demo && (!inputPath || inputPath.startsWith('-'))) {
|
|
456
|
+
throw new Error('Expected input XLSX path or --demo');
|
|
457
|
+
}
|
|
458
|
+
const externalWorkbooks = [];
|
|
459
|
+
let inspectLimit;
|
|
460
|
+
let json = false;
|
|
461
|
+
for (let index = demo ? 0 : 1; index < args.length; index += 1) {
|
|
462
|
+
const arg = args[index];
|
|
463
|
+
if (arg === undefined) {
|
|
464
|
+
throw new Error(`Unexpected missing ${commandName} argument`);
|
|
465
|
+
}
|
|
466
|
+
switch (arg) {
|
|
467
|
+
case '--demo':
|
|
468
|
+
break;
|
|
469
|
+
case '--external-workbook':
|
|
470
|
+
externalWorkbooks.push({ path: requireNextArg(args, index, '--external-workbook') });
|
|
471
|
+
index += 1;
|
|
472
|
+
break;
|
|
473
|
+
case '--external-workbook-target':
|
|
474
|
+
externalWorkbooks.push({
|
|
475
|
+
path: requireNextArg(args, index, '--external-workbook-target'),
|
|
476
|
+
target: requireNextArg(args, index + 1, '--external-workbook-target target'),
|
|
477
|
+
});
|
|
478
|
+
index += 2;
|
|
479
|
+
break;
|
|
480
|
+
case '--inspect-limit':
|
|
481
|
+
inspectLimit = parseInspectLimit(requireNextArg(args, index, '--inspect-limit'));
|
|
482
|
+
index += 1;
|
|
483
|
+
break;
|
|
484
|
+
case '--json':
|
|
485
|
+
json = true;
|
|
486
|
+
break;
|
|
487
|
+
default:
|
|
488
|
+
throw new Error(`Unknown ${commandName} option: ${arg}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
mode: demo ? 'demo' : 'file',
|
|
493
|
+
inputPath,
|
|
494
|
+
externalWorkbooks,
|
|
495
|
+
...(inspectLimit === undefined ? {} : { inspectLimit }),
|
|
496
|
+
json,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const emptyExternalWorkbookReferenceBytes = new Uint8Array(0);
|
|
500
|
+
function readExternalWorkbookReferenceInputs(workbooks) {
|
|
501
|
+
return workbooks.map((workbook) => {
|
|
502
|
+
const stats = statSync(workbook.path);
|
|
503
|
+
if (!stats.isFile()) {
|
|
504
|
+
throw new Error(`External workbook is not a file: ${workbook.path}`);
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
bytes: emptyExternalWorkbookReferenceBytes,
|
|
508
|
+
fileName: basename(workbook.path),
|
|
509
|
+
...(workbook.target ? { target: workbook.target } : {}),
|
|
510
|
+
};
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function assertWorkbookCompatibilityReportBytesApiWithinLimit(bytes, apiName) {
|
|
514
|
+
if (bytes.byteLength <= workbookCompatibilityReportBytesApiLimit) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
throw new Error([
|
|
518
|
+
`${apiName} is small-workbook only for byte-buffer XLSX input: source is ${bytes.byteLength} bytes`,
|
|
519
|
+
`limit is ${workbookCompatibilityReportBytesApiLimit} bytes`,
|
|
520
|
+
'Use buildWorkbookCompatibilityReportFromFile() for file-backed streaming-native XLSX inspection.',
|
|
521
|
+
].join('; '));
|
|
522
|
+
}
|
|
523
|
+
function knownUnsupportedFunctionNamesFromFormula(formula) {
|
|
524
|
+
return extractFormulaFunctionNames(formula).filter(isKnownUnsupportedRiskFunction);
|
|
525
|
+
}
|
|
526
|
+
function volatileFunctionNamesFromFormula(formula) {
|
|
527
|
+
const volatileNames = new Set(volatileFunctionNames);
|
|
528
|
+
return extractFormulaFunctionNames(formula).filter((name) => volatileNames.has(name));
|
|
529
|
+
}
|
|
530
|
+
function extractFormulaFunctionNames(formula) {
|
|
531
|
+
const stripped = formulaWithoutStringLiterals(formula);
|
|
532
|
+
const names = new Set();
|
|
533
|
+
for (const match of stripped.matchAll(/(?:^|[^A-Za-z0-9_.])([_A-Za-z][A-Za-z0-9_.]*)\s*\(/gu)) {
|
|
534
|
+
const rawName = match[1];
|
|
535
|
+
if (rawName) {
|
|
536
|
+
names.add(normalizeFormulaFunctionName(rawName));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return [...names].toSorted();
|
|
540
|
+
}
|
|
541
|
+
function formulaWithoutStringLiterals(formula) {
|
|
542
|
+
let stripped = '';
|
|
543
|
+
let index = 0;
|
|
544
|
+
while (index < formula.length) {
|
|
545
|
+
if (formula[index] !== '"') {
|
|
546
|
+
stripped += formula[index];
|
|
547
|
+
index += 1;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
stripped += ' ';
|
|
551
|
+
index += 1;
|
|
552
|
+
while (index < formula.length) {
|
|
553
|
+
stripped += ' ';
|
|
554
|
+
if (formula[index] === '"' && formula[index + 1] === '"') {
|
|
555
|
+
index += 2;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (formula[index] === '"') {
|
|
559
|
+
index += 1;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
index += 1;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return stripped;
|
|
566
|
+
}
|
|
567
|
+
function normalizeFormulaFunctionName(name) {
|
|
568
|
+
return name
|
|
569
|
+
.replace(/^_xlfn\./iu, '')
|
|
570
|
+
.replace(/^_xludf\./iu, '')
|
|
571
|
+
.toUpperCase();
|
|
572
|
+
}
|
|
573
|
+
function isKnownUnsupportedRiskFunction(name) {
|
|
574
|
+
return knownUnsupportedRiskFunctions.has(name) || name.startsWith('_XLD') || name.startsWith('_XLUDF') || name.startsWith('_XLL');
|
|
575
|
+
}
|
|
576
|
+
function countNamedValues(values) {
|
|
577
|
+
const counts = new Map();
|
|
578
|
+
for (const value of values) {
|
|
579
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
580
|
+
}
|
|
581
|
+
return [...counts.entries()]
|
|
582
|
+
.map(([name, count]) => ({ name, count }))
|
|
583
|
+
.toSorted((left, right) => right.count - left.count || left.name.localeCompare(right.name));
|
|
584
|
+
}
|
|
585
|
+
function formatNamedCounts(counts) {
|
|
586
|
+
return counts.map((entry) => `${entry.name} (${entry.count.toString()})`).join(', ');
|
|
587
|
+
}
|
|
588
|
+
function renderHumanReport(report) {
|
|
589
|
+
return [
|
|
590
|
+
`Workbook analyzed. Risk level: ${report.risk.level.toUpperCase()}`,
|
|
591
|
+
'Findings:',
|
|
592
|
+
`- Unsupported functions: ${report.findings.unsupportedFunctions.length > 0 ? formatNamedCounts(report.findings.unsupportedFunctions) : '0'}`,
|
|
593
|
+
`- External links: ${report.findings.externalLinks.count.toString()}`,
|
|
594
|
+
`- Macro modules: ${report.findings.macroModules.count.toString()}`,
|
|
595
|
+
`- Pivot tables: ${report.findings.pivotTables.count.toString()}`,
|
|
596
|
+
`- Volatile functions: ${report.findings.volatileFunctions.length > 0 ? formatNamedCounts(report.findings.volatileFunctions) : '0'}`,
|
|
597
|
+
`- Formula cells: ${report.workbook.formulaCellCount.toString()}`,
|
|
598
|
+
`- Stale cached formulas: ${report.findings.staleCachedFormulas.count.toString()}`,
|
|
599
|
+
`- Missing cached formula values: ${report.findings.missingCachedFormulaValues.count.toString()}`,
|
|
600
|
+
'This report identifies workbook features that may require investigation before using Bilig in a service or agent workflow. It is not an Excel compatibility certification.',
|
|
601
|
+
'',
|
|
602
|
+
].join('\n');
|
|
603
|
+
}
|
|
604
|
+
function renderHelp(commandName) {
|
|
605
|
+
return `Usage: ${commandName} <input.xlsx> [options]
|
|
606
|
+
${commandName} --demo [--json]
|
|
607
|
+
|
|
608
|
+
Inspect workbook features that may require investigation before using Bilig in a
|
|
609
|
+
Node service or agent workflow. This is not an Excel compatibility certificate.
|
|
610
|
+
|
|
611
|
+
Options:
|
|
612
|
+
--demo Generate an intentionally risky workbook and report on it.
|
|
613
|
+
--inspect-limit <all|n> Formula cells to recompute during inspection. Defaults to ${defaultFileInspectLimit.toString()} for file-backed reports and ${defaultInspectLimit} for --demo.
|
|
614
|
+
--external-workbook <path>
|
|
615
|
+
Supply a companion XLSX for external-link cache refresh. Repeatable.
|
|
616
|
+
--external-workbook-target <path> <target>
|
|
617
|
+
Supply a companion XLSX for an exact Excel link target. Repeatable.
|
|
618
|
+
--json Print the machine-readable JSON report.
|
|
619
|
+
--help, -h Show this help.
|
|
620
|
+
`;
|
|
621
|
+
}
|
|
622
|
+
function parseInspectLimit(raw) {
|
|
623
|
+
if (raw === 'all') {
|
|
624
|
+
return raw;
|
|
625
|
+
}
|
|
626
|
+
const value = Number(raw);
|
|
627
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
628
|
+
throw new Error(`Expected --inspect-limit to be "all" or a positive integer, received: ${raw}`);
|
|
629
|
+
}
|
|
630
|
+
return value;
|
|
631
|
+
}
|
|
632
|
+
function requireNextArg(args, index, option) {
|
|
633
|
+
const value = args[index + 1];
|
|
634
|
+
if (!value || value.startsWith('--')) {
|
|
635
|
+
throw new Error(`Expected value after ${option}`);
|
|
636
|
+
}
|
|
637
|
+
return value;
|
|
638
|
+
}
|
|
639
|
+
function requireInputPath(options) {
|
|
640
|
+
if (!options.inputPath) {
|
|
641
|
+
throw new Error('Expected input XLSX path');
|
|
642
|
+
}
|
|
643
|
+
return options.inputPath;
|
|
644
|
+
}
|
|
645
|
+
function shellQuote(value) {
|
|
646
|
+
return /^[A-Za-z0-9_./:=@-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
647
|
+
}
|
|
648
|
+
function toUint8Array(input) {
|
|
649
|
+
if (input instanceof Uint8Array) {
|
|
650
|
+
return input;
|
|
651
|
+
}
|
|
652
|
+
return new Uint8Array(input);
|
|
653
|
+
}
|
|
654
|
+
//# sourceMappingURL=workbook-compatibility-report.js.map
|