Skip to content

[#93] Added minimum and maximum selection limits to multi-value fields.#119

Open
AlexSkrypnyk wants to merge 5 commits into
mainfrom
feature/93-selection-limits
Open

[#93] Added minimum and maximum selection limits to multi-value fields.#119
AlexSkrypnyk wants to merge 5 commits into
mainfrom
feature/93-selection-limits

Conversation

@AlexSkrypnyk

Copy link
Copy Markdown
Member

Closes #93

Summary

Multi-value fields - ->multiple() on select and search, and the file picker - can now declare a minimum and/or maximum number of allowed selections via two new builder methods, ->minSelections(int) and ->maxSelections(int), assembled into a new SelectionBounds value object stored on Field. The limit is enforced at accept time in the interactive TUI (an out-of-range selection is rejected inline with an error naming the bound, e.g. "Select at least 2 items.") and identically in headless collection (Engine) and answer-set validation (SchemaValidator), and the active limit is surfaced as a persistent hint line in the widget view before it is reached. The bounds also appear in the machine-readable schemas so external tooling and agents can see them ahead of time.

Changes

Model and builder

  • Added src/Model/SelectionBounds.php, a readonly value object holding an optional inclusive min/max, with contains(int $count), violation(mixed $value) and describe() (the human phrase, e.g. "between 2 and 4 items", "at least 2 items", "exactly 3 items", pluralized through Translator::formatPlural()).
  • SelectionBounds::contains() treats a negative count as always out of range.
  • Its constructor rejects a bound below one and a minimum greater than the maximum.
  • src/Model/Field.php gained a ?SelectionBounds $selectionBounds constructor parameter; the constructor now throws a FormException when bounds are declared on a field that is not multiple.
  • Field::boundsViolation() now also checks selectionBounds->violation(), alongside the existing number and date bounds, so every enforcement surface that already calls it picks up selection-count checking for free.
  • src/Builder/FieldBuilder.php gained minSelections(int) and maxSelections(int), and a buildSelectionBounds() step that throws when limits are declared on a non-multiple field or when the declared minimum exceeds the maximum.

Interactive widgets

  • Added src/Widget/Capability/SelectionBoundedTrait.php, shared by SelectWidget, SearchWidget and FilePickerWidget: it overrides accept() to reject an out-of-range selection with an inline "Select @constraint." error, and renders the active bound as a dim hint line above the option list.
  • src/Widget/WidgetFactory.php now passes $field->selectionBounds through to all three widget constructors.

Schema surfaces

  • src/Schema/AgentHelp.php emits minItems/maxItems in the generated JSON Schema for a bounded field.
  • src/Schema/SchemaGenerator.php emits min_selections/max_selections alongside the existing number bounds.

Translations

  • translations/en.php gained the new bound phrases and the "Select @constraint." error string.
  • translations/uk.php gained the same phrases using the catalog's plural-form mechanism (singular/few/many forms for "item(s)").

Tests

  • tests/phpunit/Unit/Model/SelectionBoundsTest.php (new) covers contains(), violation(), describe() and the constructor guards, including a negative selection count.
  • tests/phpunit/Unit/Builder/FormTest.php and BuiltModelTest.php cover builder assembly and the non-multiple/min-above-max/min-below-one rejections.
  • tests/phpunit/Unit/Engine/EngineNonInteractiveTest.php adds headless within-bounds, below-minimum, and above-maximum coverage.
  • tests/phpunit/Unit/Schema/SchemaValidatorTest.php, AgentHelpTest.php and SchemaGeneratorTest.php cover the answer-set validator and both schema surfaces.
  • tests/phpunit/Unit/Widget/SelectWidgetTest.php, SearchWidgetTest.php and FilePickerWidgetTest.php cover the inline rejection, the accepted-within-bounds path, and the hint line for each widget.
  • All 1436 tests pass; the new code is at 100% coverage.

Before / After

BEFORE - any selection count accepted             AFTER - bounded selection, enforced everywhere
──────────────────────────────────────            ──────────────────────────────────────────────────
$panel->select('veg', 'Vegetables')               $panel->select('veg', 'Vegetables')
  ->multiple()                                       ->multiple()
  ->options([...]);                                  ->minSelections(2)->maxSelections(4)
                                                       ->options([...]);

Interactive widget                                Interactive widget
┌────────────────────────────┐                    ┌────────────────────────────┐
│ [x] carrot                 │                     │ between 2 and 4 items      │  <- hint, shown up front
│ [ ] tomato                 │                     │ [x] carrot                 │
│ [ ] potato                 │                     │ [ ] tomato                 │
└────────────────────────────┘                     │ [ ] potato                 │
  accept with 1 selected -> OK                      └────────────────────────────┘
                                                     accept with 1 selected ->
                                                     "Select at least 2 items."  <- rejected inline

Headless payload {"veg": ["carrot"]}              Headless payload {"veg": ["carrot"]}
  -> accepted                                        -> EngineException:
                                                        Invalid value for field "veg":
                                                        must be between 2 and 4 items.

Generated schemas                                 Generated schemas
  AgentHelp: no count constraint                    AgentHelp:       "minItems": 2, "maxItems": 4
  SchemaGenerator: no count constraint              SchemaGenerator: min_selections=2, max_selections=4

@AlexSkrypnyk AlexSkrypnyk added this to the 0.2.0 milestone Jul 23, 2026
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bca82760-96a3-4ea9-996f-ac5eca204edf

📥 Commits

Reviewing files that changed from the base of the PR and between 9878aca and d3123e1.

📒 Files selected for processing (22)
  • src/Builder/FieldBuilder.php
  • src/Model/Field.php
  • src/Model/SelectionBounds.php
  • src/Schema/AgentHelp.php
  • src/Schema/SchemaGenerator.php
  • src/Widget/Capability/SelectionBoundedTrait.php
  • src/Widget/FilePickerWidget.php
  • src/Widget/SearchWidget.php
  • src/Widget/SelectWidget.php
  • src/Widget/WidgetFactory.php
  • tests/phpunit/Unit/Builder/FormTest.php
  • tests/phpunit/Unit/Engine/EngineNonInteractiveTest.php
  • tests/phpunit/Unit/Model/BuiltModelTest.php
  • tests/phpunit/Unit/Model/SelectionBoundsTest.php
  • tests/phpunit/Unit/Schema/AgentHelpTest.php
  • tests/phpunit/Unit/Schema/SchemaGeneratorTest.php
  • tests/phpunit/Unit/Schema/SchemaValidatorTest.php
  • tests/phpunit/Unit/Widget/FilePickerWidgetTest.php
  • tests/phpunit/Unit/Widget/SearchWidgetTest.php
  • tests/phpunit/Unit/Widget/SelectWidgetTest.php
  • translations/en.php
  • translations/uk.php
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/93-selection-limits

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

@github-actions

Copy link
Copy Markdown
Code Coverage Report:
  2026-07-23 12:15:59

 Summary:
  Classes: 83.33% (85/102)
  Methods: 96.17% (778/809)
  Lines:   98.24% (3358/3418)

DrevOps\Tui\Answers\Answer
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Answers\Answers
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 20/ 20)
DrevOps\Tui\Answers\Provenance
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  7/  7)
DrevOps\Tui\Answers\SummaryFormatter
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 20/ 20)
DrevOps\Tui\Answers\ValueFormatter
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% (  6/  6)
DrevOps\Tui\Builder\FieldBuilder
  Methods: 100.00% (41/41)   Lines: 100.00% (150/150)
DrevOps\Tui\Builder\Form
  Methods:  92.31% (12/13)   Lines:  98.39% ( 61/ 62)
DrevOps\Tui\Builder\LayoutGuard
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  7/  7)
DrevOps\Tui\Builder\PanelBuilder
  Methods: 100.00% (20/20)   Lines: 100.00% ( 37/ 37)
DrevOps\Tui\Condition\CompositeCondition
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 19/ 19)
DrevOps\Tui\Condition\Condition
  Methods: 100.00% (10/10)   Lines: 100.00% ( 38/ 38)
DrevOps\Tui\Derive\Derive
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 14/ 14)
DrevOps\Tui\Derive\Deriver
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% ( 13/ 13)
DrevOps\Tui\Derive\Transform
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% (  7/  7)
DrevOps\Tui\Discovery\AbstractDiscover
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Discovery\Dotenv
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 19/ 19)
DrevOps\Tui\Discovery\JsonValue
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 16/ 16)
DrevOps\Tui\Discovery\PathExists
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% (  3/  3)
DrevOps\Tui\Discovery\Scan
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 20/ 20)
DrevOps\Tui\Engine\Engine
  Methods: 100.00% (18/18)   Lines: 100.00% (115/115)
DrevOps\Tui\Handler\HandlerRegistry
  Methods:  85.71% ( 6/ 7)   Lines:  95.45% ( 21/ 22)
DrevOps\Tui\Input\Binding
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Input\DefaultKeyMap
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% ( 38/ 38)
DrevOps\Tui\Input\Hint
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  2/  2)
DrevOps\Tui\Input\Key
  Methods:  87.50% ( 7/ 8)   Lines:  63.64% (  7/ 11)
DrevOps\Tui\Input\KeyMap
  Methods: 100.00% (11/11)   Lines: 100.00% ( 61/ 61)
DrevOps\Tui\Input\KeyMapManager
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% (  8/  8)
DrevOps\Tui\Input\KeyParser
  Methods:  85.71% ( 6/ 7)   Lines:  98.95% ( 94/ 95)
DrevOps\Tui\Input\Scope
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 17/ 17)
DrevOps\Tui\Input\ScopedKeyMap
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% (  4/  4)
DrevOps\Tui\Input\VimKeyMap
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% ( 12/ 12)
DrevOps\Tui\Model\Buttons
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Model\DateBounds
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 28/ 28)
DrevOps\Tui\Model\Field
  Methods:  84.62% (11/13)   Lines:  89.61% ( 69/ 77)
DrevOps\Tui\Model\FieldType
  Methods:  66.67% ( 2/ 3)   Lines:  31.82% (  7/ 22)
DrevOps\Tui\Model\FormDefinition
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 12/ 12)
DrevOps\Tui\Model\Modal
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  2/  2)
DrevOps\Tui\Model\NumberBounds
  Methods:  83.33% ( 5/ 6)   Lines:  95.24% ( 20/ 21)
DrevOps\Tui\Model\Option
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 14/ 14)
DrevOps\Tui\Model\Panel
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% (  3/  3)
DrevOps\Tui\Model\SelectionBounds
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 23/ 23)
DrevOps\Tui\Model\Weekday
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 15/ 15)
DrevOps\Tui\Render\Ansi
  Methods:  83.33% ( 5/ 6)   Lines:  76.47% ( 13/ 17)
DrevOps\Tui\Render\Box
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 15/ 15)
DrevOps\Tui\Render\ExternalEditor
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 27/ 27)
DrevOps\Tui\Render\HelpSection
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Render\Navigator
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 16/ 16)
DrevOps\Tui\Render\Overlay
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 24/ 24)
DrevOps\Tui\Render\PanelController
  Methods: 100.00% (47/47)   Lines: 100.00% (309/309)
DrevOps\Tui\Render\Scroller
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 14/ 14)
DrevOps\Tui\Render\Terminal
  Methods:  95.24% (20/21)   Lines:  96.88% ( 62/ 64)
DrevOps\Tui\Render\TerminalControl
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% (  9/  9)
DrevOps\Tui\Render\Viewport
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Resolver\InputResolver
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% ( 29/ 29)
DrevOps\Tui\Schema\AgentHelp
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 52/ 52)
DrevOps\Tui\Schema\SchemaGenerator
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 35/ 35)
DrevOps\Tui\Schema\SchemaValidator
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 32/ 32)
DrevOps\Tui\Testing\ArrayKeyStream
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 12/ 12)
DrevOps\Tui\Testing\BufferedTerminal
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 12/ 12)
DrevOps\Tui\Testing\KeyEncoder
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% ( 22/ 22)
DrevOps\Tui\Testing\TuiTester
  Methods: 100.00% (13/13)   Lines: 100.00% ( 34/ 34)
DrevOps\Tui\Testing\WidgetRunner
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  5/  5)
DrevOps\Tui\Theme\DefaultTheme
  Methods:  95.79% (91/95)   Lines:  98.66% (441/447)
DrevOps\Tui\Theme\DosTheme
  Methods:  80.00% (12/15)   Lines:  82.35% ( 14/ 17)
DrevOps\Tui\Theme\EmberTheme
  Methods:  66.67% ( 6/ 9)   Lines:  66.67% (  6/  9)
DrevOps\Tui\Theme\FrostTheme
  Methods:  66.67% ( 6/ 9)   Lines:  66.67% (  6/  9)
DrevOps\Tui\Theme\MidnightTheme
  Methods:  66.67% ( 6/ 9)   Lines:  66.67% (  6/  9)
DrevOps\Tui\Theme\MonoTheme
  Methods:  66.67% ( 6/ 9)   Lines:  66.67% (  6/  9)
DrevOps\Tui\Theme\Sgr
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Theme\ThemeManager
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% (  8/  8)
DrevOps\Tui\Translation\Translator
  Methods: 100.00% (18/18)   Lines: 100.00% ( 87/ 87)
DrevOps\Tui\Tui
  Methods: 100.00% (23/23)   Lines: 100.00% ( 72/ 72)
DrevOps\Tui\Utils\Strings
  Methods: 100.00% ( 6/ 6)   Lines: 100.00% (  9/  9)
DrevOps\Tui\Widget\AbstractWidget
  Methods: 100.00% (18/18)   Lines: 100.00% ( 49/ 49)
DrevOps\Tui\Widget\CalendarWidget
  Methods: 100.00% (13/13)   Lines: 100.00% ( 52/ 52)
DrevOps\Tui\Widget\Capability\CompletionCapableTrait
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 14/ 14)
DrevOps\Tui\Widget\Capability\FilterCapableTrait
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 15/ 15)
DrevOps\Tui\Widget\Capability\OptionsCapableTrait
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 34/ 34)
DrevOps\Tui\Widget\Capability\PagingCapableTrait
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 16/ 16)
DrevOps\Tui\Widget\Capability\SearchCapableTrait
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% (  3/  3)
DrevOps\Tui\Widget\Capability\SelectionBoundedTrait
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 10/ 10)
DrevOps\Tui\Widget\Capability\SelectionCapableTrait
  Methods: 100.00% (13/13)   Lines: 100.00% ( 85/ 85)
DrevOps\Tui\Widget\Capability\TextEditCapableTrait
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 34/ 34)
DrevOps\Tui\Widget\ConfirmWidget
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 22/ 22)
DrevOps\Tui\Widget\FilePickerWidget
  Methods: 100.00% (31/31)   Lines: 100.00% (184/184)
DrevOps\Tui\Widget\MatchResult
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)
DrevOps\Tui\Widget\MatchTier
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  6/  6)
DrevOps\Tui\Widget\Matcher
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 73/ 73)
DrevOps\Tui\Widget\NumberWidget
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 36/ 36)
DrevOps\Tui\Widget\PasswordDisplay
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  5/  5)
DrevOps\Tui\Widget\PasswordWidget
  Methods: 100.00% ( 9/ 9)   Lines: 100.00% ( 45/ 45)
DrevOps\Tui\Widget\PauseWidget
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% ( 11/ 11)
DrevOps\Tui\Widget\ReorderWidget
  Methods: 100.00% (10/10)   Lines: 100.00% ( 54/ 54)
DrevOps\Tui\Widget\SearchWidget
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 12/ 12)
DrevOps\Tui\Widget\SelectWidget
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% (  8/  8)
DrevOps\Tui\Widget\SuggestWidget
  Methods: 100.00% (12/12)   Lines: 100.00% ( 45/ 45)
DrevOps\Tui\Widget\TextWidget
  Methods: 100.00% ( 5/ 5)   Lines: 100.00% ( 16/ 16)
DrevOps\Tui\Widget\TextareaWidget
  Methods: 100.00% ( 8/ 8)   Lines: 100.00% ( 49/ 49)
DrevOps\Tui\Widget\ToggleWidget
  Methods: 100.00% ( 8/ 8)   Lines: 100.00% ( 30/ 30)
DrevOps\Tui\Widget\WidgetFactory
  Methods: 100.00% ( 8/ 8)   Lines: 100.00% ( 36/ 36)

@github-actions

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.48485% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 98.24%. Comparing base (9878aca) to head (d3123e1).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/Model/Field.php 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #119      +/-   ##
==========================================
+ Coverage   98.21%   98.24%   +0.03%     
==========================================
  Files         100      102       +2     
  Lines        3358     3418      +60     
==========================================
+ Hits         3298     3358      +60     
  Misses         60       60              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@AlexSkrypnyk AlexSkrypnyk added the Needs review Pull request needs a review from assigned developers label Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs review Pull request needs a review from assigned developers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add minimum and maximum selection limits to multi-select fields

1 participant