42futures/firm/main 64k tokens More Tools
```
├── .gitattributes (omitted)
├── .github/
   ├── ISSUE_TEMPLATE/
      ├── bug_report.md (100 tokens)
      ├── feature_request.md (100 tokens)
   ├── workflows/
      ├── pull_request.yml (100 tokens)
      ├── release.yml (300 tokens)
├── .gitignore (100 tokens)
├── .gitmodules
├── CHANGES.md (200 tokens)
├── Cargo.lock (omitted)
├── Cargo.toml
├── LICENSE (omitted)
├── README.md (2.3k tokens)
├── example/
   ├── core/
      ├── channels.firm (100 tokens)
      ├── industries.firm
      ├── main.firm (100 tokens)
      ├── strategies.firm (300 tokens)
   ├── network/
      ├── colleagues.firm (300 tokens)
   ├── resources/
      ├── branding/
         ├── 42futures_style_guide.pdf
         ├── assets.firm (100 tokens)
      ├── templates/
         ├── proposal/
            ├── proposal_templates.firm
            ├── technical_retainer_proposal.typ (400 tokens)
   ├── sales/
      ├── acme_corp.firm (600 tokens)
├── firm_cli/
   ├── Cargo.toml (200 tokens)
   ├── src/
      ├── cli.rs (400 tokens)
      ├── commands/
         ├── add.rs (1600 tokens)
         ├── build.rs (700 tokens)
         ├── field_prompt.rs (4.3k tokens)
         ├── get.rs (800 tokens)
         ├── mod.rs (100 tokens)
      ├── errors.rs
      ├── files.rs (600 tokens)
      ├── logging.rs (500 tokens)
      ├── main.rs (500 tokens)
      ├── query.rs (100 tokens)
      ├── ui.rs (900 tokens)
├── firm_core/
   ├── Cargo.toml (100 tokens)
   ├── src/
      ├── entity.rs (600 tokens)
      ├── field.rs (2.7k tokens)
      ├── graph/
         ├── graph_errors.rs (100 tokens)
         ├── mod.rs (3.4k tokens)
         ├── query.rs (5k tokens)
      ├── id.rs (600 tokens)
      ├── lib.rs (100 tokens)
      ├── schema/
         ├── builtin.rs (2.6k tokens)
         ├── mod.rs (1000 tokens)
         ├── validation.rs (1000 tokens)
         ├── validation_errors.rs (600 tokens)
├── firm_lang/
   ├── Cargo.toml (100 tokens)
   ├── src/
      ├── convert/
         ├── conversion_errors.rs (400 tokens)
         ├── mod.rs
         ├── to_entity.rs (600 tokens)
         ├── to_schema.rs (400 tokens)
      ├── generate/
         ├── from_entity.rs (1400 tokens)
         ├── from_field.rs (200 tokens)
         ├── from_value.rs (2.3k tokens)
         ├── generator_options.rs (200 tokens)
         ├── mod.rs (1200 tokens)
      ├── lib.rs (100 tokens)
      ├── parser/
         ├── mod.rs (100 tokens)
         ├── parsed_entity.rs (400 tokens)
         ├── parsed_field.rs (300 tokens)
         ├── parsed_schema.rs (500 tokens)
         ├── parsed_schema_field.rs (600 tokens)
         ├── parsed_source.rs (1800 tokens)
         ├── parsed_value.rs (3.1k tokens)
         ├── parser_errors.rs (900 tokens)
         ├── parser_utils.rs (100 tokens)
         ├── source.rs (300 tokens)
      ├── workspace/
         ├── build.rs (800 tokens)
         ├── io.rs (500 tokens)
         ├── mod.rs (200 tokens)
         ├── workspace_errors.rs (200 tokens)
   ├── tests/
      ├── convert_entity_tests.rs (4.2k tokens)
      ├── convert_schema_tests.rs (900 tokens)
      ├── parser_entity_tests.rs (3.9k tokens)
      ├── parser_schema_tests.rs (2.3k tokens)
      ├── parser_source_tests.rs (800 tokens)
      ├── workspace_tests.rs (2000 tokens)
├── install.ps1 (300 tokens)
├── install.sh (200 tokens)
├── media/
   ├── demo.gif
   ├── demo.tape (100 tokens)
```


## /.github/ISSUE_TEMPLATE/bug_report.md

---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''

---

## Description
A clear and concise description of what the bug is.

## Reproduction steps
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

## Expected behaviour
A clear and concise description of what you expected to happen.

## Platform
Describe the platform(s) that you've tested and experienced this issue on.

## Additional context
Add any other context about the problem here.


## /.github/ISSUE_TEMPLATE/feature_request.md

---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''

---

## Motivation
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

## Proposal
A clear and concise description of what you want to happen.

## Backward compatibility
Would this break backward compatibility? Is there a way to mitigate that?

## Considerations
Add any other context or screenshots about the feature request here.


## /.github/workflows/pull_request.yml

```yml path="/.github/workflows/pull_request.yml" 
name: Pull request

on: pull_request

jobs:
  validate:
    name: Validate
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: true
      - name: Run tests
        run: cargo test

```

## /.github/workflows/release.yml

```yml path="/.github/workflows/release.yml" 
name: Release

on:
  push:
    branches: ["release/*"]

jobs:
  release:
    name: Build - ${{ matrix.platform.os-name }}
    strategy:
      matrix:
        platform:
          - os-name: Linux-x86_64
            runs-on: ubuntu-24.04
            target: x86_64-unknown-linux-musl
            archive-name: firm-linux-amd64

          - os-name: Linux-aarch64
            runs-on: ubuntu-24.04
            target: aarch64-unknown-linux-musl
            archive-name: firm-linux-arm64

          - os-name: macOS-x86_64
            runs-on: macOS-latest
            target: x86_64-apple-darwin
            archive-name: firm-darwin-amd64

          - os-name: macOS-aarch64
            runs-on: macOS-latest
            target: aarch64-apple-darwin
            archive-name: firm-darwin-arm64

          - os-name: Windows-x86_64
            runs-on: windows-latest
            target: x86_64-pc-windows-msvc
            archive-name: firm-windows-amd64

    runs-on: ${{ matrix.platform.runs-on }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: true
      - name: Build binary
        uses: houseabsolute/actions-rust-cross@v1
        with:
          command: build
          target: ${{ matrix.platform.target }}
          args: "--locked --release"
          strip: false
      - name: Publish artifacts
        uses: houseabsolute/actions-rust-release@v0
        with:
          executable-name: firm
          target: ${{ matrix.platform.target }}
          archive-name: ${{ matrix.platform.archive-name }}
          changes-file: CHANGES.md

```

## /.gitignore

```gitignore path="/.gitignore" 
# Generated by Cargo
# will have compiled files and executables
debug
target

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# Generated by cargo mutants
# Contains mutation testing data
**/mutants.out*/

# RustRover
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/


# Added by cargo

/target

**.DS_Store
**.firm.graph
.cargo

```

## /.gitmodules

```gitmodules path="/.gitmodules" 
[submodule "tree-sitter-firm"]
	path = tree-sitter-firm
	url = https://github.com/42futures/tree-sitter-firm.git

```

## /CHANGES.md

# Changelog

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - 2025-10-13

### Added

- Tree-sitter grammar repo as a root-level submodule.
- A new README which unifies concepts across core, language and CLI.
- A shared workspace example.
- Pretty output support.
- Inline documentation for most features.
- Github CI pipeline for building and releasing binaries.

### Fixed

- Cargo configs for crates in the workspace.
- Broken test referencing the workspace example.

### Changed

- Migrated separate crate repo to a single Rust workspace.
- CLI add action now also outputs the generated entity.
- Refactoring and documentation cleanup.


## /Cargo.toml

```toml path="/Cargo.toml" 
[workspace]
members = ["firm_core", "firm_lang", "firm_cli"]
resolver = "3"

```

## /README.md

# Firm: Business-as-code
A text-based work management system for technologists.

![Firm CLI demo](media/demo.gif)

## Why?
Modern businesses are natively digital, but lack a unified view. Your data is scattered across SaaS tools you don't control, so you piece together answers by jumping between platforms.

Your business is a graph: customers link to projects, projects link to tasks, people link to organizations. Firm lets you define these relationships in plain text files (you own!).

Version controlled, locally stored and structured as code with the Firm DSL. This structured representation of your work, *business-as-code*, makes your business readable to yourself and to the robots that help you run it.

### Features
- **Everything in one place:** Organizations, contacts, projects, and how they relate.
- **Own your data:** Plain text files and tooling that runs on your machine.
- **Open data model:** Tailor to your business with custom schemas.
- **Automate anything:** Search, report, integrate, whatever. It's just code.
- **AI-ready:** LLMs can read, write, and query your business structure.

## Getting started
Firm operates on a "workspace": a directory containing all your `.firm` DSL files. The Firm CLI processes every file in this workspace to build a unified, queryable graph of your business.

The first step is to add an entity to your workspace. You can do this either by using the CLI or by writing the DSL yourself.

### Add entities with the CLI
Use `firm add` to interactively generate new entities. Out of the box, Firm supports a set of pre-built entity schemas for org mapping, customer relations and work management. The CLI will prompt you for the necessary info and generate corresponding DSL.

```bash
$ firm add
```
```
Adding new entity

> Type: organization
> ID: megacorp
> Name: Megacorp Ltd.
> Email: mega@corp.com
> Urls: ["corp.com"]

Writing generated DSL to file my_workspace/generated/organization.firm
```

### Write DSL manually
Alternatively, you can create a `.firm` file and write the DSL yourself.

```firm
organization megacorp {
  name = "Megacorp Ltd."
  email = "mega@corp.com"
  urls = ["corp.com"]
}
```

Both of these methods achieve the same result: a new entity defined in your Firm workspace.

### Querying the workspace
Once you have entities in your workspace, you can query them using the CLI.

#### Listing entities
Use `firm list` to see all entities of a specific type.

```bash
$ firm list task
```
```
Found 7 entities with type 'task'

ID: task.design_homepage
Name: Design new homepage
Is completed: false
Assignee ref: person.jane_doe

...
```

#### Getting an entity
To view the full details of a single entity, use `firm get` followed by the entity's type and ID.

```bash
$ firm get person john_doe
```
```
Found 'person' entity with ID 'john_doe'

ID: person.john_doe
Name: John Doe
Email: john@doe.com
```

#### Exploring relationships
The power of Firm lies in its ability to travel a graph of your business. Use `firm related` to explore connections to/from any entity.

```bash
$ firm related contact john_doe
```
```
Found 1 relationships for 'contact' entity with ID 'john_doe'

ID: interaction.megacorp_intro
Type: Call
Subject: Initial discussion about Project X
Interaction date: 2025-09-30 09:45:00 +02:00
Initiator ref: person.jane_smith
Primary contact ref: contact.john_doe
```

## Installation
The Firm CLI is available to download via [Github Releases](https://github.com/42futures/firm/releases/). Install scripts are provided to make the process easy.

### Linux and macOS

```bash
curl -fsSL https://raw.githubusercontent.com/42futures/firm/main/install.sh | sudo bash
```

If you don't feel confident running it with `sudo`, you can:

1. **Download the release**
   - Go to [Github Releases](https://github.com/42futures/firm/releases/)
   - Download the appropriate archive for your operating system and architecture. You can run `uname -m` in your terminal if you're not sure which one to pick.

2. **Extract the archive**
```bash
tar -xzf firm-[OS]-[ARCH].tar.gz
```

3. **Navigate to the extracted directory**
```bash
cd firm-[OS]-[ARCH]
```

4. **Run the application**

**Option A:** Run from current directory
```bash
./firm
```

**Option B:** Install globally (recommended)
```bash
# Make executable (if needed)
chmod +x firm

# Move to system PATH
sudo mv firm /usr/local/bin/

# Now you can run firm from anywhere
firm
```

### Windows
```bash
irm https://raw.githubusercontent.com/42futures/firm/main/install.ps1 | iex
```

## Using Firm as a library
Beyond the CLI, you can integrate Firm's core logic directly into your own software using the `firm_core` and `firm_lang` Rust packages. This allows you to build more powerful automations and integrations on top of Firm.

First, add the Firm crates to your `Cargo.toml`:

```toml
[dependencies]
firm_core = { git = "https://github.com/42futures/firm.git" }
firm_lang = { git = "https://github.com/42futures/firm.git" }
```

You can then load a workspace, build the entity graph, and query it programmatically:

```rust
use firm_lang::workspace::Workspace;
use firm_core::EntityGraph;

// Load workspace from a directory
let mut workspace = Workspace::new();
workspace.load_directory("./my_workspace")?;
let build = workspace.build()?;

// Build the graph from the workspace entities
let mut graph = EntityGraph::new();
graph.add_entities(build.entities)?;
graph.build();

// Query the graph for a specific entity
let lead = graph.get_entity(&EntityId::new("lead.ai_validation_project"))?;

// Traverse a relationship to another entity
let contact_ref = lead.get_field(FieldId::new("contact_ref"))?;
let contact = contact_ref.resolve_entity_reference(&graph)?;
```

This gives you full access to the underlying data structures, providing a foundation for building custom business automations.

## Architecture

Firm is organized as a Rust workspace with three crates:

### `firm_core`
Core data structures and graph operations.

- Entity data model
- Typed fields with references
- Relationship graph with query capabilities
- Entity schemas and validation

### `firm_lang`
DSL parsing and generation.

- Tree-sitter-based parser for `.firm` files
- Conversion between DSL and entities
- Workspace support for multi-file projects
- DSL generation from entities

Grammar is defined in [tree-sitter-firm](https://github.com/42futures/tree-sitter-firm).

### `firm_cli`

Command-line interface, making the Firm workspace interactive.

## Core concepts
Firm's data model is built on a few key concepts. Each concept is accessible declaratively through the `.firm` DSL for human-readable definitions, and programmatically through the Rust packages for building your own automations.

### Entities
Entities are the fundamental business objects in your workspace, like people, organizations, or projects. Each entity has a unique ID, a type, and a collection of fields.

**In the DSL**, you define an entity with its type and ID, followed by its fields in a block:

```firm
person john_doe {
    name = "John Doe"
    email = "john@doe.com"
}
```

**In Rust**, this corresponds to an `Entity` struct:

```rust
let person = Entity::new(EntityId::new("john_doe"), EntityType::new("person"))
    .with_field(FieldId::new("name"), "John Doe")
    .with_field(FieldId::new("email"), "john@doe.com");
```

### Fields
Fields are typed key-value pairs attached to an entity. Firm supports a rich set of types:

- `String`
- `Integer`
- `Float`
- `Boolean`
- `Currency`
- `DateTime`
- `List` of other values
- `Reference` to other fields or entities
- `Path` to a local file

**In the DSL**, the syntax maps directly to these types:

```firm
my_task design_homepage {
    title = "Design new homepage"        // String
    priority = 1                         // Integer
    completed = false                    // Boolean
    budget = 5000.00 USD                 // Currency
    due_date = 2024-12-01 at 17:00 UTC   // DateTime
    tags = ["ui", "ux"]                  // List
    assignee = person.jane_doe           // Reference
    deliverable = path"./homepage.zip"   // Path
}
```

**In Rust**, these are represented by the `FieldValue` enum:

```rust
let value = FieldValue::Integer(42);
```

### Relationships and the entity graph
The power of Firm comes from connecting entities. You create relationships using `Reference` fields.

When Firm processes your workspace, it builds the *entity graph* representing of all your entities (as nodes) and their relationships (as directed edges). This graph is what allows for traversal and querying.

**In the DSL**, creating a relationship is as simple as referencing another entity's ID.

```firm
contact john_at_acme {
    person_ref = person.john_doe
    organization_ref = organization.acme_corp
}
```

**In Rust**, you build the graph by loading entities and calling the `.build()` method, which resolves all references into queryable links.

```rust
let mut graph = EntityGraph::new();
graph.add_entities(workspace.build()?.entities)?;
graph.build(); // Builds relationships from references

// Now you can traverse the graph
let contact = graph.get_entity(&EntityId::new("contact.john_at_acme"))?;
let person_ref = contact.get_field(FieldId::new("person_ref"))?;
let person = person_ref.resolve_entity_reference(&graph)?;
```

### Schemas

Schemas allow you to define and enforce a structure for your entities, ensuring data consistency. You can specify which fields are required or optional and what their types should be.

**In the DSL**, you can define a schema that other entities can adhere to:

```firm
schema custom_project {
    field {
        name = "title"
        type = "string"
        required = true
    }
    field {
        name = "budget"
        type = "currency"
        required = false
    }
}

custom_project my_project {
    title  = "My custom project"
    budget = 42000 EUR
}
```

**In Rust**, you can define schemas programmatically to validate entities.

```rust
let schema = EntitySchema::new(EntityType::new("project"))
    .with_required_field(FieldId::new("title"), FieldType::String)
    .with_optional_field(FieldId::new("budget"), FieldType::Currency);

schema.validate(&some_project_entity)?;
```

## Built-in entities

Firm includes schemas for a range of built-in entities like Person, Organization, and Industry.

Firm's entity taxonomy is built on the [REA model (Resources, Events, Agents)](https://en.wikipedia.org/wiki/Resources,_Events,_Agents) with inspiration from [Schema.org](https://schema.org/Person), designed for flexible composition and efficient queries.

Every entity maps to a Resource (thing with value), an Event (thing that happens), or an Agent (thing that acts).

We separate objective reality from business relationships:

- **Fundamental entities** represent things that exist independently (`Person`, `Organization`, `Document`)
- **Contextual entities** represent your business relationships and processes (`Contact`, `Lead`, `Project`)

Entities reference each other rather than extending. One `Person` can be referenced by multiple `Contact`, `Employee`, and `Partner` entities simultaneously.

When the entity graph is built, all `Reference` values automatically create directed edges between entities. This enables traversal queries like "find all Tasks for Opportunities whose Contacts work at Organization X" without complex joins.


## /example/core/channels.firm

```firm path="/example/core/channels.firm" 
channel website {
    description = "The primary website for 42futures."
    name = "42futures website"
    type = "Website"
}

channel blog {
    description = "The substack blog for 42futures."
    name = "42futures blog"
    type = "Website"
}

channel linkedin_organic {
    description = "Organic inbound from LinkedIn."
    name = "LinkedIn organic"
    type = "Social Media"
}

```

## /example/core/industries.firm

```firm path="/example/core/industries.firm" 
industry software_development {
    name = "Software development"
}

```

## /example/core/main.firm

```firm path="/example/core/main.firm" 
organization main {
    name = "42futures"
    email = "daniel@42futures.com"
    urls = ["42futures.com", "blog.42futures.com"]
    industry_ref = industry.software_development

    notes = "Central point of this Firm workspace."
    created_at = 2025-08-31 at 13:45 UTC+2
}

person daniel_rothmann {
    name = "Daniel Rothmann"
    email = "daniel@42futures.com"
    urls = ["https://www.linkedin.com/danielrothmann"]

    notes = "Owner of 42futures."
    created_at = 2025-08-31 at 13:45 UTC+2
}

```

## /example/core/strategies.firm

```firm path="/example/core/strategies.firm" 
strategy positioning {
    name = "42futures positioning strategy"
    source_ref = organization.main
    owner_ref = person.daniel_rothmann
    description = """
        # Market Position: "Practical Software R&D"
        Structured 8-week technical validation for high-stakes decisions.

        ## Hook
        "I help you answer high-stakes technical questions in 8 weeks. With working code."

        ## Target
        CTOs and technology leaders facing new technology decisions (AI integration, architecture modernization, build-vs-buy)

        ## Differentiation
        Not strategy consulting (just PowerPoints) or dev shops (just build). Evidence-first validation through working software before committing big.

        ## Value Proposition
        Replace guesswork with evidence. De-risk technical bets through structured 3-phase pilot (Hypothesis → Experiment → Model).

        ## Competitive Advantage
        - Fixed 8-week timeline vs months/quarters of internal R&D
        - Working code + production-ready foundations vs recommendations
        - Unbiased perspective (no vendor ties, can recommend "stop")
        - Flexible engagement (phase-by-phase or full pilot)

        ## Positioning Frame
        "Innovation starts with a bet" - acknowledges the risk inherent in technical decisions, positions 42futures as the de-risking mechanism.

        ## Success Metric
        Quality of client's decision, not advocacy for specific solutions.
    """
}

```

## /example/network/colleagues.firm

```firm path="/example/network/colleagues.firm" 
person john_doe {
    name = "John Doe"
    urls = ["https://www.linkedin.com/in/johndoe/"]

    created_at = 2025-09-30 at 09:45 UTC+2
}

contact john_doe {
    person_ref = person.john_doe
    role = "Head of Rocket Science"
    status = "Former Colleague"

    notes = "From Acme Corp. Heads up rocket science team at Globex Inc today."
    created_at = 2025-09-30 at 09:45 UTC+2
}

interaction john_checkin {
    interaction_date = 2025-09-29 at 11:05 UTC+2

    channel_ref = channel.linkedin_organic
    initiator_ref = person.daniel_rothmann
    primary_contact_ref = contact.john_doe

    type = "LinkedIn Chat"
    subject = "Check-in"
    outcome = "I met John at a conference, and we agreed a catch up was due. I've sent him a message on LinkedIn, waiting to hear back."

    created_at = 2025-09-30 at 10:10 UTC+2
}

task john_checkin_follow_up {
    name = "Follow up with John"
    description = """
        At a conference, John gave clear intent that he wanted to meet.
        So if I don't hear back on LinkedIn, I should casually follow up.
        John is an important contact, so worth the effort to stay connected.
    """

    source_ref = interaction.john_checkin
    assignee_ref = person.daniel_rothmann

    due_date = 2025-10-10
    is_completed = true

    created_at = 2025-09-30 at 10:15 UTC+2
}

```

## /example/resources/branding/42futures_style_guide.pdf

Binary file available at https://raw.githubusercontent.com/42futures/firm/refs/heads/main/example/resources/branding/42futures_style_guide.pdf

## /example/resources/branding/assets.firm

```firm path="/example/resources/branding/assets.firm" 
file_asset ftf_style_guide {
    name = "42futures style guide"
    path = path"./42futures_style_guide.pdf"
    owner_ref = person.daniel_rothmann
    source_ref = organization.main

    notes = "A homegrown style guide detailing fonts, colors and shapes."

    created_at = 2025-08-01 at 12:00 UTC+2
}

```

## /example/resources/templates/proposal/proposal_templates.firm

```firm path="/example/resources/templates/proposal/proposal_templates.firm" 
file_asset technical_retainer_proposal_template {
    name = "Technical retainer proposal template"
    path = path"./technical_retainer_proposal.typ"
    owner_ref = person.daniel_rothmann

    created_at = 2025-09-12 at 21:50 UTC+2
}

```

## /example/resources/templates/proposal/technical_retainer_proposal.typ

```typ path="/example/resources/templates/proposal/technical_retainer_proposal.typ" 
#let template(
  customer: "",
  title: "Technical retainer proposal",
  introduction: content,
  scope_covered: content,
  scope_excluded: content,
  hourly_rate: content,
  weekly_hours: content,
  schedule: content,
  contact_person: content,
  monthly_total: content,
  payment_terms: content,
  duration: content,
  exceptions: none,
  valid_until: none,
  acceptance_email: "daniel@42futures.com",
) = [
  #set document(title: [#title], author: "42futures")
  #let footer = [#text(fill: rgb("#c566ff"), weight: 800)[42futures]#h(1fr)#context counter(page).display("1 of 1", both: true)]
  #set page(paper: "a4", footer: footer)
  #set text(font: "SF Pro Display", size: 14pt)
  #set heading(numbering: "1.")
  #set par(justify: true)

  #align(center)[
    #text(32pt, weight: 800)[#customer + 42]
    #v(-1.5em)
    #text(16pt)[#title]
    #v(0.5in)
  ]

  = Introduction
  #introduction

  The retainer provides #weekly_hours of dedicated technical support each week, #schedule, for hands-on implementation and problem-solving.

  = How it works
  #weekly_hours reserved exclusively for your technical work every week.

  - *Schedule:* #schedule (Danish local time).
  - *Sessions:* Via Google Meet.
  - *Work priorities* set by #contact_person morning of or day before.

  == Scope
  Defined scope keeps our engagement focused where I can help most.

  === Covered topics
  #scope_covered

  === Excluded topics
  #scope_excluded

  = Investment
  - *Rate:* #hourly_rate per hour.
  - *Monthly commitment:* #monthly_total.
  - *Payment terms:* #payment_terms

  = Terms
  Initial duration of #duration, automatically renewing unless either party cancels before month-end.

  #if exceptions != none [
    #exceptions
  ]

  = Next steps
  #if valid_until != none [
    This proposal is valid until #valid_until.
  ] else [
    This proposal is valid for two weeks from its issue date.
  ]

  To proceed, email #acceptance_email and we'll finalize the contract.
]

```

## /example/sales/acme_corp.firm

```firm path="/example/sales/acme_corp.firm" 
organization acme_corp {
    name = "Acme Corporation"
    email = "contact@acme.com"
    phone = "+1 555-010-2030"
    vat_id = "US123456789"
    industry_ref = industry.software_development
    address = """
        123 Innovation Drive
        Suite 404, Tech Park
        Metropolis, CA 90210
        USA
    """

    created_at = 2025-10-10 at 12:30 UTC+2
}

account acme_corp {
    name = "Acme Corp"
    organization_ref = organization.acme_corp
    owner_ref = person.daniel_rothmann
    status = "Customer"

    created_at = 2025-10-10 at 12:30 UTC+2
}

person kent_smith {
    name = "Kent Smith"
    urls = ["https://www.linkedin.com/in/kentsmith-generic/"]
    created_at = 2025-09-30 at 11:00 UTC+2
}

contact kent_smith {
    person_ref = person.kent_smith
    role = "Senior Engineer"
    status = "Former Colleague"
    notes = "From a previous company, now key contact at Acme Corp."
    created_at = 2025-09-30 at 11:00 UTC+2
}

interaction kent_checkin {
    interaction_date = 2025-09-30 at 11:00 UTC+2

    channel_ref = channel.linkedin_organic
    initiator_ref = person.daniel_rothmann
    primary_contact_ref = contact.kent_smith

    type = "LinkedIn Chat"
    subject = "Check-in / Catch up"
    outcome = "Reached out to reconnect. Expressed interest in learning about Acme's tech stack. Waiting for response."

    created_at = 2025-09-30 at 10:30 UTC+2
}

task kent_lunch_meeting {
    name = "Invite Kent for lunch"
    description = """
        After a check-in with Kent on chat, we agreed to go out for lunch next week.
        I've suggested Wednesday/Thursday near Acme Corp, but need to confirm.
    """

    source_ref = interaction.kent_checkin
    assignee_ref = person.daniel_rothmann

    due_date = 2025-10-03
    is_completed = true

    created_at = 2025-10-01 at 09:05 UTC+2
}

interaction kent_catchup {
    interaction_date = 2025-10-09 at 12:00 UTC+2

    channel_ref = channel.linkedin_organic
    initiator_ref = person.daniel_rothmann
    primary_contact_ref = contact.kent_smith

    type = "Meeting"
    subject = "Catch up & Feedback"
    outcome = """
        We caught up and I got good feedback on my 'Project X' concept.
        Kent's feedback was that the value seems obvious to the younger, tech crowd.
        He also mentioned that his work (Acme) is in the process of scaling, and there might be an opportunity there.
    """

    created_at = 2025-10-10 at 12:15 UTC+2
}

opportunity acme_scaling_pilot {
    name = "Technology Scaling Pilot"
    source_ref = contact.kent_smith
    status = "Discovery"
    value = 15000 EUR
    probability = 10

    notes = """
        Kent mentioned Acme Corp is scaling up and their next challenge is scaling their technology.
        Need to follow up with Kent to get more details on their specific challenges.
    """

    created_at = 2025-10-10 at 12:30 UTC+2
}

```

## /firm_cli/Cargo.toml

```toml path="/firm_cli/Cargo.toml" 
[package]
name = "firm-cli"
version = "0.3.0"
edition = "2024"
description = "Interact with Firm from the command line."
license = "AGPL-3.0"
repository = "https://github.com/42futures/firm"

[[bin]]
name = "firm"
path = "src/main.rs"

[dependencies]
firm_core = { path = "../firm_core" }
firm_lang = { path = "../firm_lang" }

clap = { version = "4.5.42", features = ["derive"] }
console = "0.16.0"
indicatif = "0.18.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = "0.4.27"
indicatif-log-bridge = "0.2.3"
inquire = { version = "0.7.5", default-features = false, features = [
    "console",
    "date",
] }
convert_case = "0.8.0"
chrono = "0.4.41"
rust_decimal = { version = "1.37", features = ["serde-with-str"] }
iso_currency = { version = "0.5", features = ["with-serde", "iterator"] }
pathdiff = "0.2.3"

```

## /firm_cli/src/cli.rs

```rs path="/firm_cli/src/cli.rs" 
use clap::{Parser, Subcommand};
use std::path::PathBuf;

use super::query::CliDirection;
use super::ui::OutputFormat;

/// Defines the top-level interface for the Firm CLI with clap.
#[derive(Parser, Debug)]
#[command(name = "firm")]
#[command(version, about = "Firm CLI: Work management in the terminal.")]
pub struct FirmCli {
    /// Path to firm workspace directory.
    #[arg(short, long, global = true)]
    pub workspace: Option<PathBuf>,

    /// Use cached firm graph?
    #[arg(short, long, global = true)]
    pub cached: bool,

    /// Enable verbose output?
    #[arg(short, long, global = true)]
    pub verbose: bool,

    /// Output format
    #[arg(short, long, global = true, default_value_t = OutputFormat::default())]
    pub format: OutputFormat,

    #[command(subcommand)]
    pub command: FirmCliCommand,
}

/// Defines the available subcommands of the Firm CLI.
#[derive(Subcommand, Debug, PartialEq)]
pub enum FirmCliCommand {
    /// Build workspace and entity graph.
    Build,
    /// Get an entity by ID.
    Get {
        /// Entity type (e.g. person, organization or project)
        entity_type: String,
        /// Entity ID (e.g. john_doe)
        entity_id: String,
    },
    /// List entities of type.
    List {
        /// An entity type (e.g. "person") or "schema" to list schemas
        entity_type: String,
    },
    /// Gets entities related to a given entity.
    Related {
        /// Entity type (e.g. person)
        entity_type: String,
        /// Entity ID (e.g. john_doe)
        entity_id: String,
        /// Direction of relationships (incoming, outgoing, or both if not specified)
        #[arg(short, long)]
        direction: Option<CliDirection>,
    },
    /// Interactively adds a new entity to a file in the workspace.
    Add {
        /// Target firm file.
        to_file: Option<PathBuf>,
    },
}

```

## /firm_cli/src/commands/add.rs

```rs path="/firm_cli/src/commands/add.rs" 
use convert_case::{Case, Casing};
use firm_core::graph::EntityGraph;
use firm_core::{Entity, EntitySchema, compose_entity_id};
use firm_lang::generate::generate_dsl;
use firm_lang::workspace::Workspace;
use inquire::{Confirm, Select, Text};
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;

use super::{
    build_graph, build_workspace, field_prompt::prompt_for_field_value, load_workspace_files,
};
use crate::errors::CliError;
use crate::ui::{self, OutputFormat};

pub const GENERATED_DIR_NAME: &str = "generated";
pub const FIRM_EXTENSION: &str = "firm";

/// Wrapper for EntitySchema that customizes Display for Inquire prompts.
struct InquireSchema<'a>(&'a EntitySchema);
impl<'a> std::fmt::Display for InquireSchema<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0.entity_type)
    }
}

/// Interactively add a new entity and generate DSL for it.
pub fn add_entity(
    workspace_path: &PathBuf,
    to_file: Option<PathBuf>,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    ui::header("Adding new entity");
    let mut workspace = Workspace::new();
    load_workspace_files(&workspace_path, &mut workspace).map_err(|_| CliError::BuildError)?;
    let build = build_workspace(workspace).map_err(|_| CliError::BuildError)?;
    let graph = build_graph(&build)?;

    // Let user choose entity type from built-in and custom schemas
    let mut sorted_schemas = build.schemas.clone();
    sorted_schemas.sort_by_key(|schema| schema.entity_type.to_string());
    let schema_options: Vec<_> = sorted_schemas.iter().map(InquireSchema).collect();
    let chosen_option = Select::new("Type:", schema_options)
        .prompt()
        .map_err(|_| CliError::InputError)?;

    let chosen_schema = chosen_option.0.clone();
    let chosen_type_str = format!("{}", &chosen_schema.entity_type);
    let chosen_id = Text::new("ID:")
        .prompt()
        .map_err(|_| CliError::InputError)?;

    // Make a unique ID for the entity based on the name
    let entity_id = compute_unique_entity_id(&graph, &chosen_type_str, chosen_id);

    // Create initial entity and collect required fields
    let mut entity = Entity::new(entity_id.into(), chosen_schema.entity_type.to_owned());
    let arc_graph = Arc::new(graph.clone());
    let generated_file_path = compute_dsl_path(workspace_path, to_file, chosen_type_str);
    entity = prompt_required_fields(
        &chosen_schema,
        entity.clone(),
        &arc_graph,
        &generated_file_path,
        workspace_path,
    )?;

    // If user chooses to add optionals, prompt for each optional field
    let add_optional = Confirm::new("Add optional fields?")
        .with_default(false)
        .prompt()
        .map_err(|_| CliError::InputError)?;

    if add_optional {
        entity = prompt_optional_fields(
            chosen_schema.clone(),
            entity.clone(),
            arc_graph,
            &generated_file_path,
            workspace_path,
        )?;
    }

    // Generate and write the resulting DSL
    let generated_dsl = generate_dsl(&[entity.clone()]);

    ui::info(&format!(
        "Writing generated DSL to file {}",
        generated_file_path.display()
    ));

    write_dsl(entity, generated_dsl, generated_file_path, output_format)
}

/// Prompts for each required field in an entity schema and writes it to the entity.
fn prompt_required_fields(
    chosen_schema: &EntitySchema,
    mut entity: Entity,
    arc_graph: &Arc<EntityGraph>,
    source_path: &PathBuf,
    workspace_path: &PathBuf,
) -> Result<Entity, CliError> {
    let mut required_fields: Vec<_> = chosen_schema
        .fields
        .iter()
        .filter(|(_, f)| f.is_required())
        .collect();

    required_fields.sort_by_key(|(field_id, _)| field_id.as_str());
    for (field_id, field) in required_fields {
        match prompt_for_field_value(
            field_id,
            field.expected_type(),
            field.is_required(),
            Arc::clone(arc_graph),
            source_path,
            workspace_path,
        )? {
            Some(value) => {
                entity = entity.with_field(field_id.clone(), value);
            }
            None => {}
        }
    }

    Ok(entity)
}

/// Prompts for each optional field in an entity schema and writes it to the entity.
fn prompt_optional_fields(
    chosen_schema: EntitySchema,
    mut entity: Entity,
    graph: Arc<EntityGraph>,
    source_path: &PathBuf,
    workspace_path: &PathBuf,
) -> Result<Entity, CliError> {
    let mut optional_fields: Vec<_> = chosen_schema
        .fields
        .iter()
        .filter(|(_, f)| !f.is_required())
        .collect();

    optional_fields.sort_by_key(|(field_id, _)| field_id.as_str());
    for (field_id, field) in optional_fields {
        match prompt_for_field_value(
            field_id,
            field.expected_type(),
            field.is_required(),
            Arc::clone(&graph),
            source_path,
            workspace_path,
        )? {
            Some(value) => {
                entity = entity.with_field(field_id.clone(), value);
            }
            None => {}
        }
    }

    Ok(entity)
}

/// Ensures uniqueness and comformity of a selected entity ID.
/// We do this by:
/// - Filtering for only alphabetic characters and whitespace
/// - Convert ID to snake_case
/// - Add a number at the end if ID is not unique
/// - Keep increasing the number (within reason) until it's unique
fn compute_unique_entity_id(
    graph: &EntityGraph,
    chosen_type_str: &String,
    mut chosen_id: String,
) -> String {
    chosen_id = chosen_id
        .chars()
        .filter(|&c| c == ' ' || c.is_alphabetic())
        .collect::<String>()
        .to_case(Case::Snake);

    let mut entity_id = chosen_id.clone();
    let mut id_counter = 1;
    while graph
        .get_entity(&compose_entity_id(chosen_type_str, &entity_id))
        .is_some()
        && id_counter < 1000
    {
        entity_id = format!("{}_{}", chosen_id, id_counter);
        id_counter += 1;
    }

    entity_id
}

/// Get the target path to write DSL to by:
/// - Using a custom path, if provided
/// - Generating a path from default settings
fn compute_dsl_path(
    workspace_path: &PathBuf,
    to_file: Option<PathBuf>,
    chosen_type_str: String,
) -> PathBuf {
    let dsl_path = match to_file {
        Some(file_path) => workspace_path
            .join(file_path)
            .with_extension(FIRM_EXTENSION),
        None => workspace_path
            .join(GENERATED_DIR_NAME)
            .join(&chosen_type_str)
            .with_extension(FIRM_EXTENSION),
    };

    dsl_path
}

/// Writes the DSL to a file and outputs the generated entity.
fn write_dsl(
    entity: Entity,
    generated_dsl: String,
    target_path: PathBuf,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    if let Some(parent) = target_path.parent() {
        fs::create_dir_all(parent).map_err(|_| CliError::FileError)?;
    }

    match File::options().create(true).append(true).open(target_path) {
        Ok(mut file) => match file.write_all(&generated_dsl.into_bytes()) {
            Ok(_) => {
                ui::success(&format!("Generated DSL for '{}'", &entity.id));

                match output_format {
                    OutputFormat::Pretty => ui::pretty_output_entity_single(&entity),
                    OutputFormat::Json => ui::json_output(&entity),
                }
                Ok(())
            }
            Err(e) => {
                ui::error_with_details("Couldn't write to file", &e.to_string());
                Err(CliError::FileError)
            }
        },
        Err(e) => {
            ui::error_with_details("Couldn't open file", &e.to_string());
            Err(CliError::FileError)
        }
    }
}

```

## /firm_cli/src/commands/build.rs

```rs path="/firm_cli/src/commands/build.rs" 
use firm_core::graph::{EntityGraph, GraphError};
use firm_lang::workspace::{Workspace, WorkspaceBuild, WorkspaceError};
use std::path::PathBuf;

use crate::errors::CliError;
use crate::files::save_graph_with_backup;
use crate::ui::{self};

/// Builds the selected workspace and saves the resulting entity graph.
pub fn build_and_save_graph(workspace_path: &PathBuf) -> Result<(), CliError> {
    ui::header("Building graph");

    // First load and build the workspace from DSL
    let mut workspace = Workspace::new();
    load_workspace_files(&workspace_path, &mut workspace).map_err(|_| CliError::BuildError)?;
    let build = build_workspace(workspace).map_err(|_| CliError::BuildError)?;

    // Then build and save the entity graph
    let graph = build_graph(&build).map_err(|_| CliError::BuildError)?;
    save_graph_with_backup(&workspace_path, &graph).map_err(|_| CliError::BuildError)?;

    ui::success("Graph was built and saved");

    Ok(())
}

/// Loads files in the workspace with progress indicator.
pub fn load_workspace_files(
    path: &PathBuf,
    workspace: &mut Workspace,
) -> Result<(), WorkspaceError> {
    let spinner = ui::spinner("Loading workspace files");

    match workspace.load_directory(&path) {
        Ok(_) => Ok(spinner.finish_with_message("Workspace files loaded successfully")),
        Err(e) => {
            spinner.finish_and_clear();
            ui::error_with_details(
                &format!("Failed to load directory '{}'", path.display()),
                &e.to_string(),
            );

            return Err(e);
        }
    }
}

/// Builds a workspace with progress indicator.
pub fn build_workspace(mut workspace: Workspace) -> Result<WorkspaceBuild, WorkspaceError> {
    let progress = ui::progress_bar(workspace.num_files().try_into().unwrap());

    match workspace.build_with_progress(|total, curent, phase| {
        progress.set_length(total.try_into().unwrap());
        progress.set_position(curent.try_into().unwrap());
        progress.set_message(phase.to_string());
    }) {
        Ok(build) => {
            progress.finish_with_message("Workspace built successfully");
            Ok(build)
        }
        Err(e) => {
            progress.finish_and_clear();
            ui::error_with_details("Failed to build workspace", &e.to_string());
            Err(e)
        }
    }
}

/// Builds the entity graph from a workspace with progress indicator.
pub fn build_graph(build: &WorkspaceBuild) -> Result<EntityGraph, CliError> {
    let spinner = ui::spinner("Creating graph from workspace");
    let mut graph = EntityGraph::new();

    let entity_result = graph.add_entities(build.entities.clone());
    if let Err(e) = entity_result {
        spinner.finish_and_clear();

        match e {
            GraphError::EntityAlreadyExists(entity_id) => {
                ui::error(&format!(
                    "Entities with duplicate IDs '{}' cannot be added to the graph",
                    entity_id
                ));
            }
            _ => (),
        }

        return Err(CliError::BuildError);
    }

    spinner.set_message("Building graph relationships");
    graph.build();

    spinner.finish_with_message("Graph built successfully");
    Ok(graph)
}

```

## /firm_cli/src/commands/field_prompt.rs

```rs path="/firm_cli/src/commands/field_prompt.rs" 
use chrono::{FixedOffset, Local, NaiveTime, TimeZone, Timelike};
use console::style;
use convert_case::{Case, Casing};
use firm_core::{
    FieldId, FieldType, FieldValue, ReferenceValue, compose_entity_id, graph::EntityGraph,
};
use inquire::{Confirm, CustomType, DateSelect, Select, Text, validator::Validation};
use iso_currency::{Currency, IntoEnumIterator};
use pathdiff::diff_paths;
use rust_decimal::Decimal;
use std::{
    error::Error,
    path::{Path, PathBuf},
    sync::Arc,
};

use crate::errors::CliError;

pub const SKIP_PROMPT_FRAGMENT: &str = " (esc to skip)";

/// Interactive prompt for a field value, applying relevant prompt configurations depending on the field type.
pub fn prompt_for_field_value(
    field_id: &FieldId,
    field_type: &FieldType,
    is_required: bool,
    entity_graph: Arc<EntityGraph>,
    source_path: &PathBuf,
    workspace_dir: &PathBuf,
) -> Result<Option<FieldValue>, CliError> {
    let skippable = !is_required;
    let field_id_prompt = field_id.as_str().to_case(Case::Sentence);

    match field_type {
        FieldType::Boolean => bool_prompt(skippable, &field_id_prompt),
        FieldType::String => string_prompt(skippable, &field_id_prompt),
        FieldType::Integer => int_prompt(skippable, &field_id_prompt),
        FieldType::Float => float_prompt(skippable, &field_id_prompt),
        FieldType::Currency => currency_prompt(skippable, &field_id_prompt),
        FieldType::Reference => {
            reference_prompt(skippable, &field_id_prompt, Arc::clone(&entity_graph))
        }
        FieldType::List => list_prompt(
            skippable,
            &field_id_prompt,
            Arc::clone(&entity_graph),
            source_path,
            workspace_dir,
        ),
        FieldType::DateTime => date_prompt(skippable, &field_id_prompt),
        FieldType::Path => path_prompt(
            skippable,
            &field_id_prompt,
            source_path,
            workspace_dir.clone(),
        ),
    }
}

/// Prompts for a boolean field.
/// Value must be true or false.
fn bool_prompt(skippable: bool, field_id_prompt: &String) -> Result<Option<FieldValue>, CliError> {
    let skip_message = get_skippable_prompt(skippable);

    if skippable {
        let value = Confirm::new(&format!("{}{}:", field_id_prompt, skip_message))
            .prompt_skippable()
            .map_err(|_| CliError::InputError)?;
        Ok(value.map(FieldValue::Boolean))
    } else {
        let value = Confirm::new(&format!("{}{}:", field_id_prompt, skip_message))
            .prompt()
            .map_err(|_| CliError::InputError)?;
        Ok(Some(FieldValue::Boolean(value)))
    }
}

/// Prompts for a string field (only single-line supported).
/// String must not be empty.
fn string_prompt(
    skippable: bool,
    field_id_prompt: &String,
) -> Result<Option<FieldValue>, CliError> {
    let skip_message = get_skippable_prompt(skippable);
    let prompt_text = format!("{}{}:", field_id_prompt, skip_message);

    loop {
        let result = if skippable {
            Text::new(&prompt_text)
                .prompt_skippable()
                .map_err(|_| CliError::InputError)?
        } else {
            Some(
                Text::new(&prompt_text)
                    .prompt()
                    .map_err(|_| CliError::InputError)?,
            )
        };

        match result {
            Some(v) => {
                if !v.trim().is_empty() {
                    return Ok(Some(FieldValue::String(v)));
                } else {
                    eprintln!(
                        "{}",
                        style("This field cannot be empty. Please enter a value.").red()
                    );
                }
            }
            None => {
                // This branch is only reachable if skippable is true and skip was requested.
                if skippable {
                    return Ok(None);
                } else {
                    unreachable!("Text::prompt() for a non-skippable field should not return None");
                }
            }
        }
    }
}

/// Prompts for an integer field.
/// Value must not have a decimal place.
fn int_prompt(skippable: bool, field_id_prompt: &String) -> Result<Option<FieldValue>, CliError> {
    let skip_message = get_skippable_prompt(skippable);
    let prompt_text = format!("{}{}:", field_id_prompt, skip_message);

    let value = CustomType::<i64>::new(&prompt_text)
        .with_error_message("Enter a valid integer")
        .with_help_message("Enter a whole number");

    if skippable {
        let result = value.prompt_skippable().map_err(|_| CliError::InputError)?;
        Ok(result.map(FieldValue::Integer))
    } else {
        let result = value.prompt().map_err(|_| CliError::InputError)?;
        Ok(Some(FieldValue::Integer(result)))
    }
}

/// Prompts for a float field.
/// Value must have a decimal place.
fn float_prompt(skippable: bool, field_id_prompt: &String) -> Result<Option<FieldValue>, CliError> {
    let skip_message = get_skippable_prompt(skippable);
    let prompt_text = format!("{}{}:", field_id_prompt, skip_message);

    let value = CustomType::<f64>::new(&prompt_text)
        .with_error_message("Enter a valid decimal number")
        .with_help_message("Enter a decimal number (e.g., 3.14)");

    if skippable {
        let result = value.prompt_skippable().map_err(|_| CliError::InputError)?;
        Ok(result.map(FieldValue::Float))
    } else {
        let result = value.prompt().map_err(|_| CliError::InputError)?;
        Ok(Some(FieldValue::Float(result)))
    }
}

/// Wraps currency for use in Inquire custom prompt.
struct CurrencyOption {
    currency: Currency,
}

impl std::fmt::Display for CurrencyOption {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} ({})", self.currency.code(), self.currency.name())
    }
}

/// Prompts for a currency field.
/// Currency amount must be a valid number. Currency code is selected from a list of valid options.
fn currency_prompt(
    skippable: bool,
    field_id_prompt: &String,
) -> Result<Option<FieldValue>, CliError> {
    let skip_message = get_skippable_prompt(skippable);
    let amount_prompt = format!("Amount for {}{}:", field_id_prompt, skip_message);

    // Get the amount
    let amount = CustomType::<Decimal>::new(&amount_prompt)
        .with_error_message("Enter a valid decimal amount (e.g., 123.45)")
        .with_help_message("Enter the monetary amount as a decimal number")
        .with_parser(&|input| Decimal::from_str_exact(input).map_err(|_| ()));

    let amount_value = if skippable {
        let result = amount
            .prompt_skippable()
            .map_err(|_| CliError::InputError)?;

        match result {
            Some(val) => val,
            None => return Ok(None),
        }
    } else {
        amount.prompt().map_err(|_| CliError::InputError)?
    };

    // Get the currency code
    let currencies: Vec<CurrencyOption> = Currency::iter()
        .map(|currency| CurrencyOption { currency })
        .collect();

    let currency_prompt = format!("Currency for {}:", field_id_prompt);
    let selected_option = Select::new(&currency_prompt, currencies)
        .with_help_message("Select the currency")
        .prompt()
        .map_err(|_| CliError::InputError)?;

    Ok(Some(FieldValue::Currency {
        amount: amount_value,
        currency: selected_option.currency,
    }))
}

/// Prompt for a reference field.
/// Reference must be to an existing entity or field in the graph.
/// Auto-complete is provided based on entities in the current graph.
fn reference_prompt(
    skippable: bool,
    field_id_prompt: &String,
    entity_graph: Arc<EntityGraph>,
) -> Result<Option<FieldValue>, CliError> {
    let skip_message = get_skippable_prompt(skippable);
    let prompt_text = format!("{}{}:", field_id_prompt, skip_message);

    let graph_for_validator = Arc::clone(&entity_graph);
    let validator = move |input: &str| parse_reference(input, &graph_for_validator);
    let graph_for_autocomplete = Arc::clone(&entity_graph);
    let autocomplete = move |input: &str| get_reference_suggestions(input, &graph_for_autocomplete);
    let reference_value_prompt = Text::new(&prompt_text)
        .with_help_message("Start typing the reference for autocompletion")
        .with_validator(validator)
        .with_autocomplete(autocomplete);

    let result_str = if skippable {
        let result = reference_value_prompt
            .prompt_skippable()
            .map_err(|_| CliError::InputError)?;

        match result {
            Some(val) => val,
            None => return Ok(None),
        }
    } else {
        reference_value_prompt
            .prompt()
            .map_err(|_| CliError::InputError)?
    };

    let parts: Vec<&str> = result_str.split('.').collect();
    match parts.len() {
        2 => Ok(Some(FieldValue::Reference(ReferenceValue::Entity(
            compose_entity_id(&parts[0], &parts[1]),
        )))),
        3 => Ok(Some(FieldValue::Reference(ReferenceValue::Field(
            compose_entity_id(&parts[0], &parts[1]),
            FieldId(parts[2].into()),
        )))),
        _ => unreachable!("Parser should have prevented this format."),
    }
}

/// Parses a string reference by decomposing it and checking the graph if it exists.
fn parse_reference(
    input: &str,
    graph: &EntityGraph,
) -> Result<Validation, Box<dyn Error + Send + Sync>> {
    let parts: Vec<&str> = input.split(".").collect();
    match parts.len() {
        2 => {
            let entity_type = parts[0];
            let entity_id = parts[1];
            let composite_id = compose_entity_id(entity_type, entity_id);
            match graph.get_entity(&composite_id) {
                Some(_) => Ok(Validation::Valid),
                None => Ok(Validation::Invalid(
                    "There is no entity matching this ID".into(),
                )),
            }
        }
        3 => {
            let entity_type = parts[0];
            let entity_id = parts[1];
            let composite_id = compose_entity_id(entity_type, entity_id);
            match graph.get_entity(&composite_id) {
                Some(entity) => {
                    let field_id = parts[2];
                    match entity.get_field(&field_id.into()) {
                        Some(_) => Ok(Validation::Valid),
                        None => Ok(Validation::Invalid(
                            "There is no field matching this ID".into(),
                        )),
                    }
                }
                None => Ok(Validation::Invalid(
                    "There is no entity matching this ID".into(),
                )),
            }
        }
        _ => Ok(Validation::Invalid(
            "References should have 2 or 3 parts separated by '.'".into(),
        )),
    }
}

/// Gets suggestions for the reference prompt by searching the graph for partial matches.
fn get_reference_suggestions(
    input: &str,
    graph: &EntityGraph,
) -> Result<Vec<String>, Box<dyn Error + Send + Sync>> {
    let parts: Vec<&str> = input.split('.').collect();
    let mut suggestions = Vec::new();

    match parts.len() {
        1 => {
            // Suggesting entity types
            let partial_type = parts[0];
            for entity_type in graph.get_all_entity_types() {
                if entity_type.to_string().starts_with(partial_type) {
                    suggestions.push(format!("{}.", entity_type));
                }
            }
        }
        2 => {
            // Suggesting entity IDs
            let entity_type = parts[0];
            let entity_id = parts[1];
            let composite_id = compose_entity_id(entity_type, entity_id);
            let entities = graph.list_by_type(&entity_type.into());
            for entity in entities {
                if entity.id.as_str().starts_with(composite_id.as_str()) {
                    suggestions.push(entity.id.to_string());
                }
            }
        }
        3 => {
            // Suggesting field IDs
            let entity_type = parts[0];
            let entity_id = parts[1];
            let partial_field = parts[2];
            let composite_id = compose_entity_id(entity_type, entity_id);

            if let Some(entity) = graph.get_entity(&composite_id) {
                for (field_id, _) in &entity.fields {
                    if field_id.as_str().starts_with(partial_field) {
                        suggestions.push(format!("{}.{}", entity.id, field_id.as_str()));
                    }
                }
            }
        }
        _ => {
            // No suggestions for invalid formats
        }
    }

    Ok(suggestions)
}

/// Prompt for a list field.
/// Lists must have homogeneous types.
/// User can select a valid type, then iteratively inputs values to it.
fn list_prompt(
    skippable: bool,
    field_id_prompt: &String,
    entity_graph: Arc<EntityGraph>,
    source_path: &PathBuf,
    workspace_dir: &PathBuf,
) -> Result<Option<FieldValue>, CliError> {
    // Ask for the item type
    let item_types = vec![
        FieldType::String,
        FieldType::Integer,
        FieldType::Float,
        FieldType::Boolean,
        FieldType::DateTime,
        FieldType::Currency,
    ];

    let item_type_prompt_text = format!(
        "Type for list {}{}",
        field_id_prompt,
        get_skippable_prompt(skippable)
    );

    let item_type = if skippable {
        let result = Select::new(&item_type_prompt_text, item_types)
            .with_formatter(&|field_type| format!("{}", field_type))
            .prompt_skippable()
            .map_err(|_| CliError::InputError)?;
        match result {
            Some(t) => t,
            None => return Ok(None),
        }
    } else {
        Select::new(&item_type_prompt_text, item_types)
            .with_formatter(&|field_type| format!("{}", field_type))
            .prompt()
            .map_err(|_| CliError::InputError)?
    };

    // Collect items until user skips
    let mut items = Vec::new();
    let mut item_index = 1;
    loop {
        // Prompt for each item (always treat as skippable so user can skip to finish)
        let item_field_id = FieldId::new(&format!("item_{}", item_index));
        match prompt_for_field_value(
            &item_field_id,
            &item_type,
            false,
            Arc::clone(&entity_graph),
            source_path,
            workspace_dir,
        )? {
            Some(value) => {
                items.push(value);
                item_index += 1;
            }
            None => {
                // User skipped, finish the list
                break;
            }
        }
    }

    Ok(Some(FieldValue::List(items)))
}

/// Prompts for a date field.
/// We do in 3 steps, first a calendar, then time, then UTC offset.
fn date_prompt(skippable: bool, field_id_prompt: &String) -> Result<Option<FieldValue>, CliError> {
    let skip_message = get_skippable_prompt(skippable);

    // Get the date
    let date = if skippable {
        match DateSelect::new(&format!("{}{}:", field_id_prompt, skip_message))
            .with_help_message("Use arrow keys to navigate, Enter to select")
            .prompt_skippable()
            .map_err(|_| CliError::InputError)?
        {
            Some(d) => d,
            None => return Ok(None),
        }
    } else {
        DateSelect::new(&format!("{}{}:", field_id_prompt, skip_message))
            .with_help_message("Use arrow keys to navigate, Enter to select")
            .prompt()
            .map_err(|_| CliError::InputError)?
    };

    // Get the time (HH:MM only)
    let time_input = if skippable {
        match CustomType::<NaiveTime>::new("at (esc to skip):")
            .with_error_message("Enter time in HH:MM format (e.g., 14:30)")
            .with_help_message("Format: HH:MM (24-hour format)")
            .with_parser(&|input| {
                NaiveTime::parse_from_str(input, "%H:%M")
                    .map(|t| t.with_second(0).unwrap())
                    .map_err(|_| ())
            })
            .with_default(NaiveTime::from_hms_opt(12, 0, 0).unwrap())
            .prompt_skippable()
            .map_err(|_| CliError::InputError)?
        {
            Some(t) => t,
            None => return Ok(None),
        }
    } else {
        CustomType::<NaiveTime>::new("at:")
            .with_error_message("Enter time in HH:MM format (e.g., 14:30)")
            .with_help_message("Format: HH:MM (24-hour format)")
            .with_parser(&|input| {
                NaiveTime::parse_from_str(input, "%H:%M")
                    .map(|t| t.with_second(0).unwrap())
                    .map_err(|_| ())
            })
            .with_default(NaiveTime::from_hms_opt(12, 0, 0).unwrap())
            .prompt()
            .map_err(|_| CliError::InputError)?
    };

    let naive_datetime = date.and_time(time_input);

    // Get the local timezone offset in hours
    let local_offset_seconds = Local::now().offset().local_minus_utc();
    let local_offset_hours = local_offset_seconds / 3600;

    // Get timezone offset as integer hours
    let timezone_offset = if skippable {
        match CustomType::<i32>::new("UTC offset (esc to skip):")
            .with_error_message("Enter a valid integer between -12 and +14")
            .with_help_message(&format!("Enter hours offset from UTC (e.g., 2 for +02:00, -5 for -05:00), default is {} (local timezone)", local_offset_hours))
            .with_default(local_offset_hours)
            .prompt_skippable()
            .map_err(|_| CliError::InputError)?
        {
            Some(o) => o,
            None => return Ok(None),
        }
    } else {
        CustomType::<i32>::new("UTC offset:")
            .with_error_message("Enter a valid integer between -12 and +14")
            .with_help_message(&format!("Enter hours offset from UTC (e.g., 2 for +02:00, -5 for -05:00), default is {} (local timezone)", local_offset_hours))
            .with_default(local_offset_hours)
            .prompt()
            .map_err(|_| CliError::InputError)?
    };

    // Validate offset range
    if timezone_offset < -12 || timezone_offset > 14 {
        return Err(CliError::InputError);
    }

    let offset = FixedOffset::east_opt(timezone_offset * 3600).unwrap();
    let datetime = offset.from_local_datetime(&naive_datetime).unwrap();

    Ok(Some(FieldValue::DateTime(datetime)))
}

/// Prompts for a path field.
fn path_prompt(
    skippable: bool,
    field_id_prompt: &String,
    source_path: &PathBuf,
    workspace_dir: PathBuf,
) -> Result<Option<FieldValue>, CliError> {
    let skip_message = get_skippable_prompt(skippable);
    let prompt_text = format!("{}{}:", field_id_prompt, skip_message);

    let autocomplete_workspace = workspace_dir.clone();
    let autocomplete =
        move |input: &str| get_path_suggestions(input, autocomplete_workspace.clone());
    let reference_value_prompt = Text::new(&prompt_text)
        .with_help_message("Start typing the path for autocompletion")
        .with_autocomplete(autocomplete);

    let result_str = if skippable {
        let result = reference_value_prompt
            .prompt_skippable()
            .map_err(|_| CliError::InputError)?;

        match result {
            Some(val) => val,
            None => return Ok(None),
        }
    } else {
        reference_value_prompt
            .prompt()
            .map_err(|_| CliError::InputError)?
    };

    // Transform the workspace-relative path to the source-file relative path
    let full_path = workspace_dir.join(&result_str);
    let source_dir = source_path.parent().unwrap_or(Path::new(""));
    let relative_path =
        diff_paths(&full_path, source_dir).unwrap_or_else(|| PathBuf::from(&result_str));

    Ok(Some(FieldValue::Path(relative_path)))
}

fn get_path_suggestions(
    input: &str,
    workspace_dir: PathBuf,
) -> Result<Vec<String>, Box<dyn Error + Send + Sync>> {
    let input_path = Path::new(input);
    let search_dir = if input.ends_with('/') || input.is_empty() {
        workspace_dir.join(input_path)
    } else {
        workspace_dir
            .join(input_path)
            .parent()
            .unwrap_or(&workspace_dir)
            .to_path_buf()
    };

    let mut suggestions = Vec::new();
    if let Ok(entries) = std::fs::read_dir(search_dir) {
        for entry in entries.flatten() {
            let full_path = entry.path();

            // Get the path relative to the current workspace directory
            if let Some(relative_path) = diff_paths(&full_path, &workspace_dir) {
                let mut suggestion = relative_path.to_string_lossy().to_string();

                // Add a trailing slash to directories for clairty
                if let Ok(file_type) = entry.file_type() {
                    if file_type.is_dir() {
                        suggestion.push('/');
                    }
                }

                // Add the suggestion if it starts with the user's input
                if suggestion.starts_with(input) {
                    suggestions.push(suggestion);
                }
            }
        }
    }

    Ok(suggestions)
}

/// Helper to get a prompt message fragment or empty string depending on whether field is skippable.
fn get_skippable_prompt(skippable: bool) -> &'static str {
    if skippable { SKIP_PROMPT_FRAGMENT } else { "" }
}

```

## /firm_cli/src/commands/get.rs

```rs path="/firm_cli/src/commands/get.rs" 
use firm_core::compose_entity_id;
use firm_lang::workspace::Workspace;
use std::path::PathBuf;

use super::{build_workspace, load_workspace_files};
use crate::errors::CliError;
use crate::files::load_current_graph;
use crate::query::CliDirection;
use crate::ui::{self, OutputFormat};

/// Gets an entity by ID from the current workspace entity graph.
pub fn get_entity_by_id(
    workspace_path: &PathBuf,
    entity_type: String,
    entity_id: String,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    ui::header("Getting entity by ID");
    let graph = load_current_graph(&workspace_path)?;

    let id = compose_entity_id(&entity_type, &entity_id);
    match graph.get_entity(&id) {
        Some(entity) => {
            ui::success(&format!(
                "Found '{}' entity with ID '{}'",
                entity_type, entity_id
            ));

            match output_format {
                ui::OutputFormat::Pretty => ui::pretty_output_entity_single(entity),
                ui::OutputFormat::Json => ui::json_output(entity),
            }
        }
        None => {
            ui::error(&format!(
                "Couldn't find '{}' entity with ID '{}'",
                entity_type, entity_id
            ));

            return Err(CliError::QueryError);
        }
    }

    Ok(())
}

/// Gets relatives for an entity by ID from the current workspace entity graph.
pub fn get_related_entities(
    workspace_path: &PathBuf,
    entity_type: String,
    entity_id: String,
    direction: Option<CliDirection>,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    ui::header("Getting related entities");
    let graph = load_current_graph(&workspace_path)?;

    let id = compose_entity_id(&entity_type, &entity_id);
    match graph.get_related(&id, direction.clone().map(|d| d.into())) {
        Some(entities) => {
            let direction_text = match direction {
                Some(CliDirection::To) => "references to",
                Some(CliDirection::From) => "references from",
                None => "relationships for",
            };

            ui::success(&format!(
                "Found {} {} '{}' entity with ID '{}'",
                entities.len(),
                direction_text,
                entity_type,
                entity_id
            ));

            match output_format {
                OutputFormat::Pretty => ui::pretty_output_entity_list(&entities),
                OutputFormat::Json => ui::json_output(&entities),
            }

            Ok(())
        }
        None => {
            ui::error(&format!(
                "Couldn't find '{}' entity with ID '{}'",
                entity_type, entity_id
            ));

            Err(CliError::QueryError)
        }
    }
}

/// Lists entities of a given type in the workspace.
pub fn list_entities_by_type(
    workspace_path: &PathBuf,
    entity_type: String,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    ui::header("Listing entities by type");
    let graph = load_current_graph(&workspace_path)?;

    let entities = graph.list_by_type(&entity_type.as_str().into());
    ui::success(&format!(
        "Found {} entities with type '{}'",
        entities.len(),
        entity_type,
    ));

    match output_format {
        OutputFormat::Pretty => ui::pretty_output_entity_list(&entities),
        OutputFormat::Json => ui::json_output(&entities),
    }

    Ok(())
}

/// Lists schemas in the workspace.
/// This is a special case for the CLI list action where a type of "schema" is provided.
pub fn list_schemas(workspace_path: &PathBuf, output_format: OutputFormat) -> Result<(), CliError> {
    ui::header("Listing schemas");
    let mut workspace = Workspace::new();
    load_workspace_files(&workspace_path, &mut workspace).map_err(|_| CliError::BuildError)?;
    let build = build_workspace(workspace).map_err(|_| CliError::BuildError)?;

    ui::success(&format!(
        "Found {} schemas for this workspace",
        build.schemas.len()
    ));

    match output_format {
        OutputFormat::Pretty => ui::pretty_output_schema_list(&build.schemas.iter().collect()),
        OutputFormat::Json => ui::json_output(&build.schemas),
    }
    Ok(())
}

```

## /firm_cli/src/commands/mod.rs

```rs path="/firm_cli/src/commands/mod.rs" 
mod add;
mod build;
mod field_prompt;
mod get;

pub use add::add_entity;
pub use build::{build_and_save_graph, build_graph, build_workspace, load_workspace_files};
pub use get::{get_entity_by_id, get_related_entities, list_entities_by_type, list_schemas};

```

## /firm_cli/src/errors.rs

```rs path="/firm_cli/src/errors.rs" 
/// The errors that can occur when using the CLI.
#[derive(Debug)]
pub enum CliError {
    BuildError,
    FileError,
    QueryError,
    InputError,
}

```

## /firm_cli/src/files.rs

```rs path="/firm_cli/src/files.rs" 
use firm_core::graph::EntityGraph;
use std::{env, fs, path::PathBuf};

use super::errors::CliError;
use super::ui::{self};

pub const CURRENT_GRAPH_NAME: &str = "current.firm.graph";
pub const BACKUP_GRAPH_NAME: &str = "backup.firm.graph";

/// Gets the Firm workspace path.
/// If it was provided from CLI args, use that, otherwise use current working directory.
pub fn get_workspace_path(directory_path: &Option<PathBuf>) -> Result<PathBuf, CliError> {
    let path = match directory_path {
        Some(path) => path.clone(),
        None => match env::current_dir() {
            Ok(path) => path,
            Err(e) => {
                ui::error_with_details("Cannot access current working directory", &e.to_string());
                return Err(CliError::FileError);
            }
        },
    };

    ui::debug(&format!("Using workspace directory: '{}'", path.display()));
    Ok(path)
}

/// Saves an entity graph to the workspace root.
/// If one already exists, we back it up.
pub fn save_graph_with_backup(
    workspace_path: &PathBuf,
    graph: &EntityGraph,
) -> Result<(), CliError> {
    let current_graph_path = workspace_path.join(CURRENT_GRAPH_NAME);
    let backup_graph_path = workspace_path.join(BACKUP_GRAPH_NAME);

    // If current firm graph exists, back it up
    if current_graph_path.exists() {
        ui::debug("Backing up existing graph");

        if let Err(e) = fs::rename(&current_graph_path, &backup_graph_path) {
            ui::error_with_details("Failed to rename existing graph file", &e.to_string());
            return Err(CliError::FileError);
        }
    }

    // Write new graph to file
    ui::debug("Saving current graph");
    let serialized_graph = serde_json::to_string(&graph).map_err(|e| {
        ui::error_with_details("Failed to serialize graph", &e.to_string());
        CliError::FileError
    })?;

    if let Err(e) = fs::write(&current_graph_path, serialized_graph) {
        ui::error_with_details("Failed to write graph file", &e.to_string());
        return Err(CliError::FileError);
    }

    ui::info(&format!("Graph saved to {}", current_graph_path.display()));
    Ok(())
}

/// Loads an entity graph from the workspace root.
pub fn load_current_graph(workspace_path: &PathBuf) -> Result<EntityGraph, CliError> {
    let current_graph_path = workspace_path.join(CURRENT_GRAPH_NAME);

    if !current_graph_path.exists() {
        ui::error_with_details(
            "The graph file to load didn't exist",
            &current_graph_path.display().to_string(),
        );
        return Err(CliError::FileError);
    }

    // Load graph from file
    ui::debug("Loading current graph");
    let file_content = fs::read_to_string(&current_graph_path).map_err(|e| {
        ui::error_with_details("Failed to read graph file", &e.to_string());
        CliError::FileError
    })?;

    let graph: EntityGraph = serde_json::from_str(&file_content).map_err(|e| {
        ui::error_with_details("Failed to deserialize graph file", &e.to_string());
        CliError::FileError
    })?;

    ui::info(&format!(
        "Graph loaded from {}",
        current_graph_path.display()
    ));

    Ok(graph)
}

```

## /firm_cli/src/logging.rs

```rs path="/firm_cli/src/logging.rs" 
use indicatif::MultiProgress;
use indicatif_log_bridge::LogWrapper;
use log::{Level, Log, Metadata, Record};
use std::sync::OnceLock;

use super::ui;

/// Tracks indicatif progress bars so they can be stalled when outputting logs.
static MULTI_PROGRESS: OnceLock<MultiProgress> = OnceLock::new();

/// Gets or creates a global indicatif progress bar tracker.
pub fn get_multi_progress() -> &'static MultiProgress {
    MULTI_PROGRESS.get_or_init(|| MultiProgress::new())
}

/// A logger implementation that outputs library logs to console UI messages.
struct UiLogger {
    verbose: bool,
}

impl Log for UiLogger {
    /// Configures logger to take logs from firm libraries.
    fn enabled(&self, metadata: &Metadata) -> bool {
        // Determine if the log originates from a firm crate
        let target = metadata.target();
        let is_firm_crate = target.contains("firm");

        // In verbose mode, set debug level for firm crates and warn level for others
        if self.verbose && is_firm_crate {
            metadata.level() <= Level::Debug
        } else {
            metadata.level() <= Level::Warn
        }
    }

    /// Pipes library logs to the appropriate UI message.
    fn log(&self, record: &Record) {
        if self.enabled(record.metadata()) {
            match record.level() {
                Level::Error => ui::error(&record.args().to_string()),
                Level::Warn => ui::warning(&record.args().to_string()),
                Level::Info => ui::info(&record.args().to_string()),
                Level::Debug => ui::debug(&record.args().to_string()),
                Level::Trace => ui::debug(&record.args().to_string()),
            }
        }
    }

    fn flush(&self) {}
}

/// Initializes logging for the CLI.
pub fn initialize(verbose: bool) -> Result<(), log::SetLoggerError> {
    let ui_logger = UiLogger { verbose };

    // Wrap logger, allowing indicatif progress bars to be suspended when we output logs
    let wrapped_logger = LogWrapper::new(get_multi_progress().clone(), Box::new(ui_logger));
    log::set_boxed_logger(Box::new(wrapped_logger))?;

    if verbose {
        log::set_max_level(log::LevelFilter::Debug);
    } else {
        log::set_max_level(log::LevelFilter::Warn);
    }

    Ok(())
}

```

## /firm_cli/src/main.rs

```rs path="/firm_cli/src/main.rs" 
//! The command-line interface for interacting with a Firm workspace.
//!
//! This crate provides a set of commands to manage and query entities
//! defined in `.firm` files. It uses `firm_lang` to load the workspace
//! and `firm_core` to build and query the entity graph.

mod cli;
mod commands;
mod errors;
mod files;
mod logging;
mod query;
mod ui;

use clap::Parser;
use std::process::ExitCode;

use cli::{FirmCli, FirmCliCommand};
use commands::build_and_save_graph;
use files::get_workspace_path;

fn main() -> ExitCode {
    let cli = FirmCli::parse();

    // Set up logging
    if let Err(e) = logging::initialize(cli.verbose) {
        ui::error_with_details("Failed to initialize logging", &e.to_string());
        return ExitCode::FAILURE;
    }

    // Get the workspace
    let workspace_path = match get_workspace_path(&cli.workspace) {
        Ok(path) => path,
        Err(_) => return ExitCode::FAILURE,
    };

    // Pre-build the graph unless we're using cache or doing a build command
    if !cli.cached && cli.command != FirmCliCommand::Build {
        match build_and_save_graph(&workspace_path) {
            Ok(_) => (),
            Err(_) => return ExitCode::FAILURE,
        }
    }

    // Handle CLI subcommands
    let result = match cli.command {
        FirmCliCommand::Build => build_and_save_graph(&workspace_path),
        FirmCliCommand::Get {
            entity_type,
            entity_id,
        } => commands::get_entity_by_id(&workspace_path, entity_type, entity_id, cli.format),
        FirmCliCommand::List { entity_type } => {
            if entity_type == "schema" {
                commands::list_schemas(&workspace_path, cli.format)
            } else {
                commands::list_entities_by_type(&workspace_path, entity_type, cli.format)
            }
        }
        FirmCliCommand::Related {
            entity_type,
            entity_id,
            direction,
        } => commands::get_related_entities(
            &workspace_path,
            entity_type,
            entity_id,
            direction,
            cli.format,
        ),
        FirmCliCommand::Add { to_file } => {
            commands::add_entity(&workspace_path, to_file, cli.format)
        }
    };

    result.map_or(ExitCode::FAILURE, |_| ExitCode::SUCCESS)
}

```

## /firm_cli/src/query.rs

```rs path="/firm_cli/src/query.rs" 
use clap::ValueEnum;
use firm_core::graph::Direction;

/// Wraps the underlying graph direction enum, allowing it to be used by clap.
#[derive(Clone, Debug, ValueEnum, PartialEq)]
pub enum CliDirection {
    To,
    From,
}

impl From<CliDirection> for Direction {
    fn from(dir: CliDirection) -> Direction {
        match dir {
            CliDirection::To => Direction::Incoming,
            CliDirection::From => Direction::Outgoing,
        }
    }
}

```

## /firm_cli/src/ui.rs

```rs path="/firm_cli/src/ui.rs" 
use clap::ValueEnum;
use console::Style;
use firm_core::{Entity, EntitySchema};
use indicatif::{ProgressBar, ProgressStyle};
use std::{fmt, time::Duration};

use super::logging;

/// Helpers to create consistent console UI styles.
pub struct UiStyle;

impl UiStyle {
    /// A regular message.
    pub fn normal() -> Style {
        Style::new()
    }

    /// A highlighted message.
    pub fn highlight() -> Style {
        Style::new().bold()
    }

    /// A dim message.
    pub fn dim() -> Style {
        Style::new().dim()
    }

    /// A warning message.
    pub fn warning() -> Style {
        Style::new().yellow().bold()
    }

    /// A success message.
    pub fn success() -> Style {
        Style::new().green().bold()
    }

    /// An error message
    pub fn error() -> Style {
        Style::new().red().bold()
    }
}

/// Prints a header message.
pub fn header(msg: &str) {
    eprintln!("{}", UiStyle::highlight().apply_to(msg));
}

/// Prints a debug message.
pub fn debug(msg: &str) {
    eprintln!("{}", UiStyle::dim().apply_to(msg));
}

/// Prints an info message.
pub fn info(msg: &str) {
    eprintln!("{}", UiStyle::normal().apply_to(msg));
}

/// Prints a warning message.
pub fn warning(msg: &str) {
    eprintln!("{}", UiStyle::warning().apply_to(msg));
}

/// Prints a success message.
pub fn success(msg: &str) {
    eprintln!("{}", UiStyle::success().apply_to(msg));
}

/// Prints an error message.
pub fn error(msg: &str) {
    eprintln!("{}", UiStyle::error().apply_to(msg));
}

/// Prints an error message with added details.
pub fn error_with_details(main_msg: &str, details: &str) {
    eprintln!("{}", UiStyle::error().apply_to(main_msg));
    eprintln!("   {}", UiStyle::dim().apply_to(details));
}

/// Selects the output format used by the CLI.
#[derive(Clone, Debug, ValueEnum, PartialEq)]
pub enum OutputFormat {
    Pretty,
    Json,
}

impl fmt::Display for OutputFormat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            OutputFormat::Pretty => write!(f, "pretty"),
            OutputFormat::Json => write!(f, "json"),
        }
    }
}

impl Default for OutputFormat {
    fn default() -> Self {
        OutputFormat::Pretty
    }
}

/// Outputs a single entity in pretty format.
pub fn pretty_output_entity_single(entity: &Entity) {
    println!("\n{}", entity);
}

/// Outputs a list of entities in pretty format.
pub fn pretty_output_entity_list(entities: &Vec<&Entity>) {
    for (i, entity) in entities.iter().enumerate() {
        pretty_output_entity_single(entity);

        // Add a separator after each entity, except for the last one.
        if i < entities.len() - 1 {
            println!("---------------------------------------");
        }
    }
}

/// Outputs a single entity schema in pretty format.
pub fn pretty_output_schema_single(schema: &EntitySchema) {
    println!("\n{}", schema);
}

/// Outputs a list of entity schemas in pretty format.
pub fn pretty_output_schema_list(schemas: &Vec<&EntitySchema>) {
    for (i, schema) in schemas.iter().enumerate() {
        pretty_output_schema_single(schema);

        // Add a separator after each entity, except for the last one.
        if i < schemas.len() - 1 {
            println!("---------------------------------------");
        }
    }
}

/// Outputs a serde-serializable object in json format.
pub fn json_output<T: serde::Serialize>(data: &T) {
    if let Ok(json) = serde_json::to_string_pretty(data) {
        println!("{}", json);
    }
}

/// Creates a spinner progress indicator.
pub fn spinner(msg: &str) -> ProgressBar {
    let pb = ProgressBar::new_spinner();
    pb.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner} {msg}")
            .expect("Invalid template"),
    );
    pb.enable_steady_tick(Duration::from_millis(100));
    pb.set_message(msg.to_string());

    let tracker = logging::get_multi_progress();
    tracker.add(pb.clone());

    pb
}

/// Creates a progress bar indicator.
pub fn progress_bar(len: u64) -> ProgressBar {
    let pb = ProgressBar::new(len);
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{spinner} {msg} [{bar}] {pos}/{len}")
            .expect("Invalid template")
            .progress_chars("# "),
    );
    pb.enable_steady_tick(Duration::from_millis(100));

    let tracker = logging::get_multi_progress();
    tracker.add(pb.clone());

    pb
}

```

## /firm_core/Cargo.toml

```toml path="/firm_core/Cargo.toml" 
[package]
name = "firm_core"
version = "0.3.0"
edition = "2024"
description = "Core data structures and graph operations for Firm."
license = "AGPL-3.0"
repository = "https://github.com/42futures/firm"

[dependencies]
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.141"
petgraph = { version = "0.8.2", features = ["serde-1"] }
log = "0.4.27"
rust_decimal = { version = "1.37.2", features = ["serde-with-str"] }
iso_currency = { version = "0.5.3", features = ["with-serde"] }
chrono = { version = "0.4.41", features = ["serde"] }
convert_case = "0.8.0"

[dev-dependencies]
assert_matches = "1.5"
env_logger = "0.11.8"

```

## /firm_core/src/entity.rs

```rs path="/firm_core/src/entity.rs" 
use convert_case::{Case, Casing};
use serde::{Deserialize, Serialize};
use std::fmt;

use super::{EntityId, EntityType, FieldId, FieldValue};

/// Represents a business entity in the Firm graph.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Entity {
    pub id: EntityId,
    pub entity_type: EntityType,
    pub fields: Vec<(FieldId, FieldValue)>,
}

impl Entity {
    /// Creates a new entity with the desired ID and type.
    pub fn new(id: EntityId, entity_type: EntityType) -> Self {
        Self {
            id: id,
            entity_type: entity_type,
            fields: Vec::new(),
        }
    }

    /// Builder method to add a field to a new entity.
    pub fn with_field<V>(mut self, id: FieldId, value: V) -> Self
    where
        V: Into<FieldValue>,
    {
        self.fields.push((id, value.into()));
        self
    }

    /// Try to get a entity field value for a given field ID.
    pub fn get_field(&self, id: &FieldId) -> Option<&FieldValue> {
        self.fields
            .iter()
            .find(|(field_id, _)| field_id == id)
            .map(|(_, field_value)| field_value)
    }
}

impl fmt::Display for Entity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "{}\n", self.id)?;
        for (field_id, field_value) in &self.fields {
            writeln!(
                f,
                "{}: {}",
                field_id.as_str().to_case(Case::Sentence),
                field_value
            )?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_entity_create_new() {
        let person = Entity::new(EntityId::new("john_doe"), EntityType::new("person"));

        assert_eq!(person.id, EntityId::new("john_doe"));
        assert_eq!(person.entity_type, EntityType::new("person"));
        assert!(person.fields.is_empty());
    }

    #[test]
    fn test_entity_with_fields() {
        let person = Entity::new(EntityId::new("john_doe"), EntityType::new("person"))
            .with_field(FieldId::new("name"), "John Doe")
            .with_field(FieldId::new("email"), "john@example.com");

        assert_eq!(
            person.get_field(&FieldId::new("name")),
            Some(&FieldValue::String(String::from("John Doe")))
        );
        assert_eq!(
            person.get_field(&FieldId::new("email")),
            Some(&FieldValue::String(String::from("john@example.com")))
        );
        assert_eq!(person.get_field(&FieldId::new("nonexistant")), None);
    }

    #[test]
    fn test_entity_different_types() {
        let person = Entity::new(EntityId::new("john_doe"), EntityType::new("person"));
        let organization = Entity::new(EntityId::new("megacorp"), EntityType::new("organization"));

        assert_eq!(person.entity_type, EntityType::new("person"));
        assert_eq!(organization.entity_type, EntityType::new("organization"));
    }
}

```

## /firm_core/src/field.rs

```rs path="/firm_core/src/field.rs" 
use std::fmt;

use chrono::{DateTime, FixedOffset};
use iso_currency::Currency;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::{EntityId, FieldId};

/// The supported types of an entity field.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FieldType {
    Boolean,
    String,
    Integer,
    Float,
    Currency,
    Reference,
    List,
    DateTime,
    Path,
}

impl fmt::Display for FieldType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FieldType::Boolean => write!(f, "Boolean"),
            FieldType::String => write!(f, "String"),
            FieldType::Integer => write!(f, "Integer"),
            FieldType::Float => write!(f, "Float"),
            FieldType::Currency => write!(f, "Currency"),
            FieldType::Reference => write!(f, "Reference"),
            FieldType::List => write!(f, "List"),
            FieldType::DateTime => write!(f, "DateTime"),
            FieldType::Path => write!(f, "Path"),
        }
    }
}

/// The supported reference types: to an entity or an entity field.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ReferenceValue {
    Entity(EntityId),
    Field(EntityId, FieldId),
}

impl fmt::Display for ReferenceValue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ReferenceValue::Entity(entity_id) => write!(f, "{}", entity_id),
            ReferenceValue::Field(entity_id, field_id) => write!(f, "{}.{}", entity_id, field_id),
        }
    }
}

/// The value of an entity field.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FieldValue {
    Boolean(bool),
    String(String),
    Integer(i64),
    Float(f64),
    Currency { amount: Decimal, currency: Currency },
    Reference(ReferenceValue),
    List(Vec<FieldValue>),
    DateTime(DateTime<FixedOffset>),
    Path(PathBuf),
}

impl fmt::Display for FieldValue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FieldValue::Boolean(val) => write!(f, "{}", val),
            FieldValue::String(val) => write!(f, "{}", val),
            FieldValue::Integer(val) => write!(f, "{}", val),
            FieldValue::Float(val) => write!(f, "{}", val),
            FieldValue::Currency { amount, currency } => write!(f, "{} {}", amount, currency),
            FieldValue::Reference(val) => write!(f, "{}", val),
            FieldValue::List(vals) => {
                write!(
                    f,
                    "[{}]",
                    vals.iter()
                        .map(|v| v.to_string())
                        .collect::<Vec<String>>()
                        .join(",")
                )
            }
            FieldValue::DateTime(val) => write!(f, "{}", val),
            FieldValue::Path(val) => write!(f, "{}", val.display()),
        }
    }
}

impl FieldValue {
    /// Gets the type of the given field value.
    pub fn get_type(&self) -> FieldType {
        match self {
            FieldValue::Boolean(_) => FieldType::Boolean,
            FieldValue::String(_) => FieldType::String,
            FieldValue::Integer(_) => FieldType::Integer,
            FieldValue::Float(_) => FieldType::Float,
            FieldValue::Currency {
                amount: _,
                currency: _,
            } => FieldType::Currency,
            FieldValue::Reference(ReferenceValue::Entity(_)) => FieldType::Reference,
            FieldValue::Reference(ReferenceValue::Field(_, _)) => FieldType::Reference,
            FieldValue::List(_) => FieldType::List,
            FieldValue::DateTime(_) => FieldType::DateTime,
            FieldValue::Path(_) => FieldType::Path,
        }
    }

    /// Checks if the field value has the expected type.
    pub fn is_type(&self, expected: &FieldType) -> bool {
        &self.get_type() == expected
    }
}

/// Convert from bool to FieldValue.
impl From<bool> for FieldValue {
    fn from(value: bool) -> Self {
        FieldValue::Boolean(value)
    }
}

/// Convert from &str to FieldValue.
impl From<&str> for FieldValue {
    fn from(value: &str) -> Self {
        FieldValue::String(value.to_string())
    }
}

/// Convert from String to FieldValue.
impl From<String> for FieldValue {
    fn from(value: String) -> Self {
        FieldValue::String(value)
    }
}

/// Convert from i64 to FieldValue.
impl From<i64> for FieldValue {
    fn from(value: i64) -> Self {
        FieldValue::Integer(value)
    }
}

/// Convert from f64 to FieldValue.
impl From<f64> for FieldValue {
    fn from(value: f64) -> Self {
        FieldValue::Float(value)
    }
}

/// Convert from DateTime<FixedOffset> to FieldValue.
impl From<DateTime<FixedOffset>> for FieldValue {
    fn from(value: DateTime<FixedOffset>) -> Self {
        FieldValue::DateTime(value)
    }
}

/// Convert from Vec<FieldValue> to FieldValue.
impl From<Vec<FieldValue>> for FieldValue {
    fn from(value: Vec<FieldValue>) -> Self {
        FieldValue::List(value)
    }
}

/// Convert from PathBuf to FieldValue.
impl From<PathBuf> for FieldValue {
    fn from(value: PathBuf) -> Self {
        FieldValue::Path(value)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::env::current_dir;

    #[test]
    fn test_field_value_get_type() {
        let string_value = FieldValue::String("test".to_string());
        assert_eq!(string_value.get_type(), FieldType::String);
    }

    #[test]
    fn test_field_value_is_type() {
        let string_value = FieldValue::String("test".to_string());
        assert!(string_value.is_type(&FieldType::String));
    }

    #[test]
    fn test_field_from_bool() {
        let field: FieldValue = true.into();
        assert_eq!(field, FieldValue::Boolean(true));
    }

    #[test]
    fn test_field_from_str() {
        let field: FieldValue = "Test".into();
        assert_eq!(field, FieldValue::String("Test".to_string()));
    }

    #[test]
    fn test_field_from_string() {
        let field: FieldValue = String::from("Test").into();
        assert_eq!(field, FieldValue::String(String::from("Test")));
    }

    #[test]
    fn test_field_from_i64() {
        let field: FieldValue = 42i64.into();
        assert_eq!(field, FieldValue::Integer(42));
    }

    #[test]
    fn test_field_from_f64() {
        let field: FieldValue = 3.14f64.into();
        assert_eq!(field, FieldValue::Float(3.14));
    }

    #[test]
    fn test_field_from_datetime() {
        use chrono::{FixedOffset, TimeZone};
        let offset = FixedOffset::east_opt(5 * 3600).unwrap();
        let dt = offset.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
        let field: FieldValue = dt.into();
        assert_eq!(field, FieldValue::DateTime(dt));
    }

    #[test]
    fn test_field_from_vec() {
        let values = vec![
            FieldValue::String("test1".to_string()),
            FieldValue::String("test2".to_string()),
        ];
        let field: FieldValue = values.clone().into();
        assert_eq!(field, FieldValue::List(values));
    }

    #[test]
    fn test_field_from_pathbuf() {
        let field: FieldValue = current_dir().unwrap().into();
        assert_eq!(field, FieldValue::Path(current_dir().unwrap()));
    }

    #[test]
    fn test_currency_field_value() {
        use iso_currency::Currency;
        use rust_decimal::Decimal;

        let currency_field = FieldValue::Currency {
            amount: Decimal::new(12345, 2), // $123.45
            currency: Currency::USD,
        };
        assert_eq!(currency_field.get_type(), FieldType::Currency);
        assert!(currency_field.is_type(&FieldType::Currency));
    }

    #[test]
    fn test_entity_reference_field_value() {
        let entity_ref =
            FieldValue::Reference(ReferenceValue::Entity(EntityId::new("test_entity")));
        assert_eq!(entity_ref.get_type(), FieldType::Reference);
        assert!(entity_ref.is_type(&FieldType::Reference));
    }

    #[test]
    fn test_field_reference_field_value() {
        let field_ref = FieldValue::Reference(ReferenceValue::Field(
            EntityId::new("test_entity"),
            FieldId::new("test_field"),
        ));
        assert_eq!(field_ref.get_type(), FieldType::Reference);
        assert!(field_ref.is_type(&FieldType::Reference));
    }

    #[test]
    fn test_list_field_value() {
        let list_field = FieldValue::List(vec![
            FieldValue::String("item1".to_string()),
            FieldValue::String("item2".to_string()),
        ]);
        assert_eq!(list_field.get_type(), FieldType::List);
        assert!(list_field.is_type(&FieldType::List));
    }

    #[test]
    fn test_boolean_serialization() {
        let field = FieldValue::Boolean(true);
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }

    #[test]
    fn test_string_serialization() {
        let field = FieldValue::String("test string".to_string());
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }

    #[test]
    fn test_integer_serialization() {
        let field = FieldValue::Integer(42);
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }

    #[test]
    fn test_float_serialization() {
        let field = FieldValue::Float(3.14);
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }

    #[test]
    fn test_currency_serialization() {
        use iso_currency::Currency;
        use rust_decimal::Decimal;

        let field = FieldValue::Currency {
            amount: Decimal::new(12345, 2),
            currency: Currency::USD,
        };
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }

    #[test]
    fn test_entity_reference_serialization() {
        let field = FieldValue::Reference(ReferenceValue::Entity(EntityId::new("entity1")));
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }

    #[test]
    fn test_field_reference_serialization() {
        let field = FieldValue::Reference(ReferenceValue::Field(
            EntityId::new("entity1"),
            FieldId::new("field1"),
        ));
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }

    #[test]
    fn test_datetime_serialization() {
        use chrono::{FixedOffset, TimeZone};

        let offset = FixedOffset::east_opt(5 * 3600).unwrap();
        let dt = offset.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
        let field = FieldValue::DateTime(dt);
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }

    #[test]
    fn test_string_list_serialization() {
        let field = FieldValue::List(vec![
            FieldValue::String("item1".to_string()),
            FieldValue::String("item2".to_string()),
        ]);
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }

    #[test]
    fn test_integer_list_serialization() {
        let field = FieldValue::List(vec![
            FieldValue::Integer(1),
            FieldValue::Integer(2),
            FieldValue::Integer(3),
        ]);
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }

    #[test]
    fn test_nested_string_list_serialization() {
        let nested_list = FieldValue::List(vec![
            FieldValue::List(vec![
                FieldValue::String("item1".to_string()),
                FieldValue::String("item2".to_string()),
            ]),
            FieldValue::List(vec![
                FieldValue::String("item3".to_string()),
                FieldValue::String("item4".to_string()),
            ]),
        ]);

        let serialized = serde_json::to_string(&nested_list).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, nested_list);
    }

    #[test]
    fn test_path_serialization() {
        let field = FieldValue::Path(current_dir().unwrap());
        let serialized = serde_json::to_string(&field).unwrap();
        let deserialized: FieldValue = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized, field);
    }
}

```

## /firm_core/src/graph/graph_errors.rs

```rs path="/firm_core/src/graph/graph_errors.rs" 
use crate::{EntityId, FieldId};

/// The types of errors you can get when interacting with the graph.
#[derive(Debug, Clone, PartialEq)]
pub enum GraphError {
    EntityAlreadyExists(EntityId),
    EntityNotFound(EntityId),
    FieldNotFound(EntityId, FieldId),
    CyclicReference,
    MaxDepthExceeded,
    NotAFieldReference,
    NotAnEntityReference,
    GraphNotBuilt,
}

```

## /firm_core/src/graph/mod.rs

```rs path="/firm_core/src/graph/mod.rs" 
use std::collections::HashMap;

use log::debug;
use petgraph::{Graph, graph::NodeIndex};
use serde::de::{MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

mod graph_errors;
mod query;

pub use graph_errors::GraphError;
pub use petgraph::Direction;

use crate::{Entity, EntityId, EntityType, FieldId, FieldValue, ReferenceValue};

/// Defines a relationship between entities in the graph.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Relationship {
    EntityReference {
        from_field: FieldId,
    },
    FieldReference {
        from_field: FieldId,
        to_field: FieldId,
    },
}

/// The entity graph tracks all Firm entities and their relationships.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityGraph {
    graph: Graph<Entity, Relationship>,
    entity_map: HashMap<EntityId, NodeIndex>,
    #[serde(
        serialize_with = "serialize_entity_type_map",
        deserialize_with = "deserialize_entity_type_map"
    )]
    entity_type_map: HashMap<EntityType, Vec<NodeIndex>>,
}

impl EntityGraph {
    /// Creates a new entity graph, ready to be populated and built.
    pub fn new() -> Self {
        Self {
            graph: Graph::new(),
            entity_map: HashMap::new(),
            entity_type_map: HashMap::new(),
        }
    }

    /// Clears graph data, allowing it to be populated and built from scratch.
    pub fn clear(&mut self) {
        debug!("Clearing graph data");
        self.graph.clear();
        self.entity_map.clear();
        self.entity_type_map.clear();
    }

    /// Adds a new entity to the graph.
    /// Note: After an entity is added, the graph should be re-built.
    pub fn add_entity(&mut self, entity: Entity) -> Result<(), GraphError> {
        debug!("Adding new entity '{}' to graph", entity.id);

        if self.entity_map.contains_key(&entity.id) {
            debug!("Entity '{}' already exists, skipping add", entity.id);
            return Err(GraphError::EntityAlreadyExists(entity.id));
        }

        let node_index = self.graph.add_node(entity.clone());
        self.entity_map.insert(entity.id.clone(), node_index);

        self.entity_type_map
            .entry(entity.entity_type)
            .or_insert_with(Vec::new)
            .push(node_index);

        Ok(())
    }

    /// Adds a collection of entities to the graph.
    pub fn add_entities(&mut self, entities: Vec<Entity>) -> Result<(), GraphError> {
        for entity in entities {
            self.add_entity(entity)?;
        }

        Ok(())
    }

    /// Builds relationships for all entities in the graph.
    ///
    /// Note: We always clear the edges and build from scratch.
    /// This means that it's best to add all your entities in bulk first, then build.
    /// The implementation could be improved by letting the relationships be progressively built.
    pub fn build(&mut self) {
        debug!(
            "Building relationships for graph with {} entities",
            self.graph.node_count()
        );

        self.graph.clear_edges();

        // Collect the edges to add first to avoid borrowing conflicts
        let mut edges_to_add = Vec::new();

        // Iterate through all entities in the graph
        for (from_node_index, node) in self.graph.raw_nodes().iter().enumerate() {
            let entity = &node.weight;

            // Iterate through all fields on the entity
            for (field_name, field_value) in &entity.fields {
                self.collect_relationships_from_field(
                    NodeIndex::new(from_node_index),
                    field_name,
                    field_value,
                    &mut edges_to_add,
                );
            }
        }

        // Add all the edges
        for (from_index, to_index, relationship) in edges_to_add {
            self.graph.add_edge(from_index, to_index, relationship);
        }
    }

    /// Map graph relationships from reference fields.
    /// We do this by populating an edge list which are later added to the graph.
    fn collect_relationships_from_field(
        &self,
        from_node: NodeIndex,
        field_name: &FieldId,
        field_value: &FieldValue,
        edges_to_add: &mut Vec<(NodeIndex, NodeIndex, Relationship)>,
    ) {
        match field_value {
            FieldValue::Reference(ReferenceValue::Entity(target_id)) => {
                if let Some(&to_node_index) = self.entity_map.get(target_id) {
                    let relationship = Relationship::EntityReference {
                        from_field: field_name.clone(),
                    };
                    edges_to_add.push((from_node, to_node_index, relationship));
                }
            }
            FieldValue::Reference(ReferenceValue::Field(target_entity_id, target_field_id)) => {
                if let Some(&to_node_index) = self.entity_map.get(target_entity_id) {
                    let relationship = Relationship::FieldReference {
                        from_field: field_name.clone(),
                        to_field: target_field_id.clone(),
                    };
                    edges_to_add.push((from_node, to_node_index, relationship));
                }
            }
            FieldValue::List(items) => {
                for item in items {
                    self.collect_relationships_from_field(
                        from_node,
                        field_name,
                        item,
                        edges_to_add,
                    );
                }
            }
            _ => {}
        }
    }
}

/// Custom serialization for the entity type map.
fn serialize_entity_type_map<S>(
    map: &HashMap<EntityType, Vec<NodeIndex>>,
    serializer: S,
) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let mut ser_map = serializer.serialize_map(Some(map.len()))?;
    for (k, v) in map {
        ser_map.serialize_entry(&k.to_string(), v)?;
    }
    ser_map.end()
}

/// Custom deserialization for the entity type map.
fn deserialize_entity_type_map<'de, D>(
    deserializer: D,
) -> Result<HashMap<EntityType, Vec<NodeIndex>>, D::Error>
where
    D: Deserializer<'de>,
{
    struct EntityTypeMapVisitor;

    impl<'de> Visitor<'de> for EntityTypeMapVisitor {
        type Value = HashMap<EntityType, Vec<NodeIndex>>;

        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            formatter.write_str("a map with string keys")
        }

        fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
        where
            M: MapAccess<'de>,
        {
            let mut map = HashMap::new();
            while let Some((key, value)) = access.next_entry::<String, Vec<NodeIndex>>()? {
                let entity_type = EntityType::from(key.as_str());
                map.insert(entity_type, value);
            }
            Ok(map)
        }
    }

    deserializer.deserialize_map(EntityTypeMapVisitor)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Entity, EntityId, EntityType, FieldValue};

    // Helper functions
    fn create_organization(id: &str, name: &str) -> Entity {
        Entity::new(EntityId::new(id), EntityType::new("organization"))
            .with_field(FieldId::new("name"), name)
    }

    fn create_person(id: &str, name: &str) -> Entity {
        Entity::new(EntityId::new(id), EntityType::new("person"))
            .with_field(FieldId::new("name"), name)
    }

    fn create_person_with_employer(id: &str, name: &str, employer_id: &str) -> Entity {
        Entity::new(EntityId::new(id), EntityType::new("person"))
            .with_field(FieldId::new("name"), name)
            .with_field(
                FieldId::new("employer"),
                FieldValue::Reference(ReferenceValue::Entity(EntityId::new(employer_id))),
            )
    }

    fn setup_basic_graph() -> (EntityGraph, Entity, Entity) {
        let graph = EntityGraph::new();
        let organization = create_organization("megacorp", "MegaCorp Inc.");
        let person = create_person_with_employer("john_doe", "John Doe", "megacorp");
        (graph, organization, person)
    }

    fn assert_basic_graph_structure(graph: &EntityGraph) {
        assert!(graph.entity_map.contains_key(&EntityId::new("megacorp")));
        assert!(graph.entity_map.contains_key(&EntityId::new("john_doe")));
        assert_eq!(graph.graph.edge_count(), 1);
    }

    #[test]
    fn test_build_graph_iteratively() {
        let (mut graph, organization, person) = setup_basic_graph();

        graph.add_entity(organization).unwrap();
        graph.add_entity(person).unwrap();
        graph.build();

        assert_basic_graph_structure(&graph);
    }

    #[test]
    fn test_build_graph_bulk() {
        let (mut graph, organization, person) = setup_basic_graph();

        graph.add_entities(vec![organization, person]).unwrap();
        graph.build();

        assert_basic_graph_structure(&graph);
    }

    #[test]
    fn test_add_duplicate_entity_returns_error() {
        let mut graph = EntityGraph::new();
        let entity1 = create_person("duplicate_id", "First Entity");

        assert!(graph.add_entity(entity1).is_ok());

        let entity2 = create_organization("duplicate_id", "Second Entity");
        let result = graph.add_entity(entity2);

        assert!(result.is_err());
        if let Err(GraphError::EntityAlreadyExists(entity_id)) = result {
            assert_eq!(entity_id, EntityId::new("duplicate_id"));
        }

        assert_eq!(graph.entity_map.len(), 1);
        assert_eq!(graph.graph.node_count(), 1);
    }

    #[test]
    fn test_add_entities_stops_on_duplicate() {
        let mut graph = EntityGraph::new();
        graph.add_entity(create_person("first", "First")).unwrap();

        let entities = vec![
            create_person("second", "Second"),
            create_organization("first", "Duplicate"),
            create_person("third", "Third"),
        ];

        assert!(graph.add_entities(entities).is_err());
        assert_eq!(graph.entity_map.len(), 2);
        assert!(graph.entity_map.contains_key(&EntityId::new("first")));
        assert!(graph.entity_map.contains_key(&EntityId::new("second")));
        assert!(!graph.entity_map.contains_key(&EntityId::new("third")));
    }

    #[test]
    fn test_list_field_references() {
        let mut graph = EntityGraph::new();

        let person1 =
            create_person("john", "John Doe").with_field(FieldId::new("email"), "john@example.com");
        let person2 = create_person("jane", "Jane Smith")
            .with_field(FieldId::new("email"), "jane@example.com");

        let email_campaign = create_organization("campaign1", "Newsletter Campaign").with_field(
            FieldId::new("recipient_emails"),
            FieldValue::List(vec![
                FieldValue::Reference(ReferenceValue::Field(
                    EntityId::new("john"),
                    FieldId::new("email"),
                )),
                FieldValue::Reference(ReferenceValue::Field(
                    EntityId::new("jane"),
                    FieldId::new("email"),
                )),
            ]),
        );

        graph
            .add_entities(vec![person1, person2, email_campaign])
            .unwrap();
        graph.build();

        assert_eq!(graph.graph.edge_count(), 2);
        assert_eq!(graph.graph.node_count(), 3);
    }

    fn test_serialization_roundtrip(graph: &EntityGraph) -> EntityGraph {
        let serialized = serde_json::to_string(graph).unwrap();
        serde_json::from_str(&serialized).unwrap()
    }

    #[test]
    fn test_graph_is_serializable() {
        let (mut graph, organization, person) = setup_basic_graph();
        graph.add_entities(vec![organization, person]).unwrap();
        graph.build();

        let deserialized = test_serialization_roundtrip(&graph);
        assert_basic_graph_structure(&deserialized);
    }

    #[test]
    fn test_graph_with_currency_field_serialization() {
        use iso_currency::Currency;
        use rust_decimal::Decimal;

        let mut graph = EntityGraph::new();
        let expected_amount = Decimal::new(12345, 2);
        let expected_currency = Currency::USD;

        let entity = create_organization("test_entity", "Test").with_field(
            FieldId::new("price"),
            FieldValue::Currency {
                amount: expected_amount,
                currency: expected_currency,
            },
        );

        graph.add_entity(entity).unwrap();
        graph.build();

        let deserialized = test_serialization_roundtrip(&graph);
        let node_idx = deserialized.entity_map[&EntityId::new("test_entity")];
        let node = &deserialized.graph[node_idx];

        if let Some(FieldValue::Currency { amount, currency }) =
            node.get_field(&FieldId::new("price"))
        {
            assert_eq!(*amount, expected_amount);
            assert_eq!(*currency, expected_currency);
        } else {
            panic!("Currency field not preserved");
        }
    }

    #[test]
    fn test_graph_with_datetime_field_serialization() {
        use chrono::{FixedOffset, TimeZone};

        let mut graph = EntityGraph::new();
        let offset = FixedOffset::east_opt(5 * 3600).unwrap();
        let expected_dt = offset.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();

        let entity = create_organization("test_entity", "Test").with_field(
            FieldId::new("created_at"),
            FieldValue::DateTime(expected_dt),
        );

        graph.add_entity(entity).unwrap();
        graph.build();

        let deserialized = test_serialization_roundtrip(&graph);
        let node_idx = deserialized.entity_map[&EntityId::new("test_entity")];
        let node = &deserialized.graph[node_idx];

        assert_eq!(
            node.get_field(&FieldId::new("created_at")),
            Some(&FieldValue::DateTime(expected_dt))
        );
    }

    #[test]
    fn test_graph_list_serialization() {
        let mut graph = EntityGraph::new();
        let expected_tags = vec![
            FieldValue::String("tag1".to_string()),
            FieldValue::String("tag2".to_string()),
            FieldValue::String("tag3".to_string()),
        ];

        let entity = create_organization("test_entity", "Test").with_field(
            FieldId::new("tags"),
            FieldValue::List(expected_tags.clone()),
        );

        graph.add_entity(entity).unwrap();
        graph.build();

        let deserialized = test_serialization_roundtrip(&graph);
        let node_idx = deserialized.entity_map[&EntityId::new("test_entity")];
        let node = &deserialized.graph[node_idx];

        if let Some(FieldValue::List(items)) = node.get_field(&FieldId::new("tags")) {
            assert_eq!(*items, expected_tags);
        } else {
            panic!("List field not preserved");
        }
    }

    #[test]
    fn test_graph_with_field_reference_serialization() {
        let mut graph = EntityGraph::new();
        let target_entity = create_person("target", "Target");
        let source_entity = create_organization("source", "Source").with_field(
            FieldId::new("ref_field"),
            FieldValue::Reference(ReferenceValue::Field(
                EntityId::new("target"),
                FieldId::new("name"),
            )),
        );

        graph
            .add_entities(vec![target_entity, source_entity])
            .unwrap();
        graph.build();

        let deserialized = test_serialization_roundtrip(&graph);
        assert_eq!(deserialized.graph.node_count(), 2);
        assert_eq!(deserialized.graph.edge_count(), 1);

        let source_idx = deserialized.entity_map[&EntityId::new("source")];
        let source_node = &deserialized.graph[source_idx];

        assert_eq!(
            source_node.get_field(&FieldId::new("ref_field")),
            Some(&FieldValue::Reference(ReferenceValue::Field(
                EntityId::new("target"),
                FieldId::new("name")
            )))
        );
    }

    #[test]
    fn test_entity_id_hashmap_serialization() {
        use std::collections::HashMap;

        let mut map = HashMap::new();
        map.insert(EntityId::new("test1"), 42);
        map.insert(EntityId::new("test2"), 84);

        let serialized = serde_json::to_string(&map).unwrap();
        println!("EntityId HashMap serialized: {}", serialized);

        let deserialized: HashMap<EntityId, i32> = serde_json::from_str(&serialized).unwrap();
        assert_eq!(deserialized.len(), 2);
        assert_eq!(deserialized[&EntityId::new("test1")], 42);
        assert_eq!(deserialized[&EntityId::new("test2")], 84);
    }
}

```

## /firm_core/src/graph/query.rs

```rs path="/firm_core/src/graph/query.rs" 
use log::debug;
use petgraph::{Direction, visit::EdgeRef};

use super::{EntityGraph, GraphError, Relationship};
use crate::{Entity, EntityId, EntityType, FieldId, FieldValue, ReferenceValue};

use std::collections::HashSet;

impl EntityGraph {
    /// Gets an entity in the graph by its ID.
    pub fn get_entity(&self, id: &EntityId) -> Option<&Entity> {
        debug!("Looking up entity '{}'", id);

        self.entity_map
            .get(id)
            .and_then(|&node_index| self.graph.node_weight(node_index))
    }

    /// Resolves an entity reference to the actual entity.
    pub fn resolve_entity_reference(
        &self,
        field_value: &FieldValue,
    ) -> Result<&Entity, GraphError> {
        debug!("Resolving entity reference: {:?}", field_value);

        match field_value {
            FieldValue::Reference(ReferenceValue::Entity(entity_id)) => self
                .get_entity(entity_id)
                .ok_or_else(|| GraphError::EntityNotFound(entity_id.clone())),
            _ => Err(GraphError::NotAnEntityReference),
        }
    }

    /// Resolves a field reference to the actual field value.
    pub fn resolve_field_reference(
        &self,
        field_value: &FieldValue,
    ) -> Result<&FieldValue, GraphError> {
        debug!("Resolving field reference: {:?}", field_value);

        match field_value {
            FieldValue::Reference(ReferenceValue::Field(entity_id, field_id)) => {
                self.search_field_reference(entity_id, field_id, 10, &mut HashSet::new())
            }
            _ => Err(GraphError::NotAFieldReference),
        }
    }

    /// Gets a collection of all entity types present.
    pub fn get_all_entity_types(&self) -> Vec<EntityType> {
        self.entity_type_map.keys().cloned().collect()
    }

    /// Gets all entities of a specific type.
    pub fn list_by_type(&self, entity_type: &EntityType) -> Vec<&Entity> {
        match self.entity_type_map.get(entity_type) {
            Some(nodes) => nodes
                .iter()
                .filter_map(|&node_index| self.graph.node_weight(node_index))
                .collect(),
            None => Vec::new(),
        }
    }

    /// Gets all entities that references an entity ID.
    ///
    /// Edges in the graph are directed, and here we can choose if we want only
    /// incoming references, outgoing references or both.
    pub fn get_related(&self, id: &EntityId, direction: Option<Direction>) -> Option<Vec<&Entity>> {
        match self.entity_map.get(id) {
            Some(node_index) => {
                let mut entities: Vec<&Entity> = match direction {
                    Some(Direction::Outgoing) => self
                        .graph
                        .edges_directed(node_index.clone(), Direction::Outgoing)
                        .map(|edge| &self.graph[edge.target()])
                        .collect(),
                    Some(Direction::Incoming) => self
                        .graph
                        .edges_directed(node_index.clone(), Direction::Incoming)
                        .map(|edge| &self.graph[edge.source()])
                        .collect(),
                    None => {
                        let mut all_entities = Vec::new();

                        // Collect targets of outgoing edges
                        all_entities.extend(
                            self.graph
                                .edges_directed(node_index.clone(), Direction::Outgoing)
                                .map(|edge| &self.graph[edge.target()]),
                        );

                        // Collect sources of incoming edges
                        all_entities.extend(
                            self.graph
                                .edges_directed(node_index.clone(), Direction::Incoming)
                                .map(|edge| &self.graph[edge.source()]),
                        );

                        all_entities
                    }
                };

                entities.sort_by_key(|entity| &entity.id);
                entities.dedup_by_key(|entity| &entity.id);

                Some(entities)
            }
            None => None,
        }
    }

    /// Searches for a field reference on a given entity by traversing the graph
    fn search_field_reference(
        &self,
        entity_id: &EntityId,
        field_id: &FieldId,
        max_depth: usize,
        visited: &mut HashSet<(EntityId, FieldId)>,
    ) -> Result<&FieldValue, GraphError> {
        if max_depth == 0 {
            debug!(
                "Max depth exceeded for field reference: {}.{}",
                entity_id, field_id
            );

            return Err(GraphError::MaxDepthExceeded);
        }

        // Check for cycles
        let reference_key = (entity_id.clone(), field_id.clone());
        if visited.contains(&reference_key) {
            debug!("Cyclic reference detected: {}.{}", entity_id, field_id);
            return Err(GraphError::CyclicReference);
        }
        visited.insert(reference_key);

        // Get entity
        let entity = self
            .get_entity(entity_id)
            .ok_or_else(|| GraphError::EntityNotFound(entity_id.clone()))?;

        // Get field
        let field = entity
            .get_field(field_id)
            .ok_or_else(|| GraphError::FieldNotFound(entity_id.clone(), field_id.clone()))?;

        // If it's another field reference, resolve it
        match field {
            FieldValue::Reference(ReferenceValue::Field(target_entity_id, target_field_id)) => {
                // Use graph traversal to find the target
                let source_node = self
                    .entity_map
                    .get(entity_id)
                    .ok_or_else(|| GraphError::EntityNotFound(entity_id.clone()))?;
                let target_node = self
                    .entity_map
                    .get(target_entity_id)
                    .ok_or_else(|| GraphError::EntityNotFound(target_entity_id.clone()))?;

                // Check if there's a field reference edge between these nodes
                let mut edge_found = false;
                for edge in self.graph.edges_connecting(*source_node, *target_node) {
                    if let Relationship::FieldReference {
                        from_field,
                        to_field,
                    } = edge.weight()
                    {
                        if from_field == field_id && to_field == target_field_id {
                            edge_found = true;
                            break;
                        }
                    }
                }

                if edge_found {
                    self.search_field_reference(
                        target_entity_id,
                        target_field_id,
                        max_depth - 1,
                        visited,
                    )
                } else {
                    Err(GraphError::GraphNotBuilt)
                }
            }
            _ => Ok(field),
        }
    }
}

impl FieldValue {
    /// Convenience method to resolve entity references directly on field values.
    pub fn resolve_entity_reference<'a>(
        &'a self,
        graph: &'a EntityGraph,
    ) -> Result<&'a Entity, GraphError> {
        graph.resolve_entity_reference(self)
    }

    /// Convenience method to resolve field references directly on field values.
    pub fn resolve_field_reference<'a>(
        &'a self,
        graph: &'a EntityGraph,
    ) -> Result<&'a FieldValue, GraphError> {
        graph.resolve_field_reference(self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{EntityType, FieldId};

    #[test]
    fn test_get_entity_by_id() {
        let mut graph = EntityGraph::new();

        let organization = Entity::new(EntityId::new("megacorp"), EntityType::new("organization"))
            .with_field(FieldId::new("name"), "MegaCorp Inc.");

        let person = Entity::new(EntityId::new("john_doe"), EntityType::new("person"))
            .with_field(FieldId::new("name"), "John Doe");

        graph
            .add_entities(vec![organization.clone(), person.clone()])
            .unwrap();

        // Test existing entities
        let retrieved_organization = graph.get_entity(&EntityId::new("megacorp"));
        assert!(retrieved_organization.is_some());
        assert_eq!(
            retrieved_organization.unwrap().id,
            EntityId::new("megacorp")
        );

        let retrieved_person = graph.get_entity(&EntityId::new("john_doe"));
        assert!(retrieved_person.is_some());
        assert_eq!(retrieved_person.unwrap().id, EntityId::new("john_doe"));

        // Test non-existing entity
        let non_existing = graph.get_entity(&EntityId::new("non_existing"));
        assert!(non_existing.is_none());
    }

    #[test]
    fn test_resolve_entity_reference_from_graph() {
        let mut graph = EntityGraph::new();

        let organization = Entity::new(EntityId::new("megacorp"), EntityType::new("organization"))
            .with_field(FieldId::new("name"), "MegaCorp Inc.");

        graph.add_entity(organization).unwrap();

        // Test valid entity reference
        let entity_ref = FieldValue::Reference(ReferenceValue::Entity(EntityId::new("megacorp")));
        let resolved = graph.resolve_entity_reference(&entity_ref);
        assert!(resolved.is_ok());
        assert_eq!(resolved.unwrap().id, EntityId::new("megacorp"));

        // Test invalid entity reference
        let invalid_ref =
            FieldValue::Reference(ReferenceValue::Entity(EntityId::new("non_existing")));
        let resolved_invalid = graph.resolve_entity_reference(&invalid_ref);
        assert!(resolved_invalid.is_err());
        assert_eq!(
            resolved_invalid.unwrap_err(),
            GraphError::EntityNotFound(EntityId::new("non_existing"))
        );

        // Test non-entity-reference field value
        let string_field = FieldValue::String("not a reference".to_string());
        let resolved_string = graph.resolve_entity_reference(&string_field);
        assert!(resolved_string.is_err());
        assert_eq!(
            resolved_string.unwrap_err(),
            GraphError::NotAnEntityReference
        );

        let bool_field = FieldValue::Boolean(true);
        let resolved_bool = graph.resolve_entity_reference(&bool_field);
        assert!(resolved_bool.is_err());
        assert_eq!(resolved_bool.unwrap_err(), GraphError::NotAnEntityReference);
    }

    #[test]
    fn test_resolve_field_reference_simple() {
        let mut graph = EntityGraph::new();

        let entity = Entity::new(EntityId::new("test_entity"), EntityType::new("person"))
            .with_field(FieldId::new("name"), "John Doe")
            .with_field(
                FieldId::new("name_ref"),
                FieldValue::Reference(ReferenceValue::Field(
                    EntityId::new("test_entity"),
                    FieldId::new("name"),
                )),
            );

        graph.add_entity(entity).unwrap();
        graph.build(); // Build the graph edges

        // Test resolving field reference
        let field_ref = FieldValue::Reference(ReferenceValue::Field(
            EntityId::new("test_entity"),
            FieldId::new("name_ref"),
        ));
        let resolved = graph.resolve_field_reference(&field_ref);
        assert!(resolved.is_ok());
        assert_eq!(
            resolved.unwrap(),
            &FieldValue::String("John Doe".to_string())
        );
    }

    #[test]
    fn test_resolve_field_reference_chain() {
        let mut graph = EntityGraph::new();

        let entity = Entity::new(EntityId::new("test_entity"), EntityType::new("person"))
            .with_field(FieldId::new("name"), "John Doe")
            .with_field(
                FieldId::new("name_ref1"),
                FieldValue::Reference(ReferenceValue::Field(
                    EntityId::new("test_entity"),
                    FieldId::new("name"),
                )),
            )
            .with_field(
                FieldId::new("name_ref2"),
                FieldValue::Reference(ReferenceValue::Field(
                    EntityId::new("test_entity"),
                    FieldId::new("name_ref1"),
                )),
            );

        graph.add_entity(entity).unwrap();
        graph.build(); // Build the graph edges

        // Test resolving chained field reference
        let field_ref = FieldValue::Reference(ReferenceValue::Field(
            EntityId::new("test_entity"),
            FieldId::new("name_ref2"),
        ));
        let resolved = graph.resolve_field_reference(&field_ref);
        assert!(resolved.is_ok());
        assert_eq!(
            resolved.unwrap(),
            &FieldValue::String("John Doe".to_string())
        );
    }

    #[test]
    fn test_resolve_field_reference_cycle_detection() {
        let mut graph = EntityGraph::new();

        let entity = Entity::new(EntityId::new("test_entity"), EntityType::new("person"))
            .with_field(
                FieldId::new("ref1"),
                FieldValue::Reference(ReferenceValue::Field(
                    EntityId::new("test_entity"),
                    FieldId::new("ref2"),
                )),
            )
            .with_field(
                FieldId::new("ref2"),
                FieldValue::Reference(ReferenceValue::Field(
                    EntityId::new("test_entity"),
                    FieldId::new("ref1"),
                )),
            );

        graph.add_entity(entity).unwrap();
        graph.build(); // Build the graph edges

        // Test cycle detection
        let field_ref = FieldValue::Reference(ReferenceValue::Field(
            EntityId::new("test_entity"),
            FieldId::new("ref1"),
        ));
        let resolved = graph.resolve_field_reference(&field_ref);
        assert!(resolved.is_err());
        assert_eq!(resolved.unwrap_err(), GraphError::CyclicReference);
    }

    #[test]
    fn test_resolve_field_reference_max_depth() {
        let mut graph = EntityGraph::new();

        // Create a chain of 15 field references (exceeds default limit of 10)
        let mut entity = Entity::new(EntityId::new("test_entity"), EntityType::new("person"))
            .with_field(FieldId::new("final"), "Final Value");

        for i in 0..15 {
            let field_name = format!("ref{}", i);
            let next_field = if i == 14 {
                FieldId::new("final")
            } else {
                FieldId::new(&format!("ref{}", i + 1))
            };

            entity = entity.with_field(
                FieldId::new(&field_name),
                FieldValue::Reference(ReferenceValue::Field(
                    EntityId::new("test_entity"),
                    next_field,
                )),
            );
        }

        graph.add_entity(entity).unwrap();
        graph.build(); // Build the graph edges

        // Test max depth exceeded
        let field_ref = FieldValue::Reference(ReferenceValue::Field(
            EntityId::new("test_entity"),
            FieldId::new("ref0"),
        ));
        let resolved = graph.resolve_field_reference(&field_ref);
        assert!(resolved.is_err());
        assert_eq!(resolved.unwrap_err(), GraphError::MaxDepthExceeded);
    }

    #[test]
    fn test_resolve_field_reference_entity_not_found() {
        let graph = EntityGraph::new();

        let field_ref = FieldValue::Reference(ReferenceValue::Field(
            EntityId::new("missing_entity"),
            FieldId::new("field"),
        ));
        let resolved = graph.resolve_field_reference(&field_ref);
        assert!(resolved.is_err());
        assert_eq!(
            resolved.unwrap_err(),
            GraphError::EntityNotFound(EntityId::new("missing_entity"))
        );
    }

    #[test]
    fn test_resolve_field_reference_field_not_found() {
        let mut graph = EntityGraph::new();

        let entity = Entity::new(EntityId::new("test_entity"), EntityType::new("person"))
            .with_field(FieldId::new("existing_field"), "value");

        graph.add_entity(entity).unwrap();

        let field_ref = FieldValue::Reference(ReferenceValue::Field(
            EntityId::new("test_entity"),
            FieldId::new("missing_field"),
        ));
        let resolved = graph.resolve_field_reference(&field_ref);
        assert!(resolved.is_err());
        assert_eq!(
            resolved.unwrap_err(),
            GraphError::FieldNotFound(EntityId::new("test_entity"), FieldId::new("missing_field"))
        );
    }

    #[test]
    fn test_resolve_field_reference_not_a_reference() {
        let graph = EntityGraph::new();

        // Test with non-field-reference values
        let string_field = FieldValue::String("not a reference".to_string());
        let resolved = graph.resolve_field_reference(&string_field);
        assert!(resolved.is_err());
        assert_eq!(resolved.unwrap_err(), GraphError::NotAFieldReference);

        let bool_field = FieldValue::Boolean(true);
        let resolved = graph.resolve_field_reference(&bool_field);
        assert!(resolved.is_err());
        assert_eq!(resolved.unwrap_err(), GraphError::NotAFieldReference);

        let entity_ref = FieldValue::Reference(ReferenceValue::Entity(EntityId::new("entity")));
        let resolved = graph.resolve_field_reference(&entity_ref);
        assert!(resolved.is_err());
        assert_eq!(resolved.unwrap_err(), GraphError::NotAFieldReference);
    }

    #[test]
    fn test_resolve_field_reference_graph_not_built() {
        let mut graph = EntityGraph::new();

        let entity1 = Entity::new(EntityId::new("entity1"), EntityType::new("person"))
            .with_field(FieldId::new("name"), "Entity 1")
            .with_field(
                FieldId::new("ref_to_2"),
                FieldValue::Reference(ReferenceValue::Field(
                    EntityId::new("entity2"),
                    FieldId::new("value"),
                )),
            );

        let entity2 = Entity::new(EntityId::new("entity2"), EntityType::new("person"))
            .with_field(FieldId::new("value"), "Entity 2 Value");

        graph.add_entities(vec![entity1, entity2]).unwrap();
        // Intentionally NOT calling graph.build()

        let field_ref = FieldValue::Reference(ReferenceValue::Field(
            EntityId::new("entity1"),
            FieldId::new("ref_to_2"),
        ));
        let resolved = graph.resolve_field_reference(&field_ref);
        assert!(resolved.is_err());
        assert_eq!(resolved.unwrap_err(), GraphError::GraphNotBuilt);
    }

    #[test]
    fn test_field_value_resolve_entity_reference_convenience() {
        let mut graph = EntityGraph::new();

        let organization = Entity::new(EntityId::new("megacorp"), EntityType::new("organization"))
            .with_field(FieldId::new("name"), "MegaCorp Inc.");

        graph.add_entity(organization).unwrap();

        // Test convenience method on EntityReference
        let entity_ref = FieldValue::Reference(ReferenceValue::Entity(EntityId::new("megacorp")));
        let resolved = entity_ref.resolve_entity_reference(&graph);
        assert!(resolved.is_ok());
        assert_eq!(resolved.unwrap().id, EntityId::new("megacorp"));

        // Test convenience method on non-EntityReference
        let string_field = FieldValue::String("not a reference".to_string());
        let resolved = string_field.resolve_entity_reference(&graph);
        assert!(resolved.is_err());
        assert_eq!(resolved.unwrap_err(), GraphError::NotAnEntityReference);
    }

    #[test]
    fn test_field_value_resolve_field_reference_convenience() {
        let mut graph = EntityGraph::new();

        let entity = Entity::new(EntityId::new("test_entity"), EntityType::new("person"))
            .with_field(FieldId::new("name"), "John Doe")
            .with_field(
                FieldId::new("name_ref"),
                FieldValue::Reference(ReferenceValue::Field(
                    EntityId::new("test_entity"),
                    FieldId::new("name"),
                )),
            );

        graph.add_entity(entity).unwrap();
        graph.build();

        // Test convenience method on FieldReference
        let field_ref = FieldValue::Reference(ReferenceValue::Field(
            EntityId::new("test_entity"),
            FieldId::new("name_ref"),
        ));
        let resolved = field_ref.resolve_field_reference(&graph);
        assert!(resolved.is_ok());
        assert_eq!(
            resolved.unwrap(),
            &FieldValue::String("John Doe".to_string())
        );

        // Test convenience method on non-FieldReference
        let string_field = FieldValue::String("not a reference".to_string());
        let resolved = string_field.resolve_field_reference(&graph);
        assert!(resolved.is_err());
    }

    #[test]
    fn test_list_by_type() {
        let mut graph = EntityGraph::new();

        // Create entities of different types
        let organization1 = Entity::new(EntityId::new("megacorp"), EntityType::new("organization"))
            .with_field(FieldId::new("name"), "MegaCorp Inc.");

        let organization2 = Entity::new(EntityId::new("techcorp"), EntityType::new("organization"))
            .with_field(FieldId::new("name"), "TechCorp Ltd.");

        let person1 = Entity::new(EntityId::new("john_doe"), EntityType::new("person"))
            .with_field(FieldId::new("name"), "John Doe");

        let person2 = Entity::new(EntityId::new("jane_smith"), EntityType::new("person"))
            .with_field(FieldId::new("name"), "Jane Smith");

        graph
            .add_entities(vec![
                organization1.clone(),
                organization2.clone(),
                person1.clone(),
                person2.clone(),
            ])
            .unwrap();

        // Test listing organizations
        let organizations = graph.list_by_type(&EntityType::new("organization"));
        assert_eq!(organizations.len(), 2);
        let org_ids: Vec<&EntityId> = organizations.iter().map(|e| &e.id).collect();
        assert!(org_ids.contains(&&EntityId::new("megacorp")));
        assert!(org_ids.contains(&&EntityId::new("techcorp")));

        // Test listing persons
        let persons = graph.list_by_type(&EntityType::new("person"));
        assert_eq!(persons.len(), 2);
        let person_ids: Vec<&EntityId> = persons.iter().map(|e| &e.id).collect();
        assert!(person_ids.contains(&&EntityId::new("john_doe")));
        assert!(person_ids.contains(&&EntityId::new("jane_smith")));

        // Test non-existing type
        let projects = graph.list_by_type(&EntityType::new("missing_project"));
        assert_eq!(projects.len(), 0);
    }

    #[test]
    fn test_get_related() {
        let mut graph = EntityGraph::new();

        // Create entities with relationships
        let organization = Entity::new(EntityId::new("megacorp"), EntityType::new("organization"))
            .with_field(FieldId::new("name"), "MegaCorp Inc.");

        let person1 = Entity::new(EntityId::new("john_doe"), EntityType::new("person"))
            .with_field(FieldId::new("name"), "John Doe")
            .with_field(
                FieldId::new("employer"),
                FieldValue::Reference(ReferenceValue::Entity(EntityId::new("megacorp"))),
            );

        let person2 = Entity::new(EntityId::new("jane_smith"), EntityType::new("person"))
            .with_field(FieldId::new("name"), "Jane Smith")
            .with_field(
                FieldId::new("employer"),
                FieldValue::Reference(ReferenceValue::Entity(EntityId::new("megacorp"))),
            );

        graph
            .add_entities(vec![organization.clone(), person1.clone(), person2.clone()])
            .unwrap();
        graph.build();

        // Test getting all related entities (both directions)
        let related_to_megacorp = graph.get_related(&EntityId::new("megacorp"), None);
        assert!(related_to_megacorp.is_some());
        let related = related_to_megacorp.unwrap();
        assert_eq!(related.len(), 2);

        let related_ids: Vec<&EntityId> = related.iter().map(|e| &e.id).collect();
        assert!(related_ids.contains(&&EntityId::new("john_doe")));
        assert!(related_ids.contains(&&EntityId::new("jane_smith")));

        // Test getting related entities in specific direction
        let incoming = graph.get_related(&EntityId::new("megacorp"), Some(Direction::Incoming));
        assert!(incoming.is_some());
        let incoming_entities = incoming.unwrap();
        assert_eq!(incoming_entities.len(), 2);

        let outgoing = graph.get_related(&EntityId::new("john_doe"), Some(Direction::Outgoing));
        assert!(outgoing.is_some());
        let outgoing_entities = outgoing.unwrap();
        assert_eq!(outgoing_entities.len(), 1);
        assert_eq!(outgoing_entities[0].id, EntityId::new("megacorp"));

        // Test non-existing entity
        let non_existing = graph.get_related(&EntityId::new("non_existing"), None);
        assert!(non_existing.is_none());
    }
}

```

## /firm_core/src/id.rs

```rs path="/firm_core/src/id.rs" 
use convert_case::{Case, Casing};
use serde::{Deserialize, Serialize};
use std::fmt;

/// Creates a typed identifier based on an underlying string.
/// This helps differentiate identifiers so that they are not accidentally mixed.
/// By convention, we convert the underlying value to snake_case.
macro_rules! typed_string_id {
    ($name:ident) => {
        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
        pub struct $name(pub String);

        impl $name {
            pub fn new(id: impl Into<String>) -> Self {
                Self(id.into().to_case(Case::Snake))
            }

            pub fn as_str(&self) -> &str {
                &self.0
            }
        }

        impl From<&str> for $name {
            fn from(value: &str) -> Self {
                Self(value.to_string())
            }
        }

        impl From<String> for $name {
            fn from(value: String) -> Self {
                Self(value)
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "{}", self.0)
            }
        }
    };
}

typed_string_id!(EntityId);
typed_string_id!(FieldId);
typed_string_id!(EntityType);

/// Creates a standard composite Entity ID from the entity type and ID.
/// This allows entities of different types to share the same ID.
pub fn compose_entity_id(entity_type: &str, entity_id: &str) -> EntityId {
    EntityId::new(format!(
        "{}.{}",
        entity_type.to_string().to_lowercase(),
        entity_id
    ))
}

/// Decomposes a standard composite Entity ID into its entity type and ID components.
pub fn decompose_entity_id(composite_id: &str) -> (&str, &str) {
    composite_id
        .split_once('.')
        .unwrap_or(("unknown", composite_id))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_preserves_snake_case() {
        let id = EntityId::new("john_doe");
        assert_eq!(id.to_string(), "john_doe");
    }

    #[test]
    fn test_preserves_period() {
        let id = EntityId::new("person.john_doe");
        assert_eq!(id.to_string(), "person.john_doe");
    }

    #[test]
    fn test_converts_to_snake_case() {
        let sentence_case_id = EntityId::new("John Doe");
        assert_eq!(sentence_case_id.to_string(), "john_doe");

        let pascal_case_id = EntityId::new("JohnDoe");
        assert_eq!(pascal_case_id.to_string(), "john_doe");

        let camel_case_id = EntityId::new("johnDoe");
        assert_eq!(camel_case_id.to_string(), "john_doe");
    }

    #[test]
    fn test_preserves_period_when_converted_to_snake_case() {
        let sentence_case_id = EntityId::new("Person.John Doe");
        assert_eq!(sentence_case_id.to_string(), "person.john_doe");

        let pascal_case_id = EntityId::new("Person.JohnDoe");
        assert_eq!(pascal_case_id.to_string(), "person.john_doe");

        let camel_case_id = EntityId::new("person.johnDoe");
        assert_eq!(camel_case_id.to_string(), "person.john_doe");
    }
}

```

## /firm_core/src/lib.rs

```rs path="/firm_core/src/lib.rs" 
//! Core data structures and graph operations for Firm.
//!
//! This crate provides the fundamental building blocks for managing
//! business entities, their associated data and their relationships.

pub mod entity;
pub mod field;
pub mod graph;
pub mod id;
pub mod schema;

pub use entity::Entity;
pub use field::{FieldType, FieldValue, ReferenceValue};
pub use id::{EntityId, EntityType, FieldId, compose_entity_id, decompose_entity_id};
pub use schema::EntitySchema;

```

## /firm_core/src/schema/builtin.rs

```rs path="/firm_core/src/schema/builtin.rs" 
use crate::{EntitySchema, EntityType, FieldId, FieldType};

impl EntitySchema {
    /// Instantiates all built-in schemas.
    pub fn all_builtin() -> Vec<EntitySchema> {
        vec![
            // Core entities
            EntitySchema::person(),
            EntitySchema::organization(),
            EntitySchema::industry(),
            // Customer relations
            EntitySchema::account(),
            EntitySchema::channel(),
            EntitySchema::lead(),
            EntitySchema::contact(),
            EntitySchema::interaction(),
            EntitySchema::opportunity(),
            // Work management
            EntitySchema::strategy(),
            EntitySchema::objective(),
            EntitySchema::key_result(),
            EntitySchema::project(),
            EntitySchema::task(),
            EntitySchema::review(),
            // Resources
            EntitySchema::file_asset(),
        ]
    }

    /// An individual person (an Agent in the REA model).
    ///
    /// This is a fundamental entity models people, whether they are employees, customers, or partners.
    pub fn person() -> Self {
        Self::new(EntityType::new("person"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_optional_field(FieldId::new("email"), FieldType::String)
            .with_optional_field(FieldId::new("phone"), FieldType::String)
            .with_optional_field(FieldId::new("urls"), FieldType::List)
    }

    /// An organization, company, or group (an Agent in the REA model).
    ///
    /// A fundamental entity for modeling businesses, institutions, or collections of people.
    pub fn organization() -> Self {
        Self::new(EntityType::new("organization"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_optional_field(FieldId::new("address"), FieldType::String)
            .with_optional_field(FieldId::new("email"), FieldType::String)
            .with_optional_field(FieldId::new("phone"), FieldType::String)
            .with_optional_field(FieldId::new("urls"), FieldType::List)
            .with_optional_field(FieldId::new("vat_id"), FieldType::String)
            .with_optional_field(FieldId::new("industry_ref"), FieldType::Reference)
    }

    /// Represents an industry or business sector.
    ///
    /// This entity is used to classify organizations, helping to categorize and query
    /// businesses by their area of operation.
    pub fn industry() -> Self {
        Self::new(EntityType::new("industry"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_optional_field(FieldId::new("sector"), FieldType::String)
            .with_optional_field(FieldId::new("classification_code"), FieldType::String)
            .with_optional_field(FieldId::new("classification_system"), FieldType::String)
    }

    /// Represents a business relationship with an organization, typically a customer.
    ///
    /// This is a contextual entity that links to an organization and tracks the state
    /// of your relationship with them.
    pub fn account() -> Self {
        Self::new(EntityType::new("account"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_required_field(FieldId::new("organization_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("owner_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("status"), FieldType::String)
    }

    /// Represents a communication or marketing channel.
    ///
    /// Used to track where interactions, leads, and opportunities originate from,
    /// such as "Email", "Website", or "Conference".
    pub fn channel() -> Self {
        Self::new(EntityType::new("channel"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_optional_field(FieldId::new("type"), FieldType::String)
            .with_optional_field(FieldId::new("description"), FieldType::String)
    }

    /// Represents a potential business lead.
    ///
    /// A contextual entity that captures an initial expression of interest. It typically
    /// references a person, contact or account and tracks its qualification status.
    pub fn lead() -> Self {
        Self::new(EntityType::new("lead"))
            .with_metadata()
            .with_required_field(FieldId::new("source_ref"), FieldType::Reference)
            .with_required_field(FieldId::new("status"), FieldType::String)
            .with_optional_field(FieldId::new("person_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("account_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("score"), FieldType::Integer)
    }

    /// Represents a person in the context of a business relationship.
    ///
    /// This contextual entity links a fundamental person to an account or other business
    /// context, defining their role and status.
    pub fn contact() -> Self {
        Self::new(EntityType::new("contact"))
            .with_metadata()
            .with_optional_field(FieldId::new("source_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("person_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("account_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("role"), FieldType::String)
            .with_optional_field(FieldId::new("status"), FieldType::String)
    }

    /// Represents a specific interaction or communication (an Event in the REA model).
    ///
    /// Used to log meetings, calls, emails, chats, or any other touchpoint with contacts
    /// or accounts.
    pub fn interaction() -> Self {
        Self::new(EntityType::new("interaction"))
            .with_metadata()
            .with_required_field(FieldId::new("type"), FieldType::String)
            .with_required_field(FieldId::new("subject"), FieldType::String)
            .with_required_field(FieldId::new("initiator_ref"), FieldType::Reference)
            .with_required_field(FieldId::new("primary_contact_ref"), FieldType::Reference)
            .with_required_field(FieldId::new("interaction_date"), FieldType::DateTime)
            .with_optional_field(FieldId::new("outcome"), FieldType::String)
            .with_optional_field(FieldId::new("secondary_contacts_ref"), FieldType::List)
            .with_optional_field(FieldId::new("channel_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("opportunity_ref"), FieldType::Reference)
    }

    /// Represents a potential sale or business deal.
    ///
    /// This entity tracks a qualified lead through the sales pipeline, capturing its value,
    /// status, and probability of success.
    pub fn opportunity() -> Self {
        Self::new(EntityType::new("opportunity"))
            .with_metadata()
            .with_required_field(FieldId::new("source_ref"), FieldType::Reference)
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_required_field(FieldId::new("status"), FieldType::String)
            .with_optional_field(FieldId::new("value"), FieldType::Currency)
            .with_optional_field(FieldId::new("probability"), FieldType::Integer)
    }

    /// Represents a high-level, long-term plan or goal.
    ///
    /// A foundational element for work management, strategies provide direction and can be
    /// linked to by more low-level entities.
    pub fn strategy() -> Self {
        Self::new(EntityType::new("strategy"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_optional_field(FieldId::new("description"), FieldType::String)
            .with_optional_field(FieldId::new("source_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("owner_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("status"), FieldType::String)
            .with_optional_field(FieldId::new("start_date"), FieldType::DateTime)
            .with_optional_field(FieldId::new("end_date"), FieldType::DateTime)
    }

    /// Represents a specific, measurable goal that contributes to a strategy.
    ///
    /// Objectives break down high-level strategies into actionable targets that are
    /// further defined by key result entities.
    pub fn objective() -> Self {
        Self::new(EntityType::new("objective"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_optional_field(FieldId::new("description"), FieldType::String)
            .with_optional_field(FieldId::new("strategy_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("owner_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("status"), FieldType::String)
            .with_optional_field(FieldId::new("start_date"), FieldType::DateTime)
            .with_optional_field(FieldId::new("end_date"), FieldType::DateTime)
    }

    /// Represents a measurable outcome used to track an objective.
    ///
    /// Key results make objectives concrete with quantified success metrics.
    pub fn key_result() -> Self {
        Self::new(EntityType::new("key_result"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_required_field(FieldId::new("objective_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("owner_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("start_value"), FieldType::Float)
            .with_optional_field(FieldId::new("target_value"), FieldType::Float)
            .with_optional_field(FieldId::new("current_value"), FieldType::Float)
            .with_optional_field(FieldId::new("unit"), FieldType::String)
    }

    /// Represents a planned initiative to achieve specific objectives.
    ///
    /// A project may a contain tasks and link strategic goals to day-to-day execution.
    pub fn project() -> Self {
        Self::new(EntityType::new("project"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_required_field(FieldId::new("status"), FieldType::String)
            .with_optional_field(FieldId::new("description"), FieldType::String)
            .with_optional_field(FieldId::new("owner_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("objective_refs"), FieldType::List)
            .with_optional_field(FieldId::new("due_date"), FieldType::DateTime)
    }

    /// Represents a single, actionable unit of work.
    ///
    /// Tasks are the most granular items in work management and are typically associated
    /// with a project or another source entity.
    pub fn task() -> Self {
        Self::new(EntityType::new("task"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_optional_field(FieldId::new("description"), FieldType::String)
            .with_optional_field(FieldId::new("source_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("assignee_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("due_date"), FieldType::DateTime)
            .with_optional_field(FieldId::new("is_completed"), FieldType::Boolean)
            .with_optional_field(FieldId::new("completed_at"), FieldType::DateTime)
    }

    /// Represents a periodic review or meeting (an Event in the REA model).
    ///
    /// Used to track progress on projects, objectives, or strategies, linking together
    /// relevant people and resources for a specific point in time.
    pub fn review() -> Self {
        Self::new(EntityType::new("review"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_required_field(FieldId::new("date"), FieldType::DateTime)
            .with_optional_field(FieldId::new("owner_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("source_refs"), FieldType::List)
            .with_optional_field(FieldId::new("attendee_refs"), FieldType::List)
    }

    /// Represents a digital file or document (a Resource in the REA model).
    ///
    /// This entity links to a file path and can be associated with any other entity,
    /// serving as a way to track project assets, contracts, or other documents.
    pub fn file_asset() -> Self {
        Self::new(EntityType::new("file_asset"))
            .with_metadata()
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_required_field(FieldId::new("path"), FieldType::Path)
            .with_optional_field(FieldId::new("description"), FieldType::String)
            .with_optional_field(FieldId::new("source_ref"), FieldType::Reference)
            .with_optional_field(FieldId::new("owner_ref"), FieldType::Reference)
    }
}

```

## /firm_core/src/schema/mod.rs

```rs path="/firm_core/src/schema/mod.rs" 
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display};

use crate::{EntityType, FieldId, FieldType};

mod builtin;
mod validation;
mod validation_errors;

pub use validation::ValidationResult;
pub use validation_errors::{ValidationError, ValidationErrorType};

/// Defines the mode of a field, either required or optional
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FieldMode {
    Required,
    Optional,
}

/// Defines the schema for an unnamed field which can be either required or optional.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FieldSchema {
    pub field_type: FieldType,
    pub field_mode: FieldMode,
    pub order: usize,
}

impl FieldSchema {
    pub fn new(field_type: FieldType, field_mode: FieldMode, order: usize) -> Self {
        FieldSchema {
            field_type,
            field_mode,
            order,
        }
    }

    /// Get the expected field type.
    pub fn expected_type(&self) -> &FieldType {
        &self.field_type
    }

    /// Check if the field is required.
    pub fn is_required(&self) -> bool {
        self.field_mode == FieldMode::Required
    }
}

/// Defines the schema for an entity type.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntitySchema {
    pub entity_type: EntityType,
    pub fields: HashMap<FieldId, FieldSchema>,
    insertion_order: u16,
}

impl EntitySchema {
    /// Creates a new entity schema with a given name.
    pub fn new(entity_type: EntityType) -> Self {
        Self {
            entity_type: entity_type,
            fields: HashMap::new(),
            insertion_order: 0,
        }
    }

    /// Builder method to add a field to the schema.
    pub fn add_field_schema(mut self, id: FieldId, field_schema: FieldSchema) -> Self {
        self.fields.insert(id, field_schema);
        self
    }

    /// Builder method to add a raw field.
    /// This does not preserve insertion order.
    pub fn with_raw_field(self, id: FieldId, schema: FieldSchema) -> Self {
        self.add_field_schema(id, schema)
    }

    /// Builder method to add a required field preserving insertion order.
    pub fn with_required_field(self, id: FieldId, field_type: FieldType) -> Self {
        let order = self.next_order();
        self.add_field_schema(id, FieldSchema::new(field_type, FieldMode::Required, order))
    }

    /// Builder method to add an optional field preserving insertion order.
    pub fn with_optional_field(self, id: FieldId, field_type: FieldType) -> Self {
        let order = self.next_order();
        self.add_field_schema(id, FieldSchema::new(field_type, FieldMode::Optional, order))
    }

    /// Builder method to add common metadata fields to the schema.
    pub fn with_metadata(self) -> Self {
        self.with_raw_field(
            FieldId::new("notes"),
            FieldSchema::new(FieldType::String, FieldMode::Optional, 100),
        )
        .with_raw_field(
            FieldId::new("created_at"),
            FieldSchema::new(FieldType::DateTime, FieldMode::Optional, 101),
        )
        .with_raw_field(
            FieldId::new("updated_at"),
            FieldSchema::new(FieldType::DateTime, FieldMode::Optional, 102),
        )
    }

    /// Get schema fields sorted by their order.
    pub fn ordered_fields(&self) -> Vec<(&FieldId, &FieldSchema)> {
        let mut ordered: Vec<_> = self.fields.iter().collect();
        ordered.sort_by_key(|&(_, field_schema)| field_schema.order);
        ordered
    }

    /// Gets the next order for a field, preserving insertion order.
    fn next_order(&self) -> usize {
        self.fields.len()
    }
}

impl Display for EntitySchema {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{}", self.entity_type)?;

        for (field_id, field_schema) in &self.ordered_fields() {
            writeln!(f, "\n{}", field_id)?;
            writeln!(f, "- Type: {}", field_schema.expected_type())?;
            writeln!(f, "- Required: {}", field_schema.is_required())?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_schema_create_new() {
        let schema = EntitySchema::new(EntityType::new("person"))
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_optional_field(FieldId::new("email"), FieldType::String);

        assert_eq!(schema.entity_type, EntityType::new("person"));
        let name_field = &schema.fields[&FieldId::new("name")];
        assert_eq!(name_field.field_type, FieldType::String);
        assert_eq!(name_field.field_mode, FieldMode::Required);

        let email_field = &schema.fields[&FieldId::new("email")];
        assert_eq!(email_field.field_type, FieldType::String);
        assert_eq!(email_field.field_mode, FieldMode::Optional);
    }
}

```

## /firm_core/src/schema/validation.rs

```rs path="/firm_core/src/schema/validation.rs" 
use log::debug;

use super::{EntitySchema, ValidationError};
use crate::Entity;

pub type ValidationResult = Result<(), Vec<ValidationError>>;

impl EntitySchema {
    /// Validates an entity against the schema.
    pub fn validate(&self, entity: &Entity) -> ValidationResult {
        debug!(
            "Validating entity: '{}' for schema: '{}'",
            entity.id, self.entity_type
        );

        let mut errors = Vec::new();

        // Check the entity type against the schema
        if entity.entity_type != self.entity_type {
            errors.push(ValidationError::mismatched_entity_type(
                &entity.id,
                &self.entity_type,
                &entity.entity_type,
            ))
        }

        // Check each field in the schema
        for (field_name, field_schema) in &self.fields {
            match entity.get_field(field_name) {
                // Entity has the field: Check that it has desired type
                Some(field_value) => {
                    let expected_type = field_schema.expected_type();
                    if !field_value.is_type(expected_type) {
                        errors.push(ValidationError::mismatched_field_type(
                            &entity.id,
                            field_name,
                            expected_type,
                            &field_value.get_type(),
                        ));
                    }
                }
                // Entity does not have the field: Check if it's required
                None => {
                    if field_schema.is_required() {
                        errors.push(ValidationError::missing_field(&entity.id, field_name));
                    }
                }
            }
        }

        if errors.is_empty() {
            Ok(())
        } else {
            debug!(
                "Entity '{}' failed validation with {} errors",
                entity.id,
                errors.len()
            );
            Err(errors)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::schema::ValidationErrorType;
    use crate::{
        EntityId, EntityType, FieldId,
        field::{FieldType, FieldValue},
    };
    use assert_matches::assert_matches;

    #[test]
    fn test_validate_ok() {
        let schema = EntitySchema::new(EntityType::new("person"))
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_optional_field(FieldId::new("email"), FieldType::String);

        let entity = Entity::new(EntityId::new("test_person"), EntityType::new("person"))
            .with_field(
                FieldId::new("name"),
                FieldValue::String(String::from("John Doe")),
            );

        let result = schema.validate(&entity);

        assert!(result.is_ok());
    }

    #[test]
    fn test_validate_error_mismatched_entity_types() {
        let schema = EntitySchema::new(EntityType::new("test_a"));
        let entity = Entity::new(EntityId::new("test"), EntityType::new("test_b"));

        let result = schema.validate(&entity);

        assert!(result.is_err());

        let errors = result.unwrap_err();
        assert_eq!(errors.len(), 1);

        assert_matches!(
            &errors[0].error_type,
            ValidationErrorType::MismatchedEntityType { expected, actual } if expected == &EntityType::new("test_a") && actual == &EntityType::new("test_b")
        );
    }

    #[test]
    fn test_validate_error_missing_field() {
        let schema = EntitySchema::new(EntityType::new("person"))
            .with_required_field(FieldId::new("name"), FieldType::String)
            .with_required_field(FieldId::new("email"), FieldType::String);

        let entity = Entity::new(EntityId::new("test_person"), EntityType::new("person"))
            .with_field(
                FieldId::new("name"),
                FieldValue::String(String::from("John Doe")),
            );

        let result = schema.validate(&entity);

        assert!(result.is_err());

        let errors = result.unwrap_err();
        assert_eq!(errors.len(), 1);

        assert_matches!(
            &errors[0].error_type,
            ValidationErrorType::MissingRequiredField { required } if required == &FieldId::new("email")
        );
    }

    #[test]
    fn test_validate_error_mismatched_field_types() {
        let schema = EntitySchema::new(EntityType::new("person"))
            .with_required_field(FieldId::new("is_nice"), FieldType::Boolean);

        let entity = Entity::new(EntityId::new("test_person"), EntityType::new("person"))
            .with_field(
                FieldId::new("is_nice"),
                FieldValue::String("Sure".to_string()),
            );

        let result = schema.validate(&entity);

        assert!(result.is_err());

        let errors = result.unwrap_err();
        assert_eq!(errors.len(), 1);

        assert_matches!(
            &errors[0].error_type,
            ValidationErrorType::MismatchedFieldType { expected, actual } if expected == &FieldType::Boolean && actual == &FieldType::String
        );
    }
}

```

## /firm_core/src/schema/validation_errors.rs

```rs path="/firm_core/src/schema/validation_errors.rs" 
use crate::{EntityId, EntityType, FieldId, FieldType};

/// Defines the types of errors you might encounter when validating a schema.
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationErrorType {
    /// The entity type did not match the schema.
    MismatchedEntityType {
        expected: EntityType,
        actual: EntityType,
    },
    /// The entity is missing a required field.
    MissingRequiredField { required: FieldId },
    /// The entity has a field whose type did not match the schema.
    MismatchedFieldType {
        expected: FieldType,
        actual: FieldType,
    },
}

/// Information about an error encountered while validating a schema.
#[derive(Debug, Clone, PartialEq)]
pub struct ValidationError {
    pub entity_id: Option<EntityId>,
    pub field: Option<FieldId>,
    pub message: String,
    pub error_type: ValidationErrorType,
}

impl ValidationError {
    /// Shorthand for creating a mismatched entity type error.
    pub fn mismatched_entity_type(
        entity_id: &EntityId,
        expected: &EntityType,
        actual: &EntityType,
    ) -> Self {
        Self {
            entity_id: Some(entity_id.clone()),
            field: None,
            message: format!(
                "Expected entity '{}' to be of type '{}' but it was '{}'",
                entity_id, expected, actual
            ),
            error_type: ValidationErrorType::MismatchedEntityType {
                expected: expected.clone(),
                actual: actual.clone(),
            },
        }
    }

    /// Shorthand for creating a missing required field error.
    pub fn missing_field(entity_id: &EntityId, field_id: &FieldId) -> Self {
        Self {
            entity_id: Some(entity_id.clone()),
            field: Some(field_id.clone()),
            message: format!(
                "Missing required field '{}' for entity '{}'",
                field_id, entity_id
            ),
            error_type: ValidationErrorType::MissingRequiredField {
                required: field_id.clone(),
            },
        }
    }

    /// Shorthand for creating a mismatched field type error.
    pub fn mismatched_field_type(
        entity_id: &EntityId,
        field_id: &FieldId,
        expected: &FieldType,
        actual: &FieldType,
    ) -> Self {
        Self {
            entity_id: Some(entity_id.clone()),
            field: Some(field_id.clone()),
            message: format!(
                "Expected field '{}' for entity '{}' to be of type '{}' but it was '{}'",
                field_id, entity_id, expected, actual
            ),
            error_type: ValidationErrorType::MismatchedFieldType {
                expected: expected.clone(),
                actual: actual.clone(),
            },
        }
    }
}

```

## /firm_lang/Cargo.toml

```toml path="/firm_lang/Cargo.toml" 
[package]
name = "firm_lang"
version = "0.3.0"
edition = "2024"
description = "Parse and generate DSL for Firm workspaces."
license = "AGPL-3.0"
repository = "https://github.com/42futures/firm"

[dependencies]
firm_core = { path = "../firm_core" }
tree-sitter = "0.25.8"
tree-sitter-firm = { path = "../tree-sitter-firm" }

log = "0.4.27"
rust_decimal = { version = "1.37", features = ["serde-with-str"] }
iso_currency = { version = "0.5", features = ["with-serde"] }
chrono = { version = "0.4", features = ["serde"] }
path-clean = "1.0.1"

[dev-dependencies]
assert_matches = "1.5"
env_logger = "0.11.8"
tempfile = "3.20.0"

```

## /firm_lang/src/convert/conversion_errors.rs

```rs path="/firm_lang/src/convert/conversion_errors.rs" 
use std::fmt;

/// Errors that can occur when converting a parsed entity.
#[derive(Debug)]
pub enum EntityConversionError {
    MissingEntityType,
    MissingEntityId,
    MissingFieldId,
    InvalidFieldValue,
}

impl fmt::Display for EntityConversionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            EntityConversionError::MissingEntityType => {
                write!(f, "Entity is missing required type")
            }
            EntityConversionError::MissingEntityId => {
                write!(f, "Entity is missing required id")
            }
            EntityConversionError::MissingFieldId => {
                write!(f, "Entity field is missing required id")
            }
            EntityConversionError::InvalidFieldValue => {
                write!(f, "Entity field contains an invalid value")
            }
        }
    }
}

/// Errors that can occur when converting a parsed schema.
#[derive(Debug)]
pub enum SchemaConversionError {
    MissingSchemaName,
    MissingFieldName,
    MissingFieldType,
    UnknownFieldType(String),
    InvalidFieldDefinition,
}

impl fmt::Display for SchemaConversionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            SchemaConversionError::MissingSchemaName => {
                write!(f, "Schema is missing required name")
            }
            SchemaConversionError::MissingFieldName => {
                write!(f, "Schema field is missing required name")
            }
            SchemaConversionError::MissingFieldType => {
                write!(f, "Schema field is missing required type")
            }
            SchemaConversionError::UnknownFieldType(field_type) => {
                write!(f, "Unknown field type: '{}'", field_type)
            }
            SchemaConversionError::InvalidFieldDefinition => {
                write!(f, "Schema field definition is invalid")
            }
        }
    }
}

```

## /firm_lang/src/convert/mod.rs

```rs path="/firm_lang/src/convert/mod.rs" 
pub mod conversion_errors;
pub mod to_entity;
pub mod to_schema;

pub use conversion_errors::{EntityConversionError, SchemaConversionError};

```

## /firm_lang/src/convert/to_entity.rs

```rs path="/firm_lang/src/convert/to_entity.rs" 
use firm_core::{Entity, FieldId, FieldValue, ReferenceValue, compose_entity_id};

use super::EntityConversionError;
use crate::parser::{ParsedEntity, ParsedValue};

/// Converts a ParsedEntity to an Entity.
impl TryFrom<&ParsedEntity<'_>> for Entity {
    type Error = EntityConversionError;

    fn try_from(parsed: &ParsedEntity) -> Result<Self, EntityConversionError> {
        let entity_type_str = parsed
            .entity_type()
            .ok_or(EntityConversionError::MissingEntityType)?;

        let entity_id = parsed.id().ok_or(EntityConversionError::MissingEntityId)?;
        let composite_id = compose_entity_id(entity_type_str, entity_id);
        let mut entity = Entity::new(composite_id, entity_type_str.into());

        for field in parsed.fields() {
            let field_id = field.id().ok_or(EntityConversionError::MissingFieldId)?;
            let parsed_value = field
                .value()
                .map_err(|_| EntityConversionError::InvalidFieldValue)?;

            let field_value: FieldValue = parsed_value
                .try_into()
                .map_err(|_| EntityConversionError::InvalidFieldValue)?;

            entity
                .fields
                .push((FieldId(field_id.to_string()), field_value));
        }

        Ok(entity)
    }
}

/// Converts a ParsedValue to a FieldValue.
impl TryFrom<ParsedValue> for FieldValue {
    type Error = EntityConversionError;

    fn try_from(parsed: ParsedValue) -> Result<Self, EntityConversionError> {
        match parsed {
            ParsedValue::Boolean(value) => Ok(FieldValue::Boolean(value)),
            ParsedValue::String(value) => Ok(FieldValue::String(value)),
            ParsedValue::Integer(value) => Ok(FieldValue::Integer(value)),
            ParsedValue::Float(value) => Ok(FieldValue::Float(value)),
            ParsedValue::Currency { amount, currency } => {
                Ok(FieldValue::Currency { amount, currency })
            }
            ParsedValue::EntityReference {
                entity_type,
                entity_id,
            } => {
                let composite_id = compose_entity_id(&entity_type, &entity_id);
                Ok(FieldValue::Reference(ReferenceValue::Entity(composite_id)))
            }
            ParsedValue::FieldReference {
                entity_type,
                entity_id,
                field_id,
            } => {
                let composite_id = compose_entity_id(&entity_type, &entity_id);
                Ok(FieldValue::Reference(ReferenceValue::Field(
                    composite_id,
                    FieldId(field_id),
                )))
            }
            ParsedValue::List(values) => {
                let converted_values: Result<Vec<FieldValue>, EntityConversionError> =
                    values.into_iter().map(|v| v.try_into()).collect();

                Ok(FieldValue::List(converted_values?))
            }
            ParsedValue::DateTime(value) => Ok(FieldValue::DateTime(value)),
            ParsedValue::Path(value) => Ok(FieldValue::Path(value)),
        }
    }
}

```

## /firm_lang/src/convert/to_schema.rs

```rs path="/firm_lang/src/convert/to_schema.rs" 
use firm_core::{
    EntityType, FieldId,
    field::FieldType,
    schema::{EntitySchema, FieldMode, FieldSchema},
};

use super::SchemaConversionError;
use crate::parser::ParsedSchema;

/// Converts a ParsedSchema to an EntitySchema.
impl TryFrom<&ParsedSchema<'_>> for EntitySchema {
    type Error = SchemaConversionError;

    fn try_from(parsed: &ParsedSchema) -> Result<Self, SchemaConversionError> {
        let schema_name = parsed
            .name()
            .ok_or(SchemaConversionError::MissingSchemaName)?;

        let entity_type = EntityType::new(schema_name.to_string());
        let mut schema = EntitySchema::new(entity_type);

        for (order, field) in parsed.fields().iter().enumerate() {
            let field_name = field
                .name()
                .map_err(|_| SchemaConversionError::MissingFieldName)?;

            let field_type_str = field
                .field_type()
                .map_err(|_| SchemaConversionError::MissingFieldType)?;

            let field_type = convert_field_type(&field_type_str)?;

            let field_schema = if field.required() {
                FieldSchema::new(field_type, FieldMode::Required, order)
            } else {
                FieldSchema::new(field_type, FieldMode::Optional, order)
            };

            schema.fields.insert(FieldId(field_name), field_schema);
        }

        Ok(schema)
    }
}

/// Converts a field type string to a FieldType enum.
fn convert_field_type(type_str: &str) -> Result<FieldType, SchemaConversionError> {
    match type_str {
        "boolean" => Ok(FieldType::Boolean),
        "string" => Ok(FieldType::String),
        "integer" => Ok(FieldType::Integer),
        "float" => Ok(FieldType::Float),
        "currency" => Ok(FieldType::Currency),
        "reference" => Ok(FieldType::Reference),
        "list" => Ok(FieldType::List),
        "datetime" => Ok(FieldType::DateTime),
        _ => Err(SchemaConversionError::UnknownFieldType(
            type_str.to_string(),
        )),
    }
}

```

## /firm_lang/src/generate/from_entity.rs

```rs path="/firm_lang/src/generate/from_entity.rs" 
use firm_core::{Entity, decompose_entity_id};

use super::{GeneratorOptions, from_field};

/// Generate DSL for a single entity.
pub fn generate_entity(entity: &Entity, options: &GeneratorOptions) -> String {
    let mut output = String::new();
    let (_, entity_id) = decompose_entity_id(&entity.id.0);

    // Entity declaration and open block
    output.push_str(&format!(
        "{} {} {{\n",
        entity.entity_type.to_string().to_lowercase(),
        entity_id
    ));

    // Generate fields
    let field_lines = generate_entity_fields(entity, options);
    for field_line in field_lines {
        output.push_str(&field_line);
    }

    // Close entity block
    output.push_str("}\n");

    output
}

/// Generate DSL for all fields for an entity.
fn generate_entity_fields(entity: &Entity, options: &GeneratorOptions) -> Vec<String> {
    let fields: Vec<(String, &firm_core::FieldValue)> = entity
        .fields
        .iter()
        .map(|(field_id, field_value)| (field_id.0.clone(), field_value))
        .collect();

    // Generate each field
    fields
        .into_iter()
        .map(|(field_name, field_value)| {
            let field_line = from_field::generate_field(&field_name, field_value, options);
            format!("{}{}\n", options.indent_style.indent_string(1), field_line)
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::generate::generator_options::IndentStyle;
    use firm_core::{Entity, EntityId, EntityType, FieldId, FieldValue, ReferenceValue};

    #[test]
    fn test_generate_simple_person_entity() {
        let mut fields = Vec::new();
        fields.push((
            FieldId("name".to_string()),
            FieldValue::String("John Doe".to_string()),
        ));
        fields.push((FieldId("age".to_string()), FieldValue::Integer(42)));

        let entity = Entity {
            id: EntityId("person.john_doe".to_string()),
            entity_type: EntityType::new("person"),
            fields,
        };

        let result = generate_entity(&entity, &GeneratorOptions::default());

        let expected = r#"person john_doe {
    name = "John Doe"
    age = 42
}
"#;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_generate_organization_with_multiple_fields() {
        let mut fields = Vec::new();
        fields.push((
            FieldId("name".to_string()),
            FieldValue::String("ACME Corp".to_string()),
        ));
        fields.push((
            FieldId("primary_email".to_string()),
            FieldValue::String("contact@acme.com".to_string()),
        ));
        fields.push((FieldId("active".to_string()), FieldValue::Boolean(true)));
        fields.push((
            FieldId("employee_count".to_string()),
            FieldValue::Integer(150),
        ));

        let entity = Entity {
            id: EntityId("organization.acme_corp".to_string()),
            entity_type: EntityType::new("organization"),
            fields,
        };

        let result = generate_entity(&entity, &GeneratorOptions::default());

        let expected = r#"organization acme_corp {
    name = "ACME Corp"
    primary_email = "contact@acme.com"
    active = true
    employee_count = 150
}
"#;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_generate_entity_with_references() {
        let mut fields = Vec::new();
        fields.push((
            FieldId("name".to_string()),
            FieldValue::String("Jane Smith".to_string()),
        ));
        fields.push((
            FieldId("manager".to_string()),
            FieldValue::Reference(ReferenceValue::Entity(EntityId(
                "person.john_doe".to_string(),
            ))),
        ));
        fields.push((
            FieldId("manager_email".to_string()),
            FieldValue::Reference(ReferenceValue::Field(
                EntityId("person.john_doe".to_string()),
                FieldId("email".to_string()),
            )),
        ));

        let entity = Entity {
            id: EntityId("person.jane_smith".to_string()),
            entity_type: EntityType::new("person"),
            fields,
        };

        let result = generate_entity(&entity, &GeneratorOptions::default());

        let expected = r#"person jane_smith {
    name = "Jane Smith"
    manager = person.john_doe
    manager_email = person.john_doe.email
}
"#;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_generate_entity_with_multiline_string() {
        let mut fields = Vec::new();
        fields.push((
            FieldId("title".to_string()),
            FieldValue::String("Code Review".to_string()),
        ));
        fields.push((
            FieldId("description".to_string()),
            FieldValue::String(
                "Review the pull request:\n- Check logic\n- Verify tests\n- Approve changes"
                    .to_string(),
            ),
        ));

        let entity = Entity {
            id: EntityId("task.code_review".to_string()),
            entity_type: EntityType::new("task"),
            fields,
        };

        let result = generate_entity(&entity, &GeneratorOptions::default());

        let expected = r#"task code_review {
    title = "Code Review"
    description = """
    Review the pull request:
    - Check logic
    - Verify tests
    - Approve changes
    """
}
"#;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_generate_with_custom_indent() {
        let mut fields = Vec::new();
        fields.push((
            FieldId("name".to_string()),
            FieldValue::String("Test".to_string()),
        ));

        let entity = Entity {
            id: EntityId("person.test".to_string()),
            entity_type: EntityType::new("person"),
            fields,
        };

        let options = GeneratorOptions {
            indent_style: IndentStyle::Spaces(2),
            ..Default::default()
        };

        let result = generate_entity(&entity, &options);

        let expected = r#"person test {
  name = "Test"
}
"#;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_generate_with_tab_indent() {
        let mut fields = Vec::new();
        fields.push((
            FieldId("name".to_string()),
            FieldValue::String("Test".to_string()),
        ));

        let entity = Entity {
            id: EntityId("person.test".to_string()),
            entity_type: EntityType::new("person"),
            fields,
        };

        let options = GeneratorOptions {
            indent_style: IndentStyle::Tabs,
            ..Default::default()
        };

        let result = generate_entity(&entity, &options);

        let expected = "person test {\n\tname = \"Test\"\n}\n";
        assert_eq!(result, expected);
    }
}

```

## /firm_lang/src/generate/from_field.rs

```rs path="/firm_lang/src/generate/from_field.rs" 
use firm_core::FieldValue;

use super::{GeneratorOptions, from_value};

/// Generate DSL for an entity field.
pub fn generate_field(
    field_name: &str,
    field_value: &FieldValue,
    options: &GeneratorOptions,
) -> String {
    let value_str = from_value::generate_value(field_value, options);
    format!("{} = {}", field_name, value_str)
}

#[cfg(test)]
mod tests {
    use super::*;
    use firm_core::FieldValue;

    #[test]
    fn test_generate_simple_field() {
        let options = GeneratorOptions::default();
        let result = generate_field("name", &FieldValue::String("test".to_string()), &options);
        assert_eq!(result, "name = \"test\"");
    }

    #[test]
    fn test_generate_boolean_field() {
        let options = GeneratorOptions::default();
        let result = generate_field("active", &FieldValue::Boolean(true), &options);
        assert_eq!(result, "active = true");
    }
}

```

## /firm_lang/src/generate/from_value.rs

```rs path="/firm_lang/src/generate/from_value.rs" 
use chrono::{DateTime, FixedOffset};
use std::path::PathBuf;

use firm_core::{FieldValue, ReferenceValue};

use super::GeneratorOptions;

/// Generate DSL for en entity field value.
pub fn generate_value(value: &FieldValue, options: &GeneratorOptions) -> String {
    match value {
        FieldValue::Boolean(b) => b.to_string(),
        FieldValue::String(s) => generate_string(s, options),
        FieldValue::Integer(i) => i.to_string(),
        FieldValue::Float(f) => generate_float(f),
        FieldValue::Currency { amount, currency } => {
            format!("{} {}", amount, currency.code())
        }
        FieldValue::Reference(reference) => generate_reference(reference),
        FieldValue::List(values) => generate_list(values, options),
        FieldValue::DateTime(dt) => generate_datetime(dt),
        FieldValue::Path(path) => generate_path(path),
    }
}

/// Generate string value with proper quoting.
fn generate_string(s: &str, options: &GeneratorOptions) -> String {
    if s.contains('\n') {
        // Multi-line string
        let indent = options.indent_style.indent_string(1);
        let lines: Vec<&str> = s.lines().collect();

        let mut result = String::from("\"\"\"\n");
        for line in lines {
            result.push_str(&format!("{}{}\n", indent, line));
        }
        result.push_str(&format!("{}\"\"\"", indent));
        result
    } else {
        // Single-line string with escape handling
        format!("\"{}\"", s.replace('\"', "\\\""))
    }
}

/// Generate float value, ensuring it always has a decimal place.
fn generate_float(f: &f64) -> String {
    let formatted = f.to_string();

    // If the float doesn't contain a decimal point, add .0
    if !formatted.contains('.') {
        format!("{}.0", formatted)
    } else {
        formatted
    }
}

/// Generate entity/field reference value.
fn generate_reference(reference: &ReferenceValue) -> String {
    match reference {
        ReferenceValue::Entity(entity_id) => entity_id.0.clone(),
        ReferenceValue::Field(entity_id, field_id) => {
            format!("{}.{}", entity_id.0, field_id.0)
        }
    }
}

/// Generate list value.
fn generate_list(values: &[FieldValue], options: &GeneratorOptions) -> String {
    if values.is_empty() {
        return "[]".to_string();
    }

    let value_strings: Vec<String> = values.iter().map(|v| generate_value(v, options)).collect();

    format!("[{}]", value_strings.join(", "))
}

/// Generate datetime value.
fn generate_datetime(dt: &DateTime<FixedOffset>) -> String {
    let date_str = dt.format("%Y-%m-%d").to_string();
    let time_str = dt.format("%H:%M").to_string();

    // Handle timezone
    let offset_seconds = dt.offset().local_minus_utc();
    let timezone_str = if offset_seconds == 0 {
        "UTC".to_string()
    } else {
        let hours = offset_seconds / 3600;
        if hours > 0 {
            format!("UTC+{}", hours)
        } else {
            format!("UTC{}", hours) // hours is already negative
        }
    };

    format!("{} at {} {}", date_str, time_str, timezone_str)
}

/// Generate path value.
fn generate_path(path: &PathBuf) -> String {
    format!("path\"{}\"", path.display())
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::TimeZone;
    use firm_core::{EntityId, FieldId, FieldValue, ReferenceValue};
    use iso_currency::Currency;
    use rust_decimal::Decimal;

    #[test]
    fn test_generate_boolean_true() {
        let options = GeneratorOptions::default();
        let result = generate_value(&FieldValue::Boolean(true), &options);
        assert_eq!(result, "true");
    }

    #[test]
    fn test_generate_boolean_false() {
        let options = GeneratorOptions::default();
        let result = generate_value(&FieldValue::Boolean(false), &options);
        assert_eq!(result, "false");
    }

    #[test]
    fn test_generate_string_single_line() {
        let options = GeneratorOptions::default();
        let result = generate_string("Hello World", &options);
        assert_eq!(result, "\"Hello World\"");
    }

    #[test]
    fn test_generate_string_with_quotes() {
        let options = GeneratorOptions::default();
        let result = generate_string("Say \"Hello\"", &options);
        assert_eq!(result, "\"Say \\\"Hello\\\"\"");
    }

    #[test]
    fn test_generate_string_multiline() {
        let options = GeneratorOptions::default();
        let multiline = "Line 1\nLine 2\nLine 3";
        let result = generate_string(multiline, &options);

        let expected = "\"\"\"\n    Line 1\n    Line 2\n    Line 3\n    \"\"\"";
        assert_eq!(result, expected);
    }

    #[test]
    fn test_generate_string_empty() {
        let options = GeneratorOptions::default();
        let result = generate_string("", &options);
        assert_eq!(result, "\"\"");
    }

    #[test]
    fn test_generate_integer_positive() {
        let options = GeneratorOptions::default();
        let result = generate_value(&FieldValue::Integer(42), &options);
        assert_eq!(result, "42");
    }

    #[test]
    fn test_generate_integer_negative() {
        let options = GeneratorOptions::default();
        let result = generate_value(&FieldValue::Integer(-123), &options);
        assert_eq!(result, "-123");
    }

    #[test]
    fn test_generate_integer_zero() {
        let options = GeneratorOptions::default();
        let result = generate_value(&FieldValue::Integer(0), &options);
        assert_eq!(result, "0");
    }

    #[test]
    fn test_generate_float_positive() {
        let options = GeneratorOptions::default();
        let result = generate_value(&FieldValue::Float(3.14159), &options);
        assert_eq!(result, "3.14159");
    }

    #[test]
    fn test_generate_float_negative() {
        let options = GeneratorOptions::default();
        let result = generate_value(&FieldValue::Float(-2.5), &options);
        assert_eq!(result, "-2.5");
    }

    #[test]
    fn test_generate_float_zero() {
        let options = GeneratorOptions::default();
        let result = generate_value(&FieldValue::Float(0.0), &options);
        assert_eq!(result, "0.0");
    }

    #[test]
    fn test_generate_currency_usd() {
        let options = GeneratorOptions::default();
        let result = generate_value(
            &FieldValue::Currency {
                amount: Decimal::from_str_exact("1000.50").unwrap(),
                currency: Currency::USD,
            },
            &options,
        );
        assert_eq!(result, "1000.50 USD");
    }

    #[test]
    fn test_generate_currency_eur() {
        let options = GeneratorOptions::default();
        let result = generate_value(
            &FieldValue::Currency {
                amount: Decimal::from_str_exact("750").unwrap(),
                currency: Currency::EUR,
            },
            &options,
        );
        assert_eq!(result, "750 EUR");
    }

    #[test]
    fn test_generate_currency_zero() {
        let options = GeneratorOptions::default();
        let result = generate_value(
            &FieldValue::Currency {
                amount: Decimal::ZERO,
                currency: Currency::DKK,
            },
            &options,
        );
        assert_eq!(result, "0 DKK");
    }

    #[test]
    fn test_generate_reference_entity() {
        let reference = ReferenceValue::Entity(EntityId("person.john".to_string()));
        let result = generate_reference(&reference);
        assert_eq!(result, "person.john");
    }

    #[test]
    fn test_generate_reference_field() {
        let reference = ReferenceValue::Field(
            EntityId("person.john".to_string()),
            FieldId("name".to_string()),
        );
        let result = generate_reference(&reference);
        assert_eq!(result, "person.john.name");
    }

    #[test]
    fn test_generate_empty_list() {
        let options = GeneratorOptions::default();
        let result = generate_list(&[], &options);
        assert_eq!(result, "[]");
    }

    #[test]
    fn test_generate_string_list() {
        let options = GeneratorOptions::default();
        let values = vec![
            FieldValue::String("first".to_string()),
            FieldValue::String("second".to_string()),
        ];
        let result = generate_list(&values, &options);
        assert_eq!(result, "[\"first\", \"second\"]");
    }

    #[test]
    fn test_generate_integer_list() {
        let options = GeneratorOptions::default();
        let values = vec![
            FieldValue::Integer(1),
            FieldValue::Integer(2),
            FieldValue::Integer(3),
        ];
        let result = generate_list(&values, &options);
        assert_eq!(result, "[1, 2, 3]");
    }

    #[test]
    fn test_generate_boolean_list() {
        let options = GeneratorOptions::default();
        let values = vec![
            FieldValue::Boolean(true),
            FieldValue::Boolean(false),
            FieldValue::Boolean(true),
        ];
        let result = generate_list(&values, &options);
        assert_eq!(result, "[true, false, true]");
    }

    #[test]
    fn test_generate_nested_lists() {
        let options = GeneratorOptions::default();

        // Create nested list: [["a", "b"], ["c", "d"]]
        let inner_list1 = vec![
            FieldValue::String("a".to_string()),
            FieldValue::String("b".to_string()),
        ];
        let inner_list2 = vec![
            FieldValue::String("c".to_string()),
            FieldValue::String("d".to_string()),
        ];

        let nested_list = vec![FieldValue::List(inner_list1), FieldValue::List(inner_list2)];

        let result = generate_list(&nested_list, &options);
        assert_eq!(result, "[[\"a\", \"b\"], [\"c\", \"d\"]]");
    }

    #[test]
    fn test_generate_datetime_utc() {
        let dt = FixedOffset::east_opt(0)
            .unwrap()
            .with_ymd_and_hms(2024, 3, 20, 14, 30, 0)
            .unwrap();
        let result = generate_datetime(&dt);
        assert_eq!(result, "2024-03-20 at 14:30 UTC");
    }

    #[test]
    fn test_generate_datetime_positive_offset() {
        let dt = FixedOffset::east_opt(3 * 3600)
            .unwrap() // UTC+3
            .with_ymd_and_hms(2024, 3, 20, 14, 30, 0)
            .unwrap();
        let result = generate_datetime(&dt);
        assert_eq!(result, "2024-03-20 at 14:30 UTC+3");
    }

    #[test]
    fn test_generate_datetime_negative_offset() {
        let dt = FixedOffset::west_opt(5 * 3600)
            .unwrap() // UTC-5
            .with_ymd_and_hms(2024, 3, 20, 14, 30, 0)
            .unwrap();
        let result = generate_datetime(&dt);
        assert_eq!(result, "2024-03-20 at 14:30 UTC-5");
    }

    #[test]
    fn test_generate_datetime_single_digit_hour() {
        let dt = FixedOffset::east_opt(0)
            .unwrap()
            .with_ymd_and_hms(2024, 3, 20, 9, 5, 0)
            .unwrap();
        let result = generate_datetime(&dt);
        assert_eq!(result, "2024-03-20 at 09:05 UTC");
    }

    #[test]
    fn test_generate_datetime_midnight() {
        let dt = FixedOffset::east_opt(0)
            .unwrap()
            .with_ymd_and_hms(2024, 12, 31, 0, 0, 0)
            .unwrap();
        let result = generate_datetime(&dt);
        assert_eq!(result, "2024-12-31 at 00:00 UTC");
    }

    #[test]
    fn test_generate_path() {
        let result = generate_path(&PathBuf::from("./relative/path.txt"));
        assert_eq!(result, "path\"./relative/path.txt\"");
    }
}

```

## /firm_lang/src/generate/generator_options.rs

```rs path="/firm_lang/src/generate/generator_options.rs" 
/// Formatting options when generating Firm DSL.
#[derive(Debug, Clone)]
pub struct GeneratorOptions {
    pub indent_style: IndentStyle,
    pub blank_lines_between_entities: bool,
}

impl Default for GeneratorOptions {
    fn default() -> Self {
        Self {
            indent_style: IndentStyle::Spaces(4),
            blank_lines_between_entities: true,
        }
    }
}

/// Which kinds of indents to use when generating DSL.
#[derive(Debug, Clone)]
pub enum IndentStyle {
    Spaces(usize),
    Tabs,
}

impl IndentStyle {
    pub fn indent_string(&self, level: usize) -> String {
        match self {
            IndentStyle::Spaces(size) => " ".repeat(level * size),
            IndentStyle::Tabs => "\t".repeat(level),
        }
    }
}

```

## /firm_lang/src/generate/mod.rs

```rs path="/firm_lang/src/generate/mod.rs" 
pub mod from_entity;
pub mod from_field;
pub mod from_value;
pub mod generator_options;

use firm_core::Entity;

use from_entity::generate_entity;
use generator_options::GeneratorOptions;

/// Generates Firm DSL for a collection of entities.
pub fn generate_dsl(entities: &[Entity]) -> String {
    generate_dsl_with_options(entities, &GeneratorOptions::default())
}

/// Generates DSL with formatting options.
pub fn generate_dsl_with_options(entities: &[Entity], options: &GeneratorOptions) -> String {
    let mut output = String::new();

    for (i, entity) in entities.iter().enumerate() {
        if i > 0 && options.blank_lines_between_entities {
            output.push('\n');
        }

        output.push_str(&generate_entity(entity, options));
    }

    output
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::generate::generator_options::{GeneratorOptions, IndentStyle};
    use firm_core::{Entity, EntityId, EntityType, FieldId, FieldValue, ReferenceValue};
    use iso_currency::Currency;
    use rust_decimal::Decimal;

    #[test]
    fn test_generate_empty_entities_list() {
        let result = generate_dsl(&[]);
        assert_eq!(result, "");
    }

    #[test]
    fn test_generate_multiple_entities() {
        // Create a person
        let person = Entity {
            id: EntityId("person.daniel_rothmann".to_string()),
            entity_type: EntityType::new("person"),
            fields: [
                (
                    FieldId("first_name".to_string()),
                    FieldValue::String("Daniel".to_string()),
                ),
                (
                    FieldId("last_name".to_string()),
                    FieldValue::String("Rothmann".to_string()),
                ),
                (
                    FieldId("primary_email".to_string()),
                    FieldValue::String("daniel@42futures.com".to_string()),
                ),
            ]
            .into(),
        };

        // Create an organization
        let organization = Entity {
            id: EntityId("organization.main".to_string()),
            entity_type: EntityType::new("organization"),
            fields: [
                (
                    FieldId("name".to_string()),
                    FieldValue::String("42futures".to_string()),
                ),
                (
                    FieldId("primary_email".to_string()),
                    FieldValue::String("hello@42futures.com".to_string()),
                ),
            ]
            .into(),
        };

        // Create a project with references
        let project = Entity {
            id: EntityId("project.firm_language".to_string()),
            entity_type: EntityType::new("project"),
            fields: [
                (
                    FieldId("name".to_string()),
                    FieldValue::String("Firm Language Development".to_string()),
                ),
                (
                    FieldId("owner_ref".to_string()),
                    FieldValue::Reference(ReferenceValue::Entity(EntityId(
                        "person.daniel_rothmann".to_string(),
                    ))),
                ),
                (
                    FieldId("organization_ref".to_string()),
                    FieldValue::Reference(ReferenceValue::Entity(EntityId(
                        "organization.main".to_string(),
                    ))),
                ),
                (
                    FieldId("budget".to_string()),
                    FieldValue::Currency {
                        amount: Decimal::from_str_exact("150000").unwrap(),
                        currency: Currency::EUR,
                    },
                ),
                (
                    FieldId("technologies".to_string()),
                    FieldValue::List(vec![
                        FieldValue::String("Rust".to_string()),
                        FieldValue::String("Tree-sitter".to_string()),
                        FieldValue::String("WASM".to_string()),
                    ]),
                ),
            ]
            .into(),
        };

        let result = generate_dsl(&[person, organization, project]);

        let expected = r#"person daniel_rothmann {
    first_name = "Daniel"
    last_name = "Rothmann"
    primary_email = "daniel@42futures.com"
}

organization main {
    name = "42futures"
    primary_email = "hello@42futures.com"
}

project firm_language {
    name = "Firm Language Development"
    owner_ref = person.daniel_rothmann
    organization_ref = organization.main
    budget = 150000 EUR
    technologies = ["Rust", "Tree-sitter", "WASM"]
}
"#;
        assert_eq!(result, expected);
    }

    #[test]
    fn test_generate_with_custom_options() {
        let entities = vec![
            Entity {
                id: EntityId("person.alice".to_string()),
                entity_type: EntityType::new("person"),
                fields: [(
                    FieldId("name".to_string()),
                    FieldValue::String("Alice".to_string()),
                )]
                .into(),
            },
            Entity {
                id: EntityId("person.bob".to_string()),
                entity_type: EntityType::new("person"),
                fields: [(
                    FieldId("name".to_string()),
                    FieldValue::String("Bob".to_string()),
                )]
                .into(),
            },
        ];

        let options = GeneratorOptions {
            indent_style: IndentStyle::Spaces(2),
            blank_lines_between_entities: false,
            ..Default::default()
        };

        let result = generate_dsl_with_options(&entities, &options);

        let expected = r#"person alice {
  name = "Alice"
}
person bob {
  name = "Bob"
}
"#;
        assert_eq!(result, expected);
    }
}

```

## /firm_lang/src/lib.rs

```rs path="/firm_lang/src/lib.rs" 
//! Parsing and workspace management for the Firm DSL.
//!
//! This crate handles loading `.firm` files, parsing them into an abstract
//! representation, and converting them to Firm's core data structures.

pub mod convert;
pub mod generate;
pub mod parser;
pub mod workspace;

```

## /firm_lang/src/parser/mod.rs

```rs path="/firm_lang/src/parser/mod.rs" 
mod parsed_entity;
mod parsed_field;
mod parsed_schema;
mod parsed_schema_field;
mod parsed_source;
mod parsed_value;
mod parser_errors;
mod parser_utils;
mod source;

pub use parsed_entity::ParsedEntity;
pub use parsed_field::ParsedField;
pub use parsed_schema::ParsedSchema;
pub use parsed_schema_field::ParsedSchemaField;
pub use parsed_source::ParsedSource;
pub use parsed_value::ParsedValue;
pub use parser_errors::{LanguageError, ValueParseError};
pub use source::parse_source;

```

## /firm_lang/src/parser/parsed_entity.rs

```rs path="/firm_lang/src/parser/parsed_entity.rs" 
use std::path::PathBuf;

use tree_sitter::Node;

use super::{
    ParsedField,
    parser_utils::{find_child_of_kind, get_node_text},
};

const ENTITY_TYPE_KIND: &str = "entity_type";
const ENTITY_ID_KIND: &str = "entity_id";
const FIELD_KIND: &str = "field";

/// A parsed entity definition from Firm DSL.
///
/// Represents an entity block like `contact john_doe { ... }` with
/// access to the entity type, ID, and contained fields.
#[derive(Debug)]
pub struct ParsedEntity<'a> {
    node: Node<'a>,
    source: &'a str,
    path: &'a PathBuf,
}

impl<'a> ParsedEntity<'a> {
    /// Creates a new ParsedEntity from a tree-sitter node and source text.
    pub fn new(node: Node<'a>, source: &'a str, path: &'a PathBuf) -> Self {
        Self { node, source, path }
    }

    /// Returns the entity type (e.g., "contact", "role").
    pub fn entity_type(&self) -> Option<&str> {
        let type_node = find_child_of_kind(&self.node, ENTITY_TYPE_KIND)?;
        Some(get_node_text(&type_node, self.source))
    }

    /// Returns the entity ID (e.g., "john_doe", "cto").
    pub fn id(&self) -> Option<&str> {
        let id_node = find_child_of_kind(&self.node, ENTITY_ID_KIND)?;
        Some(get_node_text(&id_node, self.source))
    }

    /// Extracts all field definitions from the entity block.
    pub fn fields(&self) -> Vec<ParsedField> {
        let mut fields = Vec::new();
        let mut cursor = self.node.walk();

        // First find the block node
        if let Some(block_node) = self
            .node
            .children(&mut cursor)
            .find(|child| child.kind() == "block")
        {
            let mut block_cursor = block_node.walk();

            // Then find field nodes within the block
            for child in block_node.children(&mut block_cursor) {
                if child.kind() == FIELD_KIND {
                    fields.push(ParsedField::new(child, &self.source, &self.path));
                }
            }
        }

        fields
    }
}

```

## /firm_lang/src/parser/parsed_field.rs

```rs path="/firm_lang/src/parser/parsed_field.rs" 
use std::path::PathBuf;

use tree_sitter::Node;

use super::{
    parsed_value::ParsedValue, parser_errors::ValueParseError, parser_utils::find_child_of_kind,
    parser_utils::get_node_text,
};

const FIELD_ID_KIND: &str = "field_name";
const VALUE_KIND: &str = "value";

/// A parsed field definition from an entity block.
///
/// Represents a field assignment like `name = "John Doe"` with
/// access to the field name and parsed value.
#[derive(Debug)]
pub struct ParsedField<'a> {
    node: Node<'a>,
    source: &'a str,
    path: &'a PathBuf,
}

impl<'a> ParsedField<'a> {
    /// Creates a new ParsedField from a tree-sitter node and source text.
    pub fn new(node: Node<'a>, source: &'a str, path: &'a PathBuf) -> Self {
        Self { node, source, path }
    }

    /// Gets the field name (e.g., "name", "age").
    pub fn id(&self) -> Option<&str> {
        let id_node = find_child_of_kind(&self.node, FIELD_ID_KIND)?;
        Some(get_node_text(&id_node, self.source))
    }

    /// Parses and gets the field's value with full type information.
    pub fn value(&self) -> Result<ParsedValue, ValueParseError> {
        let value_node =
            find_child_of_kind(&self.node, VALUE_KIND).ok_or(ValueParseError::MissingValue)?;

        ParsedValue::from_node(value_node, self.source, self.path)
    }
}

```

## /firm_lang/src/parser/parsed_schema.rs

```rs path="/firm_lang/src/parser/parsed_schema.rs" 
use std::path::PathBuf;

use tree_sitter::Node;

use super::{
    ParsedSchemaField,
    parser_utils::{find_child_of_kind, get_node_text},
};

const SCHEMA_NAME_KIND: &str = "schema_name";
const NESTED_BLOCK_KIND: &str = "nested_block";

/// A parsed schema definition from Firm DSL.
///
/// Represents a schema block like `schema project { ... }` with
/// access to the schema name and contained field definitions.
#[derive(Debug)]
pub struct ParsedSchema<'a> {
    node: Node<'a>,
    source: &'a str,
    path: &'a PathBuf,
}

impl<'a> ParsedSchema<'a> {
    /// Creates a new ParsedSchema from a tree-sitter node and source text.
    pub fn new(node: Node<'a>, source: &'a str, path: &'a PathBuf) -> Self {
        Self { node, source, path }
    }

    /// Gets the schema name (e.g., "project", "invoice").
    pub fn name(&self) -> Option<&str> {
        let name_node = find_child_of_kind(&self.node, SCHEMA_NAME_KIND)?;
        Some(get_node_text(&name_node, self.source))
    }

    /// Extracts all field definitions from the schema block.
    pub fn fields(&self) -> Vec<ParsedSchemaField> {
        let mut fields = Vec::new();
        let mut cursor = self.node.walk();

        // First find the block node
        if let Some(block_node) = self
            .node
            .children(&mut cursor)
            .find(|child| child.kind() == "block")
        {
            let mut block_cursor = block_node.walk();

            // Then find nested_block nodes within the block (these are the field definitions)
            for child in block_node.children(&mut block_cursor) {
                if child.kind() == NESTED_BLOCK_KIND {
                    // Verify this is a "field" block by checking the block_type
                    let mut nested_cursor = child.walk();
                    if let Some(block_type_node) = child
                        .children(&mut nested_cursor)
                        .find(|c| c.kind() == "block_type")
                    {
                        let block_type = get_node_text(&block_type_node, self.source);
                        if block_type == "field" {
                            fields.push(ParsedSchemaField::new(child, self.source, self.path));
                        }
                    }
                }
            }
        }

        fields
    }
}

```

## /firm_lang/src/parser/parsed_schema_field.rs

```rs path="/firm_lang/src/parser/parsed_schema_field.rs" 
use std::path::PathBuf;
use tree_sitter::Node;

use super::{
    parsed_value::ParsedValue, parser_errors::ValueParseError, parser_utils::find_child_of_kind,
};

const FIELD_KIND: &str = "field";
const BLOCK_KIND: &str = "block";

/// A parsed schema field definition from a schema block.
///
/// Represents a nested field block like:
/// \`\`\`text
/// field {
///     name = "title"
///     type = "string"
///     required = true
/// }
/// \`\`\`
#[derive(Debug)]
pub struct ParsedSchemaField<'a> {
    node: Node<'a>,
    source: &'a str,
    path: &'a PathBuf,
}

impl<'a> ParsedSchemaField<'a> {
    /// Creates a new ParsedSchemaField from a tree-sitter node and source text.
    pub fn new(node: Node<'a>, source: &'a str, path: &'a PathBuf) -> Self {
        Self { node, source, path }
    }

    /// Gets the field name from the "name" field.
    pub fn name(&self) -> Result<String, ValueParseError> {
        let name_field = self
            .find_field_by_name("name")
            .ok_or(ValueParseError::MissingValue)?;

        match name_field.value()? {
            ParsedValue::String(s) => Ok(s),
            _ => Err(ValueParseError::UnknownValueKind),
        }
    }

    /// Gets the field type from the "type" field.
    pub fn field_type(&self) -> Result<String, ValueParseError> {
        let type_field = self
            .find_field_by_name("type")
            .ok_or(ValueParseError::MissingValue)?;

        match type_field.value()? {
            ParsedValue::String(s) => Ok(s),
            _ => Err(ValueParseError::UnknownValueKind),
        }
    }

    /// Checks whether the field is required or not.
    /// Defaults to false if not specified.
    pub fn required(&self) -> bool {
        if let Some(required_field) = self.find_field_by_name("required") {
            if let Ok(ParsedValue::Boolean(b)) = required_field.value() {
                return b;
            }
        }

        false // Default to false if not specified or invalid
    }

    /// Helper method to find a field by name within this schema field block.
    fn find_field_by_name(&self, field_name: &str) -> Option<super::ParsedField> {
        // Find the block node within this field
        let block_node = find_child_of_kind(&self.node, BLOCK_KIND)?;
        let mut cursor = block_node.walk();

        // Look for field assignments within the block
        for child in block_node.children(&mut cursor) {
            if child.kind() == FIELD_KIND {
                let field = super::ParsedField::new(child, self.source, self.path);
                if let Some(id) = field.id() {
                    if id == field_name {
                        return Some(field);
                    }
                }
            }
        }

        None
    }
}

```

## /firm_lang/src/parser/parsed_source.rs

```rs path="/firm_lang/src/parser/parsed_source.rs" 
use std::path::PathBuf;

use tree_sitter::Tree;

use super::{ParsedEntity, ParsedSchema};

const ENTITY_BLOCK_KIND: &str = "entity_block";
const SCHEMA_BLOCK_KIND: &str = "schema_block";

/// A parsed Firm DSL source document.
///
/// Contains the original source text and the tree-sitter parse tree,
/// providing access to entities and syntax error detection.
#[derive(Debug)]
pub struct ParsedSource {
    /// The plain text source file.
    pub source: String,

    /// The parsed syntax tree.
    pub tree: Tree,

    /// The workspace-relative path to this source file.
    pub path: PathBuf,
}

impl ParsedSource {
    /// Creates a new ParsedSource from source text and parse tree.
    pub fn new(source: String, tree: Tree, path: PathBuf) -> Self {
        Self { source, tree, path }
    }

    /// Check if the source contains syntax errors.
    pub fn has_error(&self) -> bool {
        self.tree.root_node().has_error()
    }

    /// Extracts all entity definitions from the parsed source.
    pub fn entities(&self) -> Vec<ParsedEntity> {
        let mut entities = Vec::new();
        let root = self.tree.root_node();
        let mut cursor = root.walk();

        for child in root.children(&mut cursor) {
            if child.kind() == ENTITY_BLOCK_KIND {
                entities.push(ParsedEntity::new(child, &self.source, &self.path));
            }
        }

        entities
    }

    /// Extracts all schema definitions from the parsed source.
    pub fn schemas(&self) -> Vec<ParsedSchema> {
        let mut schemas = Vec::new();
        let root = self.tree.root_node();
        let mut cursor = root.walk();

        for child in root.children(&mut cursor) {
            if child.kind() == SCHEMA_BLOCK_KIND {
                schemas.push(ParsedSchema::new(child, &self.source, &self.path));
            }
        }

        schemas
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use crate::parser::parse_source;

    #[test]
    fn test_has_entities_for_valid_source() {
        let source = r#"
            role cto {
                name = "CTO"
                executive = true
            }

            contact john_doe {
                name = "John Doe"
                role = role.cto
            }
        "#;

        let parsed = parse_source(String::from(source), None).unwrap();

        assert!(!parsed.has_error());

        let entities = parsed.entities();
        assert!(entities.len() == 2);
    }

    #[test]
    fn test_has_schemas_for_valid_source() {
        let source = r#"
            schema project {
                field {
                    name = "title"
                    type = "string"
                    required = true
                }

                field {
                    name = "priority"
                    type = "integer"
                    required = false
                }
            }

            contact john_doe {
                name = "John Doe"
            }
        "#;

        let parsed = parse_source(String::from(source), None).unwrap();

        assert!(!parsed.has_error());

        let schemas = parsed.schemas();
        assert_eq!(schemas.len(), 1);

        let schema = &schemas[0];
        assert_eq!(schema.name(), Some("project"));

        let fields = schema.fields();
        assert_eq!(fields.len(), 2);

        // Test first field
        let field1 = &fields[0];
        assert_eq!(field1.name().unwrap(), "title");
        assert_eq!(field1.field_type().unwrap(), "string");
        assert_eq!(field1.required(), true);

        // Test second field
        let field2 = &fields[1];
        assert_eq!(field2.name().unwrap(), "priority");
        assert_eq!(field2.field_type().unwrap(), "integer");
        assert_eq!(field2.required(), false);
    }

    #[test]
    fn test_no_error_for_valid_source() {
        let source = r#"
            // Some sort of tech boss?
            role cto {
                name = "CTO"
                executive = true
            }

            // A person I know
            contact john_doe {
                name = "John Doe"
                role = role.cto
            }
        "#;

        let parsed = parse_source(String::from(source), None).unwrap();
        assert!(!parsed.has_error());
    }

    #[test]
    fn test_error_for_incomplete_entity_block() {
        let source = r#"
            role cto {
                name = "CTO"
                executive = true

                // Entity block is incomplete...
        "#;

        let parsed = parse_source(String::from(source), None).unwrap();
        assert!(parsed.has_error());
    }

    #[test]
    fn test_error_for_malformed_reference() {
        let source = r#"
            contact test {
                bad_ref = contact.too.many.parts.here
            }
        "#;

        let parsed = parse_source(String::from(source), None).unwrap();
        assert!(parsed.has_error());
    }

    #[test]
    fn test_error_for_malformed_number() {
        let source = r#"
            contact test {
                bad_number = 42.3.4
            }
        "#;

        let parsed = parse_source(String::from(source), None).unwrap();
        assert!(parsed.has_error());
    }

    #[test]
    fn test_error_for_missing_field_value() {
        let source = r#"
            contact test {
                name =
            }
        "#;

        let parsed = parse_source(String::from(source), None).unwrap();
        assert!(parsed.has_error());
    }

    #[test]
    fn test_error_for_missing_entity_id() {
        let source = r#"
            contact {
                name = "Test"
            }
        "#;

        let parsed = parse_source(String::from(source), None).unwrap();
        assert!(parsed.has_error());
    }

    #[test]
    fn test_error_for_unclosed_string() {
        let source = r#"
            contact test {
                name = "Unclosed string
            }
        "#;

        let parsed = parse_source(String::from(source), None).unwrap();
        assert!(parsed.has_error());
    }

    #[test]
    fn test_schema_with_complex_fields() {
        let source = r#"
            schema project {
                field {
                    name = "title"
                    type = "string"
                    required = true
                }

                field {
                    name = "priority"
                    type = "integer"
                    required = false
                }

                field {
                    name = "budget"
                    type = "currency"
                    required = false
                }
            }

            schema invoice {
                field {
                    name = "amount"
                    type = "currency"
                    required = true
                }

                field {
                    name = "description"
                    type = "string"
                }
            }
        "#;

        let parsed = parse_source(String::from(source), None).unwrap();
        assert!(!parsed.has_error());

        let schemas = parsed.schemas();
        assert_eq!(schemas.len(), 2);

        // Test first schema
        let project_schema = &schemas[0];
        assert_eq!(project_schema.name(), Some("project"));
        let project_fields = project_schema.fields();
        assert_eq!(project_fields.len(), 3);

        // Test required field
        let title_field = &project_fields[0];
        assert_eq!(title_field.name().unwrap(), "title");
        assert_eq!(title_field.field_type().unwrap(), "string");
        assert_eq!(title_field.required(), true);

        // Test optional field
        let priority_field = &project_fields[1];
        assert_eq!(priority_field.name().unwrap(), "priority");
        assert_eq!(priority_field.field_type().unwrap(), "integer");
        assert_eq!(priority_field.required(), false);

        // Test second schema
        let invoice_schema = &schemas[1];
        assert_eq!(invoice_schema.name(), Some("invoice"));
        let invoice_fields = invoice_schema.fields();
        assert_eq!(invoice_fields.len(), 2);

        // Test field without explicit required (should default to false)
        let description_field = &invoice_fields[1];
        assert_eq!(description_field.name().unwrap(), "description");
        assert_eq!(description_field.field_type().unwrap(), "string");
        assert_eq!(description_field.required(), false);
    }

    #[test]
    fn test_passes_default_path() {
        let parsed = parse_source(String::new(), None).unwrap();
        assert!(parsed.path == PathBuf::new());
    }

    #[test]
    fn test_passes_custom_path() {
        let path = PathBuf::from("./subdirectory/file.firm");
        let parsed = parse_source(String::new(), Some(path.clone())).unwrap();
        assert_eq!(parsed.path, path);
    }
}

```

## /firm_lang/src/parser/parsed_value.rs

```rs path="/firm_lang/src/parser/parsed_value.rs" 
use chrono::{DateTime, FixedOffset, Local, NaiveDate, NaiveTime, Offset, TimeZone};
use iso_currency::Currency;
use path_clean::PathClean;
use rust_decimal::Decimal;
use std::path::PathBuf;
use tree_sitter::Node;

use super::{parser_errors::ValueParseError, parser_utils::get_node_text};

const VALUE_KIND: &str = "value";

/// Internal enum for identifying value types during parsing.
#[derive(Debug, Clone, PartialEq)]
enum ValueKind {
    Boolean,
    String,
    Number,
    Currency,
    Reference,
    List,
    DateTime,
    Date,
    Path,
    Unknown(String),
}

impl From<&str> for ValueKind {
    fn from(kind: &str) -> Self {
        match kind {
            "boolean" => ValueKind::Boolean,
            "string" => ValueKind::String,
            "number" => ValueKind::Number,
            "currency" => ValueKind::Currency,
            "reference" => ValueKind::Reference,
            "list" => ValueKind::List,
            "datetime" => ValueKind::DateTime,
            "date" => ValueKind::Date,
            "path" => ValueKind::Path,
            _ => ValueKind::Unknown(kind.to_string()),
        }
    }
}

/// A typed value parsed from Firm DSL source.
///
/// Supports Firm value types including primitives, structured,
/// and temporal types with type safety and validation.
#[derive(Debug, Clone, PartialEq)]
pub enum ParsedValue {
    /// Boolean value (`true` or `false`)
    Boolean(bool),
    /// String value (single or multi-line)
    String(String),
    /// Integer value (`42`)
    Integer(i64),
    /// Floating-point value (`42.5`)
    Float(f64),
    /// Currency value with amount and code (`100.50 USD`)
    Currency { amount: Decimal, currency: Currency },
    /// Entity reference (`contact.john_doe`)
    EntityReference {
        entity_type: String,
        entity_id: String,
    },
    /// Field reference (`contact.john_doe.name`)
    FieldReference {
        entity_type: String,
        entity_id: String,
        field_id: String,
    },
    /// List of values (`["item1", "item2", 42]`)
    List(Vec<ParsedValue>),
    /// Date or datetime value with timezone
    DateTime(DateTime<FixedOffset>),
    /// A path to a file or directory
    Path(PathBuf),
}

impl ParsedValue {
    /// Gets the type name of this parsed value for error reporting and type checking.
    pub fn get_type_name(&self) -> &'static str {
        match self {
            ParsedValue::Boolean(_) => "Boolean",
            ParsedValue::String(_) => "String",
            ParsedValue::Integer(_) => "Integer",
            ParsedValue::Float(_) => "Float",
            ParsedValue::Currency { .. } => "Currency",
            ParsedValue::EntityReference { .. } => "EntityReference",
            ParsedValue::FieldReference { .. } => "FieldReference",
            ParsedValue::List(_) => "List",
            ParsedValue::DateTime(_) => "DateTime",
            ParsedValue::Path(_) => "Path",
        }
    }

    /// Parses a value from a tree-sitter node with type conversion.
    pub fn from_node<'a>(
        node: Node<'a>,
        source: &'a str,
        path: &'a PathBuf,
    ) -> Result<ParsedValue, ValueParseError> {
        let kind = Self::get_value_kind(node).ok_or(ValueParseError::UnknownValueKind)?;
        let raw = get_node_text(&node, source);

        match kind {
            ValueKind::Boolean => Self::parse_boolean(&raw),
            ValueKind::String => Self::parse_string(&raw),
            ValueKind::Number => Self::parse_number(&raw),
            ValueKind::Currency => Self::parse_currency(&raw),
            ValueKind::Reference => Self::parse_reference(&raw),
            ValueKind::List => Self::parse_list(node, source, path),
            ValueKind::Date => Self::parse_date(&raw),
            ValueKind::DateTime => Self::parse_datetime(&raw),
            ValueKind::Path => Self::parse_path(&raw, path),
            _ => Err(ValueParseError::MissingParseMethod),
        }
    }

    /// Determines the value type from a tree-sitter node.
    fn get_value_kind<'a>(value_node: Node<'a>) -> Option<ValueKind> {
        let mut cursor = value_node.walk();
        let kind = value_node.children(&mut cursor).next()?.kind();
        Some(kind.into())
    }

    /// Parses boolean values (`true` or `false`).
    fn parse_boolean(raw: &str) -> Result<ParsedValue, ValueParseError> {
        raw.parse()
            .map(ParsedValue::Boolean)
            .map_err(|_| ValueParseError::InvalidBoolean(raw.to_string()))
    }

    /// Parses string values, handling both single-line and multi-line formats.
    fn parse_string(raw: &str) -> Result<ParsedValue, ValueParseError> {
        // Multi-line strings start and end with triple quotes ("""stuff""")
        if raw.starts_with("\"\"\"") && raw.ends_with("\"\"\"") {
            // Handle triple quotes
            let content = raw.trim_start_matches("\"\"\"").trim_end_matches("\"\"\"");

            // Remove common indentation
            let trimmed = Self::trim_common_indentation(content);
            Ok(ParsedValue::String(trimmed))
        }
        // Single-line strings start and end with single quotes ("stuff")
        else {
            // Handle single quotes
            Ok(ParsedValue::String(raw.trim_matches('"').to_string()))
        }
    }

    /// Parses numeric values, distinguishing between integers and floats.
    fn parse_number(raw: &str) -> Result<ParsedValue, ValueParseError> {
        // Numbers with a period are floats (42.0)
        if raw.contains(".") {
            raw.parse()
                .map(ParsedValue::Float)
                .map_err(|_| ValueParseError::InvalidFloat(raw.to_string()))
        }
        // Numbers without are period are ints (42)
        else {
            raw.parse()
                .map(ParsedValue::Integer)
                .map_err(|_| ValueParseError::InvalidInteger(raw.to_string()))
        }
    }

    /// Parses currency values with amount and currency code (`42.50 USD`).
    fn parse_currency(raw: &str) -> Result<ParsedValue, ValueParseError> {
        let parts: Vec<&str> = raw.split(" ").collect();

        // Currencies have 2 parts: number and currency (42 USD or 42.24 EUR)
        match parts.as_slice() {
            [raw_amount, raw_currency] => {
                let amount = rust_decimal::Decimal::from_str_exact(&raw_amount)
                    .map_err(|_| ValueParseError::InvalidCurrencyAmount(raw_amount.to_string()))?;

                let currency = raw_currency
                    .parse::<Currency>()
                    .map_err(|_| ValueParseError::InvalidCurrencyCode(raw_currency.to_string()))?;

                Ok(ParsedValue::Currency { amount, currency })
            }
            _ => Err(ValueParseError::InvalidCurrencyFormat {
                source: raw.to_string(),
                parts_count: parts.len(),
            }),
        }
    }

    /// Parses reference values (entity or field references).
    fn parse_reference(raw: &str) -> Result<ParsedValue, ValueParseError> {
        let parts: Vec<&str> = raw.split(".").collect();
        match parts.len() {
            // References with 2 parts are for entities (contact.john)
            2 => Ok(ParsedValue::EntityReference {
                entity_type: parts[0].to_string(),
                entity_id: parts[1].to_string(),
            }),
            // References with 3 parts are for fields (contact.john.name)
            3 => Ok(ParsedValue::FieldReference {
                entity_type: parts[0].to_string(),
                entity_id: parts[1].to_string(),
                field_id: parts[2].to_string(),
            }),
            // References with more or less parts are invalid
            _ => Err(ValueParseError::InvalidReferenceFormat {
                source: raw.to_string(),
                parts_count: parts.len(),
            }),
        }
    }

    /// Parses list values by recursively parsing each contained value.
    /// Ensures all list items are of the same type (homogeneous lists).
    fn parse_list<'a>(
        node: Node<'a>,
        source: &'a str,
        path: &'a PathBuf,
    ) -> Result<ParsedValue, ValueParseError> {
        // For lists, we walk each child value node and parse it
        let mut items: Vec<ParsedValue> = Vec::new();
        let mut cursor = node.walk();
        let mut expected_type: Option<&'static str> = None;

        if let Some(list_node) = node.children(&mut cursor).next() {
            let mut list_cursor = list_node.walk();
            let mut index = 0;

            for child in list_node.children(&mut list_cursor) {
                if child.kind() == VALUE_KIND {
                    // Recursively parse the list child value
                    let item = Self::from_node(child, source, path)?;

                    // Check type homogeneity
                    let item_type = item.get_type_name();
                    match expected_type {
                        None => {
                            // First item - establish the expected type
                            expected_type = Some(item_type);
                        }
                        Some(expected) => {
                            // Subsequent items - check they match the first type
                            if item_type != expected {
                                return Err(ValueParseError::HeterogeneousList {
                                    expected_type: expected.to_string(),
                                    found_type: item_type.to_string(),
                                    index,
                                });
                            }
                        }
                    }

                    items.push(item);
                    index += 1;
                }
            }
        }

        Ok(ParsedValue::List(items))
    }

    /// Parses date values (`2024-03-20`) as datetime at midnight local time.
    fn parse_date(raw: &str) -> Result<ParsedValue, ValueParseError> {
        // Parse "naive date" in year-month-day format (2025-07-31)
        let date = NaiveDate::parse_from_str(raw, "%Y-%m-%d")
            .map_err(|_| ValueParseError::InvalidDate(raw.to_string()))?;

        // Assume time is midnight local time
        let datetime = date.and_hms_opt(0, 0, 0).unwrap();
        let local_offset = Local::now().offset().fix();

        // Convert from local datetime to timezoned datetime
        let with_tz = local_offset
            .from_local_datetime(&datetime)
            .single()
            .ok_or_else(|| ValueParseError::InvalidDate(raw.to_string()))?;

        Ok(ParsedValue::DateTime(with_tz))
    }

    /// Parses datetime values with optional timezone (`2024-03-20 at 14:30 UTC-5`).
    fn parse_datetime(raw: &str) -> Result<ParsedValue, ValueParseError> {
        // Datetimes start with year-month-day (2025-07-31) followed by " at ", then time (09:42), optionally timezone " UTC+3"
        let parts: Vec<&str> = raw.split(" at ").collect();
        match parts.as_slice() {
            [date_part, time_and_tz] => {
                // Parse the date part
                let date = NaiveDate::parse_from_str(date_part, "%Y-%m-%d")
                    .map_err(|_| ValueParseError::InvalidDateTime(raw.to_string()))?;

                // Split time and timezone
                let (time_part, tz_offset) = if time_and_tz.contains(" UTC") {
                    let tz_parts: Vec<&str> = time_and_tz.split(" UTC").collect();
                    let offset_str = tz_parts.get(1).unwrap_or(&"");
                    (tz_parts[0], Self::parse_utc_offset(offset_str)?)
                } else {
                    // No timezone specified - use local timezone
                    let local_offset = Local::now().offset().fix();
                    (*time_and_tz, local_offset)
                };

                // Parse time (handle both h:mm and hh:mm)
                let time = NaiveTime::parse_from_str(time_part, "%H:%M")
                    .or_else(|_| NaiveTime::parse_from_str(time_part, "%k:%M"))
                    .map_err(|_| ValueParseError::InvalidDateTime(raw.to_string()))?;

                // Combine date and time, then apply timezone
                let naive_dt = date.and_time(time);
                let dt = tz_offset
                    .from_local_datetime(&naive_dt)
                    .single()
                    .ok_or_else(|| ValueParseError::InvalidDateTime(raw.to_string()))?;

                Ok(ParsedValue::DateTime(dt))
            }
            _ => Err(ValueParseError::InvalidDateTime(raw.to_string())),
        }
    }

    /// Parses file path values.
    ///
    /// Relative paths are assumed to be relative to the source file they're defined in.
    /// On parse, we transform the source-relative path to a workspace-relative path and normalize it.
    /// Absolute paths are left as-is.
    fn parse_path(raw: &str, source_path: &PathBuf) -> Result<ParsedValue, ValueParseError> {
        let raw_path = raw.replace("path\"", "").trim_matches('"').to_string();
        let target_path = PathBuf::from(raw_path);

        // Transform the target path to the source path's parent directory
        let combined_path = if target_path.is_absolute() {
            target_path
        } else if let Some(source_dir) = source_path.parent() {
            source_dir.join(&target_path)
        } else {
            target_path
        };

        // Clean the path, and prepend ./ for consistency if it's relative
        let cleaned_path = combined_path.clean();
        let final_path = if cleaned_path.is_relative() && !cleaned_path.starts_with("..") {
            PathBuf::from("./").join(cleaned_path)
        } else {
            cleaned_path
        };

        Ok(ParsedValue::Path(final_path))
    }

    /// Removes common leading whitespace from multi-line strings.
    fn trim_common_indentation(s: &str) -> String {
        let lines: Vec<&str> = s.lines().collect();

        // Find minimum indentation of non-empty lines (skip first/last if empty)
        let min_indent = lines
            .iter()
            .filter(|line| !line.trim().is_empty())
            .map(|line| line.len() - line.trim_start().len())
            .min()
            .unwrap_or(0);

        // Remove common indentation and trim leading/trailing empty lines
        lines
            .iter()
            .map(|line| {
                if line.len() >= min_indent {
                    &line[min_indent..]
                } else {
                    line
                }
            })
            .collect::<Vec<_>>()
            .join("\n")
            .trim()
            .to_string()
    }

    /// Parses UTC timezone offset strings (e.g., "+3", "-5", or empty for UTC).
    fn parse_utc_offset(offset_str: &str) -> Result<FixedOffset, ValueParseError> {
        if offset_str.is_empty() {
            // Just "UTC" with no offset
            return Ok(FixedOffset::east_opt(0).unwrap());
        }

        let hours: i32 = offset_str
            .parse()
            .map_err(|_| ValueParseError::InvalidTimezone(offset_str.to_string()))?;

        FixedOffset::east_opt(hours * 3600)
            .ok_or_else(|| ValueParseError::InvalidTimezone(offset_str.to_string()))
    }
}

```

## /firm_lang/src/parser/parser_errors.rs

```rs path="/firm_lang/src/parser/parser_errors.rs" 
use std::fmt;

/// Errors that can occur during parser initialization.
#[derive(Debug, Clone, PartialEq)]
pub enum LanguageError {
    IncompatibleLanguageVersion,
    LanguageNotInitialized,
}

impl fmt::Display for LanguageError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LanguageError::IncompatibleLanguageVersion => write!(
                f,
                "Tree-sitter language version is incompatible with parser"
            ),
            LanguageError::LanguageNotInitialized => {
                write!(f, "Parser was not initialized with the language")
            }
        }
    }
}

/// Errors that can occur when parsing values from DSL source.
#[derive(Debug, Clone, PartialEq)]
pub enum ValueParseError {
    UnknownValueKind,
    MissingValue,
    MissingParseMethod,
    InvalidBoolean(String),
    InvalidInteger(String),
    InvalidFloat(String),
    InvalidCurrencyFormat {
        source: String,
        parts_count: usize,
    },
    InvalidCurrencyAmount(String),
    InvalidCurrencyCode(String),
    InvalidReferenceFormat {
        source: String,
        parts_count: usize,
    },
    InvalidDate(String),
    InvalidDateTime(String),
    InvalidTimezone(String),
    HeterogeneousList {
        expected_type: String,
        found_type: String,
        index: usize,
    },
}

impl fmt::Display for ValueParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ValueParseError::UnknownValueKind => {
                write!(
                    f,
                    "Value type could not be determined from tree-sitter node"
                )
            }
            ValueParseError::MissingValue => {
                write!(f, "Field is missing its value assignment")
            }
            ValueParseError::MissingParseMethod => {
                write!(f, "No parsing method implemented for this value type")
            }
            ValueParseError::InvalidBoolean(value) => {
                write!(f, "Boolean value could not be parsed: '{}'", value)
            }
            ValueParseError::InvalidInteger(value) => {
                write!(f, "Integer value could not be parsed: '{}'", value)
            }
            ValueParseError::InvalidFloat(value) => {
                write!(f, "Float value could not be parsed: '{}'", value)
            }
            ValueParseError::InvalidCurrencyFormat {
                source,
                parts_count,
            } => {
                write!(
                    f,
                    "Currency format is invalid (expected 'amount currency'): '{}' has {} part(s)",
                    source, parts_count
                )
            }
            ValueParseError::InvalidCurrencyAmount(amount) => {
                write!(
                    f,
                    "Currency amount could not be parsed as decimal: '{}'",
                    amount
                )
            }
            ValueParseError::InvalidCurrencyCode(code) => {
                write!(f, "Currency code is not recognized: '{}'", code)
            }
            ValueParseError::InvalidReferenceFormat {
                source,
                parts_count,
            } => {
                write!(
                    f,
                    "Reference format is invalid (expected 2 or 3 dot-separated parts): '{}' has {} part(s)",
                    source, parts_count
                )
            }
            ValueParseError::InvalidDate(date) => {
                write!(f, "Date value could not be parsed: '{}'", date)
            }
            ValueParseError::InvalidDateTime(datetime) => {
                write!(f, "DateTime value could not be parsed: '{}'", datetime)
            }
            ValueParseError::InvalidTimezone(timezone) => {
                write!(f, "Timezone offset could not be parsed: '{}'", timezone)
            }
            ValueParseError::HeterogeneousList {
                expected_type,
                found_type,
                index,
            } => {
                write!(
                    f,
                    "List contains values of different types (but must be homogeneous): expected {}, found {} at index {}",
                    expected_type, found_type, index
                )
            }
        }
    }
}

```

## /firm_lang/src/parser/parser_utils.rs

```rs path="/firm_lang/src/parser/parser_utils.rs" 
use tree_sitter::Node;

/// Finds the first child node of a specific kind.
pub fn find_child_of_kind<'a>(node: &Node<'a>, kind: &str) -> Option<Node<'a>> {
    let mut cursor = node.walk();

    node.children(&mut cursor)
        .find(|child| child.kind() == kind)
}

/// Extracts the text content of a node from the source string.
pub fn get_node_text<'a>(node: &Node<'a>, source: &'a str) -> &'a str {
    &source[node.byte_range()]
}

```

## /firm_lang/src/parser/source.rs

```rs path="/firm_lang/src/parser/source.rs" 
use std::path::PathBuf;

use tree_sitter::{Language, Parser};

use super::LanguageError;
use super::ParsedSource;

/// Gets the tree-sitter language for Firm DSL.
fn language() -> Language {
    tree_sitter_firm::LANGUAGE.into()
}

/// Parses Firm DSL source code into a structured representation.
///
/// This is the main entry point for parsing Firm DSL. It initializes
/// a tree-sitter parser and returns ParsedSource for further processing.
pub fn parse_source(source: String, path: Option<PathBuf>) -> Result<ParsedSource, LanguageError> {
    let mut parser = Parser::new();
    parser
        .set_language(&language())
        .map_err(|_| LanguageError::IncompatibleLanguageVersion)?;

    match parser.parse(&source, None) {
        Some(tree) => Ok(ParsedSource::new(source, tree, path.unwrap_or_default())),
        None => Err(LanguageError::LanguageNotInitialized),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_language_valid() {
        let source = r#"
            contact john_doe {
                name = "John Doe"
                email = "john@example.com"
                age = 42
            }
        "#;

        let result = parse_source(String::from(source), None);
        assert!(result.is_ok());
    }
}

```

## /firm_lang/src/workspace/build.rs

```rs path="/firm_lang/src/workspace/build.rs" 
use firm_core::{Entity, EntitySchema, EntityType};
use std::collections::HashMap;

use super::{Workspace, WorkspaceError};

/// Holds converted entities and schemas after the workspace is built.
#[derive(Debug)]
pub struct WorkspaceBuild {
    pub entities: Vec<Entity>,
    pub schemas: Vec<EntitySchema>,
}

impl WorkspaceBuild {
    pub fn new(entities: Vec<Entity>, schemas: Vec<EntitySchema>) -> Self {
        WorkspaceBuild { entities, schemas }
    }
}

impl Workspace {
    /// Build the workspace from all loaded files.
    pub fn build(&mut self) -> Result<WorkspaceBuild, WorkspaceError> {
        self.build_with_progress(|current, total, phase| {
            log::debug!("{}: {}/{}", phase, current, total);
        })
    }

    /// Build the workspace with progress reporting.
    pub fn build_with_progress<F>(
        &mut self,
        mut progress: F,
    ) -> Result<WorkspaceBuild, WorkspaceError>
    where
        F: FnMut(usize, usize, &str),
    {
        // Get all built-in schemas
        let builtin_schemas = EntitySchema::all_builtin();
        let mut schemas: HashMap<EntityType, EntitySchema> = builtin_schemas
            .into_iter()
            .map(|schema| (schema.entity_type.clone(), schema))
            .collect();

        let files_to_process = self.num_files();
        let mut files_processed = 0;
        progress(files_to_process, files_processed, "Building schemas");

        // First pass: Walk through workspace files to add custom schemas
        for (path, file) in &self.files {
            let parsed_schemas = file.parsed.schemas();
            for parsed_schema in &parsed_schemas {
                let schema = EntitySchema::try_from(parsed_schema)
                    .map_err(|err| WorkspaceError::ParseError(path.clone(), err.to_string()))?;

                if schemas.contains_key(&schema.entity_type) {
                    return Err(WorkspaceError::ValidationError(
                        path.clone(),
                        "Stuff".to_string(),
                    ));
                }

                schemas.insert(schema.entity_type.clone(), schema);
            }
        }

        // Second pass: Walk through workspace files to build and validate entities against schemas
        let mut entities = Vec::new();

        files_processed = 0;

        for (path, file) in &self.files {
            progress(files_to_process, files_processed, "Building entities");

            let parsed_entities = file.parsed.entities();
            for parsed_entity in &parsed_entities {
                // Build the entity
                let entity = Entity::try_from(parsed_entity)
                    .map_err(|err| WorkspaceError::ParseError(path.clone(), err.to_string()))?;

                // Find the appropriate schema for this entity
                let schema = schemas.get(&entity.entity_type).ok_or_else(|| {
                    WorkspaceError::ValidationError(
                        path.clone(),
                        format!("No schema found for entity type: {:?}", entity.entity_type),
                    )
                })?;

                // Validate the entity against its schema
                if let Err(validation_errors) = schema.validate(&entity) {
                    let error_msg = format!(
                        "Entity '{}' failed validation: {:?}",
                        entity.id, validation_errors
                    );
                    return Err(WorkspaceError::ValidationError(path.clone(), error_msg));
                }

                entities.push(entity);
            }

            files_processed += 1;
        }

        let schemas_vec = schemas.into_values().collect();
        Ok(WorkspaceBuild::new(entities, schemas_vec))
    }
}

```

## /media/demo.gif

Binary file available at https://raw.githubusercontent.com/42futures/firm/refs/heads/main/media/demo.gif

## /media/demo.tape

```tape path="/media/demo.tape" 
Output ../media/demo.gif
Set FontSize 14
Set Width 1000
Set Height 600
Set WindowBar Colorful
Set Margin 20
Set MarginFill "#c566ff"
Set BorderRadius 10
Set LineHeight 1.3

Type "tree"
Sleep 0.5s
Enter
Sleep 2s

Type "cat core/main.firm"
Sleep 0.5s
Enter
Sleep 3s

Type "clear"
Sleep 0.5s
Enter
Sleep 500ms

# List entities
Type "firm build"
Sleep 0.5s
Enter
Sleep 2s

# List entities
Type "firm -c list contact"
Sleep 0.5s
Enter
Sleep 3s

# The killer feature: traverse the graph
Type "firm -c related contact kent_smith"
Sleep 0.5s
Enter
Sleep 3s

Type "firm -c get task kent_lunch_meeting"
Sleep 0.5s
Enter
Sleep 3s

```


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!