Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guides/authoring-reusable-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const conventions = defineConventions([
A reusable convention has the same fields as a hand-written one with two adjustments:

- `name` and `description` are **required** (consumers see them in error messages and source listings).
- `must` must use the **flat object form** (`MustPredicates`). The `MustBlock[]` form is not allowed in reusable conventions — see [Restrictions](../reference/reusable-conventions.md#restrictions).
- `must` and `mustNot` must use the **flat object form** (`MustPredicates`). The `MustBlock[]` form is not allowed in reusable conventions — see [Restrictions](../reference/reusable-conventions.md#restrictions).
- `paths` is **optional**. Omit it to force consumers to supply `paths` at the use-site (which is useful when the right pattern depends on the consuming project's layout). When `paths` is omitted, consumers can only reference the convention via the `use` form.

## 3. Build and verify
Expand Down
6 changes: 4 additions & 2 deletions docs/reference/conditional-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ Switch from object to array form when you need:
"if": { "hasFile": "index.test.tsx" },
"for": { "files": "index.test.tsx" },
"excludeFiles": ["components/legacy/**"],
"must": { "import": [{ "name": "render", "from": "@/test-utils" }] }
"must": { "import": [{ "name": "render", "from": "@/test-utils" }] },
"mustNot": { "exportConstants": ["debug"] }
}
```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `must` | `MustPredicates` | yes | The predicates this block enforces. See [predicates.md](./predicates.md). |
| `must` | `MustPredicates` | yes, unless `mustNot` is present | The predicates this block enforces. See [predicates.md](./predicates.md). |
| `mustNot` | `MustPredicates` | yes, unless `must` is present | The predicates this block forbids. |
| `if` | `{ hasFile }` or `{ placeholderSatisfies }` | no | Gate. Block runs only if the condition holds. |
| `for` | `{ files: string \| string[] }` | no | Scope. Predicates apply to files matching this pattern within the parent path. |
| `excludeFiles` | `string[]` | no | Glob patterns to exclude from the block. |
Expand Down
17 changes: 15 additions & 2 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The `konsistent.json` file declares the structural conventions the CLI enforces.

## Conventions

A convention is a rule that says "files matching `paths` must satisfy `must`."
A convention is a rule that says "files matching `paths` must satisfy `must` and must not satisfy `mustNot`."

```json
{
Expand All @@ -51,7 +51,8 @@ A convention is a rule that says "files matching `paths` must satisfy `must`."
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `paths` | `string` or `string[]` | yes | Glob pattern(s) with `{placeholder}` extraction. See [path-patterns.md](./path-patterns.md). |
| `must` | `MustPredicates` or `MustBlock[]` | yes | The conditions that matched paths must satisfy. See [predicates.md](./predicates.md) and [conditional-rules.md](./conditional-rules.md). |
| `must` | `MustPredicates` or `MustBlock[]` | yes, unless `mustNot` is present | The conditions that matched paths must satisfy. See [predicates.md](./predicates.md) and [conditional-rules.md](./conditional-rules.md). |
| `mustNot` | `MustPredicates` | yes, unless `must` is present | The conditions that matched paths must not satisfy. Unlike `must`, this only accepts the object form. |
| `name` | string matching `[a-z0-9-]+` | no | Identifier shown in violation reports. |
| `description` | string | no | Human-readable explanation. |
| `severity` | `"error"` \| `"warning"` | no, default `"error"` | See [Severity](#severity). |
Expand Down Expand Up @@ -91,6 +92,18 @@ All listed predicates apply unconditionally to every matched path. See [predicat

Each entry is a `MustBlock` that can have `if`, `for`, `excludeFiles`, `name`, and `description`. See [conditional-rules.md](./conditional-rules.md). An entry may alternatively be a reusable-convention reference of the form `{ "use": "<vendor>/<name>", ...overrides }`, which expands into a `MustBlock` — see [reusable-conventions.md](./reusable-conventions.md#use-inside-a-parents-must).

## `mustNot`: negated predicates

`mustNot` accepts the same predicate object shape as object-form `must`, but reverses the result. For example, this fails when a matched file exports `debug`:

```json
"mustNot": {
"exportConstants": ["debug"]
}
```

`mustNot` is only object-form. It cannot contain a `MustBlock[]`, string references, or `{ "use": ... }` references. Use it inside a `must` block when you need `if`, `for`, or `excludeFiles` scoping.

## Severity

By default, convention violations are errors and produce a non-zero exit code. Mark a convention as a warning with `"severity": "warning"`:
Expand Down
12 changes: 6 additions & 6 deletions docs/reference/path-patterns.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Path patterns

A convention's `paths` field declares which files or directories it applies to. Path patterns combine glob syntax with **placeholders** that capture parts of the path so they can be referenced inside `must` predicates.
A convention's `paths` field declares which files or directories it applies to. Path patterns combine glob syntax with **placeholders** that capture parts of the path so they can be referenced inside `must` and `mustNot` predicates.

## Glob basics

Expand All @@ -19,7 +19,7 @@ A convention's `paths` field declares which files or directories it applies to.

## Placeholders

Wrap a path segment in `{name}` to extract it as a placeholder. The captured value becomes available inside `must` predicates as `${name}`.
Wrap a path segment in `{name}` to extract it as a placeholder. The captured value becomes available inside `must` and `mustNot` predicates as `${name}`.

```json
{
Expand All @@ -38,7 +38,7 @@ A placeholder matches one path segment by default. Use it where a literal value
"paths": "services/{svcName}/index.ts"
```

The same placeholder can appear in both the path and the `must` predicates:
The same placeholder can appear in both the path and the predicate values:

```json
{
Expand All @@ -53,7 +53,7 @@ The same placeholder can appear in both the path and the `must` predicates:

## Static placeholder values

Sometimes a placeholder name is used inside `must`, but the consumer's tree has only one concrete value and there's no wildcard segment to capture it from. Use the optional `placeholders` field on the convention to supply the value directly:
Sometimes a placeholder name is used inside `must` or `mustNot`, but the consumer's tree has only one concrete value and there's no wildcard segment to capture it from. Use the optional `placeholders` field on the convention to supply the value directly:

```json
{
Expand All @@ -65,7 +65,7 @@ Sometimes a placeholder name is used inside `must`, but the consumer's tree has
}
```

This is equivalent to `paths: "packages/{providerId}/src/index.ts"` when the tree contains exactly one provider folder, but doesn't require a wildcard. It's especially useful when consuming a [reusable convention](./reusable-conventions.md) whose `must` references a placeholder that the local tree doesn't have a wildcard for.
This is equivalent to `paths: "packages/{providerId}/src/index.ts"` when the tree contains exactly one provider folder, but doesn't require a wildcard. It's especially useful when consuming a [reusable convention](./reusable-conventions.md) whose predicates reference a placeholder that the local tree doesn't have a wildcard for.

A name may not appear in both a `{name}` placeholder in `paths` and in `placeholders` — pick one source of truth.

Expand Down Expand Up @@ -98,7 +98,7 @@ For acronyms like `openai` → `OpenAI` instead of `Openai`, declare overrides w

### Template substitutions in predicates

Templates work anywhere a string appears in `must`:
Templates work anywhere a string appears in `must` or `mustNot`:

```json
{
Expand Down
14 changes: 12 additions & 2 deletions docs/reference/predicates.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Predicates

Predicates are the assertions inside a convention's `must` block. Each predicate checks one structural property of the matched path. Listing multiple predicates in the same `must` is equivalent to AND — they all must pass.
Predicates are the assertions inside a convention's `must` or `mustNot` block. Each predicate checks one structural property of the matched path. Listing multiple predicates in the same `must` is equivalent to AND — they all must pass. Listing predicates in `mustNot` reverses the result — a matched path fails when it satisfies one of those predicates.

The full machine-readable schema lives at `node_modules/konsistent/konsistent.schema.json`.

Expand Down Expand Up @@ -34,6 +34,16 @@ The full machine-readable schema lives at `node_modules/konsistent/konsistent.sc

All predicates support template substitutions in their string values — see [path-patterns.md](./path-patterns.md#case-transformations) for the full case-transformation catalog.

`mustNot` accepts only the object form:

```json
"mustNot": {
"exportConstants": ["debug"]
}
```

It does not accept `MustBlock[]` or reusable-convention references.

---

## Filesystem predicates
Expand Down Expand Up @@ -423,4 +433,4 @@ Multiple predicates in the same `must` are AND-ed:

For OR-style logic (apply different predicates to different files), use [conditional rules](./conditional-rules.md) — the array form of `must` with `if`/`for` blocks.

Note that **reusable conventions** (those published via `@konsistent/convention` and consumed via `conventionSources`) are restricted to the flat object form of `must` — the `MustBlock[]` form is unavailable on the author side. Hand-written conventions in your own `konsistent.json` retain the full surface area. See [reusable-conventions.md](./reusable-conventions.md#restrictions).
Note that **reusable conventions** (those published via `@konsistent/convention` and consumed via `conventionSources`) are restricted to flat object-form `must` and `mustNot` predicates — the `MustBlock[]` form is unavailable on the author side. Hand-written conventions in your own `konsistent.json` can still use `MustBlock[]` in `must`. See [reusable-conventions.md](./reusable-conventions.md#restrictions).
16 changes: 8 additions & 8 deletions docs/reference/reusable-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ A bare string `"<vendor>/<name>"` inlines the named reusable convention as-is. T

### Object reference (`use` form)

`{ "use": "<vendor>/<name>", ...overrides }` references a reusable convention and overlays your overrides on top of it. The override fields available are `paths`, `placeholders`, `excludeFiles`, `severity`, `if`, `for`, and `must` — the same optional fields a hand-written convention has, minus `name` and `description` (which come from the source).
`{ "use": "<vendor>/<name>", ...overrides }` references a reusable convention and overlays your overrides on top of it. The override fields available are `paths`, `placeholders`, `excludeFiles`, `severity`, `if`, `for`, `must`, and `mustNot` — the same optional fields a hand-written convention has, minus `name` and `description` (which come from the source).

Use this form when the reusable convention has no `paths` (so you must supply them) or when you want to adjust a field for your project.

Expand Down Expand Up @@ -121,17 +121,17 @@ A hand-written convention whose `must` is a `MustBlock[]` may also reference a r
}
```

Allowed override keys at this nesting level are every field a hand-written `MustBlock` exposes — `name`, `description`, `if`, `for`, `excludeFiles`, and `must`. Top-level-only fields (`paths`, `severity`) are not accepted at the use-site, and the referenced reusable convention must not declare them either: a reusable that ships `paths` or `severity` can only be referenced from the top level of `conventions[]`. Authors who want their reusable to be usable in both contexts should publish it without those fields.
Allowed override keys at this nesting level are every field a hand-written `MustBlock` exposes — `name`, `description`, `if`, `for`, `excludeFiles`, `must`, and `mustNot`. Top-level-only fields (`paths`, `severity`) are not accepted at the use-site, and the referenced reusable convention must not declare them either: a reusable that ships `paths` or `severity` can only be referenced from the top level of `conventions[]`. Authors who want their reusable to be usable in both contexts should publish it without those fields.

Override merge follows the same rules as the top-level `use` form: arrays replace, primitives replace, and `must` deep-merges with the inherited predicates.
Override merge follows the same rules as the top-level `use` form: arrays replace, primitives replace, and `must`/`mustNot` deep-merge with the inherited predicates.

## Merge semantics

When you write `{ use: "<vendor>/<name>", ...overrides }`, `konsistent` deep-merges your overrides on top of the reusable convention with these rules:

| Field kind | Rule |
| --- | --- |
| Plain object (e.g. `must`, nested predicate definitions) | Recursive deep-merge. Keys you supply replace the inherited value; keys you omit pass through. |
| Plain object (e.g. `must`, `mustNot`, nested predicate definitions) | Recursive deep-merge. Keys you supply replace the inherited value; keys you omit pass through. |
| Array (e.g. `paths`, `excludeFiles`, predicate lists like `haveFiles`, `declareFunctions`, `export`, `exportFunctions`) | Your array fully replaces the inherited array. Use `"excludeFiles": []` to clear an inherited list. |
| Primitive (e.g. `severity`, `description`) | Your value replaces the inherited value. |

Expand Down Expand Up @@ -184,14 +184,14 @@ Note that `excludeFiles` was fully replaced (array-replace), while `must` was de

## Restrictions

- **Reusable conventions only support the object form of `must`.** They cannot ship the `MustBlock[]` form. This keeps override semantics predictable — you always know the merge target is a flat predicate object. Your own hand-written conventions are unrestricted; you can still use `MustBlock[]` there. See [predicates.md](./predicates.md).
- **Reusable conventions only support object-form `must` and `mustNot`.** They cannot ship the `MustBlock[]` form. This keeps override semantics predictable — you always know the merge target is a flat predicate object. Your own hand-written conventions can still use `MustBlock[]` in `must`; `mustNot` is object-form only everywhere. See [predicates.md](./predicates.md).
- **The `conventionSources` value is a single string.** No object form (`{ package: ... }` / `{ path: ... }`) — auto-detection by leading `.` / `/` is unambiguous.
- **No cross-source merging.** Two `conventionSources` entries cannot be merged into a single prefix. If two packages happen to ship a convention with the same name, your vendor prefix scopes them.
- **`MustBlock[]` cannot be introduced via override.** Because the source convention's `must` is always object-form, deep-merge keeps the result object-form.

## Placeholder validation

After expansion, `konsistent` walks every string inside each merged convention's `must` and checks that each `${placeholder}` referenced is declared as `{placeholder}` in at least one `paths` entry. This catches mismatches between a reusable convention's templates and the `paths` you supplied at the use-site, before any file is scanned.
After expansion, `konsistent` walks every string inside each merged convention's `must` and `mustNot` and checks that each `${placeholder}` referenced is declared as `{placeholder}` in at least one `paths` entry. This catches mismatches between a reusable convention's templates and the `paths` you supplied at the use-site, before any file is scanned.

## Error reference

Expand All @@ -209,7 +209,7 @@ All errors below are returned from `loadConfig()` as `{ success: false, error }`
| npm source missing exports condition | `Convention source "<prefix>" → "<specifier>": package does not declare an exports["./konsistent"] entry.` | The source package isn't a reusable-convention package; check the spelling or pick a different source. |
| Reusable-convention package fails schema validation | `Convention source "<prefix>" → "<specifier>": invalid reusable-convention package at <path>: <issues>` | The author shipped an invalid package; report upstream. |
| Empty source value | `Convention source "<prefix>" has empty value.` | Supply a path or npm specifier. |
| Placeholder used in `must` but not declared in `paths` or `placeholders` | `Convention "<identifier>" references "${<placeholder>}" in <key>, but neither paths nor placeholders declare "{<placeholder>}".` | Either declare the placeholder in `paths` or `placeholders`, or remove the unresolved template from `must`. |
| Placeholder used in `must` or `mustNot` but not declared in `paths` or `placeholders` | `Convention "<identifier>" references "${<placeholder>}" in <key>, but neither paths nor placeholders declare "{<placeholder>}".` | Either declare the placeholder in `paths` or `placeholders`, or remove the unresolved template. |
| `use` inside `must[]` points at a reusable that declares `paths`/`severity` | `Convention "<prefix>/<name>" referenced in conventions[<i>].must[<j>] declares top-level-only field(s) "<field>". Such conventions can only be referenced at the top level of conventions[]. Either remove the field(s) from the source convention, or move the reference out of must[].` | Drop `paths`/`severity` from the reusable, or reference it directly from `conventions[]`. |

`<identifier>` in the placeholder error is the convention's `name`, the `<vendor>/<name>` reference, or `conventions[<i>]` — whichever was available.
Expand All @@ -218,4 +218,4 @@ All errors below are returned from `loadConfig()` as `{ success: false, error }`

- [Authoring reusable conventions](../guides/authoring-reusable-conventions.md) — publish your own.
- [konsistent.json reference](./configuration.md) — the surrounding config shape.
- [Path patterns](./path-patterns.md) — placeholder syntax used in `paths` and `must`.
- [Path patterns](./path-patterns.md) — placeholder syntax used in `paths`, `must`, and `mustNot`.
29 changes: 29 additions & 0 deletions e2e/fixtures/ai-toolkit/konsistent-reverse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"version": "v1",
"conventions": [
{
"name": "no-provider-barrel-exports",
"paths": "packages/{providerId}/src/index.ts",
"mustNot": {
"export": ["${providerId}"],
"exportTypes": [
"${providerId.toPascalCase()}Provider",
"${providerId.toPascalCase()}ProviderSettings"
]
}
},
{
"name": "no-provider-interface",
"paths": "packages/{providerId}/src/${providerId}-provider.ts",
"mustNot": {
"exportInterfaces": [
{
"name": "${providerId.toPascalCase()}Provider",
"extend": "ProviderV1"
}
],
"importTypes": [{ "name": "ProviderV1", "from": "@ai-toolkit/core" }]
}
}
]
}
15 changes: 15 additions & 0 deletions e2e/fixtures/barrel-files-broken/konsistent-reverse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "v1",
"conventions": [
{
"name": "barrels-must-not-be-pure",
"paths": "src/{moduleName}",
"must": [
{
"for": { "files": "index.ts" },
"mustNot": { "areBarrelFiles": true }
}
]
}
]
}
15 changes: 15 additions & 0 deletions e2e/fixtures/barrel-files/konsistent-reverse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "v1",
"conventions": [
{
"name": "barrels-must-not-be-pure",
"paths": "src/{moduleName}",
"must": [
{
"for": { "files": "index.ts" },
"mustNot": { "areBarrelFiles": true }
}
]
}
]
}
12 changes: 12 additions & 0 deletions e2e/fixtures/declaration-order-broken/konsistent-reverse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "v1",
"conventions": [
{
"name": "declarations-not-in-order",
"paths": "src/order.ts",
"mustNot": {
"useDeclarationOrder": ["alpha", "Beta", "gamma", "missing"]
}
}
]
}
12 changes: 12 additions & 0 deletions e2e/fixtures/declaration-order/konsistent-reverse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "v1",
"conventions": [
{
"name": "declarations-not-in-order",
"paths": "src/order.ts",
"mustNot": {
"useDeclarationOrder": ["alpha", "Beta", "gamma", "missing"]
}
}
]
}
Loading