@taloswork/ose-shared 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taloswork/ose-shared",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Talos cross-OSE shared library — skills + MCP servers auto-loaded into every OSE (P93 2026-07-27).",
5
5
  "type": "module",
6
6
  "files": [
@@ -10,6 +10,6 @@
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },
13
- "_talosPublishHash": "sha256-_TBER93S6Gcnoc6VaJyOr1oERYK8cA1G_ifT0tdC3ZQ",
14
- "_talosPublishedAt": "2026-08-27T14:52:48.964Z"
13
+ "_talosPublishHash": "sha256-T36gm46DynkzOQOg4I0N8sK_-SP1h0kOm1929zOwv9U",
14
+ "_talosPublishedAt": "2026-09-02T17:54:52.828Z"
15
15
  }
@@ -200,7 +200,7 @@ Fidelity-decision self-correct, but pointed at your OWN output:
200
200
  - `bin/pdf-tables.cjs <file.pdf> [page] [minCols]` — recover text-layer tables as rows (ruled/scanned tables need render + vision instead).
201
201
  - `bin/ocr.cjs <image.png|jpg> [lang]` — read text off a scanned image (tesseract.js). For a scanned PDF, `pdf-render` a page first, then OCR the PNG. Language data downloads on first use / set `$TALOS_TESSDATA` for offline. **Prefer vision** when a vision model is available; OCR is the text-only fallback.
202
202
  - `bin/xlsx-query.cjs <file> [--summary | --agg <sheet> <col> <op> | --sample <sheet> [n]]` — spreadsheet compute.
203
- - `bin/doc-gen.cjs <docx|xlsx|pptx|pdf> <out> <spec.json> [--theme <theme.json>] [--topic <subject> | --palette <id>] [--design native|html]` — generate documents from scratch; `--theme` applies a presentation theme (colors/fonts/cover/header/footer/pptx master — see rewrite.md "Themes"; built-ins in `themes/`). **`--topic <subject>` (pptx, when no `--theme`) picks a topic-adaptive palette** so the deck is designed-for-subject instead of template-flat, and turns on richness (motif + gradient wash + depth). `--palette <id>` forces a specific palette (`doc-gen --list-palettes` prints them). **`--accent <#hex>` / `--style <skin>` (docx/pdf)**: white-paper-black-text base with ONE brand color on H1/H2 + a faint table-header tint — the practical doc shape (a document is read/printed/edited, not projected, so body text stays near-black). `--accent` takes a literal hex; `--style <skin>` pulls just the `--accent` from `styles/<skin>/skin.css` (the deck skin's full palette is NOT carried onto paper). Precedence: `--theme` > spec `"theme"` > `--accent`/`--style` > `--palette`/`--topic` > default. `--design html` (pptx only, in-app) renders eligible slides through designed HTML templates for a magazine look — richer but a flat collage (no master structure); auto-falls back to native when headless or a slide has no template. Default `native` (structured, editable, charts, headless-safe).
203
+ - `bin/doc-gen.cjs <docx|xlsx|pptx|pdf> <out> <spec.json> [--theme <theme.json>] [--topic <subject> | --palette <id>] [--design native|html]` — generate documents from scratch. DOCX/PDF body fields are exactly `paragraphs` and/or `blocks` (never `sections`); unknown fields and empty bodies fail with `DOC_SPEC_INVALID` before any output is written (`allowCoverOnly:true` is the explicit title-only opt-in). Correct the reported JSON path and rerun. `--theme` applies a presentation theme (colors/fonts/cover/header/footer/pptx master — see rewrite.md "Themes"; built-ins in `themes/`). **`--topic <subject>` (pptx, when no `--theme`) picks a topic-adaptive palette** so the deck is designed-for-subject instead of template-flat, and turns on richness (motif + gradient wash + depth). `--palette <id>` forces a specific palette (`doc-gen --list-palettes` prints them). **`--accent <#hex>` / `--style <skin>` (docx/pdf)**: white-paper-black-text base with ONE brand color on H1/H2 + a faint table-header tint — the practical doc shape (a document is read/printed/edited, not projected, so body text stays near-black). `--accent` takes a literal hex; `--style <skin>` pulls just the `--accent` from `styles/<skin>/skin.css` (the deck skin's full palette is NOT carried onto paper). Precedence: `--theme` > spec `"theme"` > `--accent`/`--style` > `--palette`/`--topic` > default. `--design html` (pptx only, in-app) renders eligible slides through designed HTML templates for a magazine look — richer but a flat collage (no master structure); auto-falls back to native when headless or a slide has no template. Default `native` (structured, editable, charts, headless-safe).
204
204
  - `bin/doc-convert.cjs <in.docx|pptx|xlsx> <out.pdf>` — faithful docx/pptx/xlsx → PDF conversion. Engine precedence (best fidelity first, each downgrade tagged on stdout `[pdf (…)]` + warned on stderr): (1) **LibreOffice** if a system `soffice` is found — true layout render (page geometry, headers/footers, text-box positions, native charts), offline; (2) **LibreOffice sidecar** if `TALOS_OFFICE_URL` is set — same true-layout fidelity via a remote converter (e.g. Gotenberg), needs network; (3) **Chromium** (in-app) — reconstructed HTML, content/structure/images preserved but in-document layout is not (pptx = one page per slide); (4) **pdf-lib text-only** headless last resort — images/tables flatten. Surface the resulting `[pdf (engine)]` caveat to the user. NOTE: spreadsheets (xlsx/xls/ods) render ONLY via tiers 1–2 (real LibreOffice); tiers 3–4 do not rebuild spreadsheets, so with no LibreOffice available the command errors (never a degraded text-only sheet).
205
205
  - `bin/doc-validate.cjs <file.docx|pptx|xlsx> [--strict] [--design-lint]` — structural OOXML validation gate (zip/parts/rels/Content_Types/XML well-formedness); run after every generation or edit; exit 1 = broken, fix before delivering. `--strict` adds full ECMA-376 schema (XSD) validation of every recognised part — REQUIRED after custom-code (doc-run/html2pptx) or raw-XML output; a strict failure means the file is non-standard even if Office opens it. `--design-lint` (pptx only) adds an ADVISORY design pass — flags `text-only-slide` (a wall of text with no visual anchor), `low-contrast-text`, `placeholder-text` (a layout pasted but never filled — 替换为…/占位/Lorem still in the slide), `font-mismatch` (CJK text set in a Latin-only font → 豆腐; expected language defaults to the system locale, override with `TALOS_DECK_LANG`), `off-canvas`, `tiny-font`, `shape-overlap`; findings are warnings (never change the exit code) but are real design bugs — fix them. See deck-design.md "Design principles".
206
206
  - `bin/deck-qa.cjs <file.pptx> [--json]` — MECHANICAL design QA (deterministic, token-free): reads slide XML and flags `off-canvas` (content clipped past the edge), `tiny-font` (<10pt), `shape-overlap` (colliding text), plus `text-only-slide` / `low-contrast-text` / `placeholder-text` / `font-mismatch` (CJK text in a Latin-only font → 豆腐). Advisory (exit 0 even with findings). `--json` → `{ok,findings:[{slide,code,message}]}` for the visual-QA loop. It is the CHEAP GATE before spending vision tokens — run it every QA round and fix all findings first; then do the vision critic + per-slide regeneration (deck-design.md "Verify before delivering").
@@ -6,7 +6,7 @@ var import_promises = require("node:fs/promises");
6
6
  var import_node_fs = require("node:fs");
7
7
  var import_node_path = require("node:path");
8
8
 
9
- // packages/core/dist/fs/deck-figures.js
9
+ // packages/core/dist/document/deck-figures.js
10
10
  var FIGURE_SPECS = {
11
11
  radar: {
12
12
  metrics: { required: ["name", "score"], derived: [] }
@@ -159,7 +159,7 @@ function derivePins(rows, warnings) {
159
159
  });
160
160
  }
161
161
 
162
- // packages/core/dist/fs/deck-build.js
162
+ // packages/core/dist/document/deck-build.js
163
163
  var SLOT_RE = /\{\{(?![#/])([^{}]*)\}\}/g;
164
164
  var BLOCK_RE = /\{\{#([A-Za-z_][\w-]*)\}\}([\s\S]*?)\{\{\/\1\}\}/g;
165
165
  function escapeHtml(s) {
@@ -9818,7 +9818,7 @@ var require_lib3 = __commonJS({
9818
9818
  // apps/admin/data/skills.lib/document/bin-src/deck-qa.mjs
9819
9819
  var import_promises = require("node:fs/promises");
9820
9820
 
9821
- // packages/core/dist/fs/doc-design-lint.js
9821
+ // packages/core/dist/document/doc-design-lint.js
9822
9822
  var import_jszip = __toESM(require_lib3(), 1);
9823
9823
  var PLACEHOLDER_RE = /替换为|在这里写|占位|示例文本|Lorem ipsum/i;
9824
9824
  var CJK_TEXT_RE = /[぀-ヿ㐀-䶿一-鿿가-힯豈-﫿]/;
@@ -148976,11 +148976,11 @@ var require_fontkit_umd = __commonJS({
148976
148976
  var import_node_path2 = require("node:path");
148977
148977
  var import_promises2 = require("node:fs/promises");
148978
148978
 
148979
- // packages/core/dist/fs/doc-convert.js
148979
+ // packages/core/dist/document/doc-convert.js
148980
148980
  var import_jszip3 = __toESM(require_lib3(), 1);
148981
148981
  var import_mammoth2 = __toESM(require_lib6(), 1);
148982
148982
 
148983
- // packages/core/dist/fs/office-converter.js
148983
+ // packages/core/dist/document/office-converter.js
148984
148984
  var import_jszip = __toESM(require_lib3(), 1);
148985
148985
  var import_mammoth = __toESM(require_lib6(), 1);
148986
148986
  function isZipMagic(buffer) {
@@ -168101,7 +168101,7 @@ var findPatchKeys = (text) => {
168101
168101
  return (_text$match = text.match(pattern)) !== null && _text$match !== void 0 ? _text$match : [];
168102
168102
  };
168103
168103
 
168104
- // packages/core/dist/fs/doc-gen.js
168104
+ // packages/core/dist/document/doc-gen.js
168105
168105
  var import_exceljs = __toESM(require_excel(), 1);
168106
168106
  var import_jszip2 = __toESM(require_lib3(), 1);
168107
168107
  var import_pptxgenjs = __toESM(require_pptxgen_cjs(), 1);
@@ -168109,7 +168109,7 @@ var import_pdf_lib = __toESM(require_cjs(), 1);
168109
168109
  var import_fontkit = __toESM(require_fontkit_umd(), 1);
168110
168110
  var import_node_fs = require("node:fs");
168111
168111
 
168112
- // packages/core/dist/fs/doc-theme.js
168112
+ // packages/core/dist/document/doc-theme.js
168113
168113
  var DEFAULT_THEME = {
168114
168114
  bestFor: "",
168115
168115
  fonts: { heading: "", body: "" },
@@ -168301,7 +168301,7 @@ function asTheme(themeInput) {
168301
168301
  return themeInput && typeof themeInput === "object" && "warnings" in themeInput ? themeInput : resolveTheme(themeInput);
168302
168302
  }
168303
168303
 
168304
- // packages/core/dist/fs/doc-palette.js
168304
+ // packages/core/dist/document/doc-palette.js
168305
168305
  var PALETTE_LIBRARY = [
168306
168306
  {
168307
168307
  name: "corporate-navy",
@@ -168536,7 +168536,7 @@ function themeSpecFromTopic(topic) {
168536
168536
  return paletteToThemeSpec(resolvePalette(topic));
168537
168537
  }
168538
168538
 
168539
- // packages/core/dist/fs/lucide-icons.js
168539
+ // packages/core/dist/document/lucide-icons.js
168540
168540
  var LUCIDE_ICONS = {
168541
168541
  check: '<path d="M20 6 9 17l-5-5"/>',
168542
168542
  "check-circle": '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
@@ -168593,16 +168593,289 @@ function iconSvgDataUri(name, colorHex) {
168593
168593
  return "data:image/svg+xml;base64," + Buffer.from(svg).toString("base64");
168594
168594
  }
168595
168595
 
168596
- // packages/core/dist/fs/doc-gen.js
168596
+ // packages/core/dist/document/doc-gen.js
168597
168597
  function bareHex(c) {
168598
168598
  return c.replace("#", "").toUpperCase();
168599
168599
  }
168600
+ var DocumentSpecValidationError = class extends Error {
168601
+ code = "DOC_SPEC_INVALID";
168602
+ issues;
168603
+ constructor(issues) {
168604
+ const detail = issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ");
168605
+ super(`DOC_SPEC_INVALID: ${detail}`);
168606
+ this.name = "DocumentSpecValidationError";
168607
+ this.issues = [...issues];
168608
+ }
168609
+ };
168610
+ var TEXT_DOCUMENT_KEYS = /* @__PURE__ */ new Set(["title", "paragraphs", "blocks", "allowCoverOnly", "fontPath", "theme", "topic", "palette"]);
168611
+ var XLSX_DOCUMENT_KEYS = /* @__PURE__ */ new Set(["sheets", "theme", "topic", "palette"]);
168612
+ var PPTX_DOCUMENT_KEYS = /* @__PURE__ */ new Set(["slides", "theme", "topic", "palette"]);
168613
+ var PPTX_SLIDE_KEYS = /* @__PURE__ */ new Set([
168614
+ "title",
168615
+ "cover",
168616
+ "toc",
168617
+ "ending",
168618
+ "section",
168619
+ "kpiCards",
168620
+ "cards",
168621
+ "timeline",
168622
+ "steps",
168623
+ "iconList",
168624
+ "splitImage",
168625
+ "comparison",
168626
+ "quadrant",
168627
+ "bigStat",
168628
+ "quote",
168629
+ "bullets",
168630
+ "table",
168631
+ "image",
168632
+ "chart"
168633
+ ]);
168634
+ function isJsonRecord(value) {
168635
+ return value !== null && typeof value === "object" && !Array.isArray(value);
168636
+ }
168637
+ function addUnknownPropertyIssues(record, allowed, path, issues) {
168638
+ for (const key of Object.keys(record)) {
168639
+ if (allowed.has(key))
168640
+ continue;
168641
+ const message = allowed === TEXT_DOCUMENT_KEYS && (key === "section" || key === "sections") ? `${key} is unsupported for DOCX/PDF content; use $.blocks with heading/paragraph/list/table/image blocks (section is PPTX-only)` : `unsupported property ${JSON.stringify(key)}; allowed properties: ${[...allowed].join(", ")}`;
168642
+ issues.push({ path: `${path}.${key}`, code: "UNKNOWN_PROPERTY", message });
168643
+ }
168644
+ }
168645
+ function nonBlankString(value) {
168646
+ return typeof value === "string" && value.trim().length > 0;
168647
+ }
168648
+ function validateTextBlock(value, path, issues) {
168649
+ if (!isJsonRecord(value)) {
168650
+ issues.push({ path, code: "INVALID_TYPE", message: "block must be an object" });
168651
+ return false;
168652
+ }
168653
+ if (typeof value.type !== "string") {
168654
+ issues.push({ path: `${path}.type`, code: "INVALID_TYPE", message: "block type must be a string" });
168655
+ return false;
168656
+ }
168657
+ const requireText = () => {
168658
+ if (nonBlankString(value.text))
168659
+ return true;
168660
+ issues.push({ path: `${path}.text`, code: "EMPTY_CONTENT", message: "text must be a non-empty string" });
168661
+ return false;
168662
+ };
168663
+ switch (value.type) {
168664
+ case "heading": {
168665
+ addUnknownPropertyIssues(value, /* @__PURE__ */ new Set(["type", "text", "level"]), path, issues);
168666
+ const valid = requireText();
168667
+ if (value.level !== void 0 && ![1, 2, 3, 4].includes(value.level)) {
168668
+ issues.push({ path: `${path}.level`, code: "INVALID_VALUE", message: "level must be one of 1, 2, 3, 4" });
168669
+ }
168670
+ return valid;
168671
+ }
168672
+ case "paragraph": {
168673
+ addUnknownPropertyIssues(value, /* @__PURE__ */ new Set(["type", "text", "bold", "italic"]), path, issues);
168674
+ const valid = requireText();
168675
+ for (const key of ["bold", "italic"]) {
168676
+ if (value[key] !== void 0 && typeof value[key] !== "boolean") {
168677
+ issues.push({ path: `${path}.${key}`, code: "INVALID_TYPE", message: `${key} must be a boolean` });
168678
+ }
168679
+ }
168680
+ return valid;
168681
+ }
168682
+ case "list": {
168683
+ addUnknownPropertyIssues(value, /* @__PURE__ */ new Set(["type", "items", "ordered"]), path, issues);
168684
+ if (!Array.isArray(value.items) || value.items.length === 0) {
168685
+ issues.push({ path: `${path}.items`, code: "EMPTY_CONTENT", message: "items must be a non-empty string array" });
168686
+ return false;
168687
+ }
168688
+ let valid = true;
168689
+ value.items.forEach((item, index) => {
168690
+ if (nonBlankString(item))
168691
+ return;
168692
+ valid = false;
168693
+ issues.push({ path: `${path}.items[${index}]`, code: "EMPTY_CONTENT", message: "list item must be a non-empty string" });
168694
+ });
168695
+ if (value.ordered !== void 0 && typeof value.ordered !== "boolean") {
168696
+ issues.push({ path: `${path}.ordered`, code: "INVALID_TYPE", message: "ordered must be a boolean" });
168697
+ }
168698
+ return valid;
168699
+ }
168700
+ case "table": {
168701
+ addUnknownPropertyIssues(value, /* @__PURE__ */ new Set(["type", "rows", "header", "columnWidths"]), path, issues);
168702
+ if (!Array.isArray(value.rows) || value.rows.length === 0) {
168703
+ issues.push({ path: `${path}.rows`, code: "EMPTY_CONTENT", message: "rows must be a non-empty array" });
168704
+ return false;
168705
+ }
168706
+ let hasCellContent = false;
168707
+ value.rows.forEach((row, rowIndex) => {
168708
+ if (!Array.isArray(row) || row.length === 0) {
168709
+ issues.push({ path: `${path}.rows[${rowIndex}]`, code: "INVALID_TYPE", message: "table row must be a non-empty array" });
168710
+ return;
168711
+ }
168712
+ row.forEach((cell, columnIndex) => {
168713
+ if (typeof cell !== "string" && typeof cell !== "number") {
168714
+ issues.push({ path: `${path}.rows[${rowIndex}][${columnIndex}]`, code: "INVALID_TYPE", message: "table cell must be a string or number" });
168715
+ } else if (String(cell).trim().length > 0) {
168716
+ hasCellContent = true;
168717
+ }
168718
+ });
168719
+ });
168720
+ if (!hasCellContent) {
168721
+ issues.push({ path: `${path}.rows`, code: "EMPTY_CONTENT", message: "table must contain at least one non-empty cell" });
168722
+ }
168723
+ if (value.header !== void 0 && typeof value.header !== "boolean") {
168724
+ issues.push({ path: `${path}.header`, code: "INVALID_TYPE", message: "header must be a boolean" });
168725
+ }
168726
+ if (value.columnWidths !== void 0 && (!Array.isArray(value.columnWidths) || value.columnWidths.some((width) => typeof width !== "number" || !Number.isFinite(width) || width <= 0))) {
168727
+ issues.push({ path: `${path}.columnWidths`, code: "INVALID_VALUE", message: "columnWidths must contain only positive finite numbers" });
168728
+ }
168729
+ return hasCellContent;
168730
+ }
168731
+ case "image": {
168732
+ addUnknownPropertyIssues(value, /* @__PURE__ */ new Set(["type", "data", "imageType", "width", "height"]), path, issues);
168733
+ if (!nonBlankString(value.data)) {
168734
+ issues.push({ path: `${path}.data`, code: "EMPTY_CONTENT", message: "image data must be non-empty base64" });
168735
+ return false;
168736
+ }
168737
+ if (value.imageType !== void 0 && !["png", "jpg", "gif", "bmp"].includes(String(value.imageType))) {
168738
+ issues.push({ path: `${path}.imageType`, code: "INVALID_VALUE", message: "imageType must be png, jpg, gif, or bmp" });
168739
+ }
168740
+ for (const key of ["width", "height"]) {
168741
+ if (value[key] !== void 0 && (typeof value[key] !== "number" || !Number.isFinite(value[key]) || value[key] <= 0)) {
168742
+ issues.push({ path: `${path}.${key}`, code: "INVALID_VALUE", message: `${key} must be a positive finite number` });
168743
+ }
168744
+ }
168745
+ return true;
168746
+ }
168747
+ default:
168748
+ issues.push({ path: `${path}.type`, code: "INVALID_VALUE", message: `unhandled block type: ${value.type}` });
168749
+ return false;
168750
+ }
168751
+ }
168752
+ function inspectTextDocumentSpec(spec) {
168753
+ const issues = [];
168754
+ if (!isJsonRecord(spec)) {
168755
+ issues.push({ path: "$", code: "INVALID_TYPE", message: "spec must be a JSON object" });
168756
+ return { issues, hasBody: false, hasTitle: false, allowCoverOnly: false };
168757
+ }
168758
+ addUnknownPropertyIssues(spec, TEXT_DOCUMENT_KEYS, "$", issues);
168759
+ const hasTitle = nonBlankString(spec.title);
168760
+ if (spec.title !== void 0 && !hasTitle) {
168761
+ issues.push({ path: "$.title", code: "EMPTY_CONTENT", message: "title must be a non-empty string when provided" });
168762
+ }
168763
+ if (spec.allowCoverOnly !== void 0 && typeof spec.allowCoverOnly !== "boolean") {
168764
+ issues.push({ path: "$.allowCoverOnly", code: "INVALID_TYPE", message: "allowCoverOnly must be a boolean" });
168765
+ }
168766
+ if (spec.fontPath !== void 0 && !nonBlankString(spec.fontPath)) {
168767
+ issues.push({ path: "$.fontPath", code: "INVALID_TYPE", message: "fontPath must be a non-empty string" });
168768
+ }
168769
+ let hasBody = false;
168770
+ if (spec.paragraphs !== void 0) {
168771
+ if (!Array.isArray(spec.paragraphs)) {
168772
+ issues.push({ path: "$.paragraphs", code: "INVALID_TYPE", message: "paragraphs must be a string array" });
168773
+ } else {
168774
+ spec.paragraphs.forEach((paragraph, index) => {
168775
+ if (nonBlankString(paragraph))
168776
+ hasBody = true;
168777
+ else
168778
+ issues.push({ path: `$.paragraphs[${index}]`, code: "EMPTY_CONTENT", message: "paragraph must be a non-empty string" });
168779
+ });
168780
+ }
168781
+ }
168782
+ if (spec.blocks !== void 0) {
168783
+ if (!Array.isArray(spec.blocks)) {
168784
+ issues.push({ path: "$.blocks", code: "INVALID_TYPE", message: "blocks must be an array" });
168785
+ } else {
168786
+ spec.blocks.forEach((block, index) => {
168787
+ if (validateTextBlock(block, `$.blocks[${index}]`, issues))
168788
+ hasBody = true;
168789
+ });
168790
+ }
168791
+ }
168792
+ return { issues, hasBody, hasTitle, allowCoverOnly: spec.allowCoverOnly === true };
168793
+ }
168600
168794
  function assertTextDocumentSpec(spec) {
168601
- if (!spec || typeof spec !== "object")
168795
+ const { issues } = inspectTextDocumentSpec(spec);
168796
+ if (issues.length > 0)
168797
+ throw new DocumentSpecValidationError(issues);
168798
+ }
168799
+ function validateRows(rows, path, issues) {
168800
+ if (!Array.isArray(rows) || rows.length === 0) {
168801
+ issues.push({ path, code: "EMPTY_CONTENT", message: "rows must be a non-empty array" });
168802
+ return false;
168803
+ }
168804
+ let hasContent = false;
168805
+ rows.forEach((row, rowIndex) => {
168806
+ if (!Array.isArray(row) || row.length === 0) {
168807
+ issues.push({ path: `${path}[${rowIndex}]`, code: "INVALID_TYPE", message: "row must be a non-empty array" });
168808
+ return;
168809
+ }
168810
+ row.forEach((cell, columnIndex) => {
168811
+ if (typeof cell !== "string" && typeof cell !== "number") {
168812
+ issues.push({ path: `${path}[${rowIndex}][${columnIndex}]`, code: "INVALID_TYPE", message: "cell must be a string or number" });
168813
+ } else if (String(cell).trim().length > 0) {
168814
+ hasContent = true;
168815
+ }
168816
+ });
168817
+ });
168818
+ if (!hasContent)
168819
+ issues.push({ path, code: "EMPTY_CONTENT", message: "rows must contain at least one non-empty cell" });
168820
+ return hasContent;
168821
+ }
168822
+ function assertDocumentSpec(kind, spec) {
168823
+ if (kind === "docx" || kind === "pdf") {
168824
+ const inspection = inspectTextDocumentSpec(spec);
168825
+ if (!inspection.hasBody && !(inspection.allowCoverOnly && inspection.hasTitle)) {
168826
+ inspection.issues.push({
168827
+ path: "$",
168828
+ code: "EMPTY_CONTENT",
168829
+ message: "document body is empty; provide non-empty paragraphs/blocks, or set allowCoverOnly=true with a non-empty title"
168830
+ });
168831
+ }
168832
+ if (inspection.issues.length > 0)
168833
+ throw new DocumentSpecValidationError(inspection.issues);
168602
168834
  return;
168603
- if (Object.prototype.hasOwnProperty.call(spec, "section")) {
168604
- throw new Error("unsupported document field: section (use blocks for DOCX/PDF content; section is PPTX-only)");
168605
168835
  }
168836
+ const issues = [];
168837
+ if (!isJsonRecord(spec)) {
168838
+ throw new DocumentSpecValidationError([{ path: "$", code: "INVALID_TYPE", message: "spec must be a JSON object" }]);
168839
+ }
168840
+ if (kind === "xlsx") {
168841
+ addUnknownPropertyIssues(spec, XLSX_DOCUMENT_KEYS, "$", issues);
168842
+ if (!isJsonRecord(spec.sheets) || Object.keys(spec.sheets).length === 0) {
168843
+ issues.push({ path: "$.sheets", code: "EMPTY_CONTENT", message: "sheets must be a non-empty object" });
168844
+ } else {
168845
+ for (const [name, sheet] of Object.entries(spec.sheets)) {
168846
+ const path = `$.sheets[${JSON.stringify(name)}]`;
168847
+ if (Array.isArray(sheet)) {
168848
+ validateRows(sheet, path, issues);
168849
+ } else if (isJsonRecord(sheet)) {
168850
+ addUnknownPropertyIssues(sheet, /* @__PURE__ */ new Set(["rows", "header", "columnWidths", "numFmt"]), path, issues);
168851
+ validateRows(sheet.rows, `${path}.rows`, issues);
168852
+ } else {
168853
+ issues.push({ path, code: "INVALID_TYPE", message: "sheet must be a row array or an object containing rows" });
168854
+ }
168855
+ }
168856
+ }
168857
+ } else if (kind === "pptx") {
168858
+ addUnknownPropertyIssues(spec, PPTX_DOCUMENT_KEYS, "$", issues);
168859
+ if (!Array.isArray(spec.slides) || spec.slides.length === 0) {
168860
+ issues.push({ path: "$.slides", code: "EMPTY_CONTENT", message: "slides must be a non-empty array" });
168861
+ } else {
168862
+ spec.slides.forEach((slide, index) => {
168863
+ const path = `$.slides[${index}]`;
168864
+ if (!isJsonRecord(slide)) {
168865
+ issues.push({ path, code: "INVALID_TYPE", message: "slide must be an object" });
168866
+ return;
168867
+ }
168868
+ addUnknownPropertyIssues(slide, PPTX_SLIDE_KEYS, path, issues);
168869
+ if (Object.keys(slide).length === 0) {
168870
+ issues.push({ path, code: "EMPTY_CONTENT", message: "slide must contain at least one supported field" });
168871
+ }
168872
+ });
168873
+ }
168874
+ } else {
168875
+ throw new Error(`unsupported kind: ${String(kind)}`);
168876
+ }
168877
+ if (issues.length > 0)
168878
+ throw new DocumentSpecValidationError(issues);
168606
168879
  }
168607
168880
  var HEADINGS = [HeadingLevel.HEADING_1, HeadingLevel.HEADING_2, HeadingLevel.HEADING_3, HeadingLevel.HEADING_4];
168608
168881
  var ORDERED_REF = "ol";
@@ -169864,6 +170137,7 @@ async function generatePdf(spec, themeInput) {
169864
170137
  return doc.save();
169865
170138
  }
169866
170139
  async function generateDocument(kind, spec, theme) {
170140
+ assertDocumentSpec(kind, spec);
169867
170141
  switch (kind) {
169868
170142
  case "docx":
169869
170143
  return generateDocx(spec, theme);
@@ -169878,10 +170152,10 @@ async function generateDocument(kind, spec, theme) {
169878
170152
  }
169879
170153
  }
169880
170154
 
169881
- // packages/core/dist/fs/doc-html.js
170155
+ // packages/core/dist/document/doc-html.js
169882
170156
  var FONT_STACK = 'TalosCJK, "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Source Han Sans SC", "WenQuanYi Zen Hei", sans-serif';
169883
170157
 
169884
- // packages/core/dist/fs/doc-convert.js
170158
+ // packages/core/dist/document/doc-convert.js
169885
170159
  var PRINT_STYLE = `
169886
170160
  @page { size: A4; margin: 20mm 18mm; }
169887
170161
  * { box-sizing: border-box; }
@@ -169992,7 +170266,7 @@ async function convertOfficeToPrintable(buffer, filename) {
169992
170266
  return { error: `\u4E0D\u652F\u6301 ${filename} \u7684\u6587\u4EF6\u683C\u5F0F\uFF08doc-convert \u53EA\u652F\u6301 .docx/.pptx\uFF09` };
169993
170267
  }
169994
170268
 
169995
- // packages/core/dist/fs/soffice.js
170269
+ // packages/core/dist/document/soffice.js
169996
170270
  var import_node_child_process = require("node:child_process");
169997
170271
  var import_node_fs2 = require("node:fs");
169998
170272
  var import_promises = require("node:fs/promises");
@@ -170138,7 +170412,7 @@ function runSoffice(bin, args, timeoutMs) {
170138
170412
  });
170139
170413
  }
170140
170414
 
170141
- // packages/core/dist/fs/office-service.js
170415
+ // packages/core/dist/document/office-service.js
170142
170416
  async function convertViaOfficeService(bytes, filename, url, options = {}) {
170143
170417
  const doFetch = options.fetchImpl ?? fetch;
170144
170418
  const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
@@ -16068,7 +16068,7 @@ var require_js = __commonJS({
16068
16068
  var import_promises = require("node:fs/promises");
16069
16069
  var import_node_path = require("node:path");
16070
16070
 
16071
- // packages/core/dist/fs/doc-template.js
16071
+ // packages/core/dist/document/doc-template.js
16072
16072
  var import_docxtemplater = __toESM(require_docxtemplater(), 1);
16073
16073
  var import_pizzip = __toESM(require_js(), 1);
16074
16074
  var DEFAULT_DELIMITERS = { start: "{{", end: "}}" };