@arnilo/prism-tools 0.0.0-stage → 0.13.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/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/browser/README.md +98 -0
- package/browser/skills/browser-automation/SKILL.md +14 -0
- package/browser/src/actions/cdp.ts +131 -0
- package/browser/src/actions/dialog.ts +25 -0
- package/browser/src/actions/download_release.ts +22 -0
- package/browser/src/actions/index.ts +37 -0
- package/browser/src/actions/locator.ts +70 -0
- package/browser/src/actions/navigate.ts +23 -0
- package/browser/src/actions/screenshot.ts +27 -0
- package/browser/src/actions/scroll.ts +24 -0
- package/browser/src/actions/select_page.ts +21 -0
- package/browser/src/actions/types.ts +114 -0
- package/browser/src/actions/upload.ts +44 -0
- package/browser/src/actions/wait.ts +45 -0
- package/browser/src/browser.ts +197 -0
- package/browser/src/cdp.ts +178 -0
- package/browser/src/checkpoint.ts +189 -0
- package/browser/src/config.ts +72 -0
- package/browser/src/downloads.ts +253 -0
- package/browser/src/errors.ts +31 -0
- package/browser/src/evaluate.ts +101 -0
- package/browser/src/index.ts +129 -0
- package/browser/src/limits.ts +271 -0
- package/browser/src/manager.ts +730 -0
- package/browser/src/network.ts +263 -0
- package/browser/src/observe.ts +178 -0
- package/browser/src/policy.ts +133 -0
- package/browser/src/screenshot.ts +107 -0
- package/browser/src/shared-sandbox.ts +53 -0
- package/browser/src/snapshot.ts +99 -0
- package/browser/src/targets.ts +165 -0
- package/browser/src/tools.ts +480 -0
- package/browser/src/types.ts +316 -0
- package/browser/src/uploads.ts +174 -0
- package/coding/README.md +337 -0
- package/coding/src/agent/acp-operations.ts +146 -0
- package/coding/src/agent/artifacts.ts +38 -0
- package/coding/src/agent/ask-user-decision.ts +623 -0
- package/coding/src/agent/atomic-write.ts +23 -0
- package/coding/src/agent/bounded-file.ts +21 -0
- package/coding/src/agent/checks.ts +301 -0
- package/coding/src/agent/coding-checkpoint.ts +828 -0
- package/coding/src/agent/delete.ts +237 -0
- package/coding/src/agent/diagnostics-tool.ts +163 -0
- package/coding/src/agent/diagnostics.ts +262 -0
- package/coding/src/agent/edit-diff.ts +382 -0
- package/coding/src/agent/edit.ts +322 -0
- package/coding/src/agent/effects.ts +102 -0
- package/coding/src/agent/env.ts +28 -0
- package/coding/src/agent/execution-policy.ts +89 -0
- package/coding/src/agent/file-mutation-queue.ts +72 -0
- package/coding/src/agent/forge/github.ts +603 -0
- package/coding/src/agent/forge/index.ts +16 -0
- package/coding/src/agent/forge/types.ts +185 -0
- package/coding/src/agent/git-aware-repository.ts +305 -0
- package/coding/src/agent/git-exec.ts +326 -0
- package/coding/src/agent/git-status.ts +177 -0
- package/coding/src/agent/git-tools.ts +360 -0
- package/coding/src/agent/git.ts +1074 -0
- package/coding/src/agent/glob-match.ts +146 -0
- package/coding/src/agent/glob.ts +182 -0
- package/coding/src/agent/goal-verify.ts +385 -0
- package/coding/src/agent/grep-worker.ts +74 -0
- package/coding/src/agent/grep.ts +459 -0
- package/coding/src/agent/identity.ts +48 -0
- package/coding/src/agent/index.ts +676 -0
- package/coding/src/agent/language/client.ts +429 -0
- package/coding/src/agent/language/framing.ts +190 -0
- package/coding/src/agent/language/index.ts +23 -0
- package/coding/src/agent/language/intelligence.ts +682 -0
- package/coding/src/agent/language/types.ts +187 -0
- package/coding/src/agent/lifecycle-events.ts +121 -0
- package/coding/src/agent/lifecycle.ts +166 -0
- package/coding/src/agent/limits.ts +214 -0
- package/coding/src/agent/move.ts +178 -0
- package/coding/src/agent/mutation-path.ts +52 -0
- package/coding/src/agent/output-accumulator.ts +304 -0
- package/coding/src/agent/path-utils.ts +120 -0
- package/coding/src/agent/process/index.ts +58 -0
- package/coding/src/agent/process/recovery.ts +508 -0
- package/coding/src/agent/process/sessions-host.ts +222 -0
- package/coding/src/agent/process/sessions-monitor.ts +141 -0
- package/coding/src/agent/process/sessions-recovery.ts +241 -0
- package/coding/src/agent/process/sessions-spawn.ts +613 -0
- package/coding/src/agent/process/sessions-teardown.ts +281 -0
- package/coding/src/agent/process/sessions.ts +30 -0
- package/coding/src/agent/process/types.ts +358 -0
- package/coding/src/agent/read-path-set.ts +126 -0
- package/coding/src/agent/read.ts +713 -0
- package/coding/src/agent/repository/glob.ts +147 -0
- package/coding/src/agent/repository/indexed-search.ts +480 -0
- package/coding/src/agent/repository/list.ts +131 -0
- package/coding/src/agent/repository/operations.ts +22 -0
- package/coding/src/agent/repository/path.ts +98 -0
- package/coding/src/agent/repository/search.ts +277 -0
- package/coding/src/agent/repository/types.ts +243 -0
- package/coding/src/agent/repository/walk.ts +126 -0
- package/coding/src/agent/repository.ts +12 -0
- package/coding/src/agent/shell-background.ts +264 -0
- package/coding/src/agent/shell-output.ts +75 -0
- package/coding/src/agent/shell.ts +526 -0
- package/coding/src/agent/todo.ts +399 -0
- package/coding/src/agent/truncate.ts +251 -0
- package/coding/src/agent/write.ts +170 -0
- package/coding/src/ask-user-host.ts +48 -0
- package/coding/src/config.ts +278 -0
- package/coding/src/hard-deny.ts +97 -0
- package/coding/src/index.ts +318 -0
- package/coding/src/principal-guard.ts +84 -0
- package/coding/src/sandbox.ts +92 -0
- package/coding/src/security/approval.ts +144 -0
- package/coding/src/security/command-rules.ts +57 -0
- package/coding/src/security/docker-cli.ts +222 -0
- package/coding/src/security/docker-sandbox.ts +1205 -0
- package/coding/src/security/e2b-sandbox.ts +853 -0
- package/coding/src/security/egress/dns-pin.ts +88 -0
- package/coding/src/security/egress/index.ts +32 -0
- package/coding/src/security/egress/limits.ts +57 -0
- package/coding/src/security/egress/policy.ts +137 -0
- package/coding/src/security/egress/proxy.ts +599 -0
- package/coding/src/security/egress/types.ts +30 -0
- package/coding/src/security/index.ts +199 -0
- package/coding/src/security/native-sandbox.ts +639 -0
- package/coding/src/security/path-containment.ts +55 -0
- package/coding/src/security/sandbox-coding-operations.ts +392 -0
- package/coding/src/security/sandbox-fs-operations.ts +644 -0
- package/coding/src/security/sandbox-limits.ts +147 -0
- package/coding/src/security/sandbox-tar.ts +308 -0
- package/coding/src/security/sandbox.ts +179 -0
- package/coding/src/security/semaphore.ts +55 -0
- package/coding/src/targets.ts +110 -0
- package/coding/src/workspace/diff.ts +78 -0
- package/coding/src/workspace/errors.ts +28 -0
- package/coding/src/workspace/index.ts +5 -0
- package/coding/src/workspace/merge.ts +151 -0
- package/coding/src/workspace/routing.ts +76 -0
- package/coding/src/workspace/service.ts +545 -0
- package/computer-use/README.md +105 -0
- package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
- package/computer-use/src/classify.ts +53 -0
- package/computer-use/src/command.ts +29 -0
- package/computer-use/src/config.ts +69 -0
- package/computer-use/src/create.ts +170 -0
- package/computer-use/src/devices.ts +247 -0
- package/computer-use/src/env.ts +28 -0
- package/computer-use/src/index.ts +157 -0
- package/computer-use/src/skill.ts +21 -0
- package/openapi/README.md +97 -0
- package/openapi/src/compile.ts +386 -0
- package/openapi/src/config.ts +73 -0
- package/openapi/src/errors.ts +16 -0
- package/openapi/src/execute.ts +241 -0
- package/openapi/src/index.ts +124 -0
- package/openapi/src/limits.ts +63 -0
- package/openapi/src/specs.ts +86 -0
- package/openapi/src/tools.ts +60 -0
- package/openapi/src/types.ts +61 -0
- package/package.json +171 -4
- package/research/README.md +72 -0
- package/research/skills/research-report/SKILL.md +12 -0
- package/research/skills/research-scoping/SKILL.md +10 -0
- package/research/skills/research-synthesis/SKILL.md +10 -0
- package/research/skills/source-triage/SKILL.md +10 -0
- package/research/src/check.ts +71 -0
- package/research/src/config.ts +41 -0
- package/research/src/index.ts +147 -0
- package/research/src/ledger.ts +157 -0
- package/research/src/urls.ts +21 -0
- package/web/README.md +287 -0
- package/web/skills/web-research/SKILL.md +17 -0
- package/web/src/brave.ts +66 -0
- package/web/src/cache.ts +151 -0
- package/web/src/config.ts +210 -0
- package/web/src/controls.ts +128 -0
- package/web/src/domains.ts +114 -0
- package/web/src/exa.ts +60 -0
- package/web/src/firecrawl.ts +160 -0
- package/web/src/index.ts +387 -0
- package/web/src/kagi.ts +73 -0
- package/web/src/limits.ts +55 -0
- package/web/src/native/convert.ts +84 -0
- package/web/src/native/fetch.ts +226 -0
- package/web/src/normalize.ts +131 -0
- package/web/src/obscura/cdp.ts +201 -0
- package/web/src/obscura/classify.ts +35 -0
- package/web/src/obscura/cli.ts +254 -0
- package/web/src/obscura/errors.ts +8 -0
- package/web/src/obscura/index.ts +59 -0
- package/web/src/obscura/limits.ts +92 -0
- package/web/src/obscura/mcp.ts +104 -0
- package/web/src/obscura/playwright.ts +133 -0
- package/web/src/obscura/process.ts +235 -0
- package/web/src/obscura/search-profile.ts +73 -0
- package/web/src/obscura/types.ts +48 -0
- package/web/src/obscura/web.ts +419 -0
- package/web/src/prompt-fetch.ts +88 -0
- package/web/src/render.ts +75 -0
- package/web/src/searxng.ts +78 -0
- package/web/src/slice.ts +139 -0
- package/web/src/tavily.ts +77 -0
- package/web/src/tools.ts +182 -0
- package/web/src/transport.ts +310 -0
- package/web/src/types.ts +199 -0
- package/work/README.md +77 -0
- package/work/THIRD_PARTY_NOTICES +24 -0
- package/work/docling/ocr.py +150 -0
- package/work/sandbox/Dockerfile +36 -0
- package/work/sandbox/soffice.sh +12 -0
- package/work/src/command.ts +29 -0
- package/work/src/config.ts +138 -0
- package/work/src/connectors/cli.ts +343 -0
- package/work/src/connectors/drafts.ts +647 -0
- package/work/src/connectors/errors.ts +8 -0
- package/work/src/connectors/file-bytes.ts +149 -0
- package/work/src/connectors/google-workspace-http.ts +399 -0
- package/work/src/connectors/google-workspace.ts +465 -0
- package/work/src/connectors/gws-tools.ts +378 -0
- package/work/src/connectors/http.ts +123 -0
- package/work/src/connectors/idempotency.ts +235 -0
- package/work/src/connectors/identity.ts +34 -0
- package/work/src/connectors/index.ts +93 -0
- package/work/src/connectors/limits.ts +50 -0
- package/work/src/connectors/m365-tools.ts +308 -0
- package/work/src/connectors/microsoft365-http.ts +428 -0
- package/work/src/connectors/microsoft365.ts +402 -0
- package/work/src/connectors/normalize.ts +120 -0
- package/work/src/connectors/tool-helpers.ts +398 -0
- package/work/src/connectors/tools.ts +42 -0
- package/work/src/connectors/types.ts +399 -0
- package/work/src/diagrams/canonicalize.ts +166 -0
- package/work/src/diagrams/caps.ts +52 -0
- package/work/src/diagrams/embed.ts +340 -0
- package/work/src/diagrams/errors.ts +76 -0
- package/work/src/diagrams/index.ts +81 -0
- package/work/src/diagrams/messages.ts +150 -0
- package/work/src/diagrams/origin.ts +52 -0
- package/work/src/diagrams/telemetry.ts +34 -0
- package/work/src/diagrams/xml.ts +267 -0
- package/work/src/document-extraction/index.ts +563 -0
- package/work/src/document-reader/ambient.d.ts +2 -0
- package/work/src/document-reader/errors.ts +7 -0
- package/work/src/document-reader/index.ts +181 -0
- package/work/src/document-reader/mistral-ocr.ts +318 -0
- package/work/src/document-reader/parsers.ts +171 -0
- package/work/src/documents/caps.ts +135 -0
- package/work/src/documents/diff.ts +269 -0
- package/work/src/documents/errors.ts +44 -0
- package/work/src/documents/fidelity.ts +98 -0
- package/work/src/documents/generate.ts +96 -0
- package/work/src/documents/hash.ts +8 -0
- package/work/src/documents/index.ts +14 -0
- package/work/src/documents/model-schema.ts +639 -0
- package/work/src/documents/parse.ts +252 -0
- package/work/src/documents/patch-history.ts +81 -0
- package/work/src/documents/patch.ts +476 -0
- package/work/src/documents/preview-html.ts +245 -0
- package/work/src/documents/preview.ts +227 -0
- package/work/src/documents/telemetry.ts +36 -0
- package/work/src/documents/translate/docx.ts +292 -0
- package/work/src/documents/translate/pptx.ts +304 -0
- package/work/src/documents/translate/xlsx.ts +137 -0
- package/work/src/documents/types.ts +188 -0
- package/work/src/index.ts +311 -0
- package/work/src/runtime/worker-pool-entry.ts +102 -0
- package/work/src/runtime/worker-pool.ts +267 -0
- package/work/src/sandbox/composition.ts +212 -0
- package/work/src/sandbox/image.ts +6 -0
- package/work/src/sandbox/index.ts +14 -0
- package/work/src/sheets/caps.ts +68 -0
- package/work/src/sheets/csv.ts +351 -0
- package/work/src/sheets/decimal.ts +388 -0
- package/work/src/sheets/errors.ts +52 -0
- package/work/src/sheets/index.ts +62 -0
- package/work/src/sheets/inference.ts +378 -0
- package/work/src/sheets/telemetry.ts +34 -0
- package/work/src/sheets/types.ts +162 -0
- package/work/src/sheets/xlsx.ts +388 -0
- package/work/src/skills/index.ts +8 -0
- package/work/src/skills/load.ts +43 -0
- package/work/src/tools/filesystem.ts +21 -0
- package/work/src/tools/index.ts +1 -0
- package/work/src/tools/local-filesystem.ts +33 -0
- package/work/src/tools/office.ts +311 -0
- package/work/vendor/hermes-agent/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
- package/work/vendor-lock.json +11 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { DocumentsCapError } from "./errors.js";
|
|
2
|
+
import type { DeckModel, DocModel, DocumentModel, SheetModel } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// --- Default and Hard Ceiling Constants ---
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_MAX_DOCUMENT_BYTES = 32 * 1024 * 1024; // 32 MiB
|
|
7
|
+
export const HARD_MAX_DOCUMENT_BYTES = 512 * 1024 * 1024; // 512 MiB
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_MAX_BLOCKS = 10_000;
|
|
10
|
+
export const HARD_MAX_BLOCKS = 50_000;
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_MAX_CELLS = 500_000;
|
|
13
|
+
export const HARD_MAX_CELLS = 2_000_000;
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_MAX_SLIDES = 500;
|
|
16
|
+
export const HARD_MAX_SLIDES = 2_000;
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_MAX_SHEETS = 100;
|
|
19
|
+
export const HARD_MAX_SHEETS = 1_000;
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_MAX_IMAGES = 100;
|
|
22
|
+
export const HARD_MAX_IMAGES = 1_000;
|
|
23
|
+
|
|
24
|
+
export interface DocumentCaps {
|
|
25
|
+
/** Maximum generated or parsed file size in bytes (default 32 MiB, hard 512 MiB). */
|
|
26
|
+
readonly maxBytes?: number;
|
|
27
|
+
/** Maximum total block elements in a doc model (default 10,000, hard 50,000). */
|
|
28
|
+
readonly maxBlocks?: number;
|
|
29
|
+
/** Maximum total table or worksheet cells (default 500,000, hard 2,000,000). */
|
|
30
|
+
readonly maxCells?: number;
|
|
31
|
+
/** Maximum presentation slides (default 500, hard 2,000). */
|
|
32
|
+
readonly maxSlides?: number;
|
|
33
|
+
/** Maximum workbook sheets (default 100, hard 1,000). */
|
|
34
|
+
readonly maxSheets?: number;
|
|
35
|
+
/** Maximum embedded images (default 100, hard 1,000). */
|
|
36
|
+
readonly maxImages?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ResolvedDocumentCaps {
|
|
40
|
+
readonly maxBytes: number;
|
|
41
|
+
readonly maxBlocks: number;
|
|
42
|
+
readonly maxCells: number;
|
|
43
|
+
readonly maxSlides: number;
|
|
44
|
+
readonly maxSheets: number;
|
|
45
|
+
readonly maxImages: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveCap(name: string, value: number | undefined, defaultVal: number, hardCap: number): number {
|
|
49
|
+
if (value === undefined) return defaultVal;
|
|
50
|
+
if (!Number.isInteger(value) || value <= 0 || value > hardCap) {
|
|
51
|
+
throw new RangeError(`documents cap ${name} must be an integer in (0, ${hardCap}], got ${value}`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolves user-supplied caps against safe defaults and hard ceilings.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveDocumentCaps(caps?: DocumentCaps): ResolvedDocumentCaps {
|
|
60
|
+
return {
|
|
61
|
+
maxBytes: resolveCap("maxBytes", caps?.maxBytes, DEFAULT_MAX_DOCUMENT_BYTES, HARD_MAX_DOCUMENT_BYTES),
|
|
62
|
+
maxBlocks: resolveCap("maxBlocks", caps?.maxBlocks, DEFAULT_MAX_BLOCKS, HARD_MAX_BLOCKS),
|
|
63
|
+
maxCells: resolveCap("maxCells", caps?.maxCells, DEFAULT_MAX_CELLS, HARD_MAX_CELLS),
|
|
64
|
+
maxSlides: resolveCap("maxSlides", caps?.maxSlides, DEFAULT_MAX_SLIDES, HARD_MAX_SLIDES),
|
|
65
|
+
maxSheets: resolveCap("maxSheets", caps?.maxSheets, DEFAULT_MAX_SHEETS, HARD_MAX_SHEETS),
|
|
66
|
+
maxImages: resolveCap("maxImages", caps?.maxImages, DEFAULT_MAX_IMAGES, HARD_MAX_IMAGES),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Enforces element and structural caps on a DocumentModel before processing.
|
|
72
|
+
* Throws {@link DocumentsCapError} if any cap is exceeded.
|
|
73
|
+
*/
|
|
74
|
+
export function validateModelCaps(model: DocumentModel, caps: ResolvedDocumentCaps): void {
|
|
75
|
+
if (model.kind === "doc") {
|
|
76
|
+
const doc = model as DocModel;
|
|
77
|
+
if (doc.blocks.length > caps.maxBlocks) {
|
|
78
|
+
throw new DocumentsCapError(`document contains ${doc.blocks.length} blocks, exceeding maxBlocks cap (${caps.maxBlocks})`);
|
|
79
|
+
}
|
|
80
|
+
let cellCount = 0;
|
|
81
|
+
let imageCount = 0;
|
|
82
|
+
for (const block of doc.blocks) {
|
|
83
|
+
if (block.type === "table") {
|
|
84
|
+
for (const row of block.cells) {
|
|
85
|
+
cellCount += row.length;
|
|
86
|
+
}
|
|
87
|
+
if (cellCount > caps.maxCells) {
|
|
88
|
+
throw new DocumentsCapError(`document tables contain > ${caps.maxCells} cells, exceeding maxCells cap`);
|
|
89
|
+
}
|
|
90
|
+
} else if (block.type === "image") {
|
|
91
|
+
imageCount += 1;
|
|
92
|
+
if (imageCount > caps.maxImages) {
|
|
93
|
+
throw new DocumentsCapError(`document contains ${imageCount} images, exceeding maxImages cap (${caps.maxImages})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else if (model.kind === "sheet") {
|
|
98
|
+
const sheetModel = model as SheetModel;
|
|
99
|
+
if (sheetModel.sheets.length > caps.maxSheets) {
|
|
100
|
+
throw new DocumentsCapError(`workbook contains ${sheetModel.sheets.length} sheets, exceeding maxSheets cap (${caps.maxSheets})`);
|
|
101
|
+
}
|
|
102
|
+
let cellCount = 0;
|
|
103
|
+
for (const sheet of sheetModel.sheets) {
|
|
104
|
+
for (const row of sheet.cells) {
|
|
105
|
+
cellCount += row.length;
|
|
106
|
+
if (cellCount > caps.maxCells) {
|
|
107
|
+
throw new DocumentsCapError(`sheet cells exceed maxCells cap (${caps.maxCells})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} else if (model.kind === "deck") {
|
|
112
|
+
const deck = model as DeckModel;
|
|
113
|
+
if (deck.slides.length > caps.maxSlides) {
|
|
114
|
+
throw new DocumentsCapError(`presentation contains ${deck.slides.length} slides, exceeding maxSlides cap (${caps.maxSlides})`);
|
|
115
|
+
}
|
|
116
|
+
let imageCount = 0;
|
|
117
|
+
for (const slide of deck.slides) {
|
|
118
|
+
if (slide.image) {
|
|
119
|
+
imageCount += 1;
|
|
120
|
+
if (imageCount > caps.maxImages) {
|
|
121
|
+
throw new DocumentsCapError(`presentation contains ${imageCount} images, exceeding maxImages cap (${caps.maxImages})`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Enforces byte size ceiling. Throws {@link DocumentsCapError} if exceeded.
|
|
130
|
+
*/
|
|
131
|
+
export function validateByteCap(byteLength: number, caps: ResolvedDocumentCaps): void {
|
|
132
|
+
if (byteLength > caps.maxBytes) {
|
|
133
|
+
throw new DocumentsCapError(`document size ${byteLength} bytes exceeds maxBytes cap (${caps.maxBytes})`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { DocumentsCapError, DocumentsValidationError } from "./errors.js";
|
|
2
|
+
import { sha256Hex } from "./hash.js";
|
|
3
|
+
import { validateDocumentModel } from "./model-schema.js";
|
|
4
|
+
import type { CellValue, DeckModel, DocBlock, DocModel, DocumentKind, DocumentModel, ImageBlock, SheetModel, SlideData } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_MAX_DIFF_OPS = 4096;
|
|
7
|
+
export const HARD_MAX_DIFF_OPS = 16_384;
|
|
8
|
+
export const DEFAULT_MAX_DIFF_NODES = 8192;
|
|
9
|
+
export const HARD_MAX_DIFF_NODES = 32_768;
|
|
10
|
+
|
|
11
|
+
export type DocumentDiffOp = "add" | "remove" | "replace";
|
|
12
|
+
|
|
13
|
+
export interface DocumentDiffChange {
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly op: DocumentDiffOp;
|
|
16
|
+
readonly from?: unknown;
|
|
17
|
+
readonly to?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DocumentDiff {
|
|
21
|
+
readonly kind: DocumentKind;
|
|
22
|
+
readonly changes: readonly DocumentDiffChange[];
|
|
23
|
+
readonly truncated: boolean;
|
|
24
|
+
readonly operations: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DiffDocumentOptions {
|
|
28
|
+
readonly maxOps?: number;
|
|
29
|
+
readonly maxNodes?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function bound(name: string, value: number | undefined, fallback: number, hard: number): number {
|
|
33
|
+
if (value === undefined) return fallback;
|
|
34
|
+
if (!Number.isInteger(value) || value < 1 || value > hard) {
|
|
35
|
+
throw new DocumentsCapError(`${name} must be an integer in 1..${hard}`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function clip(value: unknown): unknown {
|
|
41
|
+
if (typeof value === "string" && value.length > 256) return `${value.slice(0, 256)}…`;
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cellKey(value: CellValue): string {
|
|
46
|
+
if (value === null) return "null";
|
|
47
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
48
|
+
if ("type" in value) return `${value.type}:${value.value}`;
|
|
49
|
+
if ("formula" in value) return `formula:${value.formula}:${String(value.cachedValue ?? "")}`;
|
|
50
|
+
return JSON.stringify(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function imageKey(image: ImageBlock): string {
|
|
54
|
+
const digest = image.data ? sha256Hex(Buffer.from(image.data)) : "";
|
|
55
|
+
return `${image.ref ?? ""}:${image.mimeType ?? ""}:${image.alt ?? ""}:${digest}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function blockKey(block: DocBlock): string {
|
|
59
|
+
switch (block.type) {
|
|
60
|
+
case "heading":
|
|
61
|
+
return `h${block.level}:${block.text}`;
|
|
62
|
+
case "paragraph":
|
|
63
|
+
return block.text ?? (block.runs ?? []).map((run) => run.text).join("");
|
|
64
|
+
case "list":
|
|
65
|
+
return JSON.stringify({ ordered: block.ordered ?? false, items: block.items });
|
|
66
|
+
case "page-break":
|
|
67
|
+
return "page-break";
|
|
68
|
+
case "image":
|
|
69
|
+
return imageKey(block);
|
|
70
|
+
case "chart":
|
|
71
|
+
return JSON.stringify({ chartType: block.chartType, title: block.title, data: block.data });
|
|
72
|
+
case "table":
|
|
73
|
+
return `table:${block.rows}x${block.columns}`;
|
|
74
|
+
default:
|
|
75
|
+
return JSON.stringify(block);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function slideKey(slide: SlideData): string {
|
|
80
|
+
return JSON.stringify({
|
|
81
|
+
layout: slide.layout,
|
|
82
|
+
title: slide.title,
|
|
83
|
+
subtitle: slide.subtitle,
|
|
84
|
+
bullets: slide.bullets,
|
|
85
|
+
notes: slide.notes,
|
|
86
|
+
image: slide.image ? imageKey(slide.image) : undefined,
|
|
87
|
+
chart: slide.chart,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface Sink {
|
|
92
|
+
visit(): boolean;
|
|
93
|
+
emit(change: DocumentDiffChange): boolean;
|
|
94
|
+
truncated: boolean;
|
|
95
|
+
operations: number;
|
|
96
|
+
changes: DocumentDiffChange[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createSink(maxOps: number, maxNodes: number): Sink {
|
|
100
|
+
const changes: DocumentDiffChange[] = [];
|
|
101
|
+
let operations = 0;
|
|
102
|
+
let nodes = 0;
|
|
103
|
+
let truncated = false;
|
|
104
|
+
return {
|
|
105
|
+
get truncated() {
|
|
106
|
+
return truncated;
|
|
107
|
+
},
|
|
108
|
+
get operations() {
|
|
109
|
+
return operations;
|
|
110
|
+
},
|
|
111
|
+
get changes() {
|
|
112
|
+
return changes;
|
|
113
|
+
},
|
|
114
|
+
visit() {
|
|
115
|
+
if (truncated) return false;
|
|
116
|
+
if (nodes >= maxNodes) {
|
|
117
|
+
truncated = true;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
nodes += 1;
|
|
121
|
+
return true;
|
|
122
|
+
},
|
|
123
|
+
emit(change) {
|
|
124
|
+
if (truncated) return false;
|
|
125
|
+
if (operations >= maxOps) {
|
|
126
|
+
truncated = true;
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
operations += 1;
|
|
130
|
+
changes.push({
|
|
131
|
+
path: change.path,
|
|
132
|
+
op: change.op,
|
|
133
|
+
...(change.from === undefined ? {} : { from: clip(change.from) }),
|
|
134
|
+
...(change.to === undefined ? {} : { to: clip(change.to) }),
|
|
135
|
+
});
|
|
136
|
+
return true;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function diffScalar(sink: Sink, path: string, from: unknown, to: unknown): boolean {
|
|
142
|
+
if (!sink.visit()) return false;
|
|
143
|
+
if (from === to) return true;
|
|
144
|
+
if (from === undefined) return sink.emit({ path, op: "add", to });
|
|
145
|
+
if (to === undefined) return sink.emit({ path, op: "remove", from });
|
|
146
|
+
return sink.emit({ path, op: "replace", from, to });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function diffCells(sink: Sink, path: string, from: readonly (readonly CellValue[])[], to: readonly (readonly CellValue[])[]): boolean {
|
|
150
|
+
const rows = Math.max(from.length, to.length);
|
|
151
|
+
for (let r = 0; r < rows; r += 1) {
|
|
152
|
+
const leftRow = from[r];
|
|
153
|
+
const rightRow = to[r];
|
|
154
|
+
if (!sink.visit()) return false;
|
|
155
|
+
if (leftRow === undefined) {
|
|
156
|
+
if (!sink.emit({ path: `${path}/${r}`, op: "add", to: rightRow?.map(cellKey) })) return false;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (rightRow === undefined) {
|
|
160
|
+
if (!sink.emit({ path: `${path}/${r}`, op: "remove", from: leftRow.map(cellKey) })) return false;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const cols = Math.max(leftRow.length, rightRow.length);
|
|
164
|
+
for (let c = 0; c < cols; c += 1) {
|
|
165
|
+
const left = leftRow[c];
|
|
166
|
+
const right = rightRow[c];
|
|
167
|
+
if (left === undefined) {
|
|
168
|
+
if (!diffScalar(sink, `${path}/${r}/${c}`, undefined, right === undefined ? undefined : cellKey(right))) return false;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (right === undefined) {
|
|
172
|
+
if (!diffScalar(sink, `${path}/${r}/${c}`, cellKey(left), undefined)) return false;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!diffScalar(sink, `${path}/${r}/${c}`, cellKey(left), cellKey(right))) return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function diffDoc(sink: Sink, from: DocModel, to: DocModel): boolean {
|
|
182
|
+
if (!diffScalar(sink, "title", from.title, to.title)) return false;
|
|
183
|
+
const n = Math.max(from.blocks.length, to.blocks.length);
|
|
184
|
+
for (let i = 0; i < n; i += 1) {
|
|
185
|
+
const left = from.blocks[i];
|
|
186
|
+
const right = to.blocks[i];
|
|
187
|
+
if (!sink.visit()) return false;
|
|
188
|
+
if (left === undefined) {
|
|
189
|
+
if (!sink.emit({ path: `blocks/${i}`, op: "add", to: right ? blockKey(right) : undefined })) return false;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (right === undefined) {
|
|
193
|
+
if (!sink.emit({ path: `blocks/${i}`, op: "remove", from: blockKey(left) })) return false;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (left.type !== right.type) {
|
|
197
|
+
if (!sink.emit({ path: `blocks/${i}`, op: "replace", from: blockKey(left), to: blockKey(right) })) return false;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (left.type === "table" && right.type === "table") {
|
|
201
|
+
if (!diffCells(sink, `blocks/${i}/cells`, left.cells, right.cells)) return false;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (!diffScalar(sink, `blocks/${i}`, blockKey(left), blockKey(right))) return false;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function diffSheet(sink: Sink, from: SheetModel, to: SheetModel): boolean {
|
|
210
|
+
if (!diffScalar(sink, "title", from.title, to.title)) return false;
|
|
211
|
+
const n = Math.max(from.sheets.length, to.sheets.length);
|
|
212
|
+
for (let i = 0; i < n; i += 1) {
|
|
213
|
+
const left = from.sheets[i];
|
|
214
|
+
const right = to.sheets[i];
|
|
215
|
+
if (!sink.visit()) return false;
|
|
216
|
+
if (left === undefined) {
|
|
217
|
+
if (!sink.emit({ path: `sheets/${i}`, op: "add", to: right?.name })) return false;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (right === undefined) {
|
|
221
|
+
if (!sink.emit({ path: `sheets/${i}`, op: "remove", from: left.name })) return false;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (!diffScalar(sink, `sheets/${i}/name`, left.name, right.name)) return false;
|
|
225
|
+
if (!diffCells(sink, `sheets/${i}/cells`, left.cells, right.cells)) return false;
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function diffDeck(sink: Sink, from: DeckModel, to: DeckModel): boolean {
|
|
231
|
+
if (!diffScalar(sink, "title", from.title, to.title)) return false;
|
|
232
|
+
const n = Math.max(from.slides.length, to.slides.length);
|
|
233
|
+
for (let i = 0; i < n; i += 1) {
|
|
234
|
+
const left = from.slides[i];
|
|
235
|
+
const right = to.slides[i];
|
|
236
|
+
if (!sink.visit()) return false;
|
|
237
|
+
if (left === undefined) {
|
|
238
|
+
if (!sink.emit({ path: `slides/${i}`, op: "add", to: right ? slideKey(right) : undefined })) return false;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (right === undefined) {
|
|
242
|
+
if (!sink.emit({ path: `slides/${i}`, op: "remove", from: slideKey(left) })) return false;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (!diffScalar(sink, `slides/${i}`, slideKey(left), slideKey(right))) return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Structural Document Model diff. Caps report `truncated` instead of walking unbounded trees. Decimal cells compare as strings. */
|
|
251
|
+
export function diffDocument(from: DocumentModel, to: DocumentModel, options?: DiffDocumentOptions): DocumentDiff {
|
|
252
|
+
validateDocumentModel(from);
|
|
253
|
+
validateDocumentModel(to);
|
|
254
|
+
if (from.kind !== to.kind) {
|
|
255
|
+
throw new DocumentsValidationError(`cannot diff "${from.kind}" against "${to.kind}"`);
|
|
256
|
+
}
|
|
257
|
+
const maxOps = bound("maxOps", options?.maxOps, DEFAULT_MAX_DIFF_OPS, HARD_MAX_DIFF_OPS);
|
|
258
|
+
const maxNodes = bound("maxNodes", options?.maxNodes, DEFAULT_MAX_DIFF_NODES, HARD_MAX_DIFF_NODES);
|
|
259
|
+
const sink = createSink(maxOps, maxNodes);
|
|
260
|
+
if (from.kind === "doc") diffDoc(sink, from, to as DocModel);
|
|
261
|
+
else if (from.kind === "sheet") diffSheet(sink, from, to as SheetModel);
|
|
262
|
+
else diffDeck(sink, from, to as DeckModel);
|
|
263
|
+
return {
|
|
264
|
+
kind: from.kind,
|
|
265
|
+
changes: Object.freeze(sink.changes),
|
|
266
|
+
truncated: sink.truncated,
|
|
267
|
+
operations: sink.operations,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class DocumentsError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
|
|
4
|
+
constructor(message: string, code = "ERR_PRISM_DOCUMENTS") {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "DocumentsError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class DocumentsCapError extends DocumentsError {
|
|
12
|
+
constructor(message: string) {
|
|
13
|
+
super(message, "ERR_PRISM_DOCUMENTS_CAP");
|
|
14
|
+
this.name = "DocumentsCapError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class DocumentsValidationError extends DocumentsError {
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(message, "ERR_PRISM_DOCUMENTS_INVALID_MODEL");
|
|
21
|
+
this.name = "DocumentsValidationError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class DocumentsFormatError extends DocumentsError {
|
|
26
|
+
constructor(message: string) {
|
|
27
|
+
super(message, "ERR_PRISM_DOCUMENTS_UNSUPPORTED_FORMAT");
|
|
28
|
+
this.name = "DocumentsFormatError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class DocumentsParseError extends DocumentsError {
|
|
33
|
+
constructor(message: string) {
|
|
34
|
+
super(message, "ERR_PRISM_DOCUMENTS_PARSE_FAILED");
|
|
35
|
+
this.name = "DocumentsParseError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class DocumentsPatchError extends DocumentsError {
|
|
40
|
+
constructor(message: string) {
|
|
41
|
+
super(message, "ERR_PRISM_DOCUMENTS_UNSAFE_PATH");
|
|
42
|
+
this.name = "DocumentsPatchError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { DocumentKind } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Cap on reported lost-structure issues (truncated after this). */
|
|
4
|
+
export const HARD_FIDELITY_ISSUES = 256;
|
|
5
|
+
const HARD_ZIP_ENTRIES = 8192;
|
|
6
|
+
|
|
7
|
+
export type ImportFidelityLost = "dropped" | "approximated";
|
|
8
|
+
|
|
9
|
+
export interface ImportFidelityIssue {
|
|
10
|
+
readonly code: string;
|
|
11
|
+
readonly part?: string;
|
|
12
|
+
readonly lost: ImportFidelityLost;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ImportFidelityReport {
|
|
16
|
+
readonly kind: DocumentKind;
|
|
17
|
+
readonly issues: readonly ImportFidelityIssue[];
|
|
18
|
+
readonly truncated: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const EOCD = 0x06054b50;
|
|
22
|
+
const CDFH = 0x02014b50;
|
|
23
|
+
|
|
24
|
+
const RULES: ReadonlyArray<{ readonly match: RegExp; readonly code: string; readonly lost: ImportFidelityLost }> = [
|
|
25
|
+
{ match: /(^|\/)vbaProject/i, code: "macros", lost: "dropped" },
|
|
26
|
+
{ match: /EncryptionInfo|EncryptedPackage/i, code: "encrypted", lost: "dropped" },
|
|
27
|
+
{ match: /^word\/comments/i, code: "comments", lost: "dropped" },
|
|
28
|
+
{ match: /^xl\/(comments|threadedComments)/i, code: "comments", lost: "dropped" },
|
|
29
|
+
{ match: /^word\/(header|footer)\d/i, code: "headers-footers", lost: "dropped" },
|
|
30
|
+
{ match: /\/embeddings\//i, code: "embedded-ole", lost: "dropped" },
|
|
31
|
+
{ match: /\/media\//i, code: "media", lost: "dropped" },
|
|
32
|
+
{ match: /\/charts\//i, code: "charts", lost: "dropped" },
|
|
33
|
+
{ match: /\/diagrams\//i, code: "smartart", lost: "dropped" },
|
|
34
|
+
{ match: /^xl\/pivot/i, code: "pivot-tables", lost: "dropped" },
|
|
35
|
+
{ match: /^xl\/externalLinks/i, code: "external-links", lost: "dropped" },
|
|
36
|
+
{ match: /(^|\/)drawings\//i, code: "drawings", lost: "dropped" },
|
|
37
|
+
{ match: /customXml/i, code: "custom-xml", lost: "dropped" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function u16(bytes: Uint8Array, offset: number): number {
|
|
41
|
+
if (offset + 2 > bytes.length) return 0;
|
|
42
|
+
return bytes[offset]! | (bytes[offset + 1]! << 8);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function u32(bytes: Uint8Array, offset: number): number {
|
|
46
|
+
if (offset + 4 > bytes.length) return 0;
|
|
47
|
+
return (bytes[offset]! | (bytes[offset + 1]! << 8) | (bytes[offset + 2]! << 16) | (bytes[offset + 3]! << 24)) >>> 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findEocd(bytes: Uint8Array): number {
|
|
51
|
+
const min = Math.max(0, bytes.length - 22 - 65535);
|
|
52
|
+
for (let i = bytes.length - 22; i >= min; i -= 1) {
|
|
53
|
+
if (u32(bytes, i) === EOCD) return i;
|
|
54
|
+
}
|
|
55
|
+
return -1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Local-file names from the ZIP central directory — no inflate. */
|
|
59
|
+
export function listZipEntryNames(bytes: Uint8Array): { readonly names: readonly string[]; readonly zip64: boolean } {
|
|
60
|
+
const eocd = findEocd(bytes);
|
|
61
|
+
if (eocd < 0) return { names: [], zip64: false };
|
|
62
|
+
const entries = u16(bytes, eocd + 10);
|
|
63
|
+
const cdOffset = u32(bytes, eocd + 16);
|
|
64
|
+
if (cdOffset === 0xffffffff || entries === 0xffff) return { names: [], zip64: true };
|
|
65
|
+
const names: string[] = [];
|
|
66
|
+
let off = cdOffset;
|
|
67
|
+
const n = Math.min(entries, HARD_ZIP_ENTRIES);
|
|
68
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
69
|
+
for (let i = 0; i < n; i += 1) {
|
|
70
|
+
if (off + 46 > bytes.length || u32(bytes, off) !== CDFH) break;
|
|
71
|
+
const nameLen = u16(bytes, off + 28);
|
|
72
|
+
const extraLen = u16(bytes, off + 30);
|
|
73
|
+
const commentLen = u16(bytes, off + 32);
|
|
74
|
+
const nameStart = off + 46;
|
|
75
|
+
const nameEnd = nameStart + nameLen;
|
|
76
|
+
if (nameEnd > bytes.length) break;
|
|
77
|
+
names.push(decoder.decode(bytes.subarray(nameStart, nameEnd)));
|
|
78
|
+
off = nameEnd + extraLen + commentLen;
|
|
79
|
+
}
|
|
80
|
+
return { names, zip64: false };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function reportImportFidelity(bytes: Uint8Array, kind: DocumentKind): ImportFidelityReport {
|
|
84
|
+
const { names, zip64 } = listZipEntryNames(bytes);
|
|
85
|
+
const issues: ImportFidelityIssue[] = [];
|
|
86
|
+
if (zip64) issues.push({ code: "zip64", lost: "dropped" });
|
|
87
|
+
for (const part of names) {
|
|
88
|
+
if (issues.length >= HARD_FIDELITY_ISSUES) break;
|
|
89
|
+
const rule = RULES.find((candidate) => candidate.match.test(part));
|
|
90
|
+
if (!rule) continue;
|
|
91
|
+
issues.push({ code: rule.code, part, lost: rule.lost });
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
kind,
|
|
95
|
+
issues: Object.freeze(issues.slice(0, HARD_FIDELITY_ISSUES)),
|
|
96
|
+
truncated: issues.length > HARD_FIDELITY_ISSUES || names.length >= HARD_ZIP_ENTRIES,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type DocumentCaps, resolveDocumentCaps, validateByteCap, validateModelCaps } from "./caps.js";
|
|
2
|
+
import { DocumentsFormatError } from "./errors.js";
|
|
3
|
+
import { sha256Hex } from "./hash.js";
|
|
4
|
+
import { validateDocumentModel } from "./model-schema.js";
|
|
5
|
+
import type { DocumentsTelemetry } from "./telemetry.js";
|
|
6
|
+
import { generateDocxBytes } from "./translate/docx.js";
|
|
7
|
+
import { generatePptxBytes } from "./translate/pptx.js";
|
|
8
|
+
import { generateXlsxBytes } from "./translate/xlsx.js";
|
|
9
|
+
import type { DeckModel, DocModel, DocumentModel, SheetModel } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export type DocumentFormat = "docx" | "xlsx" | "pptx";
|
|
12
|
+
|
|
13
|
+
export interface GenerateDocumentOptions {
|
|
14
|
+
/** Target OOXML output format. Must match document kind (doc->docx, sheet->xlsx, deck->pptx). */
|
|
15
|
+
readonly format: DocumentFormat;
|
|
16
|
+
/** Optional caps overrides. */
|
|
17
|
+
readonly caps?: DocumentCaps;
|
|
18
|
+
/** Optional telemetry seam hook for recording span metrics without text leaking. */
|
|
19
|
+
readonly telemetry?: DocumentsTelemetry;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GenerateDocumentResult {
|
|
23
|
+
/** In-memory spec-compliant OOXML binary bytes (PK zip container). */
|
|
24
|
+
readonly bytes: Uint8Array;
|
|
25
|
+
/** 64-character lowercase SHA-256 hex digest of the output bytes. */
|
|
26
|
+
readonly contentHash: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generates an in-memory, spec-compliant OOXML document (DOCX, XLSX, or PPTX)
|
|
31
|
+
* from a typed Prism Document Model.
|
|
32
|
+
*
|
|
33
|
+
* Enforces schema validation and element/byte caps before generation.
|
|
34
|
+
* Operates purely in memory with zero filesystem or network I/O.
|
|
35
|
+
*/
|
|
36
|
+
export async function generateDocument(model: DocumentModel, options: GenerateDocumentOptions): Promise<GenerateDocumentResult> {
|
|
37
|
+
const { format, caps: userCaps, telemetry } = options;
|
|
38
|
+
|
|
39
|
+
const span = telemetry?.startSpan("documents.generate", {
|
|
40
|
+
"documents.kind": model?.kind ?? "unknown",
|
|
41
|
+
"documents.format": format,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
if (model.kind === "doc" && format !== "docx") {
|
|
46
|
+
throw new DocumentsFormatError(`unsupported format "${format}" for doc model (expected "docx")`);
|
|
47
|
+
}
|
|
48
|
+
if (model.kind === "sheet" && format !== "xlsx") {
|
|
49
|
+
throw new DocumentsFormatError(`unsupported format "${format}" for sheet model (expected "xlsx")`);
|
|
50
|
+
}
|
|
51
|
+
if (model.kind === "deck" && format !== "pptx") {
|
|
52
|
+
throw new DocumentsFormatError(`unsupported format "${format}" for deck model (expected "pptx")`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 1. Trust boundary: validate model against Draft-07 schema and structural invariants
|
|
56
|
+
validateDocumentModel(model);
|
|
57
|
+
|
|
58
|
+
// 2. Resolve caps and check element counts before paying generation cost
|
|
59
|
+
const caps = resolveDocumentCaps(userCaps);
|
|
60
|
+
validateModelCaps(model, caps);
|
|
61
|
+
|
|
62
|
+
// 3. Translate and generate in-memory OOXML zip bytes
|
|
63
|
+
let bytes: Uint8Array;
|
|
64
|
+
switch (model.kind) {
|
|
65
|
+
case "doc":
|
|
66
|
+
bytes = generateDocxBytes(model as DocModel);
|
|
67
|
+
break;
|
|
68
|
+
case "sheet":
|
|
69
|
+
bytes = generateXlsxBytes(model as SheetModel);
|
|
70
|
+
break;
|
|
71
|
+
case "deck":
|
|
72
|
+
bytes = generatePptxBytes(model as DeckModel);
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
throw new DocumentsFormatError(`unsupported document kind: "${String((model as DocumentModel).kind)}"`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 4. Validate output byte length against maxBytes cap
|
|
79
|
+
validateByteCap(bytes.byteLength, caps);
|
|
80
|
+
|
|
81
|
+
// 5. Compute SHA-256 contentHash over the generated bytes
|
|
82
|
+
const contentHash = sha256Hex(bytes);
|
|
83
|
+
|
|
84
|
+
span?.setAttribute("documents.bytes", bytes.byteLength);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
bytes,
|
|
88
|
+
contentHash,
|
|
89
|
+
};
|
|
90
|
+
} catch (err) {
|
|
91
|
+
span?.recordError();
|
|
92
|
+
throw err;
|
|
93
|
+
} finally {
|
|
94
|
+
span?.end();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./caps.js";
|
|
2
|
+
export * from "./diff.js";
|
|
3
|
+
export * from "./errors.js";
|
|
4
|
+
export * from "./fidelity.js";
|
|
5
|
+
export * from "./generate.js";
|
|
6
|
+
export * from "./hash.js";
|
|
7
|
+
export * from "./model-schema.js";
|
|
8
|
+
export * from "./parse.js";
|
|
9
|
+
export * from "./patch.js";
|
|
10
|
+
export * from "./patch-history.js";
|
|
11
|
+
export * from "./preview.js";
|
|
12
|
+
export * from "./preview-html.js";
|
|
13
|
+
export * from "./telemetry.js";
|
|
14
|
+
export * from "./types.js";
|