OSS-first docs

These docs teach the open system first: contracts, generated surfaces, runtimes, governance, and incremental adoption. Studio shows up as the operating layer on top, not as the source of truth.

Build

Import flexible files with data-exchange templates

Publish one recommended import shape, then let users import CSV/JSON/XML files with partner-specific headers, skipped rows, localized values, and alternate metadata layouts.

What you'll build

  • A canonical template with target fields, aliases, and formats.

  • A dry-run import that reports confidence, gaps, and ignored columns.

  • A client review state where users remap, update formats, or accept inferred mappings.

1) Define the template

Use

defineDataExchangeTemplate

for neutral import/export naming.

defineImportTemplate

and

defineExportTemplate

remain available aliases.

src/data-exchange/accounts-import.ts
import {
  createImportPlan,
  createRecordBatch,
  defineDataExchangeTemplate,
  previewImport,
} from "@contractspec/lib.data-exchange-core";
import { defineSchemaModel, ScalarTypeEnum } from "@contractspec/lib.schema";

const AccountImportSchema = defineSchemaModel({
  name: "AccountImport",
  fields: {
    id: { type: ScalarTypeEnum.ID(), isOptional: false },
    status: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
    amount: { type: ScalarTypeEnum.Float_unsecure(), isOptional: false },
    active: { type: ScalarTypeEnum.Boolean(), isOptional: false },
    tags: { type: ScalarTypeEnum.JSON(), isOptional: true },
  },
});

const accountImportTemplate = defineDataExchangeTemplate({
  key: "accounts.import",
  version: "1.0.0",
  title: "Account import",
  columns: [
    {
      key: "id",
      label: "Account ID",
      targetField: "id",
      required: true,
      sourceAliases: ["Account Identifier", "External ID", "No compte"],
    },
    {
      key: "status",
      label: "Status",
      targetField: "status",
      required: true,
      sourceAliases: ["Statut", "State"],
      format: { kind: "text", trim: true, case: "lowercase" },
    },
    {
      key: "amount",
      label: "Amount",
      targetField: "amount",
      required: true,
      sourceAliases: ["Montant", "Balance"],
      format: { kind: "number", decimalSeparator: ",", thousandsSeparator: "." },
    },
    {
      key: "active",
      label: "Active",
      targetField: "active",
      sourceAliases: ["Actif", "Enabled"],
      format: { kind: "boolean", trueValues: ["yes", "oui"], falseValues: ["no", "non"] },
    },
    {
      key: "tags",
      label: "Tags",
      targetField: "tags",
      format: { kind: "split", delimiter: ";" },
    },
  ],
});

const sourceBatch = createRecordBatch([
  {
    "No compte": "acc-1",
    Statut: " Active ",
    Montant: "1.234,50",
    Actif: "oui",
    Tags: "vip; beta",
  },
]);

const preview = previewImport(
  createImportPlan({
    source: { kind: "memory", batch: sourceBatch, format: "csv" },
    target: { kind: "memory", format: "json" },
    schema: AccountImportSchema,
    sourceBatch,
    template: accountImportTemplate,
  }),
);

console.log(preview.plan.mappingSource); // "template"
console.log(preview.normalizedRecords[0]);

Mapping precedence

  • Explicit mappings win first, so existing integrations can keep their current mapping arrays.

  • Template resolution checks exact headers, aliases, normalized labels, and SchemaModel fallback inference.

  • Format profiles can override formats by target field or template column key without changing the template.

  • Required template columns become visible preview issues when no source column can be matched.

Supported value formats

  • text trim and case normalization

  • localized numbers with decimal and thousands separators

  • custom true/false boolean labels

  • dates and datetimes with accepted input formats

  • JSON parsing, empty-as-null, and default values

  • split/join delimiters, currency symbols, and percentages

2) Dry-run partner CSV, JSON, or XML files on the server

File, HTTP, and storage adapters accept codec options. CSV can set delimiters, quotes, skipped rows, header rows, or explicit columns. JSON can read records and metadata from custom keys. XML can use custom root, record, metadata, and attribute fields.

src/server/import-accounts.ts
import { defineDataExchangeTemplate } from "@contractspec/lib.data-exchange-core";
import { dryRunImport, executeImport } from "@contractspec/lib.data-exchange-server";

const template = defineDataExchangeTemplate({
  key: "accounts.import",
  version: "1.0.0",
  columns: [
    { key: "id", label: "Account ID", targetField: "id", required: true, sourceAliases: ["Account Identifier"] },
    { key: "amount", label: "Amount", targetField: "amount", format: { kind: "currency", currencySymbol: "€", decimalSeparator: "," } },
  ],
});

const partnerSource = {
  kind: "file",
  path: "partner-accounts.csv",
  format: "csv",
  codecOptions: { csv: { delimiter: ";", skipRows: 1 } },
} as const;

const formatProfile = {
  columns: {
    amount: { kind: "currency", currencySymbol: "€", decimalSeparator: "," },
  },
} as const;

const preview = await dryRunImport({
  source: partnerSource,
  target: { kind: "memory", format: "json" },
  schema: AccountImportSchema,
  template,
  formatProfile,
});

const blockingIssues = preview.issues.filter((issue) => issue.severity === "error");

if (blockingIssues.length === 0) {
  await executeImport({
    source: partnerSource,
    target: { kind: "memory", format: "json" },
    schema: AccountImportSchema,
    template,
    formatProfile,
  });
}

3) Let users review the mapping

The shared controller exposes template rows, matched source columns, confidence, required status, formatting summaries, unmatched required rows, and ignored source columns. Actions let users remap columns, select aliases, update field formats, reset to the template, or accept inferred mappings.

src/components/ImportMappingReview.tsx
"use client";

import { useDataExchangeController } from "@contractspec/lib.data-exchange-client";

export function ImportMappingReview({ preview }) {
  const controller = useDataExchangeController({ preview });
  const replacementSourceColumn = controller.model.ignoredSourceColumns[0];

  return (
    <section>
      {controller.model.templateRows.map((row) => (
        <button
          key={row.id}
          type="button"
          disabled={!replacementSourceColumn}
          onClick={() => {
            if (!replacementSourceColumn) return;
            controller.selectAlias(row.targetField, replacementSourceColumn);
          }}
        >
          {row.label}: {row.sourceField || "Unmatched"} -> {row.targetField}
          {row.required ? " required" : ""}
          {row.formatLabel ? ` (${row.formatLabel})` : ""}
        </button>
      ))}
      {controller.model.unmatchedRequiredRows.length > 0 ? (
        <p>Resolve required columns before import.</p>
      ) : null}
    </section>
  );
}

4) Verify the package stack

verification
cd packages/libs/data-exchange-core && bun test && bun run typecheck && bun run lint:check
cd packages/libs/data-exchange-client && bun test && bun run typecheck && bun run lint:check
cd packages/libs/data-exchange-server && bun test && bun run typecheck && bun run lint:check

Prompt: build a template

template-authoring.prompt.md
You are adding an import flow to a ContractSpec app.

Define a reusable data-exchange template for this canonical schema:
- target fields, required flags, and display labels
- known partner column aliases
- value formats for numbers, dates, booleans, JSON, split/join lists, currency, and percentages

Wire the template into core preview planning and server dry-run execution. Keep explicit mappings higher precedence than template resolution. Return the template, preview call, server dry-run call, and tests for alias matching plus localized formatting.

Prompt: inspect a partner file

partner-import-review.prompt.md
A partner sent a CSV/JSON/XML file that does not match our recommended import template.

Compare the incoming headers and value samples against this ContractSpec data-exchange template. Propose:
- source-to-target column matches with confidence
- missing required target fields
- ignored source columns
- format overrides for localized numbers, booleans, dates, JSON, split/join lists, currency, or percentages

Do not execute the import. Produce a dry-run plan and the user-facing review copy.