# Doctrine: verb composition

When the user request needs more than one verb, chain native
NAC-3 verbs in a deterministic order. Don't fall back to
`yujin.shell.exec` to glue multiple operations together.

## The rule

For any composite operation, pick the chain of NAC-3 verbs that
produces the result, invoke them in the natural data-flow order,
and explain the result in prose. Never reach for shell as a
shortcut.

## Common chains

| Goal                                | Chain                                            |
| ----------------------------------- | ------------------------------------------------ |
| Read JSON file + inspect a field    | `yujin.fs.read` -> `yujin.json.parse` -> `yujin.json.query` |
| Parse URL + get one query param     | `yujin.url.parse` (its result already has the query map) |
| Compare two files                   | `yujin.fs.read` x2 -> `yujin.text.diff`          |
| Confirm Excel append landed         | `office.excel.append-sheet` -> `office.excel.list-sheets` |
| Summarize repo state                | `yujin.git.status` + `yujin.git.branch-list`     |
| Lookup outdated deps + classify     | `yujin.npm.outdated` -> prose explanation        |
| Read .docx outline + show section   | `office.word.read` (include_outline=true)        |

## Order matters

Some chains have a mandatory order because step N consumes step
N-1's output:

- `fs.read` MUST precede `json.parse` (parse needs the content).
- `list-sheets` MUST precede `excel.read` when the sheet name is
  unknown (read needs the right name).
- `url.parse` MUST precede `json.stringify` if the goal is "URL
  -> JSON" (stringify needs the parsed object).

The harness asserts these via `verb_invoked_in_order`. If you
invoke them out of order, the gate fails.

## Anti-patterns

- `yujin.shell.exec` with `cat | jq` -- use `fs.read` +
  `json.parse` + `json.query`.
- `yujin.shell.exec` with `git status` -- use `yujin.git.status`.
- `yujin.shell.exec` with `env` -- use `yujin.env.list` (and
  respect the secret-suppression rationale).
- Regex-extracting a single URL component from a string -- use
  `yujin.url.parse`.
- Hand-rolling a unified diff -- use `yujin.text.diff`.

## When you genuinely need shell

Some operations are still shell-only:
- Running arbitrary build commands (`pnpm test`, `yarn build`).
- Pinging external endpoints (`curl`, `wget`).
- OS-specific tooling not wrapped by a NAC-3 verb.

In those cases, invoke `yujin.shell.exec` directly. The classifier
will gate destructive / network / elevation commands behind the
approval modal; safe ones auto-approve.

## Long-running commands -> yujin.task.run (MANDATORY)

`yujin.shell.exec` runs SYNCHRONOUSLY: it blocks the turn and the
child process is killed when the turn ends. So a command that does NOT
terminate on its own -- a dev server, a watcher, `tail -f`, a tunnel
(`npm/pnpm run dev|start|serve|watch`, `expo start`, `vite`,
`next dev`, `nodemon`, `wrangler dev`, `docker compose up`,
`cloudflared tunnel`, ...) -- MUST be launched with `yujin.task.run`,
which runs it detached so it SURVIVES the turn. Follow it with
`yujin.task.status` / `yujin.task.list`; the user stops or deletes it
from the Tareas tab in the pizarron.

shell.exec ENFORCES this: it refuses such commands and points you to
`yujin.task.run` (error `long_running_must_use_task`). Only pass
`allow_long_running:true` in the rare case you truly need it in-turn.

## When to fetch this doctrine

Fetch when:
- The user request feels like it needs >=2 verbs.
- You're considering `yujin.shell.exec` for a data operation.
- You're not sure which verb to pick for a known intent (this
  doctrine's chain table covers the common cases).
