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*meansDockerfileplus filename variants such asDockerfile.dev..env*means.envplus variants such as.env.local.docker-compose*.yml/.yamlincludes variants such asdocker-compose.override.yaml.tsconfig*.jsonincludes files liketsconfig.app.json.- ESLint files currently recognized are
.eslintrc,.eslintrc.json,.eslintrc.yml,.eslintrc.yaml, and any file namedeslint.config.*.
How contradictions are evaluated
conflic compares extracted values using concept-aware semantics:
- Versions understand exact semver values, partial versions like
20or3.12, npm-style ranges like^20or>=18 <20, and Docker tags like22-alpine. - Ports understand single ports, ranges like
3000-3005, and Docker-style mappings like3000: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 stagesdeclared: project declarations such aspackage.json,pyproject.toml,Gemfile,go.mod,.env,pom.xml,*.csproj, andDockerfile EXPOSEenforced: hard constraints such as final Docker runtime images, CI runtime settings,docker-composeports,global.json,tsconfigstrict 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:
--severityand[conflic].severityaffect 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:
From source:
Without the LSP server:
Quick start
# Scan the current directory
# Scan a specific path
# Create a starter config in that path
# List built-in extractor IDs and descriptions
# Keep only selected concepts in the normal scan output
# Emit machine-readable JSON
# Emit SARIF
# Show discovery, extractor, assertion, and comparison details
# Scope the scan to changes since a git ref
# Or pass changed paths on stdin, one path per line
|
# Create or update a baseline
# Suppress findings already present in that baseline
# Preview fix proposals without writing anything
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, andvenv. [conflic].excludecan 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
.eslintrcfiles are tried as JSON/JSON5 first, then YAML. - YAML parsing supports anchors and merge keys.
tsconfig*.jsonand structured ESLint configs resolve localextendschains with cycle detection.- Local
extendstargets are blocked if they resolve outside the scan root. Those cases are surfaced asPARSE002. - Missing local config references such as
tsconfig.basealso surface asPARSE002. eslint.config.*files are parsed, not executed. Current support is for exported object/array literals that are JSON5-like, optionally wrapped indefineConfig(...),tseslint.config(...),typescriptEslint.config(...), oreslint.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 toPATH/.conflic.toml--configoverrides the config file path- relative
--configpaths are resolved from the scan root, not from the shell working directory - a missing implicit config is fine
- a missing explicit
--configpath is an error
Example:
[]
= "warning"
= "terminal"
= []
= []
# [[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, orinfo[conflic].format:terminal,json, orsarif[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--conceptin 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 withDockerfilefiles = ["Dockerfile", ".nvmrc"]suppresses only findings where both sides match one of those suffixesrule = "VER001"narrows the ignore to one rule IDreasonis stored in config for humans but is not used by the engine
Monorepo settings:
- When
[monorepo].per_package = trueandpackage_rootsis 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_conceptsbypasses package scoping and compares those concepts repo-wide.global_conceptscurrently 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 IDdisplay_name: human-readable concept namecategory: known valuesruntime-version,port,strict-mode,build-tool,package-manager; anything else is kept as a custom category stringtype:version,port,boolean, orstring; unknown values currently behave likestring
Supported per-source fields:
file: exact filename, exact path, or glob patternformat:json,yaml,toml,env,plain, ordockerfileauthority: useenforced,declared, oradvisory; unknown values currently fall back toadvisorypattern: optional regex; if capture group 1 exists, that capture becomes the extracted value, otherwise the full match is usedpath: dot-separated lookup used byjson,yaml, andtomlsourceskey: exact key used byenvsources
Format-specific behavior:
json,yaml, andtomlsources read a single value atpathenvsources read the first matchingkeyplainsources operate on the whole trimmed filedockerfilesources test eachFROMinstruction'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
CONFIG001diagnostic is emitted - missing format-specific fields such as
pathorkeyare not validated eagerly; those sources will simply produce no assertions
Example:
[[]]
= "redis-version"
= "Redis Version"
= "runtime-version"
= "version"
[[]]
= "docker-compose.yml"
= "yaml"
= "services.redis.image"
= "redis:(.*)"
= "enforced"
[[]]
= ".env"
= "env"
= "REDIS_VERSION"
= "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
# Write a baseline from the current result
# Suppress already-known findings and diagnostics
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-baselinewrites the file and then continues with normal reporting and normal exit-code handling--baselinesuppresses matching contradictions and matching parse/config diagnostics- if the
--baselinefile does not exist, it is silently ignored - if
--baselineand--update-baselinepoint to the same file,conflicexits 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:
.nvmrcand.node-version.python-version.ruby-versionpackage.jsonengines.nodeglobal.jsonsdk.versiongo.modgo.tool-versionsentries for Node, Java, and RubyGemfilerubypom.xmlJava version tags*.csprojTargetFrameworkDockerfile*FROMimage tags for Node, Python, Go, Ruby, Java, and .NET.envand.env.*plainKEY=valueport assignmentsDockerfile*EXPOSE
Current fix limitations:
docker-composeports are not auto-fixed- CI runtime settings are not auto-fixed
tsconfigand ESLint strict-mode assertions are not auto-fixed- matrix assertions are never auto-fixed
.csprojTargetFrameworksentries are extracted as matrix assertions and therefore are not auto-fixed.envexpressions such as${PORT:-3000}are compared but not rewritten automatically- exact-value files such as
.nvmrcare only rewritten when the winner can be rendered safely as an exact token
Operational details:
- backups are written as
*.conflic.bakunless--no-backupis used - writes are applied atomically
--dry-runexits with code1whenever 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 findingsjson: top-levelversion,concepts,parse_diagnostics, andsummarysarif: SARIF 2.1.0 with contradiction findings and parse/config diagnostics
Terminal output notes:
- by default, only concepts with findings are shown
--verbosealso shows concepts whose assertions are fully consistent--quietsuppresses output unless findings or diagnostics exist at or above the active threshold
Rule IDs currently emitted:
VER001: version contradictionPORT001: port contradictionBOOL001: boolean contradictionSTR001: string contradictionPARSE001: file read or parse failurePARSE002: blocked or failed localextendsresolutionCONFIG001: invalid custom extractor configuration
LSP server
The default build includes an LSP server:
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.tomlreload 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:
use ConflicConfig;
let root = new;
let config = load?;
let result = scan?;
Public entry points currently re-exported from the crate root include:
scanscan_with_overridesscan_diffscan_doctorgit_changed_filesIncrementalWorkspaceIncrementalScanKindIncrementalScanStatsDoctorReportDoctorFileInfo
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.