# Workflow: import → diff → deploy

The CLI manages configuration as files so you can review, edit, version, and
promote it between environments.

## 1. import — snapshot live config to files

```bash
tllt import --profile acme-prod
```

Writes to `./{profile}/{api}/{Entity}/...` in the **current directory** (override
with `--dir <path>`), so the config can live in its own git repo. Volatile fields (`id`,
`createdAt`, `updatedAt`) are stripped, so a content difference always reflects
a real configuration change. Each leaf folder gets a `_meta.json` recording how
to route writes (api, microservice, endpoint, and for scheduler the dataTemplate).

### Snapshot layout

```
acme-prod/            # written into the current directory (or --dir)
  do/
    Asset/
      _meta.json                 # routing: {api:"do", microservice:"core", endpoint:"assets", entity:"Asset"}
      _schema.json               # distilled, agent-readable contract (see schemas.md)
      _schema.source.json        # raw authoritative schema (DO dynamic-UI model)
      Filler 1.rdata
    ActivityTemplate/
      Pre start check/
        ActivityTemplateItem/
          _meta.json             # nested entities carry their own meta
          CostImpact.rdata
  scheduler/
    DataTemplate/
      Line A.rdata
    Line A/                      # scheduler config is nested per dataTemplate
      Equipment/
        _meta.json               # {api:"scheduler", endpoint:"equipment", entity:"Equipment", dataTemplate:"Line A"}
        Filler.rdata
```

The **filename is the natural key** (the record's `name`, or a composite for
some entities). This is what `diff`/`deploy` match on across environments.
Sidecar files (any `_*.json`) are schema/routing metadata and are ignored by
`diff`/`deploy`.

### RData records

Import writes one `.rdata` file per record, preserving the existing folder layout
and natural-key filename. Routing and schema sidecars remain JSON. For example:

```rdata
data Asset {
  { name: "Filler 1"  active: true  assetClass: {"name": "Bottling"} }
}
```

`diff` and `deploy` parse these files back into JSON records for the existing
DO/Scheduler write APIs. Legacy `.json` records are still readable; equivalent
JSON and RData snapshots compare equal. Changeset `rel` fields retain a logical
`.json` suffix for compatibility, regardless of the physical file extension.
Refreshing a record replaces its old JSON file with RData. Keeping both formats
for the same identity is an error.

You can also place several rows in an `Entity.rdata` file in the entity's existing
snapshot folder (with its `_meta.json`). Each row must have a distinct natural
key. Keep nested entities in their existing parent folders. One data block is
allowed per snapshot file; an empty block represents no records.

Supported values are quoted strings, numbers, booleans, null, bare enum values,
and JSON objects/arrays. Comments and `defaults { ... }` are supported. Mattec-style
`assetClassId: @AssetClass[name="Bottling"]` references are converted to
`assetClass: {"name":"Bottling"}` using relationships in the adjacent
`_schema.json`; missing mappings fail explicitly. Import emits relationship
objects as JSON values, retaining the API's field names and payload shape.
RDSL parameters must be compiled before use; `moved` clauses are rejected (use
`tllt rename` for supported live renames). Numbers outside JavaScript's safe
integer range are rejected rather than silently rounded.

### Compatibility with the platform's RData (WMS, APS)

Files follow the RData grammar of the platform toolchain (`faos/rdsl`), the one
the WMS and APS models use. Whatever tllt writes parses there unchanged, and
tllt reads platform files the same way. This has been checked against
`cdm-v0.89.0`, APS's pinned rdsl commit and `development`.

- Strings use the platform's escapes: `\"`, `\\`, `\n`, `\r`, `\t`, `\b`, `\f`,
  `\v` and `\0`. Any other `\x` reads as `x`. There is no `\u`, so other control
  characters (a GS1 separator, say) are written as is.
- A field named `data`, `moved`, `true`, `false` or `null` can't be an RData
  key. Import keeps such a record as JSON, as it does for any record RData can't
  hold, and warns.
- Known platform issue: rdsl currently reads a decimal whose fraction starts
  with zero (`0.05`, `1.007`) as if the zeros weren't there (`0.5`, `1.7`).
  tllt reads them correctly.

## 2. diff — compute a changeset

```bash
tllt diff --from acme-stage --to acme-prod
```

Produces the changes needed to make `to` look like `from`:

- **create** — in `from`, not in `to`
- **update** — in both, content differs; `fields` lists each changed top-level
  field as `{field, before, after}` (target value, desired value)
- **delete** — in `to`, not in `from`

Entity/endpoint routing for each change is resolved from the nearest `_meta.json`,
so nested entities are attributed correctly. Save with `--out plan.json`.

## 3. deploy — apply the changeset

```bash
tllt deploy --from acme-stage --to acme-prod --dry-run   # preview
tllt deploy --from acme-stage --to acme-prod --yes       # apply
tllt deploy --changeset plan.json --to acme-prod --yes   # from a saved plan
```

- `create` → `POST` the record. `update` → `PUT /{id}`. `delete` → `DELETE /{id}`.
- Target **ids are resolved by natural key**: deploy fetches the live collection
  and matches on `name` (or `code`), since ids aren't in the snapshot.
- **Deletes require `--prune`** so a partial snapshot can't wipe live config.
- **Nothing is written until everything validates.** Deploy resolves every
  target collection first and checks each change: required fields present,
  every reference resolvable (live, or created earlier in the same deploy),
  every update/delete target live. If anything fails, nothing is applied and
  all failures are reported together with a `code` and `detail`. Pass
  `--allow-partial` to apply the valid changes anyway.
- **Writes run in reference order**: a DO record is written after the records
  it references, including new records of the same entity (a new parent before
  its new children), whatever the folder order; scheduler follows its
  descriptor order. Deletes run last, in exactly the reverse order.
- `--dry-run` needs no credentials (it only reads local snapshots).
- `--reconcile` re-imports what changed and folds server normalization back
  into the `--from` snapshot. A value the target did not keep is kept in the
  snapshot and reported (`notApplied`), and the deploy fails, so the next diff
  still shows it. A record the server re-keyed takes the live name; one the
  import filter hides (an inactive Asset) is dropped from the snapshot, as a
  re-import would.

## Limitations to be aware of

- Update/delete matching is by `name`/`code`. Entities whose natural key is a
  **composite** (e.g. `ActivityAssignment`, some activity sub-entities) may not
  resolve a live id for update/delete; create still works.
- Records in a scheduler **dataTemplate created or moved to another location
  by the same deploy** are checked for required fields only; their references
  are resolved when they are written, after the template exists there.
- A **reference cycle** between new records (A's parent is B, B's parent is A)
  passes validation but can't be written in one run.
