Skip to content

Say in the contract who refuses a value, the CLI or the API - #26

Closed
curquiza wants to merge 3 commits into
search-postfrom
api-checks-its-own-values
Closed

curquiza wants to merge 3 commits into
search-postfrom
api-checks-its-own-values

Conversation

@curquiza

Copy link
Copy Markdown
Member

What

Write down in docs/contract/output.md who refuses a bad value, the CLI or the API.

Based on #25, which implements the rule and carries the tests that pin it. GitHub shows only the two added bullets while that base is open, and retargets this branch to main when #25 merges.

Contract

  • The command name and flags derive from its routes by the naming rules of docs/contract/commands.md, and each route is reachable by a distinct set of flags, or the exception is recorded with its reason (or this pull request changes the contract and nothing else)

The three boxes below are about a command, and this pull request adds none. Nothing in the diff touches src/.

  • Output goes through src/lib/output.ts, errors through src/lib/errors.ts
  • The command never waits for a human: no prompt, no question, no $EDITOR; a delete command spreads BaseCommand.confirmFlags and its examples carry --yes
  • The command never speaks HTTP itself: the SDK or the generated client only, no fetch outside src/lib/clients/

Tests

  • pnpm lint, pnpm typecheck, pnpm test pass locally

The three boxes below have nothing to add here, and the promise is not untested: #25 pins both halves of it. test/commands/search.test.ts asserts that --vector 0.1,nope --ranking-score-threshold abc --hybrid '{nope}' reaches the API as {"vector":[0.1,"nope"],"rankingScoreThreshold":"abc","hybrid":"{nope}"}, and test/integration/meilisearch/search.test.ts asserts that a real Meilisearch answers invalid_search_ranking_score_threshold and exit 1. test/contract/errors.test.ts reads the Errors section only, so the cli_input_invalid named in the new bullet is prose, not a second registry.

  • --help snapshot added or updated on purpose
  • JSON output snapshot for one success case per route and one error case
  • Integration test added (Meilisearch API commands only)

Simplicity

  • Nothing in the diff exists for a case the API cannot produce, a caller that does not exist, or a generality nobody asked for (rule 16)
  • Anything shared lives in src/lib/, is used by this command, and is named below

Nothing added.

Docs

  • docs/contract/ updated for every name, flag, variable or code this change adds or renames, or nothing to update
  • The ADRs that touch this change were read; what they decide still holds, or a new ADR says what changed
  • No accepted ADR was edited. A new ADR was added if a decision changed.

ADR 0002 read: "the CLI returns the API response without a wrapper" and "the CLI never rewrites an API message" are the same stance one step earlier, on the way in rather than on the way out. No new ADR: this records a rule the contract already implied, it does not change a decision.

Housekeeping

  • Changeset added, or the change is invisible to users of the package (docs of this repository, tests, CI)
  • New dependency justified below, or none added

No changeset: docs/contract/ is not shipped in the package. The manifest box is left unticked below because nothing in src/commands/ moved, so there was nothing to regenerate.

  • oclif.manifest.json regenerated

Notes for the reviewer

The rule, in one line: the CLI validates an input only when it cannot hand it to the API.

  • A body has to be parsed before the flags can be merged into it, so --file and --data keep failing locally with cli_input_invalid, as the section already said.
  • A flag with a closed list of values carries that list in its --help, so the CLI can answer without a round trip: cli_usage, exit 2.
  • Everything else travels as it is, and the API refuses it with its own code and its own message.

The last bullet also records why converting anyway is a trap, because that is the part a reader will otherwise re-discover: Number("abc") is NaN, JSON.stringify writes NaN as null, and Meilisearch reads a null rankingScoreThreshold as no threshold at all. A typo would answer exit 0 with the wrong hits. Measured on 1.54 while writing #25.

One thing I did not do, say if you want it: nothing cross-references this from rule 7 of commands.md, which is where a reader learns that an object travels as JSON text. One home per rule seemed better than two that can drift.

🤖 Generated with Claude Code

The rule was decided while writing `meili search` and lived only in a
comment: the CLI validates an input when it cannot hand it to the API,
and otherwise sends it as it is. A body has to be parsed before the
flags can be merged into it, so `--file` and `--data` are validated. A
flag value is one field and can always travel as text, so the API
refuses it with its own code and names the value.

Two bullets in the Input section of output.md. No code: #25 implements
it, and its tests pin it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 65ef36b8-afd9-4409-8725-66fe38e60c19

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

curquiza and others added 2 commits October 26, 2026 11:03
`Number` converts more than a decimal number: `0x1` and an overflow to
Infinity both convert, and both change the value in silence. The bullet
now says which text becomes a number, and why the line is drawn there.

Follows the fix on #25, found by CodeRabbit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--limit` and `--offset` are `Flags.integer` in `BaseCommand`, so
`meili get indexes --limit abc` already answers `cli_usage` and exit 2
before any call. The second bullet said every value but a body travels
as it is, which that falsifies.

The line is now drawn where it really runs: a flag `--help` types, an
integer or a closed list, is refused by the parser; a flag the command
converts itself, a fractional number or an object, is sent as its text
when the text does not spell the value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@curquiza

Copy link
Copy Markdown
Member Author

Replaced by #27, which carries the same two bullets off main plus one correction.

This branch was cut from search-post, so once #25 merged its diff against main was the whole of #25 again, and it was missing 81782d2, the 0x1 fix. Rebasing it in place needed a force push, so #27 starts clean from main instead.

The correction: the first draft said every value but a body travels as it is, which --limit and --offset already falsified, being Flags.integer in BaseCommand. #27 draws the line between what the parser types and what a command converts itself.

@curquiza

Copy link
Copy Markdown
Member Author

Closing in favour of #27.

@curquiza curquiza closed this Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant