Skip to main content

Core API

TypeScript/JavaScript API reference for @dotit/core.

npm install @dotit/core

Current version: 3.0.0. (Formerly published as @intenttext/core — those packages are deprecated with pointers; same code, same format.)

Parser

parseIntentText(source, options?)

Parse .it source into a document object.

import { parseIntentText } from "@dotit/core";

const doc = parseIntentText(`
title: Quarterly Report
meta: | author: Finance Team | date: 2026-03-31 | period: Q1

section: Revenue
Revenue grew 12% year-over-year.
metric: Revenue | value: $4.2M | target: $4.0M | status: above
`);

console.log(doc.metadata.title); // "Quarterly Report"
console.log(doc.blocks.length); // 5

Options:

interface ParseOptions {
extensions?: IntentExtension[]; // Custom block/inline parsers
includeHistorySection?: boolean; // Parse trust history (default: false)
}

Escaping: | (space-pipe-space) is the reserved property delimiter. Write a literal pipe as \| and a literal backslash as \\ — the parser unescapes them anywhere in content and property values, and documentToSource re-escapes on output, so escape round-trips are a fixpoint. Colons need no escaping inside content (quote: He said: watch this is fine).

parseIntentTextSafe(source, options?)

Like parseIntentText but collects errors instead of throwing.

import { parseIntentTextSafe } from "@dotit/core";

const result = parseIntentTextSafe(source);
if (result.errors.length > 0) {
console.error(result.errors);
} else {
const doc = result.document;
}

Renderer

renderHTML(document, options?)

Render to themed HTML.

import { parseIntentText, renderHTML } from "@dotit/core";

const doc = parseIntentText(source);
const html = renderHTML(doc, { theme: "corporate" });

renderPrint(document, options?)

Print-optimized HTML with @page rules, headers, footers, and watermarks.

import { renderPrint } from "@dotit/core";

const printHtml = renderPrint(doc, { theme: "print" });

collectPrintLayout(document)

Extract page, header, footer, and watermark blocks.

import { collectPrintLayout } from "@dotit/core";

const layout = collectPrintLayout(doc);
// layout.page, layout.header, layout.footer, layout.watermark, layout.breaks

Options:

interface RenderOptions {
theme?: string | IntentTheme; // Theme name or object
}

Theme

getBuiltinTheme(name)

import { getBuiltinTheme, listBuiltinThemes } from "@dotit/core";

const theme = getBuiltinTheme("corporate");
const names = listBuiltinThemes();
// ['corporate', 'minimal', 'warm', 'technical', 'print', 'legal', 'editorial', 'dark']

generateThemeCSS(theme, mode?)

Generate CSS custom properties from a theme object.

import { generateThemeCSS } from "@dotit/core";

const css = generateThemeCSS(theme, "web"); // or 'print'

registerBuiltinTheme(theme)

Register a custom theme for use by name.

import { registerBuiltinTheme } from "@dotit/core";

registerBuiltinTheme({
name: "brand",
version: "1.0",
fonts: {
body: "Georgia",
heading: "Georgia",
mono: "Courier",
size: "11pt",
leading: "1.6",
},
colors: {
text: "#1a1a1a",
heading: "#003366",
muted: "#666",
accent: "#003366",
border: "#ccc",
background: "#fff",
"code-bg": "#f5f5f5",
},
spacing: {
"page-margin": "1in",
"section-gap": "2rem",
"block-gap": "0.75rem",
indent: "0",
},
});

IntentTheme

interface IntentTheme {
name: string;
version: string;
description?: string;
author?: string;
fonts: ThemeFonts;
colors: ThemeColors;
spacing: ThemeSpacing;
blocks?: Record<string, Record<string, string | boolean>>;
print?: ThemePrint;
}

interface ThemeFonts {
body: string;
heading: string;
mono: string;
size: string;
leading: string;
}

interface ThemeColors {
text: string;
heading: string;
muted: string;
accent: string;
border: string;
background: string;
"code-bg": string;
"trust-approved"?: string;
"trust-signed"?: string;
"trust-frozen"?: string;
"trust-warning"?: string;
watermark?: string;
}

interface ThemeSpacing {
"page-margin": string;
"section-gap": string;
"block-gap": string;
indent: string;
}

interface ThemePrint {
"header-font-size"?: string;
"footer-font-size"?: string;
"header-color"?: string;
"footer-color"?: string;
}

Query

queryBlocks(document, options)

Execute a structured query against document blocks.

import { parseIntentText, queryBlocks } from "@dotit/core";

const doc = parseIntentText(source);
const result = queryBlocks(doc, "type=deadline sort:date:asc limit:10");
// result.blocks, result.total, result.matched

parseQuery(queryString)

Parse query syntax into a QueryOptions object.

import { parseQuery } from "@dotit/core";

const opts = parseQuery(
"type=task owner=Ahmed due<2026-03-01 sort:due:asc limit:10",
);

queryDocument(document, query?)

Simple, intuitive filter API. All conditions are ANDed; type arrays are ORed. Returns matching blocks (never mutates the document).

import { queryDocument } from "@dotit/core";

const tasks = queryDocument(doc, { type: "step" });
const urgent = queryDocument(doc, {
type: ["task", "deadline"],
section: "Scope",
properties: { priority: "high" },
limit: 10,
});
interface SimpleQueryOptions {
type?: string | string[];
content?: string | RegExp; // case-insensitive substring or regex
properties?: Record<string, string | RegExp>;
section?: string | RegExp;
limit?: number;
}

formatQueryResult(result, format?)

Format query results as "simple" text (default), "table", or "json".

import { formatQueryResult } from "@dotit/core";

const table = formatQueryResult(result, "table");
const json = formatQueryResult(result, "json");

Query types

interface QueryOptions {
where?: QueryClause[];
sort?: QuerySort[];
limit?: number;
offset?: number;
}

interface QueryClause {
field: string;
operator:
| "="
| "!="
| "<"
| ">"
| "<="
| ">="
| "contains"
| "startsWith"
| "exists";
value?: string | number | boolean;
}

interface QuerySort {
field: string;
direction: "asc" | "desc";
}

interface QueryResult {
blocks: IntentBlock[];
total: number;
matched: number;
}

readTypedValue(raw, unit?) / metricTypedValue(block)

Interpret a raw value (or a metric: block's value) into a typed object — the number, unit, currency, and a kind (money / percent / quantity / number / text). Use this to do math on totals or to sort/format metric: values without re-parsing strings.

import { readTypedValue, metricTypedValue, parseNumericValue } from "@dotit/core";

readTypedValue("17,325 QAR");
// { raw: "17,325 QAR", number: 17325, unit: null, currency: "QAR", kind: "money" }

const m = parseIntentText("metric: Margin | value: 18%").blocks[0];
metricTypedValue(m); // { raw: "18%", number: 18, unit: "%", currency: null, kind: "percent" }

parseNumericValue("1,250.50"); // 1250.5

Merge

mergeData(document, data, options?)

Resolve {{variable}} interpolations (dot paths, array indices, each: table loops).

import { parseIntentText, mergeData } from "@dotit/core";

const template = parseIntentText(`
title: Invoice {{number}}
Amount due: {{amount}}
`);

const merged = mergeData(template, { number: "INV-2847", amount: "$5,000" });

mergeData resolves {{vars}} inside multi-line prose blocks too, so a fully merged template (no placeholders left) can be sealed.

parseAndMerge(source, data, options?)

Parse and merge in one call. options.missing controls unresolved {{fields}}: "keep" (default — shows the marker, good while authoring) or "blank" (renders empty — use for finished documents so an invoice never prints {{customer.phone}}).

import { parseAndMerge } from "@dotit/core";

const doc = parseAndMerge(templateSource, invoiceData, { missing: "blank" });

Document styles

collectDocumentStyles(document) / documentStyleCSS(document, selectorMap?, prefix?)

Read a document's style: rules (sanitized; unknown targets dropped) and build the CSS for them. renderHTML/renderPrint call this automatically; pass a custom selectorMap to apply the same rules to your own markup (the web editor does exactly this for its live canvas).

import { collectDocumentStyles, documentStyleCSS } from "@dotit/core";

collectDocumentStyles(doc);
// [{ target: "section", declarations: "color: #0a7; font-weight: 600" }]

Trust

A seal is a SHA-256 hash over the document's CONTENT under a recorded spec: version (currently SEAL_SPEC = 4). v4 excludes styling + comments and normalizes line endings (CRLF/CR → LF) + trailing whitespace — so restyling, reformatting, a CRLF transform, or a trailing-whitespace re-save never break a seal; only a real content change does. The seal is versioned: every sign:/freeze: line stamps the spec: that produced its hash, and verification applies exactly that version forever, so a future rule change can never silently break a historical seal. A v4 seal also records a separate appearance: hash that flags a hidden-content restyle (e.g. opacity: 0) without invalidating the seal — content is intact, but verifyDocument sets appearanceChanged. Preserving exact bytes is storage hygiene, not what the seal enforces. See SPEC §4.

sealDocument(source, options)

Seal a document: appends a sign: line (optional) and a freeze: line carrying the SHA-256 content hash, the appearance: hash, and spec: 4. Returns the updated source — store it as returned (re-serializing through a model on save is fine because the seal hashes content, not bytes; but a clean store is still the safest default).

import { sealDocument } from "@dotit/core";

const result = sealDocument(source, { signer: "Ahmed Al-Rashid", role: "CEO" });
// result.success, result.hash ("sha256:…"), result.source (sealed text), result.at

verifyDocument(source)

Verify document integrity against its seal. Multi-sign aware: each signer is reported with signedCurrentVersion (did their signature match the current content?), plus the recorded spec and whether it is specOutdated (an older, weaker ruleset). A signer who signed an earlier version is reported as such, not as a blanket failure.

import { verifyDocument } from "@dotit/core";

const result = verifyDocument(source);
// result.intact, result.hash, result.frozen, result.spec, result.specOutdated,
// result.appearanceChanged, result.signers

signDocument(source, options) / unsealDocument(source)

signDocument appends a sign: line carrying the content-scope hash and spec: 4 (identity-bound, no freeze:) — use it when a party signs but isn't the one locking the document. unsealDocument strips the freeze: line (and is a no-op on an unsealed document), so a sealed draft can be reopened for editing and then re-sealed.

import { signDocument, unsealDocument } from "@dotit/core";

const signed = signDocument(source, { signer: "Ahmed", role: "CEO" }); // { success, hash, source, at }
const reopened = unsealDocument(sealedSource); // freeze: removed

computeDocumentHash(source) / computeSignatureHash(source, signer)

Compute the SHA-256 hash of document content (above the history boundary). The two scopes: computeDocumentHash is the seal scope (content + signatures + freeze metadata); computeSignatureHash is the content scope for one signer (also binds their identity).

import { computeDocumentHash, computeSignatureHash } from "@dotit/core";

const sealHash = computeDocumentHash(source);
// "sha256:a1b2c3..."
const sigHash = computeSignatureHash(source, { signer: "Ahmed", role: "CEO", at });

signatureIdentity(line) / signatureMatchesContent(source, signer)

signatureIdentity extracts the bound { signer, role, at } from a sign: line; signatureMatchesContent checks whether a signer's hash still matches the current content (the per-signer integrity check behind verifyDocument).

Integrity-gated trust band — renderTrustBand, TRUST_BAND_CSS, trustBandPositionCss

renderTrustBand(source, options?) renders the on-document trust band (sealed, signed, broken, …). It verifies before it draws: a tampered document renders a red "SEAL BROKEN" stamp on every surface (screen, print, PDF) — never a clean seal. Pair with TRUST_BAND_CSS (the stylesheet) and trustBandPositionCss (placement). Tiers include the new sealed (indigo) and broken (red).

import { renderTrustBand, TRUST_BAND_CSS, trustBandPositionCss } from "@dotit/core";

const bandHtml = renderTrustBand(source); // "SEAL BROKEN" if the content was tampered

computeTrustDiff(before, after)

Compute semantic diff for trust history writing.

import { computeTrustDiff } from "@dotit/core";

const diff = computeTrustDiff(oldDoc, newDoc);
// diff.added, diff.removed, diff.modified, diff.moved, diff.unchanged

Trust types

interface SealOptions {
signer: string;
role?: string;
skipSign?: boolean; // freeze without adding a sign: line
}

interface SealResult {
success: boolean;
hash: string; // "sha256:…" — the content hash (not a hash of the raw bytes)
source: string; // the sealed text
at: string;
error?: string;
}

interface VerifyResult {
intact: boolean;
frozen: boolean;
frozenAt?: string;
appearanceChanged?: boolean; // content intact, but styling changed since sealing (possible hidden-content restyle)
spec?: number; // the recorded seal ruleset (currently 4)
specOutdated?: boolean; // true if the seal predates the current SEAL_SPEC
signers?: Array<{
signer: string;
role?: string;
at: string;
valid: boolean;
signedCurrentVersion: boolean;
spec?: number;
specOutdated?: boolean;
}>;
hash?: string;
expectedHash?: string;
error?: string;
warning?: string;
}

interface TrustDiff {
added: BlockSnapshot[];
removed: BlockSnapshot[];
modified: BlockSnapshot[];
moved: BlockSnapshot[];
unchanged: BlockSnapshot[];
}

History

updateHistory(previousSource, currentSource, options)

Compute diff between versions and write history section.

import { updateHistory } from "@dotit/core";

const updated = updateHistory(oldSource, newSource, { by: "Ahmed" });

parseHistorySection(raw)

Parse a history section string into structured data.

import { parseHistorySection } from "@dotit/core";

const { registry, revisions, registryIntact } =
parseHistorySection(historyText);

Index

buildShallowIndex(folder, files, coreVersion)

Build a shallow .it-index for a folder.

import { buildShallowIndex } from "@dotit/core";

const index = buildShallowIndex("./contracts", filesMap, "1.0.0");

buildIndexEntry(document, source, modifiedAt)

Extract metadata and block summaries for a single file.

import { buildIndexEntry } from "@dotit/core";

const entry = buildIndexEntry(doc, source, "2026-03-15T10:00:00Z");

composeIndexes(indexes)

Merge multiple folder indexes into a flat result list.

import { composeIndexes, queryComposed } from "@dotit/core";

const all = composeIndexes([contractsIndex, invoicesIndex]);
const deadlines = queryComposed(all, parseQuery("type=deadline"));

checkStaleness(index, document, source)

Check if an index entry needs refresh.

updateIndex(existing, filename, document, source, modifiedAt)

Update or add a single entry in an existing index.

Index types

interface ItIndex {
version: "1";
scope: "shallow";
folder: string;
built_at: string;
core_version: string;
files: Record<string, IndexFileEntry>;
}

interface IndexFileEntry {
hash: string;
modified_at: string;
metadata: { title?: string; type?: string; domain?: string; track?: boolean };
blocks: IndexBlockEntry[];
}

interface IndexBlockEntry {
type: string;
content: string;
section?: string;
properties: Record<string, string | number>;
}

interface ComposedResult {
file: string;
block: IndexBlockEntry;
}

Diff

diffDocuments(before, after)

Semantic diff between two parsed documents.

import { diffDocuments } from "@dotit/core";

const diff = diffDocuments(oldDoc, newDoc);
// diff.added, diff.removed, diff.modified, diff.unchanged
// diff.summary — "2 added, 1 removed, 3 modified, 10 unchanged"

compareVersions(before, after, options?) / mergeThreeWay(base, mine, theirs, options?)

compareVersions produces a redline .it document (track-change spans) between two versions of a document's source — the same diff the editor's "Review changes" view and the <Redline> viewer render. mergeThreeWay does a 3-way (base / mine / theirs) merge for async co-authoring, marking conflicts as track-change regions.

import { compareVersions, mergeThreeWay } from "@dotit/core";

const redlineSource = compareVersions(oldSource, newSource, { by: "Sarah" });

const { source, conflicts } = mergeThreeWay(baseSource, mineSource, theirsSource);
// conflicts: number of unresolved regions (0 = clean merge)

Workflow

executeWorkflow(document, runtime)

Run a workflow document against a runtime. Policy enforcement and gate checks are applied before execution begins.

import { executeWorkflow } from "@dotit/core";

const result = await executeWorkflow(doc, {
executeStep: async (block) => {
// dispatch block to your agent/tool
return { status: "completed", output: "Done" };
},
evaluateDecision: async (block) => {
return { branch: "yes" };
},
checkGate: async (block) => {
return { passed: true };
},
dryRun: false,
});

console.log(result.status); // "completed"
console.log(result.executedSteps); // ["step-1", "step-2", ...]

Return value:

interface ExecutionResult {
status: "completed" | "gate_blocked" | "policy_blocked" | "error" | "dry_run";
executedSteps: string[];
skippedSteps: string[];
blockingGate?: string;
blockingPolicy?: string;
error?: string;
dryRunPlan?: string[];
}

Status reference:

StatusMeaning
completedAll steps executed successfully
gate_blockedA gate: check returned passed: false — execution halted at that gate
policy_blockedA policy: block's requires: gate was not satisfied before execution
errorA step threw an unhandled exception
dry_runRuntime dryRun: true — plan returned without execution

Runtime interface:

interface WorkflowRuntime {
executeStep: (block: IntentBlock) => Promise<StepResult>;
evaluateDecision?: (block: IntentBlock) => Promise<DecisionResult>;
checkGate?: (block: IntentBlock) => Promise<GateResult>;
dryRun?: boolean;
}

interface StepResult {
status: "completed" | "error";
output?: string;
error?: string;
}

interface GateResult {
passed: boolean;
reason?: string;
}

interface DecisionResult {
branch: string;
}

Policy enforcement:

Before the execution loop starts, executeWorkflow checks every policy: block for requires: gate. If the required gate: block has not passed, the function returns immediately with status: "policy_blocked" and does not execute any steps.

// policy enforcement example
const doc = parseIntentText(`
policy: | requires: gate | gate: security-review
step: Deploy to production
`);

const result = await executeWorkflow(doc, runtime);
// result.status === "policy_blocked" if security-review gate is unmet
// result.blockingPolicy === "policy-id"

extractWorkflow(document)

Extract a task DAG from step/gate/decision blocks without executing.

import { extractWorkflow } from "@dotit/core";

const graph = extractWorkflow(doc);
// graph.entryPoints, graph.steps, graph.executionOrder, graph.gatePositions
interface WorkflowGraph {
entryPoints: string[];
steps: Record<string, WorkflowStep>;
executionOrder: string[][]; // Topologically sorted layers
gatePositions: number[];
hasTerminal: boolean;
warnings: string[];
}

interface WorkflowStep {
block: IntentBlock;
dependsOn: string[];
dependedOnBy: string[];
isGate: boolean;
isTerminal: boolean;
isParallel: boolean;
}

Source

documentToSource(document)

Convert a parsed document back to .it source text.

import { documentToSource } from "@dotit/core";

const source = documentToSource(doc);

Localized keywords round-trip as written. When a line used one of the 32 Arabic keyword names (عنوانtitle, مهمةtask, صفrow, توقيعsign, …) the parser records the keyword as written on block.keywordAlias, and documentToSource re-emits that form instead of normalizing to the canonical English keyword. An Arabic document stays Arabic and a sealed document keeps its hash through a parse → serialize cycle. Table keywords (أعمدة/صف, headers) are preserved the same way. (There are no Latin synonym aliases — a word like abstract: is not reserved, so it parses as your own custom block and round-trips verbatim, never silently rewritten.) Reserved characters are re-escaped on output (\|, \\), so escape round-trips are stable too.

reconcileEdit(originalSource, editedSource)

Source-preserving edit reconciliation: returns the edited document with every unchanged block restored to its exact original bytes. For each edited block, if a semantically identical block (same type, content, properties, children) existed in the original, the original's bytes are kept; only genuinely new or changed blocks take the edited serialization. A no-op edit round-trips byte-for-byte, so a sealed document keeps its hash.

import { reconcileEdit } from "@dotit/core";

const saved = reconcileEdit(originalSource, editorOutput);
reconcileEdit(sealedSource, sealedSource) === sealedSource; // true (no-op preserves bytes)

Use this whenever you accept edited .it text from a model-based editor. See Byte Preservation.

effectiveProperties(block) / effectiveField(block, field) / defaultFor(type, field)

The parser is a faithful recorder: it stores only what the author wrote, so the model round-trips byte-for-byte. Block-type defaults (a step:'s status: pending, a done:'s status: done, a bare toc:'s depth/title, an image at: read as src:) are applied at read time, not baked into block.properties. Use these when you need a block's interpreted values:

import { effectiveProperties, effectiveField } from "@dotit/core";

const step = parseIntentText("step: Verify").blocks[0];
step.properties?.status; // undefined — not stored (bytes stay exact)
effectiveField(step, "status"); // "pending" — the read-time default
effectiveProperties(step).status; // "pending"

The renderer, query engine, and index already use these, so rendering and status=done queries behave exactly as before — only the stored model changed. See Byte Preservation.

Approval routing & audit chain

In-file approval workflow — the policy, live state, and a tamper-evident trail all live in the document. See Approval Workflows for the full guide.

workflowState(source) / extractRoute(document)

Derive the live approval state from a document's route:/require: policy and its approve: lines. Nothing is stored — re-deriving always matches the file.

import { workflowState, extractRoute } from "@dotit/core";

const state = workflowState(source);
// { hasRoute, order: "sequential"|"parallel", required, active,
// fulfilled: string[], pending: string[], next: string|null, complete: boolean }

if (!state.complete) throw new Error(`Awaiting: ${state.pending.join(", ")}`);

extractRoute(parseIntentText(source)); // { order, required } | null — the raw policy

require: supports | when: <condition> (conditional on the document's own metric:/meta: values) and | optional: yes. A document with no policy returns complete: true.

appendApproval(source, options) / verifyAuditChain(source) / auditTrail(source)

Append a hash-chained approval (carries prev: = hash of the preceding event, so the approval order is tamper-evident) and verify the chain. Use appendApproval before sealing — approve: is part of the hashed body.

import { appendApproval, verifyAuditChain, auditTrail } from "@dotit/core";

let src = appendApproval(doc, { by: "Sarah Chen", role: "manager", note: "Reviewed" });
src = appendApproval(src, { by: "James Miller", role: "finance" });

verifyAuditChain(src);
// { valid: true, length: 2, chained: 2 }
// on tamper → { valid: false, brokenAt: 1, reason: "audit link 1 …", … }

auditTrail(src); // ordered AuditEvent[] — kind: approve|sign|freeze|amendment|revision

A plain hand-written approve: (no prev:) is a valid un-chained link, never reported as tampered.

Forms

A document with meta: type: form turns input: lines into fields a recipient fills — text, choice, date, number, signature, table, and attachment, with required:, show-if: (conditional), and compute: (computed) modifiers. See the fillable forms recipe.

isForm(source) / isFormComplete(source) / extractFormFields(source)

import { isForm, isFormComplete, extractFormFields, missingRequiredFields } from "@dotit/core";

isForm(source); // true if meta: type: form
isFormComplete(source); // true once every required field has a value
missingRequiredFields(source); // string[] of unanswered required field ids

applyAnswers(source, answers) / setFieldValue / computeFormValues

Fill a form programmatically and resolve computed fields. applyAnswers returns the updated .it source — a complete form is then signable.

import { applyAnswers, isFormComplete, computeFormValues } from "@dotit/core";

const filled = applyAnswers(blankForm, {
legal_name: "Acme",
qty: "10",
unit_price: "250",
});
computeFormValues(filled); // resolves compute: fields (e.g. qty * unit_price)
if (isFormComplete(filled)) {
/* now signable */
}

sealFormStructure(source, opts) / verifyFormStructure(source)

Seal a form's structure (its fields, not its answers) so a recipient can fill it but cannot add, remove, or re-type fields without detection — a tamper-evident form template.

import { sealFormStructure, verifyFormStructure } from "@dotit/core";

const { source, structureHash } = sealFormStructure(blankForm, { sealer: "HR" });
verifyFormStructure(source); // { sealed, intact, sealer, structureHash, expected }

submitForm(source, opts) / buildSubmission(source, opts?)

POST a completed form to an endpoint. buildSubmission builds the payload ({ source, answers?, hash, submittedAt, formId? }) without sending; submitForm sends it. requireComplete (default behavior) refuses to submit an incomplete form.

import { submitForm } from "@dotit/core";

const result = await submitForm(filledForm, {
endpoint: "https://hr.example.com/api/intake",
requireComplete: true,
});
// { ok: boolean, status: number, body?, error? }

Conversion

convertMarkdownToIntentText(markdown)

Convert Markdown → .it. Headings become title:/section:/sub:, fenced code and tables map across, and ordinary paragraphs become clean bare prose (no text: prefix — text: is added only when a line would otherwise parse as a keyword).

import { convertMarkdownToIntentText } from "@dotit/core";

const itSource = convertMarkdownToIntentText("# My Doc\n\nSome text");

convertIntentTextToMarkdown(source, opts?)

Convert .it → clean GitHub-flavored Markdown (the inverse of the above). Headings, prose, lists, task lists, tables, fenced code, blockquotes, images and links map 1:1; typed and custom blocks (metric:, clause:, sign:, …) degrade to readable **keyword:** labeled lines so no content is lost; document-metadata and print/layout directives are dropped. The shared subset round-trips: md → it → md is byte-stable.

import { convertIntentTextToMarkdown } from "@dotit/core";

const markdown = convertIntentTextToMarkdown("title: My Doc\n\nSome text");

convertHtmlToIntentText(html)

Convert HTML to .it format.

import { convertHtmlToIntentText } from "@dotit/core";

const itSource = convertHtmlToIntentText("<h1>My Doc</h1><p>Some text</p>");

Spreadsheets — convertXlsxToIntentText(data, opts?) / convertIntentTextToXlsx(source, opts?)

Round-trip .xlsx.it. convertXlsxToIntentText takes the file bytes (Uint8Array | Buffer) and emits .it source (each sheet becomes a section with a table); convertIntentTextToXlsx takes .it source and returns .xlsx bytes (Uint8Array) with each table written to its own sheet.

import { readFileSync } from "node:fs";
import {
convertXlsxToIntentText,
convertIntentTextToXlsx,
} from "@dotit/core";

const itSource = convertXlsxToIntentText(readFileSync("report.xlsx"));
const xlsxBytes = convertIntentTextToXlsx(itSource);

Word documents — convertDocxToIntentText(data, opts?) / convertIntentTextToDocx(source, opts?)

Round-trip .docx.it. convertDocxToIntentText takes the file bytes (Uint8Array | Buffer) and emits .it source; convertIntentTextToDocx takes .it source and returns .docx bytes (Uint8Array).

import { readFileSync } from "node:fs";
import {
convertDocxToIntentText,
convertIntentTextToDocx,
} from "@dotit/core";

const itSource = convertDocxToIntentText(readFileSync("contract.docx"));
const docxBytes = convertIntentTextToDocx(itSource);

All of these converters are also exposed on the CLI via dotit convert <in> <out> (extension-pair dispatch — .md/.html/.xlsx/.docx.it and .it.md/.xlsx/.docx; see CLI › Convert existing files), and the text converters (md↔it, html→it) are MCP tools (markdown_to_intenttext, intenttext_to_markdown, html_to_intenttext).

E-invoice (UBL)

buildUBLInvoice(input) / intentToUBL(source, overrides?)

Emit a UBL 2.1 invoice XML (the format behind EN 16931 / PEPPOL e-invoicing) from structured input, or directly from an .it invoice. buildUBLInvoice takes a typed object; intentToUBL derives the fields from a parsed .it document (with optional overrides).

import { buildUBLInvoice, intentToUBL } from "@dotit/core";

const xml = buildUBLInvoice({
id: "INV-2026-0042",
issueDate: "2026-06-23",
currency: "QAR",
supplier: { name: "Jadwal Technology", vatId: "…", country: "QA" },
customer: { name: "Acme Corporation", country: "QA" },
lines: [{ name: "Managed hosting", quantity: 1, unitPrice: 17325, unit: "MON" }],
taxPercent: 5,
});

const xmlFromIt = intentToUBL(invoiceSource, { currency: "QAR" });

Storage

.it is plain UTF-8 text, so it can live in a database field instead of a file. These helpers guard against a storage layer silently re-encoding or corrupting the bytes. This byte-integrity tag is distinct from the seal hash: the seal (computeDocumentHash) covers only the canonicalized content and tolerates a CRLF/whitespace transform, whereas this tag hashes the whole source byte-for-byte to catch any storage corruption — useful when you want a hard guarantee that the bytes you read are the bytes you wrote.

toStorageRecord(source) / fromStorageRecord(record) / verifyStorageRecord(record)

import {
toStorageRecord,
fromStorageRecord,
verifyStorageRecord,
} from "@dotit/core";

const record = toStorageRecord(source);
// { source, bytesSha256 } — persist both columns as-is

const intact = verifyStorageRecord(record); // boolean
const restored = fromStorageRecord(record); // throws if the bytes were altered
interface StoredDocument {
source: string; // the exact .it source — store as UTF-8 with NO normalization
bytesSha256: string; // SHA-256 (hex) of the exact source bytes, set at write time
}

Validation

validateDocument(document, schema)

Validate a document against a schema.

import { validateDocument, PREDEFINED_SCHEMAS } from "@dotit/core";

const result = validateDocument(doc, PREDEFINED_SCHEMAS["project"]);
// result.valid, result.errors, result.warnings

validateDocumentSemantic(document)

Semantic validation: cross-references, duplicate IDs, empty sections, unresolved variables.

import { validateDocumentSemantic } from "@dotit/core";

const result = validateDocumentSemantic(doc);
// result.valid, result.issues

createSchema(name, config)

Create a custom validation schema.

import { createSchema } from "@dotit/core";

const schema = createSchema("invoice", {
requiredBlocks: ["title", "note"],
blockSchemas: {
title: { type: "title", content: { required: true, minLength: 3 } },
},
});

Predefined schemas

project, meeting, article, checklist, agentic

checkConformance(input, options?)

Check a document (source string or parsed IntentDocument) against the format's conformance rules and return a structured report. Two levels: "lax" (default — only hard errors count) and "strict" (warnings also fail conformance). Useful as a gate before sealing or publishing.

import { checkConformance } from "@dotit/core";

const report = checkConformance(source, { level: "strict" });
// { conformant: boolean, level, errors: number, warnings: number, issues: SemanticIssue[] }
if (!report.conformant) console.error(report.issues);
type ConformanceLevel = "lax" | "strict";

interface ConformanceReport {
conformant: boolean;
level: ConformanceLevel;
errors: number;
warnings: number;
issues: SemanticIssue[];
}

Ask (AI Query)

askDocuments(results, question, options?)

Natural language query over indexed documents. Requires ANTHROPIC_API_KEY.

import { askDocuments, composeIndexes } from "@dotit/core";

const all = composeIndexes([index1, index2]);
const answer = await askDocuments(all, "Which contracts expire this quarter?");
interface AskOptions {
maxTokens?: number; // default: 1024
format?: "text" | "json";
}

Core types

IntentBlock

interface IntentBlock {
id: string;
type: BlockType;
keywordAlias?: string; // keyword AS WRITTEN when the line used a localized (Arabic) keyword name — re-emitted on serialize
content: string;
originalContent?: string;
properties?: Record<string, string | number>;
inline?: InlineNode[];
children?: IntentBlock[];
table?: {
headers?: string[];
rows: string[][];
headersKeyword?: string; // keyword as written for the headers line (e.g. أعمدة, headers)
rowKeyword?: string; // keyword as written for row lines (e.g. صف)
};
}

IntentDocument

interface IntentDocument {
version?: string;
blocks: IntentBlock[];
metadata?: IntentDocumentMetadata;
diagnostics?: Diagnostic[];
history?: HistorySection;
}

IntentDocumentMetadata

interface IntentDocumentMetadata {
title?: string;
summary?: string;
language?: "ltr" | "rtl";
agent?: string;
model?: string;
context?: Record<string, string>;
version?: string;
tracking?: { version: string; by: string; active: boolean };
signatures?: Array<{
signer: string;
role?: string;
at: string;
hash: string;
valid?: boolean;
}>;
freeze?: { at: string; hash: string; status: "locked" };
meta?: Record<string, string>;
}

BlockType

Union type covering all 40 canonical keywords plus extension namespace blocks. The exact list and tier split is exported as CANONICAL_KEYWORDS and KEYWORD_TIERS (so it can never drift from the docs).

Canonical (40 total), by tier (KEYWORD_TIERS):

  • Core (13): title, summary, meta, section, sub, text, info, quote, code, image, link, task, done
  • Agent (9): context, ask, step, decision, gate, trigger, result, policy, audit
  • Contract (8): track, approve, sign, freeze, amendment, certify, route, require
  • Data (3): headers, row, metric
  • Print (7): toc, page, header, footer, watermark, style, break

Notes: the table-header keyword is headers — write headers: (there are no synonym aliases anymore, so columns: is no longer reserved; it now parses as an ordinary custom block). policy and context are agent-tier (not Trust). certify/route/require are contract-tier — they power authority certification and in-file approval routing.

Boundary / extension keywords (recognized, but outside the canonical 40):

  • Forms (x-form): input and output — fillable/computed form fields. See Forms.
  • Cross-document (x-doc): attach — embed/reference another document. See Attachments.
  • Other x-ns: type extension blocks (e.g., x-agent: loop) are typed as { type: string; namespace: string } and passed through the renderer without core evaluation. See Extension keywords →.

InlineNode

type InlineNode =
| { type: "text"; value: string }
| { type: "bold"; value: string }
| { type: "italic"; value: string }
| { type: "strike"; value: string }
| { type: "highlight"; value: string }
| { type: "code"; value: string }
| { type: "inline-quote"; value: string }
| { type: "inline-note"; value: string }
| { type: "date"; value: string }
| { type: "mention"; value: string }
| { type: "tag"; value: string }
| { type: "label"; value: string }
| { type: "link"; value: string; url: string }
| { type: "footnote-ref"; value: string };

Diagnostic

interface Diagnostic {
severity: "error" | "warning";
message: string;
line: number;
column: number;
code: string;
}

ALIASES

Record mapping each localized (Arabic) keyword name to its canonical type. As of core 1.25 this is exactly the 32 Arabic keywords (عنوانtitle, قسمsection, مهمةtask, مؤشرmetric, اعتمادapprove, تجميدfreeze, …) — an Arabic document gets full canonical semantics, and the keyword serializes back exactly as written. The Latin synonym aliases that older versions shipped (note, warning, tip, columns, milestone, due, …) have been removed: there are now zero non-localized aliases, so every one of those words parses as your own custom block instead. Callout variants are set with the type: property (info: … | type: warning), not a warning: keyword. See Aliases Reference.

KEYWORDS

Array of every recognized keyword string — the 40 canonical keywords, the 32 Arabic localized keyword names, and the namespaced extension keywords (def, contact, deadline, ref, figure, input, output, font, …). It contains no synonym aliases: any word not in this array is a reliable, collision-free custom keyword of your own.

Server-side PDFs — @dotit/pdf

Core keeps a tiny runtime footprint (two small deps); real PDF bytes on a server (email attachments, compliance archiving, batch statement runs) come from the opt-in companion package:

npm i @dotit/pdf
npm i puppeteer # or: puppeteer-core + your system Chrome (CHROME_PATH)
import { issuePDF } from "@dotit/pdf";

const { source, hash, at, pdf } = await issuePDF(templateSource, data, {
signer: "Acme Billing",
});
// `source` is the SEALED .it text — store it on the record (the verifiable legal
// artifact); `pdf` is the bytes you email/archive.

Also: issueDocument() (same flow minus Chrome — returns print-ready HTML for sidecars like Gotenberg), renderPDF(), htmlToPDF(), and createPdfRenderer() for batch runs. Full guide: ERP / App Integration.

Cryptographic signatures and certification

@dotit/core's sign:/freeze: seals are integrity hashes (they prove the content is unchanged, not who signed). For real cryptographic identity — Ed25519 signatures that verify offline from the bytes alone, plus an authority certification chain — add the opt-in @dotit/sign (current 2.0.0).

npm i @dotit/sign
import {
generateSigningKey,
publicKeyFor,
signDocumentCrypto,
verifyDocumentSignatures,
certifyDocument,
verifyCertifications,
} from "@dotit/sign";

const { publicKey, privateKey } = generateSigningKey(); // store privateKey in a KMS
const signed = signDocumentCrypto(source, { signer: "Ahmed", role: "CEO", privateKey });

// verification needs NO key — each sign:/certify: line carries the public key
verifyDocumentSignatures(signed); // per-signer { signer, role, at, publicKey, valid }

// an authority (e.g. UTS) attests this exact content; verify against its published key
verifyCertifications(certifiedSource, { trustedKey });

Issuance helpers (certifyDocument, issueIntermediate, parseIntermediateCert, verifyIntermediateCert) build the authority side; the MCP server exposes the verification tools (it never holds an authority key). Full guide: Trust & Signing.

Standards-conformant PDF signatures (PAdES)

@dotit/pades (current 1.0.1) applies PAdES (PDF Advanced Electronic Signatures, ETSI standards-conformant) to a rendered PDF — X.509/ECDSA + CMS, with RFC 3161 timestamping and CA/CSR issuance for self-hosted PKI. A PDF is shown as trusted in Adobe Reader / a court portal only when it is signed under a certificate chain the verifier already trusts (a public CA, or an eIDAS QTSP for qualified signatures); the default self-signed cert is standards-conformant but shows as "identity not trusted" until you supply a trusted chain.

import { signPdf, verifyPdfSignature, requestTimestampToken } from "@dotit/pades";
// also: createCertificateAuthority, createCsr, issueCertificate, signDetachedCms, …

Native .it seals stay Ed25519; PAdES is the standards bridge for handing a signed PDF to a counterparty's Adobe/eIDAS workflow.

Math rendering

@dotit/math (current 0.1.0) renders the format's math markers — dependency-free lite MathML by default, with optional KaTeX for full fidelity.

import { renderMath, mathToMathML, renderMathInHtml, hydrateMath } from "@dotit/math";