Skip to content

feat(preflight): reject over-long SelectionCriteria arrays on remaining read-get commands#577

Merged
axisrow merged 2 commits into
mainfrom
feat/571-preflight-criteria-limits
Jun 17, 2026
Merged

feat(preflight): reject over-long SelectionCriteria arrays on remaining read-get commands#577
axisrow merged 2 commits into
mainfrom
feat/571-preflight-criteria-limits

Conversation

@axisrow

@axisrow axisrow commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

Extends enforce_criteria_array_limits (added in #555 for dynamicads/keywordbids/smartadtargets) to the rest of the read-get surface: ads, adgroups, bids, bidmodifiers, campaigns, keywords, dynamicfeedadtargets, audiencetargets. Each command now refuses an over-long SelectionCriteria array before the request and surfaces the array name + ceiling, instead of leaking the opaque API error_code=4001 (Exceed the maximum number of IDs per array …).

Closes #571. Closes a downstream consistency gap for axisrow/yandex-direct-mcp-plugin#201 (the plugin can now drop its own check_batch_limit(max=10) and forward the CLI error).

How per-filter caps were measured

Live, sandbox (direct --sandbox --profile default), 2026-06-17, probe set N=10/100/1000/10000. For «secondary» fields (VCardIds, SitelinkSetIds, NegativeKeywordSharedSetIds) an anchor --campaign-ids 1 was needed so the API would evaluate array length instead of returning «must specify at least one of Ids/AdGroupIds/CampaignIds».

Script: scripts/measure_criteria_limits.py (manual tool, not part of CI — re-run only when Yandex Direct is suspected of changing a cap). Full per-target transcripts: #571 (comment)

Command Capped (→ *_GET_CRITERIA_LIMITS) Uncapped at N=10000
ads CampaignIds=10, AdGroupIds=1000, VCardIds=10, SitelinkSetIds=10 Ids, AdExtensionIds
adgroups CampaignIds=10, NegativeKeywordSharedSetIds=10 Ids, TagIds
bids CampaignIds=10, AdGroupIds=1000 KeywordIds
bidmodifiers CampaignIds=1000 Ids, AdGroupIds
campaigns Ids=1000
keywords CampaignIds=10, AdGroupIds=1000 Ids
dynamicfeedadtargets CampaignIds=2, AdGroupIds=1000 Ids
audiencetargets CampaignIds=100, AdGroupIds=1000, RetargetingListIds=1000, InterestIds=1000 Ids

4 commands (strategies, sitelinks, vcards, adextensions) had their single Ids array measured uncapped, so they get no constant. The cleared list (14 uncapped keys total) is recorded in direct_cli/utils.py::UNCAPPED_CRITERIA_KEYS so future drift is visible and auditable.

Other notes

  • Tests: 18 new methods in TestSelectionCriteriaPreflight via a shared _assert_preflight_rejects(cmd, flag, cap) helper (Russian/English-locale-agnostic, asserts "more than {cap} elements" and absence of 4001).
  • Pyright cleanup of touched files: criteria = {} annotated dict[str, Any]; dict[str, object]dict[str, Any] on touched _build_* signatures; misc Dictdict for consistency. No behavior change.
  • scripts/_criteria_limits_results.json (live transcript cache) is gitignored — constants in code are the source of truth.

Test plan

  • pytest tests/test_integration.py::TestSelectionCriteriaPreflight -v — 21/21 PASSED (3 from Add preflight validation for SelectionCriteria array sizes (and other uncovered limits) #555 + 18 new)
  • pytest tests/test_dry_run.py tests/test_cli.py tests/test_integration.py::TestSelectionCriteriaPreflight — 1462/1462 PASSED
  • flake8 --max-line-length=88 on touched files — clean
  • black --check on touched files — clean
  • Live smoke (prod) on 5 commands: direct ads/keywords/bids/dynamicfeedadtargets/campaigns get --…-ids "1,…,M+1"UsageError with SelectionCriteria.<key> ... and no API call

🤖 Generated with Claude Code

…ng read-get commands (#571)

Extend `enforce_criteria_array_limits` (added in #555) to 8 more read-get
commands: ads, adgroups, bids, bidmodifiers, campaigns, keywords,
dynamicfeedadtargets, audiencetargets. Each command now refuses an over-long
SelectionCriteria array before the request and raises a UsageError naming the
array and ceiling, instead of leaking the opaque API `error_code=4001`.

Per-filter caps were measured live against the Yandex Direct sandbox on
2026-06-17 (probe set: N=10/100/1000/10000, anchored with --campaign-ids 1
for secondary fields). The measurement script and a documented audit set
(`UNCAPPED_CRITERIA_KEYS` in utils.py) are committed so future drift is
visible — see scripts/measure_criteria_limits.py and the per-command
`*_GET_CRITERIA_LIMITS` constants for the exact transcripts.

Downstream: the MCP plugin (axisrow/yandex-direct-mcp-plugin#201) can now
drop its own batch-size guard and forward the CLI error.

Tests: 18 new methods in TestSelectionCriteriaPreflight via a shared
`_assert_preflight_rejects(cmd, flag, cap)` helper.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
@axisrow

axisrow commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 1)

Reviewed locally (Claude subagent + Codex companion), no bots pinged.

Codex: approve, 0 findings.
Claude subagent: 0 critical, 3 minor (all SKIP).

Verdict Reviewer Finding Location
SKIP claude UNCAPPED_CRITERIA_KEYS defined but no runtime consumer — intentional audit-registry; matches the docstring («so future drift is visible»). Not dead code, it's living documentation. direct_cli/utils.py:202
SKIP claude client_from_ctx(ctx, create_client) runs before enforce_criteria_array_limits in 7/8 modified commands → credential resolution (incl. 1Password/Bitwarden refs) is paid even when preflight will reject. Following the #555 reference (keywordbids.py:93→120, dynamicads.py:58→72, smartadtargets.py:57→71) — re-ordering only the 7 new files would break homogeneity. A repo-wide reorder belongs in a separate PR covering all 11 commands. bids.py:59, adgroups.py:537, bidmodifiers.py:197, campaigns.py:974, keywords.py:274, dynamicfeedadtargets.py:61, audiencetargets.py:69
SKIP claude scripts/measure_criteria_limits.py hardcodes --profile default; cryptic auth failure if operator's profile has another name. Manual one-off tool (docstring already says «not part of CI»); cleanup is nice-to-have, not blocking. scripts/measure_criteria_limits.py:90

Cycle 1/3 triage complete: 0 FIX, 3 SKIP. Proceeding to final cleanup pass (step 6.5).

measure_criteria_limits.py was hardcoding `--profile default`; if the
operator's only `direct auth` profile has another name, the sweep failed
with a cryptic auth error before the first probe. Let YANDEX_DIRECT_MEASURE_PROFILE
override.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
@axisrow

axisrow commented Jun 17, 2026

Copy link
Copy Markdown
Owner Author

Final cleanup pass (step 6.5) applied in b817c0a: measure_criteria_limits.py now honors YANDEX_DIRECT_MEASURE_PROFILE env override instead of hardcoding --profile default (review SKIP #3). The other two SKIPs (UNCAPPED_CRITERIA_KEYS as audit-registry, validator-after-client_from_ctx matching the #555 pattern) intentionally left as-is — reasons in the previous comment.

@axisrow
axisrow merged commit 76381f1 into main Jun 17, 2026
6 checks passed
@axisrow
axisrow deleted the feat/571-preflight-criteria-limits branch June 17, 2026 12:21
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.

Extend enforce_criteria_array_limits to remaining get-commands (per-filter, live-measured)

1 participant