Skip to content

Revalidate fallback values in bindEnv() and bindConfig()#777

Merged
dahlia merged 22 commits intomainfrom
fix/issue-414-bind-validation
Apr 8, 2026
Merged

Revalidate fallback values in bindEnv() and bindConfig()#777
dahlia merged 22 commits intomainfrom
fix/issue-414-bind-validation

Conversation

@dahlia
Copy link
Copy Markdown
Owner

@dahlia dahlia commented Apr 8, 2026

Fixes #414.

Until this change, bindEnv() and bindConfig() handed fallback values (environment variables parsed through an env-level parser, config values extracted from a loaded file, and the configured default) back to the caller without running them through the inner CLI parser's validation. As a result, constraints attached to the inner parser (regex patterns, numeric bounds, choice() values, and so on) could be silently bypassed whenever a value came from a source other than the CLI. The issue reproduction showed that bindEnv(option("--name", string({ pattern: /^[A-Z]+$/ })), { parser: string(), default: "abc" }) would happily accept "abc" even though that same value would be rejected on the command line. This was both a correctness hole and a validation gap that could quietly accept invalid data in production.

The fix introduces a new optional method on the Parser interface, validateValue(), which re-checks whether an arbitrary value satisfies the parser's underlying ValueParser constraints. Primitive parsers (option() and argument()) implement it as a format() + parse() round-trip: the value is stringified through the same ValueParser.format() that would be used to display it, and the resulting string is fed back through ValueParser.parse() so that every constraint the user attached to the parser is applied. Combinators that can meaningfully forward validation (optional(), withDefault(), group(), command()) now pass validateValue() through from their inner parser, and outer wrappers like nested bindEnv(bindConfig(...)) compositions forward it as well. map() explicitly strips it, because the mapping function is one-way and the mapped output type no longer corresponds to the inner parser's constraints. or(), longestMatch(), merge(), and concat() deliberately do not forward because the active branch is not known at validation time.

bindEnv() and bindConfig() then use this new hook to revalidate every fallback value they produce. The env-level parser (which is often intentionally looser than the inner CLI parser) no longer has the final word: once it successfully parses an env var, the result is piped through the inner parser's validateValue() so the stricter constraints are applied. Configured defaults receive the same treatment. The same is true for the dependency-source extraction path, so a constraint-violating env or config value cannot leak to a derived parser either. In bindConfig(), the getMissingSourceValue hook that feeds defaults into the dependency runtime is also routed through the validator.

Dependency-derived value parsers (derive / deriveFrom) are explicitly exempt from validation attachment. Their format() has to rebuild the parser from default dependency values rather than the live-resolved ones, so a round-trip would end up validating against the wrong branch. The implementation guards against this with isDerivedValueParser() checks in both option() and argument(), and the docs and changelog now spell that out alongside the existing map() exemption.

Here is a concrete example of what flips with this change:

bindEnv(option("--name", string({ pattern: /^[A-Z]+$/ })), {
  context: envContext,
  key: "NAME",
  parser: string(),    // looser than the inner CLI parser
  default: "abc",      // now rejected: must match /^[A-Z]+$/
});

Before this change the "abc" default was silently accepted. After this change it is rejected with the same error that a CLI-sourced value would produce. The same holds for integer bounds, choices, and any other constraint expressible through a value parser.

This is a behavior change for any code that previously relied on constraint-violating env/config values or defaults being accepted. The changelog calls this out under @optique/core, @optique/env, and @optique/config. The documentation under docs/integrations/env.md and docs/integrations/config.md gained a new "Fallback validation" subsection, and the bindEnv() / bindConfig() API reference entries now mention that fallback values are revalidated.

The work followed the TDD workflow: regression tests reproducing the issue were written first against main, confirmed to fail, then the implementation was added, and finally the full test matrix was run. The diff adds regression tests in packages/env/src/index.test.ts and packages/config/src/index.test.ts (pattern rejection, integer bound rejection, env/config value revalidation, optional() and withDefault() forwarding, map() fall-through, async mode, group() forwarding, dependency-source env and config validation, and derived value parser exemption), plus unit tests for validateValue itself in packages/core/src/primitives.test.ts and packages/core/src/modifiers.test.ts.

The fix was reviewed by Codex across two rounds. The first round flagged four issues: derived value parsers being incorrectly validated, the dependency-source paths not being routed through the validator, a boolean short-circuit in option().validateValue that bypassed validation for custom boolean parsers, and missing forwarding through group() and command(). All four were addressed in the second commit. The second round flagged a documentation mismatch where the integration guides still described the old forwarding set, which was fixed in the third commit. The final Codex review reports no findings.

Test plan

  • mise test:deno
  • mise test:node
  • mise test:bun
  • mise check
  • cd docs && pnpm build
  • Manually verify the issue reproduction script now fails as expected

dahlia and others added 4 commits April 8, 2026 07:19
bindEnv() and bindConfig() previously returned fallback values
(environment variables parsed by a looser env-level parser, values
loaded from config files, and configured defaults) directly to the
caller without running them through the bound CLI parser's
validation. Parser constraints like integer({ min }) and
string({ pattern }) could be silently bypassed through any of these
fallback paths.

This commit adds an optional Parser.validateValue() hook that
re-validates a value as if it had been parsed from CLI input. The
primitive parsers option() and argument() implement it by
round-tripping the value through their stored ValueParser's format()
and parse() calls, which re-runs every constraint check. optional()
and withDefault() forward the hook from their inner parser; map()
strips it because the mapping function is one-way; bindEnv() and
bindConfig() forward it from their wrapped parser so nested
compositions like bindEnv(bindConfig(option(...))) propagate
validation down to the primitive.

With the hook in place, bindEnv's getEnvOrDefault() and bindConfig's
getConfigOrDefault() now route every fallback value through
innerParser.validateValue() when available. When the inner parser
does not implement the hook (for example, it sits behind map()), the
wrappers fall through to the current behavior so existing use cases
that pre-validate their defaults are preserved.

Fixes #414.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Close the gaps in the initial bindEnv()/bindConfig() validation fix:

 -  Skip validateValue on dependency-derived value parsers. Their
    format() rebuilds the parser from default dependency values,
    so a format+parse round-trip would validate against the wrong
    branch.
 -  Forward validateValue through group() and command() so the
    inner primitive's validator survives wrapping.
 -  Validate env-sourced values and defaults in the dependency-source
    extraction path (getEnvSourceValue), closing a gap where a
    constraint-violating env value could still reach a derived parser.
 -  Validate config-sourced values and defaults in the dependency-source
    extraction path (getConfigSourceValue) and in the getMissingSourceValue
    hook used for source-default fill.

Also adds regression tests for each of the above in the fallback
validation blocks of the env/config test suites, and replaces the
flag-form boolean shortcut in option().validateValue with an explicit
trivial validator so value-parser options always go through the full
format+parse round-trip.

See: #414
Update the env and config integration guides to match the actual
behavior now that validateValue also forwards through group() and
command(), and is exempt for dependency-derived value parsers:

- Add group() and command() to the list of combinators that forward
  fallback validation.
- Note that derived value parsers (derive / deriveFrom) are also
  exempt alongside map(), and explain why (format() rebuilds from
  default dependency values rather than live-resolved ones).

See: #414
Codex: /home/dahlia/.codex/sessions/019d6a08-7539-7812-aa17-7f721a1c124f
@dahlia dahlia requested a review from Copilot April 8, 2026 01:05
@dahlia dahlia self-assigned this Apr 8, 2026
@dahlia dahlia added the bug Something isn't working label Apr 8, 2026
@dahlia dahlia added this to the Optique 1.0 milestone Apr 8, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 8, 2026

Codecov Report

❌ Patch coverage is 82.18085% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.39%. Comparing base (0d05b44) to head (0063254).
⚠️ Report is 23 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/core/src/primitives.ts 73.87% 29 Missing ⚠️
packages/core/src/modifiers.ts 87.87% 16 Missing ⚠️
packages/config/src/index.ts 79.66% 12 Missing ⚠️
packages/env/src/index.ts 85.07% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #777      +/-   ##
==========================================
- Coverage   89.53%   89.39%   -0.15%     
==========================================
  Files          43       43              
  Lines       26564    26904     +340     
  Branches     6711     6784      +73     
==========================================
+ Hits        23785    24051     +266     
- Misses       2694     2769      +75     
+ Partials       85       84       -1     

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

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the Parser.validateValue() method to @optique/core, enabling re-validation of fallback values from environment variables, configuration files, and defaults against inner CLI parser constraints. The implementation updates bindEnv() and bindConfig() to enforce these constraints and ensures that combinators like optional() and withDefault() forward validation correctly. Feedback identifies a ReferenceError in packages/core/src/primitives.ts involving an undefined variable, a redundant try/catch block in packages/core/src/modifiers.ts that may miss async rejections, and a style guide violation in packages/env/src/index.ts where manual promise checks are used instead of the required mode dispatch patterns.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR closes a validation gap in Optique’s env/config integrations by ensuring that fallback values (env vars, config values, and configured defaults) are revalidated against the inner CLI parser’s constraints, preventing constraint bypasses described in issue #414.

Changes:

  • Added optional Parser.validateValue() and implemented/forwarded it through relevant primitives and wrappers.
  • Updated bindEnv() / bindConfig() to route all fallback values (including dependency-source paths) through validateValue() when available.
  • Added regression/unit tests and updated integration docs + changelog to describe the new behavior and exemptions.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/env/src/index.ts Revalidates env/default fallback values via inner parser validateValue() and forwards the hook through bind wrappers.
packages/env/src/index.test.ts Adds regression tests covering fallback revalidation behavior (sync/async, wrappers, dependency-source paths, exemptions).
packages/core/src/primitives.ts Introduces validateValue() for option()/argument() and forwards through command().
packages/core/src/primitives.test.ts Adds unit tests for validateValue() on primitives.
packages/core/src/parser.ts Extends the Parser interface with optional validateValue() and documents intended forwarding/exemptions.
packages/core/src/modifiers.ts Forwards/strips validateValue() through optional(), withDefault(), and map().
packages/core/src/modifiers.test.ts Adds tests verifying validateValue() forwarding/stripping behavior in modifiers.
packages/core/src/constructs.ts Documents non-forwarding in branchy constructs and forwards validateValue() through group().
packages/core/src/constructs.test.ts Updates async parser helper to avoid invalid validateValue spreading across mode boundaries.
packages/config/src/index.ts Revalidates config/default fallback values via inner parser validateValue() (including dependency-source default path).
packages/config/src/index.test.ts Adds regression tests for bindConfig fallback validation behavior and forwarding/exemptions.
docs/integrations/env.md Documents “Fallback validation” behavior and the forwarding/exemption rules.
docs/integrations/config.md Documents “Fallback validation” behavior and the forwarding/exemption rules.
CHANGES.md Changelog entries describing the new validateValue() hook and the bindEnv/bindConfig behavior change.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds an optional Parser.validateValue(value) hook and propagates it through primitives, modifiers, and integration bindings so fallback values are re-validated as if provided on the CLI. Primitive parsers implement validateValue via format()→parse() round-trip; wrappers selectively forward or omit the hook (optional, withDefault, multiple, nonEmpty, group, command forward; map, or, longestMatch, merge, concat do not). bindEnv() and bindConfig() now thread the inner parser into fallback resolution and invoke validateValue when present. Tests and documentation updated to cover forwarding, arity rules, and exempted combinators.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #233 — Overlaps modifier wrapper behavior (optional/withDefault) and their interaction with bindEnv()/bindConfig(); this PR changes wrapper propagation and fallback validation relevant to that issue.

Possibly related PRs

  • #776 — Both modify multiple() to add per-element validation and enforce arity on fallback values.
  • #769 — Touches bindEnv()/bindConfig and wrapper composition/metadata forwarding; strongly related to inner-parser forwarding changes.
  • #633 — Modifies bindEnv()/getEnvOrDefault; both PRs change environment fallback parsing/validation pathways.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: revalidating fallback values in bindEnv() and bindConfig(), which is the primary objective of the PR.
Description check ✅ Passed The description comprehensively explains the issue, the fix, and provides concrete examples with before/after behavior that directly relate to the changeset.
Linked Issues check ✅ Passed The PR fully addresses all objectives from issue #414: introduces validateValue() hook, implements revalidation in bindEnv/bindConfig, exempts derived parsers, routes dependency paths through validators, and includes comprehensive tests and documentation.
Out of Scope Changes check ✅ Passed All changes are in-scope: core validateValue hook implementation, modifier/primitive forwarding, env/config revalidation, tests, docs, and changelog updates directly support the core objective of revalidating fallback values.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-414-bind-validation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/integrations/config.md`:
- Around line 801-807: Change the fenced block language to "typescript twoslash"
and add a tiny fixture prelude that declares the example variable used by the
snippet (declare the configContext symbol before the snippet body, e.g. a
`declare const configContext: any`/appropriate type placed in the prelude before
the // ---cut-before--- marker) so the VitePress twoslash type-checker sees
configContext and the example remains validated; keep the code inside the block
using the existing bindConfig, option, integer and key "port" identifiers.

In `@docs/integrations/env.md`:
- Around line 370-377: The TypeScript example using bindEnv(...) inside the docs
lacks the twoslash modifier so it isn't type-checked by the VitePress pipeline;
update the fenced code block that contains the bindEnv(...) example (the block
showing option("--name", string({ pattern: /^[A-Z]+$/ })) and parser: string())
to use the twoslash modifier (i.e., change the fence to include "twoslash") so
the snippet is validated and shows TypeScript hover/info in VitePress.
- Around line 388-389: The final warning sentence currently mentions only
derive(), so update it to include deriveFrom() as well (e.g., "derive() and
deriveFrom() will therefore silently bypass fallback validation") to keep the
documentation consistent; locate the sentence referring to derive() in the docs
text (mentions derive and deriveFrom earlier) and amend the wording to reference
both derive() and deriveFrom() so both bypass cases are documented.

In `@packages/core/src/modifiers.ts`:
- Around line 646-668: The multiple() wrapper must implement and forward a
validateValue hook similar to optionalParser: add a validateValue method on the
parser returned by multiple() that, when v is undefined/empty delegates
appropriately or else runs the inner parser.validateValue element-wise and
enforces multiple()'s own min/max rules, returning the wrapped ModeValue;
nonEmpty() should not override or replace validateValue but simply pass through
the inner parser's validateValue (i.e., preserve/forward it unchanged) so
bindEnv()/bindConfig() re-runs the inner validations; apply the same changes in
the corresponding duplicate block around the other occurrence mentioned (the
block at the 1145-1171 region). Ensure you reference and use
parser.validateValue, innerValidate bound to the inner parser, and return
ModeValue<M, ValueParserResult<...>> just like the optionalParser
implementation.

In `@packages/core/src/primitives.test.ts`:
- Line 7175: Replace the type-obscuring cast on the test call to
parser.validateValue! by using an explicit TypeScript negative-test directive:
remove `"purple" as "red"` and instead put `// `@ts-expect-error`` immediately
before the direct call `parser.validateValue!("purple")`, preserving the
non-null assertion and making the intentional type violation explicit to match
the file's existing pattern.

In `@packages/core/src/primitives.ts`:
- Around line 1410-1478: The validateValue implementation currently returns raw
ValueParser.parse failures which bypass option()/argument() error formatting;
update validateValue (the non-derived branch that defines result.validateValue)
to catch parse errors and forward them through the same error formatter/handler
used by option().complete()/argument().complete() instead of returning raw
failures — i.e., wrap vp.parse(...) and the async branch inside try/catch and on
parse failure convert the thrown/returned error into the existing
errors.invalidValue format (preserving the CLI prefixes/metavar) before
returning via wrapForMode/dispatchByMode; ensure both sync and async parse paths
and sentinel/default early-return branches use the shared formatter so
bindEnv()/bindConfig() revalidation yields identical formatted errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 11dfb2ce-9b95-42fa-a478-a1df5581fc1f

📥 Commits

Reviewing files that changed from the base of the PR and between 0d05b44 and ef18174.

📒 Files selected for processing (14)
  • CHANGES.md
  • docs/integrations/config.md
  • docs/integrations/env.md
  • packages/config/src/index.test.ts
  • packages/config/src/index.ts
  • packages/core/src/constructs.test.ts
  • packages/core/src/constructs.ts
  • packages/core/src/modifiers.test.ts
  • packages/core/src/modifiers.ts
  • packages/core/src/parser.ts
  • packages/core/src/primitives.test.ts
  • packages/core/src/primitives.ts
  • packages/env/src/index.test.ts
  • packages/env/src/index.ts

dahlia added 4 commits April 8, 2026 10:26
CLI-sourced parser failures are prefixed with the option name or the
argument metavar (and respect options.errors.invalidValue when set) via
a formatInvalidValueError helper defined inside option().complete() /
argument().complete().  Until now, validateValue() did not share that
formatter, so fallback revalidation from bindEnv() / bindConfig() would
surface the raw ValueParser.parse() error without the option/metavar
prefix or the errors.invalidValue customization.

Hoist formatInvalidValueError out of complete() into the enclosing
option() / argument() scope so validateValue() can apply the same
wrapping to round-trip parse failures.  CLI-sourced and fallback-sourced
errors are now identical, which also makes the CHANGES.md claim that
failures are rejected "with the same error that a CLI-sourced value
would produce" accurate.  Also clarify the flag-form option() comment
which previously claimed "the only runtime value is `true`" even though
complete() returns `false` for the missing case.

Adds tests asserting option()/argument() prefixes, the
errors.invalidValue override, and end-to-end bindEnv/bindConfig
fallback errors including the --port prefix; the choice() unit test
switches from the `"purple" as "red"` cast to an explicit
@ts-expect-error directive, matching the repository's negative-type
convention.

Review comments addressed:

- #777 (comment)
- #777 (comment)
- #777 (comment)
- #777 (comment)
- #777 (comment)
- #777 (comment)
…Value

The try/catch wrapper around withDefault() validateValue forwarding is
redundant: option() / argument() validateValue already swallow format()
exceptions internally for sentinel defaults, so the only thing the
outer try/catch would catch is a hypothetical programming error inside
the inner validator.  It also fails to catch async rejections from the
returned Promise, so it never fully delivered on the safety it was
trying to provide.  Delegate directly instead.

getEnvSourceValue() previously checked `parsed instanceof Promise` to
route sync vs. async validation through the inner parser.  This is
against the repository's mode-dispatch rule ("All mode-based type
assertions are isolated in mode-dispatch.ts" in CLAUDE.md), so switch
to mapModeValue() keyed on the env value parser's own \$mode instead.

Review comments addressed:

- #777 (comment)
- #777 (comment)
Until now, multiple() and nonEmpty() did not expose a validateValue
hook, which meant bindConfig(multiple(option("--role",
choice(["admin","user"])))) would silently accept ["root"] from a
config file because the inner choice() constraint never ran on
fallback values.  The same hole applied to multiple()'s own min/max
arity rules: a default shorter than min or longer than max slipped
through bindEnv() / bindConfig() fallback paths.

multiple().validateValue() now:

- runs the inner parser's validateValue() on each array element when
  available (returning the first element failure), and
- re-checks the array length against the configured min / max bounds
  regardless of whether the inner parser has a validator attached.

nonEmpty().validateValue() passes through unchanged from the inner
parser — nonEmpty() is state-shape preserving and only constrains
parse-time token consumption, so there is nothing of its own to
enforce against a fallback value.  Users who need an arity-1 check on
fallback values should use `multiple(..., { min: 1 })` instead.

Adds forwarding tests in modifiers.test.ts and an end-to-end bindConfig
integration test that loads an array containing an out-of-set element
and another asserting the multiple() min rule on defaults.  CHANGES.md
and the env/config integration guides now mention multiple() /
nonEmpty() alongside the other forwarding combinators.

Review comment addressed:

- #777 (comment)
The new bindEnv and bindConfig fallback-validation examples were
plain \`typescript\` fences, so the VitePress build no longer
type-checked them against live Optique types.  The config example also
relied on an undeclared \`configContext\` symbol.  Promote both
examples to \`typescript twoslash\` and add a short fixture prelude
(imports plus the context construction) before the \`// ---cut-before---\`
marker so the docs pipeline validates them while the reader still sees
only the bind call.

Review comments addressed:

- #777 (comment)
- #777 (comment)
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/integrations/env.md`:
- Around line 387-391: Replace the source-specific phrase "config-loaded array"
with a source-neutral term (e.g., "fallback array" or "resolved array") in the
docs text that discusses multiple() validation; update the sentence that
currently reads "a config-loaded array that contains..." so it instead reads
something like "a fallback array that contains an invalid element..." to make
the description apply to both bindEnv() and bindConfig() cases while keeping
references to multiple(), bindEnv(), and bindConfig().

In `@packages/config/src/index.test.ts`:
- Around line 357-371: The test currently only asserts !result.success after
calling parse(parser, []); instead, assert the actual diagnostic from the failed
parse to ensure the failure is due to the inner string pattern (e.g., check
result.success === false and that result.error/diagnostic contains the pattern
validation message or the formatted diagnostic from the inner parser). Update
the test that constructs parser via bindConfig(option("--name", string({
pattern: /^[A-Z]+$/ })), { context, key: "name", default: "abc" as never }) to
assert the specific error fragment (from result.error or the parser's diagnostic
formatter) that indicates the pattern constraint failed rather than relying
solely on success boolean; apply the same change to the other listed tests that
use parse(parser, []) so they assert the stable diagnostic text from the inner
parser.

In `@packages/core/src/modifiers.test.ts`:
- Around line 8022-8041: Add a regression test that verifies
nonEmpty(multiple(...)).validateValue is a direct pass-through to the inner
multiple's validation for empty arrays: construct two inner parsers with
multiple(option("-x", integer(...))) where one has min:0 and the other has
min:1, wrap each with nonEmpty(...), call validateValue!([]) on both wrapped
parsers, and assert that the results match the inner multiple's
validateValue([]) behavior (first succeeds, second fails), referencing the
nonEmpty, multiple and validateValue symbols to locate the code to update.
- Around line 7922-8042: Add tests that exercise async-mode validateValue
forwarding: create async parsers using option("--format", asyncChoice(...)) (or
similar asyncChoice) and wrap them with optional, withDefault, multiple and
nonEmpty, then call parser.validateValue! with Promise-resolving inputs and
assert the returned result shape and success/failure mirrors the inner async
parser (e.g., invalid value yields success: false, undefined passes for
optional, default is accepted for withDefault, arrays validated element-wise for
multiple, empty rejected for nonEmpty). Ensure you include a map case to confirm
async mapping strips validateValue and verify validateValue is undefined for
map-wrapped async parsers.

In `@packages/core/src/modifiers.ts`:
- Around line 2329-2350: The validateArity error messages are inconsistent with
validateMultipleResult; update validateArity (the validateArity function) to use
the same number formatting and wording as validateMultipleResult: replace
String(min), String(max) and String(values.length) usage with
number.toLocaleString("en") (e.g., min.toLocaleString("en"),
max.toLocaleString("en"), values.length.toLocaleString("en")) and adjust
phrasing to match ("but got only ..." instead of "but got ...") so both fallback
validation and CLI parsing produce identical formatted messages.

In `@packages/env/src/index.ts`:
- Around line 543-574: Update the JSDoc for the exported bindEnv() function(s)
to document that the inner parser's validateValue() hook may be invoked during
fallback resolution and therefore can throw from within bindEnv(); add an
`@throws` tag that references innerParser.validateValue / validateSync /
validateAsync as the source of the exception and note that thrown errors can
occur even when no CLI tokens are parsed (fallback/default path). Ensure both
exported overloads/variants of bindEnv (the docblocks around the bindEnv
declaration and the second docblock at the later overload, lines referenced in
review) are updated so all public bindEnv JSDoc blocks include this `@throws`
entry describing the exception path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 37b05e86-7279-4f41-8a5c-4f90cac3f6a2

📥 Commits

Reviewing files that changed from the base of the PR and between ef18174 and ccbf6f1.

📒 Files selected for processing (10)
  • CHANGES.md
  • docs/integrations/config.md
  • docs/integrations/env.md
  • packages/config/src/index.test.ts
  • packages/core/src/modifiers.test.ts
  • packages/core/src/modifiers.ts
  • packages/core/src/primitives.test.ts
  • packages/core/src/primitives.ts
  • packages/env/src/index.test.ts
  • packages/env/src/index.ts

dahlia and others added 4 commits April 8, 2026 11:17
Both integration guides described a \`multiple()\` fallback array as
"config-loaded", but the sentence lives in a section that covers
\`bindEnv()\` and \`bindConfig()\` equally.  The env guide in particular
made the behavior sound specific to \`bindConfig()\`.  Rename to
"fallback array" so the description applies to both env and config
fallback paths.

Review comment addressed:

- #777 (comment)
multiple().validateValue() was constructing arity failures with its
own inline messages: \`String(min)\` formatting, a "but got ..."
phrasing, and no honoring of \`options.errors.tooFew\` / \`tooMany\`.
The CLI path (\`validateMultipleResult\` inside the result-building
branch) already uses \`.toLocaleString("en")\`, a "but got only"
phrasing for the too-few branch, and routes through the custom
error messages.  This left fallback validation errors visibly
different from CLI errors for the same violation.

Rewrite \`validateArity\` to mirror that branch exactly, so the same
\`multiple({ min, errors })\` configuration produces identical error
text regardless of whether the violation was caught during CLI
parsing or during \`bindEnv()\` / \`bindConfig()\` fallback
revalidation.

Adds tests that lock in the formatting (min violation matches
\`"Expected at least 3 values, but got only 1."\`) and that the
\`errors.tooFew\` and \`errors.tooMany\` customizations flow through
\`validateValue\` the same way they flow through \`complete()\`.

Review comment addressed:

- #777 (comment)
Address three CodeRabbit review comments:

-  Tests that assert parse failure without inspecting the error could
   pass even if the rejection came from an unrelated code path.  Lock
   in the actual pattern-violation / bounds-violation errors by
   formatting the message and checking it mentions the offending
   option name and the relevant constraint.
-  Add a regression test for the `nonEmpty(multiple(..., { min: 0 }))`
   empty-array case, documenting that `nonEmpty()` delegates fallback
   arity entirely to the inner parser instead of layering an extra
   non-empty check on top.
-  Cover async mode explicitly for `optional()`, `withDefault()`,
   `multiple()`, `nonEmpty()`, and `map()` to prove `validateValue`
   forwarding (and the `map()` strip) behave the same way when the
   inner value parser is async.

See:

-  #777 (comment)
-  #777 (comment)
-  #777 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Since the fallback-validation fix for #414 landed, both bindEnv() and
bindConfig() invoke the inner parser's validateValue() hook while
resolving environment/config/default values.  A throwing inner
validator now surfaces out of bindEnv()/bindConfig() even when no CLI
tokens are parsed, so the public JSDoc must list that exception path.

Per the project guideline that every function/method which can throw
must document the throw via @throws, extend the existing @throws
entries for both exported wrappers to name validateValue() explicitly
and cross-reference issue #414.

See: #777 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dahlia dahlia requested a review from Copilot April 8, 2026 02:30
@dahlia
Copy link
Copy Markdown
Owner Author

dahlia commented Apr 8, 2026

@codex review

@dahlia
Copy link
Copy Markdown
Owner Author

dahlia commented Apr 8, 2026

/gemini review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the Parser.validateValue() method to the core library, enabling bindEnv() and bindConfig() to re-validate fallback values—such as environment variables, configuration files, and defaults—against the constraints of the inner CLI parser. The implementation includes logic to forward this validation through common combinators like optional(), withDefault(), and multiple(), while intentionally excluding it from one-way transformations like map(). Feedback was provided regarding the multiple() modifier's validation logic, specifically to ensure that non-array inputs are rejected with descriptive error messages that include the received type.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/env/src/index.ts (1)

643-666: ⚠️ Potential issue | 🟡 Minor

Add the validateValue() exception path to this helper’s JSDoc.

getEnvSourceValue() now revalidates successful fallbacks via innerParser.validateValue(), so it can throw from that path in addition to the env-source/value-parser calls already documented here.

As per coding guidelines, "Functions or methods that throw exceptions must include the @throws tag in their JSDoc comments."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/env/src/index.ts` around lines 643 - 666, The JSDoc for
getEnvSourceValue is missing the new exception path: update its `@throws` block to
also document that innerParser.validateValue() can throw when revalidating
fallback values (in addition to errors from sourceData.source(fullKey) and
options.parser.parse(rawValue)); reference getEnvSourceValue and
innerParser.validateValue in the description so callers know validation errors
propagate from that hook.
packages/config/src/index.ts (1)

971-986: ⚠️ Potential issue | 🟡 Minor

Document the new validateValue() throw path on these fallback helpers.

getConfigOrDefault(), validateFallbackValue(), and getConfigSourceValue() now propagate exceptions from innerParser.validateValue(), but the updated docblocks still only document the key-callback TypeError (or nothing for the new helper). Add explicit @throws entries so the helper contract matches the implementation.

As per coding guidelines, "Functions or methods that throw exceptions must include the @throws tag in their JSDoc comments."

Also applies to: 1057-1075, 1078-1101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/index.ts` around lines 971 - 986, Update the JSDoc for
the fallback helper functions to document that exceptions thrown by
innerParser.validateValue() are propagated: add an explicit "@throws {Error}"
(or more specific error type if known) to the docblocks for getConfigOrDefault,
validateFallbackValue, and getConfigSourceValue indicating that validation
errors from innerParser.validateValue() will be thrown; do the same for the
related docblocks in the other ranges noted (lines ~1057-1075 and ~1078-1101) so
each helper's contract matches the implementation and mentions both the
key-callback TypeError and the validateValue propagation.
♻️ Duplicate comments (1)
packages/config/src/index.test.ts (1)

461-475: ⚠️ Potential issue | 🟡 Minor

Pin the failure cause in the remaining fallback-validation tests.

These cases still pass on any failure, including an unrelated missing-value/default path. Assert a stable diagnostic fragment—or compare against the equivalent CLI failure result—so the tests prove the new validateValue() forwarding is what failed.

Based on learnings, "when verifying behavioral equivalence between two parser configurations (e.g., uuid() default vs uuid({ strict: true })), prefer asserting deep equality of the entire parse results ... rather than only comparing .success, so differences in returned values or diagnostics are detected."

Also applies to: 477-494, 514-538, 540-563, 565-589, 591-625, 681-698

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/index.test.ts` around lines 461 - 475, The test currently
only checks result.success and can pass for unrelated failures; update the
failing tests (e.g., the case using createConfigContext + bindConfig +
optional(option("--name", string(...))) and other similar blocks) to assert a
stable diagnostic fragment or perform deep equality against the equivalent CLI
parse result instead of only checking .success. Locate the parser creation
(bindConfig/optional/option), call parse(parser, []) as before, then compare
result.diagnostics (or the full result object) to the expected diagnostics/parse
result from the CLI-equivalent run so the assertion pins the validateValue()
forwarding failure rather than any missing-default path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/integrations/config.md`:
- Around line 818-832: The paragraph incorrectly implies wrapping a parser in
map()/derive()/deriveFrom() disables all fallback checks and that nonEmpty()
enforces array non-emptiness itself; update the text to state precisely that
map()/derive()/deriveFrom() suppress revalidation of the inner parser's
validateValue only (so inner value-level validation is skipped), while outer
combinators like multiple() always perform their own checks
(multiple().validateValue enforces min/max arity unconditionally), and
nonEmpty() merely forwards the inner validateValue hook unchanged rather than
adding its own arity enforcement. Mention the specific symbols map, derive,
deriveFrom, multiple, nonEmpty, and validateValue so readers can locate the
behavior.

In `@packages/core/src/modifiers.test.ts`:
- Line 8113: Replace the typed casts that mask intentionally invalid inputs by
changing the test calls like parser.validateValue!("xml" as "json") to use an
explicit as never cast (e.g., "xml" as never) so the runtime validation tests
clearly signal invalid-type inputs; update all occurrences where
parser.validateValue! (and similar test invocations at the noted spots) use
"xml" as "json" to use as never instead.

In `@packages/core/src/modifiers.ts`:
- Around line 2371-2376: The current branch in modifiers.ts returns success when
a fallback for a multiple(...) parser is not an array (the if
(!Array.isArray(values)) block), which lets mis-typed defaults slip through
bindEnv()/bindConfig(); instead, change that branch to reject non-array
fallbacks by returning a failure ModeValue for parser.$mode (i.e., a
ModeValue<M, ValueParserResult<readonly TValue[]>>) with success: false and a
clear error describing "expected array for multiple(...) but got <type/value>",
so validateValue() remains the guardrail and multiple(...) only accepts arrays
from defaults/fallbacks.

---

Outside diff comments:
In `@packages/config/src/index.ts`:
- Around line 971-986: Update the JSDoc for the fallback helper functions to
document that exceptions thrown by innerParser.validateValue() are propagated:
add an explicit "@throws {Error}" (or more specific error type if known) to the
docblocks for getConfigOrDefault, validateFallbackValue, and
getConfigSourceValue indicating that validation errors from
innerParser.validateValue() will be thrown; do the same for the related
docblocks in the other ranges noted (lines ~1057-1075 and ~1078-1101) so each
helper's contract matches the implementation and mentions both the key-callback
TypeError and the validateValue propagation.

In `@packages/env/src/index.ts`:
- Around line 643-666: The JSDoc for getEnvSourceValue is missing the new
exception path: update its `@throws` block to also document that
innerParser.validateValue() can throw when revalidating fallback values (in
addition to errors from sourceData.source(fullKey) and
options.parser.parse(rawValue)); reference getEnvSourceValue and
innerParser.validateValue in the description so callers know validation errors
propagate from that hook.

---

Duplicate comments:
In `@packages/config/src/index.test.ts`:
- Around line 461-475: The test currently only checks result.success and can
pass for unrelated failures; update the failing tests (e.g., the case using
createConfigContext + bindConfig + optional(option("--name", string(...))) and
other similar blocks) to assert a stable diagnostic fragment or perform deep
equality against the equivalent CLI parse result instead of only checking
.success. Locate the parser creation (bindConfig/optional/option), call
parse(parser, []) as before, then compare result.diagnostics (or the full result
object) to the expected diagnostics/parse result from the CLI-equivalent run so
the assertion pins the validateValue() forwarding failure rather than any
missing-default path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9e8ed2fc-b45e-4f58-9096-f95fa5952673

📥 Commits

Reviewing files that changed from the base of the PR and between ccbf6f1 and 57e07e1.

📒 Files selected for processing (7)
  • docs/integrations/config.md
  • docs/integrations/env.md
  • packages/config/src/index.test.ts
  • packages/config/src/index.ts
  • packages/core/src/modifiers.test.ts
  • packages/core/src/modifiers.ts
  • packages/env/src/index.ts

dahlia and others added 5 commits April 8, 2026 11:53
Fallback validation is the only barrier between a mis-typed default
(e.g., one escaped through \`as never\`) and the parsed result now
that bindEnv() / bindConfig() re-run inner validators on fallback
paths.  multiple() can never produce a non-array shape from CLI
input, so a non-array fallback is always a type error and must be
surfaced instead of silently passed through.

Replace the non-array escape hatch in multiple().validateValue with
an explicit failure that names the received type so users can locate
the offending source.  Cover both "string" and "null" shapes in a
new regression test.

See:

-  #777 (comment)
-  #777 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous paragraph in both env and config docs said validation
was forwarded through \`nonEmpty()\` and claimed that wrapping an
inner parser in \`map()\` / \`derive()\` / \`deriveFrom()\` would
\"silently bypass fallback validation\".  Both read the shipped
behavior imprecisely:

 -  \`nonEmpty()\` does not add its own arity check on the fallback
    path; it merely forwards the inner parser's \`validateValue\`
    hook unchanged.  Listing it alongside forwarding combinators
    implied users get an empty-array rejection for free from
    \`nonEmpty(multiple(...))\`, which is not the case.
 -  \`map()\` / \`derive()\` / \`deriveFrom()\` only strip the inner
    parser's \`validateValue\`.  Outer combinators layered above them
    (notably \`multiple()\`'s own unconditional arity enforcement)
    still apply to fallback values.

Rewrite the paragraph to split the three behaviors — plain
forwarding, \`multiple()\`'s unconditional arity + non-array
rejection, \`nonEmpty()\`'s pass-through, and the \`map()\` / derive
strip — so readers can tell exactly which constraints survive each
wrapper.

See: #777 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The \`toAsyncParser()\` test helper destructures \`validateValue\` off
the sync parser and re-attaches it on the async wrapper.  Before this
change it called the destructured function directly, dropping whatever
\`this\` binding the original implementation might have relied on.

Other forwarding sites in the codebase (e.g., \`bindEnv()\`,
\`bindConfig()\`, \`optional()\`) explicitly bind \`parser\` when
forwarding \`validateValue\`.  Optique's own primitives don't use
\`this\` today, but keeping the helper consistent with the rest of
the codebase prevents third-party custom parsers from subtly breaking
when their sync variant is tested through the async wrapper.

Use \`validateValue.call(parser, v)\` since we already hold the
destructured reference.

See: #777 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The flag-form \`option()\` test for \`validateValue\` previously
stated \"the only valid value is \`true\`\" and only asserted the
\`true\` branch.  \`option().complete()\` actually yields \`false\`
when the flag is missing, and the attached trivial validator accepts
both booleans, so the comment was misleading and the coverage
one-sided.

Rewrite the comment to reflect the shipped semantics and add an
assertion that \`validateValue(false)\` also returns success.

See: #777 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The async-mode validateValue tests used \`\"xml\" as \"json\"\` to
force a type-level invalid value through the \`choice(['json',
'yaml'])\` validator.  That cast obscures the intent — the literal
is being coerced into a type it cannot structurally satisfy — while
\`as never\` is the project's established signal for \"this is
deliberately an invalid-type test input\" (already used in the
bindEnv/bindConfig fallback validation tests added earlier in this
PR).

Replace all four occurrences in the async-mode block so the
invalid-input tests are easier to grep for and match the repo-wide
convention.

See: #777 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dahlia dahlia requested a review from Copilot April 8, 2026 03:04
@dahlia
Copy link
Copy Markdown
Owner Author

dahlia commented Apr 8, 2026

@codex review

@dahlia
Copy link
Copy Markdown
Owner Author

dahlia commented Apr 8, 2026

/gemini review

Per the project guideline that every function which can throw must
document the throw via @throws, extend the docblocks of the internal
fallback helpers so their contracts match the implementation:

-  \`getEnvSourceValue()\` in @optique/env — already mentioned throws
   from the env source callback and \`options.parser.parse()\`, but
   not the \`innerParser.validateValue()\` call added in #414.
-  \`getConfigOrDefault()\` in @optique/config — only mentioned the
   \`TypeError\` thrown when a key callback returns a thenable.
-  \`validateFallbackValue()\` in @optique/config — had no @throws
   entry at all even though it is the actual call site for
   \`innerParser.validateValue()\`.
-  \`getConfigSourceValue()\` in @optique/config — only mentioned the
   key-callback \`TypeError\`.

Each docblock now explicitly names \`innerParser.validateValue()\` as
a possible throw source and cross-references issue #414 so callers
know validation errors can escape from these helpers.

See: #777

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the Parser.validateValue() method to enforce CLI-level constraints (such as regex patterns and numeric bounds) on fallback values sourced from environment variables, configuration files, and defaults. The implementation includes updates to bindEnv() and bindConfig() to trigger re-validation, along with forwarding logic for standard combinators and modifiers. Review feedback identifies a type mismatch in the validator for flag-form options when operating in asynchronous mode and highlights several instances where manual mode-based type assertions should be replaced by existing helpers to comply with the repository's style guide.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e24e3b8a71

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/integrations/env.md`:
- Around line 361-362: The doc line claiming "Since Optique 1.0.0" is incorrect
for the fallback revalidation change; update the sentence in
docs/integrations/env.md that begins with "Since Optique 1.0.0" to either the
correct release that includes PR `#777` (replace "Optique 1.0.0" with the actual
release version) or remove the "Since ..." clause entirely if the release is not
finalized, and optionally mention "PR `#777`" for traceability; locate the
sentence around the phrase "fallback values produced by `bindEnv()` are
re-validated" and edit it accordingly.

In `@packages/core/src/modifiers.ts`:
- Around line 2397-2413: multiple().validateValue() is returning the original
values array instead of the inner parser's normalized results; update both
branches inside dispatchByMode (the sync branch where innerValidate is called
and the async branch where await innerValidate is used) to collect each
successful r.value into a new array (e.g., normalizedValues) and return that
normalized array as the value in the success result instead of returning the
original values variable; ensure types remain ModeValue<M,
ValueParserResult<readonly TValue[]>> and keep the early-return behavior on
r.success === false.

In `@packages/core/src/primitives.test.ts`:
- Around line 7148-7162: The test currently skips assertions if
parser.validateValue is missing, letting regressions pass; update the boolean
option test to assert that parser.validateValue exists before using it by
explicitly failing when typeof parser.validateValue !== "function" (e.g.,
assert.ok(typeof parser.validateValue === "function", "...")), then continue to
call parser.validateValue(true) and parser.validateValue(false) and assert their
success results; reference the parser.validateValue checks in the boolean option
test so the test fails hard if the hook is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 47908938-36aa-4d09-91cc-7fd84fb8e634

📥 Commits

Reviewing files that changed from the base of the PR and between 57e07e1 and e24e3b8.

📒 Files selected for processing (6)
  • docs/integrations/config.md
  • docs/integrations/env.md
  • packages/core/src/constructs.test.ts
  • packages/core/src/modifiers.test.ts
  • packages/core/src/modifiers.ts
  • packages/core/src/primitives.test.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/env/src/index.ts (1)

547-618: ⚠️ Potential issue | 🟡 Minor

Add a @throws docblock for getEnvOrDefault().

This helper now calls sourceData.source(), options.parser.parse(), and innerParser.validateValue(), but it still has no JSDoc @throws. Please document that internal throw contract here as well.

Suggested doc fix
+/**
+ * Resolves an env-backed fallback value from the environment or the
+ * configured default.
+ *
+ * `@throws` {Error} Propagates errors thrown by the env source callback
+ *                 (`sourceData.source(fullKey)`), `options.parser.parse()`,
+ *                 or `innerParser.validateValue()` while re-validating
+ *                 fallback values (see issue `#414`).
+ */
 function getEnvOrDefault<M extends Mode, TValue>(
   state: unknown,
   options: BindEnvOptions<M, TValue>,
   mode: M,

As per coding guidelines, "Functions or methods that throw exceptions must include the @throws tag in their JSDoc comments."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/env/src/index.ts` around lines 547 - 618, Add a JSDoc `@throws` entry
to the getEnvOrDefault() comment that documents the internal throw contracts:
note it can throw errors from sourceData.source() (when reading the env source),
from options.parser.parse() (sync or async parsing), and from
innerParser.validateValue() (validation failures), and include brief
descriptions for each to aid callers; update the existing JSDoc directly above
getEnvOrDefault() to list these three possible throw sources.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/env/src/index.ts`:
- Around line 547-618: Add a JSDoc `@throws` entry to the getEnvOrDefault()
comment that documents the internal throw contracts: note it can throw errors
from sourceData.source() (when reading the env source), from
options.parser.parse() (sync or async parsing), and from
innerParser.validateValue() (validation failures), and include brief
descriptions for each to aid callers; update the existing JSDoc directly above
getEnvOrDefault() to list these three possible throw sources.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8a54fda0-0371-4625-94b7-c73df25b447b

📥 Commits

Reviewing files that changed from the base of the PR and between e24e3b8 and e0d070a.

📒 Files selected for processing (2)
  • packages/config/src/index.ts
  • packages/env/src/index.ts

dahlia and others added 3 commits April 8, 2026 12:28
The flag-form \`option()\` \`validateValue\` implementation had three
related issues called out in the latest review round:

-  The return type was hardcoded to \`ModeValue<\"sync\", ...>\` with a
   non-\`wrapForMode\` sentinel cast, which violates the declared
   \`ModeValue<M, ...>\` contract even though at runtime M is always
   \"sync\" for a valueless option.
-  The validator accepted any value unconditionally, so a
   \`bindEnv()\` / \`bindConfig()\` fallback coming from an
   \`unknown\`-typed config / env source (e.g., a string \"yes\") would
   leak through as the parsed result even though the CLI parser can
   only ever produce a boolean.
-  The existing test gated assertions behind
   \`if (typeof parser.validateValue === \"function\")\`, so the #414
   regression contract would silently disappear if the hook were ever
   dropped.

Rework the validator to:

-  Use \`wrapForMode(mode, ...)\` so the returned shape matches
   \`ModeValue<M, ValueParserResult<boolean>>\`, matching the pattern
   used for the non-flag branch just below.
-  Reject non-boolean inputs with an option-scoped error produced via
   the shared \`formatInvalidValueError\` helper (so it picks up the
   \`options.errors.invalidValue\` customization uniformly).

Tighten the existing test to hard-fail on a missing hook and add a
new regression test that covers both string and number fallback
rejection with an assertion on the option-name prefix.

See:

-  #777 (comment)
-  #777 (comment)
-  #777 (comment)
-  #777 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The per-element loop in \`multiple().validateValue\` walked each
element through the inner parser's \`validateValue\` but then threw
away the returned \`r.value\` and returned the original \`values\`
array.  If the inner parser canonicalizes on a successful round-trip
— for example a URL parser that strips a trailing slash, or a string
parser that folds casing — the CLI parse path uses the canonical
value but the fallback path would silently leak the pre-canonical
one, breaking the \"same semantics as CLI\" guarantee that issue
#414's fix is supposed to provide.

Collect each successful \`r.value\` into a new array, track whether
any element actually changed, and return the new array only when a
change occurred (so the common \"already canonical\" path does not
pay for a pointless copy).  Mirror the fix across both the sync and
async dispatch branches.

Regression tested with an inline uppercasing \`ValueParser\` (one
sync, one async) that feeds \`[\"hello\", \"world\"]\` into the
fallback path and asserts the returned array is
\`[\"HELLO\", \"WORLD\"]\`.

See:

-  #777 (comment)
-  #777 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The project style guide asks that any \`as ModeValue<M, T>\`
assertions live inside \`mode-dispatch.ts\`, because the helpers
exported from there (\`wrapForMode\`, \`dispatchByMode\`,
\`mapModeValue\`) already encode the unsafe narrowing once and
return a properly typed \`ModeValue<M, T>\`.  The fallback-validation
work added in #414 left three fresh hand-written casts behind that
the helpers can absorb instead:

-  \`option()\` -> \`argument()\` \`validateValue\` in
   \`primitives.ts\`: the three \`wrapForMode(vpMode, ...) as
   ModeValue<M, ValueParserResult<T>>\` and
   \`dispatchByMode(...) as ModeValue<M, ...>\` sites can be replaced
   with explicit generic parameters on the helper calls.
-  \`optional()\` \`validateValue\` in \`modifiers.ts\`: the
   \`wrapForMode(..., { success: true, value: v })\` cast for the
   \`undefined\` branch can be passed as an explicit generic, and the
   \`innerValidate(v) as ModeValue<...>\` cast is redundant since
   \`ValueParserResult<TValue>\` is structurally a subtype of
   \`ValueParserResult<TValue | undefined>\` under \`ModeValue<M, …>\`.
-  \`multiple()\` \`validateValue\` in \`modifiers.ts\`: four casts on
   \`wrapForMode\` / \`dispatchByMode\` calls collapse into explicit
   generic parameters.
-  \`withDefault()\` dependency-source branch in \`modifiers.ts\`: the
   \`mapModeValue(...) as ModeValue<...>\` cast collapses into an
   explicit generic parameter.

No behavior change; the full test suite still passes.  Other
\`as ModeValue<...>\` sites in the codebase (e.g., the facade /
\`parse()\` API surface and the internal helpers inside
\`mode-dispatch.ts\`) are outside the review scope and left alone.

See:

-  #777 (comment)
-  #777 (comment)
-  #777 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dahlia dahlia requested a review from Copilot April 8, 2026 03:37
@dahlia
Copy link
Copy Markdown
Owner Author

dahlia commented Apr 8, 2026

@codex review

@dahlia
Copy link
Copy Markdown
Owner Author

dahlia commented Apr 8, 2026

/gemini review

\`getEnvOrDefault()\` had no JSDoc at all — just a lowercase comment
inside the function body describing the inline \`validateSync\` /
\`validateAsync\` helpers.  Per the project guideline that every
function which can throw must document the throw via \`@throws\`,
attach a proper JSDoc block that lists the four sources of thrown
errors the helper propagates:

-  \`sourceData.source(fullKey)\` (env source callback, sync/async).
-  \`options.parser.parse(rawValue)\` (env value parser, sync/async).
-  \`innerParser.validateValue()\` when re-validating a successful
   env-sourced value or the configured \`default\`.
-  \`innerParser.complete()\` when falling through to the inner
   parser because neither env nor default is set (important for
   nested \`bindEnv(bindConfig(...))\` composition).

Also document the parameter list so future readers don't have to
guess why \`innerParser\`, \`innerState\`, and \`exec\` are optional.

See: #777

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the Parser.validateValue() method to the @optique/core package, enabling the re-validation of fallback values—such as environment variables, configuration file entries, and hardcoded defaults—against a parser's underlying constraints. The implementation updates bindEnv() and bindConfig() to enforce these constraints and includes logic in combinators like optional(), withDefault(), and multiple() to forward or implement validation. Documentation and extensive regression tests for issue #414 have been added across the affected packages. I have no feedback to provide.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@dahlia dahlia merged commit 8727623 into main Apr 8, 2026
6 of 8 checks passed
@dahlia dahlia deleted the fix/issue-414-bind-validation branch April 8, 2026 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bindEnv() and bindConfig() do not revalidate fallback values with the bound parser

2 participants