# Configuration
CukeDedup works without configuration. Add a project configuration only when the built-in
discovery or policy does not match the repository.
## Precedence
Values are resolved in this order, from highest to lowest priority:
```text
CLI flags
explicit --config
auto-discovered CukeDedup configuration
detected framework configuration
built-in defaults
```
`--config/-c <file>` bypasses automatic discovery. A missing or malformed explicit file is fatal.
Relative configuration, output, and baseline paths are resolved from the analyzed root.
Without `--config`, CukeDedup stops at the first valid source in this order:
```text
.cuke-dedup.json
.config/cuke-dedup.json
.config/.cuke-dedup.json
cuke-dedup.config.json
package.json#cukeDedup
```
An invalid auto-discovered file produces a warning, then discovery continues to the next source.
JavaScript projects may use the `cukeDedup` key in `package.json`; other projects should prefer
`.cuke-dedup.json`.
## Complete example
```json
{
"definitions": ["features/steps/**/*.ts"],
"features": ["features/**/*.{feature,feature.md}"],
"exclude": ["dist/**"],
"excludeDefaults": true,
"includeHidden": false,
"threshold": 5,
"nearDuplicateHandlerSimilarity": 0.7,
"requireDefinitions": true,
"requireFeatures": true,
"failOnIncomplete": false,
"failOnUnparseable": false,
"registrations": ["defineDomainStep"],
"parameterTypes": { "colour": "red|green|amber" },
"maxCandidateComparisons": 2000000,
"maxStructuralClassComparisons": 250000,
"noMetrics": false,
"reporters": ["terminal", "json", "html", "sarif"],
"output": "reports/cuke-dedup",
"rules": {
"duplicate-matcher": "error",
"duplicate-handler": "error",
"near-duplicate-step": "warning",
"unused-definition": "off"
},
"suppressions": [
{
"rule": "duplicate-handler",
"path": "features/steps/legacy.ts",
"matcher": "the legacy flow is complete",
"reason": "Kept distinct while the legacy flow is retired"
}
]
}
```
Collection values—`definitions`, `features`, `exclude`, `reporters`, and `suppressions`—replace
the corresponding value from a lower-precedence layer. Scalar settings and individual rule
severities merge by setting or rule name. Configured excludes replace lower-precedence custom
excludes, but built-in generated-directory exclusions remain a separate policy.
## Discovery patterns
`definitions`, `features`, `exclude`, and suppression `path` values use `globset` syntax against
root-relative paths normalized with `/`.
- `*` and `?` may cross `/`.
- `**` makes a recursive directory boundary explicit to readers.
- Character classes such as `[ab]` are supported.
- Brace alternatives such as `{js,ts}` are supported.
- Backslash escaping is supported.
Opt-in languages, currently Ruby, are selected only by a `definitions` pattern that depends on
their suffix, such as `features/**/*.rb`. A suffix-agnostic pattern such as `features/**` selects
JS/TS sources and reports the Ruby files it skipped in a warning. A skipped Ruby file that
dependency resolution follows a load to (a literal top-level `require` or `require_relative`,
direct or through other loaded files, within the load budget) is analyzed and not reported.
These patterns differ from `.gitignore` and `.cuke-dedupignore`, which use directory-scoped
gitignore semantics.
`excludeDefaults: false` permits an explicitly selected workspace under `node_modules`, `target`,
or another protected directory. `includeHidden: true` permits traversal into dot-directories.
Their CLI equivalents are `--no-default-excludes` and `--include-hidden`. Explicit definition
patterns do not implicitly weaken either safety default.
## Ignore files
A repository may put `.cuke-dedupignore` at the analysis root or in any descendant directory.
Each file applies to its own subtree and follows gitignore syntax:
- Blank lines and `#` comments are ignored.
- A leading `/` anchors the rule to the ignore file's directory.
- A trailing `/` selects directories.
- `!` negates an earlier matching rule.
Example:
```gitignore
# Generated feature sources
features/generated/*
**/*.generated.ts
# Keep one reviewed generated definition
!features/generated/reviewed.generated.ts
```
`.cuke-dedupignore` applies in addition to `.gitignore`, built-in exclusions, and configured
`exclude` patterns. `--no-default-excludes` disables only the built-in list. A configured exclude
is a hard exclusion and cannot be negated from an ignore file.
## Custom registration wrappers
`registrations` names local functions that register steps. CukeDedup already infers a top-level,
synchronous, single-statement wrapper that forwards its leading parameters directly to a known
registration:
```ts
function step(text, handler) {
Given(text, handler);
}
```
Nested, asynchronous, generator, conditional, multi-statement, reordered, rewritten, or
dynamically constructed wrappers cannot be inferred safely. Declare those names when they still
take the matcher first and handler second.
`registrations` also enables bare registration names that are not ambient by default, including
lowercase `given`/`when`/`then` used as globals. Without an entry here they are recognised only
through a resolved import (see `safety-and-limitations.md`).
`registrations` applies to JS/TS sources only. Every run still validates each entry as a JavaScript
identifier, but the Ruby adapter does not read the names; Ruby forwarding helpers are inferred (see
[Registrations and providers](discovery-and-frameworks.md#registrations-and-providers)).
## Trusted assertion modules
`assertionModules` names module specifiers whose `expect` export is a real assertion factory.
For Ruby, trust requires a preceding resolved source load and a unique, closed assertion namespace. `require_relative`, configured `rubyLoadPaths`, immutable Method/hash/factory aliases and closed fixture re-exports are supported. Configuration may name the load specifier or a project-relative provider path. Source proofs check lookup and forwarding; names alone grant no authority. Reopening, replacement, unknown effects, escapes and shared mutations remove trust. Installed gems and unselected runtime sources are not analyzed.
Supported terminals are `to_be`, `to_equal`, `equal_to`, `to_have_class`, `to_have_value` and `to_be_visible`, with `not`/`to` modifiers and literal dispatch. Expected values support scalar literals, regexes, arrays/hashes, supported source-bound matchers, and binding-sensitive local reads/member access. Unary expressions, ranges, rationals, character literals and mutable values remain unsupported and remove handler-comparison authority. Provider parse errors, relevant cycles and file/byte/read/work limits retain conservative incompleteness. TypeScript-only configuration does not affect unrelated Ruby loads. No project code executes.
For JS/TS, CukeDedup recognizes `@playwright/test`, `playwright/test`, `@jest/globals`, `expect`, `vitest`, `chai`, and `bun:test` without configuration, and it also trusts the ambient `expect` that Jest, Vitest, and Playwright inject. Chain recognition is shape-based, so Chai's `expect(x).to.equal(y)` is understood exactly as Jest's `expect(x).toBe(y)` is; only the origin of the factory is listed.
Declare a module when the factory reaches your steps another way:
```json
{
"assertionModules": ["./support/fixtures"]
}
```
A local module that re-exports `expect` is the common case:
```ts
// support/fixtures.ts
export { expect } from "@playwright/test";
```
Without configuration, JS/TS local re-exports remain untrusted: their expected values do not count as behavior, so conflicting assertions may produce a `parameterization-candidate`. Configuration restores that distinction for both `import` and `require`.
Configure only actual assertion factories; trusting unrelated modules can manufacture similarity evidence.
## Custom parameter types
`parameterTypes` maps a project-defined Cucumber Expression parameter type to the regular
expression it accepts. This mirrors `defineParameterType` without executing project code:
```json
{
"parameterTypes": {
"colour": "red|green|amber"
}
}
```
An undeclared type uses a permissive fallback that prevents a false `unused-definition` finding
but cannot prove ambiguity or overlap. Declaring it restores exact analysis. Invalid expressions
fail configuration rather than silently disabling checks.
## Suppressions
Every configured suppression must select at least a `path` or `matcher` and include a non-empty
reason of at most 512 Unicode characters.
- When both selectors are present, both must match the same definition.
- For pair findings, a path must contain every involved definition.
- A matcher must select at least one involved definition.
This prevents a directory exception from hiding a conflict that crosses into maintained code.
A source-local suppression can instead be placed immediately above a registration:
```ts
// cuke-dedup:ignore duplicate-handler -- retained for an external compatibility contract
Given("the legacy flow completes", legacyHandler);
```
Ruby uses the same directive in a `#` comment:
```ruby
# cuke-dedup:ignore duplicate-handler -- retained for an external compatibility contract
Given('the legacy flow completes') { legacy_flow }
```
Malformed directives and oversized reasons are operational errors. A directive applies only to
findings involving its attached definition.
## CLI overrides and diagnostics
Discovery and rule options can be overridden directly:
```sh
cuke-dedup . \
--config .cuke-dedup.json \
--definitions 'features/steps/**/*.ts' \
--features 'features/**/*.feature' \
--exclude 'generated/legacy.ts' \
--exclude 'vendor' \
--threshold 5 \
--require-definitions \
--rule duplicate-matcher=warning
```
`--exclude` may be repeated or receive comma-separated patterns. The analysis-bound overrides
`--max-candidate-comparisons` and `--max-structural-class-comparisons` are the CLI equivalents of the
`maxCandidateComparisons` and `maxStructuralClassComparisons` config keys and are subject to the same
hard safety ceilings.
Use `--explain-discovery` to show effective pattern origins, selected parsers, matching patterns,
and definition inputs without changing report output. Use `--print-config` to print the fully
merged and validated configuration as JSON, including its sources and warnings, then exit without
running discovery.
See [Discovery and frameworks](discovery-and-frameworks.md) for framework-derived defaults and
incomplete-corpus behavior, and [Rules](rules.md) for severity configuration.
## Ruby dependency sources
Ruby dependency paths are a configuration trust boundary: absolute paths, paths containing `..`, and relative paths through symlinks may resolve outside the analysis root. A configured load-path directory is an explicitly trusted root after canonicalization, including when it is a symlink to an external directory. Only analyze trusted path configuration; discovered registration bodies from those sources may appear in report snippets. Sources are read statically, never executed.
`rubyLoadPaths` lists ordered local directories containing Ruby dependency sources. Paths are relative to the analysis root unless absolute. The analyzer follows direct, literal `require` and `require_relative` calls without executing Ruby or installing gems. Bare `require` names use only these declared load paths; explicit `./` paths use the analysis root. Repeated requires and cycles load each source once. Missing, excluded, dynamic, deferred or non-Ruby dependencies remain incomplete; `load` is not treated as require-once. Pin dependency source revisions outside the analyzer before using them.
```json
{ "rubyLoadPaths": ["lib", "vendor/aruba/lib"] }
```
Resolved files can contribute definitions outside the entrypoint globs, but cannot escape the project or explicitly declared load-path roots. An unresolved dependency must not be interpreted as a clean complete corpus.