fix(docs): pin command-reference metavar format as a stable contract (#513) - #517
Conversation
…513) The release-asset reference (help.keboola.com, connection-docs freshness gate) is a published contract, but its option metavar column was rendered via Click's make_metavar(), whose default drifted between releases (bare `TEXT`/`INTEGER` at Click 8.x vs `<str>`/`<int>` later). The downstream connection-docs gate (keboola/connection-docs#1037) detects value-taking options by matching /^<.*>$/ on the metavar span, so a future dependency bump reverting to bare `TEXT` would silently break the docs build. Derive option metavars from Click's version-stable `ParamType.name` instead of `make_metavar()`: value-taking options always render as a `<...>` span (`<str>`, `<int>`, `<path>`, `<a|b|c>` for choices with literal case preserved), flags carry none. Document the column contract in the module header and add tests that fail CI on a Click/Typer bump that would change the shape, instead of letting the drift surface downstream.
padak
left a comment
There was a problem hiding this comment.
Second-opinion review (OpenAI Codex, gpt-5.6-sol, high reasoning) — verdict: NEEDS CHANGES.
Context check first: the reviewer inspected 922 value options and 226 flags in the current CLI — all existing types produce the intended spans, and no current command uses count or tuple options. So nothing breaks today; the findings are about valid Click parameter shapes the stable contract should survive, which is the whole point of this PR.
- High —
scripts/gen_command_reference.py:114— count flags render as value-taking options. In Click,click.Option(..., count=True)hascount=Truebutis_flag=False; the code would emit--verbose <int>, making the downstream gate think the option consumes a value. Suppress the metavar whenis_flagorcountis true, and add a_format_param()test with a count option. - Medium —
scripts/gen_command_reference.py:102-104— tuple/composite and unknown-type fallback can produce malformed spans.click.Tuple([STRING, INT])has name"<text integer>"→ rendered as<<text-integer>>. A nameless custom type silently becomes<str>. Add explicit composite handling viaptype.types(e.g.<str,int>) and a neutral<value>fallback for unknown types. - Medium —
scripts/gen_command_reference.py:99-101—str(choice)is not always the CLI token. Withclick.Choice(SomeEnum), Click accepts member names (RED|BLUE) while this emitsColor.RED|Color.BLUE. HandleEnummembers via.name(or Click'snormalize_choicepath) and add an Enum-backed choice test. - Low —
tests/test_gen_command_reference.py:113-133— tests overstate coverage. They exercise five synthetic external-Click options while the app uses Typer's vendored Click objects, and integration assertions check token presence, not every row. Add focused tests for count, tuple/nargs,multiple, Enum choice, custom/nameless types, plus a walk of the live Typer command tree validating every emitted option row.
Int ranges, float ranges, paths/files, scalar choices, regular boolean flags, and value-taking boolean options are handled consistently. The main remaining drift risk is composite/custom types and choices, not make_metavar() itself.
…options (#513) Review follow-up on the stable-metavar contract. Three valid Click parameter shapes could still emit an off-contract span; none is used by the CLI today, so the generated reference is byte-identical -- the point is that it stays that way when one of them appears. - Count options no longer get a value metavar. `click.Option(count=True)` leaves `is_flag` False, so keying only on `is_flag` published `--verbose <int>` and would make the downstream connection-docs gate demand a value the CLI does not accept. Suppression now goes through `_takes_a_value()` (is_flag OR count). - Composite types render their members. `click.Tuple([STRING, INT])` has `name == "<text integer>"`, which the sanitizer turned into the nested `<<text-integer>>`; composites are now expanded via `ptype.types` to `<str,int>`, and a scalar type with `nargs > 1` repeats the same way. - Enum-backed choices render member names. Click parses `RED` off the command line while `str(member)` is `Color.RED`; `_choice_token()` uses `.name`. Click's own `normalize_choice()` is deliberately not used -- it casefolds a `case_sensitive=False` choice, which would rewrite real tokens like `readOnly`. - Nameless/unrecognised custom types fall back to a neutral `<value>` instead of asserting `<str>`, and every token is folded into the documented alphabet so no span can be malformed. Tests: `_format_param()`-level cases for count/flag/value options, composite and multi-value nargs, `multiple=True`, nameless and unmapped custom types, and Enum-backed plus case-insensitive choices. Added a walk of the LIVE Typer tree (1072 option rows) validating every emitted row against the option-cell grammar, cross-checking metavar presence against `_takes_a_value()`, and rejecting nested angle brackets -- the synthetic cases use plain `click`, while the app renders Typer's vendored Click, so this is the test that actually catches vendored-Click drift.
|
All four findings addressed in 1. High — count flags rendered as value-taking. Fixed. Metavar suppression moved out of the inline 2. Medium — composite/unknown-type fallback. Fixed. 3. Medium — Enum-backed 4. Low — tests overstate coverage. Added Docstring contract in the module header updated to state the new rules (no span for Verification: |
| "filename": "path", | ||
| "file": "file", |
There was a problem hiding this comment.
🔍 File-typed option renders as
_METAVAR_BY_TYPE_NAME maps "filename": "path", and Click's File type has name == "filename", so a click.File() option renders <path>, not <file>. The "file": "file" entry is unreachable for stock Click. Cosmetic, but worth confirming intent.
Was this helpful? React with 👍 or 👎 to provide feedback.
pyproject/plugin.json/marketplace.json were already renumbered to 0.89.0 by v0.88.0. Changelog: adds 0.89.0 entries for #645 (describe-batch --from-file shape validation, issue #640), #642 (table-detail human column descriptions), #620 (sync-action forwards root authorization/runtime), #517 (stable metavar contract, issue #513), #586 (documented prompt budget gated against the enforced one, issue #585) and #641 (docs-only), and decorates the existing Silent-drift surfaces: * gotchas.md -- resolves both "(Release step: ... tag this sentence)" placeholders. Both were left by commits AFTER the v0.88.0 tag (#642 and #645), so both are tagged (since v0.89.0), not 0.88.0. Adds the #620 gotcha: below 0.89.0 a sync action on an OAuth / Service-Account component died with an opaque empty-body 400 because the broker reference was never forwarded. * #620 shipped with no doc surfaces at all -- CLAUDE.md, AGENT_CONTEXT and commands-reference.md now carry the forwarding rule (root only, never row-overridden, only when non-empty) with its version gate. * #645 never reached CLAUDE.md -- the describe-batch shape check and its behaviour change are recorded there now; commands-reference gains the version tag. * #642's human Description column is version-tagged in CLAUDE.md, commands-reference.md, AGENT_CONTEXT and storage-describe-workflow.md. * #643 was otherwise complete; adds the two surfaces it did not touch -- safe-write-workflow.md (delete is reversible; never blind-retry on <= 0.88.x) and a keboola-expert.md matrix row for delete/restore/trash-list. keboola-expert.md is 49 774 B, well inside the 70 000 B budget. make check green: 5934 passed, 12 skipped. version-gate-check resolves all 438 markers across 72 versions.
pyproject/plugin.json/marketplace.json were already renumbered to 0.89.0 by v0.88.0. Changelog: adds 0.89.0 entries for #645 (describe-batch --from-file shape validation, issue #640), #642 (table-detail human column descriptions), #620 (sync-action forwards root authorization/runtime), #517 (stable metavar contract, issue #513), #586 (documented prompt budget gated against the enforced one, issue #585) and #641 (docs-only), and decorates the existing Silent-drift surfaces: * gotchas.md -- resolves both "(Release step: ... tag this sentence)" placeholders. Both were left by commits AFTER the v0.88.0 tag (#642 and #645), so both are tagged (since v0.89.0), not 0.88.0. Adds the #620 gotcha: below 0.89.0 a sync action on an OAuth / Service-Account component died with an opaque empty-body 400 because the broker reference was never forwarded. * #620 shipped with no doc surfaces at all -- CLAUDE.md, AGENT_CONTEXT and commands-reference.md now carry the forwarding rule (root only, never row-overridden, only when non-empty) with its version gate. * #645 never reached CLAUDE.md -- the describe-batch shape check and its behaviour change are recorded there now; commands-reference gains the version tag. * #642's human Description column is version-tagged in CLAUDE.md, commands-reference.md, AGENT_CONTEXT and storage-describe-workflow.md. * #643 was otherwise complete; adds the two surfaces it did not touch -- safe-write-workflow.md (delete is reversible; never blind-retry on <= 0.88.x) and a keboola-expert.md matrix row for delete/restore/trash-list. keboola-expert.md is 49 774 B, well inside the 70 000 B budget. make check green: 5934 passed, 12 skipped. version-gate-check resolves all 438 markers across 72 versions.
…es (#651) Release prep for 0.89.0: adds the changelog entries for everything merged since v0.88.0 (#620, #642, #643, #644, #645, #646, #647, #648, #649, #650, #517, #586, #641), resolves every vNEXT placeholder left by feature PRs to v0.89.0 per the new #648 release process, closes the 10 gaps a full doc-surface audit found across the kbagent plugin (SKILL.md triggers, commands-reference, gotchas, workflow files, keboola-expert.md, AGENT_CONTEXT, CLAUDE.md), and records the live e2e verification evidence. Version files were already at 0.89.0 (bumped by #643); make version-sync is a no-op.
What
The release-asset command reference (
scripts/gen_command_reference.py, consumed by help.keboola.com and the connection-docs freshness gate) rendered its option metavar column via Click'smake_metavar(). That default is not stable across Click versions — it already drifted once: bare`TEXT`/`INTEGER`at v0.70.1 (Click 8.x),`<str>`/`<int>`at v0.72.0.This PR pins the option metavar shape to a documented convention independent of the installed Click:
ParamType.name(a version-stable token:text/integer/float/path/choice) instead ofmake_metavar(), via a new_stable_option_metavar().<...>span —<str>,<int>,<float>,<path>, and<a|b|c>for choices (literal case preserved, e.g.<admin|guest|readOnly|share>, since choice values are real CLI tokens). Flags carry no metavar span.ALIAS→<alias>).`NAME` (positional)and are not part of the| `--flag` `<type>` |shape the downstream gate parses.Why
The connection-docs freshness gate (keboola/connection-docs#1037, PRDCT-556) detects whether an option takes a value by matching
/^<.*>$/on the metavar span. That works today only because 0.72.0 happens to emit the<…>form. A future Typer/Click bump reverting to bareTEXTwould silently make the checker misparse value-taking options, turning valid commands into false-positive "unknown command" errors and reddening the docs build on correct content. Pinning the format at the source makes the published asset a contract by intent, not by accident of the dependency version.How it was tested
TestMetavarContractintests/test_gen_command_reference.py: asserts scalar types map to the documented<...>forms, choices preserve literal case, explicit metavars are wrapped/lowercased, every value option satisfies the<...>span contract, flags carry no span, no bare uppercaseTEXT/INTEGER/PATH/FLOATleaks onto an option row, and the documented scalar/choice forms are present in the generated asset. A Click/Typer bump that changesmake_metavar()now fails CI here instead of downstream.make checkgreen (lint + format + changelog-check + typecheck + full suite: 4620 passed, 8 skipped).<...>-wrapped; no bare uppercase metavars remain.Note: no version bump / changelog entry — this only stabilizes the generated asset's shape (already
<str>in the last release), it does not change the CLI surface. The immediate mitigation on the consumer side (accept<str>|TEXT|[a|b]) is still recommended in the linked issue; this is the durable source-side fix.Fixes #513