From efb11b7a690933e63f5d11d2708df2ff24d87065 Mon Sep 17 00:00:00 2001 From: Thomas Dickson Date: Tue, 11 Aug 2026 08:00:32 +0100 Subject: [PATCH 1/2] fix(config): let `enabled` reach the check that declares it (CD-324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Refactor.PurityHeuristic`'s only option is called `enabled`, and the config loader stripped that key out of every check's option bag before validation as a forward-compatible placeholder. The check's catalogue page told the user to write `enabled = true`; the check never saw it, so a registered check was unreachable by any user. `enabled` now flows through to the option bag like any other option, and `options_for_raw` drops it again only for the checks that do not declare it — so configs carrying a stray `enabled` keep loading rather than failing on upgrade. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019afugU2bTYCtXn6Vy9dn1v --- CHANGELOG.md | 1 + crates/cofferdam-engine/src/config/loader.rs | 27 ++++---- crates/cofferdam-engine/src/config/mod.rs | 61 ++++++++++++++++--- crates/cofferdam-engine/src/config/options.rs | 18 ++++++ crates/cofferdam-engine/src/config/schema.rs | 2 +- docs/reference/config.md | 2 +- 6 files changed, 87 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d33a5f..ee8e4232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A check declares a set of languages rather than one, and `Consistency.SpellingDialect` now reads Markdown as well as TypeScript (CD-316). The check shipped against the codebase but not against the corpus that motivated it: a docs tree split between "analyz*" and "analys*", which it could not reach because `Check::language()` returned a single `Language` and the engine dispatched on equality. `Check::languages()` returns `&'static [Language]`, defaulting to `&[Language::TypeScript]`, so every check that does not care compiles unchanged; the four that do — three Rust checks and one HTML — name their own. Registering a second check id for Markdown was the cheaper option and was rejected: two entries in the catalogue for one convention is a worse surface than the gap. In Markdown the whole document is prose, so what gets excluded is the code a page quotes — fenced blocks, inline code spans, link destinations and YAML frontmatter — but not indented blocks, since four spaces is also a nested list continuation. Markdown discovery remains opt-in behind `[engine] extra_extensions`, so no project sees new findings without asking. ### Fixed +- `Refactor.PurityHeuristic` can be switched on (CD-324). Its one option is called `enabled`, and the config loader stripped that key out of every check's option bag before validation as a forward-compatible placeholder — so the check's own catalogue page told the user to write `enabled = true`, and the check never saw it. A registered check no user could reach is worse than an unrecognised key, so `enabled` now flows through to the option bag like any other option, and is dropped again only for the checks that do not declare it. Configs carrying a stray `enabled` therefore keep loading rather than failing on upgrade, which is why the fix is silent rather than a warning: making the inert case loud needs a warning sink `options_for_raw` does not have, and is not worth a breaking change to reach. - `scripts/version.mjs set X.Y.Z --regen` could stamp the previous version into `docs/public/checks.json` and `docs/public/llms.txt` (CD-317). A stale cargo fingerprint after a `Cargo.toml` revert-then-rebump left `cargo build --workspace` reporting `Finished` without recompiling `cofferdam-cli`, so the subsequent `gen-docs` ran the old binary and wrote the old version, and `node scripts/version.mjs check` then failed on locations that regen was meant to fix. `--regen` now runs `cargo clean -p cofferdam-cli` before the build to force that crate's recompile, checks the built binary's own `--version` against the target before trusting it to run `gen-docs`, and re-reads `checks.json`/`llms.txt` afterwards to confirm they landed on the target version — failing loudly and naming both versions if not. - `--hide-baselined` now applies to every output format, not text alone (CD-315). The flag filtered findings inside the text formatter, so `--format=json`, `compact` and `sarif` received the untouched list and printed baselined findings anyway, marked `"baselined": true`. Nothing in the help text said so, and a CI pipeline reading JSON was quietly counting pre-existing findings as new work. The filter now runs once in the CLI, ahead of the per-format branch, so all four formats render the same list. Summary counts are deliberately left whole — `total`, `new` and `baselined` still describe the full run, matching what the text formatter has always done — so the gate count stays visible when the entries are hidden. diff --git a/crates/cofferdam-engine/src/config/loader.rs b/crates/cofferdam-engine/src/config/loader.rs index 4cd6e65e..44990215 100644 --- a/crates/cofferdam-engine/src/config/loader.rs +++ b/crates/cofferdam-engine/src/config/loader.rs @@ -11,13 +11,6 @@ use super::{ConfigError, ContextSuppressRule, OverrideBlock, OverrideCheck, Proj /// Filename loaders look for during walk-up discovery. pub const FILE_NAME: &str = "cofferdam.toml"; -/// Meta-keys that may appear inside `[checks."X.Y"]` blocks but aren't -/// per-check options. `severity` (cd-t1a) is now first-class — extracted -/// into `ProjectConfig::severity_overrides` rather than passed through -/// to `validate_options`. `enabled` is still a forward-compatible -/// placeholder (no behaviour wired yet). -const META_KEYS: &[&str] = &["severity", "enabled"]; - /// TOML document layout. Top-level sections grow additively here. #[derive(Debug, Deserialize, Default)] pub struct TomlDoc { @@ -158,9 +151,18 @@ pub fn parse(path: &Path, raw: &str) -> Result { let mut options: BTreeMap = BTreeMap::new(); for (key, val) in table { - // Meta-keys are extracted into their own slots, not passed - // through to per-check option validation (which would - // reject them as UnknownKey). + // `severity` (cd-t1a) is a meta-key, extracted into its own + // slot rather than passed through to per-check option + // validation (which would reject it as UnknownKey). + // + // `enabled` used to get the same treatment, as a + // forward-compatible placeholder. That silently discarded + // the only option `Refactor.PurityHeuristic` has — its + // catalogue page tells the user to write `enabled = true` — + // leaving a registered check nobody could turn on (CD-324). + // It now reaches the option bag; `options_for_raw` drops it + // again for the checks that do not declare it, so configs + // written against the old placeholder keep loading. if key == "severity" { let s = match &val { toml::Value::String(s) => s.clone(), @@ -180,11 +182,6 @@ pub fn parse(path: &Path, raw: &str) -> Result { severity_overrides.insert(check_id.clone(), sev); continue; } - if META_KEYS.contains(&key.as_str()) { - // `enabled` (and any future placeholders) — silently - // accepted, no behaviour wired today. - continue; - } let raw = toml_to_raw(&val).ok_or_else(|| ConfigError::UnsupportedValue { path: path.to_path_buf(), check_id: check_id.clone(), diff --git a/crates/cofferdam-engine/src/config/mod.rs b/crates/cofferdam-engine/src/config/mod.rs index d187edc3..bf426f6a 100644 --- a/crates/cofferdam-engine/src/config/mod.rs +++ b/crates/cofferdam-engine/src/config/mod.rs @@ -7,7 +7,8 @@ //! [checks."Readability.MaxLineLength"] //! limit = 120 //! severity = "warning" # phase-3 (cd-t1a) — accepted, not yet enforced -//! enabled = true # phase-3 — accepted, not yet enforced +//! enabled = true # an option like any other; only checks that +//! # declare it see it (CD-324) //! //! [checks."Readability.MaxFunctionLength"] //! limit = 50 @@ -572,19 +573,65 @@ enabled = true .checks .get("Readability.MaxLineLength") .expect("present"); - // `limit` flows through to the per-check option bag; `severity` - // goes to `severity_overrides`; `enabled` is silently accepted - // (no behaviour wired today). - assert_eq!(bag.len(), 1); + // `limit` and `enabled` flow through to the per-check option + // bag; `severity` goes to `severity_overrides`. + assert_eq!(bag.len(), 2); assert!(bag.contains_key("limit")); + assert!(bag.contains_key("enabled")); assert!(!bag.contains_key("severity")); - assert!(!bag.contains_key("enabled")); assert_eq!( cfg.severity_overrides.get("Readability.MaxLineLength"), Some(&Severity::High) ); } + /// CD-324: `enabled` used to be stripped by the loader for every + /// check, which made `Refactor.PurityHeuristic` — whose only option + /// is called `enabled` — impossible to switch on. It now reaches the + /// option bag when the check declares it. + #[test] + fn enabled_reaches_a_check_that_declares_it() { + let raw = r#" +[checks."Refactor.PurityHeuristic"] +enabled = true +"#; + let cfg = loader::parse(Path::new("test.toml"), raw).expect("parse"); + let schema = &[cofferdam_core::OptionSpec { + name: "enabled", + kind: cofferdam_core::OptionKind::Bool, + default: cofferdam_core::OptionDefault::Bool(false), + doc: "opt in", + }]; + let opts = options_for( + &cfg, + Path::new("test.toml"), + "Refactor.PurityHeuristic", + schema, + ) + .expect("validates"); + assert_eq!(opts.get_bool("enabled"), Some(true)); + } + + /// ...and is still tolerated (dropped, not an error) for the checks + /// that do not, so configs written against the old placeholder keep + /// loading. + #[test] + fn enabled_is_dropped_for_a_check_that_does_not_declare_it() { + let raw = r#" +[checks."Readability.MaxLineLength"] +enabled = false +"#; + let cfg = loader::parse(Path::new("test.toml"), raw).expect("parse"); + let opts = options_for( + &cfg, + Path::new("test.toml"), + "Readability.MaxLineLength", + &[], + ) + .expect("must not reject a legacy `enabled` key"); + assert_eq!(opts.get_bool("enabled"), None); + } + #[test] fn parse_context_suppress_block() { let raw = r#" @@ -991,7 +1038,7 @@ ui = ["domain"] // // The parse() step strips `limit` into the raw bag before // reaching options_for, but `plugins` (an array) would be kept - // as-is if it weren't in META_KEYS — so we test options_for + // as-is — so we test options_for // directly with `plugins` in the raw bag. let err = options_for_with_raw_key( "Readability.MaxLineLength", diff --git a/crates/cofferdam-engine/src/config/options.rs b/crates/cofferdam-engine/src/config/options.rs index a772780c..b06a0ef4 100644 --- a/crates/cofferdam-engine/src/config/options.rs +++ b/crates/cofferdam-engine/src/config/options.rs @@ -42,6 +42,24 @@ pub fn options_for_raw( schema: &[cofferdam_core::OptionSpec], raw: &BTreeMap, ) -> Result { + // `enabled` was historically stripped by the config loader for every + // check, as a forward-compatible placeholder. That made + // `Refactor.PurityHeuristic` — whose sole option is called `enabled` + // — impossible to turn on (CD-324). The loader now passes it + // through; drop it here only for the checks that do not declare it, + // so configs written against the old placeholder keep loading. + let stripped; + let raw = if raw.contains_key("enabled") && !schema.iter().any(|s| s.name == "enabled") { + stripped = raw + .iter() + .filter(|(k, _)| k.as_str() != "enabled") + .map(|(k, v)| (k.clone(), v.clone())) + .collect::>(); + &stripped + } else { + raw + }; + validate_options(check_id, schema, raw).map_err(|source| { // When `validate_options` rejects a key that looks like a // well-known top-level cofferdam.toml key (plugins, extends, diff --git a/crates/cofferdam-engine/src/config/schema.rs b/crates/cofferdam-engine/src/config/schema.rs index 071b98c7..fce97282 100644 --- a/crates/cofferdam-engine/src/config/schema.rs +++ b/crates/cofferdam-engine/src/config/schema.rs @@ -150,7 +150,7 @@ const CHECK_ENTRY_KEYS: &[KeySpec] = &[ key: "enabled", type_name: "bool", default: None, - doc: "Accepted but not yet wired to anything. To disable a check over a path glob, use `[[overrides]]` with `disabled = true`.", + doc: "Passed to the check as an option. Only `Refactor.PurityHeuristic` declares it (as its opt-in switch); for every other check it is accepted and ignored. To turn a check off over a path glob, use `[[overrides]]` with `disabled = true`.", }, ]; diff --git a/docs/reference/config.md b/docs/reference/config.md index f06b0e1a..d59eaa7a 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -35,7 +35,7 @@ Keys are chosen by you — each is a check id. | Key | Type | Default | Meaning | |---|---|---|---| | `severity` | string | — | Override the check's default severity: `info`, `low`, `medium`, `high` or `critical`. | -| `enabled` | bool | — | Accepted but not yet wired to anything. To disable a check over a path glob, use `[[overrides]]` with `disabled = true`. | +| `enabled` | bool | — | Passed to the check as an option. Only `Refactor.PurityHeuristic` declares it (as its opt-in switch); for every other check it is accepted and ignored. To turn a check off over a path glob, use `[[overrides]]` with `disabled = true`. | ## `[layers]` From bf134cba1b6a581b698ad4e99197ea2bd3208d32 Mon Sep 17 00:00:00 2001 From: Thomas Dickson Date: Tue, 11 Aug 2026 08:02:43 +0100 Subject: [PATCH 2/2] docs(checks): stop recommending .cofferdamignore for tests (CD-325) `Design.LayerViolation`'s page offered `.cofferdamignore` as the simple way to keep tests out of layer analysis. That prunes the files before discovery, so they contribute no import edges either, and `Design.OrphanExport` then reports every symbol only the tests import as never imported in the project. Point at `[[overrides]] disabled = true` instead, which turns off the one check without removing the files from the graph. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019afugU2bTYCtXn6Vy9dn1v --- CHANGELOG.md | 1 + crates/cofferdam-checks/docs/Design.LayerViolation.md | 8 ++++++-- docs/checks/Design.LayerViolation.md | 8 ++++++-- docs/public/checks.json | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee8e4232..83818d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A check declares a set of languages rather than one, and `Consistency.SpellingDialect` now reads Markdown as well as TypeScript (CD-316). The check shipped against the codebase but not against the corpus that motivated it: a docs tree split between "analyz*" and "analys*", which it could not reach because `Check::language()` returned a single `Language` and the engine dispatched on equality. `Check::languages()` returns `&'static [Language]`, defaulting to `&[Language::TypeScript]`, so every check that does not care compiles unchanged; the four that do — three Rust checks and one HTML — name their own. Registering a second check id for Markdown was the cheaper option and was rejected: two entries in the catalogue for one convention is a worse surface than the gap. In Markdown the whole document is prose, so what gets excluded is the code a page quotes — fenced blocks, inline code spans, link destinations and YAML frontmatter — but not indented blocks, since four spaces is also a nested list continuation. Markdown discovery remains opt-in behind `[engine] extra_extensions`, so no project sees new findings without asking. ### Fixed +- `Design.LayerViolation`'s catalogue page no longer recommends `.cofferdamignore` for excluding tests. That is the exact configuration that makes `Design.OrphanExport` claim a symbol is "never imported in the project" when its only importers are the ignored tests — `.cofferdamignore` prunes files before discovery, so they contribute no import edges either (CD-325). The page now points at an `[[overrides]]` block with `disabled = true`, which turns off the one check without removing the files from the graph. The underlying behaviour is unchanged and tracked separately. - `Refactor.PurityHeuristic` can be switched on (CD-324). Its one option is called `enabled`, and the config loader stripped that key out of every check's option bag before validation as a forward-compatible placeholder — so the check's own catalogue page told the user to write `enabled = true`, and the check never saw it. A registered check no user could reach is worse than an unrecognised key, so `enabled` now flows through to the option bag like any other option, and is dropped again only for the checks that do not declare it. Configs carrying a stray `enabled` therefore keep loading rather than failing on upgrade, which is why the fix is silent rather than a warning: making the inert case loud needs a warning sink `options_for_raw` does not have, and is not worth a breaking change to reach. - `scripts/version.mjs set X.Y.Z --regen` could stamp the previous version into `docs/public/checks.json` and `docs/public/llms.txt` (CD-317). A stale cargo fingerprint after a `Cargo.toml` revert-then-rebump left `cargo build --workspace` reporting `Finished` without recompiling `cofferdam-cli`, so the subsequent `gen-docs` ran the old binary and wrote the old version, and `node scripts/version.mjs check` then failed on locations that regen was meant to fix. `--regen` now runs `cargo clean -p cofferdam-cli` before the build to force that crate's recompile, checks the built binary's own `--version` against the target before trusting it to run `gen-docs`, and re-reads `checks.json`/`llms.txt` afterwards to confirm they landed on the target version — failing loudly and naming both versions if not. - `--hide-baselined` now applies to every output format, not text alone (CD-315). The flag filtered findings inside the text formatter, so `--format=json`, `compact` and `sarif` received the untouched list and printed baselined findings anyway, marked `"baselined": true`. Nothing in the help text said so, and a CI pipeline reading JSON was quietly counting pre-existing findings as new work. The filter now runs once in the CLI, ahead of the per-format branch, so all four formats render the same list. Summary counts are deliberately left whole — `total`, `new` and `baselined` still describe the full run, matching what the text formatter has always done — so the gate count stays visible when the entries are hidden. diff --git a/crates/cofferdam-checks/docs/Design.LayerViolation.md b/crates/cofferdam-checks/docs/Design.LayerViolation.md index 901921c6..bae16ada 100644 --- a/crates/cofferdam-checks/docs/Design.LayerViolation.md +++ b/crates/cofferdam-checks/docs/Design.LayerViolation.md @@ -70,8 +70,12 @@ whose resolved target is in layer **B**, where **B** is not in the * No "test layer" sugar yet. If you want tests to import from anywhere, add a `test` layer to your config and put it on every other layer's - `allow` list (or, simpler: exclude tests from the analysis via - `.cofferdamignore` since tests typically don't need their own layer). + `allow` list (or, simpler, turn this one check off over the test glob + with an `[[overrides]]` block carrying `disabled = true`). Do not reach + for `.cofferdamignore` here: it prunes the files before discovery, so + they contribute no import edges either, and `Design.OrphanExport` then + reports every symbol only the tests import as never imported at all + (CD-325). * Re-exports through barrel files attribute the violation to the re-exporter, not the eventual consumer. If you want barrels to be transparent, file an issue — the graph already records re-export diff --git a/docs/checks/Design.LayerViolation.md b/docs/checks/Design.LayerViolation.md index 9bcfdcc0..efefbd19 100644 --- a/docs/checks/Design.LayerViolation.md +++ b/docs/checks/Design.LayerViolation.md @@ -76,8 +76,12 @@ whose resolved target is in layer **B**, where **B** is not in the * No "test layer" sugar yet. If you want tests to import from anywhere, add a `test` layer to your config and put it on every other layer's - `allow` list (or, simpler: exclude tests from the analysis via - `.cofferdamignore` since tests typically don't need their own layer). + `allow` list (or, simpler, turn this one check off over the test glob + with an `[[overrides]]` block carrying `disabled = true`). Do not reach + for `.cofferdamignore` here: it prunes the files before discovery, so + they contribute no import edges either, and `Design.OrphanExport` then + reports every symbol only the tests import as never imported at all + (CD-325). * Re-exports through barrel files attribute the violation to the re-exporter, not the eventual consumer. If you want barrels to be transparent, file an issue — the graph already records re-export diff --git a/docs/public/checks.json b/docs/public/checks.json index 3dcf7703..25f00650 100644 --- a/docs/public/checks.json +++ b/docs/public/checks.json @@ -253,7 +253,7 @@ "base_priority": 9, "default_severity": "High", "explanation": "An import crosses a declared architectural layer in a direction not permitted by [layers].allow.", - "body": "---\nid: Design.LayerViolation\ncategory: Design\ndefault_severity: High\nbase_priority: 9\n---\n\n# Design.LayerViolation\n\nEnforces architectural layering rules declared in\n`cofferdam.invariants.toml` (see [the invariants reference](../invariants.md)\nfor the canonical config location and field reference). Each file is\nmapped to a layer via gitignore-style globs, and every import edge is\nchecked against an explicit allow-list of cross-layer dependencies.\n\n## Why\n\nLayered architectures (hexagonal, onion, clean, n-tier) are easier to\nreason about and refactor when the dependency direction is enforced\nmechanically. Without enforcement, an `app/` file imports a `domain/`\nhelper which imports back into `app/` \"just this once,\" and within a\nquarter the layers are sand. Cofferdam can hold the line at PR time.\n\n## Configuration\n\nIn `cofferdam.invariants.toml` (the legacy single-file `cofferdam.toml`\nform is deprecated — see `docs/invariants.md`):\n\n```toml\n[layers]\ninfra = [\"src/infra/**\"]\ndomain = [\"src/domain/**\", \"src/shared/**\"]\napp = [\"src/app/**\"]\n\n[layers.allow]\ndomain = [\"infra\"] # domain may import from infra\napp = [\"domain\", \"infra\"] # app may import from both\n# infra omitted → infra is isolated, must not import from any other layer\n```\n\nGlob patterns follow gitignore syntax. They're matched against each\nfile's path relative to the project root (where\n`cofferdam.invariants.toml` lives). When multiple layers match a file,\nthe one with the most-specific glob (longest non-glob prefix in its\ninclude patterns) wins; alphabetical layer name breaks true ties. Use\n`!pattern` within a layer's glob list to carve out subtrees explicitly:\n\n```toml\n[layers]\nui = [\"components/ui/**\"]\ncomponents = [\"components/**\", \"!components/ui/**\"]\n```\n\n## What gets flagged\n\nEvery static or dynamic import whose source file is in layer **A** and\nwhose resolved target is in layer **B**, where **B** is not in the\n`allow` list for **A**.\n\n## What's not flagged\n\n* Files outside any declared layer — the project hasn't said how to\n think about them yet, so the check stays silent.\n* Same-layer imports — always permitted.\n* Type-only imports (`import type { … }`) — they're erased at compile\n time, no runtime layering implication.\n* External imports (anything in `node_modules`) — out of scope.\n\n## Limitations\n\n* No \"test layer\" sugar yet. If you want tests to import from anywhere,\n add a `test` layer to your config and put it on every other layer's\n `allow` list (or, simpler: exclude tests from the analysis via\n `.cofferdamignore` since tests typically don't need their own layer).\n* Re-exports through barrel files attribute the violation to the\n re-exporter, not the eventual consumer. If you want barrels to be\n transparent, file an issue — the graph already records re-export\n source paths so attribution can chain.\n", + "body": "---\nid: Design.LayerViolation\ncategory: Design\ndefault_severity: High\nbase_priority: 9\n---\n\n# Design.LayerViolation\n\nEnforces architectural layering rules declared in\n`cofferdam.invariants.toml` (see [the invariants reference](../invariants.md)\nfor the canonical config location and field reference). Each file is\nmapped to a layer via gitignore-style globs, and every import edge is\nchecked against an explicit allow-list of cross-layer dependencies.\n\n## Why\n\nLayered architectures (hexagonal, onion, clean, n-tier) are easier to\nreason about and refactor when the dependency direction is enforced\nmechanically. Without enforcement, an `app/` file imports a `domain/`\nhelper which imports back into `app/` \"just this once,\" and within a\nquarter the layers are sand. Cofferdam can hold the line at PR time.\n\n## Configuration\n\nIn `cofferdam.invariants.toml` (the legacy single-file `cofferdam.toml`\nform is deprecated — see `docs/invariants.md`):\n\n```toml\n[layers]\ninfra = [\"src/infra/**\"]\ndomain = [\"src/domain/**\", \"src/shared/**\"]\napp = [\"src/app/**\"]\n\n[layers.allow]\ndomain = [\"infra\"] # domain may import from infra\napp = [\"domain\", \"infra\"] # app may import from both\n# infra omitted → infra is isolated, must not import from any other layer\n```\n\nGlob patterns follow gitignore syntax. They're matched against each\nfile's path relative to the project root (where\n`cofferdam.invariants.toml` lives). When multiple layers match a file,\nthe one with the most-specific glob (longest non-glob prefix in its\ninclude patterns) wins; alphabetical layer name breaks true ties. Use\n`!pattern` within a layer's glob list to carve out subtrees explicitly:\n\n```toml\n[layers]\nui = [\"components/ui/**\"]\ncomponents = [\"components/**\", \"!components/ui/**\"]\n```\n\n## What gets flagged\n\nEvery static or dynamic import whose source file is in layer **A** and\nwhose resolved target is in layer **B**, where **B** is not in the\n`allow` list for **A**.\n\n## What's not flagged\n\n* Files outside any declared layer — the project hasn't said how to\n think about them yet, so the check stays silent.\n* Same-layer imports — always permitted.\n* Type-only imports (`import type { … }`) — they're erased at compile\n time, no runtime layering implication.\n* External imports (anything in `node_modules`) — out of scope.\n\n## Limitations\n\n* No \"test layer\" sugar yet. If you want tests to import from anywhere,\n add a `test` layer to your config and put it on every other layer's\n `allow` list (or, simpler, turn this one check off over the test glob\n with an `[[overrides]]` block carrying `disabled = true`). Do not reach\n for `.cofferdamignore` here: it prunes the files before discovery, so\n they contribute no import edges either, and `Design.OrphanExport` then\n reports every symbol only the tests import as never imported at all\n (CD-325).\n* Re-exports through barrel files attribute the violation to the\n re-exporter, not the eventual consumer. If you want barrels to be\n transparent, file an issue — the graph already records re-export\n source paths so attribution can chain.\n", "requires_types": false, "consistency": false, "autofix": false,