@invompt/invoml 1.0.0-alpha.6 → 1.0.0-alpha.7

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 CHANGED
@@ -6,7 +6,7 @@
6
6
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square)](./LICENSE)
7
7
  [![CI](https://img.shields.io/github/actions/workflow/status/invompt/InvoML/ci.yml?style=flat-square)](https://github.com/invompt/InvoML/actions)
8
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?style=flat-square)](https://www.typescriptlang.org/)
9
- [![test vectors](https://img.shields.io/badge/test_vectors-18-brightgreen?style=flat-square)](./test-vectors/)
9
+ [![test vectors](https://img.shields.io/badge/test_vectors-20-brightgreen?style=flat-square)](./test-vectors/)
10
10
 
11
11
  InvoML is a **format specification** for invoice documents designed from the ground up for AI structured output and human authoring. `@invompt/invoml` is the official TypeScript reference implementation.
12
12
 
@@ -184,6 +184,19 @@ const md = toMarkdown(doc)
184
184
  | `validateStyle(style, sectionNames?)` | Validate a style object against normative rules |
185
185
  | `resolveOrder(doc)` | Resolve effective block rendering order for a document |
186
186
  | `resolveStyle(doc)` | Resolve the full style object with defaults applied |
187
+ | `setSchema(schema)` | Inject the JSON Schema directly (required for browser/edge runtimes) |
188
+
189
+ ### Browser / Edge Runtime Usage
190
+
191
+ The default validator loads the JSON Schema from the filesystem (Node.js only). For browser or edge runtimes, inject the schema manually:
192
+
193
+ ```typescript
194
+ import { setSchema, parse, calculate } from 'invoml'
195
+ import schema from 'invoml/invoml-v1.0.schema.json'
196
+
197
+ setSchema(schema)
198
+ // Now parse() and validateSchema() work without filesystem access
199
+ ```
187
200
 
188
201
  ---
189
202
 
@@ -194,7 +207,7 @@ const md = toMarkdown(doc)
194
207
  The InvoML v1.0 JSON Schema is at [`invoml-v1.0.schema.json`](./invoml-v1.0.schema.json). Pass it directly to any LLM structured-output API:
195
208
 
196
209
  ```typescript
197
- import schema from '@invompt/invoml/invoml-v1.0.schema.json' assert { type: 'json' }
210
+ import schema from '@invompt/invoml/invoml-v1.0.schema.json' with { type: 'json' }
198
211
 
199
212
  // OpenAI structured outputs
200
213
  const completion = await openai.beta.chat.completions.parse({
@@ -228,7 +241,7 @@ npx @invompt/invoml html invoice.json > invoice.html
228
241
 
229
242
  ### Test vectors for conformance
230
243
 
231
- The [`test-vectors/`](./test-vectors/) directory contains 18 canonical input/expected pairs. Any implementation claiming InvoML v1.0 conformance must pass all 18 vectors.
244
+ The [`test-vectors/`](./test-vectors/) directory contains 20 canonical input/expected pairs. Any implementation claiming InvoML v1.0 conformance must pass all 20 vectors.
232
245
 
233
246
  | # | Scenario |
234
247
  |---|---|
@@ -250,6 +263,8 @@ The [`test-vectors/`](./test-vectors/) directory contains 18 canonical input/exp
250
263
  | 16 | Error — unknown tax category |
251
264
  | 17 | Error — no default tax |
252
265
  | 18 | Proportional discount tie-breaking |
266
+ | 19 | JPY zero-decimal currency (Japan Consumption Tax) |
267
+ | 20 | KWD three-decimal currency (Kuwait VAT) |
253
268
 
254
269
  ---
255
270
 
@@ -311,7 +326,7 @@ See [`docs/WHY-INVOML.md`](./docs/WHY-INVOML.md) for a detailed comparison with
311
326
 
312
327
  See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, test vector authoring, and the pull request process.
313
328
 
314
- Implementations in other languages are welcome. Any implementation that passes all 18 test vectors and implements the arithmetic rules in `SPEC.md` is a conforming InvoML v1.0 implementation.
329
+ Implementations in other languages are welcome. Any implementation that passes all 20 test vectors and implements the arithmetic rules in `SPEC.md` is a conforming InvoML v1.0 implementation.
315
330
 
316
331
  ---
317
332
 
@@ -6,16 +6,31 @@ import { validateSchema } from '../src/schema.js';
6
6
  import { toJSON, toMarkdown } from '../src/serializer.js';
7
7
  import { toHTML } from '../src/html-renderer.js';
8
8
  const [, , command, file, format] = process.argv;
9
- if (!command || !file) {
10
- console.log('Usage: invoml <validate|calculate|serialize|html> <file.json> [format]');
9
+ if (!command) {
10
+ console.log('Usage: invoml <validate|calculate|serialize|html> [file.json | -]');
11
11
  process.exit(1);
12
12
  }
13
13
  let content;
14
- try {
15
- content = readFileSync(file, 'utf8');
14
+ if (file === '-' || (!file && !process.stdin.isTTY)) {
15
+ try {
16
+ content = readFileSync(0, 'utf8');
17
+ }
18
+ catch (e) {
19
+ console.error(`Error reading from stdin: ${e.message}`);
20
+ process.exit(1);
21
+ }
22
+ }
23
+ else if (file) {
24
+ try {
25
+ content = readFileSync(file, 'utf8');
26
+ }
27
+ catch (e) {
28
+ console.error(`Error reading file "${file}": ${e.message}`);
29
+ process.exit(1);
30
+ }
16
31
  }
17
- catch (e) {
18
- console.error(`Error reading file "${file}": ${e.message}`);
32
+ else {
33
+ console.log('Usage: invoml <validate|calculate|serialize|html> [file.json | -]');
19
34
  process.exit(1);
20
35
  }
21
36
  if (command === 'validate') {
@@ -148,7 +148,7 @@ export function calculate(doc) {
148
148
  total = round(new InternalDecimal(afterDiscounts.toString()).plus(taxTotal.toString()).minus(withholdingTotal.toString()).toNumber());
149
149
  }
150
150
  // ── STEP 6: Amount Due ──
151
- const prepaid = doc.totals?.prepaidAmount ?? 0;
151
+ const prepaid = doc.prepaidAmount ?? doc.totals?.prepaidAmount ?? 0;
152
152
  const amountDue = round(new InternalDecimal(total.toString()).minus(prepaid.toString()).toNumber());
153
153
  return {
154
154
  subtotal,
@@ -1,4 +1,4 @@
1
1
  /** Base CSS for all InvoML HTML output. Applied to every document regardless of template. */
2
- export declare const BASE_CSS = "\n/* InvoML base styles */\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\nbody { background: #f5f5f5; }\n\n.invoml-container {\n --invoml-color-accent: #2563eb;\n --invoml-color-text: #1a1a1a;\n --invoml-color-muted: #666666;\n --invoml-color-border: #e0e0e0;\n --invoml-color-background: #ffffff;\n --invoml-font-heading: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n --invoml-font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n --invoml-padding-y: 10mm;\n --invoml-padding-x: 12mm;\n --invoml-section-gap: 18px;\n --invoml-table-row-padding: 10px;\n --invoml-parties-gap: 28px;\n --invoml-payment-margin-top: 28px;\n --invoml-meta-gap: 18px;\n --invoml-totals-margin: 18px;\n --invoml-line-height: 1.5;\n --invoml-paragraph-spacing: 12px;\n\n width: 210mm;\n margin: 0 auto;\n padding: var(--invoml-padding-y) var(--invoml-padding-x);\n background: var(--invoml-color-background);\n color: var(--invoml-color-text);\n font-family: var(--invoml-font-body);\n font-size: 14px;\n line-height: var(--invoml-line-height);\n -webkit-font-smoothing: antialiased;\n}\n\n.invoml-container h1, .invoml-container h2, .invoml-container h3,\n.invoml-container h4, .invoml-container h5, .invoml-container h6 {\n font-family: var(--invoml-font-heading);\n color: var(--invoml-color-text);\n line-height: 1.3;\n margin: 0;\n}\n\n.invoml-container a { color: var(--invoml-color-accent); text-decoration: none; }\n.invoml-container a:hover { text-decoration: underline; }\n.invoml-container table { border-collapse: collapse; border-spacing: 0; width: 100%; }\n.invoml-container th, .invoml-container td { text-align: left; vertical-align: top; }\n.invoml-container ul, .invoml-container ol { padding-left: 1.5em; }\n.invoml-container p + p { margin-top: var(--invoml-paragraph-spacing); }\n\n/* Density variants */\n.invoml-density-compact {\n --invoml-padding-y: 5mm;\n --invoml-padding-x: 6mm;\n --invoml-section-gap: 6px;\n --invoml-table-row-padding: 3px;\n --invoml-parties-gap: 10px;\n --invoml-payment-margin-top: 10px;\n --invoml-meta-gap: 6px;\n --invoml-totals-margin: 6px;\n --invoml-line-height: 1.3;\n --invoml-paragraph-spacing: 4px;\n}\n.invoml-density-spacious {\n --invoml-padding-y: 20mm;\n --invoml-padding-x: 24mm;\n --invoml-section-gap: 36px;\n --invoml-table-row-padding: 16px;\n --invoml-parties-gap: 48px;\n --invoml-payment-margin-top: 48px;\n --invoml-meta-gap: 28px;\n --invoml-totals-margin: 32px;\n --invoml-line-height: 1.8;\n --invoml-paragraph-spacing: 20px;\n}\n\n/* Header block */\n.invoml-header { margin-bottom: var(--invoml-section-gap); }\n.invoml-header-title {\n font-size: 28px;\n font-weight: 300;\n letter-spacing: -0.5px;\n color: var(--invoml-color-text);\n font-family: var(--invoml-font-heading);\n margin-bottom: 6px;\n}\n.invoml-header-number {\n font-size: 13px;\n font-weight: 600;\n color: var(--invoml-color-accent);\n letter-spacing: 0.5px;\n margin-bottom: 14px;\n display: inline-block;\n background: color-mix(in srgb, var(--invoml-color-accent) 10%, transparent);\n padding: 3px 10px;\n border-radius: 4px;\n}\n.invoml-header-meta {\n display: flex;\n flex-wrap: wrap;\n gap: var(--invoml-meta-gap);\n font-size: 13px;\n}\n.invoml-header-meta-item { display: flex; flex-direction: column; gap: 2px; min-width: 90px; }\n.invoml-header-meta-label {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 1px;\n color: var(--invoml-color-muted);\n font-weight: 500;\n}\n.invoml-header-meta-value { color: var(--invoml-color-text); font-weight: 500; font-size: 13px; }\n\n/* Party blocks */\n.invoml-party {\n font-size: 14px;\n line-height: var(--invoml-line-height);\n margin-bottom: var(--invoml-section-gap);\n vertical-align: top;\n}\n.invoml-party-label {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n color: var(--invoml-color-muted);\n margin-bottom: 8px;\n font-weight: 500;\n}\n.invoml-party-name {\n font-weight: 600;\n color: var(--invoml-color-text);\n font-size: 15px;\n margin-bottom: 6px;\n}\n.invoml-party-details { color: var(--invoml-color-muted); font-size: 13px; line-height: var(--invoml-line-height); }\n.invoml-party-details > div { margin: 2px 0; }\n\n/* Items table */\n.invoml-items { margin: var(--invoml-totals-margin) 0; }\n.invoml-items th {\n padding: var(--invoml-table-row-padding) 0;\n border-bottom: 1px solid var(--invoml-color-border);\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 1px;\n color: var(--invoml-color-muted);\n font-weight: 500;\n}\n.invoml-items td {\n padding: var(--invoml-table-row-padding) 0;\n border-bottom: 1px solid color-mix(in srgb, var(--invoml-color-border) 50%, transparent);\n font-size: 14px;\n color: var(--invoml-color-text);\n}\n.invoml-items .col-right { text-align: right; padding-right: 0; padding-left: 12px; }\n\n/* Totals */\n.invoml-totals { display: flex; justify-content: flex-end; margin-top: var(--invoml-totals-margin); }\n.invoml-totals-inner { width: 300px; }\n.invoml-totals-row {\n display: flex;\n justify-content: space-between;\n padding: 5px 0;\n font-size: 14px;\n color: var(--invoml-color-text);\n}\n.invoml-totals-row.is-grand {\n border-top: 2px solid var(--invoml-color-text);\n margin-top: 8px;\n padding-top: 10px;\n font-size: 15px;\n font-weight: 600;\n}\n.invoml-totals-row.is-amount-due {\n border-top: 1px solid var(--invoml-color-border);\n margin-top: 4px;\n padding-top: 8px;\n font-weight: 600;\n}\n.invoml-totals-label { color: var(--invoml-color-muted); }\n.invoml-totals-label.is-bold { color: var(--invoml-color-text); font-weight: 600; }\n.invoml-totals-amount { font-variant-numeric: tabular-nums; }\n\n/* Payment block */\n.invoml-payment {\n margin-top: var(--invoml-payment-margin-top);\n padding-top: 24px;\n border-top: 1px solid var(--invoml-color-border);\n}\n.invoml-payment-title {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n color: var(--invoml-color-muted);\n margin-bottom: 12px;\n font-weight: 500;\n}\n.invoml-payment-details { font-size: 14px; line-height: var(--invoml-line-height); color: var(--invoml-color-muted); }\n.invoml-payment-details strong { color: var(--invoml-color-text); font-weight: 600; }\n\n/* Notes block */\n.invoml-notes {\n margin-top: var(--invoml-section-gap);\n padding-top: var(--invoml-section-gap);\n border-top: 1px solid var(--invoml-color-border);\n font-size: 13px;\n color: var(--invoml-color-muted);\n}\n\n/* Section block */\n.invoml-section { margin: var(--invoml-section-gap) 0; }\n.invoml-section-title {\n font-size: 13px;\n font-weight: 600;\n color: var(--invoml-color-text);\n margin-bottom: 8px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n.invoml-section-content { font-size: 14px; color: var(--invoml-color-text); line-height: var(--invoml-line-height); }\n";
2
+ export declare const BASE_CSS = "\n/* InvoML base styles */\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\nbody { background: #f5f5f5; }\n\n.invoml-container {\n --invoml-color-accent: #2563eb;\n --invoml-color-text: #1a1a1a;\n --invoml-color-muted: #666666;\n --invoml-color-border: #e0e0e0;\n --invoml-color-background: #ffffff;\n --invoml-font-heading: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n --invoml-font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n --invoml-padding-y: 10mm;\n --invoml-padding-x: 12mm;\n --invoml-section-gap: 18px;\n --invoml-table-row-padding: 10px;\n --invoml-parties-gap: 28px;\n --invoml-payment-margin-top: 28px;\n --invoml-meta-gap: 18px;\n --invoml-totals-margin: 18px;\n --invoml-line-height: 1.5;\n --invoml-paragraph-spacing: 12px;\n\n width: 210mm;\n margin: 0 auto;\n padding: var(--invoml-padding-y) var(--invoml-padding-x);\n background: var(--invoml-color-background);\n color: var(--invoml-color-text);\n font-family: var(--invoml-font-body);\n font-size: 14px;\n line-height: var(--invoml-line-height);\n -webkit-font-smoothing: antialiased;\n}\n\n.invoml-container h1, .invoml-container h2, .invoml-container h3,\n.invoml-container h4, .invoml-container h5, .invoml-container h6 {\n font-family: var(--invoml-font-heading);\n color: var(--invoml-color-text);\n line-height: 1.3;\n margin: 0;\n}\n\n.invoml-container a { color: var(--invoml-color-accent); text-decoration: none; }\n.invoml-container a:hover { text-decoration: underline; }\n.invoml-container table { border-collapse: collapse; border-spacing: 0; width: 100%; }\n.invoml-container th, .invoml-container td { text-align: left; vertical-align: top; }\n.invoml-container ul, .invoml-container ol { padding-left: 1.5em; }\n.invoml-container p + p { margin-top: var(--invoml-paragraph-spacing); }\n\n/* Density variants */\n.invoml-density-compact {\n --invoml-padding-y: 5mm;\n --invoml-padding-x: 6mm;\n --invoml-section-gap: 6px;\n --invoml-table-row-padding: 3px;\n --invoml-parties-gap: 10px;\n --invoml-payment-margin-top: 10px;\n --invoml-meta-gap: 6px;\n --invoml-totals-margin: 6px;\n --invoml-line-height: 1.3;\n --invoml-paragraph-spacing: 4px;\n}\n.invoml-density-spacious {\n --invoml-padding-y: 20mm;\n --invoml-padding-x: 24mm;\n --invoml-section-gap: 36px;\n --invoml-table-row-padding: 16px;\n --invoml-parties-gap: 48px;\n --invoml-payment-margin-top: 48px;\n --invoml-meta-gap: 28px;\n --invoml-totals-margin: 32px;\n --invoml-line-height: 1.8;\n --invoml-paragraph-spacing: 20px;\n}\n\n/* Header block */\n.invoml-header { margin-bottom: var(--invoml-section-gap); }\n.invoml-header-title {\n font-size: 28px;\n font-weight: 300;\n letter-spacing: -0.5px;\n color: var(--invoml-color-text);\n font-family: var(--invoml-font-heading);\n margin-bottom: 6px;\n}\n.invoml-header-number {\n font-size: 13px;\n font-weight: 600;\n color: var(--invoml-color-accent);\n letter-spacing: 0.5px;\n margin-bottom: 14px;\n display: inline-block;\n background: color-mix(in srgb, var(--invoml-color-accent) 10%, transparent);\n padding: 3px 10px;\n border-radius: 4px;\n}\n.invoml-header-meta {\n display: flex;\n flex-wrap: wrap;\n gap: var(--invoml-meta-gap);\n font-size: 13px;\n}\n.invoml-header-meta-item { display: flex; flex-direction: column; gap: 2px; min-width: 90px; }\n.invoml-header-meta-label {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 1px;\n color: var(--invoml-color-muted);\n font-weight: 500;\n}\n.invoml-header-meta-value { color: var(--invoml-color-text); font-weight: 500; font-size: 13px; }\n\n/* Party blocks */\n.invoml-party {\n font-size: 14px;\n line-height: var(--invoml-line-height);\n margin-bottom: var(--invoml-section-gap);\n vertical-align: top;\n}\n.invoml-party-label {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n color: var(--invoml-color-muted);\n margin-bottom: 8px;\n font-weight: 500;\n}\n.invoml-party-name {\n font-weight: 600;\n color: var(--invoml-color-text);\n font-size: 15px;\n margin-bottom: 6px;\n}\n.invoml-party-details { color: var(--invoml-color-muted); font-size: 13px; line-height: var(--invoml-line-height); }\n.invoml-party-details > div { margin: 2px 0; }\n\n/* Items table */\n.invoml-items { margin: var(--invoml-totals-margin) 0; }\n.invoml-items th {\n padding: var(--invoml-table-row-padding) 0;\n border-bottom: 1px solid var(--invoml-color-border);\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 1px;\n color: var(--invoml-color-muted);\n font-weight: 500;\n}\n.invoml-items td {\n padding: var(--invoml-table-row-padding) 0;\n border-bottom: 1px solid color-mix(in srgb, var(--invoml-color-border) 50%, transparent);\n font-size: 14px;\n color: var(--invoml-color-text);\n}\n.invoml-items .col-right { text-align: right; padding-right: 0; padding-left: 12px; }\n\n/* Totals */\n.invoml-totals { display: flex; justify-content: flex-end; margin-top: var(--invoml-totals-margin); }\n.invoml-totals-inner { width: 300px; }\n.invoml-totals-row {\n display: flex;\n justify-content: space-between;\n padding: 5px 0;\n font-size: 14px;\n color: var(--invoml-color-text);\n}\n.invoml-totals-row.is-grand {\n border-top: 2px solid var(--invoml-color-text);\n margin-top: 8px;\n padding-top: 10px;\n font-size: 15px;\n font-weight: 600;\n}\n.invoml-totals-row.is-amount-due {\n border-top: 1px solid var(--invoml-color-border);\n margin-top: 4px;\n padding-top: 8px;\n font-weight: 600;\n}\n.invoml-totals-label { color: var(--invoml-color-muted); }\n.invoml-totals-label.is-bold { color: var(--invoml-color-text); font-weight: 600; }\n.invoml-totals-amount { font-variant-numeric: tabular-nums; }\n\n/* Payment block */\n.invoml-payment {\n margin-top: var(--invoml-payment-margin-top);\n padding-top: 24px;\n border-top: 1px solid var(--invoml-color-border);\n}\n.invoml-payment-title {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n color: var(--invoml-color-muted);\n margin-bottom: 12px;\n font-weight: 500;\n}\n.invoml-payment-details { font-size: 14px; line-height: var(--invoml-line-height); color: var(--invoml-color-muted); }\n.invoml-payment-details strong { color: var(--invoml-color-text); font-weight: 600; }\n\n/* Notes block */\n.invoml-notes {\n margin-top: var(--invoml-section-gap);\n padding-top: var(--invoml-section-gap);\n border-top: 1px solid var(--invoml-color-border);\n font-size: 13px;\n color: var(--invoml-color-muted);\n}\n\n/* Section block */\n.invoml-section { margin: var(--invoml-section-gap) 0; }\n.invoml-section-title {\n font-size: 13px;\n font-weight: 600;\n color: var(--invoml-color-text);\n margin-bottom: 8px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n.invoml-section-content { font-size: 14px; color: var(--invoml-color-text); line-height: var(--invoml-line-height); }\n\n@media print {\n body { background: none; margin: 0; padding: 0; }\n .invoml-container { width: 100%; margin: 0; box-shadow: none; }\n}\n";
3
3
  /** Per-template CSS overrides. Applied when style.template is set. */
4
4
  export declare const TEMPLATE_CSS: Record<string, string>;
@@ -220,6 +220,11 @@ body { background: #f5f5f5; }
220
220
  letter-spacing: 0.5px;
221
221
  }
222
222
  .invoml-section-content { font-size: 14px; color: var(--invoml-color-text); line-height: var(--invoml-line-height); }
223
+
224
+ @media print {
225
+ body { background: none; margin: 0; padding: 0; }
226
+ .invoml-container { width: 100%; margin: 0; box-shadow: none; }
227
+ }
223
228
  `;
224
229
  /** Per-template CSS overrides. Applied when style.template is set. */
225
230
  export const TEMPLATE_CSS = {
@@ -1,7 +1,7 @@
1
1
  export { getCurrencyDecimals } from './rounding.js';
2
2
  export { parse } from './parser.js';
3
3
  export type { ParseResult } from './parser.js';
4
- export { validateSchema } from './schema.js';
4
+ export { validateSchema, setSchema } from './schema.js';
5
5
  export type { ValidationResult } from './schema.js';
6
6
  export { calculate } from './calculator.js';
7
7
  export { toJSON, toMarkdown } from './serializer.js';
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { getCurrencyDecimals } from './rounding.js';
2
2
  export { parse } from './parser.js';
3
- export { validateSchema } from './schema.js';
3
+ export { validateSchema, setSchema } from './schema.js';
4
4
  export { calculate } from './calculator.js';
5
5
  export { toJSON, toMarkdown } from './serializer.js';
6
6
  export { toHTML } from './html-renderer.js';
@@ -1,3 +1,5 @@
1
+ /** Inject the JSON Schema directly, bypassing filesystem loading. Required for browser and edge runtimes. */
2
+ export declare function setSchema(schema: object): void;
1
3
  export interface ValidationResult {
2
4
  valid: boolean;
3
5
  errors: string[];
@@ -10,9 +10,15 @@ const srcPath = join(__dirname, '..', 'invoml-v1.0.schema.json');
10
10
  const schemaPath = existsSync(distPath) ? distPath : srcPath;
11
11
  let ajvInstance = null;
12
12
  let validateFn = null;
13
+ let injectedSchema = null;
14
+ /** Inject the JSON Schema directly, bypassing filesystem loading. Required for browser and edge runtimes. */
15
+ export function setSchema(schema) {
16
+ injectedSchema = schema;
17
+ validateFn = null;
18
+ }
13
19
  function getValidator() {
14
20
  if (!validateFn) {
15
- const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
21
+ const schema = injectedSchema ?? JSON.parse(readFileSync(schemaPath, 'utf8'));
16
22
  ajvInstance = new Ajv({ allErrors: true, strict: false, validateFormats: true });
17
23
  addFormats(ajvInstance);
18
24
  validateFn = ajvInstance.compile(schema);
@@ -8,6 +8,7 @@ export interface InvoMLDocument {
8
8
  payment?: InvoMLPayment;
9
9
  sections?: Record<string, InvoMLSection>;
10
10
  notes?: string;
11
+ prepaidAmount?: number;
11
12
  totals?: InvoMLTotals;
12
13
  style?: InvoMLStyle;
13
14
  }
@@ -18,7 +19,7 @@ export interface InvoMLStyle {
18
19
  blocks?: Record<string, Record<string, string>>;
19
20
  }
20
21
  export interface InvoMLMeta {
21
- documentType: 'invoice' | 'quote' | 'credit_note' | 'receipt';
22
+ documentType: 'invoice' | 'quote' | 'credit_note' | 'receipt' | 'estimate';
22
23
  number: string;
23
24
  issueDate: string;
24
25
  currency: string;
@@ -5,18 +5,20 @@
5
5
  "description": "Invoice Markup Language v1.0 — compact invoice format for AI structured output",
6
6
  "type": "object",
7
7
  "required": ["$invoml", "meta", "items"],
8
- "properties": {
8
+ "additionalProperties": false,
9
+ "properties": {
9
10
  "$invoml": { "type": "string", "const": "1.0", "description": "InvoML specification version. Must be \"1.0\"." },
10
11
  "meta": {
11
12
  "type": "object",
12
13
  "description": "Document metadata — type, number, dates, currency, and tax configuration.",
13
14
  "required": ["documentType", "number", "issueDate", "currency"],
14
- "properties": {
15
- "documentType": { "type": "string", "enum": ["invoice", "quote", "credit_note", "receipt"], "description": "The type of commercial document." },
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "documentType": { "type": "string", "enum": ["invoice", "quote", "credit_note", "receipt", "estimate"], "description": "The type of commercial document." },
16
18
  "number": { "type": "string", "minLength": 1, "description": "Unique document identifier (e.g. INV-2026-001)." },
17
- "issueDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "Date the document was issued (YYYY-MM-DD)." },
18
- "dueDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "Payment due date (YYYY-MM-DD). Optional." },
19
- "expiryDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "Expiry date for quotes and estimates (YYYY-MM-DD). Optional." },
19
+ "issueDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "format": "date", "description": "Date the document was issued (YYYY-MM-DD)." },
20
+ "dueDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "format": "date", "description": "Payment due date (YYYY-MM-DD). Optional." },
21
+ "expiryDate": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "format": "date", "description": "Expiry date for quotes and estimates (YYYY-MM-DD). Optional." },
20
22
  "currency": { "type": "string", "pattern": "^[A-Z]{3}$", "description": "ISO 4217 currency code (e.g. USD, EUR, GBP)." },
21
23
  "locale": { "type": "string", "description": "BCP 47 locale tag for number/date formatting (e.g. en-US, de-DE). Optional." },
22
24
  "reference": { "type": "string", "description": "External reference such as a purchase order number. Optional." },
@@ -28,7 +30,8 @@
28
30
  "type": "object",
29
31
  "description": "Simple tax — a single label and rate applied to all items.",
30
32
  "required": ["label", "rate"],
31
- "properties": {
33
+ "additionalProperties": false,
34
+ "properties": {
32
35
  "label": { "type": "string", "minLength": 1, "description": "Display name for the tax (e.g. VAT, GST, Sales Tax)." },
33
36
  "rate": { "type": "number", "description": "Tax rate as a percentage (e.g. 10 for 10%)." },
34
37
  "inclusive": { "type": "boolean", "description": "If true, item prices already include this tax." }
@@ -38,7 +41,8 @@
38
41
  "type": "object",
39
42
  "description": "Full tax — multiple categories with individual rates and behaviors.",
40
43
  "required": ["categories"],
41
- "properties": {
44
+ "additionalProperties": false,
45
+ "properties": {
42
46
  "system": { "type": "string", "description": "Name of the tax system (e.g. GST, VAT). For display purposes." },
43
47
  "compound": { "type": "boolean", "description": "If true, all tax categories apply to the full base (not split by line item category)." },
44
48
  "inclusive": { "type": "boolean", "description": "If true, item prices already include tax. Tax is backed out during calculation." },
@@ -50,7 +54,8 @@
50
54
  "type": "object",
51
55
  "description": "A single tax category with its rate and behavior flags.",
52
56
  "required": ["id", "label", "rate"],
53
- "properties": {
57
+ "additionalProperties": false,
58
+ "properties": {
54
59
  "id": { "type": "string", "minLength": 1, "description": "Unique identifier for this category. Referenced by items via taxCategory." },
55
60
  "label": { "type": "string", "minLength": 1, "description": "Display name (e.g. Standard Rate, Reduced Rate, Exempt)." },
56
61
  "rate": { "type": "number", "description": "Tax rate as a percentage." },
@@ -73,7 +78,8 @@
73
78
  "from": {
74
79
  "type": "object",
75
80
  "description": "The issuer (seller, freelancer, service provider). Use content for free-form Markdown or structured fields.",
76
- "properties": {
81
+ "additionalProperties": false,
82
+ "properties": {
77
83
  "content": { "type": "string", "description": "Free-form text with Markdown formatting (bold, italic, lists, links). Contains the full party display text. Example: '**Acme Corp**\\n123 Main St\\nNew York, NY 10001'. If provided, structured fields below are ignored for rendering." },
78
84
  "name": { "type": "string", "description": "Legal or business name." },
79
85
  "address": { "type": "string", "description": "Full postal address." },
@@ -89,7 +95,8 @@
89
95
  "to": {
90
96
  "type": "object",
91
97
  "description": "The recipient (buyer, client, customer).",
92
- "properties": {
98
+ "additionalProperties": false,
99
+ "properties": {
93
100
  "content": { "type": "string", "description": "Free-form text with Markdown formatting (bold, italic, lists, links). Contains the full party display text. Example: '**Client LLC**\\n456 Oak Ave\\nSan Francisco, CA 94102'. If provided, structured fields below are ignored for rendering." },
94
101
  "name": { "type": "string", "description": "Legal or business name." },
95
102
  "address": { "type": "string", "description": "Full postal address." },
@@ -110,7 +117,8 @@
110
117
  "type": "object",
111
118
  "description": "A single line item.",
112
119
  "required": ["description", "quantity", "unitPrice"],
113
- "properties": {
120
+ "additionalProperties": false,
121
+ "properties": {
114
122
  "description": { "type": "string", "minLength": 1, "description": "Description of the good or service." },
115
123
  "quantity": { "type": "number", "description": "Quantity (e.g. 10 hours, 3 units)." },
116
124
  "unitPrice": { "type": "number", "description": "Price per unit before discounts." },
@@ -123,7 +131,8 @@
123
131
  "type": "object",
124
132
  "description": "Structured discount with explicit type and value.",
125
133
  "required": ["type", "value"],
126
- "properties": {
134
+ "additionalProperties": false,
135
+ "properties": {
127
136
  "type": { "type": "string", "enum": ["percentage", "fixed"], "description": "Whether the discount is a percentage of the line total or a fixed amount." },
128
137
  "value": { "type": "number", "description": "The discount value (percentage points or currency amount)." }
129
138
  }
@@ -143,7 +152,8 @@
143
152
  "type": "object",
144
153
  "description": "An invoice-level discount.",
145
154
  "required": ["type", "value"],
146
- "properties": {
155
+ "additionalProperties": false,
156
+ "properties": {
147
157
  "type": { "type": "string", "enum": ["percentage", "fixed"], "description": "Whether the discount is a percentage or a fixed amount." },
148
158
  "value": { "type": "number", "description": "The discount value." },
149
159
  "label": { "type": "string", "description": "Display label (e.g. Early Payment Discount, Loyalty Discount). Optional." }
@@ -153,7 +163,8 @@
153
163
  "payment": {
154
164
  "type": "object",
155
165
  "description": "Payment instructions. Use content for free-form Markdown or structured fields for specific payment methods.",
156
- "properties": {
166
+ "additionalProperties": false,
167
+ "properties": {
157
168
  "title": { "type": "string", "description": "Custom title for the payment section." },
158
169
  "content": { "type": "string", "description": "Free-form Markdown payment instructions. If provided, structured fields are ignored in rendering." },
159
170
  "method": { "type": "string", "enum": ["bank-international", "bank-domestic", "crypto", "card", "other"], "description": "Payment method type." },
@@ -175,7 +186,8 @@
175
186
  "type": "object",
176
187
  "description": "A custom content section.",
177
188
  "required": ["title", "content"],
178
- "properties": {
189
+ "additionalProperties": false,
190
+ "properties": {
179
191
  "title": { "type": "string", "description": "Section heading." },
180
192
  "content": { "type": "string", "description": "Section body in Markdown." }
181
193
  }
@@ -184,6 +196,7 @@
184
196
  "additionalProperties": false
185
197
  },
186
198
  "notes": { "type": "string", "description": "Free-form notes displayed at the bottom of the document." },
199
+ "prepaidAmount": { "type": "number", "description": "Amount already paid or deposited. Deducted from total to compute amountDue. The runtime reads this field; AI models MAY set it." },
187
200
  "style": {
188
201
  "type": "object",
189
202
  "description": "Presentation hints for renderers. Controls block ordering, visual arrangement, and appearance. Machine consumers MUST ignore this field.",
@@ -215,13 +228,15 @@
215
228
  "totals": {
216
229
  "type": "object",
217
230
  "description": "Computed totals. Populated by the runtime after calculation. AI models should NOT set these fields.",
218
- "properties": {
231
+ "additionalProperties": false,
232
+ "properties": {
219
233
  "subtotal": { "type": "number", "description": "Sum of all line item amounts before invoice-level discounts." },
220
234
  "discountDetails": {
221
235
  "type": "array",
222
236
  "description": "Breakdown of each invoice-level discount applied.",
223
237
  "items": {
224
238
  "type": "object",
239
+ "additionalProperties": false,
225
240
  "properties": {
226
241
  "label": { "type": "string", "description": "Discount label." },
227
242
  "amount": { "type": "number", "description": "Discount amount in currency." }
@@ -234,6 +249,7 @@
234
249
  "description": "Per-category tax breakdown.",
235
250
  "items": {
236
251
  "type": "object",
252
+ "additionalProperties": false,
237
253
  "properties": {
238
254
  "category": { "type": "string", "description": "Tax category ID." },
239
255
  "label": { "type": "string", "description": "Tax category display label." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invompt/invoml",
3
- "version": "1.0.0-alpha.6",
3
+ "version": "1.0.0-alpha.7",
4
4
  "description": "InvoML — Invoice Markup Language: parse, validate, calculate",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -22,6 +22,10 @@
22
22
  "types": "./dist/src/html-css.d.ts",
23
23
  "import": "./dist/src/html-css.js"
24
24
  },
25
+ "./types": {
26
+ "types": "./dist/src/types.d.ts",
27
+ "import": "./dist/src/types.js"
28
+ },
25
29
  "./invoml-v1.0.schema.json": "./invoml-v1.0.schema.json",
26
30
  "./package.json": "./package.json"
27
31
  },