Skip to content

feat: ability to limit combinations#3372

Closed
RobinTail wants to merge 21 commits intomasterfrom
limit-combinations-2
Closed

feat: ability to limit combinations#3372
RobinTail wants to merge 21 commits intomasterfrom
limit-combinations-2

Conversation

@RobinTail
Copy link
Copy Markdown
Owner

@RobinTail RobinTail commented May 6, 2026

Instead of #3344

Summary by CodeRabbit

  • New Features

    • Docs generator gains configurable limits for example and security combination generation (defaults unlimited) and threads example limits through request/response depiction.
    • Documentation now detects and surfaces security-related headers in generated parameter lists.
  • Tests

    • Added/updated tests for limits, header extraction, max-examples behavior, and example-combination handling.

@RobinTail RobinTail added the enhancement New feature or request label May 6, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1b71781c-27c1-4adf-a8de-4486839e678d

📥 Commits

Reviewing files that changed from the base of the PR and between 4fd3b23 and 4b4847b.

📒 Files selected for processing (1)
  • express-zod-api/src/documentation.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • express-zod-api/src/documentation.ts

📝 Walkthrough

Walkthrough

Threads configurable limits for example and security-schema combination generation through core helpers, JSON-schema flattening, documentation depiction, endpoint example registration, logical-container processing, diagnostics, and tests; adds header extraction for security configurations and documents the change in CHANGELOG v27.4.0.

Changes

Example & Documentation Limits

Layer / File(s) Summary
Foundation helpers
express-zod-api/src/common-helpers.ts
Add exported defaultMaxCombinations; refactor combinations(left, right, merge, limit) to nested loops with early-exit and optional limit.
Core JSON-schema shaping
express-zod-api/src/json-schema-helpers.ts
Replace MergeMode with options objects; processAllOf(..., { isStrict, isOptional }), mergeExamples(..., { isOptional, limit }), flattenIO(..., { isStrict, maxExamples }), and pullRequestExamples(subject, limit) to support max example limits.
Documentation pipeline
express-zod-api/src/documentation-helpers.ts, express-zod-api/src/documentation.ts
Add ReqResCommons.maxExamples?; thread maxExamples through depictors (depictIntersection, depictRequestParams, depictResponse, depictRequest, depictBody). defaultIsHeader now accepts Set<string>. DocumentationParams gains limits?: { examples?: number; security?: number }; constructor extracts and propagates maxExamples/maxSecurity, obtains securityHeaders via getSecurityHeaders(...), and applies security limits via processContainers(..., maxSecurity).
Security header extraction
express-zod-api/src/security.ts
Add pickHeaders and exported getSecurityHeaders(containers) returning Set<string> of header names extracted from LogicalContainer structures.
Core combination usage
express-zod-api/src/logical-container.ts
processContainers(containers, maxCombinations = defaultMaxCombinations) added; calls combinations<...> with a bounded limit.
Response example pulling
express-zod-api/src/result-helpers.ts
pullResponseExamples(subject, limit = defaultMaxCombinations) added; use R.mergeRight combiner and pass limit into combinations.
Endpoint wiring / caching
express-zod-api/src/endpoint.ts
getResponses(variant, { maxExamples? }) signature; new #ensureOutputExamples(limit?) uses the provided limit when pulling/registering output examples.
Diagnostics & Integration
express-zod-api/src/diagnostics.ts, express-zod-api/src/integration.ts
Pass { maxExamples: 0 } to endpoint example/IO inspection calls to avoid generating examples during discovery/path checks.
Tests & Changelog
express-zod-api/tests/*, CHANGELOG.md
Update/add tests for new signatures (Sets, options objects, limits) and assert limit behavior for combinations, mergeExamples, pullRequestExamples, pullResponseExamples, processContainers, getSecurityHeaders, and getResponses. Add v27.4.0 changelog entry documenting limits.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

documentation, coverage

🐰
I counted combos, trimmed the sprawl,
set gentle caps to tame them all.
Headers gathered in a handy Set,
docs and tests now nicely met.
Hopped away with a cheerful nibble.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: ability to limit combinations' directly and clearly summarizes the main change: adding a configurable limit parameter to the combinations utility and propagating this capability throughout the codebase for controlling example generation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch limit-combinations-2

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@coveralls-official
Copy link
Copy Markdown

coveralls-official Bot commented May 6, 2026

Coverage Status

coverage: 100.0%. remained the same — limit-combinations-2 into master

@RobinTail RobinTail force-pushed the limit-combinations-2 branch from ea89779 to 83e0642 Compare May 6, 2026 09:53
@RobinTail RobinTail marked this pull request as ready for review May 6, 2026 12:18
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Important

Two semantic mismatches between the CHANGELOG and the implementation, plus a footgun where limits.security: 0 silently drops the security field from generated operations. Public API surface is unaffected (none of the touched helpers are re-exported from src/index.ts).

Minor test gap: the top-level new Documentation({ limits: { ... } }) path has no end-to-end coverage, and Endpoint.getResponses({ maxExamples: N }) is only exercised with N = 0 (where the limit is a no-op on schemas without examples).

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread express-zod-api/src/json-schema-helpers.ts
Comment thread express-zod-api/src/logical-container.ts Outdated
Comment thread express-zod-api/src/endpoint.ts Outdated
Comment thread express-zod-api/tests/endpoint.spec.ts
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Incremental review: the new commit adds maxExamples = defaultMaxCombinations default on getResponses and gates #ensureOutputExamples behind maxExamples > 0, addressing the prior "required params arg without default" nit. Nothing new to flag in this range. Prior findings on CHANGELOG wording, mergeExamples asymmetry at limit === 0, and processContainers(..., 0) producing empty security are unaddressed but still stand.

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@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: 1

🧹 Nitpick comments (3)
express-zod-api/tests/result-helpers.spec.ts (1)

120-121: ⚡ Quick win

Assert exact limited payloads, not just count

Line 120 currently verifies only the count. This can still pass if the limiter returns wrong/duplicated examples. Please assert the exact returned subset to make the limit behavior deterministic.

Suggested test tightening
-      expect(pullResponseExamples(schema, 3)).toHaveLength(3);
+      expect(pullResponseExamples(schema, 3)).toEqual([
+        { a: "one", b: 1, c: false },
+        { a: "one", b: 2, c: false },
+        { a: "two", b: 1, c: false },
+      ]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@express-zod-api/tests/result-helpers.spec.ts` around lines 120 - 121, The
test only checks the count from pullResponseExamples(schema, 3) which can hide
duplicate/wrong items; update the assertion to verify the exact subset returned
(e.g., compare to the expected first three examples) by replacing the
toHaveLength check with an equality assertion (use toEqual) against the
deterministic array you expect from the schema variable so the limiter behavior
is strictly validated for pullResponseExamples.
express-zod-api/tests/logical-container.spec.ts (1)

209-210: ⚡ Quick win

Strengthen max-combination test with concrete expected output

Line 209 only checks array size. This won’t catch incorrect truncation logic (e.g., wrong branch preference). Please assert concrete combinations for the capped result.

Suggested assertion update
-      ).toHaveLength(2);
+      ).toEqual([
+        [1, 2, 5, 6],
+        [1, 2, 7, 8],
+      ]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@express-zod-api/tests/logical-container.spec.ts` around lines 209 - 210, The
test currently only asserts ".toHaveLength(2)" which misses incorrect truncation
or branch preference; replace the length-only assertion on the produced
combinations with a concrete equality/assertion that lists the expected
combination objects/strings (instead of just size) so the capped-result behavior
is verified precisely—locate the assertion using the existing ".toHaveLength(2)"
in the test and change it to assert the exact array of combinations (or use an
order-insensitive matcher like toEqual with a sorted/normalized array or
expect.arrayContaining) for the function under test.
express-zod-api/src/json-schema-helpers.ts (1)

89-112: 💤 Low value

Optional: branches handle limit <= 0 asymmetrically.

The optional branch short-circuits with if (!(limit > 0)) return;, preserving any existing target.examples. The non-optional branch unconditionally calls combinations(...), which returns [] when limit <= 0, overwriting a previously populated target.examples with an empty array. Consider hoisting the guard before the branch for consistent behavior.

♻️ Symmetric guard
   if (!entry.examples?.length) return;
+  if (!(limit > 0)) return;
   if (isOptional) {
-    if (!(limit > 0)) return;
     target.examples = R.concat(target.examples || [], entry.examples).slice(
       0,
       limit,
     );
   } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@express-zod-api/src/json-schema-helpers.ts` around lines 89 - 112, In
mergeExamples, the guard for limit > 0 is only applied in the optional branch
which lets the non-optional branch overwrite target.examples with [] when limit
<= 0; move the limit check to the top of mergeExamples (before branching on
isOptional) and return early when limit <= 0 so both branches preserve existing
target.examples; keep the rest of the logic involving target.examples,
entry.examples, combinations and R.mergeDeepRight unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@express-zod-api/src/documentation.ts`:
- Around line 90-103: Add JSDoc for the new "limits" property on the
DocumentationParams type (used by the Documentation constructor) and for its
sub-properties "examples" and "security": include `@desc` describing purpose,
`@default` referencing defaultMaxCombinations, and `@example` showing a concrete
numeric example (e.g., 10) for both examples and security; ensure the top-level
limits JSDoc explains it groups per-feature combination limits and includes
`@default` and `@example` as well so the public API (Documentation constructor
params) has full documentation coverage.

---

Nitpick comments:
In `@express-zod-api/src/json-schema-helpers.ts`:
- Around line 89-112: In mergeExamples, the guard for limit > 0 is only applied
in the optional branch which lets the non-optional branch overwrite
target.examples with [] when limit <= 0; move the limit check to the top of
mergeExamples (before branching on isOptional) and return early when limit <= 0
so both branches preserve existing target.examples; keep the rest of the logic
involving target.examples, entry.examples, combinations and R.mergeDeepRight
unchanged.

In `@express-zod-api/tests/logical-container.spec.ts`:
- Around line 209-210: The test currently only asserts ".toHaveLength(2)" which
misses incorrect truncation or branch preference; replace the length-only
assertion on the produced combinations with a concrete equality/assertion that
lists the expected combination objects/strings (instead of just size) so the
capped-result behavior is verified precisely—locate the assertion using the
existing ".toHaveLength(2)" in the test and change it to assert the exact array
of combinations (or use an order-insensitive matcher like toEqual with a
sorted/normalized array or expect.arrayContaining) for the function under test.

In `@express-zod-api/tests/result-helpers.spec.ts`:
- Around line 120-121: The test only checks the count from
pullResponseExamples(schema, 3) which can hide duplicate/wrong items; update the
assertion to verify the exact subset returned (e.g., compare to the expected
first three examples) by replacing the toHaveLength check with an equality
assertion (use toEqual) against the deterministic array you expect from the
schema variable so the limiter behavior is strictly validated for
pullResponseExamples.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c388c653-9982-4e07-a342-df2fb36da197

📥 Commits

Reviewing files that changed from the base of the PR and between 3427c98 and 72fdf78.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • express-zod-api/src/common-helpers.ts
  • express-zod-api/src/diagnostics.ts
  • express-zod-api/src/documentation-helpers.ts
  • express-zod-api/src/documentation.ts
  • express-zod-api/src/endpoint.ts
  • express-zod-api/src/integration.ts
  • express-zod-api/src/json-schema-helpers.ts
  • express-zod-api/src/logical-container.ts
  • express-zod-api/src/result-helpers.ts
  • express-zod-api/src/security.ts
  • express-zod-api/tests/common-helpers.spec.ts
  • express-zod-api/tests/documentation-helpers.spec.ts
  • express-zod-api/tests/endpoint.spec.ts
  • express-zod-api/tests/json-schema-helpers.spec.ts
  • express-zod-api/tests/logical-container.spec.ts
  • express-zod-api/tests/result-helpers.spec.ts
  • express-zod-api/tests/security.spec.ts

Comment thread express-zod-api/src/documentation.ts
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Important

The new commit addresses the prior processContainers(_, 0) footgun by clamping the per-step limit to Math.max(maxCombinations || 0, 1), so limits.security: 0 no longer wipes the documented security field. The fix itself is correct, but no test pins the new contract — a future revert of the early-exit removal would not be caught by CI.

Reviewed the following changes:

  • Removed the if (!(maxCombinations > 0)) return []; early exit in processContainers and clamped the inner combinations() limit to at least 1, so limits.security: 0 produces a single combination instead of an empty security array
  • Tweaked the limits.security JSDoc to note the ≥1 contract and re-pointed @see at Middleware

Prior review feedback:

  • processContainers(_, 0) produced empty security on documented operations — addressed via the clamp

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread express-zod-api/src/logical-container.ts
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues. Reviewed the following changes:

  • Replaced the if (!(limit > 0)) return; early-exit in mergeExamples' optional branch with a Math.max(0, limit || 0) clamp on slice, so both branches now wipe target.examples to [] when limit <= 0 (matching the non-optional branch's combinations(..., 0) → [] behavior).

Prior review feedback:

  • mergeExamples asymmetric behavior at limit === 0 — addressed via the slice clamp (resolved by aligning the optional branch to also wipe, rather than the originally suggested early-return on the non-optional branch).

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues. Reviewed the following changes:

  • Added a parameterised test for Endpoint.getResponses("positive", { maxExamples }) covering 0, -1, 1, 2, and NaN, asserting the resulting examples array length matches maxExamples > 0 ? maxExamples : 0.

Prior review feedback:

  • Endpoint.getResponses({ maxExamples: N }) only exercised with N = 0 — addressed by the new parameterised test.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@express-zod-api/tests/endpoint.spec.ts`:
- Around line 293-295: The parameterized test using test.each in the "should
respect the limit=%s of examples for positive variant" title should include the
index placeholder %# per the test naming guideline; update the test title string
passed to test.each to "should respect the limit=%s of examples for positive
variant (case %#)" or similar so it contains both %s and %#, leaving the
test.each([...]) and the test callback (maxExamples) unchanged.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: f83a1510-0bb5-4114-bb9c-c944ce2ee708

📥 Commits

Reviewing files that changed from the base of the PR and between 330ccb3 and 9941ce4.

📒 Files selected for processing (1)
  • express-zod-api/tests/endpoint.spec.ts

Comment thread express-zod-api/tests/endpoint.spec.ts
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues. Reviewed the following changes:

  • Parameterised the processContainers max-combinations test to cover 0, -1, 1, 2, NaN, asserting the result length equals limit > 1 ? limit : 1 — pinning the prior commit's Math.max(maxCombinations || 0, 1) clamp so a future revert would be caught by CI.

Prior review feedback:

  • No test pinned the new processContainers clamp contract — addressed by the parameterised test.each in security.spec.ts.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@RobinTail RobinTail force-pushed the limit-combinations-2 branch from 8a64edc to 29ea1e5 Compare May 6, 2026 20:18
@RobinTail RobinTail force-pushed the limit-combinations-2 branch from 29ea1e5 to 57ca58f Compare May 6, 2026 20:19
Copy link
Copy Markdown
Contributor

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@express-zod-api/src/documentation.ts`:
- Around line 214-219: The header extraction is using the full endpoint.security
tree (via getSecurityHeaders(endpoint.security)) even though securityRefs are
built from the truncated processContainers(endpoint.security, maxSecurity) and
limits.security should apply; update the flow so getSecurityHeaders is fed the
same truncated security representation (e.g., pass securityRefs or the output of
processContainers/limits.security) before calling depictRequestParams and any
other header extraction sites (like the similar block around the
depictRequestParams call at the other location), ensuring headers for discarded
OR-branches are not emitted as request parameters.

In `@express-zod-api/tests/logical-container.spec.ts`:
- Around line 200-201: The test title for the parameterized spec using
test.each([0, -1, 1, 2, NaN]) is missing the index placeholder; update the title
string in the test.each(...) call (the test name passed to test.each) to include
the %# placeholder in addition to %s so it prints both the value and the case
index (i.e., change the title from "should limit the maxCombinations=%s to at
least 1" to include "%#" alongside "%s" in the test.each invocation).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1af5be6e-7bf0-4d00-b14d-a53b2f1fc31a

📥 Commits

Reviewing files that changed from the base of the PR and between 330ccb3 and 116e65a.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • express-zod-api/src/documentation.ts
  • express-zod-api/tests/endpoint.spec.ts
  • express-zod-api/tests/logical-container.spec.ts

Comment thread express-zod-api/src/documentation.ts
Comment thread express-zod-api/tests/logical-container.spec.ts
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Important

The new JSDoc block on DocumentationParams.limits wraps its directives in backticks (`@desc`, `@default`, `@example`) instead of using them as real JSDoc tags. Every other JSDoc block in this file (including the nested examples and security inside this same limits) uses the plain @desc / @default / @example form required by AGENTS.md. The backticked variant renders the tags as inline code text and makes them invisible to any tooling that consumes JSDoc directives.

Reviewed the following changes:

  • Added JSDoc to the top-level DocumentationParams.limits property and expanded the nested limits.examples description — addresses prior CodeRabbit feedback about missing JSDoc on the new limits option.

Prior review feedback:

  • Documentation constructor limits option lacked JSDoc — addressed (modulo the directive-formatting issue flagged below).

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread express-zod-api/src/documentation.ts Outdated
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues. Reviewed the following changes:

  • Unwrapped the @desc, @default, and @example directives on DocumentationParams.limits so they're real JSDoc tags rather than inline-code text — the nested limits.examples and limits.security blocks were already correct and unchanged.

Prior review feedback:

  • DocumentationParams.limits JSDoc directives were backticked and invisible to JSDoc tooling — addressed by removing the backticks.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copy link
Copy Markdown
Contributor

@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.

♻️ Duplicate comments (2)
express-zod-api/src/documentation.ts (2)

214-219: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

securityHeaders is still derived from the full security tree while securityRefs uses the truncated one.

securityHeaders is extracted from the complete endpoint.security (line 214), but securityRefs is built from processContainers(endpoint.security, maxSecurity) (line 269), which truncates OR-branches to maxSecurity entries. When the limit discards combinations, headers belonging to those dropped security schemes are still injected into the request parameters via depictRequestParams, even though the corresponding schemes are no longer documented in the operation's security field — creating an inconsistency visible to API consumers.

The fix is to derive securityHeaders from the same truncated representation used for securityRefs. One approach:

📝 Proposed fix
-     const securityHeaders = getSecurityHeaders(endpoint.security);
+     const truncatedSecurity = processContainers(endpoint.security, maxSecurity);
+     const securityHeaders = getSecurityHeaders(truncatedSecurity);
      const depictedParams = depictRequestParams({
        ...commons,
        inputSources,
        isHeader,
        securityHeaders,
        request,
        ...
      });

      ...

      const securityRefs = depictSecurityRefs(
        depictSecurity(
-         processContainers(endpoint.security, maxSecurity),
+         truncatedSecurity,
          inputSources,
        ),
        ...
      );

Also applies to: 267-271

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@express-zod-api/src/documentation.ts` around lines 214 - 219, securityHeaders
is currently derived from the full endpoint.security while securityRefs uses the
truncated output of processContainers(endpoint.security, maxSecurity), causing
injected headers that don't correspond to documented security combos; change the
code to compute truncatedSecurity = processContainers(endpoint.security,
maxSecurity) (or reuse the existing variable used to build securityRefs) and
pass that truncatedSecurity into getSecurityHeaders and depictRequestParams
instead of endpoint.security so getSecurityHeaders(...) and
depictRequestParams(..., securityHeaders, ...) both reflect the same truncated
security representation (update any references to securityHeaders, securityRefs,
and the call sites of getSecurityHeaders and depictRequestParams to use the
truncated value).

95-108: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

@example directive still missing on examples and security inner properties.

Both inner properties (examples and security) have @desc, @default, and @see but are missing @example, which is required by the coding guidelines for all publicly available entity properties. This was previously proposed (with @example 20 / @example 10) and confirmed as addressed, but the current code doesn't include them.

📝 Proposed fix
   /**
    * `@desc` Caps the number of generated examples (request/response examples from examples of individual properties).
    * `@default` defaultMaxCombinations
    * `@see` defaultMaxCombinations
+   * `@example` 20
    * */
   examples?: number;
   /**
    * `@desc` Caps the number of security schemas combinations. Must be at least 1.
    * `@default` defaultMaxCombinations
    * `@see` Middleware
+   * `@example` 10
    * */
   security?: number;

As per coding guidelines: "All properties of publicly available entities exposed via index.ts must have JSDoc documentation with @desc, @default, and @example directives."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@express-zod-api/src/documentation.ts` around lines 95 - 108, Add missing
`@example` JSDoc tags for the two inner properties under the limits option: add an
`@example` value (e.g. "@example 20") to the examples property and an `@example`
value (e.g. "@example 10") to the security property in the JSDoc above the
limits block so that limits.examples and limits.security include `@desc`,
`@default`, and `@example` as required by the public API guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@express-zod-api/src/documentation.ts`:
- Around line 214-219: securityHeaders is currently derived from the full
endpoint.security while securityRefs uses the truncated output of
processContainers(endpoint.security, maxSecurity), causing injected headers that
don't correspond to documented security combos; change the code to compute
truncatedSecurity = processContainers(endpoint.security, maxSecurity) (or reuse
the existing variable used to build securityRefs) and pass that
truncatedSecurity into getSecurityHeaders and depictRequestParams instead of
endpoint.security so getSecurityHeaders(...) and depictRequestParams(...,
securityHeaders, ...) both reflect the same truncated security representation
(update any references to securityHeaders, securityRefs, and the call sites of
getSecurityHeaders and depictRequestParams to use the truncated value).
- Around line 95-108: Add missing `@example` JSDoc tags for the two inner
properties under the limits option: add an `@example` value (e.g. "@example 20")
to the examples property and an `@example` value (e.g. "@example 10") to the
security property in the JSDoc above the limits block so that limits.examples
and limits.security include `@desc`, `@default`, and `@example` as required by the
public API guidelines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 748b1341-f50d-4432-85b1-524f48003e1a

📥 Commits

Reviewing files that changed from the base of the PR and between 116e65a and 4fd3b23.

📒 Files selected for processing (1)
  • express-zod-api/src/documentation.ts

@RobinTail
Copy link
Copy Markdown
Owner Author

@pullfrog please review the whole idea again, evaluate the design and the messiness of the implementation

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Important

Holistic re-review at your request. The core idea lands well — combinations(..., limit) is the right place for the cap, splitting getSecurityHeaders from processContainers is a genuine improvement, and the isStrict/options-bag refactor of flattenIO/mergeExamples/processAllOf is cleaner than the "coerce"|"throw" string it replaces. But three wrinkles are worth addressing before this ships.

Design

  1. The headline feature is disarmed by default. defaultMaxCombinations = Infinity + @todo set to 20 in v28 means v27.4.0 ships the mechanism but none of the policy. Users who don't already know they have a combinatorial-explosion problem still won't, and the CHANGELOG's 64-combo example doesn't tell them what to set. Either pick a conservative default now (the v28 TODO is already admitting it's the right thing to do), or make the CHANGELOG prescriptive ("we recommend { examples: 20, security: 10 }") so the feature has an on-ramp.

  2. maxExamples is over-plumbed as a cross-cutting concern. It flows through DocumentationcommonsOpenAPIContext.ctxdepictRequest/depictResponse/depictBody/depictRequestParamsflattenIOmergeExamples/pullRequestExamples, and separately through Endpoint.getResponses#ensureOutputExamplespullResponseExamples. OpenAPIContext already exists to carry this kind of per-build value; every depicter that destructures maxExamples into its own signature is adding noise to functions that don't actually use it. Committing fully to ctx (or closing over maxExamples at factory-construction time) would collapse several of these layers.

Messiness

  1. Name drift between layers. The same number is called examples/maxExamples/limit, and its sibling is security/maxSecurity/maxCombinations. The public shape limits: { examples, security } is fine — it's the internal translation that proliferates. Picking one spine (e.g. maxExamples/maxSecurity at boundaries, limit only inside the generic combinations helper) would be a 10-minute rename.

  2. Three NaN/zero guards for the same concept: !(limit > 0) → [] in combinations/pullRequestExamples/pullResponseExamples; Math.max(0, limit || 0) in mergeExamples; Math.max(maxCombinations || 0, 1) in processContainers. The security branch has different semantics on purpose (always emit ≥1 combo) but the idiom should be shared — e.g. a single clampPositive(n, min) helper.

  3. Leftover TODOs in public JSDoc (common-helpers.ts:112, documentation.ts:105). The most recent commit literally added one of them rather than closing it.

  4. Missing @example on limits.examples / limits.security. Already flagged by CodeRabbit upthread; still missing at the time of review.

Correctness — one subtle item worth fixing, one worth documenting

  1. #ensureOutputExamples lost R.once but gained a sticky global-registry cache. First call wins permanently — two new Documentation({ limits: { examples: N } }) against the same endpoints with different N now silently reuse the first N. The new endpoint test exercises a single call per factory and can't catch this. Either re-add instance-scoped memoization keyed on limit, or don't mutate globalRegistry at all and return the truncated list to the caller.

  2. processContainers silently clamps security: 0 to 1 while its own JSDoc asks users to "Must be at least 1." The clamp is defensible (an empty security array is almost always a documentation bug) but it violates the JSDoc's implied contract.

Inline comments below are scoped to the sharpest concrete issues; the rest of the list above is best handled as a tidy-up pass.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment on lines +112 to +113
/** @todo set to 20 in v28 to avoid too many combinations */
export const defaultMaxCombinations = Infinity;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shipping the limit feature with Infinity as the default means v27.4.0 users still hit the combinatorial explosion the CHANGELOG warns about unless they discover and configure limits. The @todo already concedes the right default is lower — consider picking it now (e.g. 20) and treating the cap-by-default as the breaking change it effectively is in v28, rather than landing a disarmed guardrail.

Comment on lines +96 to +108
/**
* @desc Caps the number of generated examples (request/response examples from examples of individual properties).
* @default defaultMaxCombinations
* @see defaultMaxCombinations
* */
examples?: number;
/**
* @desc Caps the number of security schemas combinations. Must be at least 1.
* @default defaultMaxCombinations
* @todo decouple from defaultMaxCombinations, use higher but still fixed limit in v28
* @see Middleware
* */
security?: number;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two issues on the nested properties: (1) a @todo directive is landing in public-API JSDoc, meaning the open design question ships to users; decide now whether security shares defaultMaxCombinations or gets its own constant. (2) AGENTS.md requires @example on all optional public-entity properties — both examples and security are missing it (the parent limits has one). CodeRabbit already flagged #2 upthread.

Suggested change
/**
* @desc Caps the number of generated examples (request/response examples from examples of individual properties).
* @default defaultMaxCombinations
* @see defaultMaxCombinations
* */
examples?: number;
/**
* @desc Caps the number of security schemas combinations. Must be at least 1.
* @default defaultMaxCombinations
* @todo decouple from defaultMaxCombinations, use higher but still fixed limit in v28
* @see Middleware
* */
security?: number;
/**
* @desc Caps the number of generated examples (request/response examples from examples of individual properties).
* @default defaultMaxCombinations
* @see defaultMaxCombinations
* @example 20
* */
examples?: number;
/**
* @desc Caps the number of security schemas combinations. Values below 1 are clamped to 1.
* @default defaultMaxCombinations
* @see Middleware
* @example 10
* */
security?: number;

R.map((opt) => (isSimple(opt) ? [opt] : opt.and), entry),
([a, b]) => R.concat(a, b),
R.concat,
Math.max(maxCombinations || 0, 1),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Math.max(maxCombinations || 0, 1) silently coerces 0/negatives/NaN to 1, which contradicts the limits.security JSDoc saying "Must be at least 1." Either enforce the contract at the Documentation boundary (throw DocumentationError on invalid input), or weaken the JSDoc to say values below 1 are clamped. Right now readers are told one thing and given another.

Comment on lines 94 to +104
/** considered expensive operation, only required for generators */
#ensureOutputExamples = R.once(() => {
#ensureOutputExamples(limit: number) {
if (globalRegistry.get(this.#def.outputSchema)?.examples?.length) return; // examples on output schema, or pull up:
if (!isSchema<z.core.$ZodObject>(this.#def.outputSchema, "object")) return;
const examples = pullResponseExamples(this.#def.outputSchema);
const examples = pullResponseExamples(this.#def.outputSchema, limit);
if (!examples.length) return;
const current = this.#def.outputSchema.meta();
globalRegistry
.remove(this.#def.outputSchema) // reassign to avoid cloning
.add(this.#def.outputSchema, { ...current, examples });
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dropping R.once here traded closure-memoization for a sticky globalRegistry cache (the examples?.length check at line 96 short-circuits subsequent calls). The effect: the first limit value passed wins permanently for a given endpoint instance. If a consumer creates two Documentation generators with different limits.examples sharing endpoints, the second silently reuses the first's cap. The new test at endpoint.spec.ts:292-309 only exercises one call per factory. Consider re-adding instance-scoped memoization keyed on limit, or stop mutating globalRegistry and return the truncated examples to the caller so each consumer gets independent results.

Comment on lines 59 to 62
public abstract getResponses(
variant: ResponseVariant,
params: { maxExamples?: number },
): ReadonlyArray<NormalizedResponse>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The options bag is required but its only field is optional, so every caller that doesn't want to cap must still write getResponses(variant, {}) or { maxExamples: N }. Four existing sites now carry that boilerplate. Making the bag itself optional would let indifferent callers drop it entirely.

Suggested change
public abstract getResponses(
variant: ResponseVariant,
params: { maxExamples?: number },
): ReadonlyArray<NormalizedResponse>;
public abstract getResponses(
variant: ResponseVariant,
params?: { maxExamples?: number },
): ReadonlyArray<NormalizedResponse>;

@RobinTail
Copy link
Copy Markdown
Owner Author

I think I'd rather make a reasonably high limit in #3208 instead depending on some other factor

@RobinTail RobinTail closed this May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant