Metadata-Version: 2.3
Name: ontology-builder-cli
Version: 0.0.1
Summary: CLI for the OntologyEngine REST API — build, validate, and run ontology graphs
Author: radu-mocanu
Author-email: radu-mocanu <radumocanu15@gmail.com>
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Requires-Dist: httpx>=0.28.1
Requires-Dist: pydantic>=2.13.3
Requires-Dist: pyyaml>=6.0.2
Requires-Dist: rich>=15.0.0
Requires-Dist: typer>=0.25.1
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# ontology-builder-cli

A typed Python CLI for the OntologyEngine REST API. Designed to be driven by
agents (Claude Code, scripts, CI) more than by humans, but works fine for both.

## What is this

`ontology-builder-cli` is a thin httpx + Pydantic + Typer wrapper around the
OntologyBuilder backend at `http://localhost:5286`. Every resource the BE
exposes (projects, nodes, edges, routing rules, assertions, validation, runs)
has a corresponding command group, and every wire-format quirk (camelCase,
int-encoded enums, JSON-string fields, discriminated condition trees) is
hidden behind named flags and string enum choices.

There is no authentication today — the BE has no auth scheme registered.
Run `ontology-cli auth check` to verify this against your environment.

## Install

### From PyPI

```bash
pip install ontology-builder-cli
# or
uv add ontology-builder-cli
```

### From source

```bash
cd OntologyBuilderSkills
uv sync
uv run ontology-cli --help
```

`uv` creates a `.venv` in the project root. Use `uv run ontology-cli ...`
or activate the venv directly.

## Configuration

| variable | default | meaning |
| --- | --- | --- |
| `ONTOLOGY_API_URL` | `http://localhost:5286` | Base URL of the BE. |
| `--base-url` flag  | (env or default) | Per-invocation override. |

## Output formats

Every read or write command supports `--json`. Without it, the CLI prints
a Rich panel/table for humans. With it, you get raw JSON to stdout — that
is the form agents and scripts should use:

```bash
PID=$(uv run ontology-cli project create --name demo --json | jq -r .id)
```

## Errors

API errors are caught and printed as `HTTP <status> <method> <url>` followed
by the BE's `code` and `message` (when present). Exit code is `1` for
4xx/5xx, `0` for success, `2` for the `validate` command when error-severity
issues exist.

The BE returns stable string error codes you can branch on:

- Routing-rule conditions: `CONDITION_UNKNOWN_FIELD`,
  `CONDITION_OPERATOR_NOT_SUPPORTED`, `CONDITION_VALUE_SHAPE_MISMATCH`,
  `CONDITION_VALUE_TYPE_MISMATCH`, `CONDITION_EMPTY_GROUP`,
  `CONDITION_NESTING_TOO_DEEP`.
- Output value rules: `OUTPUT_VALUE_EMPTY_RULE`, `OUTPUT_VALUE_MISSING_DEFAULT`,
  `OUTPUT_VALUE_DEFAULT_NOT_LAST`, `OUTPUT_VALUE_TYPE_MISMATCH`,
  `OUTPUT_VALUE_CONDITION_INVALID`.
- Assertions: `ASSERTION_EXPECTATIONS_CONFLICT` (terminal + coverage expectations combined),
  `ASSERTION_PATH_NODE_NOT_FOUND` (unknown node id in `expectedPath`),
  `ASSERTION_EXPECTED_OUTPUT_UNKNOWN_FIELD` (node or output name in `expectedNodeOutputs`
  not declared in the project).
- Nodes: `DUPLICATE_IO_NAME` (two inputs or two outputs on the same node share a name).
- Runs: `RUN_BLOCKED_BY_STATIC_ERRORS`, `RUN_NO_ASSERTIONS`,
  `RUN_NO_ENTRY`, `RUN_AMBIGUOUS_ENTRY`.
- Validation issue codes (returned in `validate` results, not as errors):
  `EDGE_INTEGRITY`, `REFERENCE_INTEGRITY` (rule's target node has no
  edge from the source — common when modelling error routes; create a
  sink node and an edge first), `ROUTING_COMPLETENESS`, `UNREACHABLE_NODE`,
  `CYCLE_NO_EXIT`, `CONDITION_INTEGRITY`.

## Command map

```
ontology-cli
├── blueprint compile                write a .ont YAML, compile + import (preferred for new graphs)
├── auth check                       probe whether auth is enforced
├── project list / get / create / patch / delete / export / import
├── node    list / get / create / delete         (per project)
├── edge    list / create / delete               (per project)
├── rule    list / create / delete               (per node)
├── assertion list / create / delete             (per project)
├── imports list / get / cancel                  (project import jobs)
├── run     start / list / get                   (per project)
└── validate                                     (per project)
```

Use `--help` on any subcommand to see flags. Every command that takes
project-scoped IDs takes them as positional arguments in the order they
appear in the URL path.

## Quickstart: build an ontology from a `.ont` blueprint (preferred)

The blueprint workflow is the recommended path for creating new
ontologies — especially for AI agents. You write one YAML file with
human-readable names, no UUIDs, and the CLI handles ID generation,
expression parsing, and upload in a single command.

```bash
cat > auth_flow.ont <<'EOF'
name: AuthFlow

nodes:
  - name: entry
    inputs:
      - {name: user_type, type: string, values: [admin, guest]}
  - name: dashboard
  - name: public_view

edges:
  - {from: entry, to: dashboard,   label: to admin}
  - {from: entry, to: public_view, label: to guest}

rules:
  - {node: entry, to: dashboard,   when: "user_type == 'admin'"}
  - {node: entry, to: public_view, default: true}

assertions:
  - {name: admin, given: {user_type: admin}, expect: dashboard}
  - {name: guest, given: {user_type: guest}, expect: public_view}
EOF

uv run ontology-cli blueprint compile auth_flow.ont --import
```

This compiles the YAML to a valid `.ob`, posts it to the BE, polls
until the import completes, and prints the new `projectId`. See the
`build-ontology` skill in `skills/build-ontology/SKILL.md` for the
full `.ont` reference (expressions, error routes, assertion
semantics).

## Advanced: build an ontology by individual REST calls

If you need to update an existing project, or your tooling can't write
files, the imperative path is still available. The pattern is: create
project → create nodes → create edges → create rules → create
assertions → validate → run.

```bash
# 1. Project
PID=$(uv run ontology-cli project create --name "auth-flow" --json | jq -r .id)

# 2. Two nodes. The first declares an input we'll branch on.
cat > inputs.json <<'EOF'
[{"name":"user_type","type":"string","required":true,"values":["admin","guest"]}]
EOF
cat > outputs.json <<'EOF'
[{"name":"route","type":"string","required":true}]
EOF

CLASSIFY=$(uv run ontology-cli node create $PID \
  --label "Classify" --inputs @inputs.json --outputs @outputs.json --json | jq -r .id)
DASHBOARD=$(uv run ontology-cli node create $PID --label "Dashboard" --json | jq -r .id)

# 3. Edge connecting them.
EDGE=$(uv run ontology-cli edge create $PID --source $CLASSIFY --target $DASHBOARD --json \
  | jq -r .id)

# 4. Routing rule: when user_type == "admin", route to Dashboard.
#    The fieldId is the input id from the Classify node — read it back:
FIELD=$(uv run ontology-cli node get $PID $CLASSIFY --json | jq -r '.inputs[0].id')

cat > condition.json <<EOF
{"kind":"clause","fieldId":"$FIELD","compare":0,"value":{"type":"literal","value":"admin"}}
EOF

uv run ontology-cli rule create $PID $CLASSIFY \
  --target $DASHBOARD --condition @condition.json --order 0 --json

# 5. Assertions: given user_type=admin, expect the run to reach Dashboard.
cat > given.json <<'EOF'
{"user_type":"admin"}
EOF

# Terminal expectation — assert the run ends at Dashboard.
uv run ontology-cli assertion create $PID \
  --name "admin -> dashboard" --scope e2e \
  --given @given.json --expected-target-node $DASHBOARD --json

# Coverage expectation — assert the path and what Dashboard produced.
cat > path.json <<EOF
["$DASHBOARD"]
EOF
cat > outputs.json <<EOF
{"$DASHBOARD": {"route": "admin"}}
EOF

uv run ontology-cli assertion create $PID \
  --name "admin path + outputs" --scope e2e \
  --given @given.json --expected-path @path.json --expected-node-outputs @outputs.json --json

# 6. Validate, then run.
uv run ontology-cli validate $PID --json
uv run ontology-cli run start $PID --json
```

Notes on PowerShell: avoid passing JSON as an inline string — PowerShell
mangles double quotes. Always use `@file.json` for `--inputs`, `--outputs`,
`--condition`, `--given`, `--expected-path`, `--expected-node-outputs`, and
`--expected-side-effects`. To capture an `id`
from `--json` output in PowerShell:

```powershell
$pid = (uv run ontology-cli project create --name demo --json | ConvertFrom-Json).id
```

### Modelling error routes

`--error-route` does **not** create a new sink — it only marks a rule
as fired by a raised error. You still need a real target node with an
outbound edge from the source. Create a terminal node per error class
(e.g. `BadPasswordError`), add an edge from the raising node to it,
then create the rule with `--error-route --error-name "bad_password" --target $ERR_NODE`.
Otherwise `validate` returns `REFERENCE_INTEGRITY`.

## Bulk create via import

To seed a whole project at once, assemble an `.ob` file (raw UTF-8 JSON
matching `ExportDocumentV1`, documented in `BE-done.md` § Project Export)
and upload it:

```bash
# Fire-and-forget — returns the importJobId immediately.
uv run ontology-cli project import path/to/project.ob --name "optional-rename"

# Wait for the job to finish and print the final header + logs.
uv run ontology-cli project import path/to/project.ob --wait --json
```

Inspect or cancel jobs through `ontology-cli imports`:

```bash
uv run ontology-cli imports list --json
uv run ontology-cli imports list --project-id <pid> --json
uv run ontology-cli imports get <jobId> --json
uv run ontology-cli imports cancel <jobId>
```

To round-trip an existing project, `project export <id>` writes a `.ob`
file you can re-`import`. Pass `--out` to override the filename.

### Known gotchas

- **`--expected-target-node` conflicts with `--expected-path` / `--expected-node-outputs`.**
  Combining them on the same assertion returns `ASSERTION_EXPECTATIONS_CONFLICT`. Use one
  or the other, or omit `--expected-target-node` when using coverage expectations.
  `--expected-error` can combine freely with either style.
- **`--given` scoped overrides for downstream stubs.** For E2E assertions, keys of the
  form `"NodeLabel.outputName"` in the Given JSON inject a fixed output value when the
  engine reaches that node — no separate stub command needed. The BE warns with
  `NODE_LABEL_NOT_UNIQUE` if two nodes share a label and a scoped key is ambiguous.
- **Node-scoped assertions still enter at the graph's entry node.**
  `--target-node` only narrows what is asserted; the engine still walks
  from the entry. Your `--given` payload must include the entry node's
  inputs even when targeting a downstream node.
- **`rule delete` is `delete <project-id> <node-id> <rule-id>`** (three
  positional args, no `--json`). It is the only resource where delete
  takes a parent id beyond the project.

## Reference: condition trees

Routing rules and (per-output) value rules use the same discriminated tree:

```jsonc
// Single comparison
{ "kind": "clause", "fieldId": "<input-uuid>", "compare": 0,
  "value": { "type": "literal", "value": "admin" } }

// AND/OR group
{ "kind": "group", "op": 0, "children": [<clause>, <clause>] }
```

`op`: `0` = AND, `1` = OR.

`compare` codes:

| code | name        | requires value          | valid types        |
| ---- | ----------- | ----------------------- | ------------------ |
| 0    | eq          | literal                 | any                |
| 1    | neq         | literal                 | any                |
| 2    | gt          | literal                 | number, date       |
| 3    | gte         | literal                 | number, date       |
| 4    | lt          | literal                 | number, date       |
| 5    | lte         | literal                 | number, date       |
| 6    | contains    | literal                 | string             |
| 7    | startsWith  | literal                 | string             |
| 8    | endsWith    | literal                 | string             |
| 9    | isEmpty     | (omit `value`)          | string, array      |
| 10   | isNotEmpty  | (omit `value`)          | string, array      |
| 11   | isNull      | (omit `value`)          | any                |
| 12   | isNotNull   | (omit `value`)          | any                |
| 13   | in          | list                    | any                |
| 14   | notIn       | list                    | any                |

Max nesting depth is 4. Empty groups are rejected.

## Reference: enums on input

The CLI accepts these as named strings (lowercase). They map to the BE's
int wire format internally.

| flag                       | choices                                          |
| -------------------------- | ------------------------------------------------ |
| `node create --mode`       | `simulated` (default), `implemented`             |
| `assertion --scope`        | `e2e` (default), `node` (coming soon)            |
| `rule --async-behavior`    | `immediate` (default), `delayed`, `conditional`  |

## Reference: enums in responses

Responses contain bare ints. Decode them with the table from `BE-done.md`
(also mirrored in `src/ontology_builder_cli/enums.py`):

- `node.mode`: 0 simulated, 1 implemented
- `assertion.scope`: 0 node, 1 e2e
- `rule.asyncBehavior`: 0 immediate, 1 delayed, 2 conditional
- `validation issue.severity`: 0 error, 1 warning
- `run.status`: 0 running, 1 passed, 2 failed, 3 errored
- `runStep.kind`: 0 assertionStarted, 1 nodeEntered, 2 nodeExited,
  3 edgeTraversed, 4 assertionFinished, 5 error
- `runStep.payload[outcome]` (when kind=4): 0 passed, 1 failed, 2 errored

## Project layout

```
OntologyCli/
├── pyproject.toml
├── README.md
└── src/ontology_builder_cli/
    ├── __init__.py
    ├── cli.py              # root Typer app + auth probe
    ├── client.py           # httpx wrapper, one method per BE route
    ├── enums.py            # IntEnums mirroring the BE wire format
    ├── models.py           # Pydantic models, camelCase aliases
    ├── _io.py              # printing, JSON loading, error decorator
    └── commands/
        ├── projects.py
        ├── nodes.py
        ├── edges.py
        ├── rules.py
        ├── assertions.py
        └── runs.py
```

Each file is small enough to read at a glance. Wire-format quirks live in
exactly one place (`models.py` aliases, `enums.py` ints), so the CLI surface
stays clean.
