jaz-clio 5.6.6 → 5.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-api
3
- version: 5.6.6
3
+ version: 5.6.8
4
4
  description: >-
5
5
  Use this skill whenever you call, debug, or review code that touches the Jaz
6
6
  REST API. Covers field names, response shapes, 141 production gotchas, error
@@ -206,7 +206,7 @@ The rest of this skill — field names, gotchas, error catalog, dependency order
206
206
  ### Response Shape Gotchas
207
207
  66. **Contact boolean fields are `customer`/`supplier`** — NOT `isCustomer`/`isSupplier`. These are plain booleans on the contact object: `{ "customer": true, "supplier": false }`. Using `isCustomer` or `isSupplier` in code will be `undefined`.
208
208
  67. **Finalized statuses differ by resource type** — NOT `"FINALIZED"`, `"FINAL"`, or `"POSTED"`. Journals → `"APPROVED"`. Invoices/Bills → `"UNPAID"` (progresses to `"PAID"`, `"OVERDUE"`). Customer/Supplier Credit Notes → `"UNAPPLIED"` (progresses to `"APPLIED"`). All types support `"DRAFT"` and `"VOIDED"`. When creating without `saveAsDraft: true`, the response status matches the type's finalized status.
209
- 68. **Create/pay responses are minimal** — POST create endpoints (invoices, bills, journals, contacts, payments) return only `{ resourceId: "..." }` (plus a few metadata fields). They do NOT return the full entity. To verify field values after creation, you MUST do a subsequent `GET /:type/:resourceId`. Never assert on field values from a create response.
209
+ 68. **Create/pay responses are minimal by default** — POST create endpoints (invoices, bills, journals, contacts, payments) return only `{ resourceId: "..." }` (plus a few metadata fields). They do NOT return the full entity. To verify field values after creation, do a subsequent `GET /:type/:resourceId`. **MCP tool shortcut:** `create_invoice` / `create_bill` / `create_journal` / `create_contact` / `create_item` accept `returnFullEntity: true` — the executor performs the GET server-side and returns the full entity inline, saving a turn. The raw REST `POST` is still minimal-only; only the MCP tools collapse the round trip. **If the post-create GET fails** (transient 5xx, network blip), the tool returns the minimal create envelope augmented with `_hydration: { status: 'failed', resourceId, message }` — the write committed; the agent should retry only the `get_*` call, NEVER the create (would duplicate the document).
210
210
  69. **No `amountDue` field** — Invoices and bills do NOT have an `amountDue` field. To check if a transaction is fully paid, inspect the `paymentRecords` array: if `paymentRecords.length > 0`, payments exist. Compare `totalAmount` with the sum of `paymentRecords[].transactionAmount` to determine remaining balance.
211
211
  70. **Response dates include time component** — Even though request dates are `YYYY-MM-DD`, response dates are epoch milliseconds (see Rule 52). When comparing dates from responses, always convert with `new Date(epochMs).toISOString().slice(0, 10)` — never string-match against the raw epoch value. Remember: business dates are org-timezone (see Rule 52).
212
212
  71. **Items POST requires `saleItemName`/`purchaseItemName`** — When creating items with `appliesToSale: true` or `appliesToPurchase: true`, you MUST include `saleItemName` and/or `purchaseItemName` respectively. These are the display names shown on sale/purchase documents. Omitting them causes 422: "saleItemName is a required field". If not specified, default to the `internalName` value.
@@ -449,7 +449,17 @@ Bills, invoices, and credit notes share identical mandatory field specs. Adding
449
449
 
450
450
  142. **`capsuleRecipe` payload is mutually exclusive with `capsuleResourceId`** on trigger mutations (create/update of invoice, bill, journal, cash_in, cash_out). Use `capsuleRecipe` to CREATE a new capsule via the recipe engine; use `capsuleResourceId` to ATTACH a base-trx to an existing capsule. Sending both returns 422 (`excluded_with` validator).
451
451
 
452
- 143. **Capsule recipe publish is best-effort post-commit.** On success the trigger-mutation response carries `capsuleRecipeJob: { jobResourceId, capsuleResourceId, subscriptionFBPath, totalRecords, idempotentHit, recipeKey }` (verified live 2026-05-27). **Note `jobResourceId` (NOT `resourceId`)** on the trigger-mutation payload — this is the polling key. If the base-trx commits but the recipe publish fails, `capsuleRecipeJob` is null and the base-trx is still saved. Recovery: poll `search_background_jobs` filtered by **`baseTransactionResourceId`** NOT `capsuleResourceId` (the capsule may not exist yet if the recipe never started). The standalone `resume_capsule_recipe(capsuleResourceId)` response IS the full `CapsuleRecipeJob` shape with `resourceId` (distinct from the inline trigger-mutation payload).
452
+ 143. **Capsule recipe publish is best-effort post-commit — silent-null failure mode.** On success the trigger-mutation response carries `capsuleRecipeJob: { jobResourceId, capsuleResourceId, subscriptionFBPath, totalRecords, idempotentHit, recipeKey }` (verified live 2026-05-27). **Note `jobResourceId` (NOT `resourceId`)** on the trigger-mutation payload — this is the polling key. **On publish failure, `capsuleRecipeJob` is absent (or null) from the response, the trigger mutation STILL returns 201, the base-trx is committed, and NO error reason is surfaced to the caller.** The response echoes `capsuleRecipe.{recipeName, inputs}` back unchanged, which can look like success at a glance. **Three known causes** of silent null `capsuleRecipeJob` (must pre-validate before sending):
453
+ - **(a) Wrong `recipeName` for the base trx type** — every recipe is locked to `allowedBaseTransactionTypes` (e.g. PREPAID_AMORTIZATION = PURCHASE only; DEFERRED_REVENUE = SALE only; ACCRUAL_REVERSAL, IFRS16_LEASE = JOURNAL_MANUAL only; LOAN_AMORTIZATION = JOURNAL_DIRECT_CASH_IN or JOURNAL_MANUAL). On `preview_capsule_recipe`, mismatch surfaces as 422 `RECIPE_INVALID_BASE_TRANSACTION_TYPE`. **On the trigger mutation, it silently nulls the job** — the validation happens post-commit in customer-service and arap catches the exception. Always check `get_capsule_recipe(name).allowedBaseTransactionTypes` matches the trigger mutation you're calling.
454
+ - **(b) Currency mismatch** — see Rule 156 (single-currency v1 recipes — recipe `currency`, every `*AccountResourceId` account's `currencyCode`, and the base trx currency MUST all match). Mismatch surfaces as 422 `ERR_RECIPE_ACCOUNT_CURRENCY_MISMATCH` on `preview_capsule_recipe` but silently nulls on the trigger mutation.
455
+ - **(c) Wrong `x-accountClass` on an input field** — see Rule 157 (each `*AccountResourceId` slot has a required account class). Mismatch silently nulls; preview returns the matching `RECIPE_FIELDS_*` 422.
456
+
457
+ **Diagnosis sequence when the response has null/absent `capsuleRecipeJob`:**
458
+ 1. **Re-run `preview_capsule_recipe`** with the same `recipeName` + `inputs` (without a base trx). This is the canonical pre-flight: it returns the exact 422 reason — `ERR_RECIPE_ACCOUNT_CURRENCY_MISMATCH`, `RECIPE_INVALID_BASE_TRANSACTION_TYPE`, `RECIPE_FIELDS_MUST_DIFFER`, etc. Most reliable diagnostic — fix the input and retry the trigger.
459
+ 2. **Poll `search_background_jobs --filter '{"baseTransactionResourceId":{"eq":"<id>"}}'`** — if a `FAILED` job exists, its `errorDetails` has the publish failure reason. **If no job exists for the base-trx, the publish never queued** (validation rejected pre-queue in customer-service).
460
+ 3. **`resume_capsule_recipe(capsuleResourceId)`** is only available if a capsule WAS created — i.e. the recipe partially ran. For pre-queue rejections (3 causes above), no capsule exists; the only recovery is to re-issue the trigger mutation with corrected inputs.
461
+
462
+ Pre-flight gate (recommended for agents and integrations): always call `preview_capsule_recipe(recipeName, inputs)` before the trigger mutation. Preview is pure-compute (no side effects) and surfaces every input/account/currency problem with a clear error_type — eliminates the silent-null class entirely.
453
463
 
454
464
  144. **`recipeName` IS enum-constrained at the API layer** (verified live 2026-05-27): closed enum `LOAN_AMORTIZATION | ACCRUAL_REVERSAL | PREPAID_AMORTIZATION | DEFERRED_REVENUE | IFRS16_LEASE` on `POST /capsule-recipes/preview` and on `capsuleRecipe.recipeName` payloads on trigger mutations. Send a string not in the set → 422 validation_error. Don't hard-code the 5 values in motherboard descriptions — discover via `list_capsule_recipes` (the source of truth) and pass the discovered name through.
455
465
 
@@ -463,7 +473,7 @@ Bills, invoices, and credit notes share identical mandatory field specs. Adding
463
473
 
464
474
  149. **`rollback_capsule_recipe` returning `status=PARTIAL_ROLLBACK`** with `blockedAtomResourceIds[]` is safe to retry (rollback is idempotent on already-deleted atoms). Persistent partial-rollback typically indicates an atom is referenced downstream; escalate to ops if retry doesn't resolve.
465
475
 
466
- 150. **`preview_capsule_recipe` and trigger mutations return 422 `RECIPE_INVALID_BASE_TRANSACTION_TYPE`** if `baseTransactionType` is not in the recipe's `allowedBaseTransactionTypes` (see descriptor at `get_capsule_recipe`). Example: PREPAID_AMORTIZATION only allows PURCHASE; supplying SALE returns 422.
476
+ 150. **`preview_capsule_recipe` returns 422 `RECIPE_INVALID_BASE_TRANSACTION_TYPE`** when the recipe's `allowedBaseTransactionTypes` doesn't include the supplied base trx type (see descriptor at `get_capsule_recipe`). **The trigger mutation does NOT surface this 422 to the caller** — it silently nulls `capsuleRecipeJob` on the response (see Rule 143). The allowed types are: PREPAID_AMORTIZATION PURCHASE only; DEFERRED_REVENUE SALE only; ACCRUAL_REVERSAL → JOURNAL_MANUAL only; IFRS16_LEASE → JOURNAL_MANUAL only; LOAN_AMORTIZATION → JOURNAL_DIRECT_CASH_IN or JOURNAL_MANUAL. Always check `get_capsule_recipe(name).allowedBaseTransactionTypes` before sending `capsuleRecipe` on any trigger mutation.
467
477
 
468
478
  151. **Sending BOTH `capsuleRecipe` AND `capsuleResourceId`** on the same trigger mutation returns 422 (`excluded_with` validator — same lock as Rule 142). Pick one based on intent.
469
479
 
@@ -475,6 +485,14 @@ Bills, invoices, and credit notes share identical mandatory field specs. Adding
475
485
 
476
486
  155. **Pseudo-SQL schema is canonical — call `get_pseudo_sql_schema` before any query.** The response returns the live curated catalog (tables / columns / joins / functions) PLUS the canonical `jaz-pseudo-sql.md` skill body in `agentSkillsDoc.content`. Drop the `.md` body into context as the syntax-rules source; treat `tables[] / joins[] / functions[]` as the column-list source. **Cache contract:** the `version` field is a stable 16-char hex hash; within a session, cache by version and don't re-call unless you have reason to believe the schema changed (e.g. a fresh backend deploy mid-session). Don't re-fetch on a wall-clock timer (upstream is `private, no-cache, must-revalidate`). A static curated-schema snapshot used to ship with the `jaz-pseudo-sql` skill before v5.6.0; it was dropped because it was structurally guaranteed to drift. Never write a query from a memorized column list.
477
487
 
488
+ 156. **v1 capsule recipes are single-currency — `ERR_RECIPE_ACCOUNT_CURRENCY_MISMATCH`.** The recipe `currency` field, every `*AccountResourceId` account's `currencyCode`, and the base transaction's `currencyCode` ALL MUST match. Preview returns 422 `ERR_RECIPE_ACCOUNT_CURRENCY_MISMATCH` with a concrete message (e.g. "account X is denominated in USD but the recipe currency is SGD"). **Trigger mutation silently nulls `capsuleRecipeJob`** (Rule 143). Practical recipe: never hardcode `currency`; derive from `get_account(prepaidAssetAccountResourceId).currencyCode` (or the base trx currency) and use the same value as the recipe input. The recipe descriptor's `currency.x-baseTrxBinding: "strict"` field marks this — when present, the value is bound to (and must equal) `trx.currencyCode` post-commit. Caught by smoke runs since v5.5.0 — tests 65/66 hardcoded `SGD` and silently nulled against a USD fire-test org for 20+ hours.
489
+
490
+ 157. **Recipe input `*AccountResourceId` fields are account-class-locked — `x-accountClass` in inputSchema is authoritative.** Each `*AccountResourceId` slot on a recipe's input schema carries an `x-accountClass` constraint (`"Asset"`, `"Liability"`, `"Expense"`, `"Revenue"`, `"Equity"`). Passing an account whose class doesn't match the slot's `x-accountClass` is rejected post-commit and silently nulls `capsuleRecipeJob` (Rule 143). Schema location: `get_capsule_recipe(name).data.versions[0].inputSchema.properties.<fieldName>['x-accountClass']` — **note `versions[0].inputSchema`, NOT `inputSchema` at the top level**. Examples: PREPAID_AMORTIZATION needs `prepaidAssetAccountResourceId: Asset` + `expenseAccountResourceId: Expense`; DEFERRED_REVENUE needs `deferredRevenueAccountResourceId: Liability` + `revenueAccountResourceId: Revenue`; ACCRUAL_REVERSAL needs `expenseAccountResourceId: Expense` + `accruedLiabilityAccountResourceId: Liability`. Always pre-validate via `get_account(resourceId).accountClass` against the slot constraint, or just call `preview_capsule_recipe(recipeName, inputs)` to surface every class violation as a clean 422.
491
+
492
+ ### Error Recovery
493
+
494
+ 158. **Tool error envelopes may carry a structured `repair` suggestion (W1.3).** When a `create_*` / `update_*` / `get_*` tool fails with a high-confidence pattern (404 not found, 422 missing FK, duplicate reference, tax-profile direction mismatch), the response is enriched with `repair: { tool, arguments, reason }`. The `tool` field is ALWAYS read-only AND verified to exist in the registry. Consumption pattern: if `repair` is present, call `execute_tool(repair.tool, repair.arguments)` directly to recover; do NOT retry the original write tool until the repair surfaces the correct resourceId/reference. Full envelope shape, pattern list, and worked example in `references/errors.md` (Repair Suggestions section). Purely additive — no match → no `repair`, fall back to free-text `hint`. Infinite-loop protection comes from the agent-loop repetition guard (same tool+input retried 3× is blocked).
495
+
478
496
  ## Supporting Files
479
497
 
480
498
  For detailed reference, read these files in this skill directory:
@@ -858,3 +858,57 @@ Journals support a top-level `currency` object to create entries in a foreign cu
858
858
  ---
859
859
 
860
860
  *Last updated: 2026-03-13 — Added: Nano-classifier errors (classes field, double-wrapped GET), payment record errors (cashflow vs payment IDs), sub-resource raw array errors. Previous: Cash entry path migration, Quick Fix errors.*
861
+
862
+ ---
863
+
864
+ ## Repair Suggestions (W1.3)
865
+
866
+ When a Jaz tool call fails, the MCP `execute_tool` response (and the daemon-side executor return) may now carry a structured `repair` block alongside the existing `error` / `hint` fields. The block is built by the motherboard registry (`src/core/registry/repair-hints.ts`); the API server is unchanged.
867
+
868
+ ### Shape
869
+
870
+ ```json
871
+ {
872
+ "error": "lineItems[0].accountResourceId is required if [saveAsDraft] is false",
873
+ "status": 422,
874
+ "endpoint": "/api/v1/invoices",
875
+ "hint": "Validation error — check field values against the tool description.",
876
+ "repair": {
877
+ "tool": "search_accounts",
878
+ "arguments": {},
879
+ "reason": "Line item missing accountResourceId. Search for the right GL account first (e.g. by name or accountType), then retry with the resourceId on each line item."
880
+ }
881
+ }
882
+ ```
883
+
884
+ The `repair` field is OMITTED entirely when no high-confidence pattern matches. The agent then falls back to the free-text `hint` field (existing behavior).
885
+
886
+ ### Safety invariants
887
+
888
+ - `repair.tool` always starts with a read-only prefix: `search_*`, `list_*`, `get_*`, `view_*`, `describe_*`. Never a write or destructive tool.
889
+ - `repair.tool` is verified to exist in the registry before the suggestion ships. Bogus names (typos, naive plurals) are silently dropped.
890
+ - Recursion bounded by the existing agent-loop repetition guard (`MAX_REPEAT = 2`): same tool+input retried 3× is blocked.
891
+
892
+ ### Patterns matched in v1
893
+
894
+ | Trigger | Suggested tool | Used when input contains |
895
+ |---|---|---|
896
+ | `status === 404` or `not found` | `search_contacts` | `contactResourceId` (string) AND error mentions "contact" |
897
+ | `status === 404` on `get_*` / `update_*` / `delete_*` / `finalize_*` / `pay_*` | `search_<entity>s` (only if exists in registry) | any `resourceId` |
898
+ | `status === 422` mentioning `accountResourceId` | `search_accounts` | (no input requirement) |
899
+ | `status === 422` + `duplicate` / `already exists` on `create_*` | `search_<entity>s` filtered by `reference` | `reference` (string) |
900
+ | `status === 422` + tax-profile direction mismatch | `search_tax_profiles` | (no input requirement) |
901
+
902
+ Non-API errors (validation, schema, network) do NOT carry a repair suggestion — pattern-matching free-text is too brittle. The existing `hint` field covers those cases.
903
+
904
+ ### Recommended agent consumption
905
+
906
+ ```
907
+ 1. Receive tool error result with `repair` field present.
908
+ 2. Call execute_tool(repair.tool, repair.arguments).
909
+ 3. Use the search result to construct a corrected payload.
910
+ 4. Retry the original tool ONCE with the corrected payload.
911
+ 5. If it still fails, do NOT loop — surface the error to the user.
912
+ ```
913
+
914
+ Repair suggestions are advisory, not authoritative. The agent may ignore them when context (recent tool calls, user intent) suggests a different approach is better.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-cli
3
- version: 5.6.6
3
+ version: 5.6.8
4
4
  description: >-
5
5
  Use this skill when running Clio CLI commands, building shell scripts with
6
6
  Clio, debugging auth issues, understanding --json output, paginating results,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-conversion
3
- version: 5.6.6
3
+ version: 5.6.8
4
4
  description: >-
5
5
  Use this skill when migrating accounting data into Jaz — importing from Xero,
6
6
  QuickBooks, Sage, MYOB, or Excel exports. Covers the full conversion pipeline:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-pseudo-sql
3
- version: 5.6.6
3
+ version: 5.6.8
4
4
  description: >-
5
5
  Use this skill when answering ad-hoc data questions that aren't covered by
6
6
  download_export (canonical reports — anomaly, audit, aging, P&L, BS, GL,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-jobs
3
- version: 5.6.6
3
+ version: 5.6.8
4
4
  description: >-
5
5
  Use this skill for recurring accounting workflows — month/quarter/year-end
6
6
  close, bank reconciliation, GST/VAT filing, payment runs, credit control,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-practice
3
- version: 5.6.6
3
+ version: 5.6.8
4
4
  description: >-
5
5
  Use this skill whenever an accounting practitioner is doing client work in
6
6
  Jaz — closing the books, filing GST, year-end statutory, onboarding a new
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-recipes
3
- version: 5.6.6
3
+ version: 5.6.8
4
4
  description: >-
5
5
  Use this skill when modeling complex multi-step accounting transactions —
6
6
  anything that spans multiple periods, involves changing amounts, or requires
@@ -299,10 +299,26 @@ A second path: 5 IFRS recipes (Loan Amortization, Accrual Reversal, Prepaid Amor
299
299
 
300
300
  **Trigger-mutation payload** — to create a base-trx AND fire a recipe in one shot, pass `capsuleRecipe: {recipeName, recipeVersion?, inputs}` to the create/update mutation. Mutually exclusive with `capsuleResourceId` (the "attach to existing capsule" path).
301
301
 
302
- **Recovery flow**:
303
- - `capsuleRecipeJob` is null on the response → recipe never started server-side. Poll `search_background_jobs` filtered by `baseTransactionResourceId` (NOT `capsuleResourceId` — which may not exist yet).
304
- - Job status `FAILED` use `resume_capsule_recipe` (≤3 attempts) OR `rollback_capsule_recipe(dryRun=true)` first then `rollback_capsule_recipe(dryRun=false)`.
305
- - Capsule wasn't created via the recipe engine → rollback returns 422 `RECIPE_ROLLBACK_JOB_NOT_FOUND`; use `delete_capsule` for legacy capsules.
302
+ ### Three pre-flight gates BEFORE sending `capsuleRecipe` (else the response silently nulls)
303
+
304
+ The trigger mutation is **best-effort post-commit**: if the recipe publish fails inside customer-service, the base-trx still commits, the response still returns 201, but `capsuleRecipeJob` is null and **no error reason is surfaced on the response body**. Three causes of silent null — gate every one of them before sending:
305
+
306
+ | Gate | Constraint | Pre-flight check |
307
+ |---|---|---|
308
+ | **Base trx type** | `recipeName` must match a trigger mutation in the recipe's `allowedBaseTransactionTypes`. PREPAID_AMORTIZATION→PURCHASE, DEFERRED_REVENUE→SALE, ACCRUAL_REVERSAL→JOURNAL_MANUAL, IFRS16_LEASE→JOURNAL_MANUAL, LOAN_AMORTIZATION→JOURNAL_DIRECT_CASH_IN \| JOURNAL_MANUAL | `get_capsule_recipe(name).allowedBaseTransactionTypes` ↔ trigger mutation |
309
+ | **Currency** | Recipe `currency`, every `*AccountResourceId` account's `currencyCode`, and base trx `currencyCode` ALL must match (v1 recipes are single-currency) | `get_account(<id>).currencyCode` for every input account |
310
+ | **Account class** | Each `*AccountResourceId` slot has an `x-accountClass` constraint in the recipe inputSchema (Asset/Liability/Expense/Revenue) | `get_capsule_recipe(name).versions[0].inputSchema.properties.<field>['x-accountClass']` vs `get_account(<id>).accountClass` |
311
+
312
+ **The canonical pre-flight is one call**: `preview_capsule_recipe(recipeName, inputs)`. Pure-compute (no side effects). Surfaces every input/class/currency violation as a clean 422 with a concrete `error_type`. The trigger mutation does NOT surface these — it just returns 201 with no `capsuleRecipeJob`. Always preview first if you can't trust the inputs.
313
+
314
+ See `jaz-api` Rule 143 (silent-null failure mode + diagnosis sequence), Rule 144 (closed enum on `recipeName`), Rule 150 (RECIPE_INVALID_BASE_TRANSACTION_TYPE — preview-only, NOT trigger), Rule 156 (ERR_RECIPE_ACCOUNT_CURRENCY_MISMATCH), Rule 157 (x-accountClass slot constraint).
315
+
316
+ **Recovery flow** (when `capsuleRecipeJob` is null on the response):
317
+
318
+ 1. **Re-run `preview_capsule_recipe`** with the same `recipeName` + `inputs`. The 422 you get back is the exact reason the trigger mutation silently nulled. Fix the input and retry.
319
+ 2. **Poll `search_background_jobs --filter '{"baseTransactionResourceId":{"eq":"<id>"}}'`** — if a `FAILED` job exists, `errorDetails` has the publish failure. If no job exists, the publish never queued (validation rejected pre-queue).
320
+ 3. **Job status `FAILED`** → `resume_capsule_recipe` (≤3 attempts) OR `rollback_capsule_recipe(dryRun=true)` first, then `rollback_capsule_recipe(dryRun=false)`.
321
+ 4. **Capsule wasn't created via the recipe engine** → rollback returns 422 `RECIPE_ROLLBACK_JOB_NOT_FOUND`; use `delete_capsule` for legacy capsules.
306
322
 
307
323
  **DO NOT** use server-side execution for `fx-reval` — Jaz auto-handles ALL period-end IAS 21.23 FX translation; double-posting risk identical to the offline `execute_recipe(recipe: 'fx-reval')` warning.
308
324