42futures/firm/main 188k tokens More Tools
```
├── .gitattributes (omitted)
├── .github/
   ├── ISSUE_TEMPLATE/
      ├── bug_report.md (100 tokens)
      ├── feature_request.md (100 tokens)
   ├── workflows/
      ├── docs.yml (200 tokens)
      ├── pull_request.yml (100 tokens)
      ├── release.yml (300 tokens)
├── .gitignore (200 tokens)
├── .gitmodules
├── CHANGES.md (900 tokens)
├── Cargo.lock (omitted)
├── Cargo.toml
├── LICENSE (omitted)
├── README.md (400 tokens)
├── docs/
   ├── book.toml (100 tokens)
   ├── src/
      ├── SUMMARY.md (200 tokens)
      ├── concepts/
         ├── built-in-entities.md (600 tokens)
         ├── entities.md (300 tokens)
         ├── fields.md (600 tokens)
         ├── relationships.md (300 tokens)
         ├── schemas.md (400 tokens)
      ├── getting-started/
         ├── installation.md (200 tokens)
         ├── quick-start.md (300 tokens)
         ├── workspace.md (300 tokens)
      ├── guide/
         ├── automations-and-ai.md (600 tokens)
         ├── creating-schemas.md (500 tokens)
         ├── querying.md (600 tokens)
         ├── relationships.md (500 tokens)
      ├── introduction.md (300 tokens)
      ├── library/
         ├── architecture.md (500 tokens)
         ├── examples.md (900 tokens)
         ├── getting-started.md (500 tokens)
      ├── media/
         ├── demo.gif
         ├── demo.tape (100 tokens)
         ├── demo/
            ├── contacts/
               ├── alice.firm
               ├── bob.firm
            ├── me.firm
            ├── projects/
               ├── platform.firm
               ├── website.firm
            ├── schemas.firm (200 tokens)
            ├── tasks.firm (100 tokens)
      ├── reference/
         ├── cli-reference.md (1700 tokens)
         ├── dsl-reference.md (900 tokens)
         ├── query-reference.md (1800 tokens)
   ├── theme/
      ├── custom.css (900 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)
   ├── schemas/
      ├── account.firm (200 tokens)
      ├── channel.firm (100 tokens)
      ├── contact.firm (200 tokens)
      ├── file_asset.firm (200 tokens)
      ├── industry.firm (100 tokens)
      ├── interaction.firm (300 tokens)
      ├── key_result.firm (200 tokens)
      ├── lead.firm (200 tokens)
      ├── objective.firm (200 tokens)
      ├── opportunity.firm (200 tokens)
      ├── organization.firm (200 tokens)
      ├── person.firm (100 tokens)
      ├── project.firm (200 tokens)
      ├── review.firm (200 tokens)
      ├── strategy.firm (200 tokens)
      ├── task.firm (200 tokens)
├── firm_cli/
   ├── AGENTS.md.template (1600 tokens)
   ├── Cargo.toml (200 tokens)
   ├── src/
      ├── cli.rs (800 tokens)
      ├── commands/
         ├── add.rs (4.2k tokens)
         ├── build.rs (600 tokens)
         ├── check.rs (500 tokens)
         ├── field_prompt.rs (4.5k tokens)
         ├── get.rs (600 tokens)
         ├── init.rs (1700 tokens)
         ├── list.rs (400 tokens)
         ├── lsp.rs (100 tokens)
         ├── mcp.rs (200 tokens)
         ├── mod.rs (100 tokens)
         ├── query.rs (300 tokens)
         ├── related.rs (300 tokens)
         ├── source.rs (500 tokens)
      ├── errors.rs
      ├── files.rs (600 tokens)
      ├── logging.rs (500 tokens)
      ├── main.rs (700 tokens)
      ├── query.rs (100 tokens)
      ├── ui.rs (900 tokens)
├── firm_core/
   ├── Cargo.toml (100 tokens)
   ├── src/
      ├── entity.rs (600 tokens)
      ├── field.rs (2.8k tokens)
      ├── graph/
         ├── access.rs (5k tokens)
         ├── graph_errors.rs (100 tokens)
         ├── mod.rs (3.4k tokens)
         ├── query/
            ├── aggregation/
               ├── average.rs (900 tokens)
               ├── count.rs (600 tokens)
               ├── median.rs (1000 tokens)
               ├── mod.rs (900 tokens)
               ├── select.rs (1100 tokens)
               ├── sum.rs (1700 tokens)
            ├── filter/
               ├── boolean.rs (1100 tokens)
               ├── currency.rs (1800 tokens)
               ├── datetime.rs (1800 tokens)
               ├── list.rs (4.2k tokens)
               ├── mod.rs (1800 tokens)
               ├── numeric.rs (2.4k tokens)
               ├── reference.rs (1600 tokens)
               ├── string.rs (2.5k tokens)
               ├── types.rs (400 tokens)
            ├── mod.rs (100 tokens)
            ├── order.rs (5.3k tokens)
            ├── query_errors.rs (500 tokens)
            ├── related.rs (2000 tokens)
            ├── types.rs (2.8k tokens)
      ├── id.rs (600 tokens)
      ├── lib.rs (100 tokens)
      ├── schema/
         ├── mod.rs (1300 tokens)
         ├── validation.rs (2.1k tokens)
         ├── validation_errors.rs (700 tokens)
├── firm_lang/
   ├── Cargo.toml (100 tokens)
   ├── src/
      ├── convert/
         ├── conversion_errors.rs (400 tokens)
         ├── mod.rs
         ├── to_entity.rs (600 tokens)
         ├── to_query.rs (1600 tokens)
         ├── to_schema.rs (500 tokens)
      ├── defaults.rs (2.8k tokens)
      ├── diagnostics/
         ├── mod.rs (400 tokens)
         ├── syntax.rs (1900 tokens)
         ├── workspace.rs (4.7k tokens)
      ├── generate/
         ├── from_entity.rs (1400 tokens)
         ├── from_field.rs (200 tokens)
         ├── from_schema.rs (1000 tokens)
         ├── from_value.rs (2.3k tokens)
         ├── generator_options.rs (200 tokens)
         ├── mod.rs (1200 tokens)
      ├── lib.rs (100 tokens)
      ├── parser/
         ├── dsl/
            ├── mod.rs (100 tokens)
            ├── parsed_entity.rs (400 tokens)
            ├── parsed_field.rs (400 tokens)
            ├── parsed_schema.rs (500 tokens)
            ├── parsed_schema_field.rs (700 tokens)
            ├── parsed_source.rs (1800 tokens)
            ├── parsed_value.rs (3.2k tokens)
            ├── parser_errors.rs (900 tokens)
            ├── parser_utils.rs (100 tokens)
            ├── source.rs (300 tokens)
         ├── mod.rs
         ├── query/
            ├── grammar.pest (700 tokens)
            ├── mod.rs
            ├── parsed_query.rs (900 tokens)
            ├── parser.rs (3.7k tokens)
      ├── workspace/
         ├── build.rs (700 tokens)
         ├── io.rs (400 tokens)
         ├── mod.rs (600 tokens)
         ├── workspace_errors.rs (400 tokens)
   ├── tests/
      ├── convert_entity_tests.rs (4.2k tokens)
      ├── convert_query_tests.rs (1300 tokens)
      ├── convert_schema_tests.rs (1100 tokens)
      ├── parser_entity_tests.rs (3.9k tokens)
      ├── parser_query_tests.rs (1900 tokens)
      ├── parser_schema_tests.rs (2.3k tokens)
      ├── parser_source_tests.rs (800 tokens)
      ├── workspace_tests.rs (3.5k tokens)
├── firm_lsp/
   ├── Cargo.toml (100 tokens)
   ├── src/
      ├── completion.rs (2.9k tokens)
      ├── lib.rs
      ├── server.rs (5.5k tokens)
├── firm_mcp/
   ├── Cargo.toml (200 tokens)
   ├── src/
      ├── lib.rs (100 tokens)
      ├── resources.rs (1100 tokens)
      ├── server.rs (4.6k tokens)
      ├── tools/
         ├── add_entity.rs (3k tokens)
         ├── build.rs (200 tokens)
         ├── delete_source.rs (600 tokens)
         ├── dsl_reference.rs (300 tokens)
         ├── dsl_reference_content.rs (1300 tokens)
         ├── find_source.rs (400 tokens)
         ├── get.rs (300 tokens)
         ├── list.rs (200 tokens)
         ├── mod.rs (300 tokens)
         ├── query.rs (500 tokens)
         ├── read_source.rs (200 tokens)
         ├── related.rs (500 tokens)
         ├── replace_source.rs (1200 tokens)
         ├── search_source.rs (600 tokens)
         ├── source_tree.rs (300 tokens)
         ├── write_source.rs (900 tokens)
   ├── tests/
      ├── add_entity_tests.rs (2.1k tokens)
      ├── delete_source_tests.rs (1000 tokens)
      ├── find_source_tests.rs (1300 tokens)
      ├── get_tests.rs (1200 tokens)
      ├── helpers.rs (300 tokens)
      ├── list_tests.rs (1100 tokens)
      ├── query_tests.rs (1200 tokens)
      ├── read_source_tests.rs (800 tokens)
      ├── related_tests.rs (1400 tokens)
      ├── replace_source_tests.rs (2.8k tokens)
      ├── search_source_tests.rs (1300 tokens)
      ├── source_tree_tests.rs (900 tokens)
      ├── write_source_tests.rs (1800 tokens)
├── install.ps1 (300 tokens)
├── install.sh (200 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/docs.yml

```yml path="/.github/workflows/docs.yml" 
name: Deploy documentation

on:
  push:
    branches:
      - main
    paths:
      - "docs/**"
      - ".github/workflows/docs.yml"
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true

      - name: Setup mdBook
        uses: peaceiris/actions-mdbook@v2
        with:
          mdbook-version: "0.4.52"

      - name: Build documentation
        run: |
          cd docs
          mdbook build

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: ./docs/book

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

```

## /.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
.claude
.zed

# mdBook build output
docs/book
AGENTS.md
CLAUDE.md

```

## /.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.5.0] - 2026-02-06

### Added

- **MCP Server**: New `firm mcp` command exposes workspace operations as a local Model Context Protocol server for AI assistant integration
  - Tools for querying, listing, getting, adding entities, and reading/writing source files
  - DSL reference tool for in-context syntax documentation
  - Validated workspace updates with automatic rollback on error
- **Query Aggregations**: Optional final clause for computing summary values from query results
  - `select` extracts specific field values: `from task | select @id, name, due_date`
  - `count` counts entities, optionally by field presence: `from task | count`
  - `sum`, `average`, `median` for numeric fields: `from invoice | sum amount`
  - Currency-aware sum with mixed-currency error reporting
- **Compound Query Conditions**: Combine multiple conditions with `and` or `or` in `where` clauses
  - Example: `from invoice | where status == "draft" or status == "sent"`
- **Source Command**: New `firm source` command to locate the source file for an entity or schema
- **Documentation Site**: mdbook-based documentation deployed to GitHub Pages, replacing long-form README

### Fixed

- Schema display now shows allowed enum values
- Invalid queries (unsupported operators, unknown entity types, invalid dates) now return descriptive errors instead of failing silently

### Changed

- `firm list` now outputs only entity IDs or schema names, rather than full details. Use `get` for details or `query` for filtered search.
- `Query::execute` returns `QueryResult` enum (entities or aggregation result) instead of `Vec<&Entity>`
- CLI access commands reorganized into individual files per command

## [0.4.0] - 2026-01-10

### Added

- **Query Engine**: SQL-like query language for filtering, traversing, sorting, and limiting entities
  - New `firm query` command with composable operations: `where`, `related`, `order`, `limit`
  - Supports all field types and operators (==, !=, >, <, >=, <=, contains, startswith, endswith, in)
  - Example: `firm query 'from project | where status == "in progress" | related(2) task | limit 10'`
- **Workspace Initialization**: New `firm init` command for setting up workspaces
  - Creates default schemas as editable DSL files in workspace
  - Generates .gitignore configuration for graph files
  - Optional starter entities for first-time setup
  - Creates AGENTS.md file for AI context
- **Enum Field Type**: New field type for constrained string values
  - Case-insensitive enum matching with interactive picker in CLI
  - Build-time validation with helpful error messages
  - Default schemas updated with sensible enum defaults
- **Non-Interactive Add**: Automation support for `firm add` command
  - Use `--type`, `--id`, and `--field` arguments to add entities programmatically
  - List support with `--list <name> <type>` and `--list-value <name> <value>`
  - Validation against schemas with helpful error messages
- **Field Ordering**: Field schemas now preserve insertion order
  - Explicit `order` attribute on field schemas
  - Entity fields retain DSL ordering when displayed
- Path autocomplete in CLI add command
- GitHub issue templates and pull request workflow

### Fixed

- Path fields now correctly resolve relative to source files
- Both interactive and non-interactive add modes now use pre-built graph

### Changed

- **BREAKING**: Built-in schemas removed from core package. Existing workspaces must run `firm init` to create schema files.
- **BREAKING**: Entity field access changed from direct field ID indexing to `get_field()` method or entity field index.
- **BREAKING**: Module reorganization - DSL parsing moved from `parser::` to `parser::dsl::` namespace in firm_lang crate.
- Entity fields now stored in vector instead of hash map
- Updated README with query language documentation

## [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_mcp", "firm_lsp", "firm_cli"]
resolver = "3"

```

## /README.md

# Firm: Business-as-code

A text-based work management system for technologists.

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

## Why Firm?

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.

Think of your business as a graph: organizations link to people, people link to projects, projects link to tasks, and so on. Firm lets you define these relationships in plain text files.

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

### Features

- **Everything in one place:** Organizations, contacts, projects, and their relationships.
- **Own your data:** Plain text files and tooling that works 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:** Bots can easily read, write, and query your business structure.

## Quick start

### Install Firm

**With Homebrew:**
```bash
brew tap 42futures/firm
brew install firm
```

**Or download from Github releases:**
https://github.com/42futures/firm/releases

### Initialize your workspace

```bash
cd my_workspace
firm init
```

### Add an entity

```bash
firm add --type organization --id megacorp --field name "Megacorp Ltd."
```

### Query your data

```bash
firm list organization
firm query 'from organization | where name contains "Megacorp"'
```

**[Read the full documentation to learn more.](https://firm.42futures.com)**

## Contributing

Contributions are welcome. Please feel free to submit an issue or pull request.

## License

Firm is licensed under AGPL-v3. That means you can use and extend Firm freely. If you build commercial extensions or services on top of Firm, those must also be open source under AGPL-v3.

See [LICENSE](LICENSE) for full details.


## /docs/book.toml

```toml path="/docs/book.toml" 
[book]
authors = ["Daniel Rothmann"]
language = "en"
src = "src"
title = "Firm"

[output.html]
default-theme = "coal"
preferred-dark-theme = "coal"
git-repository-url = "https://github.com/42futures/firm"
git-repository-icon = "fa-github"
additional-css = ["theme/custom.css"]
no-section-label = true

[output.html.playground]
editable = false

[output.html.print]
enable = false

[output.html.fold]
enable = true
level = 0

```

## /docs/src/SUMMARY.md

# Summary

[Introduction](./introduction.md)

# Getting started

- [Installation](./getting-started/installation.md)
- [Quick start](./getting-started/quick-start.md)
- [Your workspace](./getting-started/workspace.md)

# Guides

- [Creating schemas](./guide/creating-schemas.md)
- [Working with relationships](./guide/relationships.md)
- [Querying data](./guide/querying.md)
- [Automations and AI assistants](./guide/automations-and-ai.md)

# Concepts

- [Entities](./concepts/entities.md)
- [Fields](./concepts/fields.md)
- [Relationships](./concepts/relationships.md)
- [Schemas](./concepts/schemas.md)
- [Default entity types](./concepts/built-in-entities.md)

# Firm as a library

- [Architecture](./library/architecture.md)
- [Using the Rust crates](./library/getting-started.md)
- [Examples](./library/examples.md)

# Reference

- [CLI reference](./reference/cli-reference.md)
- [DSL reference](./reference/dsl-reference.md)
- [Query reference](./reference/query-reference.md)


## /docs/src/concepts/built-in-entities.md

# Default entity types

When you run `firm init`, you get a set of default schemas for common business entities. These schemas reflect a design philosophy focused on flexibility, composability, and real-world business modeling.

## Design philosophy

### REA model foundation

The default entity types are built on the [REA model (Resources, Events, Agents)](https://en.wikipedia.org/wiki/Resources,_Events,_Agents), a proven accounting and business modeling framework.

Every entity maps to one of these categories:
- **Resource** - Things with value (projects, documents)
- **Event** - Things that happen (interactions, transactions)
- **Agent** - Things that act (people, organizations)

### Fundamental vs. contextual entities

We separate objective reality from business relationships:

**Fundamental entities** represent things that exist independently:
- `person` - An individual human being
- `organization` - A company or group
- `file_asset` - A file or artifact

**Contextual entities** represent your business relationships and processes:
- `contact` - Your business relationship with a person
- `lead` - A sales opportunity
- `project` - A body of work

The same `person` can be a `contact` at one organization, an `employee` at yours, and a `partner` in a joint venture—all simultaneously.

### Composition over inheritance

Entities reference each other rather than extending each other. This provides more flexibility:

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

contact john_at_acme {
    person_ref = person.john_doe
    organization_ref = organization.acme_corp
    role = "CTO"
}

employee john_employee {
    person_ref = person.john_doe
    organization_ref = organization.my_company
    role = "Consultant"
}
```

One `person` entity, multiple relationship contexts.

## Included default types

### Core entities
- **person** - An individual
- **organization** - A company or group
- **industry** - A business sector or classification

### Customer relations
- **account** - Business relationship with an organization
- **channel** - Communication or marketing channel
- **lead** - Potential business lead
- **contact** - Person in a business relationship context
- **interaction** - Communication or meeting
- **opportunity** - Potential sale or business deal

### Work management
- **strategy** - High-level, long-term plan or goal
- **objective** - Specific, measurable goal contributing to a strategy
- **key_result** - Measurable outcome tracking an objective
- **project** - Planned initiative to achieve objectives
- **task** - Single, actionable unit of work
- **review** - Periodic review or meeting

### Resources
- **file_asset** - Digital file or document

## Customization

These default schemas are a starting point. You can:
- Modify them to fit your needs
- Add new custom entity types
- Remove types you don't use

See the [Creating schemas guide](../guide/creating-schemas.md) for details on customization.


## /docs/src/concepts/entities.md

# 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.

## Defining entities

**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,no_run
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");
```

## Entity structure

Every entity has:

- **Type**: What kind of entity this is (e.g., `person`, `organization`, `task`)
- **ID**: A unique identifier within its type (e.g., `john_doe`)
- **Fields**: Key-value pairs containing the entity's data

## Composite entity identifier

When Firm works with entities, it assigns each one a composite identifier of its type and ID. This means you can have several entities with the same ID, as long as they have different types.

For example, you could have:
- `person.hello` - A person named Hello
- `organization.hello` - An organization called Hello

When referencing entities, Firm uses the format `type.id`:

- `person.john_doe`
- `organization.megacorp`
- `task.design_homepage`


## /docs/src/concepts/fields.md

# Fields

Fields are typed key-value pairs attached to an entity. Firm supports a rich set of types to represent your business data.

## Field types

### String

Text values:

```firm
person john {
    name = "John Doe"
    bio = "Software engineer and entrepreneur"
}
```

For multiline strings, use triple quotes.

```firm
project website {
    description = """
        # Complete redesign
        Includes new homepage, about page, and contact form.
    """
}
```

Common indentation across the multiline string is removed when parsed.

### Integer

Numbers without a decimal place:

```firm
task design {
    priority = 1
    estimated_hours = 40
}
```

### Float

Numbers with a decimal place:

```firm
person john {
    height = 1.75
    weight = 70.5
}
```

### Boolean

True or false values:

```firm
task design {
    completed = false
    billable = true
}
```

### Currency

Monetary values with currency codes:

```firm
project website {
    budget = 5000.00 USD
    spent = 2500.00 USD
}
```

Firm supports ISO 4217 currency codes (USD, EUR, GBP, JPY, etc.).

### DateTime

Dates and times support three variants:

```firm
task design {
    # Date only (YYYY-MM-DD)
    start_date = 2025-01-15

    # Date and time (YYYY-MM-DD at HH:MM)
    due_date = 2025-01-15 at 17:00

    # Date and time with UTC offset (YYYY-MM-DD at HH:MM UTC+Z)
    created = 2025-01-15 at 17:00 UTC+3
}
```

**Timezone handling:**
- When you specify just a date (like `2025-01-15`), Firm assumes midnight (00:00) in your local timezone
- When you specify date and time without a timezone (like `2025-01-15 at 17:00`), Firm uses your local timezone
- When you specify a UTC offset (like `UTC+3` or `UTC-5`), Firm uses that timezone
- If you write `UTC` with no offset, it uses UTC+0
- Only `UTC` timezone offsets are supported (`EST`, `CET`, etc. are not)

### List

Collections of values. Lists are required to have homogeneous types (all items must be the same type):

```firm
person john {
    tags = ["developer", "manager", "consultant"]
    skills = ["rust", "python", "javascript"]
}
```

### Reference

Links to other entities:

```firm
task design {
    assignee_ref = person.jane_doe
    project_ref = project.website_redesign
}
```

References create relationships in the entity graph. See [Relationships](./relationships.md) for more details.

### Path

Local file paths:

```firm
project website {
    deliverable = path"./deliverables/website.zip"
    contract = path"/Users/john/Documents/contracts/megacorp_contract.pdf"
}
```

Paths are specified relative to the `.firm` source file. When parsed, they are transformed to be relative to the workspace root. Absolute paths are left unchanged.

### Enum

Predefined values:

```firm
task design {
    status = enum"in_progress"
    priority = enum"high"
}
```

Enums are useful when combined with [schemas](./schemas.md) that define allowed values.


## /docs/src/concepts/relationships.md

# Relationships

The power of Firm comes from connecting entities using reference fields.

## Making relationships

When you add a reference field to an entity, you create a relationship:

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

This creates two relationships:
- From `contact.john_at_acme` to `person.john_doe`
- From `contact.john_at_acme` to `organization.acme_corp`

## The entity graph

When Firm processes your workspace, it builds the **entity graph**: an in-memory data structure representing all your entities (as nodes) and their relationships (as directed edges).

The process:
1. Parse all `.firm` files in your workspace
2. Build entity objects with their fields
3. Read reference fields to identify relationships
4. Build directed edges between entities
5. Serialize and cache the graph for future queries

The entity graph is central to how Firm works. When you run queries, traverse relationships, or explore connections, you're working with this graph.

## Graph caching

When the graph is built, it's serialized and stored in your workspace. This way it can be used again for future queries if you don't wish to rebuild the graph on every interaction.

The `.gitignore` provided with `firm init` by default ignores the graph files.

When you rebuild the graph and it would overwrite the existing cached one, Firm backs it up. Your workspace will therefore usually have:
- `current.firm.graph` - The current graph
- `backup.firm.graph` - The previous graph

The contents of the serialized graph is JSON, so it can be used if you want to do more advanced queries and automations outside of the provided Rust crates.


## /docs/src/concepts/schemas.md

# Schemas

Schemas define the structure and validation rules for entity types in your workspace. They provide consistency and ensure data integrity across your business data.

## What schemas do

Schemas specify:
- Which fields are available for an entity type
- Which fields are required vs optional
- The expected data type for each field

## Validation and flexibility

Entities follow an open data model in the entity graph itself. Cchema validation occurs when you build your workspace (using `firm build` or any command that builds the workspace). At build time, Firm validates each entity against its schema.

The validation rules are:

- **Fields in the schema must match the defined types** - A field marked as `boolean` cannot contain a number
- **Required fields must be present** - If a field is marked `required = true`, the entity must have it
- **Entities can have fields not defined in their schema** - Schemas define minimum requirements, not maximum constraints

This gives you structure where you need it, while allowing flexibility for additional custom data.

## Example

```firm
schema task {
    field {
        name = "name"
        type = "string"
        required = true
    }

    field {
        name = "completed"
        type = "boolean"
        required = true
    }
}

task design_homepage {
    name = "Design new homepage"
    completed = false
    custom_priority = "high"  # Not in schema, but allowed
}
```

This entity is valid because:
- It has all required fields (`name` and `completed`)
- Those fields have the correct types
- The extra `custom_priority` field is allowed

## Default schemas

When you run `firm init`, you get default schemas for common entity types like `person`, `organization`, `task`, and `project`. See the [Quick start guide](../getting-started/quick-start.md) to learn more about initializing a workspace.


## /docs/src/getting-started/installation.md

# Installation

## Homebrew (recommended)

```bash
brew tap 42futures/firm
brew install firm
```

## Manual installation

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

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

3. **Install globally**
   ```bash
   chmod +x firm
   sudo mv firm /usr/local/bin/
   ```

## Verify installation

After installation, verify that Firm is working:

```bash
firm --version
```

You should see the version number printed to your terminal.


## /docs/src/getting-started/quick-start.md

# Quick start

## Initialize your workspace

The easiest way to start is by running `firm init` in an empty directory:

```bash
mkdir my_workspace
cd my_workspace
firm init
```

This interactive command will help you set up your workspace by:
- Creating default schemas for common entity types (Person, Organization, Task, etc.)
- Adding a `.gitignore` file for Firm's graph files
- Creating starter entities (you and your organization)
- Adding AI context documentation for AI coding assistants

Once initialized, your workspace is ready to use!

## Add your first entity

Use `firm add` to generate new entities. 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
```

You can also use `firm add` non-interactively by providing its type, ID and fields:

```bash
$ firm add --type organization --id megacorp --field name "Megacorp Ltd."
```

## View your entities

Use `firm list` to see all entities of a specific type:

```bash
$ firm list organization
```
```
Found 1 entities with type 'organization'

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

## Query your data

Once you have a few entities, you can search and filter them using Firm's query language:

```bash
$ firm query 'from organization | where name contains "Mega"'
```


## /docs/src/getting-started/workspace.md

# Your workspace

## What is a workspace?

Firm operates on a directory containing all your `.firm` DSL files. That's what we call your "workspace". Firm processes every file in this workspace to build a graph of your business.

You can interact with entities in your workspace either by using the Firm CLI or by writing Firm DSL yourself.

The CLI by default uses your current working directory as the root of the workspace. If you'd like to use a different workspace, you can specify it with `firm --workspace <path>`, where the path can be relative or absolute.

## Writing DSL

You can create `.firm` files and write the DSL yourself. These files are automatically included when they're in a Firm workspace.

DSL example (e.g., `organizations.firm`):

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

See the [DSL reference](../reference/dsl-reference.md) for more.

## Organizing your files

You can organize your `.firm` files however you like:
- Single file with all entities
- One file per entity type (e.g., `people.firm`, `organizations.firm`)
- Directory structure by project or client
- Any combination that makes sense for your business

Firm will discover and process all `.firm` files in your workspace directory recursively.

## Version control

Since your workspace is just plain text files, you can (and should!) put it in version control:

```bash
git init
git add .
git commit -m "Initial workspace"
```

This gives you:
- Auditable history of changes
- Collaboration with teammates
- Backup and recovery
- Branch-based workflows for planning




## /docs/src/guide/automations-and-ai.md

# Automations and AI assistants

Firm is designed to interoperate with automation tools and AI assistants. Because your data is stored as plain text files in a structured format, it's easy for both scripts and AI to read, write, and query.

## Why Firm works well with automation

- **Plain text**: Everything is in `.firm` files that any tool can read
- **Version controlled**: Changes are tracked in git, making automation safe
- **Queryable**: The CLI provides programmatic access to your data
- **Structured**: The DSL is unambiguous and parseable

## Using Firm in scripts

You can call Firm commands from bash scripts or other automation tools:

```bash
#!/bin/bash

# Create a new task
firm add --type task \
  --id "daily_standup_$(date +%Y%m%d)" \
  --field name "Daily standup" \
  --field due_date "$(date +%Y-%m-%d) at 09:00 UTC"

# Query incomplete tasks
firm query 'from task | where is_completed == false'
```

## AI context files

When you run `firm init`, it creates an `AGENTS.md` file that helps AI assistants understand your workspace. This file provides:

- CLI command reference and best practices
- Query language syntax and examples
- Common workflows for working with entities
- JSON output options for programmatic use

This gives AI coding assistants like Claude, GitHub Copilot, or Cursor the context they need to help you work with Firm data.

## Working with AI assistants

AI assistants can help you:

- **Generate entities**: "Create a new meeting for Jane at Acme Corp"
- **Query data**: "Show me all incomplete tasks assigned to John"
- **Build relationships**: "Link this task to the website project"
- **Analyze patterns**: "How is the sales pipeline looking?"
- **Write scripts**: "Create a script that generates a weekly report"

Because Firm files are plain text, LLMs can read your workspace and provide context-aware suggestions.

## MCP server

Firm includes a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that allows AI assistants to interact with your workspace through a standardized interface.

### Running the MCP server

Start the server with:

```bash
firm mcp
```

The server runs over stdio and exposes tools for querying, listing, and modifying entities. It's designed to run locally alongside your workspace.

Most MCP-compatible clients (like Claude Desktop or other AI assistants) can be configured to connect to local MCP servers.

### Remote deployment

The MCP server is built for local use, but you can deploy it to your own backend if you need remote access. How you handle authentication, security, and hosting is up to you.

For an example of how you could go about it, see the [Firm Remote MCP repository](https://github.com/42futures/firm-remote-mcp).

## Programmatic access

For more complex automation, you can use Firm as a Rust library. See the [Rust library guide](../library/getting-started.md) for details.


## /docs/src/guide/creating-schemas.md

# Creating schemas

Firm works on whatever schemas are in your workspace. If you've run `firm init`, you already have some default schemas with sensible defaults for common entity types like Person, Organization, Task, and Project.

You can customize these schemas any way you want, or create entirely new ones.

Schemas can be included in any `.firm` file and you can put them anywhere in your workspace. They will get discovered and included automatically.

## What schemas define

Each schema defines:
- Which fields are available for that entity type
- Which fields are required vs optional
- The data type of each field

This defines your data model.

## Example: Simple schema

Here's a basic schema for a task entity:

```firm
schema task {
    field {
        name = "name"
        type = "string"
        required = true
    }

    field {
        name = "description"
        type = "string"
        required = false
    }

    field {
        name = "completed"
        type = "boolean"
        required = true
    }
}
```

Now you can define entities that adhere to this schema:

```firm
task design_homepage {
    name = "Design new homepage"
    description = "Create mockups for the new homepage design"
    completed = false
}
```

## Available field types

When creating or customizing schemas, you can use these field types:

- **boolean** - True/false values
- **string** - Text values
- **integer** - Integer numbers
- **float** - Decimal numbers
- **currency** - Monetary values with currency codes
- **reference** - Links to other entities
- **list** - Lists of values
- **datetime** - Date and time values
- **path** - Local file paths
- **enum** - Enumerated values with allowed options

See the [Fields reference](../concepts/fields.md) for more details on how each field type works.

## Enum fields and allowed values

For enum fields in schemas, you must provide a set of allowed values. The enum type is intended to be a static set of options that constrains the field to only those specific values.

Here's an example of a schema with an enum field:

```firm
schema project {
    field {
        name = "name"
        type = "string"
        required = true
    }

    field {
        name = "status"
        type = "enum"
        allowed_values = ["planning", "active", "completed"]
        required = true
    }

    field {
        name = "budget"
        type = "float"
        required = false
    }
}
```


## /docs/src/guide/querying.md

# Querying data

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

## 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
```

## 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

...
```

## Custom queries

For deeper insights, use `firm query` which supports a SQL-like query language. This allows you to filter, traverse relationships, sort, and limit results in one expression.

### Query syntax

```
from <type> | <operation> | <operation> | ... | <aggregation>
```

### Available operations

- `from <type>` - Selects the initial entity set
- `where <field> <operator> <value>` - Filter entities by field values
- `related([degrees]) [<type>]` - Traverse relationships
- `order <field> [asc|desc]` - Sort results
- `limit <n>` - Limit the number of results

### Aggregations

An optional final clause that summarizes the result set:

- `select <field>, ...` - Extract specific field values
- `count [<field>]` - Count entities (optionally only those with the field)
- `sum <field>` - Sum a numeric field
- `average <field>` - Compute the mean of a numeric field
- `median <field>` - Compute the median of a numeric field

### Examples

**Find all incomplete tasks:**
```bash
$ firm query 'from task | where is_completed == false'
```

**Find tasks assigned to a specific person:**
```bash
$ firm query 'from task | where assignee_ref == person.john_doe'
```

**Find invoices that are draft or sent:**
```bash
$ firm query 'from invoice | where status == "draft" or status == "sent"'
```

**Find recent incomplete tasks related to active projects, sorted by due date:**
```bash
$ firm query 'from project | where status == "in progress" | related(2) task | where is_completed == false | where due_date > 2025-01-01 | order due_date | limit 10'
```

**Count incomplete tasks:**
```bash
$ firm query 'from task | where is_completed == false | count'
```

**Sum invoice amounts:**
```bash
$ firm query 'from invoice | where status == "sent" | sum amount'
```

**Extract specific fields:**
```bash
$ firm query 'from task | where is_completed == false | select @id, name, due_date'
```

### Query operators

You can filter by any field or metadata (`@type`, `@id`), traverse relationships multiple degrees deep, and compose operations to build the exact query you need.

**Comparison operators:**
- `==` - Equal
- `!=` - Not equal
- `>` - Greater than
- `<` - Less than
- `>=` - Greater than or equal
- `<=` - Less than or equal

For more details, see the [Query reference](../reference/query-reference.md).


## /docs/src/guide/relationships.md

# Working with relationships

The power of Firm comes from connecting entities together to build a graph of your business. You create relationships using reference fields.

## Types of references

Firm supports two kinds of references:

### Entity references

Entity references point to an entire entity using the format `type.id`. This is the primary way to connect entities in Firm:

```firm
task design_homepage {
    assignee_ref = person.john_doe
}
```

### Field references

Field references point to a specific field on an entity using the format `type.id.field`:

```firm
task design_homepage {
    assignee_name = person.john_doe.name
}
```

Currently, Firm primarily uses entity references.

## Creating a reference

A reference field links one entity to another. Here's a simple example:

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

task design_homepage {
    name = "Design new homepage"
    assignee_ref = person.john_doe
    completed = false
}
```

The `assignee_ref` field contains a reference to `person.john_doe`, creating a connection from the task to the person.

## Multiple references

An entity can reference multiple other entities:

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

This creates two relationships:
- From `contact.john_at_acme` to `person.john_doe`
- From `contact.john_at_acme` to `organization.acme_corp`

## Building a connected graph

By connecting entities with references, you build a graph that represents your business relationships:

```firm
organization acme_corp {
    name = "Acme Corp"
}

person jane_smith {
    name = "Jane Smith"
}

contact jane_at_acme {
    person_ref = person.jane_smith
    organization_ref = organization.acme_corp
    role = "CEO"
}

project website_redesign {
    name = "Website Redesign"
    organization_ref = organization.acme_corp
}

task design_mockups {
    name = "Design mockups"
    project_ref = project.website_redesign
    assignee_ref = person.jane_smith
}
```

Now you can explore these relationships:
- Find all projects for Acme Corp
- Find all tasks assigned to Jane
- Find all contacts at Acme Corp
- Find all tasks for projects at Acme Corp

## Querying relationships

Use `firm related` to explore connections:

```bash
firm related organization acme_corp
```

This shows all entities connected to the organization.

For more complex queries, use `firm query`:

```bash
# Find all tasks for projects at Acme Corp
firm query 'from organization | where name contains "Acme" | related project | related task'
```

See the [Querying data](./querying.md) guide for more examples.


## /docs/src/introduction.md

# Introduction

**Firm** is a text-based work management system for technologists. It provides a unified view of your business by representing everything as plain text files you own and control.

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

## Why Firm?

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.

Think of your business as a graph: organizations link to people, people link to projects, projects link to tasks, and so on. Firm lets you define these relationships in plain text files.

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

## Features

- **Everything in one place:** Organizations, contacts, projects, and their relationships.
- **Own your data:** Plain text files and tooling that works 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:** Bots can easily read, write, and query your business structure.

## Who is it for?

I built Firm to help me run [42futures](https://42futures.com): a solo venture specializing in software pilots.

The tool is suitable for:
- **Freelancers** managing multiple clients and projects
- **Technical teams** who prefer plain text and version control over SaaS dashboards
- **Anyone** who wants to own their data and tools, not rent them from someone else




## /docs/src/library/architecture.md

# Architecture

Firm is organized as a Rust workspace with three crates, each with a specific responsibility.

## Crate overview

```
firm/
├── firm_core/     - Core data structures and graph operations
├── firm_lang/     - DSL parsing and generation
└── firm_cli/      - Command-line interface
```

## firm_core

Core data structures and graph operations.

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

**Key types:**
- `Entity` - A business object with ID, type, and fields
- `EntityGraph` - Graph of entities with relationship edges
- `FieldValue` - Typed field values (String, Integer, Reference, etc.)
- `EntitySchema` - Schema definitions for validation

**Usage:**
```rust,no_run
use firm_core::{Entity, EntityGraph, EntityId, FieldId, FieldValue};

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

let mut graph = EntityGraph::new();
graph.add_entity(entity)?;
graph.build();
```

## firm_lang

DSL parsing and generation.

**Responsibilities:**
- Tree-sitter-based DSL parser for `.firm` files
- Pest-based query language parser for the CLI
- Converting and generating DSL
- Workspace support for multi-file projects

**Key types:**
- `Workspace` - Multi-file workspace manager
- `parser::dsl` - DSL parsing
- `parser::query` - Query parsing
- `generate` - DSL generation

**Usage:**
```rust,no_run
use firm_lang::workspace::Workspace;

let mut workspace = Workspace::new();
workspace.load_directory("./my_workspace")?;
let build = workspace.build()?;
```

## firm_cli

Command-line interface, making the Firm workspace interactive.

**Responsibilities:**
- Interactive and non-interactive commands
- User-friendly output formatting
- Integration of core and lang crates

**Commands:**
- `firm init` - Initialize a workspace
- `firm add` - Add entities
- `firm list` - List entities by type
- `firm get` - Get a specific entity
- `firm related` - Find related entities
- `firm query` - Run custom queries
- `firm source` - Get the source file path for an entity

**Usage:**
```bash
firm init
firm add --type person --id john
firm query 'from person | where name == "John"'
```


## /docs/src/library/examples.md

# Examples

Here are some practical examples of what you might build using the Firm Rust crates.

## Custom reporting tool

Build a tool that generates weekly reports from your workspace:

```rust,no_run
use firm_lang::workspace::Workspace;
use firm_core::{
    EntityGraph, EntityType, FieldId,
    Query, EntitySelector, QueryOperation,
    FilterCondition, FilterOperator, FilterValue, FieldRef
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load workspace
    let mut workspace = Workspace::new();
    workspace.load_directory("./my_workspace")?;
    let build = workspace.build()?;

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

    // Find completed tasks
    let query = Query::new(EntitySelector::Type(EntityType::new("task")))
        .with_operation(QueryOperation::Where(
            FilterCondition::new(
                FieldRef::Regular(FieldId::new("is_completed")),
                FilterOperator::Equal,
                FilterValue::Boolean(true),
            )
        ));

    let completed_tasks = query.execute(&graph);

    // Generate report
    println!("Weekly Report");
    println!("Completed {} tasks", completed_tasks.len());

    Ok(())
}
```

## Custom validation

Add business-specific validation rules:

```rust,no_run
use firm_core::{EntityGraph, EntityType, FieldId};

fn validate_business_rules(graph: &EntityGraph) -> Result<(), String> {
    // Ensure every project has an owner
    let projects = graph.list_by_type(&EntityType::new("project"));

    for project in projects {
        if project.get_field(&FieldId::new("owner_ref")).is_none() {
            return Err(format!("Project {} missing owner", project.id));
        }
    }

    // Ensure all opportunities have a value
    let opportunities = graph.list_by_type(&EntityType::new("opportunity"));

    for opp in opportunities {
        if opp.get_field(&FieldId::new("value")).is_none() {
            return Err(format!("Opportunity {} missing value", opp.id));
        }
    }

    Ok(())
}
```

## Data synchronization

Sync data between Firm and external systems:

```rust,no_run
use firm_lang::workspace::Workspace;
use firm_core::{Entity, EntityType, FieldId, FieldValue};

fn sync_from_crm() -> Result<(), Box<dyn std::error::Error>> {
    // Fetch from external CRM
    let crm_contacts = fetch_from_crm()?;

    // Load existing workspace
    let mut workspace = Workspace::new();
    workspace.load_directory("./workspace")?;

    // Create or update contacts
    for crm_contact in crm_contacts {
        let entity = Entity::new(
            EntityId::new(&crm_contact.id),
            EntityType::new("contact")
        )
        .with_field(FieldId::new("name"), FieldValue::String(crm_contact.name))
        .with_field(FieldId::new("email"), FieldValue::String(crm_contact.email));

        // Generate DSL and write to file
        let dsl = generate_dsl(&entity)?;
        std::fs::write(
            format!("./workspace/contacts/{}.firm", crm_contact.id),
            dsl
        )?;
    }

    Ok(())
}
```

## Automated task creation

Automatically generate tasks based on events:

```rust,no_run
use firm_core::{Entity, EntityGraph, EntityType, EntityId, FieldId, FieldValue, ReferenceValue};
use firm_lang::generator::generate_dsl;

fn create_followup_tasks(graph: &EntityGraph) -> Result<(), Box<dyn std::error::Error>> {
    // Find interactions from this week without follow-up tasks
    let interactions = graph.list_by_type(&EntityType::new("interaction"));

    for interaction in interactions {
        // Check if follow-up task exists
        let related = graph.get_related(&interaction.id, None);
        let has_followup = related
            .map(|entities| {
                entities.iter().any(|e| e.entity_type == EntityType::new("task"))
            })
            .unwrap_or(false);

        if !has_followup {
            // Create follow-up task
            let task = Entity::new(
                EntityId::new(&format!("followup_{}", interaction.id.as_str())),
                EntityType::new("task")
            )
            .with_field(
                FieldId::new("name"),
                FieldValue::String(format!("Follow up on {}", interaction.id.as_str()))
            )
            .with_field(
                FieldId::new("source_ref"),
                FieldValue::Reference(ReferenceValue::Entity(interaction.id.clone()))
            );

            // Write to file
            let dsl = generate_dsl(&task)?;
            std::fs::write(
                format!("./workspace/tasks/followup_{}.firm", interaction.id.as_str()),
                dsl
            )?;
        }
    }

    Ok(())
}
```


## /docs/src/library/getting-started.md

# Using the RustRust crates

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 automations and tools on top of Firm.

## Adding dependencies

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" }
```

## Basic usage

Here's an example of loading a workspace and querying the entity graph:

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 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"))?;
    println!("Found lead: {:?}", lead);

    Ok(())
}
```

## Working with references

You can traverse relationships and access field values:

```rust,no_run
use firm_core::{EntityId, FieldId};

// Get an entity
let lead = graph.get_entity(&EntityId::new("lead.ai_validation_project"))?;

// Get a field value
let contact_ref = lead.get_field(FieldId::new("contact_ref"))?;

// Resolve the reference to another entity
let contact = contact_ref.resolve_entity_reference(&graph)?;

println!("Contact: {:?}", contact);
```

## Creating entities programmatically

You can create entities in code:

```rust,no_run
use firm_core::{Entity, EntityId, EntityType, FieldId, FieldValue};

let person = Entity::new(
    EntityId::new("john_doe"),
    EntityType::new("person")
)
.with_field(FieldId::new("name"), FieldValue::String("John Doe".to_string()))
.with_field(FieldId::new("email"), FieldValue::String("john@example.com".to_string()));

// Add to the graph
graph.add_entity(person)?;
```

## Generating DSL

You can also generate DSL from entities:

```rust,no_run
use firm_lang::generate::generate_dsl;

let dsl = generate_dsl(&entity)?;
println!("{}", dsl);
```

This outputs:

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


## /docs/src/media/demo.gif

Binary file available at https://raw.githubusercontent.com/42futures/firm/refs/heads/main/docs/src/media/demo.gif

## /docs/src/media/demo.tape

```tape path="/docs/src/media/demo.tape" 
Output ../demo.gif
Set FontSize 14
Set Width 1200
Set Height 700
Set WindowBar Colorful
Set Margin 20
Set MarginFill "#c566ff"
Set BorderRadius 10
Set LineHeight 1.3

Type "tree"
Sleep 0.5s
Enter
Sleep 2.5s

Type "cat contacts/alice.firm"
Sleep 0.5s
Enter
Sleep 3s

Type "firm related contact alice_acme"
Sleep 0.5s
Enter
Sleep 3s

Type `firm add --type task --id review --field name 'Review' --field project_ref 'project.website'`
Sleep 0.5s
Enter
Sleep 2.5s

Type `firm query 'from project | where status == "active" | related task'`
Sleep 0.5s
Enter
Sleep 4s

```

## /docs/src/media/demo/contacts/alice.firm

```firm path="/docs/src/media/demo/contacts/alice.firm" 
person alice {
    name = "Alice Chen"
    company = "Acme Corp"
}

contact alice_acme {
    person_ref = person.alice
    role = "CTO"
}

```

## /docs/src/media/demo/contacts/bob.firm

```firm path="/docs/src/media/demo/contacts/bob.firm" 
person bob {
    name = "Bob Martinez"
    company = "TechCo"
}

contact bob_techco {
    person_ref = person.bob
    role = "VP Engineering"
}

```

## /docs/src/media/demo/me.firm

```firm path="/docs/src/media/demo/me.firm" 
person me {
    name = "Sam Developer"
    email = "sam@example.com"
}

```

## /docs/src/media/demo/projects/platform.firm

```firm path="/docs/src/media/demo/projects/platform.firm" 
project platform {
    name = "Platform Build"
    contact_ref = contact.bob_techco
    status = "active"
}

```

## /docs/src/media/demo/projects/website.firm

```firm path="/docs/src/media/demo/projects/website.firm" 
project website {
    name = "Website Redesign"
    contact_ref = contact.alice_acme
    status = "active"
}

```

## /docs/src/media/demo/schemas.firm

```firm path="/docs/src/media/demo/schemas.firm" 
schema person {
    field {
        name = "name"
        type = "string"
        required = true
    }
}

schema contact {
    field {
        name = "person_ref"
        type = "reference"
        required = true
    }
    field {
        name = "role"
        type = "string"
        required = false
    }
}

schema project {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "contact_ref"
        type = "reference"
        required = false
    }
    field {
        name = "status"
        type = "string"
        required = false
    }
}

schema task {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "project_ref"
        type = "reference"
        required = false
    }
    field {
        name = "status"
        type = "string"
        required = false
    }
}

```

## /docs/src/media/demo/tasks.firm

```firm path="/docs/src/media/demo/tasks.firm" 
task wireframes {
    name = "Create wireframes"
    project_ref = project.website
    status = "open"
}

task content {
    name = "Content audit"
    project_ref = project.website
    status = "done"
}

task api_docs {
    name = "Write API docs"
    project_ref = project.platform
    status = "open"
}

task setup_ci {
    name = "Setup CI pipeline"
    project_ref = project.platform
    status = "open"
}

```

## /docs/src/reference/cli-reference.md

# CLI reference

Complete reference for the Firm command-line interface.

## Global options

These options apply to all commands.

Each global option can be set via a command-line flag, an environment variable, or left to its default. They are evaluated in the following order (highest priority first):

1. **Command-line flag** — always takes precedence
2. **Environment variable** — used when no flag is provided
3. **Default value** — used when neither flag nor environment variable is set

### --workspace (-w)

Specify the workspace directory:

```bash
firm --workspace ./my_workspace list task
firm -w /absolute/path/to/workspace get person john_doe
```

Default: Current working directory

Environment variable: `FIRM_WORKSPACE`

### --cached (-c)

Use the cached entity graph instead of rebuilding:

```bash
firm --cached list task
firm -c query 'from task | where is_completed == false'
```

Default: false (graph is rebuilt before each command)

Environment variable: `FIRM_CACHED`

### --verbose (-v)

Enable verbose logging output:

```bash
firm --verbose build
firm -v list task
```

Environment variable: `FIRM_VERBOSE`

### --format (-f)

Specify output format:

```bash
firm --format json list task
firm -f pretty get person john_doe
```

Options:
- `pretty` (default) - Human-readable formatted output
- `json` - JSON output for programmatic use

Environment variable: `FIRM_FORMAT`

## Commands

### init

Initialize a new Firm workspace with default schemas and files.

```bash
firm init
```

This interactively gives you the options create:
- Default entity type schemas (person, organization, task, etc.)
- `.gitignore` file for graph files
- Starter entities (you and your organization)
- `AGENTS.md` file with AI assistant context

### build

Build the workspace and entity graph.

```bash
firm build
```

This:
- Parses all `.firm` files in the workspace
- Validates entities against their schemas
- Builds the entity graph with relationships
- Saves the graph to `current.firm.graph`

**Note:** Most commands automatically build the graph unless `--cached` is used.

### get

Get details of a specific entity or schema.

```bash
firm get <target_type> <target_id>
```

**Arguments:**
- `target_type` - Entity type (e.g., `person`, `organization`, `task`) or `schema`
- `target_id` - Entity ID (e.g., `john_doe`) or schema name (e.g., `project`)

**Examples:**

```bash
# Get an entity
firm get person john_doe
firm get organization acme_corp
firm get task design_homepage

# Get a schema
firm get schema project
firm get schema person
```

### list

List all entities of a specific type, or list all schemas.

```bash
firm list <target_type>
```

**Arguments:**
- `target_type` - Entity type (e.g., `person`, `organization`) or `schema` to list all schemas

**Examples:**

```bash
# List all tasks
firm list task

# List all people
firm list person

# List all available schemas
firm list schema
```

### related

Get entities related to a specific entity.

```bash
firm related <entity_type> <entity_id> [--direction <dir>]
```

**Arguments:**
- `entity_type` - The type of entity
- `entity_id` - The ID of the entity

**Options:**
- `--direction` or `-d` - Filter by relationship direction
  - `to` - Only incoming relationships (entities referencing this one)
  - `from` - Only outgoing relationships (entities this one references)
  - No direction specified - Both incoming and outgoing

**Examples:**

```bash
# All related entities (both directions)
firm related organization acme_corp

# Only entities that reference this organization
firm related organization acme_corp --direction to

# Only entities this person references
firm related person john_doe --direction from
firm related person john_doe -d from
```

### add

Add a new entity to the workspace.

**Interactive mode** (prompts for input):

```bash
firm add
firm add path/to/file.firm
```

**Non-interactive mode** (all details provided):

```bash
firm add [to_file] --type <type> --id <id> [--field <name> <value>]...
```

**Options:**
- `to_file` - Optional path to the `.firm` file to write to
- `--type` - Entity type (required for non-interactive mode)
- `--id` - Entity ID (required for non-interactive mode)
- `--field <name> <value>` - Add a field (repeatable)
- `--list <name> <item_type>` - Declare a list field (repeatable)
- `--list-value <name> <value>` - Add an item to a list field (repeatable)

**Examples:**

```bash
# Interactive mode
firm add

# Non-interactive with fields
firm add --type person --id jane_smith \
  --field name "Jane Smith" \
  --field email "jane@example.com"

# Write to specific file
firm add people.firm --type person --id bob_jones \
  --field name "Bob Jones"

# With list fields
firm add --type person --id alice_wong \
  --field name "Alice Wong" \
  --list skills string \
  --list-value skills "rust" \
  --list-value skills "python"
```

### query

Query entities using the Firm query language.

```bash
firm query '<query_string>'
```

**Arguments:**
- `query_string` - A query in the Firm query language

**Examples:**

```bash
# Find incomplete tasks
firm query 'from task | where is_completed == false'

# Find high-value opportunities
firm query 'from opportunity | where value >= 10000.00 USD'

# Find tasks for active projects
firm query 'from project | where status == "active" | related task'

# Complex multi-hop query
firm query 'from organization | where industry == "tech" | related(2) task | where is_completed == false | limit 10'

# Sort and limit
firm query 'from task | order due_date desc | limit 5'
```

See the [Query reference](./query-reference.md) for complete query language documentation.

### source

Find the source file path where an entity or schema is defined.

```bash
firm source <target_type> <target_id>
```

**Arguments:**
- `target_type` - Entity type (e.g., `person`, `organization`) or `schema`
- `target_id` - Entity ID or schema name

**Examples:**

```bash
# Find where a person entity is defined
firm source person john_doe

# Find where an organization is defined
firm source organization acme_corp

# Find where a schema is defined
firm source schema project

# Output as JSON
firm --format json source person john_doe
```

**Output:**
Returns the absolute path to the `.firm` file containing the definition. This is useful for locating and editing entity or schema definitions.

### mcp

Start an MCP (Model Context Protocol) server for the workspace.

```bash
firm mcp
```

This starts an MCP server over stdio that exposes your Firm workspace to AI assistants and other MCP-compatible clients. The server provides tools for querying, listing, and modifying entities programmatically.

**Available tools:**
- `list` - List entities by type or list all schemas
- `get` - Get details of a specific entity or schema
- `query` - Query entities using the Firm query language
- `related` - Find entities related to a given entity
- `find_source` - Find the source file for an entity or schema
- `read_source` - Read the contents of a `.firm` file
- `write_source` - Write content to a `.firm` file
- `replace_source` - Replace a string in a `.firm` file
- `add_entity` - Create a new entity from structured JSON
- `build` - Rebuild and validate the workspace
- `dsl_reference` - Get DSL syntax documentation

**Examples:**

```bash
# Start the MCP server (runs until terminated)
firm mcp

# Start for a specific workspace
firm --workspace ./my_workspace mcp
```

See [Automations and AI assistants](../guide/automations-and-ai.md) for details on configuring MCP clients.

## Exit codes

- `0` - Success
- `1` - Failure (error details printed to stderr)

## Examples

### Initialize and explore a workspace

```bash
# Create a new workspace
mkdir my_workspace && cd my_workspace
firm init

# List all schemas
firm list schema

# List all people
firm list person

# Get details of a person
firm get person me
```

### Add entities

```bash
# Add interactively
firm add

# Add non-interactively
firm add --type organization --id acme \
  --field name "Acme Corp" \
  --field email "contact@acme.com"

firm add --type contact --id john_at_acme \
  --field person_ref "person.john_doe" \
  --field organization_ref "organization.acme"
```

### Query and explore

```bash
# Find all incomplete tasks
firm query 'from task | where is_completed == false'

# Find organizations and their contacts
firm query 'from organization | related contact'

# Output as JSON for scripting
firm --format json query 'from task | limit 10' | jq '.[].id'
```


## /docs/src/reference/dsl-reference.md

# DSL reference

The Firm DSL (Domain-Specific Language) is used to define entities and schemas in plain text `.firm` files.

## Design philosophy

The Firm language is inspired by HashiCorp Configuration Language (HCL) but simplified for business entity modeling. Firm keeps HCL's clean block syntax and nested blocks while reducing complexity with a more restricted grammar and focused type system.

The syntax is intentionally simple, making it:
- Easy to write by hand
- Straightforward to parse programmatically
- Simple to generate from tooling

This makes it suited for both human authoring and machine generation in business workflows.

The grammar is defined in the [tree-sitter-firm](https://github.com/42futures/tree-sitter-firm) repository, which is a submodule of this project. Using Tree-sitter also enables editor integrations like syntax highlighting and code navigation in editors such as Zed.

## Blocks

Blocks are the fundamental structural elements in Firm DSL, enclosed in curly braces `{ }`.

### Entity blocks

Define an entity with a type and ID:

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

Syntax: `<entity_type> <entity_id> { <fields> }`

### Schema blocks

Define a schema for an entity type:

```firm
schema task {
    field {
        name = "name"
        type = "string"
        required = true
    }

    field {
        name = "is_completed"
        type = "boolean"
        required = false
    }
}
```

Syntax: `schema <schema_name> { <field_definitions> }`

### Nested blocks

Schemas use nested blocks for field definitions:

```firm
schema project {
    field {
        name = "status"
        type = "enum"
        allowed_values = ["planning", "active", "completed"]
        required = true
    }
}
```

## Fields

Fields are key-value pairs defined with the assignment operator `=`.

Syntax: `<field_name> = <value>`

## Field types

### String

Single-line strings:

```firm
name = "John Doe"
```

Multiline strings with triple quotes:

```firm
description = """
This is a multiline string.
It can span multiple lines.
"""
```

### Number

Integers and floats:

```firm
age = 30
height = 1.75
```

### Boolean

True or false values:

```firm
is_completed = true
is_active = false
```

### Currency

Monetary values with ISO 4217 currency codes:

```firm
budget = 5000.00 USD
cost = 299.99 EUR
```

Syntax: `<amount> <CURRENCY_CODE>`

### Date

ISO 8601 date format:

```firm
start_date = 2025-01-15
```

Syntax: `YYYY-MM-DD`

### DateTime

Date with time and optional timezone:

```firm
due_date = 2025-01-15 at 17:00
created = 2025-01-15 at 17:00 UTC+3
meeting = 2025-01-15 at 09:00 UTC
```

Syntax: `YYYY-MM-DD at HH:MM [UTC[+/-]Z]`

### Reference

Entity references:

```firm
assignee_ref = person.john_doe
```

Field references:

```firm
assignee_name = person.john_doe.name
```

Syntax: `<type>.<id>` or `<type>.<id>.<field>`

### List

Homogeneous lists (all items must be the same type):

```firm
tags = ["urgent", "frontend", "bug"]
urls = ["https://example.com", "https://github.com"]
```

Trailing commas are allowed:

```firm
tags = [
    "urgent",
    "frontend",
    "bug",
]
```

### Path

File path literals:

```firm
contract = path"./contracts/acme.pdf"
deliverable = path"/Users/john/Documents/report.pdf"
```

Syntax: `path"<path>"`

### Enum

Enumerated values:

```firm
status = enum"active"
priority = enum"high"
```

Syntax: `enum"<value>"`

## Comments

Single-line comments:

```firm
// This is a single-line comment
person john_doe {
    name = "John Doe" // Inline comment
}
```

Multi-line comments:

```firm
/*
This is a multi-line comment.
It can span multiple lines.
*/
person john_doe {
    name = "John Doe"
}
```

## Identifiers

Identifiers (entity types, entity IDs, field names, schema names) must:
- Start with a letter or underscore
- Contain only letters, numbers, and underscores
- Use snake_case convention

Valid identifiers: `person`, `john_doe`, `my_organization`, `_private`

Invalid identifiers: `123abc`, `my-entity`, `my.field`

## Why not YAML or JSON?

The Firm language is optimized for readability and compactness while retaining rich typing information:

- **More scannable than YAML** - Block syntax makes entity boundaries clear at a glance
- **Less verbose than JSON** - No need for extensive quoting and bracket nesting
- **Native support for business concepts** - Built-in support for currency, dates, and references
- **Schema definitions** - First-class support for defining custom entity types

The result is a format that's both human-friendly for manual editing and machine-friendly for programmatic generation.


## /docs/src/reference/query-reference.md

# Query reference

The Firm query language provides a SQL-like, compact syntax for querying your entity graph from the command line.

## Design philosophy

The query language is inspired by Microsoft's Kusto Query Language (KQL), using pipe operators to chain operations together. It's designed to be:

- **SQL-like and familiar** - Easy to learn if you know SQL
- **Compact for CLI use** - Minimal syntax for quick queries
- **Composable** - Build complex queries by piping operations together

The query grammar is defined using [Pest](https://pest.rs/) and can be found in `firm_lang/src/parser/query/grammar.pest`.

### Bag of entities model

Firm queries always operate on a "bag of entities". At every stage in query execution, you're processing complete, unmodified entities - we only read, filter, and traverse them, but never modify them or extract individual fields.

The `from` clause selects the initial set of entities, and every subsequent operation filters, expands, limits, or orders that entity set. This keeps the query language simple and focused on navigating the entity graph.

Optionally, a final **aggregation** clause can be added at the end of a query to compute a summary value (like a count or sum) or extract specific fields from the final entity set. Aggregations are the only operation that transforms the result from entities into a different shape.

## Basic syntax

All queries follow this structure:

```
from <entity_selector> | <operation> | <operation> | ... | <aggregation>
```

Start with a `from` clause, then chain operations using the pipe symbol `|`. Optionally end with an aggregation clause.

## Entity selector

Select which entities to start with:

```bash
# Select entities of a specific type
from task

# Select all entities (wildcard)
from *
```

## Operations

### where

Filter entities by field values or metadata:

```bash
# Filter by field value
from task | where is_completed == false

# Filter by metadata
from * | where @type == "task"
```

**Compound conditions:**

Combine multiple conditions in a single `where` clause using `and` or `or`:

```bash
# Match any of multiple values (OR)
from invoice | where status == "draft" or status == "sent"

# Require all conditions (AND)
from task | where is_completed == false and priority > 5

# Multiple OR conditions
from opportunity | where status == enum"open" or status == enum"negotiation" or status == enum"proposal"
```

You cannot mix `and` and `or` in the same `where` clause. Use separate `where` clauses to combine them:

```bash
# (status is draft OR sent) AND (amount > 1000)
from invoice | where status == "draft" or status == "sent" | where amount > 1000
```

**Chaining where clauses:**

Multiple `where` clauses joined by pipes act as implicit AND:

```bash
# These are equivalent:
from task | where is_completed == false | where priority > 5
from task | where is_completed == false and priority > 5
```

**Supported operators:**

- `==` - Equal to
- `!=` - Not equal to
- `>` - Greater than
- `<` - Less than
- `>=` - Greater than or equal to
- `<=` - Less than or equal to
- `contains` - String/list contains value
- `startswith` - String starts with value
- `endswith` - String ends with value
- `in` - Value in list

**Field references:**

- Regular fields: `field_name`
- Metadata fields: `@type`, `@id`

**Value types:**

```bash
# String (quoted)
where name == "John Doe"
where status == 'active'

# Number
where age > 30
where price <= 99.99

# Boolean
where is_completed == true
where active != false

# Currency
where budget >= 5000.00 USD

# Date/DateTime
where due_date > 2025-01-15
where created_at >= 2025-01-15 at 09:00 UTC

# Reference
where assignee_ref == person.john_doe

# Enum
where status == enum"active"

# Path
where file == path"./contracts/acme.pdf"

# List
where tags contains "urgent"
```

### related

Traverse relationships to find connected entities:

```bash
# Find all related entities (1 degree)
from organization | related

# Find related entities of a specific type
from organization | related task

# Traverse multiple degrees of separation
from organization | related(2)

# Combine degrees and type filter
from organization | related(2) task
```

**Syntax:**
- `related` - All related entities (1 degree)
- `related <type>` - Related entities of a specific type (1 degree)
- `related(<n>)` - All related entities (n degrees)
- `related(<n>) <type>` - Related entities of a specific type (n degrees)

### order

Sort results by a field:

```bash
# Sort ascending (default)
from task | order due_date

# Sort descending
from task | order due_date desc

# Sort ascending (explicit)
from task | order priority asc

# Sort by metadata
from * | order @type
```

**Syntax:**
- `order <field>` - Sort ascending
- `order <field> asc` - Sort ascending (explicit)
- `order <field> desc` - Sort descending

### limit

Limit the number of results:

```bash
# Get first 10 results
from task | limit 10

# Get top 5 high-priority tasks
from task | where priority > 8 | order priority desc | limit 5
```

**Syntax:** `limit <number>`

## Aggregations

Aggregations are optional clauses that go at the end of a query. They transform the entity set into a summary value or extracted fields. Only one aggregation can be used per query.

### select

Extract specific field values from entities:

```bash
# Select a single field
from person | select name

# Select multiple fields
from task | select name, status, due_date

# Include metadata fields
from task | where is_completed == false | select @id, name, due_date
```

**Syntax:** `select <field>, <field>, ...`

Fields can be regular field names or metadata fields (`@id`, `@type`). Missing fields appear as empty values.

### count

Count entities, optionally filtering by field presence:

```bash
# Count all matching entities
from task | where is_completed == false | count

# Count entities that have a specific field
from person | count email
```

**Syntax:**
- `count` - Count all entities in the result set
- `count <field>` - Count entities that have the specified field

### sum

Sum numeric field values across entities:

```bash
# Sum integer or float fields
from line_item | sum quantity

# Sum currency fields
from invoice | where status == "sent" | sum amount
```

**Syntax:** `sum <field>`

Works with integer, float, and currency fields. Entities missing the field are skipped. Currency values must all share the same currency code — mixed currencies produce an error.

### average

Compute the mean of a numeric field:

```bash
from task | average estimated_hours
```

**Syntax:** `average <field>`

Works with integer, float, and currency fields. Entities missing the field are skipped. Returns an error if no entities have the field.

### median

Compute the median of a numeric field:

```bash
from task | median estimated_hours
```

**Syntax:** `median <field>`

Works with integer, float, and currency fields. Entities missing the field are skipped. For an even number of values, returns the average of the two middle values. Returns an error if no entities have the field.

## Examples

### Find incomplete tasks

```bash
from task | where is_completed == false
```

### Find tasks assigned to a person

```bash
from task | where assignee_ref == person.john_doe
```

### Find high-value opportunities

```bash
from opportunity | where value >= 10000.00 USD | order value desc
```

### Find tasks for active projects

```bash
from project | where status == "active" | related task
```

### Count incomplete tasks

```bash
from task | where is_completed == false | count
```

### Total invoice amount

```bash
from invoice | where status == "sent" | sum amount
```

### Task summary with select

```bash
from task | where is_completed == false | order due_date | select @id, name, due_date
```

### Complex multi-hop query

```bash
from organization | where industry == "tech" | related(2) task | where is_completed == false | order due_date | limit 10
```

This query:
1. Starts with tech organizations
2. Finds entities within 2 degrees of separation that are tasks
3. Filters to incomplete tasks
4. Orders by due date
5. Limits to 10 results

## Query execution

Queries are executed left to right, with each operation transforming the result set:

```
from task                            → [all tasks]
| where is_completed == false        → [filtered tasks]
| related project                    → [related projects]
| order name                         → [sorted projects]
| limit 5                            → [top 5 projects]
```

Each operation receives the output of the previous operation and produces a new result set.

If an aggregation is present, it runs last and transforms the entity set into a result value:

```
from invoice                         → [all invoices]
| where status == "sent"             → [filtered invoices]
| sum amount                         → 15000.00 USD
```


## /docs/theme/custom.css

```css path="/docs/theme/custom.css" 
/* 42futures.com inspired styling for Firm documentation */

:root {
    /* Color scheme matching 42futures.com */
    --bg-primary: #161618;
    --bg-secondary: #1f1f22;
    --accent-purple: #c566ff;
    --accent-green: #02a82b;
    --accent-orange: #ff7744;
    --text-primary: #ffffff;
    --text-secondary: #a0a0a0;
    --text-tertiary: #6a6a6a;

    /* Typography */
    --font-family:
        -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue",
        "Roboto", "Inter", sans-serif;
    --font-mono: "Menlo", "Monaco", "Courier New", monospace;
    --font-weight-heavy: 800;
}

/* Override mdBook coal theme colors */
.coal {
    --bg: var(--bg-primary);
    --fg: var(--text-primary);
    --sidebar-bg: var(--bg-secondary);
    --sidebar-fg: var(--text-secondary);
    --sidebar-non-existant: var(--text-tertiary);
    --sidebar-active: var(--accent-purple);
    --sidebar-spacer: var(--bg-primary);
    --scrollbar: var(--text-tertiary);
    --icons: var(--text-secondary);
    --icons-hover: var(--accent-purple);
    --links: var(--accent-purple);
    --inline-code-color: var(--accent-green);
    --theme-popup-bg: var(--bg-secondary);
    --theme-popup-border: var(--text-tertiary);
    --theme-hover: var(--bg-primary);
    --quote-bg: var(--bg-secondary);
    --quote-border: var(--accent-purple);
    --table-border-color: var(--text-tertiary);
    --table-header-bg: var(--bg-secondary);
    --table-alternate-bg: var(--bg-secondary);
    --searchbar-border-color: var(--text-tertiary);
    --searchbar-bg: var(--bg-secondary);
    --searchbar-fg: var(--text-primary);
    --search-mark-bg: var(--accent-orange);
}

/* Typography overrides */
body {
    font-family: var(--font-family);
    background-color: var(--bg-primary);
    color: var(--text-primary);
}

/* Heavy weight headings like 42futures */
h1,
h2,
h3,
h4,
h5,
h6 {
    font-weight: var(--font-weight-heavy);
    color: var(--text-primary);
}

h1 {
    color: var(--accent-purple);
    border-bottom: 2px solid var(--accent-purple);
    margin-top: 0;
}

h2 {
    color: var(--text-primary);
    margin-top: 1.5em;
}

h3 {
    margin-top: 1.2em;
}

/* Code blocks */
code {
    font-family: var(--font-mono);
    background-color: var(--bg-secondary);
    color: var(--accent-green);
    padding: 0.1em 0.3em;
    border-radius: 3px;
}

pre {
    background-color: var(--bg-secondary);
    border: 1px solid var(--text-tertiary);
    border-radius: 4px;
}

pre code {
    color: var(--text-primary);
}

/* Links */
a {
    color: var(--accent-purple);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

/* Sidebar styling */
.sidebar {
    background-color: var(--bg-secondary);
}

.sidebar-scrollbox {
    background-color: var(--bg-secondary);
}

.chapter {
    color: var(--text-secondary);
}

.chapter.active {
    color: var(--accent-purple);
    font-weight: 600;
}

.chapter:hover {
    color: var(--text-primary);
}

/* Content area */
.content {
    max-width: 70ch;
    color: var(--text-primary);
}

/* Generous spacing like 42futures */
.content p {
    line-height: 1.7;
    margin-bottom: 1.2em;
}

.content ul,
.content ol {
    line-height: 1.7;
    margin-bottom: 1.2em;
}

/* Blockquotes */
blockquote {
    border-left: 4px solid var(--accent-purple);
    background-color: var(--bg-secondary);
    padding: 1em;
    margin: 1.5em 0;
}

/* Tables */
table {
    border-collapse: collapse;
    width: 100%;
    margin: 1.5em 0;
}

table thead {
    background-color: var(--bg-secondary);
}

table th,
table td {
    border: 1px solid var(--text-tertiary);
    padding: 0.5em 1em;
}

/* Search bar */
#searchbar {
    background-color: var(--bg-secondary);
    border: 1px solid var(--text-tertiary);
    color: var(--text-primary);
}

/* Navigation buttons */
.nav-chapters {
    color: var(--text-secondary);
}

.nav-chapters:hover {
    color: var(--accent-purple);
    text-decoration: none;
}

/* Menu bar */
.menu-bar {
    background-color: var(--bg-secondary);
    border-bottom: 1px solid var(--text-tertiary);
}

/* Buttons */
button {
    background-color: var(--bg-secondary);
    color: var(--text-primary);
    border: 1px solid var(--text-tertiary);
}

button:hover {
    background-color: var(--bg-primary);
    border-color: var(--accent-purple);
}

/* Improve contrast for better readability */
.content main {
    padding-top: 2em;
    padding-bottom: 3em;
}

/* Hide theme selector */
#theme-toggle {
    display: none;
}

/* Remove double box around code blocks */
.content pre {
    margin: 0;
}

.content figure {
    margin: 1.5em 0;
}

.content figure pre {
    margin: 0;
}

```

## /example/core/channels.firm

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

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

channel linkedin_organic {
    description = "Organic inbound from LinkedIn."
    name = "LinkedIn organic"
    type = enum"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 = enum"Network"

    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 = enum"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 = enum"Client"

    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 = enum"Client"
    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 = enum"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 = enum"In-person 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 = enum"Open"
    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
}

```

## /example/schemas/account.firm

```firm path="/example/schemas/account.firm" 
schema account {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "organization_ref"
        type = "reference"
        required = true
    }
    field {
        name = "owner_ref"
        type = "reference"
        required = false
    }
    field {
        name = "status"
        type = "enum"
        allowed_values = ["prospect", "engaged", "client", "partner", "closed"]
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/channel.firm

```firm path="/example/schemas/channel.firm" 
schema channel {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "type"
        type = "enum"
        allowed_values = ["website", "social media", "email", "meeting", "other"]
        required = false
    }
    field {
        name = "description"
        type = "string"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/contact.firm

```firm path="/example/schemas/contact.firm" 
schema contact {
    field {
        name = "source_ref"
        type = "reference"
        required = false
    }
    field {
        name = "person_ref"
        type = "reference"
        required = false
    }
    field {
        name = "account_ref"
        type = "reference"
        required = false
    }
    field {
        name = "role"
        type = "string"
        required = false
    }
    field {
        name = "status"
        type = "enum"
        allowed_values = ["prospect", "partner", "client", "network", "other"]
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/file_asset.firm

```firm path="/example/schemas/file_asset.firm" 
schema file_asset {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "path"
        type = "path"
        required = true
    }
    field {
        name = "description"
        type = "string"
        required = false
    }
    field {
        name = "source_ref"
        type = "reference"
        required = false
    }
    field {
        name = "owner_ref"
        type = "reference"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/industry.firm

```firm path="/example/schemas/industry.firm" 
schema industry {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "sector"
        type = "string"
        required = false
    }
    field {
        name = "classification_code"
        type = "string"
        required = false
    }
    field {
        name = "classification_system"
        type = "string"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/interaction.firm

```firm path="/example/schemas/interaction.firm" 
schema interaction {
    field {
        name = "type"
        type = "enum"
        allowed_values = ["online meeting", "in-person meeting", "chat", "email", "other"]
        required = false
    }
    field {
        name = "subject"
        type = "string"
        required = true
    }
    field {
        name = "initiator_ref"
        type = "reference"
        required = true
    }
    field {
        name = "primary_contact_ref"
        type = "reference"
        required = true
    }
    field {
        name = "interaction_date"
        type = "datetime"
        required = true
    }
    field {
        name = "outcome"
        type = "string"
        required = false
    }
    field {
        name = "secondary_contacts_ref"
        type = "list"
        required = false
    }
    field {
        name = "channel_ref"
        type = "reference"
        required = false
    }
    field {
        name = "opportunity_ref"
        type = "reference"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/key_result.firm

```firm path="/example/schemas/key_result.firm" 
schema key_result {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "objective_ref"
        type = "reference"
        required = true
    }
    field {
        name = "owner_ref"
        type = "reference"
        required = false
    }
    field {
        name = "start_value"
        type = "float"
        required = false
    }
    field {
        name = "target_value"
        type = "float"
        required = false
    }
    field {
        name = "current_value"
        type = "float"
        required = false
    }
    field {
        name = "unit"
        type = "string"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/lead.firm

```firm path="/example/schemas/lead.firm" 
schema lead {
    field {
        name = "source_ref"
        type = "reference"
        required = true
    }
    field {
        name = "status"
        type = "enum"
        allowed_values = ["new", "qualified", "converted", "discarded"]
        required = false
    }
    field {
        name = "person_ref"
        type = "reference"
        required = false
    }
    field {
        name = "account_ref"
        type = "reference"
        required = false
    }
    field {
        name = "score"
        type = "integer"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/objective.firm

```firm path="/example/schemas/objective.firm" 
schema objective {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "description"
        type = "string"
        required = false
    }
    field {
        name = "strategy_ref"
        type = "reference"
        required = false
    }
    field {
        name = "owner_ref"
        type = "reference"
        required = false
    }
    field {
        name = "status"
        type = "string"
        required = false
    }
    field {
        name = "start_date"
        type = "datetime"
        required = false
    }
    field {
        name = "end_date"
        type = "datetime"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/opportunity.firm

```firm path="/example/schemas/opportunity.firm" 
schema opportunity {
    field {
        name = "source_ref"
        type = "reference"
        required = true
    }
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "status"
        type = "enum"
        allowed_values = ["open", "closed won", "closed lost"]
        required = false
    }
    field {
        name = "value"
        type = "currency"
        required = false
    }
    field {
        name = "probability"
        type = "integer"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/organization.firm

```firm path="/example/schemas/organization.firm" 
schema organization {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "address"
        type = "string"
        required = false
    }
    field {
        name = "email"
        type = "string"
        required = false
    }
    field {
        name = "phone"
        type = "string"
        required = false
    }
    field {
        name = "urls"
        type = "list"
        required = false
    }
    field {
        name = "vat_id"
        type = "string"
        required = false
    }
    field {
        name = "industry_ref"
        type = "reference"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/person.firm

```firm path="/example/schemas/person.firm" 
schema person {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "email"
        type = "string"
        required = false
    }
    field {
        name = "phone"
        type = "string"
        required = false
    }
    field {
        name = "urls"
        type = "list"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/project.firm

```firm path="/example/schemas/project.firm" 
schema project {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "status"
        type = "enum"
        allowed_values = ["planned", "in progress", "completed", "cancelled"]
        required = false
    }
    field {
        name = "description"
        type = "string"
        required = false
    }
    field {
        name = "owner_ref"
        type = "reference"
        required = false
    }
    field {
        name = "objective_refs"
        type = "list"
        required = false
    }
    field {
        name = "due_date"
        type = "datetime"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/review.firm

```firm path="/example/schemas/review.firm" 
schema review {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "date"
        type = "datetime"
        required = true
    }
    field {
        name = "owner_ref"
        type = "reference"
        required = false
    }
    field {
        name = "source_refs"
        type = "list"
        required = false
    }
    field {
        name = "attendee_refs"
        type = "list"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/strategy.firm

```firm path="/example/schemas/strategy.firm" 
schema strategy {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "description"
        type = "string"
        required = false
    }
    field {
        name = "source_ref"
        type = "reference"
        required = false
    }
    field {
        name = "owner_ref"
        type = "reference"
        required = false
    }
    field {
        name = "status"
        type = "enum"
        allowed_values = ["draft", "active", "completed", "archived"]
        required = false
    }
    field {
        name = "start_date"
        type = "datetime"
        required = false
    }
    field {
        name = "end_date"
        type = "datetime"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /example/schemas/task.firm

```firm path="/example/schemas/task.firm" 
schema task {
    field {
        name = "name"
        type = "string"
        required = true
    }
    field {
        name = "description"
        type = "string"
        required = false
    }
    field {
        name = "source_ref"
        type = "reference"
        required = false
    }
    field {
        name = "assignee_ref"
        type = "reference"
        required = false
    }
    field {
        name = "due_date"
        type = "datetime"
        required = false
    }
    field {
        name = "is_completed"
        type = "boolean"
        required = false
    }
    field {
        name = "completed_at"
        type = "datetime"
        required = false
    }
    field {
        name = "notes"
        type = "string"
        required = false
    }
    field {
        name = "created_at"
        type = "datetime"
        required = false
    }
    field {
        name = "updated_at"
        type = "datetime"
        required = false
    }
}

```

## /firm_cli/AGENTS.md.template

```template path="/firm_cli/AGENTS.md.template" 
# Firm Workspace AI Guide

This workspace uses [Firm](https://github.com/42futures/firm), a text-based work management system for defining business entities and their relationships as code.

## How to interact with this workspace

**Prefer using the `firm` CLI for querying data.** The CLI provides validated, structured access to the workspace. You can edit `.firm` files directly when needed, but always run `firm build` afterward to validate your changes.

### Quick Overview Commands

Start with these commands to understand the workspace:

\`\`\`bash
# See what entity types are available
firm list schema

# List all entities of a specific type
firm list <entity_type>

# Get details for a specific entity
firm get <entity_type> <entity_id>

# Explore relationships for an entity
firm related <entity_type> <entity_id>

# Query the workspace
firm query <query_string>
\`\`\`

### Best Practices

1. **Prefer the CLI for queries**: Use `firm get`, `firm list`, and `firm related` rather than parsing `.firm` files manually
2. **Use non-interactive add**: Consider `firm add --type --id --field` for programmatic entity creation instead of creating files directly
3. **Validate after changes**: Always run `firm build` after changing a firm workspace to ensure your changes are valid
4. **Check schemas first**: Use `firm list schema` to see available entity types and their required fields
5. **Explore relationships**: Use `firm related` to understand connections between entities
6. **Use JSON output for automation**: Add `--format json` to any command for structured output

### CLI Reference

#### `firm build`
Validate the workspace and build the entity graph.

#### `firm get <target_type> <target_id>`
Get details for a specific entity or schema.

Examples:
\`\`\`bash
# Get an entity
firm get contact john_at_acme

# Get a schema
firm get schema project
\`\`\`

#### `firm list <target_type>`
List all entities of a type. Use `firm list schema` to see available schemas.

Example: `firm list task`

#### `firm related <entity_type> <entity_id>`
Show all entities related to the specified entity.

Example: `firm related contact john_at_acme`

#### `firm source <target_type> <target_id>`
Find the source file path where an entity or schema is defined. Useful when you've found an entity via `get`, `list`, or `query` and want to edit the source file.

Examples:
\`\`\`bash
# Find where a person entity is defined
firm source person john_doe

# Find where a schema is defined
firm source schema project
\`\`\`

Returns the absolute path to the `.firm` file containing the definition.

#### `firm add`
Add a new entity. Can be used interactively (without flags) or non-interactively for automation.

**Non-interactive mode** (recommended for AI agents):
\`\`\`bash
# Add an entity with fields
firm add --type person --id john_doe \
  --field name "John Doe" \
  --field email "john@example.com"

# Add an entity with lists
firm add --type person --id jane_smith \
  --field name "Jane Smith" \
  --list urls string \
  --list-value urls "https://github.com/janesmith" \
  --list-value urls "https://linkedin.com/in/janesmith"
\`\`\`

Arguments:
- `--type <entity_type>`: The entity type (required for non-interactive mode)
- `--id <entity_id>`: The entity ID (required for non-interactive mode)
- `--field <field_name> <value>`: Add a field (repeatable)
- `--list <field_name> <item_type>`: Declare a list field with its item type
- `--list-value <field_name> <value>`: Add an item to a list (repeatable)

The command validates against schemas and provides error messages for self-correction.
In cases where you're adding new entities, this is preferable to first editing the DSL and then building the workspace to verify.

### Querying with the Query Language

For advanced queries, use `firm query` with the Firm Query Language. This provides filtering, relationship traversal, sorting, and limiting capabilities without needing external tools like `jq`.

**Basic syntax:**
\`\`\`
from <type or wildcard> | <operation> | <operation> | ...
\`\`\`

**Available operations:**

1. **`where <field> <operator> <value>`** - Filter entities
   - Operators: `==`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `startswith`, `endswith`, `in`
   - Works with all field types and metadata fields (`@type`, `@id`)
   - Examples:
     \`\`\`bash
     firm query 'from task | where is_completed == false'
     firm query 'from person | where email contains "@acme.com"'
     firm query 'from * | where @type == "task"'
     \`\`\`

2. **`related([degrees]) [type]`** - Traverse relationships
   - Default: 1 degree of separation
   - Optional type filter to only return specific entity types
   - Examples:
     \`\`\`bash
     firm query 'from person | related task'
     firm query 'from project | related(2)'
     firm query 'from contact | related(2) interaction'
     \`\`\`

3. **`order <field> [asc|desc]`** - Sort results
   - Default: ascending
   - Works with regular fields and metadata (`@type`, `@id`)
   - Examples:
     \`\`\`bash
     firm query 'from task | order due_date'
     firm query 'from task | order due_date desc'
     firm query 'from * | order @type'
     \`\`\`

4. **`limit <n>`** - Limit number of results
   - Example: `firm query 'from task | limit 10'`

**Complex query example:**
\`\`\`bash
# Find incomplete tasks due soon from active projects
firm query 'from project | where status == "in progress" | related(2) task | where is_completed == false | where due_date > 2025-01-01 | order due_date | limit 10'
\`\`\`

**Composable operations:**
Operations can be chained in any order. The query engine processes them left-to-right, with each operation transforming the result set for the next operation.

**Field types in queries:**
- Strings: `"value"` or `'value'`
- Numbers: `42` or `3.14`
- Booleans: `true` or `false`
- Currency: `5000.50 USD`
- DateTime: `2025-01-15` or `2025-01-15 at 14:00` or `2025-01-15 at 14:00 UTC` `2025-01-15 at 14:00 UTC+3`
- References: `person.john_doe`
- Enums: `enum"value"` or `"value"`
- Paths: `path"./file.txt"`  or `"./file.txt"`

### Manual Workflows
Sometimes you might want to read or edit firm DSL directly.
This is always a possibility if the CLI doesn't provide the necessary functionality, but you should always remember to build the workspace with the CLI afterwards to verify that the changes were valid.

#### Adding a new contact and interaction

When adding contacts via DSL, create a `.firm` file with the contact, their organization, and any interactions:

\`\`\`firm
organization acme {
    name = "Acme Corp"
}

person john_doe {
    name = "John Doe"
}

contact john_at_acme {
    person_ref = person.john_doe
    account_ref = account.acme
    role = "CEO"
}

interaction intro_call {
    type = "Call"
    subject = "Introduction call"
    interaction_date = 2025-12-30 at 14:00 UTC
    initiator_ref = person.me
    primary_contact_ref = contact.john_at_acme
}
\`\`\`

#### Creating a task

Tasks can reference other entities as their source:

\`\`\`firm
task follow_up {
    name = "Follow up with John"
    source_ref = interaction.intro_call
    assignee_ref = person.me
    due_date = 2026-01-05
    is_completed = false
}
\`\`\`

### Output Formats

For structured output that's easy to parse programmatically, use `--format json`:

\`\`\`bash
firm list task --format json
firm query 'from task | where is_completed == false' --format json

# Combine with jq to extract specific data
firm query 'from task | order due_date asc | limit 5' --format json | jq '.[].id'
\`\`\`

### Notes for AI Agents

- The workspace structure is validated by schemas defined in the `schemas/` directory
- Entity IDs should be unique and follow snake_case naming
- References between entities create a queryable graph structure
- Always validate changes with `firm build` before committing

```

## /firm_cli/Cargo.toml

```toml path="/firm_cli/Cargo.toml" 
[package]
name = "firm-cli"
version = "0.5.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" }
firm_mcp = { path = "../firm_mcp" }
firm_lsp = { path = "../firm_lsp" }

tokio = { version = "1", features = ["rt-multi-thread"] }

clap = { version = "4.5.42", features = ["derive", "env"] }
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, env = "FIRM_WORKSPACE")]
    pub workspace: Option<PathBuf>,

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

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

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

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

/// Defines the available subcommands of the Firm CLI.
#[derive(Subcommand, Debug, PartialEq)]
pub enum FirmCliCommand {
    /// Initialize a new Firm workspace with default schemas and files.
    Init,
    /// Build workspace and entity graph.
    Build,
    /// Get an entity or schema.
    Get {
        /// Entity type (e.g. person, organization) or "schema"
        target_type: String,
        /// Entity ID (e.g. john_doe) or schema name (e.g. project)
        target_id: String,
    },
    /// List entities of a type, or list all schemas.
    List {
        /// Entity type (e.g. person, organization) or "schema" to list schemas
        target_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>,
    },
    /// Adds a new entity to a file in the workspace. If type, id or fields are not provided, this is done interactively.
    Add {
        /// Target firm file.
        to_file: Option<PathBuf>,
        /// Entity type for non-interactive mode (e.g., person, organization)
        #[arg(long)]
        r#type: Option<String>,
        /// Entity ID for non-interactive mode (e.g., john_doe)
        #[arg(long)]
        id: Option<String>,
        /// Field for non-interactive mode (can be repeated). Format: --field <field_name> <value>
        #[arg(long = "field", num_args = 2, value_names = ["FIELD_NAME", "VALUE"])]
        fields: Vec<String>,
        /// List declaration for non-interactive mode (can be repeated). Format: --list <field_name> <item_type>
        #[arg(long = "list", num_args = 2, value_names = ["FIELD_NAME", "ITEM_TYPE"])]
        lists: Vec<String>,
        /// List value for non-interactive mode (can be repeated). Format: --list-value <field_name> <value>
        #[arg(long = "list-value", num_args = 2, value_names = ["FIELD_NAME", "VALUE"])]
        list_values: Vec<String>,
    },
    /// Query entities in the workspace using a query language.
    Query {
        /// Query string (e.g., "from task | where is_completed == false | limit 5")
        query: String,
    },
    /// Find the source file for an entity or schema.
    Source {
        /// Entity type (e.g. person, organization) or "schema"
        target_type: String,
        /// Entity ID (e.g. john_doe) or schema name (e.g. project)
        target_id: String,
    },
    /// Check workspace files for errors. Optionally check a single file.
    Check {
        /// Path to a specific .firm file to check (checks entire workspace if omitted)
        file: Option<PathBuf>,
    },
    /// Start the language server (stdio transport).
    Lsp,
    /// Start the MCP server (stdio transport).
    Mcp,
}

```

## /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, FieldId, FieldType, FieldValue, compose_entity_id};
use firm_lang::generate::generate_dsl;
use firm_lang::parser::dsl::ParsedValue;
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_workspace, field_prompt::prompt_for_field_value, load_workspace_files};
use crate::errors::CliError;
use crate::files::load_current_graph;
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)
    }
}

/// Add a new entity and generate DSL for it.
/// If type, id, or fields are provided, uses non-interactive mode.
pub fn add_entity(
    workspace_path: &PathBuf,
    to_file: Option<PathBuf>,
    entity_type: Option<String>,
    entity_id: Option<String>,
    fields: Vec<String>,
    lists: Vec<String>,
    list_values: Vec<String>,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    // Check if we're in non-interactive mode
    let is_non_interactive = entity_type.is_some()
        || entity_id.is_some()
        || !fields.is_empty()
        || !lists.is_empty()
        || !list_values.is_empty();

    if is_non_interactive {
        // Validate that both type and id are provided
        if entity_type.is_none() || entity_id.is_none() {
            ui::error("Non-interactive mode requires both --type and --id arguments");
            return Err(CliError::InputError);
        }

        return add_entity_non_interactive(
            workspace_path,
            to_file,
            entity_type.unwrap(),
            entity_id.unwrap(),
            fields,
            lists,
            list_values,
            output_format,
        );
    }

    // Otherwise, use interactive mode
    add_entity_interactive(workspace_path, to_file, output_format)
}

/// Add a new entity non-interactively using CLI arguments.
fn add_entity_non_interactive(
    workspace_path: &PathBuf,
    to_file: Option<PathBuf>,
    entity_type: String,
    entity_id: String,
    fields: Vec<String>,
    lists: Vec<String>,
    list_values: Vec<String>,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    // Load the pre-built graph and build workspace for schemas
    let graph = load_current_graph(workspace_path)?;
    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)?;

    // Find the schema for the given type
    let schema = build
        .schemas
        .iter()
        .find(|s| s.entity_type.to_string() == entity_type)
        .ok_or_else(|| {
            ui::error(&format!(
                "Schema for '{}' not found in workspace",
                entity_type
            ));
            CliError::InputError
        })?;

    // Check if the entity ID is unique
    let sanitized_id = sanitize_entity_id(entity_id.clone());
    let composite_id = compose_entity_id(&entity_type, &sanitized_id);
    if graph.get_entity(&composite_id).is_some() {
        ui::error(&format!(
            "An entity with ID '{}' already exists",
            composite_id
        ));
        return Err(CliError::InputError);
    }

    // Parse fields from CLI args
    let mut entity = Entity::new(composite_id.clone(), schema.entity_type.to_owned());

    // Parse list declarations (--list field_name item_type)
    let mut list_types: std::collections::HashMap<String, String> =
        std::collections::HashMap::new();
    for chunk in lists.chunks(2) {
        if chunk.len() == 2 {
            list_types.insert(chunk[0].to_string(), chunk[1].to_string());
        }
    }

    // Group list values by field name (--list-value field_name value)
    let mut list_value_groups: std::collections::HashMap<String, Vec<String>> =
        std::collections::HashMap::new();
    for chunk in list_values.chunks(2) {
        if chunk.len() == 2 {
            list_value_groups
                .entry(chunk[0].to_string())
                .or_default()
                .push(chunk[1].to_string());
        }
    }

    // Compute the generated file path early so we can use it for path parsing
    let generated_file_path =
        compute_dsl_path(workspace_path, to_file.clone(), entity_type.clone());

    // Process regular fields (--field field_name value)
    for chunk in fields.chunks(2) {
        if chunk.len() == 2 {
            let field_name = chunk[0].as_str();
            let field_value_str = chunk[1].as_str();
            let field_id = FieldId::new(field_name);

            // Find the field in the schema to get its expected type
            let schema_field = schema.fields.get(&field_id).ok_or_else(|| {
                ui::error(&format!(
                    "Field '{}' is not defined in schema '{}'",
                    field_name, entity_type
                ));
                ui::error("\nAvailable fields in this schema:");
                for (field_id, field_def) in &schema.fields {
                    let required_str = if field_def.is_required() {
                        "required"
                    } else {
                        "optional"
                    };
                    ui::error(&format!(
                        "  - {} ({}, {})",
                        field_id.as_str(),
                        field_def.expected_type(),
                        required_str
                    ));
                }
                CliError::InputError
            })?;

            let expected_type = schema_field.expected_type();

            // Parse the field value
            let parsed_value = parse_field_value_from_string(
                field_value_str,
                expected_type,
                &generated_file_path,
            )?;
            let field_value: FieldValue = parsed_value.try_into().map_err(|_| {
                ui::error(&format!(
                    "Failed to convert parsed value for field '{}'",
                    field_name
                ));
                CliError::InputError
            })?;

            entity = entity.with_field(field_id, field_value);
        }
    }

    // Process list fields (--list field_name item_type and --list-value field_name value)
    for (list_field_name, item_type_str) in &list_types {
        let field_id = FieldId::new(list_field_name);

        // Validate field exists in schema
        let schema_field = schema.fields.get(&field_id).ok_or_else(|| {
            ui::error(&format!(
                "List field '{}' is not defined in schema '{}'",
                list_field_name, entity_type
            ));
            ui::error("\nAvailable fields in this schema:");
            for (field_id, field_def) in &schema.fields {
                let required_str = if field_def.is_required() {
                    "required"
                } else {
                    "optional"
                };
                ui::error(&format!(
                    "  - {} ({}, {})",
                    field_id.as_str(),
                    field_def.expected_type(),
                    required_str
                ));
            }
            CliError::InputError
        })?;

        // Verify this field is actually a list type in the schema
        if !matches!(schema_field.expected_type(), FieldType::List) {
            ui::error(&format!(
                "Field '{}' is not a list type in the schema",
                list_field_name
            ));
            return Err(CliError::InputError);
        }

        // Parse the item type
        let item_field_type = parse_field_type(item_type_str)?;

        // Get the values for this list
        let values = list_value_groups.get(list_field_name).ok_or_else(|| {
            ui::error(&format!(
                "No values provided for list '{}' (use --list-value)",
                list_field_name
            ));
            CliError::InputError
        })?;

        // Parse each value using the declared item type
        let mut parsed_items = Vec::new();
        for value_str in values {
            let item = parse_field_value_from_string(
                value_str.as_str(),
                &item_field_type,
                &generated_file_path,
            )?;
            parsed_items.push(item);
        }

        // Validate homogeneity and create list
        let parsed_list = ParsedValue::parse_list_from_vec(parsed_items).map_err(|e| {
            ui::error(&format!(
                "Failed to create list for field '{}': {}",
                list_field_name, e
            ));
            CliError::InputError
        })?;

        let field_value: FieldValue = parsed_list.try_into().map_err(|_| {
            ui::error(&format!(
                "Failed to convert list for field '{}'",
                list_field_name
            ));
            CliError::InputError
        })?;

        entity = entity.with_field(field_id, field_value);
    }

    // Validate entity against schema
    schema.validate(&entity).map_err(|errors| {
        ui::error("Entity validation failed:");
        for error in errors {
            ui::error(&format!("  - {}", error.message));
        }
        CliError::InputError
    })?;

    // Generate and write 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)
}

/// Interactively add a new entity and generate DSL for it.
fn add_entity_interactive(
    workspace_path: &PathBuf,
    to_file: Option<PathBuf>,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    ui::header("Adding new entity");
    let graph = load_current_graph(workspace_path)?;
    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 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 {
        if let Some(value) = prompt_for_field_value(
            field_id,
            field.expected_type(),
            field.is_required(),
            field.allowed_values(),
            Arc::clone(arc_graph),
            source_path,
            workspace_path,
        )? {
            entity = entity.with_field(field_id.clone(), value);
        }
    }

    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 {
        if let Some(value) = prompt_for_field_value(
            field_id,
            field.expected_type(),
            field.is_required(),
            field.allowed_values(),
            Arc::clone(&graph),
            source_path,
            workspace_path,
        )? {
            entity = entity.with_field(field_id.clone(), value);
        }
    }

    Ok(entity)
}

/// Sanitize a string to be a valid entity ID.
/// - Filters for only alphanumeric characters, underscores, dashes, and whitespace
/// - Converts to snake_case
pub fn sanitize_entity_id(input: String) -> String {
    input
        .chars()
        .filter(|&c| c == ' ' || c == '_' || c == '-' || c.is_alphanumeric())
        .collect::<String>()
        .to_case(Case::Snake)
}

/// Ensures uniqueness and conformity of a selected entity ID.
/// We do this by:
/// - Filtering for only alphanumeric characters, underscores, dashes, 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,
    chosen_id: String,
) -> String {
    let sanitized_id = sanitize_entity_id(chosen_id);

    let mut entity_id = sanitized_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!("{}_{}", sanitized_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 {
    

    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),
    }
}

/// 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)
        }
    }
}

/// Parses a field type string into a FieldType enum.
fn parse_field_type(type_str: &str) -> Result<FieldType, CliError> {
    match type_str.to_lowercase().as_str() {
        "string" => Ok(FieldType::String),
        "integer" => Ok(FieldType::Integer),
        "float" => Ok(FieldType::Float),
        "boolean" => Ok(FieldType::Boolean),
        "currency" => Ok(FieldType::Currency),
        "reference" => Ok(FieldType::Reference),
        "datetime" => Ok(FieldType::DateTime),
        "path" => Ok(FieldType::Path),
        "enum" => Ok(FieldType::Enum),
        _ => {
            ui::error(&format!(
                "Unknown field type '{}'. Valid types: string, integer, float, boolean, currency, reference, datetime, path, enum",
                type_str
            ));
            Err(CliError::InputError)
        }
    }
}

/// Parses a field value from a string based on the expected type.
fn parse_field_value_from_string(
    value_str: &str,
    expected_type: &FieldType,
    source_path: &PathBuf,
) -> Result<ParsedValue, CliError> {
    match expected_type {
        FieldType::Boolean => ParsedValue::parse_boolean(value_str),
        FieldType::String => ParsedValue::parse_string(value_str),
        FieldType::Integer | FieldType::Float => ParsedValue::parse_number(value_str),
        FieldType::Currency => ParsedValue::parse_currency(value_str),
        FieldType::Reference => ParsedValue::parse_reference(value_str),
        FieldType::DateTime => {
            // Try parsing as datetime first, then as date
            ParsedValue::parse_datetime(value_str).or_else(|_| ParsedValue::parse_date(value_str))
        }
        FieldType::Enum => ParsedValue::parse_enum(value_str),
        FieldType::Path => {
            // For paths in non-interactive mode, the user specifies them relative to CWD
            // But we need to store them relative to the generated .firm file
            // So we need to transform: CWD-relative -> absolute -> source-file-relative
            let user_path = PathBuf::from(value_str);
            let absolute_path = if user_path.is_absolute() {
                user_path
            } else {
                // Resolve relative to current working directory
                std::env::current_dir()
                    .map_err(|_| {
                        ui::error("Failed to get current working directory");
                        CliError::InputError
                    })?
                    .join(&user_path)
            };

            // Now make it relative to the source file's parent directory
            // Canonicalize both paths to ensure diff_paths works correctly
            let canonical_target = absolute_path
                .canonicalize()
                .unwrap_or(absolute_path.clone());
            let source_dir = source_path.parent().unwrap_or(std::path::Path::new(""));
            let canonical_source_dir = source_dir
                .canonicalize()
                .unwrap_or(source_dir.to_path_buf());

            let relative_to_source = pathdiff::diff_paths(&canonical_target, &canonical_source_dir)
                .unwrap_or(canonical_target.clone());

            Ok(ParsedValue::Path(relative_to_source))
        }
        FieldType::List => {
            ui::error("List fields must be specified using --list and --list-value flags");
            return Err(CliError::InputError);
        }
    }
    .map_err(|e| {
        ui::error(&format!("Failed to parse field value: {}", e));
        CliError::InputError
    })
}

```

## /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(_) => {
            spinner.finish_with_message("Workspace files loaded successfully");
            Ok(())
        },
        Err(e) => {
            spinner.finish_and_clear();
            ui::error_with_details(
                &format!("Failed to load directory '{}'", path.display()),
                &e.to_string(),
            );

            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();

        if let GraphError::EntityAlreadyExists(entity_id) = e {
            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/check.rs

```rs path="/firm_cli/src/commands/check.rs" 
//! Check command: validates workspace files and reports diagnostics.

use std::path::{Path, PathBuf};

use firm_lang::diagnostics;
use firm_lang::parser::dsl::parse_source;
use firm_lang::workspace::Workspace;

use crate::errors::CliError;
use crate::ui;

/// Check for errors. Checks the whole workspace, or a single file if provided.
pub fn check(workspace_path: &Path, file: Option<PathBuf>) -> Result<(), CliError> {
    match file {
        Some(file_path) => check_file(&file_path),
        None => check_workspace(workspace_path),
    }
}

/// Check a single .firm file for syntax errors.
fn check_file(file_path: &Path) -> Result<(), CliError> {
    let source = std::fs::read_to_string(file_path).map_err(|e| {
        ui::error_with_details(
            &format!("Failed to read '{}'", file_path.display()),
            &e.to_string(),
        );
        CliError::FileError
    })?;

    let parsed = parse_source(source, Some(file_path.to_path_buf())).map_err(|e| {
        ui::error_with_details("Failed to parse file", &e.to_string());
        CliError::BuildError
    })?;

    let file_diagnostics = diagnostics::collect_syntax_errors(&parsed);
    report_diagnostics(&file_diagnostics)
}

/// Check all files in the workspace for syntax errors.
fn check_workspace(workspace_path: &Path) -> Result<(), CliError> {
    let mut workspace = Workspace::new();

    super::load_workspace_files(&workspace_path.to_path_buf(), &mut workspace)
        .map_err(|_| CliError::BuildError)?;

    let mut all_diagnostics = Vec::new();
    for parsed in workspace.parsed_sources() {
        all_diagnostics.extend(diagnostics::collect_syntax_errors(parsed));
    }

    // Only run workspace-level diagnostics if there are no syntax errors,
    // since syntax errors may cause incomplete parse trees.
    if all_diagnostics.is_empty() {
        all_diagnostics.extend(diagnostics::collect_workspace_diagnostics(&workspace));
    }

    report_diagnostics(&all_diagnostics)
}

/// Print diagnostics and return Ok if none, Err if any errors found.
fn report_diagnostics(diagnostics: &[diagnostics::Diagnostic]) -> Result<(), CliError> {
    for diagnostic in diagnostics {
        ui::error(&diagnostic.to_string());
    }

    if diagnostics.is_empty() {
        ui::success("No errors found");
        Ok(())
    } else {
        ui::error(&format!("\nFound {} error(s)", diagnostics.len()));
        Err(CliError::BuildError)
    }
}

```

## /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,
    allowed_values: Option<&Vec<String>>,
    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(),
        ),
        FieldType::Enum => enum_prompt(skippable, &field_id_prompt, allowed_values),
    }
}

/// 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,
            None,
            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 !(-12..=14).contains(&timezone_offset) {
        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()
                    && 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)
}

/// Prompts for an enum field.
fn enum_prompt(
    skippable: bool,
    field_id_prompt: &String,
    allowed_values: Option<&Vec<String>>,
) -> Result<Option<FieldValue>, CliError> {
    match allowed_values {
        Some(values) => {
            let skip_message = get_skippable_prompt(skippable);
            let prompt_text = format!("{}{}:", field_id_prompt, skip_message);

            let selected_option = Select::new(&prompt_text, values.to_vec())
                .prompt()
                .map_err(|_| CliError::InputError)?;

            Ok(Some(FieldValue::Enum(selected_option)))
        }
        None => Err(CliError::InputError),
    }
}

/// 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::ui::{self, OutputFormat};

/// Gets an entity or schema by type and ID/name.
pub fn get_item(
    workspace_path: &PathBuf,
    target_type: String,
    target_id: String,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    // Special case: if target_type is "schema", get schema instead of entity
    if target_type == "schema" {
        return get_schema(workspace_path, target_id, output_format);
    }

    get_entity(workspace_path, target_type, target_id, output_format)
}

/// Gets a single entity by type and ID.
fn get_entity(
    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),
            }
            Ok(())
        }
        None => {
            ui::error(&format!(
                "Couldn't find '{}' entity with ID '{}'",
                entity_type, entity_id
            ));
            Err(CliError::QueryError)
        }
    }
}

/// Gets a single schema by name.
fn get_schema(
    workspace_path: &PathBuf,
    schema_name: String,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    ui::header("Getting schema");
    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)?;

    // Find the schema by name
    let schema = build
        .schemas
        .iter()
        .find(|s| s.entity_type.as_str() == schema_name);

    match schema {
        Some(schema) => {
            ui::success(&format!("Found schema '{}'", schema_name));

            match output_format {
                OutputFormat::Pretty => ui::pretty_output_schema_single(schema),
                OutputFormat::Json => ui::json_output(schema),
            }
            Ok(())
        }
        None => {
            ui::error(&format!("Schema '{}' not found in workspace", schema_name));
            Err(CliError::QueryError)
        }
    }
}

```

## /firm_cli/src/commands/init.rs

```rs path="/firm_cli/src/commands/init.rs" 
use std::fs;
use std::io::Write;
use std::path::Path;

use firm_core::{Entity, EntityId, EntityType, FieldId, FieldValue};
use firm_lang::defaults;
use firm_lang::generate::{generate_dsl, generate_schema_dsl};
use inquire::{Confirm, Text};

use super::add::sanitize_entity_id;
use crate::errors::CliError;
use crate::ui;

/// Initialize a new Firm workspace with default schemas and files.
pub fn init_workspace(workspace_path: &Path) -> Result<(), CliError> {
    ui::header(&format!(
        "Initializing Firm workspace at {}",
        workspace_path.display()
    ));

    // Prompt for default schemas
    let include_schemas = Confirm::new("Include default schemas?")
        .with_default(true)
        .prompt()
        .map_err(|_| CliError::InputError)?;

    if include_schemas {
        create_default_schemas(workspace_path)?;
    }

    // Prompt for .gitignore
    create_or_update_gitignore(workspace_path)?;

    // Prompt for default entities
    let create_entities = Confirm::new("Add default entities (you and your organization)?")
        .with_default(true)
        .prompt()
        .map_err(|_| CliError::InputError)?;

    if create_entities {
        create_default_entities(workspace_path)?;
    }

    // Prompt for AI context
    let add_ai_context = Confirm::new("Add AI context (AGENTS.md)?")
        .with_default(true)
        .prompt()
        .map_err(|_| CliError::InputError)?;

    if add_ai_context {
        create_ai_context(workspace_path)?;
    }

    ui::success("Workspace initialized!");

    Ok(())
}

/// Create AI context file (AGENTS.md).
fn create_ai_context(workspace_path: &Path) -> Result<(), CliError> {
    let agents_md_path = workspace_path.join("AGENTS.md");

    // Check if AGENTS.md already exists
    if agents_md_path.exists() {
        let overwrite = Confirm::new("AGENTS.md already exists. Overwrite?")
            .with_default(false)
            .prompt()
            .map_err(|_| CliError::InputError)?;

        if !overwrite {
            ui::info("Skipped AI context creation");
            return Ok(());
        }
    }

    // Load AGENTS.md template from embedded file
    let agents_md_content = include_str!("../../AGENTS.md.template");

    fs::write(&agents_md_path, agents_md_content).map_err(|_| CliError::FileError)?;
    ui::success("Created AGENTS.md");

    Ok(())
}

/// Create default entities (person and organization) in main.firm file.
fn create_default_entities(workspace_path: &Path) -> Result<(), CliError> {
    let main_file_path = workspace_path.join("main.firm");

    // Check if main.firm already exists
    if main_file_path.exists() {
        let overwrite = Confirm::new("main.firm already exists. Overwrite?")
            .with_default(false)
            .prompt()
            .map_err(|_| CliError::InputError)?;

        if !overwrite {
            ui::info("Skipped entity creation");
            return Ok(());
        }
    }

    ui::info("Let's set up your core entities");

    // Prompt for person name
    let person_name = Text::new("Your name:")
        .prompt()
        .map_err(|_| CliError::InputError)?;

    // Prompt for organization name
    let org_name = Text::new("Your organization name:")
        .prompt()
        .map_err(|_| CliError::InputError)?;

    // Create person entity with sanitized ID (filters numbers, converts to snake_case)
    let person_id = sanitize_entity_id(person_name.clone());

    let person_entity = Entity::new(
        EntityId(format!("person.{}", person_id)),
        EntityType::new("person"),
    )
    .with_field(FieldId::new("name"), FieldValue::String(person_name));

    // Create organization entity with sanitized ID (filters numbers, converts to snake_case)
    let org_id = sanitize_entity_id(org_name.clone());

    let org_entity = Entity::new(
        EntityId(format!("organization.{}", org_id)),
        EntityType::new("organization"),
    )
    .with_field(FieldId::new("name"), FieldValue::String(org_name));

    // Generate DSL
    let entities = vec![person_entity, org_entity];
    let dsl_content = generate_dsl(&entities);

    // Write to main.firm
    fs::write(&main_file_path, dsl_content).map_err(|_| CliError::FileError)?;

    ui::success("Created main.firm with your person and organization");

    Ok(())
}

/// Create default schema files in the schemas/ directory.
fn create_default_schemas(workspace_path: &Path) -> Result<(), CliError> {
    let schemas_dir = workspace_path.join("schemas");
    let schemas = defaults::all_default_schemas();

    // Check which schema files already exist
    let existing_files: Vec<String> = schemas
        .iter()
        .map(|schema| format!("{}.firm", schema.entity_type))
        .filter(|filename| schemas_dir.join(filename).exists())
        .collect();

    // If files exist, ask for confirmation to overwrite
    if !existing_files.is_empty() {
        ui::warning(&format!(
            "{} schema file(s) already exist:",
            existing_files.len()
        ));
        for filename in &existing_files {
            ui::info(&format!("  - schemas/{}", filename));
        }

        let overwrite = Confirm::new("Overwrite existing schema files?")
            .with_default(false)
            .prompt()
            .map_err(|_| CliError::InputError)?;

        if !overwrite {
            ui::info("Skipped schema creation");
            return Ok(());
        }
    }

    // Create schemas directory
    fs::create_dir_all(&schemas_dir).map_err(|_| CliError::FileError)?;

    let spinner = ui::spinner(&format!("Creating {} default schemas", schemas.len()));

    for schema in &schemas {
        let schema_name = schema.entity_type.to_string();
        let file_path = schemas_dir.join(format!("{}.firm", schema_name));
        let dsl_content = generate_schema_dsl(schema);

        let mut file = fs::File::create(&file_path).map_err(|_| CliError::FileError)?;
        file.write_all(dsl_content.as_bytes())
            .map_err(|_| CliError::FileError)?;
    }

    spinner.finish_with_message(format!(
        "Created {} schema files in schemas/",
        schemas.len()
    ));

    Ok(())
}

/// Create or update .gitignore file with Firm-specific entries.
fn create_or_update_gitignore(workspace_path: &Path) -> Result<(), CliError> {
    let gitignore_path = workspace_path.join(".gitignore");
    let gitignore_entries = "**/*.firm.graph\n";

    if gitignore_path.exists() {
        // File exists, ask if they want to update it
        let update = Confirm::new("Update existing .gitignore with Firm entries?")
            .with_default(true)
            .prompt()
            .map_err(|_| CliError::InputError)?;

        if !update {
            ui::info("Skipped .gitignore update");
            return Ok(());
        }

        // Read existing content
        let existing_content =
            fs::read_to_string(&gitignore_path).map_err(|_| CliError::FileError)?;

        // Check if entries already exist
        if existing_content.contains(".DS_Store") && existing_content.contains("*.firm.graph") {
            ui::info(".gitignore already contains Firm entries");
            return Ok(());
        }

        // Append entries
        let mut file = fs::OpenOptions::new()
            .append(true)
            .open(&gitignore_path)
            .map_err(|_| CliError::FileError)?;

        // Add a newline before our entries if file doesn't end with one
        let prefix = if existing_content.ends_with('\n') {
            ""
        } else {
            "\n"
        };
        file.write_all(format!("{}{}", prefix, gitignore_entries).as_bytes())
            .map_err(|_| CliError::FileError)?;

        ui::success("Updated .gitignore with Firm entries");
    } else {
        // File doesn't exist, ask if they want to create it
        let create = Confirm::new("Create .gitignore file?")
            .with_default(true)
            .prompt()
            .map_err(|_| CliError::InputError)?;

        if !create {
            ui::info("Skipped .gitignore creation");
            return Ok(());
        }

        // Create new file
        fs::write(&gitignore_path, gitignore_entries).map_err(|_| CliError::FileError)?;

        ui::success("Created .gitignore");
    }

    Ok(())
}

```

## /firm_cli/src/commands/list.rs

```rs path="/firm_cli/src/commands/list.rs" 
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::ui::{self, OutputFormat};

/// Lists entities of a type or all schemas.
pub fn list_items(
    workspace_path: &PathBuf,
    target_type: String,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    // Special case: if target_type is "schema", list all schemas instead of entities
    if target_type == "schema" {
        return list_schemas(workspace_path, output_format);
    }

    list_entities(workspace_path, target_type, output_format)
}

/// Lists entity IDs of a given type in the workspace.
fn list_entities(
    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());
    let ids: Vec<&str> = entities.iter().map(|e| e.id.as_str()).collect();

    ui::success(&format!(
        "Found {} entities with type '{}'",
        ids.len(),
        entity_type,
    ));

    ui::list_output(&ids, output_format);

    Ok(())
}

/// Lists all schema names in the workspace.
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)?;

    let names: Vec<&str> = build
        .schemas
        .iter()
        .map(|s| s.entity_type.as_str())
        .collect();

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

    ui::list_output(&names, output_format);
    Ok(())
}

```

## /firm_cli/src/commands/lsp.rs

```rs path="/firm_cli/src/commands/lsp.rs" 
//! Language server command implementation.

use std::path::Path;

use firm_lsp::FirmLspServer;

use crate::errors::CliError;
use crate::ui;

/// Start the language server on stdio.
pub fn serve(workspace_path: &Path) -> Result<(), CliError> {
    ui::debug("Starting language server...");

    let rt = tokio::runtime::Runtime::new().map_err(|e| {
        ui::error_with_details("Failed to create async runtime", &e.to_string());
        CliError::BuildError
    })?;

    rt.block_on(async {
        FirmLspServer::serve_stdio(workspace_path.to_path_buf())
            .await
            .map_err(|e| {
                ui::error_with_details("Language server error", &e);
                CliError::BuildError
            })
    })
}

```

## /firm_cli/src/commands/mcp.rs

```rs path="/firm_cli/src/commands/mcp.rs" 
//! MCP server command implementation.

use std::path::Path;

use firm_mcp::FirmMcpServer;

use crate::errors::CliError;
use crate::ui;

/// Start the MCP server on stdio.
pub fn serve(workspace_path: &Path) -> Result<(), CliError> {
    ui::debug("Starting MCP server...");

    // Create a tokio runtime for the async MCP server
    let rt = tokio::runtime::Runtime::new().map_err(|e| {
        ui::error_with_details("Failed to create async runtime", &e.to_string());
        CliError::BuildError
    })?;

    rt.block_on(async {
        // Create the MCP server
        let server = FirmMcpServer::new(workspace_path.to_path_buf()).map_err(|e| {
            ui::error_with_details("Failed to load workspace", &e.to_string());
            CliError::BuildError
        })?;

        // Serve over stdio (blocks until connection closes)
        server.serve_stdio().await.map_err(|e| {
            ui::error_with_details("MCP server error", &e.to_string());
            CliError::BuildError
        })
    })
}

```

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

```rs path="/firm_cli/src/commands/mod.rs" 
mod add;
mod build;
pub mod check;
mod field_prompt;
mod get;
mod init;
mod list;
pub mod lsp;
pub mod mcp;
mod query;
mod related;
mod source;

pub use add::add_entity;
pub use build::{build_and_save_graph, build_workspace, load_workspace_files};
pub use get::get_item;
pub use init::init_workspace;
pub use list::list_items;
pub use query::query_entities;
pub use related::get_related_entities;
pub use source::find_item_source;

```

## /firm_cli/src/commands/query.rs

```rs path="/firm_cli/src/commands/query.rs" 
use std::path::PathBuf;

use firm_core::graph::{Query, QueryResult};
use firm_lang::parser::query::parse_query;

use crate::errors::CliError;
use crate::files::load_current_graph;
use crate::ui::{self, OutputFormat};

/// Executes a query against the workspace entity graph.
pub fn query_entities(
    workspace_path: &PathBuf,
    query_string: String,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    ui::header("Executing query");
    let graph = load_current_graph(workspace_path)?;

    // Parse the query
    let parsed_query = parse_query(&query_string).map_err(|e| {
        ui::error(&format!("Failed to parse query: {}", e));
        CliError::QueryError
    })?;

    // Convert to executable query
    let query: Query = parsed_query.try_into().map_err(|e| {
        ui::error(&format!("Failed to convert query: {}", e));
        CliError::QueryError
    })?;

    // Execute the query
    ui::debug("Executing query");
    let result = query.execute(&graph).map_err(|e| {
        ui::error(&format!("Query execution failed: {}", e));
        CliError::QueryError
    })?;

    // Output results
    match result {
        QueryResult::Entities(entities) => {
            ui::success(&format!("Query returned {} entities", entities.len()));
            match output_format {
                OutputFormat::Pretty => ui::pretty_output_entity_list(&entities),
                OutputFormat::Json => ui::json_output(&entities),
            }
        }
        QueryResult::Aggregation(agg_result) => match output_format {
            OutputFormat::Pretty => ui::raw_output(&agg_result.to_string()),
            OutputFormat::Json => ui::json_output(&agg_result),
        },
    }

    Ok(())
}

```

## /firm_cli/src/commands/related.rs

```rs path="/firm_cli/src/commands/related.rs" 
use firm_core::compose_entity_id;
use std::path::PathBuf;

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

/// Gets entities related to a specific entity.
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)
        }
    }
}

```

## /firm_cli/src/commands/source.rs

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

use super::load_workspace_files;
use crate::errors::CliError;
use crate::ui::{self, OutputFormat};

/// Finds the source file for an entity or schema by its type and ID/name.
pub fn find_item_source(
    workspace_path: &PathBuf,
    target_type: String,
    target_id: String,
    output_format: OutputFormat,
) -> Result<(), CliError> {
    // Load workspace files (parse DSL but don't build/validate)
    let mut workspace = Workspace::new();
    load_workspace_files(workspace_path, &mut workspace).map_err(|_| CliError::BuildError)?;

    // Special case: if entity_type is "schema", search for schemas instead of entities
    let source_path = if target_type == "schema" {
        workspace.find_schema_source(&target_id)
    } else {
        workspace.find_entity_source(&target_type, &target_id)
    };

    match source_path {
        Some(source_path) => {
            match output_format {
                OutputFormat::Pretty => {
                    let is_schema = target_type == "schema";
                    let item_type = if is_schema { "schema" } else { "entity" };
                    let identifier = if is_schema { "name" } else { "ID" };
                    ui::success(&format!(
                        "Found source file for '{}' {} with {} '{}'",
                        target_type, item_type, identifier, target_id
                    ));
                    ui::raw_output(&source_path.display().to_string());
                }
                OutputFormat::Json => {
                    #[derive(serde::Serialize)]
                    struct SourceResult {
                        target_type: String,
                        target_id: String,
                        source_path: PathBuf,
                    }
                    ui::json_output(&SourceResult {
                        target_type,
                        target_id,
                        source_path,
                    });
                }
            }
            Ok(())
        }
        None => {
            let error_msg = if target_type == "schema" {
                format!("Schema with name '{}' not found in workspace", target_id)
            } else {
                format!(
                    "Entity '{}' with type '{}' not found in workspace",
                    target_id, target_type
                )
            };
            ui::error(&error_msg);
            Err(CliError::QueryError)
        }
    }
}

```

## /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/init/source/mcp command
    let skip_build = cli.cached
        || matches!(
            cli.command,
            FirmCliCommand::Build
                | FirmCliCommand::Init
                | FirmCliCommand::Source { .. }
                | FirmCliCommand::Check { .. }
                | FirmCliCommand::Lsp
                | FirmCliCommand::Mcp
        );

    if !skip_build {
        match build_and_save_graph(&workspace_path) {
            Ok(_) => (),
            Err(_) => return ExitCode::FAILURE,
        }
    }

    // Handle CLI subcommands
    let result = match cli.command {
        FirmCliCommand::Init => commands::init_workspace(&workspace_path),
        FirmCliCommand::Build => build_and_save_graph(&workspace_path),
        FirmCliCommand::Get {
            target_type,
            target_id,
        } => commands::get_item(&workspace_path, target_type, target_id, cli.format),
        FirmCliCommand::List { target_type } => {
            commands::list_items(&workspace_path, target_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,
            r#type,
            id,
            fields,
            lists,
            list_values,
        } => commands::add_entity(
            &workspace_path,
            to_file,
            r#type,
            id,
            fields,
            lists,
            list_values,
            cli.format,
        ),
        FirmCliCommand::Query { query } => {
            commands::query_entities(&workspace_path, query, cli.format)
        }
        FirmCliCommand::Source {
            target_type,
            target_id,
        } => commands::find_item_source(&workspace_path, target_type, target_id, cli.format),
        FirmCliCommand::Check { file } => commands::check::check(&workspace_path, file),
        FirmCliCommand::Lsp => commands::lsp::serve(&workspace_path),
        FirmCliCommand::Mcp => commands::mcp::serve(&workspace_path),
    };

    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, Default)]
pub enum OutputFormat {
    #[default]
    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"),
        }
    }
}

/// 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 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);
    }
}

/// Outputs a list of strings (one per line for pretty, array for JSON).
pub fn list_output(items: &[&str], format: OutputFormat) {
    match format {
        OutputFormat::Pretty => {
            for item in items {
                println!("{}", item);
            }
        }
        OutputFormat::Json => json_output(&items),
    }
}

/// Outputs raw text (e.g., file paths, simple values).
pub fn raw_output(text: &str) {
    println!("{}", text);
}

/// 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.5.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,
            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,
    Enum,
}

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"),
            FieldType::Enum => write!(f, "Enum"),
        }
    }
}

/// 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),
    Enum(String),
}

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()),
            FieldValue::Enum(val) => write!(f, "{}", val),
        }
    }
}

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,
            FieldValue::Enum(_) => FieldType::Enum,
        }
    }

    /// 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);
    }

    #[test]
    fn test_enum_field_value() {
        let enum_field = FieldValue::Enum("active".to_string());
        assert_eq!(enum_field.get_type(), FieldType::Enum);
        assert!(enum_field.is_type(&FieldType::Enum));
    }

    #[test]
    fn test_enum_serialization() {
        let field = FieldValue::Enum("customer".to_string());
        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/access.rs

```rs path="/firm_core/src/graph/access.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, Direction::Outgoing)
                        .map(|edge| &self.graph[edge.target()])
                        .collect(),
                    Some(Direction::Incoming) => self
                        .graph
                        .edges_directed(*node_index, 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, Direction::Outgoing)
                                .map(|edge| &self.graph[edge.target()]),
                        );

                        // Collect sources of incoming edges
                        all_entities.extend(
                            self.graph
                                .edges_directed(*node_index, 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()
                        && 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/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/query/mod.rs

```rs path="/firm_core/src/graph/query/mod.rs" 
//! Query engine for executing queries against the entity graph
//!
//! This module provides a complete query execution system with:
//! - Filter conditions for matching entities
//! - Query operations (where, related, order, limit)
//! - Query execution against the entity graph

mod aggregation;
mod filter;
mod order;
mod query_errors;
mod related;
mod types;

// Re-export all public types
pub use filter::*;
pub use query_errors::*;
pub use related::*;
pub use types::*;

```

## /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_lang/src/parser/mod.rs

```rs path="/firm_lang/src/parser/mod.rs" 
pub mod dsl;
pub mod query;

```


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!