conflic 1.0.0

Detect semantic contradictions across config files
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
<p align="center">
  <img src="./static/logo/conflic-scan.svg" alt="conflic scanning animation" width="800">
</p>

<p align="center">
  <em>Detect semantic contradictions across config files</em>
</p>

<p align="center">
  <a href="https://github.com/onplt/conflic/actions/workflows/ci.yml"><img src="https://github.com/conflic/conflic/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
  <a href="https://crates.io/crates/conflic"><img src="https://img.shields.io/crates/v/conflic.svg" alt="crates.io"></a>
  <a href="https://github.com/conflic/conflic/blob/main/LICENSE"><img src="https://img.shields.io/crates/l/conflic.svg" alt="License: MIT"></a>
  <a href="https://github.com/conflic/conflic/releases"><img src="https://img.shields.io/github/v/release/conflic/conflic" alt="GitHub Release"></a>
</p>

---

`conflic` scans a directory tree, extracts semantic assertions from configuration files, and reports contradictions between them.

The current implementation ships with:

- 29 built-in extractors across 8 built-in concepts
- custom extractors loaded from `.conflic.toml`
- terminal, JSON, and SARIF output
- diff-scoped scans
- baselines for suppressing known findings
- fix planning and safe auto-fix for supported targets
- an optional LSP server with incremental rescans and quick-fix code actions

## What `conflic` understands today

Recognized CI runtime settings come from YAML files under:

- `.github/workflows`
- `.circleci`
- `.gitlab-ci`
- the repository root file `.gitlab-ci.yml`

Built-in concepts and their current sources:

| Concept ID | Display name | Current sources |
| --- | --- | --- |
| `node-version` | Node.js Version | `.nvmrc`, `.node-version`, `package.json` `engines.node`, `Dockerfile*` `FROM node:*`, recognized CI YAML `node-version` / `node_version`, `.tool-versions` entries for `nodejs` or `node` |
| `python-version` | Python Version | `.python-version`, `pyproject.toml` `project.requires-python`, `pyproject.toml` `tool.poetry.dependencies.python`, `Dockerfile*` `FROM python:*`, recognized CI YAML `python-version` / `python_version` |
| `go-version` | Go Version | `go.mod` `go` directive, `Dockerfile*` `FROM golang:*` |
| `java-version` | Java Version | `pom.xml` tags `maven.compiler.source`, `maven.compiler.target`, `java.version`, or `release`, `Dockerfile*` `FROM openjdk:*`, `eclipse-temurin:*`, `amazoncorretto:*`, or `ibm-semeru-runtimes:*`, `.sdkmanrc` `java=...`, `.tool-versions` `java`, recognized CI YAML `java-version` / `java_version` |
| `ruby-version` | Ruby Version | `.ruby-version`, `Gemfile` `ruby "..."`, `Dockerfile*` `FROM ruby:*`, `.tool-versions` `ruby`, recognized CI YAML `ruby-version` / `ruby_version` |
| `dotnet-version` | .NET Version | `*.csproj` `TargetFramework` and `TargetFrameworks`, `global.json` `sdk.version`, `Dockerfile*` `FROM mcr.microsoft.com/dotnet/{sdk,aspnet,runtime}:...` |
| `app-port` | Application Port | `.env` and `.env.*` keys `PORT`, `APP_PORT`, or `SERVER_PORT`, `docker-compose*.yml` / `docker-compose*.yaml` service ports, `Dockerfile*` `EXPOSE` |
| `ts-strict-mode` | TypeScript Strict Mode | `tsconfig*.json` `compilerOptions.strict`, plus ESLint configs that explicitly turn off `@typescript-eslint/strict-boolean-expressions`, `@typescript-eslint/strict-type-checked`, or `@typescript-eslint/no-explicit-any` |

Important details behind those sources:

- `Dockerfile*` means `Dockerfile` plus filename variants such as `Dockerfile.dev`.
- `.env*` means `.env` plus variants such as `.env.local`.
- `docker-compose*.yml` / `.yaml` includes variants such as `docker-compose.override.yaml`.
- `tsconfig*.json` includes files like `tsconfig.app.json`.
- ESLint files currently recognized are `.eslintrc`, `.eslintrc.json`, `.eslintrc.yml`, `.eslintrc.yaml`, and any file named `eslint.config.*`.

## How contradictions are evaluated

`conflic` compares extracted values using concept-aware semantics:

- Versions understand exact semver values, partial versions like `20` or `3.12`, npm-style ranges like `^20` or `>=18 <20`, and Docker tags like `22-alpine`.
- Ports understand single ports, ranges like `3000-3005`, and Docker-style mappings like `3000:8080`. For mappings, the container port is treated as the application port.
- Boolean concepts compare literal `true` / `false`.
- String concepts compare exact string equality.
- Custom extractors can opt into `version`, `port`, `boolean`, or plain string semantics.

Each assertion also carries an authority level:

- `advisory`: informational sources such as `.nvmrc`, `.node-version`, `.python-version`, `.ruby-version`, `.tool-versions`, `.sdkmanrc`, and non-final Docker build stages
- `declared`: project declarations such as `package.json`, `pyproject.toml`, `Gemfile`, `go.mod`, `.env`, `pom.xml`, `*.csproj`, and `Dockerfile EXPOSE`
- `enforced`: hard constraints such as final Docker runtime images, CI runtime settings, `docker-compose` ports, `global.json`, `tsconfig` strict mode, and ESLint strict-related rules that are turned off

Current severity mapping:

| Authority pair | Result |
| --- | --- |
| `enforced` + `enforced` | `error` |
| `enforced` + `declared` | `error` |
| `enforced` + `advisory` | `warning` |
| `declared` + `declared` | `warning` |
| `declared` + `advisory` | `info` |
| `advisory` + `advisory` | `info` |

Important current behavior:

- `--severity` and `[conflic].severity` affect exit codes and `--quiet`.
- They do not currently filter lower-severity findings out of terminal, JSON, or SARIF output.

## Installation

Requirements:

- Rust 1.94 or newer

From crates.io:

```bash
cargo install conflic
```

From source:

```bash
git clone https://github.com/conflic/conflic.git
cd conflic
cargo install --path .
```

Without the LSP server:

```bash
cargo install conflic --no-default-features
```

## Quick start

```bash
# Scan the current directory
conflic

# Scan a specific path
conflic path/to/workspace

# Create a starter config in that path
conflic path/to/workspace --init

# List built-in extractor IDs and descriptions
conflic --list-concepts

# Keep only selected concepts in the normal scan output
conflic --check node,python

# Emit machine-readable JSON
conflic --format json

# Emit SARIF
conflic --format sarif > conflic.sarif

# Show discovery, extractor, assertion, and comparison details
conflic --doctor

# Scope the scan to changes since a git ref
conflic --diff origin/main

# Or pass changed paths on stdin, one path per line
git diff --name-only origin/main | conflic --diff-stdin

# Create or update a baseline
conflic --update-baseline .conflic-baseline.json

# Suppress findings already present in that baseline
conflic --baseline .conflic-baseline.json

# Preview fix proposals without writing anything
conflic --fix --dry-run
```

## Discovery and parsing

Current implementation details that matter in practice:

- Discovery respects `.gitignore`, `.git/info/exclude`, and global Git ignore files.
- The walker always skips these directories: `node_modules`, `.git`, `vendor`, `target`, `dist`, `build`, `__pycache__`, `.tox`, `.venv`, and `venv`.
- `[conflic].exclude` can add extra exclusions as simple path segments, exact path prefixes, or glob patterns.
- JSON files are parsed as strict JSON first, then JSON5 as a fallback. This means comments, trailing commas, single-quoted strings, and unquoted keys are accepted when the JSON5 parser can handle them.
- Extensionless `.eslintrc` files are tried as JSON/JSON5 first, then YAML.
- YAML parsing supports anchors and merge keys.
- `tsconfig*.json` and structured ESLint configs resolve local `extends` chains with cycle detection.
- Local `extends` targets are blocked if they resolve outside the scan root. Those cases are surfaced as `PARSE002`.
- Missing local config references such as `tsconfig.base` also surface as `PARSE002`.
- `eslint.config.*` files are parsed, not executed. Current support is for exported object/array literals that are JSON5-like, optionally wrapped in `defineConfig(...)`, `tseslint.config(...)`, `typescriptEslint.config(...)`, or `eslint.config(...)`.
- Parse and configuration diagnostics are preserved in terminal output, JSON output, SARIF output, baselines, and LSP diagnostics.

## Configuration

By default `conflic` looks for `.conflic.toml` in the scan root.

- `conflic --init [PATH]` writes a starter file to `PATH/.conflic.toml`
- `--config` overrides the config file path
- relative `--config` paths are resolved from the scan root, not from the shell working directory
- a missing implicit config is fine
- a missing explicit `--config` path is an error

Example:

```toml
[conflic]
severity = "warning"
format = "terminal"
exclude = []
skip_concepts = []

# [[ignore]]
# rule = "VER001"
# files = ["Dockerfile", ".nvmrc"]
# reason = "Intentional drift"

# [monorepo]
# per_package = true
# package_roots = ["packages/*", "apps/*"]
# global_concepts = ["node-version", "ts-strict-mode"]

# [[custom_extractor]]
# concept = "redis-version"
# display_name = "Redis Version"
# category = "runtime-version"
# type = "version"
#
# [[custom_extractor.source]]
# file = "docker-compose.yml"
# format = "yaml"
# path = "services.redis.image"
# pattern = "redis:(.*)"
# authority = "enforced"
#
# [[custom_extractor.source]]
# file = ".env"
# format = "env"
# key = "REDIS_VERSION"
# authority = "declared"
```

Config fields:

- `[conflic].severity`: `error`, `warning`, or `info`
- `[conflic].format`: `terminal`, `json`, or `sarif`
- `[conflic].exclude`: extra names, path prefixes, or glob patterns to skip during discovery
- `[conflic].skip_concepts`: concepts to drop before reporting; full IDs and built-in aliases are accepted
- `[[ignore]]`: contradiction-only suppression rules
- `[monorepo]`: package scoping controls
- `[[custom_extractor]]`: custom concept definitions

Built-in selector aliases accepted by:

- `--check`
- `[conflic].skip_concepts`
- `--concept` in fix mode

| Alias | Concept ID |
| --- | --- |
| `node` | `node-version` |
| `python` | `python-version` |
| `go` | `go-version` |
| `java` | `java-version` |
| `ruby` | `ruby-version` |
| `dotnet` | `dotnet-version` |
| `port` | `app-port` |
| `ts-strict` | `ts-strict-mode` |

Ignore rules behave like this:

- `file = "Dockerfile"` suppresses any finding where either side ends with `Dockerfile`
- `files = ["Dockerfile", ".nvmrc"]` suppresses only findings where both sides match one of those suffixes
- `rule = "VER001"` narrows the ignore to one rule ID
- `reason` is stored in config for humans but is not used by the engine

Monorepo settings:

- When `[monorepo].per_package = true` and `package_roots` is non-empty, contradictions are checked within each matched package instead of across the whole repository.
- Root-level files are still compared against each other and against package-local files.
- If multiple package root patterns match, the most specific match wins.
- `[monorepo].global_concepts` bypasses package scoping and compares those concepts repo-wide.
- `global_concepts` currently expects full concept IDs, not aliases.

## Custom extractors

Custom extractors are compiled from `.conflic.toml` at startup and merged with the built-in extractor set.

Supported extractor-level fields:

- `concept`: unique concept ID
- `display_name`: human-readable concept name
- `category`: known values `runtime-version`, `port`, `strict-mode`, `build-tool`, `package-manager`; anything else is kept as a custom category string
- `type`: `version`, `port`, `boolean`, or `string`; unknown values currently behave like `string`

Supported per-source fields:

- `file`: exact filename, exact path, or glob pattern
- `format`: `json`, `yaml`, `toml`, `env`, `plain`, or `dockerfile`
- `authority`: use `enforced`, `declared`, or `advisory`; unknown values currently fall back to `advisory`
- `pattern`: optional regex; if capture group 1 exists, that capture becomes the extracted value, otherwise the full match is used
- `path`: dot-separated lookup used by `json`, `yaml`, and `toml` sources
- `key`: exact key used by `env` sources

Format-specific behavior:

- `json`, `yaml`, and `toml` sources read a single value at `path`
- `env` sources read the first matching `key`
- `plain` sources operate on the whole trimmed file
- `dockerfile` sources test each `FROM` instruction's argument string and use the first match

Current validation behavior:

- invalid source formats, invalid file globs, invalid path globs, invalid relative globs, and invalid regex patterns are surfaced as `CONFIG001`
- if every source in a custom extractor is invalid, that extractor is skipped and a `CONFIG001` diagnostic is emitted
- missing format-specific fields such as `path` or `key` are not validated eagerly; those sources will simply produce no assertions

Example:

```toml
[[custom_extractor]]
concept = "redis-version"
display_name = "Redis Version"
category = "runtime-version"
type = "version"

[[custom_extractor.source]]
file = "docker-compose.yml"
format = "yaml"
path = "services.redis.image"
pattern = "redis:(.*)"
authority = "enforced"

[[custom_extractor.source]]
file = ".env"
format = "env"
key = "REDIS_VERSION"
authority = "declared"
```

## Diff scans and baselines

### Diff scans

`--diff <REF>` does more than scan changed files in isolation:

- it uses `git diff --name-only <REF> --`
- it also includes untracked files from `git ls-files --others --exclude-standard`
- it scans those changed files first
- it then pulls in peer files for any impacted concepts so comparisons remain meaningful

`--diff-stdin` uses the same peer-file expansion, but the changed path list comes from stdin instead of Git.

Current diff-mode behavior:

- parse diagnostics from untouched peer files are not carried into the diff result
- paths outside the scan root are ignored

### Baselines

```bash
# Write a baseline from the current result
conflic --update-baseline .conflic-baseline.json

# Suppress already-known findings and diagnostics
conflic --baseline .conflic-baseline.json
```

Baselines fingerprint both contradictions and parse/config diagnostics using stable fields such as:

- rule ID
- concept ID
- severity
- scan-root-relative file path
- key path
- normalized value text

Current baseline behavior:

- `--update-baseline` writes the file and then continues with normal reporting and normal exit-code handling
- `--baseline` suppresses matching contradictions and matching parse/config diagnostics
- if the `--baseline` file does not exist, it is silently ignored
- if `--baseline` and `--update-baseline` point to the same file, `conflic` exits with an error to avoid suppressing the freshly generated result

## Auto-fix

`conflic --fix` always prints a preview first. If proposals exist and `--dry-run` is not set, it then prompts for confirmation unless `--yes` is present.

Current fix winner model:

- the highest-authority assertion wins
- lower-authority contradictory assertions are proposed for update
- if multiple top-authority assertions disagree, the concept is marked unfixable instead of picking a winner arbitrarily

Currently supported fix targets:

- `.nvmrc` and `.node-version`
- `.python-version`
- `.ruby-version`
- `package.json` `engines.node`
- `global.json` `sdk.version`
- `go.mod` `go`
- `.tool-versions` entries for Node, Java, and Ruby
- `Gemfile` `ruby`
- `pom.xml` Java version tags
- `*.csproj` `TargetFramework`
- `Dockerfile*` `FROM` image tags for Node, Python, Go, Ruby, Java, and .NET
- `.env` and `.env.*` plain `KEY=value` port assignments
- `Dockerfile*` `EXPOSE`

Current fix limitations:

- `docker-compose` ports are not auto-fixed
- CI runtime settings are not auto-fixed
- `tsconfig` and ESLint strict-mode assertions are not auto-fixed
- matrix assertions are never auto-fixed
- `.csproj` `TargetFrameworks` entries are extracted as matrix assertions and therefore are not auto-fixed
- `.env` expressions such as `${PORT:-3000}` are compared but not rewritten automatically
- exact-value files such as `.nvmrc` are only rewritten when the winner can be rendered safely as an exact token

Operational details:

- backups are written as `*.conflic.bak` unless `--no-backup` is used
- writes are applied atomically
- `--dry-run` exits with code `1` whenever proposals or unfixable items exist

## Output formats

`conflic` currently supports three output formats:

- `terminal`: grouped by concept, with parse diagnostics first and concept assertions shown before findings
- `json`: top-level `version`, `concepts`, `parse_diagnostics`, and `summary`
- `sarif`: SARIF 2.1.0 with contradiction findings and parse/config diagnostics

Terminal output notes:

- by default, only concepts with findings are shown
- `--verbose` also shows concepts whose assertions are fully consistent
- `--quiet` suppresses output unless findings or diagnostics exist at or above the active threshold

Rule IDs currently emitted:

- `VER001`: version contradiction
- `PORT001`: port contradiction
- `BOOL001`: boolean contradiction
- `STR001`: string contradiction
- `PARSE001`: file read or parse failure
- `PARSE002`: blocked or failed local `extends` resolution
- `CONFIG001`: invalid custom extractor configuration

## LSP server

The default build includes an LSP server:

```bash
conflic --lsp
```

Current LSP capabilities:

- diagnostics for both sides of a contradiction
- diagnostics for parse and configuration issues
- quick-fix code actions backed by the same fix planner used by `--fix`
- incremental text sync
- debounced rescans
- targeted peer-file rescans through `IncrementalWorkspace`
- live `.conflic.toml` reload when the config file changes on disk or in an open editor buffer

For debugging incremental behavior, setting `CONFLIC_LSP_SCAN_STATS=1` causes the server to log full-scan and incremental-scan stats through the LSP log channel.

## Rust library usage

The crate can also be used as a library:

```rust
use conflic::config::ConflicConfig;

let root = std::path::Path::new(".");
let config = ConflicConfig::load(root, None)?;
let result = conflic::scan(root, &config)?;
```

Public entry points currently re-exported from the crate root include:

- `scan`
- `scan_with_overrides`
- `scan_diff`
- `scan_doctor`
- `git_changed_files`
- `IncrementalWorkspace`
- `IncrementalScanKind`
- `IncrementalScanStats`
- `DoctorReport`
- `DoctorFileInfo`

## CLI reference

| Flag | Current behavior |
| --- | --- |
| `[PATH]` | Directory to scan. Defaults to `.`. With `--init`, `.conflic.toml` is created in this directory. |
| `-f, --format <FORMAT>` | Output format: `terminal`, `json`, or `sarif`. Defaults to config first, then `terminal`. |
| `-s, --severity <SEVERITY>` | Active severity threshold: `error`, `warning`, or `info`. Affects exit status and `--quiet`, not report filtering. |
| `--check <A,B,...>` | Keep only selected concepts in the normal scan result. Accepts full concept IDs and built-in aliases. |
| `--init` | Create a template `.conflic.toml`. Exits with code `3` if the file already exists. |
| `-c, --config <PATH>` | Use an explicit config file. Relative paths are resolved from the scan root. |
| `-q, --quiet` | Suppress output unless findings or diagnostics exist at or above the active threshold. |
| `-v, --verbose` | Show consistent concepts as well as contradictory ones in terminal output. |
| `--no-color` | Disable terminal colors. |
| `--list-concepts` | Print built-in extractor IDs and descriptions, then exit. Custom extractors are not loaded for this command. |
| `--doctor` | Run diagnostic mode and exit. |
| `--diff <REF>` | Use Git to collect changed tracked files since `<REF>` plus untracked files, then run a diff-scoped scan. |
| `--diff-stdin` | Read changed file paths from stdin, one path per line, and run the same diff-scoped scan. |
| `--fix` | Build and print a fix plan, then apply proposals unless `--dry-run` is also set. |
| `--dry-run` | With `--fix`, preview only. Returns code `1` if any proposal or unfixable item exists. |
| `-y, --yes` | Skip the interactive confirmation prompt in fix mode. |
| `--no-backup` | Do not create `*.conflic.bak` files when applying fixes. |
| `--concept <CONCEPT>` | In fix mode, keep only proposals and unfixable items for one concept selector. |
| `--baseline <PATH>` | Suppress findings and parse/config diagnostics that match the baseline file, if that file exists. |
| `--update-baseline <PATH>` | Write a baseline JSON file from the current scan result, then continue normal reporting. |
| `--lsp` | Start the LSP server on stdin/stdout. |

## Exit codes

| Code | Meaning |
| --- | --- |
| `0` | No error findings, and no warning findings when the active threshold is `warning` or lower |
| `1` | Error-level contradiction or parse/config diagnostic, or an operational failure such as a config/Git error, rejected fix apply, or `--fix --dry-run` with work to do |
| `2` | Warning-level findings are present and the active threshold is `warning` or `info` |
| `3` | `--init` refused to overwrite an existing `.conflic.toml` |

Info-only findings do not currently produce a non-zero exit code.

## License

[MIT](LICENSE)