Skip to content

Composition and Imports

Oliver Atkinson edited this page May 31, 2026 · 1 revision

Composition and Imports

Composition lets a project split vocabulary across files while still generating and searching one connected graph.

When to Split Files

Split a lexicon when a vocabulary has different owners or update cadences:

  • shared-commerce.lexicon for reusable product, UI, and data types.
  • storefront-api.lexicon for API terms owned by backend engineers.
  • product-ui.lexicon for UI terms owned by app engineers and designers.
  • analytics.lexicon for event names and funnel states.
  • support.lexicon for operational language and support workflows.

Keep one root lexicon that imports the pieces used by a generated target or editor workspace.

Document-Level Import

@ ./shared-commerce.lexicon

commerce:
	api:
		order:

A document-level import composes the imported document with the current document. Use this for shared roots that should participate in the same graph.

Node-Level Import

commerce:
	api:
		storefront:
		@ ./storefront-api.lexicon
	ui:
		product:
		@ ./product-ui.lexicon

A node-level import grafts the imported root under the node where the import appears. This lets a file own a local subtree while the root lexicon controls where it lands.

Real Project Layout

lexicons/
  commerce.lexicon
  shared-commerce.lexicon
  api/
    storefront.lexicon
  ui/
    product.lexicon
  analytics/
    events.lexicon
lexicon-lsp.json

commerce.lexicon:

@ ./shared-commerce.lexicon

commerce:
	api:
		storefront:
		@ ./api/storefront.lexicon
	ui:
		product:
		@ ./ui/product.lexicon
	analytics:
		@ ./analytics/events.lexicon

lexicon-lsp.json:

{
  "lexicon": "lexicons/commerce.lexicon"
}

The editor and generator now see the same composed graph.

Composition from Swift

let source = URL(fileURLWithPath: "lexicons/commerce.lexicon")
let document = try TaskPaper(Data(contentsOf: source)).decodeDocument()
let resolver = FileLexiconImportResolver(baseURL: source.deletingLastPathComponent())
let plan = try document.composed(resolving: resolver)

guard plan.conflicts.isEmpty else {
	throw ValidationError(plan.conflicts.map(\.description).joined(separator: "\n"))
}

let composed = plan.document
let lexicon = try await Lexicon.from(composed, root: "commerce")

Conflicts

Composition reports conflicts instead of silently choosing a winner. Treat conflicts as design feedback:

  • Two files declare the same concrete node with incompatible metadata.
  • A graft point would cause a path collision.
  • An imported branch carries references that cannot be resolved after composition.

Resolve conflicts by making ownership explicit. Usually that means moving the shared part into a shared import, renaming a local dialect, or replacing duplicate structure with a synonym.

Branch Exports

The CLI can export a subtree as a self-contained fragment:

swift run lexicon excerpt commerce.lexicon commerce.ui.product.card

External references are reported as diagnostics. Lexicon adds imports where it can infer them, so exported branches can become new shared files without losing their dependencies.

Import Safety

The file import resolver resolves local imports relative to the base URL of the source lexicon. Remote imports are intentionally restricted to HTTP(S). Do not depend on arbitrary local absolute paths in project lexicons; keep imports relative to the root lexicon directory.

Clone this wiki locally