|
| 1 | +--- |
| 2 | +"@objectstack/cli": minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(cli): `--format json` failure envelopes carry the ADR-0112 `code` and `httpStatus` (#13347) |
| 6 | + |
| 7 | +Every machine-readable failure this CLI emits was built the same way — 48 sites |
| 8 | +under `packages/cli/src/commands/`: |
| 9 | + |
| 10 | +```ts |
| 11 | +await emitJson({ success: false, error: error.message }); |
| 12 | +``` |
| 13 | + |
| 14 | +The payload carried the human sentence and nothing else. The error reaching |
| 15 | +those `catch` blocks from `@objectstack/client` is not a bare `Error`: the SDK's |
| 16 | +`fetch` wrapper attaches `err.code` (the semantic ADR-0112 string, normalized to |
| 17 | +the same spelling across the flat `@objectstack/rest` envelope and the wrapped |
| 18 | +runtime-dispatcher one) and `err.httpStatus`. Both were discarded at the CLI |
| 19 | +boundary, so the one outcome a script most needs to branch on — *someone else |
| 20 | +edited it, re-read and retry* vs *you are not allowed* vs *the server is down* — |
| 21 | +was separable only by substring-matching an English sentence that no contract |
| 22 | +pins. |
| 23 | + |
| 24 | +A stale-pin refusal from `os meta delete --if-match` used to read: |
| 25 | + |
| 26 | +```json |
| 27 | +{ |
| 28 | + "success": false, |
| 29 | + "error": "[metadata_conflict] view/race_probe has been modified since you loaded it. …" |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +and now reads: |
| 34 | + |
| 35 | +```json |
| 36 | +{ |
| 37 | + "success": false, |
| 38 | + "error": "[metadata_conflict] view/race_probe has been modified since you loaded it. …", |
| 39 | + "code": "METADATA_CONFLICT", |
| 40 | + "httpStatus": 409 |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +Maintainer ruling 2026-08-30 (option **A** of three): |
| 45 | + |
| 46 | +- The payload stays **FLAT**. Nesting into `{ error: { code, message, httpStatus } }` |
| 47 | + was considered and declined as breaking. |
| 48 | +- `success` and `error` keep their current meaning **and spelling**. |
| 49 | +- The two keys are emitted **only when the thrown error carries them**, and are |
| 50 | + **absent** — not `undefined` — otherwise. No fallback code is invented for a |
| 51 | + locally-thrown plain `Error`: this CLI's own input refusals get no code, |
| 52 | + deliberately, because ADR-0112's ledger is the authority on who may mint one. |
| 53 | + |
| 54 | +One value-space note, stated so the declaration matches what ships: `code` is a |
| 55 | +**pass-through**, never minted or filtered. On wire failures it is the semantic |
| 56 | +ADR-0112 string the SDK attached; on local I/O failures inside the same `try` |
| 57 | +(`os validate` reading a `src/docs` that is a file, say) it is the Node errno |
| 58 | +(`ENOENT`, `ENOTDIR`, …). The two vocabularies are disjoint — errnos are |
| 59 | +`E`-prefixed OS names — so a consumer branching on ADR-0112 codes cannot |
| 60 | +false-match an errno, but not every emitted `code` is ledger-owned. |
| 61 | + |
| 62 | +Human (`table`) output is untouched. |
| 63 | + |
| 64 | +**Why `minor` and not `patch`.** Maintainer-set, and it overrides the obvious |
| 65 | +reading: this is a shape change to an **already-published error envelope**, and |
| 66 | +that makes it minor even though it is purely additive. |
| 67 | + |
| 68 | +**Migration.** Nothing is required — no key is removed, renamed or re-typed, and |
| 69 | +every payload this CLI emitted before is still emitted, byte-for-byte, minus the |
| 70 | +two new keys. Two things are worth knowing before you rely on the new ones: |
| 71 | + |
| 72 | +- **The envelope is polymorphic, by design.** `code` and `httpStatus` are absent |
| 73 | + whenever the failure did not carry them, which a consumer cannot distinguish |
| 74 | + from an older CLI. Branch on presence (`if (payload.code === 'METADATA_CONFLICT')`), |
| 75 | + never on absence meaning "success" or "unsupported version". This cost was |
| 76 | + weighed against breaking every existing consumer, and the non-breaking side won. |
| 77 | +- **Stop substring-matching the sentence.** `error` is prose and no contract pins |
| 78 | + its wording; the bracketed `[metadata_conflict]` tag some messages carry today |
| 79 | + is a property of one producer, not a contract, and a separate card argues for |
| 80 | + removing it. Code that reads the sentence to classify a failure should move to |
| 81 | + `code` (with `httpStatus` as the coarse fallback). |
0 commit comments