docs← Back to article

Markdown for LLMs

Cases and processes

The source Markdown for this article. Copy it into your assistant or download it as a text file.

Download this articlePlain text ↗
# Cases and processes

A case is not a bag of facts. It is a journal of proposals: each fact is proposed, selected assertions are accepted, revocations remove an assertion while keeping its history, and replacements link the new proposal to the revoked one. Dependent answers are rechecked after every change, and work resumes from the journal, never from memory. Some canons also model procedures — stages a matter passes through and transitions guarded by conditions; a process agent records what happened in an event journal and asks the engine what each event established.

This page walks one case trace on the CLI, then one procedure replay. The rule that ties both halves together: **a computed transition is not a performed act.**

## A case trace: propose, accept, replace, recheck, diff

Status: ran locally (`law 0.1.1`, local CLI from a full checkout, on a scratch copy; see the Validation record).

The trace runs on a scratch copy of the GUM worked-example project (`otchet-neopredelennosti`, package `examples.jcgm_gum.otchet_neopredelennosti`, the same project as in [Follow a task guide](/agent-engineering/task-guides/)), case `KVybranChelovekom`, with the saved input-count query `chislo-vkhodov`. The editing commands require lock version 0.2 with a `resources` array, which the committed example (lock 0.1) does not have, so the scratch copy carries `"lockVersion": "0.2"` and `"resources": []`. Everything else is the committed project.

A lab note arrives first. Documents enter with a positional file argument; `--as` must be an identifier:

```bash
law case document add incoming/note-n7.txt --case KVybranChelovekom \
  --as note_n7 --type jcgm.gum::Input \
  --observed 2026-10-01T09:00:00+05:00 --recorded 2026-10-01T09:05:00+05:00
# {"case":"KVybranChelovekom","contentHash":"sha256:bc8999e9…",
#  "evidence":"note_n7","id":"KVybranChelovekom-note_n7",
#  "path":"resources/documents/note-n7.txt"}
```

Baseline the query with a saved answer (revision 1: two inputs), then propose the note's fact — a candidate third input; entity arguments are bare URNs:

```bash
law ask . --case KVybranChelovekom --query-json queries/chislo-vkhodov.json --out r1
# input_count → 2 (COMPUTED)

law case facts propose --case KVybranChelovekom --predicate input_of \
  --arg 'r=urn:example:gum:r' --arg 'x=urn:example:gum:x3' \
  --by urn:law:operator/agent-eng --at 2026-10-04T10:00:00+05:00
# {"added":["p-0001"],"case":"KVybranChelovekom",
#  "package":"examples.jcgm_gum.otchet_neopredelennosti",
#  "path":"analysis/proposals/KVybranChelovekom.json"}
```

The proposal changes nothing by itself. Acceptance takes the proposal id as a positional argument and appends the assertion to the case file:

```bash
law case facts accept p-0001 --case KVybranChelovekom \
  --by urn:law:operator/agent-eng --at 2026-10-04T10:05:00+05:00
# {"case":"KVybranChelovekom","id":"p-0001","newId":null,"status":"accepted"}

law ask . --case KVybranChelovekom --query-json queries/chislo-vkhodov.json --out r2
# input_count → 3 (COMPUTED)

law case diff r1 r2
# answers: [chislo-vkhodov: changed (2 → 3)]
```

Review finds the identifier mistyped: the input is `x4`, not `x3`. Replacement revokes the old assertion and proposes the new fact in one transaction, linked by `newId`:

```bash
law case facts replace p-0001 --case KVybranChelovekom --predicate input_of \
  --arg 'r=urn:example:gum:r' --arg 'x=urn:example:gum:x4' \
  --by urn:law:operator/agent-eng --at 2026-10-04T10:20:00+05:00 \
  --reason 'note identifier corrected: x3 -> x4'
# {"case":"KVybranChelovekom","id":"p-0001","newId":"p-0002","status":"replaced"}
```

The replacement is a proposal, not an acceptance: the old assertion is gone while the new one is not yet in. Asking before the separate acceptance shows it:

```bash
law ask . --case KVybranChelovekom --query-json queries/chislo-vkhodov.json --out r2b
# input_count → 2 (COMPUTED): the replacement alone asserts nothing
```

The journal shows the succession: `p-0001` carries `accepted` then `replaced` events (the latter with the reason and `"replaces": "p-0002"`); `p-0002` waits with no events. Only a separate acceptance admits it:

```bash
law case facts accept p-0002 --case KVybranChelovekom \
  --by urn:law:operator/agent-eng --at 2026-10-04T10:25:00+05:00
# {"case":"KVybranChelovekom","id":"p-0002","newId":null,"status":"accepted"}

law ask . --case KVybranChelovekom --query-json queries/chislo-vkhodov.json --out r3
# input_count → 3 (COMPUTED)
```

Recheck reruns every saved question of the case, and the closing diff compares the two accepted states:

```bash
law case check --case KVybranChelovekom --json
# 16 questions, each COMPUTED (chislo-vkhodov → 3, chislo-vkladov → 2, …)

law case diff r2 r3
# answers: [chislo-vkhodov: same (3 → 3)]; document/result hashes differ
```

The last diff is the point of the lifecycle: equal derived values, different bytes. The journal event is real even when the value does not move, and only the recheck plus the diff together say what a change did.

Two observed limits of the editing path. Facts with `Quantity` arguments propose but do not accept: `LPK-E0804: term type urn:law:std#Quantity not supported by case printing`. And the short predicate form resolves against the case world (`--predicate input_of`), while the `Pkg::name` form is refused as undeclared there.

## Propose, accept, revoke, replace

Facts arrive in two ways: typed values submitted from a file, or a single fact entered against the predicate's declared parameter types. The proposal store keeps one journal file per case:

```text
law case document add <file> --as <name> …
law case facts propose --from <file> | --predicate P --arg name=value …
law case facts accept <id>
law case facts revoke <id> --reason …
law case facts replace <id> --predicate P --arg name=value …
```

The trace executed `document add`, `facts propose`, `facts accept` and `facts replace` (plus the `ask`, `check` and `diff` reads); `facts revoke` and `facts propose --from` are NOT RUN here (reproduction: the same scratch copy, the commands above).

Keep four events apart. A pinned document alone asserts nothing. A proposal is a journal state that moves no derivation. Acceptance is what moves a derivation. Admission is a different event again: the evidence policy's decision about a support, not this journal's decision about a fact (see [Collect facts and evidence](/agent-engineering/collect-facts/)).

**SDKs.** Node (`openCasePackage(dir)` from `@arxo/law/case-package`) and Python (`add_document` and siblings) open a case package from its directory and run the same commands through the same package transaction as the CLI. They shell out to the `law` binary — taken from the `law` option, the `ARXO_LAW_BINARY` setting, the bundled CLI package or the system path — so the bytes match the CLI for the same input. Status: labeled pseudocode from the published Guide shapes, NOT RUN here.

```ts
openCasePackage(dir)      // Node: @arxo/law/case-package
addDocument               // Python: add_document, etc.
proposeFacts
acceptFact
revokeFact
replaceFact
check
```

**MCP.** Questions against a case package go through `law_case_ask` with a saved question identifier or a card plus arguments. Case edits belong to the `editing` facet (`law_case_document`, `law_case_propose`, `law_case_facts`, `law_case_decide`, `law_case_check`), which the [MCP tool reference](/guide/mcp-tools/) documents as present only in the author's local slice; public profiles refuse it with a reason. `by` and `at` are the operator's, and the server never reads the machine clock. Status: NOT RUN — the session's MCP server was unreachable; confirm the facet through the server's own `tools/list` before scripting against it.

A shape verified on the CLI is not thereby verified on the SDK or MCP. No symmetry is claimed beyond the documented fact that the SDKs run the CLI binary and the MCP editing facet uses the same package transaction.

## History, recompute, resume

**History is append-only from the agent's side.** Every proposal, acceptance, revocation and replacement is an event in the journal; revisions are answer directories produced by `law ask --out`, and two revisions compare with `law case diff`. The agent never rewrites a past event to make the present look consistent. Corrections are new events — a revocation or a replacement — with their own timestamps and reasons.

**Recompute after every change.** Saved questions rerun over the new journal state, and each answer is inspected, not assumed fresh. An agent that accepted a fact and skipped the recheck is reporting stale derivations as current; the lifecycle forbids that shortcut.

**Resume from the journal, not the chat.** The agent reopens the package, lists saved questions, checks their stored answers against the current journal, and continues from the first step whose inputs changed or whose answer is missing. Two aids:

- The "what was known on the date" projection (`law case check --as-of`, `asOf` on `law_case_check`) evaluates the journal at a past moment in a temporary copy without changing the package. Status: NOT RUN here.
- The task-guide journal replays each recorded answer and renders the document from current entries, so a paused report resumes with verification, not trust ([Follow a task guide](/agent-engineering/task-guides/)).

An answer that depended on an assumption carries that assumption in its record, and resumption re-presents the condition rather than silently inheriting the conclusion. Handing a case to another agent or person is an application concern: the platform provides no native handoff envelope or merge — the case package and its journal are what you pass.

## Procedures: read the model before walking it

Status: ran locally (`law engine process` and `law engine process-run`, local CLI from a full checkout, on the pinned compiled snapshot of the package).

The model reads through `law engine process`; the journal replays through `law engine process-run`. The MCP twins `law_process` and `law_process_run` are the same engine behind the assistant interface (NOT RUN here; see the [MCP tool reference](/guide/mcp-tools/)).

`process` prints each procedure's stages, transitions with their guard conditions, terminal stages and structural defects (stages no transition reaches, transitions that lead nowhere). Example: `kz-gosuslugi` (Law on State and Socially Responsible Services, No. 88-V), procedure `OkazanieUslugi`:

```text
Process layer (O-5): 1 procedures, 6 states, 7 transitions
  OkazanieUslugi(Zayavlenie) — initial Podano
    ObzhalovatOtkaz: OtkazanoVOkazanii → ObzhalovanieVeditsya (1 conditions)
    Okazat: VProizvodstve → UslugaOkazana (1 conditions)
    Otkazat: VProizvodstve → OtkazanoVOkazanii (1 conditions)
    OtkazatPosleNeustraneniya: Priostanovleno → OtkazanoVOkazanii (1 conditions)
    PrinyatZayavlenie: Podano → VProizvodstve (1 conditions)
    Priostanovit: VProizvodstve → Priostanovleno (1 conditions)
    Vozobnovit: Priostanovleno → VProizvodstve (1 conditions)
  no structural defects found
```

Read it as: from stage `Podano` the matter moves to `VProizvodstve` when the `PrinyatZayavlenie` guard is established, and so on. A stage with no outgoing transition is terminal-or-open: whether it ends the matter by design is not visible from the model alone.

## Run the procedure journal, frame by frame

`process-run` takes the compiled snapshot, the procedure, the instance (which application, lot or matter this is) and the journal: dated steps, each with an attempted transition, its instant and the facts asserted at that frame. The law applied to each frame is the law of that frame's date. The answer tells which stages were passed, which attempted transition had no legal effect, which duties were breached and which time limits run.

A successful run: the application is accepted, then the result is issued. Each attempt carries its instant (`time`) and its guard fact (`zayavlenie_prinyato`, then `rezultat_vydan`):

```text
Case run (O-5): procedure OkazanieUslugi,
instance urn:kz:case:gosuslugi:zayavlenie:1, frames 2, evaluate calls 43
  2026-03-06: states [Podano, VProizvodstve]
      now: [VProizvodstve]
      transition PrinyatZayavlenie: TRUE_ONLY — effect
  2026-03-25: states [Podano, UslugaOkazana, VProizvodstve]
      now: [UslugaOkazana]
      transition PrinyatZayavlenie: TRUE_ONLY — effect
      transition Okazat: TRUE_ONLY — effect
Verdict: CLEAN
```

A refusal on an unmet condition: same journal, but the second frame asserts nothing, so the `Okazat` guard `rezultat_vydan` is not established:

```text
  2026-03-25: states [Podano, VProizvodstve]
      now: [VProizvodstve]
      transition PrinyatZayavlenie: TRUE_ONLY — effect
      transition Okazat: NEITHER — NO EFFECT
  FINDING ATTEMPT_WITHOUT_EFFECT: step 1 (2026-03-25): transition attempt
  Okazat produced no legal effect — §164 attempted without valid
Verdict: FINDINGS
```

Per-frame axes lines are omitted from both excerpts; the verdicts and transition lines are verbatim. The refusal is an answer about the matter, not about the call: the attempt is recorded, the guard is silent, the matter stays where it was. One edge observed on the package's own journals: an attempt without `time` is not presented at all — each such step draws a `STEP_WITHOUT_TIME` finding and the matter stays at its initial stage. Date the attempt to the instant, not just the day.

## The agent's route is not the domain procedure

Two procedures are in play, and confusing them is the main failure mode of process agents:

| | Domain procedure | Agent action route |
|---|---|---|
| Defined by | the canon | the agent's task and tools |
| Steps | stages and guarded transitions | collect, ask, check, escalate, record |
| Moves when | the guard condition is established | the agent decides and acts |
| Recorded in | the event journal | the run log |

The agent route serves the domain procedure: it gathers the facts that may satisfy a guard, asks the engine, and appends the outcome to the journal. It never advances the domain procedure by itself — only an established guard does that, as computed by the engine over journal facts. An agent step "send the notice" may precede the domain transition whose guard is "notice sent", but the transition fires when the guard is established from journal evidence, not when the agent acts.

## A computed transition is not a performed act

When the replay says a transition fired, it says the model moved: given the journal, the matter now counts as being at the next stage. It does not say anyone did anything in the world. Keep three events apart:

- the external act (the notice was sent, the fee was paid);
- the journal entry recording it (date, attempt, asserted facts);
- the computed transition (the engine moved the matter to the next stage).

Each can fail without the others: the act without the entry never enters the model; the entry without the act computes a fiction the engine will happily walk; the computation without replay leaves the matter's position unknown. The agent's duty is the middle row — faithful, dated, provenance-marked entries — plus refusing to invent the other two. The same holds for case facts: an accepted fact is a recorded assertion, not proof that the event happened.

What the replay does not say:

- Passed stages mean "entered at some point", not "currently there": the replay states no single current position beyond what it prints.
- Available next steps are computed only when the call names the moment of the hypothetical attempt; without it, nothing is stated about what is available — time is not guessed.
- Duties and time limits named in the replay are positions computed from the journal, with the same conditionality as any answer: change the journal, change the outcome.

## How to verify

**Case trace.** Copy the worked-example project to scratch, replace its lockfile with the ready 0.2 lockfile, copy in the lab note, and replay: `document add`, `ask --out r1`, `facts propose`, `facts accept`, `ask --out r2`, `case diff r1 r2`, `facts replace`, `ask --out r2b` (expect the drop to 2), `facts accept` of the replacement, `ask --out r3`, `case check --json`, `case diff r2 r3`. Inputs and assembly commands: [examples/lifecycle/README.md](/agent-engineering/files/lifecycle/README.md) and [examples/task-guides/README.md](/agent-engineering/files/task-guides/README.md). The bundled note has replacement bytes, so expect a different `contentHash` than the one quoted above.

**Procedure replay.** From the checkout root:

```bash
./law engine process corpus/clir/kz-gosuslugi.lawir.json
./law engine process-run corpus/clir/kz-gosuslugi.lawir.json --journal <file>
```

Use [examples/process/journal-success.json](/agent-engineering/files/process/journal-success.json) for the `CLEAN` run and [examples/process/journal-refusal.json](/agent-engineering/files/process/journal-refusal.json) for the `ATTEMPT_WITHOUT_EFFECT` run. Replaying the package's own `ustranenie-i-okazanie` journal shows the `STEP_WITHOUT_TIME` edge. Both halves need the `law` CLI from a full checkout.

## Validation record

| Checked | Result | Version |
|---|---|---|
| Case trace on the bundle assembly (lock 0.2) | values 2 → 3 → 2 → 3; replacement `newId` as quoted; `check` 16/16 `COMPUTED` | `law 0.1.1`, `law.core/0.2`, 2026-10-04 |
| Editing refusals | lock without `resources` `LPK-E0805`; propose on lock 0.1 `LDC-E8704`; hyphen in `--as`; `Pkg::name` predicate; Quantity accept `LPK-E0804` | same |
| `engine process`, gosuslugi | model as quoted | same |
| `engine process-run`, two bundle journals | `CLEAN`; `ATTEMPT_WITHOUT_EFFECT`, `FINDINGS` | same |
| Package journal `ustranenie-i-okazanie` | `STEP_WITHOUT_TIME` on all four steps | same |
| `facts revoke`, `propose --from`, `check --as-of`, SDK calls, MCP (`law_case_*`, `law_process*`) | NOT RUN (MCP server unreachable) | — |

Previous: [Follow a task guide](/agent-engineering/task-guides/)
Next: [Safety and reliability](/agent-engineering/safety-and-reliability/)