From 874b67313f1e1c57cb583798d15b3843bfc8aefa Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 13 Jul 2026 18:16:10 -0400 Subject: [PATCH 1/9] Add evaluations setup and first eval definition for definition-of-done skill --- .../.agents/skills/.gitignore | 4 ++ .../dart_skills_lint/.agents/skills/README.md | 47 +++++++++++++++++++ .../definition-of-done/evals/evals.json | 16 +++++++ tool/dart_skills_lint/.gitignore | 6 +++ tool/dart_skills_lint/.npmrc | 1 + tool/dart_skills_lint/skills-lock.json | 6 +++ 6 files changed, 80 insertions(+) create mode 100644 tool/dart_skills_lint/.agents/skills/definition-of-done/evals/evals.json create mode 100644 tool/dart_skills_lint/.npmrc diff --git a/tool/dart_skills_lint/.agents/skills/.gitignore b/tool/dart_skills_lint/.agents/skills/.gitignore index 466a859..1471a06 100644 --- a/tool/dart_skills_lint/.agents/skills/.gitignore +++ b/tool/dart_skills_lint/.agents/skills/.gitignore @@ -6,6 +6,10 @@ !dart-skills-lint-integration/ !definition-of-done/ +# Keep evals inside un-ignored skill folders +!**/evals/ +!**/evals/** + # Keep essential configuration and docs !.gitignore !README.md diff --git a/tool/dart_skills_lint/.agents/skills/README.md b/tool/dart_skills_lint/.agents/skills/README.md index 46f969d..f4e6ae3 100644 --- a/tool/dart_skills_lint/.agents/skills/README.md +++ b/tool/dart_skills_lint/.agents/skills/README.md @@ -39,3 +39,50 @@ When adding new external skills to this directory, follow these guidelines: 1. Use `npx skills install` to pull them from upstream. 2. Add the generated skill folder name to `.gitignore` inside this directory (`.agents/skills/.gitignore`) to prevent checking third-party content into version control. 3. Commit the updated `skills-lock.json` file located in `tool/dart_skills_lint/` to track dependency versions across the team. + +## Running Evaluations (Evals) + +To measure the effectiveness and quality of an agent's skill execution, we use a rubric-based LLM-as-a-judge system following the [Anthropic skill-creator format](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md). + +### 📁 Eval Directory Structure + +For any skill (e.g. `definition-of-done/`), evals are organized as: +* `/evals/evals.json`: The test suite definition (prompts and expectations). **Tracked in Git**. +* `-workspace/`: Persistent test execution outputs and results directory. **Ignored in Git**. + * `iteration-/eval-/`: Contains results of a specific test run. + * `eval_metadata.json`: Contains the prompt and evaluation ID metadata. + * `with_skill/`: Run directories containing: + * `outputs/`: Modified files generated by the agent. + * `transcript.md`: Markdown log of the agent's thoughts and tool calls. + * `timing.json`: Logged duration and token usage stats. + * `grading.json`: Judge evaluation results for each expectation. + +--- + +### 🚀 Running an Evaluation (Agentic Process) + +Evaluations can be executed entirely through subagents without writing custom code: + +#### Step 1: Run the Task +Spawn an executor subagent of type `self` with **`Workspace: share`** (to isolate edits from your active working directory). Pass it the prompt from `evals.json` and direct it to follow the skill guidelines. +Once complete, copy the modified files to the run's `outputs/` folder, generate a `transcript.md` of its thinking, and write `eval_metadata.json` and `timing.json`. + +#### Step 2: Grade the Run +Spawn a grader subagent of type `self` using the [Anthropic grader guidelines](https://github.com/anthropics/skills/blob/main/skills/skill-creator/agents/grader.md). Pass it the list of expectations, the path to `transcript.md`, and the `outputs/` folder. Direct it to grade each expectation and write the results to `grading.json` in the run directory. + +--- + +### 🖥️ Viewing the Eval Reports + +We use the Anthropic static evaluation viewer to inspect the runs and grades: + +1. Download and install the `skill-creator` tool (which contains the viewer scripts) into the ignored `.agents/skills/` directory: + ```sh + npx skills add anthropics/skills --skill skill-creator --yes + ``` +2. Run the python review generator script pointing to the skill workspace: + ```sh + python3 .agents/skills/skill-creator/eval-viewer/generate_review.py .agents/skills/-workspace --static .html --skill-name "" + ``` +3. Open the generated static HTML file in your web browser. + diff --git a/tool/dart_skills_lint/.agents/skills/definition-of-done/evals/evals.json b/tool/dart_skills_lint/.agents/skills/definition-of-done/evals/evals.json new file mode 100644 index 0000000..9499d01 --- /dev/null +++ b/tool/dart_skills_lint/.agents/skills/definition-of-done/evals/evals.json @@ -0,0 +1,16 @@ +{ + "skill_name": "definition-of-done", + "evals": [ + { + "id": 1, + "prompt": "Modify lib/src/levenshtein.dart to add a comment explaining how the algorithm calculates distance (e.g. tracking deletion, insertion, substitution cost), and make sure you finish the task completely.", + "expected_output": "The file lib/src/levenshtein.dart has a new explanatory comment, code is formatted, analyzed cleanly, and contains no forbidden temporal terms.", + "expectations": [ + "The levenshtein.dart file contains a new comment explaining how edit distance is calculated.", + "The new comment does not contain any relative temporal words such as 'now', 'currently', 'new', 'old', or 'existing'.", + "The levenshtein.dart file is formatted cleanly according to dart format.", + "The project has no dart analyze errors or warnings." + ] + } + ] +} diff --git a/tool/dart_skills_lint/.gitignore b/tool/dart_skills_lint/.gitignore index 6875411..15504fa 100644 --- a/tool/dart_skills_lint/.gitignore +++ b/tool/dart_skills_lint/.gitignore @@ -22,3 +22,9 @@ build/ # Logs .agents/*.log + +# Dynamic evaluation workspace directories (convention derived from the skill-creator skill: https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md) +# (e.g. definition-of-done-workspace/ containing outputs, transcripts, timing, and grading JSONs) +.agents/skills/*-workspace/ +skills/*-workspace/ + diff --git a/tool/dart_skills_lint/.npmrc b/tool/dart_skills_lint/.npmrc new file mode 100644 index 0000000..214c29d --- /dev/null +++ b/tool/dart_skills_lint/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/tool/dart_skills_lint/skills-lock.json b/tool/dart_skills_lint/skills-lock.json index 4279009..a9d2efe 100644 --- a/tool/dart_skills_lint/skills-lock.json +++ b/tool/dart_skills_lint/skills-lock.json @@ -97,6 +97,12 @@ "skillPath": "skills/productivity/grill-me/SKILL.md", "computedHash": "784f0dbb7403b0f00324bce9a112f715342777a0daee7bbb7385f9c6f0a170ea" }, + "skill-creator": { + "source": "anthropics/skills", + "sourceType": "github", + "skillPath": "skills/skill-creator/SKILL.md", + "computedHash": "5ea13a6d9f0d4bb694405d79acd00cadec0d21bb138c4dd10fcf3c500cb835c2" + }, "test-driven-development": { "source": "obra/superpowers", "sourceType": "github", From a48be661572740e41c50d22cb82ca7e1a20d9e60 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 13 Jul 2026 19:49:38 -0400 Subject: [PATCH 2/9] Fix skills linter tests by adding ignore for definition-of-done-workspace and restructuring yaml configuration --- tool/dart_skills_lint/.agents/skills/ignore.json | 9 ++++++++- tool/dart_skills_lint/dart_skills_lint.yaml | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tool/dart_skills_lint/.agents/skills/ignore.json b/tool/dart_skills_lint/.agents/skills/ignore.json index 1ab118f..07cac70 100644 --- a/tool/dart_skills_lint/.agents/skills/ignore.json +++ b/tool/dart_skills_lint/.agents/skills/ignore.json @@ -1,3 +1,10 @@ { - "skills": {} + "skills": { + "definition-of-done-workspace": [ + { + "rule_id": "path-does-not-exist", + "file_name": ".agents/skills/definition-of-done-workspace" + } + ] + } } \ No newline at end of file diff --git a/tool/dart_skills_lint/dart_skills_lint.yaml b/tool/dart_skills_lint/dart_skills_lint.yaml index 330bf5b..31191a8 100644 --- a/tool/dart_skills_lint/dart_skills_lint.yaml +++ b/tool/dart_skills_lint/dart_skills_lint.yaml @@ -19,6 +19,7 @@ dart_skills_lint: - path: "example/valid" rules: prevent-skills-sh-publishing: error + individual_skills: - path: ".agents/skills/add-dart-lint-validation-rule" rules: prevent-skills-sh-publishing: error From e307cc055f55f96ae12ebfeade83d6a80cda5325 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 13 Jul 2026 21:05:23 -0400 Subject: [PATCH 3/9] feat: standardize rule severities and options API naming and structures - Rename Validator parameter ruleOverrides to customRuleSeverities. - Rename severity mapping fields and parameters to ruleSeverities. - Rename option mapping fields and parameters to ruleOptions. - Added validation checks for options keys and types in YAML parsing. - Decoupled and exported ValidationResult model. --- .../skills/definition-of-done/SKILL.md | 4 +- tool/dart_skills_lint/CHANGELOG.md | 17 ++ tool/dart_skills_lint/README.md | 16 +- tool/dart_skills_lint/RULES.md | 11 + .../lib/dart_skills_lint.dart | 1 + .../lib/src/config_parser.dart | 7 +- .../lib/src/models/check_type.dart | 10 +- .../lib/src/models/skill_rule.dart | 3 + .../lib/src/rule_registry.dart | 15 +- .../lib/src/validation_session.dart | 129 ++++++++++- tool/dart_skills_lint/lib/src/validator.dart | 206 ++++++------------ .../test/absolute_paths_test.dart | 9 +- .../test/api_defaults_test.dart | 32 +-- .../test/cli_integration_test.dart | 4 +- .../test/custom_rule_test.dart | 17 ++ .../test/directory_structure_test.dart | 3 +- .../test/field_constraints_test.dart | 1 + .../test/metadata_validation_test.dart | 1 + .../test/relative_path_flag_test.dart | 5 +- .../test/relative_paths_test.dart | 21 +- .../test/resolve_rules_test.dart | 11 +- .../test/tracked_skills_publishing_test.dart | 7 +- 22 files changed, 330 insertions(+), 200 deletions(-) diff --git a/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md b/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md index 0877ec1..4ed4c9f 100644 --- a/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md +++ b/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md @@ -18,7 +18,7 @@ Before stating that a task is complete, you MUST execute and pass the following 3. **Metrics/Linter**: Run `dart run dart_code_linter:metrics analyze lib` and ensure there are zero issues. This checks for cyclomatic complexity and custom rules like file naming and redundant async. 4. **Tests**: Run `dart test` and ensure all tests pass successfully. 5. **Skill Validation**: If any skill files were modified, run `dart run dart_skills_lint -d .agents/skills` to ensure they are valid. -6. **Changelog**: Ensure `CHANGELOG.md` is updated if the task includes user-facing features, bug fixes, or behavioral changes. +6. **Changelog**: Ensure `CHANGELOG.md` is updated if the task includes user-facing features, bug fixes, or behavioral changes. Audit all entries against the *previously released version* (do not document changes to intermediate PR development code or new unreleased APIs as breaking changes). 7. **Temporal Words**: Ensure that code and code comments contain no relative temporal terms (e.g., 'now', 'currently', 'new', 'old', 'existing behavior'). ## 🚦 Completion Checklist @@ -28,6 +28,6 @@ Before stating that a task is complete, you MUST execute and pass the following - [ ] Metrics/Linter are clean (`dart run dart_code_linter:metrics analyze lib`). - [ ] Tests are passing (`dart test`). - [ ] Skills validated if modified (`dart run dart_skills_lint -d .agents/skills`). -- [ ] `CHANGELOG.md` updated for user-facing features, bug fixes, or behavioral changes. +- [ ] `CHANGELOG.md` updated and audited against the previously released version (no intermediate or unreleased breaking changes listed). - [ ] Verified that code and code comments contain no relative temporal terms (e.g., 'now', 'currently', 'new', 'old', 'existing behavior'). - [ ] Documentation is updated. diff --git a/tool/dart_skills_lint/CHANGELOG.md b/tool/dart_skills_lint/CHANGELOG.md index 6dfcaae..020fb68 100644 --- a/tool/dart_skills_lint/CHANGELOG.md +++ b/tool/dart_skills_lint/CHANGELOG.md @@ -1,5 +1,22 @@ +## 0.5.0 + +- Added support for rule-specific custom options in `dart_skills_lint.yaml`, allowing rules to be configured with parameter maps (e.g. passing exclusions, thresholds, length limits, etc.). +- Refactored `path-does-not-exist` from an inline structure check into a class-based `SkillRule`, enabling it to be disabled or overridden. +- Exposed namespaced CLI flags for custom options (e.g. `--path-does-not-exist-exclude`) with support for empty string overrides to clear options. +- Implemented option type-coercion for `int`, `bool`, and `List` types parsed from the command line. + +### Breaking Changes + +- Refactored `ValidationResult` out of `src/validator.dart` and into `src/models/validation_result.dart`. Packages that imported the internal implementation file `package:dart_skills_lint/src/validator.dart` and referenced `ValidationResult` will need to explicitly import `package:dart_skills_lint/src/models/validation_result.dart` or migrate to importing the public entrypoint `package:dart_skills_lint/dart_skills_lint.dart`. +- Renamed rule severity configuration concepts across model structures and APIs to use consistent `ruleSeverities` / `customRuleSeverities` naming: + - Renamed `Validator` constructor parameter `ruleOverrides` to `customRuleSeverities`. + - Renamed `Configuration.configuredRules` and `LintTargetConfig.rules` to `ruleSeverities`. + - Renamed `ValidationSession` constructor parameter `resolvedRules` to `resolvedRuleSeverities`. + - Renamed `ValidationSession` method `resolveRulesForPath` to `resolveRuleSeveritiesForPath`. + ## 0.4.0 + - Fixed issue #166 by adding support for configuring individual skills via the `individual_skills:` key in `dart_skills_lint.yaml`, enabling path-specific rule severity mapping without relying on root directory scanning. - Native binaries for macOS arm64, macOS x64, Linux x64, and Linux arm64 are now published to GitHub Releases. Install without the diff --git a/tool/dart_skills_lint/README.md b/tool/dart_skills_lint/README.md index fe1b08f..e795d6f 100644 --- a/tool/dart_skills_lint/README.md +++ b/tool/dart_skills_lint/README.md @@ -174,12 +174,16 @@ dart run dart_skills_lint ### Rule Precedence -When resolving which severity to apply for a rule, `dart_skills_lint` evaluates settings in the following order of precedence (highest to lowest): - -1. **CLI Flags / API Overrides**: Explicit flags passed to the CLI (e.g., `--check-trailing-whitespace`) or rules passed to the `validateSkills` API via `resolvedRules`. -2. **Path-Specific Config**: Rules defined under `directories:` or `individual_skills:` in `dart_skills_lint.yaml` for a matching path. If a skill matches multiple configured paths (e.g., an `individual_skills` path that overlaps with a `directories` path, or nested `directories`), the rules are applied additively in the order they appear in the configuration file. Later entries override earlier entries. This allows you to set broad rules for a root directory and then selectively override them for specific nested skills. -3. **Global Config**: Rules defined under the top-level `rules:` in `dart_skills_lint.yaml`. -4. **Defaults**: The hardcoded default severity for each rule. +When resolving which severity and options to apply for a rule, `dart_skills_lint` evaluates settings in the following order of precedence (highest to lowest): + +1. **CLI Flags / API Overrides**: + - Rule severity overrides: Explicit flags passed to the CLI (e.g., `--check-trailing-whitespace` or `--no-check-trailing-whitespace`). + - Rule options overrides: Namespaced command-line option flags (e.g., `--path-does-not-exist-exclude=".*-workspace"`). Passing an empty string (e.g. `--path-does-not-exist-exclude=""`) explicitly clears the custom option. +2. **Path-Specific Config**: Rules defined under `directories:` or `individual_skills:` in `dart_skills_lint.yaml` for a matching path. + - If a target config specifies a **map** (e.g. `path-does-not-exist: { severity: error, exclude: "..." }`), the options map completely overrides any global options for that rule. + - If a target config specifies a **simple string** severity (e.g. `path-does-not-exist: error`), the severity is overridden, but the global options map is inherited/preserved. +3. **Global Config**: Rules and options defined under the top-level `rules:` in `dart_skills_lint.yaml`. +4. **Defaults**: The hardcoded defaults for each rule. This ensures that you can always override configuration file settings for a specific run by using CLI flags. diff --git a/tool/dart_skills_lint/RULES.md b/tool/dart_skills_lint/RULES.md index 9ac8491..f9ae4d6 100644 --- a/tool/dart_skills_lint/RULES.md +++ b/tool/dart_skills_lint/RULES.md @@ -174,3 +174,14 @@ governs how changes to these rules ship. - **Auto-fix behavior:** none. A broken frontmatter block isn't safely mechanically repairable. - **Disable:** `--no-valid-yaml-metadata`. + +## path-does-not-exist + +- **Default severity:** error +- **Fixable:** no +- **What it checks:** the skill directory exists, is actually a directory, and contains a `SKILL.md` file. +- **Diagnostic shape:** + `SKILL.md is missing in directory: (see https://agentskills.io/specification#directory-structure)` +- **Auto-fix behavior:** none. +- **Disable:** `--no-path-does-not-exist`. + diff --git a/tool/dart_skills_lint/lib/dart_skills_lint.dart b/tool/dart_skills_lint/lib/dart_skills_lint.dart index 1952bd8..3ed50ed 100644 --- a/tool/dart_skills_lint/lib/dart_skills_lint.dart +++ b/tool/dart_skills_lint/lib/dart_skills_lint.dart @@ -8,4 +8,5 @@ export 'src/models/analysis_severity.dart'; export 'src/models/skill_context.dart'; export 'src/models/skill_rule.dart'; export 'src/models/validation_error.dart'; +export 'src/models/validation_result.dart'; export 'src/validator.dart'; diff --git a/tool/dart_skills_lint/lib/src/config_parser.dart b/tool/dart_skills_lint/lib/src/config_parser.dart index 47192e0..59fa151 100644 --- a/tool/dart_skills_lint/lib/src/config_parser.dart +++ b/tool/dart_skills_lint/lib/src/config_parser.dart @@ -10,7 +10,9 @@ import 'package:logging/logging.dart'; import 'package:yaml/yaml.dart'; import 'models/analysis_severity.dart'; +import 'models/check_type.dart'; import 'path_utils.dart'; +import 'rule_registry.dart'; final _log = Logger('dart_skills_lint'); @@ -68,7 +70,7 @@ class ConfigParser { final parsingErrors = []; _validateTopLevelKeys(toolConfig, parsingErrors); - final configuredRules = _parseRules(toolConfig); + final rulesResult = _parseGlobalRules(toolConfig, parsingErrors); final directoryConfigs = _parseConfigList(toolConfig, _directoriesKey, parsingErrors); final individualSkillConfigs = _parseConfigList( toolConfig, @@ -79,7 +81,8 @@ class ConfigParser { return Configuration( directoryConfigs: directoryConfigs, individualSkillConfigs: individualSkillConfigs, - configuredRules: configuredRules, + ruleSeverities: rulesResult.rules, + globalRuleOptions: rulesResult.ruleOptions.isEmpty ? null : rulesResult.ruleOptions, parsingErrors: parsingErrors, ); } diff --git a/tool/dart_skills_lint/lib/src/models/check_type.dart b/tool/dart_skills_lint/lib/src/models/check_type.dart index f887354..38e1457 100644 --- a/tool/dart_skills_lint/lib/src/models/check_type.dart +++ b/tool/dart_skills_lint/lib/src/models/check_type.dart @@ -6,7 +6,12 @@ import 'analysis_severity.dart'; /// Encapsulates metadata and severity state for a specific validation rule. class CheckType { - const CheckType({required this.name, required this.defaultSeverity, required this.help}); + const CheckType({ + required this.name, + required this.defaultSeverity, + required this.help, + this.allowedOptions = const {}, + }); final String name; /// The default severity if not overridden by config or flags. @@ -14,4 +19,7 @@ class CheckType { /// The help message displayed by the CLI. final String help; + + /// Custom configuration options supported by this check. + final Map allowedOptions; } diff --git a/tool/dart_skills_lint/lib/src/models/skill_rule.dart b/tool/dart_skills_lint/lib/src/models/skill_rule.dart index 041f127..3c392df 100644 --- a/tool/dart_skills_lint/lib/src/models/skill_rule.dart +++ b/tool/dart_skills_lint/lib/src/models/skill_rule.dart @@ -26,6 +26,9 @@ abstract class SkillRule { AnalysisSeverity get severity; + /// Custom configuration options supported by this rule (mapping option name to expected Type). + Map get allowedOptions => const {}; + /// Validates the skill provided in [context]. Future> validate(SkillContext context); } diff --git a/tool/dart_skills_lint/lib/src/rule_registry.dart b/tool/dart_skills_lint/lib/src/rule_registry.dart index 489016c..92de96d 100644 --- a/tool/dart_skills_lint/lib/src/rule_registry.dart +++ b/tool/dart_skills_lint/lib/src/rule_registry.dart @@ -9,6 +9,7 @@ import 'rules/absolute_paths_rule.dart'; import 'rules/description_length_rule.dart'; import 'rules/disallowed_field_rule.dart'; import 'rules/name_format_rule.dart'; +import 'rules/path_does_not_exist_rule.dart'; import 'rules/prevent_skills_sh_publishing_rule.dart'; import 'rules/relative_paths_rule.dart'; import 'rules/trailing_whitespace_rule.dart'; @@ -19,6 +20,12 @@ class RuleRegistry { /// All registered rules and their default configurations. // TODO(reidbaker): Break out flags vs options here so entry_point can generate appropriate CLI arguments. static final List allChecks = [ + const CheckType( + name: PathDoesNotExistRule.ruleName, + defaultSeverity: AnalysisSeverity.error, + help: 'Check if SKILL.md and directory structure are correct.', + allowedOptions: {'exclude': String}, + ), const CheckType( name: AbsolutePathsRule.ruleName, defaultSeverity: AbsolutePathsRule.defaultSeverity, @@ -62,8 +69,14 @@ class RuleRegistry { ]; /// Creates a rule instance by name, or returns null if not a class-based rule. - static SkillRule? createRule(String name, AnalysisSeverity severity) { + static SkillRule? createRule( + String name, + AnalysisSeverity severity, [ + Map? options, + ]) { switch (name) { + case PathDoesNotExistRule.ruleName: + return PathDoesNotExistRule(severity: severity, options: options); case AbsolutePathsRule.ruleName: return AbsolutePathsRule(severity: severity); case DescriptionLengthRule.ruleName: diff --git a/tool/dart_skills_lint/lib/src/validation_session.dart b/tool/dart_skills_lint/lib/src/validation_session.dart index e8b869c..4c8bb3c 100644 --- a/tool/dart_skills_lint/lib/src/validation_session.dart +++ b/tool/dart_skills_lint/lib/src/validation_session.dart @@ -17,6 +17,7 @@ import 'models/skill_context.dart'; import 'models/skill_rule.dart'; import 'models/skills_ignores.dart'; import 'models/validation_error.dart'; +import 'models/validation_result.dart'; import 'path_utils.dart'; import 'skills_ignores_storage.dart'; import 'validator.dart'; @@ -45,14 +46,31 @@ const directoryErrorMsg = 'Directory error:'; /// Per-invocation state and orchestration for skill validation. /// /// One session is constructed per CLI invocation (or embedded call). The -/// caller invokes [processIndividualSkill] for each `--skill` path and +/// session aggregates configuration options, custom rules, ignores, and CLI overrides, +/// then orchestrates the validation of multiple target skill directories. +/// +/// Callers invoke [processIndividualSkill] for each `--skill` path and /// [processSkillRoot] for each `--skills-directory` path, then optionally /// [reportNoSkillsValidated] to emit the "no skills found" diagnostics. -/// Failure state is exposed via [anyFailed] and [anySkillsValidated]. +/// The failure state of the session is exposed via [anyFailed] and [anySkillsValidated]. class ValidationSession { + /// Creates a validation session with the specified configuration, overrides, and rules. + /// + /// * [config] is the parsed YAML configuration file settings. + /// * [resolvedRuleSeverities] maps rule names to custom severities override mapping. + /// * [resolvedRuleOptions] maps rule names to rule-specific custom options overrides (e.g., CLI-passed options). + /// * [ignoreFileOverride] specifies a custom file path containing lint ignores to load. + /// * [customRules] contains programmatically injected custom skill rule checks. + /// * [printWarnings] controls whether warnings are printed to stdout. + /// * [fastFail] controls whether validation stops immediately on the first error. + /// * [quiet] controls whether success messages and other info logs are silenced. + /// * [generateBaseline] controls whether the validation should output/update baseline ignores. + /// * [fix] controls whether to apply fixable rule modifications directly to files. + /// * [fixApply] is the deprecated flag indicating if fixes should be automatically applied. ValidationSession({ required this.config, - required this.resolvedRules, + this.resolvedRuleSeverities = const {}, + this.resolvedRuleOptions = const {}, required this.ignoreFileOverride, required this.customRules, required this.printWarnings, @@ -67,7 +85,8 @@ class ValidationSession { ]; final Configuration config; - final Map resolvedRules; + final Map resolvedRuleSeverities; + final Map> resolvedRuleOptions; final String? ignoreFileOverride; final List customRules; final bool printWarnings; @@ -109,9 +128,18 @@ class ValidationSession { return true; } - final Map localRules = resolveRulesForPath(normalizedSkillPath); + final Map localRuleSeverities = resolveRuleSeveritiesForPath( + normalizedSkillPath, + ); + final Map> localRuleOptions = resolveRuleOptionsForPath( + normalizedSkillPath, + ); final String? localIgnoreFile = resolveIgnoreFile(normalizedSkillPath); - final validator = Validator(ruleOverrides: localRules, customRules: customRules); + final validator = Validator( + customRuleSeverities: localRuleSeverities, + customRules: customRules, + ruleOptions: localRuleOptions, + ); final ({SkillsIgnores ignores, String ignorePath}) loaded = await _loadIgnores( localIgnoreFile, @@ -196,9 +224,18 @@ class ValidationSession { } final String normalizedSkillPath = p.normalize(entity.path); - final Map localRules = resolveRulesForPath(normalizedSkillPath); + final Map localRuleSeverities = resolveRuleSeveritiesForPath( + normalizedSkillPath, + ); + final Map> localRuleOptions = resolveRuleOptionsForPath( + normalizedSkillPath, + ); final String? localIgnoreFile = resolveIgnoreFile(normalizedSkillPath); - final validator = Validator(ruleOverrides: localRules, customRules: customRules); + final validator = Validator( + customRuleSeverities: localRuleSeverities, + customRules: customRules, + ruleOptions: localRuleOptions, + ); final String ignorePath = localIgnoreFile != null ? p.normalize(expandPath(localIgnoreFile)) @@ -283,28 +320,96 @@ class ValidationSession { } @visibleForTesting - Map resolveRulesForPath(String path) { + Map resolveRuleSeveritiesForPath(String path) { final String normalizedPath = p.absolute(path); final localRules = {}; // 1. Global Config (from YAML) - localRules.addAll(config.configuredRules); + localRules.addAll(config.ruleSeverities); // 2. Path-Specific Config (from YAML) for (final ({String normalizedPath, LintTargetConfig config}) entry in _normalizedDirectoryConfigs) { final String configPath = entry.normalizedPath; if (p.equals(configPath, normalizedPath) || p.isWithin(configPath, normalizedPath)) { - localRules.addAll(entry.config.rules); + localRules.addAll(entry.config.ruleSeverities); } } // 3. Overrides (CLI flags or API caller) take highest precedence - localRules.addAll(resolvedRules); + localRules.addAll(resolvedRuleSeverities); return localRules; } + /// Resolves the final rule options configuration for a specific file or directory path. + /// + /// Merges options in order of precedence (highest to lowest): + /// 1. Command-line overrides (`resolvedRuleOptions`), where a `null` value indicates + /// the option is cleared. + /// 2. Target-specific configurations in `dart_skills_lint.yaml` that match [path]. + /// 3. Global configurations defined in `dart_skills_lint.yaml`. + /// + /// Returns a map where: + /// * The outer key is the rule name (e.g., `'path-does-not-exist'`). + /// * The inner map contains the resolved key-value options for that rule (e.g., `{'exclude': '.*-workspace'}`). + @visibleForTesting + Map> resolveRuleOptionsForPath(String path) { + final String targetPath = p.absolute(path); + final resolved = >{}; + + // Step 1: Initialize with global rule options from configuration file. + final Map>? globalOptions = config.globalRuleOptions; + if (globalOptions != null) { + for (final MapEntry> entry in globalOptions.entries) { + resolved[entry.key] = Map.from(entry.value); + } + } + + // Step 2: Merge in path-specific rule options for matching targets. + for (final ({String normalizedPath, LintTargetConfig config}) entry + in _normalizedDirectoryConfigs) { + final String configPath = entry.normalizedPath; + final bool isMatch = p.equals(configPath, targetPath) || p.isWithin(configPath, targetPath); + + if (isMatch && entry.config.ruleOptions != null) { + for (final MapEntry> ruleEntry + in entry.config.ruleOptions!.entries) { + resolved[ruleEntry.key] = Map.from(ruleEntry.value); + } + } + } + + // Step 3: Apply CLI overrides (command-line options take highest precedence). + // An override value of `null` explicitly removes/clears the option. + for (final MapEntry> ruleOverrideEntry + in resolvedRuleOptions.entries) { + final String ruleName = ruleOverrideEntry.key; + final Map overrides = ruleOverrideEntry.value; + + final Map currentOptions = resolved[ruleName] ?? {}; + + for (final MapEntry optionEntry in overrides.entries) { + final String optionName = optionEntry.key; + final Object? value = optionEntry.value; + + if (value == null) { + currentOptions.remove(optionName); + } else { + currentOptions[optionName] = value; + } + } + + if (currentOptions.isNotEmpty) { + resolved[ruleName] = currentOptions; + } else { + resolved.remove(ruleName); + } + } + + return resolved; + } + @visibleForTesting String? resolveIgnoreFile(String path) { final String normalizedPath = p.absolute(path); diff --git a/tool/dart_skills_lint/lib/src/validator.dart b/tool/dart_skills_lint/lib/src/validator.dart index 95a3dca..995f0fe 100644 --- a/tool/dart_skills_lint/lib/src/validator.dart +++ b/tool/dart_skills_lint/lib/src/validator.dart @@ -13,17 +13,25 @@ import 'models/check_type.dart'; import 'models/skill_context.dart'; import 'models/skill_rule.dart'; import 'models/validation_error.dart'; +import 'models/validation_result.dart'; import 'rule_registry.dart'; - -const _dirStructureUrl = 'https://agentskills.io/specification#directory-structure'; +import 'rules/path_does_not_exist_rule.dart'; final _log = Logger('dart_skills_lint'); /// Validates agent skill directories against the Agent Skills specification. class Validator { - Validator({Map? ruleOverrides, List? customRules}) - : _customSeverities = ruleOverrides ?? {}, - _rules = _buildRules(ruleOverrides ?? {}, customRules ?? []); + /// Creates a validator with optional rule severities, custom rules, and rule options. + /// + /// * [customRuleSeverities] overrides default severities of rules (e.g. mapping a rule to [AnalysisSeverity.disabled] or [AnalysisSeverity.warning]). + /// * [customRules] specifies custom rules to be included in the validation. + /// * [ruleOptions] maps rule names to their rule-specific custom options maps (e.g. passing regex patterns, thresholds, lists). + Validator({ + Map? customRuleSeverities, + List? customRules, + Map>? ruleOptions, + }) : _customRuleSeverities = customRuleSeverities ?? {}, + _rules = _buildRules(customRuleSeverities ?? {}, customRules ?? [], ruleOptions ?? {}); static const String _skillFileName = SkillContext.skillFileName; /// The name of the special check for missing files or directories. @@ -35,14 +43,14 @@ class Validator { /// The name of the special check for unexpected errors. static const String unexpectedError = 'unexpected-error'; - final Map _customSeverities; + final Map _customRuleSeverities; final List _rules; /// Returns the rules used by this validator. List get rules => _rules; AnalysisSeverity _getSeverity(String name, AnalysisSeverity defaultSeverity) { - return _customSeverities[name] ?? defaultSeverity; + return _customRuleSeverities[name] ?? defaultSeverity; } /// Validates a single skill directory. @@ -51,55 +59,54 @@ class Validator { /// constraints like name format and field lengths using registered rules. Future validate(Directory dir) async { final validationErrors = []; - - final bool isValidDir = await _checkDirectoryStructure(dir, validationErrors); - if (!isValidDir) { - return ValidationResult(validationErrors: validationErrors); - } - final skillMdFile = File(p.join(dir.path, _skillFileName)); - String content; - try { - content = await skillMdFile.readAsString(); - } on FileSystemException catch (e) { - validationErrors.add( - ValidationError( - ruleId: skillFileInaccessible, - file: skillMdFile.path, - message: 'Failed to read $_skillFileName: $e', - severity: _getSeverity(skillFileInaccessible, AnalysisSeverity.error), - ), - ); - return ValidationResult(validationErrors: validationErrors); - } catch (e) { - validationErrors.add( - ValidationError( - ruleId: unexpectedError, - file: skillMdFile.path, - message: 'Unexpected error reading $_skillFileName: $e', - severity: _getSeverity(unexpectedError, AnalysisSeverity.error), - ), - ); - return ValidationResult(validationErrors: validationErrors); - } + final bool skillMdExists = dir.existsSync() && skillMdFile.existsSync(); + var content = ''; YamlMap? parsedYaml; String? yamlParsingError; - try { - final RegExpMatch? match = SkillContext.skillStartRegex.firstMatch(content); - if (match != null) { - final String yamlStr = match.group(1)!; - final Object? doc = loadYaml(yamlStr); - if (doc is YamlMap) { - parsedYaml = doc; + + if (skillMdExists) { + try { + content = await skillMdFile.readAsString(); + } on FileSystemException catch (e) { + validationErrors.add( + ValidationError( + ruleId: skillFileInaccessible, + file: skillMdFile.path, + message: 'Failed to read $_skillFileName: $e', + severity: _getSeverity(skillFileInaccessible, AnalysisSeverity.error), + ), + ); + return ValidationResult(validationErrors: validationErrors); + } catch (e) { + validationErrors.add( + ValidationError( + ruleId: unexpectedError, + file: skillMdFile.path, + message: 'Unexpected error reading $_skillFileName: $e', + severity: _getSeverity(unexpectedError, AnalysisSeverity.error), + ), + ); + return ValidationResult(validationErrors: validationErrors); + } + + try { + final RegExpMatch? match = SkillContext.skillStartRegex.firstMatch(content); + if (match != null) { + final String yamlStr = match.group(1)!; + final Object? doc = loadYaml(yamlStr); + if (doc is YamlMap) { + parsedYaml = doc; + } else { + yamlParsingError = 'YAML frontmatter is not a map'; + } } else { - yamlParsingError = 'YAML frontmatter is not a map'; + yamlParsingError = 'Missing YAML metadata in $_skillFileName'; } - } else { - yamlParsingError = 'Missing YAML metadata in $_skillFileName'; + } catch (e) { + yamlParsingError = 'Failed to parse YAML: $e'; } - } catch (e) { - yamlParsingError = 'Failed to parse YAML: $e'; } final context = SkillContext( @@ -110,6 +117,9 @@ class Validator { ); for (final SkillRule rule in _rules) { + if (!skillMdExists && rule.name != PathDoesNotExistRule.ruleName) { + continue; + } final List errors = await rule.validate(context); for (final error in errors) { if (error.severity != rule.severity) { @@ -124,9 +134,18 @@ class Validator { return ValidationResult(validationErrors: validationErrors, context: context); } + /// Compiles the final list of active rules for the validator. + /// + /// * [customRuleSeverities] overrides default severities of rules (e.g. mapping a rule to [AnalysisSeverity.disabled] or [AnalysisSeverity.warning]). + /// * [customRules] specifies custom rules to be included in the validation. + /// * [ruleOptions] maps rule names to their rule-specific custom options maps (e.g. passing regex patterns, thresholds, lists). + /// + /// Rules configured with [AnalysisSeverity.disabled] are excluded. + /// Throws an [ArgumentError] if a duplicate rule name is encountered. static List _buildRules( - Map customSeverities, + Map customRuleSeverities, List customRules, + Map> ruleOptions, ) { final rules = []; final seenNames = {}; @@ -142,8 +161,9 @@ class Validator { } for (final CheckType check in RuleRegistry.allChecks) { - final AnalysisSeverity severity = customSeverities[check.name] ?? check.defaultSeverity; - final SkillRule? rule = RuleRegistry.createRule(check.name, severity); + final AnalysisSeverity severity = customRuleSeverities[check.name] ?? check.defaultSeverity; + final Map? options = ruleOptions[check.name]; + final SkillRule? rule = RuleRegistry.createRule(check.name, severity, options); if (rule != null) { addRule(rule); } @@ -153,86 +173,4 @@ class Validator { return rules; } - - Future _checkDirectoryStructure( - Directory dir, - List validationErrors, - ) async { - final AnalysisSeverity pathDoesNotExistSeverity = _getSeverity( - pathDoesNotExist, - AnalysisSeverity.error, - ); - - if (!dir.existsSync()) { - if (File(dir.path).existsSync()) { - validationErrors.add( - ValidationError( - ruleId: pathDoesNotExist, - file: dir.path, - message: 'Path is not a directory: ${dir.path} (see $_dirStructureUrl)', - severity: pathDoesNotExistSeverity, - ), - ); - } else { - validationErrors.add( - ValidationError( - ruleId: pathDoesNotExist, - file: dir.path, - message: 'Directory does not exist: ${dir.path} (see $_dirStructureUrl)', - severity: pathDoesNotExistSeverity, - ), - ); - } - return false; - } - - final skillMdFile = File(p.join(dir.path, _skillFileName)); - if (!skillMdFile.existsSync()) { - validationErrors.add( - ValidationError( - ruleId: pathDoesNotExist, - file: dir.path, - message: '$_skillFileName is missing in directory: ${dir.path} (see $_dirStructureUrl)', - severity: pathDoesNotExistSeverity, - ), - ); - return false; - } - return true; - } -} - -/// The result of a skill directory validation attempt. -class ValidationResult { - ValidationResult({ - this.validationErrors = const [], - List warnings = const [], - this.context, - }) : _manualWarnings = warnings; - - /// The context used during validation. - final SkillContext? context; - - /// Whether the skill directory is valid according to the specification. - bool get isValid => - !validationErrors.any((e) => e.severity == AnalysisSeverity.error && !e.isIgnored); - - /// A list of structured validation errors found. - final List validationErrors; - - final List _manualWarnings; - - /// A list of error messages for failing checks (excluding ignored ones). - List get errors => validationErrors - .where((e) => e.severity == AnalysisSeverity.error && !e.isIgnored) - .map((e) => e.message) - .toList(); - - /// A list of warning messages for suboptimal setups or recommendations. - List get warnings => [ - ..._manualWarnings, - ...validationErrors - .where((e) => e.severity == AnalysisSeverity.warning && !e.isIgnored) - .map((e) => e.message), - ]; } diff --git a/tool/dart_skills_lint/test/absolute_paths_test.dart b/tool/dart_skills_lint/test/absolute_paths_test.dart index 46c3261..27ca0e1 100644 --- a/tool/dart_skills_lint/test/absolute_paths_test.dart +++ b/tool/dart_skills_lint/test/absolute_paths_test.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'package:dart_skills_lint/src/models/analysis_severity.dart'; import 'package:dart_skills_lint/src/models/skill_context.dart'; +import 'package:dart_skills_lint/src/models/validation_result.dart'; import 'package:dart_skills_lint/src/rules/absolute_paths_rule.dart'; import 'package:dart_skills_lint/src/rules/relative_paths_rule.dart'; @@ -67,7 +68,7 @@ void main() { ).writeAsString('${buildFrontmatter(name: 'test-skill')}[Relative link](C:relative.md)\n'); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.disabled}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.disabled}, ); final ValidationResult result = await validator.validate(skillDir); @@ -83,7 +84,7 @@ void main() { ).writeAsString('${buildFrontmatter(name: 'test-skill')}[Relative link](file.md)\n'); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.disabled}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.disabled}, ); final ValidationResult result = await validator.validate(skillDir); @@ -98,7 +99,7 @@ void main() { ); final validator = Validator( - ruleOverrides: {AbsolutePathsRule.ruleName: AnalysisSeverity.disabled}, + customRuleSeverities: {AbsolutePathsRule.ruleName: AnalysisSeverity.disabled}, ); final ValidationResult result = await validator.validate(skillDir); @@ -116,7 +117,7 @@ void main() { ); final validator = Validator( - ruleOverrides: {AbsolutePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {AbsolutePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); diff --git a/tool/dart_skills_lint/test/api_defaults_test.dart b/tool/dart_skills_lint/test/api_defaults_test.dart index d7bb29c..132b09e 100644 --- a/tool/dart_skills_lint/test/api_defaults_test.dart +++ b/tool/dart_skills_lint/test/api_defaults_test.dart @@ -35,7 +35,7 @@ void main() { // Create validator with the rule disabled. final validator = Validator( - ruleOverrides: {'valid-yaml-metadata': AnalysisSeverity.disabled}, + customRuleSeverities: {'valid-yaml-metadata': AnalysisSeverity.disabled}, ); final ValidationResult result = await validator.validate(skillDir); @@ -66,7 +66,7 @@ dart_skills_lint: final Configuration config = await ConfigParser.loadConfig( path: '~/dart_skills_lint_temp_test.yaml', ); - expect(config.configuredRules, contains('check-relative-paths')); + expect(config.ruleSeverities, contains('check-relative-paths')); } finally { if (tempFile.existsSync()) { await tempFile.delete(); @@ -96,7 +96,7 @@ Line with space directoryConfigs: [ LintTargetConfig( path: configDir.path, - rules: {'check-trailing-whitespace': AnalysisSeverity.error}, + ruleSeverities: {'check-trailing-whitespace': AnalysisSeverity.error}, ), ], ); @@ -141,14 +141,14 @@ dart_skills_lint: directoryConfigs: [ LintTargetConfig( path: p.join(tempDir.path, 'skills'), - rules: { + ruleSeverities: { 'check-trailing-whitespace': AnalysisSeverity.error, 'description-length': AnalysisSeverity.warning, }, ), LintTargetConfig( path: p.join(tempDir.path, 'skills/nested'), - rules: { + ruleSeverities: { 'description-length': AnalysisSeverity.error, 'check-trailing-whitespace': AnalysisSeverity.disabled, }, @@ -158,7 +158,7 @@ dart_skills_lint: final session = ValidationSession( config: config, - resolvedRules: {}, + resolvedRuleSeverities: {}, ignoreFileOverride: null, customRules: [], printWarnings: true, @@ -170,14 +170,14 @@ dart_skills_lint: ); // Parent path should have parent rules applied - final Map parentRules = session.resolveRulesForPath( + final Map parentRules = session.resolveRuleSeveritiesForPath( p.join(tempDir.path, 'skills/some-skill'), ); expect(parentRules['check-trailing-whitespace'], equals(AnalysisSeverity.error)); expect(parentRules['description-length'], equals(AnalysisSeverity.warning)); // Child path should merge and override parent rules - final Map childRules = session.resolveRulesForPath( + final Map childRules = session.resolveRuleSeveritiesForPath( p.join(tempDir.path, 'skills/nested/nested-skill'), ); expect( @@ -197,18 +197,18 @@ dart_skills_lint: LintTargetConfig( path: p.join(tempDir.path, 'skills'), ignoreFile: 'parent_ignores.json', - rules: {}, + ruleSeverities: {}, ), LintTargetConfig( path: p.join(tempDir.path, 'skills/nested'), - rules: {}, + ruleSeverities: {}, ), ], ); final session = ValidationSession( config: config, - resolvedRules: {}, + resolvedRuleSeverities: {}, ignoreFileOverride: null, customRules: [], printWarnings: true, @@ -239,14 +239,14 @@ dart_skills_lint: directoryConfigs: [ LintTargetConfig( path: 'skills', - rules: {'check-trailing-whitespace': AnalysisSeverity.error}, + ruleSeverities: {'check-trailing-whitespace': AnalysisSeverity.error}, ), ], ); final session = ValidationSession( config: config, - resolvedRules: {}, + resolvedRuleSeverities: {}, ignoreFileOverride: null, customRules: [], printWarnings: true, @@ -258,14 +258,16 @@ dart_skills_lint: ); // 1. Evaluate relative input: 'skills/my-skill' - final Map relativeRules = session.resolveRulesForPath( + final Map relativeRules = session.resolveRuleSeveritiesForPath( 'skills/my-skill', ); expect(relativeRules['check-trailing-whitespace'], equals(AnalysisSeverity.error)); // 2. Evaluate absolute input final String absoluteInput = p.absolute('skills/my-skill'); - final Map absoluteRules = session.resolveRulesForPath(absoluteInput); + final Map absoluteRules = session.resolveRuleSeveritiesForPath( + absoluteInput, + ); expect(absoluteRules['check-trailing-whitespace'], equals(AnalysisSeverity.error)); }); } diff --git a/tool/dart_skills_lint/test/cli_integration_test.dart b/tool/dart_skills_lint/test/cli_integration_test.dart index 2c1f415..3998da8 100644 --- a/tool/dart_skills_lint/test/cli_integration_test.dart +++ b/tool/dart_skills_lint/test/cli_integration_test.dart @@ -482,7 +482,7 @@ dart_skills_lint: } }); - test('CLI help does not display path-does-not-exist', () async { + test('CLI help displays path-does-not-exist', () async { final TestProcess process = await TestProcess.start('dart', [ p.normalize(p.absolute('bin/cli.dart')), '--help', @@ -491,7 +491,7 @@ dart_skills_lint: final List stdout = await process.stdout.rest.toList(); final String stdoutStr = stdout.join('\n'); - expect(stdoutStr, isNot(contains(Validator.pathDoesNotExist))); + expect(stdoutStr, contains(Validator.pathDoesNotExist)); }); test('ignores directory missing SKILL.md if listed in ignore file', () async { diff --git a/tool/dart_skills_lint/test/custom_rule_test.dart b/tool/dart_skills_lint/test/custom_rule_test.dart index 09a56ca..b1984e4 100644 --- a/tool/dart_skills_lint/test/custom_rule_test.dart +++ b/tool/dart_skills_lint/test/custom_rule_test.dart @@ -122,5 +122,22 @@ Body'''); expect(() => Validator(customRules: [rule1, rule2]), throwsArgumentError); }); + + test('Validator skips other rules if SKILL.md is missing', () async { + final Directory skillDir = await Directory('${tempDir.path}/missing-skill').create(); + + final validator = Validator(customRules: [AlwaysFailsRule()]); + final ValidationResult result = await validator.validate(skillDir); + + // Verify path-does-not-exist error is reported + expect(result.isValid, isFalse); + expect(result.errors, contains(contains('SKILL.md is missing'))); + + // Verify always-fails-rule was NOT executed (its error is not present) + final bool hasAlwaysFails = result.validationErrors.any( + (e) => e.ruleId == 'always-fails-rule', + ); + expect(hasAlwaysFails, isFalse); + }); }); } diff --git a/tool/dart_skills_lint/test/directory_structure_test.dart b/tool/dart_skills_lint/test/directory_structure_test.dart index 8d501a1..cc27920 100644 --- a/tool/dart_skills_lint/test/directory_structure_test.dart +++ b/tool/dart_skills_lint/test/directory_structure_test.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:dart_skills_lint/src/models/analysis_severity.dart'; +import 'package:dart_skills_lint/src/models/validation_result.dart'; import 'package:dart_skills_lint/src/validator.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -120,7 +121,7 @@ void main() { await IOOverrides.runWithIOOverrides(() async { try { final validator = Validator( - ruleOverrides: {Validator.skillFileInaccessible: AnalysisSeverity.warning}, + customRuleSeverities: {Validator.skillFileInaccessible: AnalysisSeverity.warning}, ); final ValidationResult validationResult = await validator.validate(skillDir); diff --git a/tool/dart_skills_lint/test/field_constraints_test.dart b/tool/dart_skills_lint/test/field_constraints_test.dart index 1321cda..10cf126 100644 --- a/tool/dart_skills_lint/test/field_constraints_test.dart +++ b/tool/dart_skills_lint/test/field_constraints_test.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:dart_skills_lint/src/models/skill_context.dart'; +import 'package:dart_skills_lint/src/models/validation_result.dart'; import 'package:dart_skills_lint/src/rules/description_length_rule.dart'; import 'package:dart_skills_lint/src/rules/name_format_rule.dart'; import 'package:dart_skills_lint/src/rules/valid_yaml_metadata_rule.dart'; diff --git a/tool/dart_skills_lint/test/metadata_validation_test.dart b/tool/dart_skills_lint/test/metadata_validation_test.dart index cd807a7..2312473 100644 --- a/tool/dart_skills_lint/test/metadata_validation_test.dart +++ b/tool/dart_skills_lint/test/metadata_validation_test.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:dart_skills_lint/src/models/validation_error.dart'; +import 'package:dart_skills_lint/src/models/validation_result.dart'; import 'package:dart_skills_lint/src/rules/disallowed_field_rule.dart'; import 'package:dart_skills_lint/src/validator.dart'; import 'package:test/test.dart'; diff --git a/tool/dart_skills_lint/test/relative_path_flag_test.dart b/tool/dart_skills_lint/test/relative_path_flag_test.dart index b2a30dd..897b5f7 100644 --- a/tool/dart_skills_lint/test/relative_path_flag_test.dart +++ b/tool/dart_skills_lint/test/relative_path_flag_test.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:dart_skills_lint/src/models/analysis_severity.dart'; +import 'package:dart_skills_lint/src/models/validation_result.dart'; import 'package:dart_skills_lint/src/rules/absolute_paths_rule.dart'; import 'package:dart_skills_lint/src/rules/relative_paths_rule.dart'; import 'package:dart_skills_lint/src/validator.dart'; @@ -34,7 +35,7 @@ void main() { ); final validator = Validator( - ruleOverrides: { + customRuleSeverities: { RelativePathsRule.ruleName: AnalysisSeverity.warning, AbsolutePathsRule.ruleName: AnalysisSeverity.error, }, @@ -58,7 +59,7 @@ void main() { await File('${skillDir.path}/valid.md').writeAsString('Valid file content'); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); diff --git a/tool/dart_skills_lint/test/relative_paths_test.dart b/tool/dart_skills_lint/test/relative_paths_test.dart index 3225ad8..9c2b19b 100644 --- a/tool/dart_skills_lint/test/relative_paths_test.dart +++ b/tool/dart_skills_lint/test/relative_paths_test.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:dart_skills_lint/src/models/analysis_severity.dart'; +import 'package:dart_skills_lint/src/models/validation_result.dart'; import 'package:dart_skills_lint/src/rules/absolute_paths_rule.dart'; import 'package:dart_skills_lint/src/rules/relative_paths_rule.dart'; import 'package:dart_skills_lint/src/validator.dart'; @@ -37,7 +38,7 @@ void main() { await File('${refDir.path}/DETAILS.md').writeAsString('Details here'); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); @@ -53,7 +54,7 @@ void main() { ); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); @@ -78,7 +79,7 @@ void main() { await File('${refs.path}/DETAILS.md').writeAsString('Details'); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); expect(result.isValid, isTrue); @@ -96,7 +97,7 @@ void main() { await File('${refs.path}/UNRELATED.txt').writeAsString('Nope'); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); expect(result.isValid, isTrue); @@ -111,7 +112,7 @@ void main() { ); final validator = Validator( - ruleOverrides: { + customRuleSeverities: { RelativePathsRule.ruleName: AnalysisSeverity.warning, AbsolutePathsRule.ruleName: AnalysisSeverity.error, }, @@ -129,7 +130,7 @@ void main() { ); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); @@ -148,7 +149,7 @@ void main() { await File('${imgDir.path}/screenshot.png').writeAsString('image content'); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); @@ -168,7 +169,7 @@ void main() { await File('${tempDir.path}/a/CONTRIBUTING.md').create(recursive: true); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); @@ -186,7 +187,7 @@ void main() { await File('${skillDir.path}/styleguide.md').writeAsString('Styleguide content'); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); @@ -204,7 +205,7 @@ void main() { await File('${skillDir.path}/styleguide.md').writeAsString('Styleguide content'); final validator = Validator( - ruleOverrides: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, + customRuleSeverities: {RelativePathsRule.ruleName: AnalysisSeverity.warning}, ); final ValidationResult result = await validator.validate(skillDir); diff --git a/tool/dart_skills_lint/test/resolve_rules_test.dart b/tool/dart_skills_lint/test/resolve_rules_test.dart index 8fd667b..35af38b 100644 --- a/tool/dart_skills_lint/test/resolve_rules_test.dart +++ b/tool/dart_skills_lint/test/resolve_rules_test.dart @@ -12,7 +12,7 @@ import 'package:dart_skills_lint/src/rules/valid_yaml_metadata_rule.dart'; import 'package:test/test.dart'; void main() { - group('resolveRules', () { + group('resolveRuleSeverities', () { ArgParser createParser() { final parser = ArgParser(); for (final CheckType check in RuleRegistry.allChecks) { @@ -24,19 +24,20 @@ void main() { test('returns empty map when no CLI overrides are provided', () { final ArgResults results = createParser().parse([]); - final Map resolved = resolveRules(results); + final Map resolved = resolveRuleSeverities(results); expect( resolved, isEmpty, - reason: 'resolveRules should return an empty map when no CLI override flags are provided.', + reason: + 'resolveRuleSeverities should return an empty map when no CLI override flags are provided.', ); }); test('CLI flags override defaults', () { final ArgResults results = createParser().parse(['--${RelativePathsRule.ruleName}']); - final Map resolved = resolveRules(results); + final Map resolved = resolveRuleSeverities(results); expect(resolved[RelativePathsRule.ruleName], AnalysisSeverity.error); }); @@ -44,7 +45,7 @@ void main() { test('CLI flag disabled overrides defaults', () { final ArgResults results = createParser().parse(['--no-${ValidYamlMetadataRule.ruleName}']); - final Map resolved = resolveRules(results); + final Map resolved = resolveRuleSeverities(results); expect(resolved[ValidYamlMetadataRule.ruleName], AnalysisSeverity.disabled); }); diff --git a/tool/dart_skills_lint/test/tracked_skills_publishing_test.dart b/tool/dart_skills_lint/test/tracked_skills_publishing_test.dart index a07bb17..aec3f08 100644 --- a/tool/dart_skills_lint/test/tracked_skills_publishing_test.dart +++ b/tool/dart_skills_lint/test/tracked_skills_publishing_test.dart @@ -40,7 +40,7 @@ void main() { final Configuration config = await ConfigParser.loadConfig(); final session = ValidationSession( config: config, - resolvedRules: {}, + resolvedRuleSeverities: {}, ignoreFileOverride: null, customRules: [], printWarnings: false, @@ -53,10 +53,11 @@ void main() { for (final skillDir in trackedSkillDirs) { final expectedPath = '.agents/skills/$skillDir'; - final Map resolvedRules = session.resolveRulesForPath(expectedPath); + final Map resolvedRuleSeverities = session + .resolveRuleSeveritiesForPath(expectedPath); expect( - resolvedRules.containsKey('prevent-skills-sh-publishing'), + resolvedRuleSeverities.containsKey('prevent-skills-sh-publishing'), isTrue, reason: 'The tracked skill "$skillDir" must have "prevent-skills-sh-publishing" explicitly configured in dart_skills_lint.yaml.', From fa6f435525c789458657e09c10038cb5abbda3c6 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 13 Jul 2026 21:27:23 -0400 Subject: [PATCH 4/9] feat: introduce ConfigurableSkillRule and CustomRuleOptions type-safe wrapper - Extract options validation to ConfigurableSkillRule base constructor. - Introduce CustomRuleOptions class wrapping options configuration maps. - Refactor PathDoesNotExistRule to extend ConfigurableSkillRule. --- .../lib/dart_skills_lint.dart | 2 + .../src/models/configurable_skill_rule.dart | 42 +++++++++ .../lib/src/models/custom_rule_options.dart | 26 ++++++ .../lib/src/models/skill_rule.dart | 3 - .../lib/src/models/validation_result.dart | 42 +++++++++ .../lib/src/rule_registry.dart | 6 +- .../src/rules/path_does_not_exist_rule.dart | 93 +++++++++++++++++++ 7 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 tool/dart_skills_lint/lib/src/models/configurable_skill_rule.dart create mode 100644 tool/dart_skills_lint/lib/src/models/custom_rule_options.dart create mode 100644 tool/dart_skills_lint/lib/src/models/validation_result.dart create mode 100644 tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart diff --git a/tool/dart_skills_lint/lib/dart_skills_lint.dart b/tool/dart_skills_lint/lib/dart_skills_lint.dart index 3ed50ed..6482ce1 100644 --- a/tool/dart_skills_lint/lib/dart_skills_lint.dart +++ b/tool/dart_skills_lint/lib/dart_skills_lint.dart @@ -5,6 +5,8 @@ export 'src/config_parser.dart'; export 'src/entry_point.dart'; export 'src/models/analysis_severity.dart'; +export 'src/models/configurable_skill_rule.dart'; +export 'src/models/custom_rule_options.dart'; export 'src/models/skill_context.dart'; export 'src/models/skill_rule.dart'; export 'src/models/validation_error.dart'; diff --git a/tool/dart_skills_lint/lib/src/models/configurable_skill_rule.dart b/tool/dart_skills_lint/lib/src/models/configurable_skill_rule.dart new file mode 100644 index 0000000..e9a0b79 --- /dev/null +++ b/tool/dart_skills_lint/lib/src/models/configurable_skill_rule.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'custom_rule_options.dart'; +import 'skill_rule.dart'; + +/// Intermediate abstract class for rules that support configuration options. +/// +/// Concrete subclasses of [ConfigurableSkillRule] are compile-time required to pass +/// their custom options to the base class constructor. Option keys and types are +/// automatically validated against the rule's defined [allowedOptions] schema. +abstract class ConfigurableSkillRule extends SkillRule { + ConfigurableSkillRule(this.customRuleOptions) { + _validateOptions(); + } + + /// The parsed custom configuration options for this rule. + final CustomRuleOptions? customRuleOptions; + + /// The schema mapping allowed options parameter keys to their expected Types. + Map get allowedOptions; + + void _validateOptions() { + final CustomRuleOptions? opts = customRuleOptions; + if (opts == null) { + return; + } + for (final String key in opts.keys) { + if (!allowedOptions.containsKey(key)) { + throw ArgumentError('Rule "$name" does not support option "$key".'); + } + final Type? expectedType = allowedOptions[key]; + final Object? actualValue = opts[key]; + if (actualValue != null && actualValue.runtimeType != expectedType) { + throw ArgumentError( + 'Option "$key" for rule "$name" must be of type $expectedType (found ${actualValue.runtimeType}).', + ); + } + } + } +} diff --git a/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart b/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart new file mode 100644 index 0000000..f29f8ec --- /dev/null +++ b/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart @@ -0,0 +1,26 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// A wrapper around raw rule options parameters. +/// +/// Prevents exposing raw [Map] APIs directly inside rule logic, and provides +/// standard lookups and properties for rule configuration parameters. +class CustomRuleOptions { + const CustomRuleOptions(this.params); + + /// The underlying map containing the parameters. + final Map params; + + /// Whether the configuration contains no parameters. + bool get isEmpty => params.isEmpty; + + /// Whether the configuration contains parameters. + bool get isNotEmpty => params.isNotEmpty; + + /// Retrieves the value of the parameter associated with [key]. + Object? operator [](String key) => params[key]; + + /// The parameter keys defined in this configuration. + Iterable get keys => params.keys; +} diff --git a/tool/dart_skills_lint/lib/src/models/skill_rule.dart b/tool/dart_skills_lint/lib/src/models/skill_rule.dart index 3c392df..041f127 100644 --- a/tool/dart_skills_lint/lib/src/models/skill_rule.dart +++ b/tool/dart_skills_lint/lib/src/models/skill_rule.dart @@ -26,9 +26,6 @@ abstract class SkillRule { AnalysisSeverity get severity; - /// Custom configuration options supported by this rule (mapping option name to expected Type). - Map get allowedOptions => const {}; - /// Validates the skill provided in [context]. Future> validate(SkillContext context); } diff --git a/tool/dart_skills_lint/lib/src/models/validation_result.dart b/tool/dart_skills_lint/lib/src/models/validation_result.dart new file mode 100644 index 0000000..93f0145 --- /dev/null +++ b/tool/dart_skills_lint/lib/src/models/validation_result.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'analysis_severity.dart'; +import 'skill_context.dart'; +import 'validation_error.dart'; + +/// The result of a skill directory validation attempt. +class ValidationResult { + ValidationResult({ + this.validationErrors = const [], + List warnings = const [], + this.context, + }) : _manualWarnings = warnings; + + /// The context used during validation. + final SkillContext? context; + + /// Whether the skill directory is valid according to the specification. + bool get isValid => + !validationErrors.any((e) => e.severity == AnalysisSeverity.error && !e.isIgnored); + + /// A list of structured validation errors found. + final List validationErrors; + + final List _manualWarnings; + + /// A list of error messages for failing checks (excluding ignored ones). + List get errors => validationErrors + .where((e) => e.severity == AnalysisSeverity.error && !e.isIgnored) + .map((e) => e.message) + .toList(); + + /// A list of warning messages for suboptimal setups or recommendations. + List get warnings => [ + ..._manualWarnings, + ...validationErrors + .where((e) => e.severity == AnalysisSeverity.warning && !e.isIgnored) + .map((e) => e.message), + ]; +} diff --git a/tool/dart_skills_lint/lib/src/rule_registry.dart b/tool/dart_skills_lint/lib/src/rule_registry.dart index 92de96d..df680c3 100644 --- a/tool/dart_skills_lint/lib/src/rule_registry.dart +++ b/tool/dart_skills_lint/lib/src/rule_registry.dart @@ -4,6 +4,7 @@ import 'models/analysis_severity.dart'; import 'models/check_type.dart'; +import 'models/custom_rule_options.dart'; import 'models/skill_rule.dart'; import 'rules/absolute_paths_rule.dart'; import 'rules/description_length_rule.dart'; @@ -76,7 +77,10 @@ class RuleRegistry { ]) { switch (name) { case PathDoesNotExistRule.ruleName: - return PathDoesNotExistRule(severity: severity, options: options); + return PathDoesNotExistRule( + severity: severity, + customRuleOptions: options != null ? CustomRuleOptions(options) : null, + ); case AbsolutePathsRule.ruleName: return AbsolutePathsRule(severity: severity); case DescriptionLengthRule.ruleName: diff --git a/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart b/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart new file mode 100644 index 0000000..35345f5 --- /dev/null +++ b/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart @@ -0,0 +1,93 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import '../models/analysis_severity.dart'; +import '../models/configurable_skill_rule.dart'; +import '../models/custom_rule_options.dart'; +import '../models/skill_context.dart'; +import '../models/validation_error.dart'; + +/// Checks that a skill directory exists and contains a SKILL.md file. +/// +/// If [exclude] is specified, it compiles it as a [RegExp] and skips validation +/// if the directory name matches the pattern. +class PathDoesNotExistRule extends ConfigurableSkillRule { + PathDoesNotExistRule({required this.severity, CustomRuleOptions? customRuleOptions}) + : super(customRuleOptions) { + if (customRuleOptions != null) { + final Object? excludeVal = customRuleOptions[excludeOption]; + if (excludeVal is String && excludeVal.isNotEmpty) { + _excludeRegExp = RegExp(excludeVal); + } + } + } + + static const String ruleName = 'path-does-not-exist'; + static const String excludeOption = 'exclude'; + static const String _skillFileName = 'SKILL.md'; + static const String _dirStructureUrl = 'https://agentskills.io/specification#directory-structure'; + + @override + final AnalysisSeverity severity; + + RegExp? _excludeRegExp; + + @override + String get name => ruleName; + + @override + Map get allowedOptions => const {excludeOption: String}; + + @override + Future> validate(SkillContext context) async { + final List errors = []; + final Directory dir = context.directory; + final String dirName = p.basename(dir.path); + + if (_excludeRegExp != null && _excludeRegExp!.hasMatch(dirName)) { + return errors; + } + + if (!dir.existsSync()) { + if (File(dir.path).existsSync()) { + errors.add( + ValidationError( + ruleId: ruleName, + file: dir.path, + message: 'Path is not a directory: ${dir.path} (see $_dirStructureUrl)', + severity: severity, + ), + ); + } else { + errors.add( + ValidationError( + ruleId: ruleName, + file: dir.path, + message: 'Directory does not exist: ${dir.path} (see $_dirStructureUrl)', + severity: severity, + ), + ); + } + return errors; + } + + final skillMdFile = File(p.join(dir.path, _skillFileName)); + if (!skillMdFile.existsSync()) { + errors.add( + ValidationError( + ruleId: ruleName, + file: dir.path, + message: '$_skillFileName is missing in directory: ${dir.path} (see $_dirStructureUrl)', + severity: severity, + ), + ); + } + + return errors; + } +} From 90d7da4b359c55085484b5736983633d016a549d Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 13 Jul 2026 22:02:28 -0400 Subject: [PATCH 5/9] refactor: unify options validation and migrate parser to CustomRuleOptions - Centralize options schema validation and formatting in CheckType.validateOptions. - Remove redundant allowedOptions getter from ConfigurableSkillRule and its subclasses. - Update ConfigParser, entrypoint, Validator, and ValidationSession to use CustomRuleOptions instead of raw maps. - Add unit tests for resolving options and testing PathDoesNotExistRule with custom exclusions. --- .../lib/src/config_parser.dart | 106 +++++++++++-- .../dart_skills_lint/lib/src/entry_point.dart | 101 +++++++++++-- .../lib/src/models/check_type.dart | 39 +++++ .../src/models/configurable_skill_rule.dart | 27 ++-- .../lib/src/models/custom_rule_options.dart | 3 + .../lib/src/rule_registry.dart | 7 +- .../src/rules/path_does_not_exist_rule.dart | 3 - .../lib/src/validation_session.dart | 35 ++--- tool/dart_skills_lint/lib/src/validator.dart | 7 +- .../test/config_file_test.dart | 127 ++++++++++++++++ .../test/custom_rule_test.dart | 15 ++ .../test/resolve_options_test.dart | 122 +++++++++++++++ .../rules/path_does_not_exist_rule_test.dart | 141 ++++++++++++++++++ 13 files changed, 665 insertions(+), 68 deletions(-) create mode 100644 tool/dart_skills_lint/test/resolve_options_test.dart create mode 100644 tool/dart_skills_lint/test/rules/path_does_not_exist_rule_test.dart diff --git a/tool/dart_skills_lint/lib/src/config_parser.dart b/tool/dart_skills_lint/lib/src/config_parser.dart index 59fa151..fa8c815 100644 --- a/tool/dart_skills_lint/lib/src/config_parser.dart +++ b/tool/dart_skills_lint/lib/src/config_parser.dart @@ -11,6 +11,7 @@ import 'package:yaml/yaml.dart'; import 'models/analysis_severity.dart'; import 'models/check_type.dart'; +import 'models/custom_rule_options.dart'; import 'path_utils.dart'; import 'rule_registry.dart'; @@ -23,6 +24,7 @@ class ConfigParser { static const _individualSkillsKey = 'individual_skills'; static const _pathKey = 'path'; static const _ignoreFileKey = 'ignore_file'; + static const _severityKey = 'severity'; static const Set _allowedTopLevelKeys = { _rulesKey, @@ -70,7 +72,7 @@ class ConfigParser { final parsingErrors = []; _validateTopLevelKeys(toolConfig, parsingErrors); - final rulesResult = _parseGlobalRules(toolConfig, parsingErrors); + final rulesResult = _parseDefaultRules(toolConfig, parsingErrors); final directoryConfigs = _parseConfigList(toolConfig, _directoriesKey, parsingErrors); final individualSkillConfigs = _parseConfigList( toolConfig, @@ -105,19 +107,71 @@ class ConfigParser { } } - /// Parses the global rules configuration from the `dart_skills_lint` map. - /// Returns a map of rule names to their resolved `AnalysisSeverity`. - static Map _parseRules(YamlMap toolConfig) { - final configuredRules = {}; + /// Parses the baseline rules configuration under the top-level `rules` key. + /// + /// The settings parsed here serve as the global defaults that apply to all + /// validated skills in the project. Any target-specific settings defined + /// under `directories` or `individual_skills` will override these global defaults. + /// + /// Extracts both default severities and options, appending any option type or key + /// validation errors to [parsingErrors]. + static ({Map rules, Map ruleOptions}) + _parseDefaultRules(YamlMap toolConfig, List parsingErrors) { if (toolConfig.containsKey(_rulesKey)) { final rules = toolConfig[_rulesKey]; if (rules is YamlMap) { - for (final key in rules.keys) { - configuredRules[key.toString()] = _parseSeverity(rules[key]?.toString() ?? ''); + return _parseRulesMap(rules, parsingErrors, 'Global rules'); + } + } + return (rules: const {}, ruleOptions: const {}); + } + + /// Parses a map of rules to their respective severity and option configurations. + /// + /// Validates that option keys and value types match their definitions in the registry, + /// appending any validation errors to [parsingErrors] labeled by [contextLabel]. + static ({Map rules, Map ruleOptions}) + _parseRulesMap(YamlMap rulesMap, List parsingErrors, String contextLabel) { + final rules = {}; + final ruleOptions = {}; + + for (final key in rulesMap.keys) { + final ruleName = key.toString(); + final value = rulesMap[key]; + + // Rules must have a unique name so we can assume one match. + final checkMatches = RuleRegistry.allChecks.where((c) => c.name == ruleName); + final CheckType? check = checkMatches.isEmpty ? null : checkMatches.first; + + if (value is YamlMap) { + final severityStr = value[_severityKey]?.toString() ?? ''; + rules[ruleName] = _parseSeverity(severityStr); + + final options = {}; + for (final optKey in value.keys) { + final optName = optKey.toString(); + if (optName == _severityKey) { + continue; + } + options[optName] = value[optKey]; + } + if (options.isNotEmpty) { + final customOpts = CustomRuleOptions(options); + ruleOptions[ruleName] = customOpts; + + if (check != null) { + final errors = check.validateOptions(customOpts); + for (final error in errors) { + parsingErrors.add('$contextLabel: $error'); + } + } } + } else { + rules[ruleName] = _parseSeverity(value?.toString() ?? ''); } } - return configuredRules; + + return (rules: rules, ruleOptions: ruleOptions); } /// Parses a list of targets (directories or individual skills) from the configuration. @@ -165,12 +219,17 @@ class ConfigParser { } final rules = {}; + final ruleOptions = {}; if (dir.containsKey(_rulesKey)) { final localRules = dir[_rulesKey]; if (localRules is YamlMap) { - for (final key in localRules.keys) { - rules[key.toString()] = _parseSeverity(localRules[key]?.toString() ?? ''); - } + final result = _parseRulesMap( + localRules, + parsingErrors, + '$entryLabelCap rules for "$path"', + ); + rules.addAll(result.rules); + ruleOptions.addAll(result.ruleOptions); } else { parsingErrors.add( '$entryLabelCap "$_rulesKey" for "$path" must be a map; ' @@ -193,7 +252,14 @@ class ConfigParser { } } - configs.add(LintTargetConfig(path: path, rules: rules, ignoreFile: ignoreFile)); + configs.add( + LintTargetConfig( + path: path, + ruleSeverities: rules, + ruleOptions: ruleOptions.isEmpty ? null : ruleOptions, + ignoreFile: ignoreFile, + ), + ); } } } @@ -206,14 +272,20 @@ class ConfigParser { /// Allows overriding rules and specifying a custom ignore file for skills /// located within or at this path. class LintTargetConfig { - LintTargetConfig({required this.path, required this.rules, this.ignoreFile}); + LintTargetConfig({ + required this.path, + required this.ruleSeverities, + this.ruleOptions, + this.ignoreFile, + }); /// The path to the directory containing skills. /// /// Can be absolute or relative to the current working directory. /// Supports tilde expansion (e.g., `~/...`). final String path; - final Map rules; + final Map ruleSeverities; + final Map? ruleOptions; final String? ignoreFile; } @@ -222,11 +294,13 @@ class Configuration { Configuration({ this.directoryConfigs = const [], this.individualSkillConfigs = const [], - this.configuredRules = const {}, + this.ruleSeverities = const {}, + this.globalRuleOptions, this.parsingErrors = const [], }); final List directoryConfigs; final List individualSkillConfigs; - final Map configuredRules; + final Map ruleSeverities; + final Map? globalRuleOptions; final List parsingErrors; } diff --git a/tool/dart_skills_lint/lib/src/entry_point.dart b/tool/dart_skills_lint/lib/src/entry_point.dart index cb2337a..5d6573c 100644 --- a/tool/dart_skills_lint/lib/src/entry_point.dart +++ b/tool/dart_skills_lint/lib/src/entry_point.dart @@ -12,6 +12,7 @@ import 'config_parser.dart'; import 'missing_defaults_exception.dart'; import 'models/analysis_severity.dart'; import 'models/check_type.dart'; +import 'models/custom_rule_options.dart'; import 'models/skill_rule.dart'; import 'rule_registry.dart'; import 'validation_session.dart'; @@ -86,12 +87,17 @@ Future runApp(List args) async { final ArgParser parser = _createArgParser(helpFlag); final ArgResults results; + final Map resolvedRuleSeverities; + final Map resolvedRuleOptions; + try { results = parser.parse(args); if (results[helpFlag] as bool) { _printUsage(parser); return; } + resolvedRuleSeverities = resolveRuleSeverities(results); + resolvedRuleOptions = resolveRuleOptionsOverrides(results); } catch (e) { _printUsage(parser, e.toString()); exitCode = 64; // Bad usage @@ -107,8 +113,6 @@ Future runApp(List args) async { final skillDirPaths = results[_skillsDirectoryFlag] as List; final individualSkillPaths = results[_skillOption] as List; - final Map resolvedRules = resolveRules(results); - final printWarnings = results[_printWarningsFlag] as bool; final fastFail = results[_fastFailFlag] as bool; final quiet = results[_quietFlag] as bool; @@ -139,7 +143,8 @@ Future runApp(List args) async { success = await validateSkillsInternal( skillDirPaths: skillDirPaths, individualSkillPaths: individualSkillPaths, - resolvedRules: resolvedRules, + resolvedRuleSeverities: resolvedRuleSeverities, + resolvedRuleOptions: resolvedRuleOptions, printWarnings: printWarnings, fastFail: fastFail, quiet: quiet, @@ -175,6 +180,22 @@ ArgParser _createArgParser(String helpFlag) { defaultsTo: check.defaultSeverity != AnalysisSeverity.disabled, help: check.help, ); + + // Register namespaced delegated options + for (final String optionName in check.allowedOptions.keys) { + final Type expectedType = check.allowedOptions[optionName]!; + if (expectedType == List || expectedType == List) { + parser.addMultiOption( + '${check.name}-$optionName', + help: "Override option '$optionName' list for rule '${check.name}'.", + ); + } else { + parser.addOption( + '${check.name}-$optionName', + help: "Override option '$optionName' for rule '${check.name}'.", + ); + } + } } parser @@ -298,7 +319,8 @@ Future _loadConfig(ArgResults results) async { Future validateSkills({ List skillDirPaths = const [], List individualSkillPaths = const [], - Map resolvedRules = const {}, + Map resolvedRuleSeverities = const {}, + Map resolvedRuleOptions = const {}, bool printWarnings = true, bool fastFail = false, bool quiet = false, @@ -310,7 +332,8 @@ Future validateSkills({ return validateSkillsInternal( skillDirPaths: skillDirPaths, individualSkillPaths: individualSkillPaths, - resolvedRules: resolvedRules, + resolvedRuleSeverities: resolvedRuleSeverities, + resolvedRuleOptions: resolvedRuleOptions, printWarnings: printWarnings, fastFail: fastFail, quiet: quiet, @@ -330,7 +353,8 @@ Future validateSkills({ Future validateSkillsInternal({ List skillDirPaths = const [], List individualSkillPaths = const [], - Map resolvedRules = const {}, + Map resolvedRuleSeverities = const {}, + Map resolvedRuleOptions = const {}, bool printWarnings = true, bool fastFail = false, bool quiet = false, @@ -355,7 +379,8 @@ Future validateSkillsInternal({ final session = ValidationSession( config: config ?? Configuration(), - resolvedRules: resolvedRules, + resolvedRuleSeverities: resolvedRuleSeverities, + resolvedRuleOptions: resolvedRuleOptions, ignoreFileOverride: ignoreFileOverride, customRules: customRules, printWarnings: printWarnings, @@ -428,10 +453,10 @@ List _getEffectiveSkillDirPaths({ } @visibleForTesting -Map resolveRules(ArgResults results) { +Map resolveRuleSeverities(ArgResults results) { final resolved = {}; - // Only load rules explicitly set via CLI flags. + // Only load rule severities explicitly set via CLI flags. for (final CheckType check in RuleRegistry.allChecks) { final String name = check.name; @@ -454,6 +479,64 @@ Map resolveRules(ArgResults results) { return resolved; } +/// Resolves rule-specific options overrides from CLI arguments. +/// +/// Iterates over all checks in [RuleRegistry.allChecks], looks for parsed command-line +/// flags matches in the format `--[rule-name]-[option-name]` (e.g. `--path-does-not-exist-exclude`), +/// and returns a structured map mapping rule names to their custom key-value parameter overrides. +/// +/// Applies type coercion based on the expected type defined in [CheckType.allowedOptions]: +/// * [int]: Coerced using [int.tryParse]. +/// * [bool]: Coerced to `true` if matching "true" case-insensitively, `false` otherwise. +/// * Other: Parsed as-is (e.g. [String] or [List] types). +/// * Empty String: If the CLI flag is passed as an empty string `""`, it is parsed +/// as a `null` map entry to explicitly clear/reset the option value. +Map resolveRuleOptionsOverrides(ArgResults results) { + final overrides = {}; + + for (final CheckType check in RuleRegistry.allChecks) { + final Map checkOverrides = {}; + for (final String optionName in check.allowedOptions.keys) { + final optionFlag = '${check.name}-$optionName'; + if (results.wasParsed(optionFlag)) { + final Type expectedType = check.allowedOptions[optionName]!; + final Object? rawValue = results[optionFlag]; + + if (rawValue == '') { + // Empty string override clears the option completely + checkOverrides[optionName] = null; + } else { + // Coerce type + if (expectedType == int) { + final int? parsedInt = int.tryParse(rawValue.toString()); + if (parsedInt == null) { + throw FormatException( + 'Invalid value "$rawValue" for option "$optionFlag". Expected an integer.', + ); + } + checkOverrides[optionName] = parsedInt; + } else if (expectedType == bool) { + final String lower = rawValue.toString().toLowerCase(); + if (lower != 'true' && lower != 'false') { + throw FormatException( + 'Invalid value "$rawValue" for option "$optionFlag". Expected "true" or "false".', + ); + } + checkOverrides[optionName] = lower == 'true'; + } else { + checkOverrides[optionName] = rawValue; + } + } + } + } + if (checkOverrides.isNotEmpty) { + overrides[check.name] = CustomRuleOptions(checkOverrides); + } + } + + return overrides; +} + void _printUsage(ArgParser parser, [String? error]) { if (error != null) { _log.severe('Error: $error'); diff --git a/tool/dart_skills_lint/lib/src/models/check_type.dart b/tool/dart_skills_lint/lib/src/models/check_type.dart index 38e1457..fac13d0 100644 --- a/tool/dart_skills_lint/lib/src/models/check_type.dart +++ b/tool/dart_skills_lint/lib/src/models/check_type.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'analysis_severity.dart'; +import 'custom_rule_options.dart'; /// Encapsulates metadata and severity state for a specific validation rule. class CheckType { @@ -22,4 +23,42 @@ class CheckType { /// Custom configuration options supported by this check. final Map allowedOptions; + + /// Validates the given [options] against this check's [allowedOptions] schema. + /// + /// Returns a list of error messages for any unrecognized options or type mismatches. + List validateOptions(CustomRuleOptions options) { + final List errors = []; + for (final String key in options.keys) { + if (!allowedOptions.containsKey(key)) { + errors.add('Unrecognized option "$key" for rule "$name".'); + continue; + } + final Type expectedType = allowedOptions[key]!; + final Object? actualValue = options[key]; + if (actualValue != null && !_isTypeValid(actualValue, expectedType)) { + errors.add( + 'Invalid type for option "$key" in rule "$name". ' + 'Expected $expectedType, got "$actualValue" (${actualValue.runtimeType}).', + ); + } + } + return errors; + } + + static bool _isTypeValid(Object value, Type expectedType) { + if (expectedType == String) { + return value is String; + } + if (expectedType == int) { + return value is int; + } + if (expectedType == bool) { + return value is bool; + } + if (expectedType == List || expectedType == List) { + return value is List; + } + return true; + } } diff --git a/tool/dart_skills_lint/lib/src/models/configurable_skill_rule.dart b/tool/dart_skills_lint/lib/src/models/configurable_skill_rule.dart index e9a0b79..0e6893a 100644 --- a/tool/dart_skills_lint/lib/src/models/configurable_skill_rule.dart +++ b/tool/dart_skills_lint/lib/src/models/configurable_skill_rule.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import '../rule_registry.dart'; +import 'check_type.dart'; import 'custom_rule_options.dart'; import 'skill_rule.dart'; @@ -9,7 +11,7 @@ import 'skill_rule.dart'; /// /// Concrete subclasses of [ConfigurableSkillRule] are compile-time required to pass /// their custom options to the base class constructor. Option keys and types are -/// automatically validated against the rule's defined [allowedOptions] schema. +/// automatically validated against the rule's defined schema in [RuleRegistry]. abstract class ConfigurableSkillRule extends SkillRule { ConfigurableSkillRule(this.customRuleOptions) { _validateOptions(); @@ -18,25 +20,20 @@ abstract class ConfigurableSkillRule extends SkillRule { /// The parsed custom configuration options for this rule. final CustomRuleOptions? customRuleOptions; - /// The schema mapping allowed options parameter keys to their expected Types. - Map get allowedOptions; - void _validateOptions() { final CustomRuleOptions? opts = customRuleOptions; if (opts == null) { return; } - for (final String key in opts.keys) { - if (!allowedOptions.containsKey(key)) { - throw ArgumentError('Rule "$name" does not support option "$key".'); - } - final Type? expectedType = allowedOptions[key]; - final Object? actualValue = opts[key]; - if (actualValue != null && actualValue.runtimeType != expectedType) { - throw ArgumentError( - 'Option "$key" for rule "$name" must be of type $expectedType (found ${actualValue.runtimeType}).', - ); - } + // Rules must have a unique name so we can assume one match. + final Iterable checkMatches = RuleRegistry.allChecks.where((c) => c.name == name); + if (checkMatches.isEmpty) { + return; + } + final CheckType check = checkMatches.first; + final List errors = check.validateOptions(opts); + if (errors.isNotEmpty) { + throw ArgumentError(errors.join('\n')); } } } diff --git a/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart b/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart index f29f8ec..ec1251f 100644 --- a/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart +++ b/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart @@ -23,4 +23,7 @@ class CustomRuleOptions { /// The parameter keys defined in this configuration. Iterable get keys => params.keys; + + /// Whether the configuration contains the specified [key]. + bool containsKey(String key) => params.containsKey(key); } diff --git a/tool/dart_skills_lint/lib/src/rule_registry.dart b/tool/dart_skills_lint/lib/src/rule_registry.dart index df680c3..d39a24d 100644 --- a/tool/dart_skills_lint/lib/src/rule_registry.dart +++ b/tool/dart_skills_lint/lib/src/rule_registry.dart @@ -73,14 +73,11 @@ class RuleRegistry { static SkillRule? createRule( String name, AnalysisSeverity severity, [ - Map? options, + CustomRuleOptions? options, ]) { switch (name) { case PathDoesNotExistRule.ruleName: - return PathDoesNotExistRule( - severity: severity, - customRuleOptions: options != null ? CustomRuleOptions(options) : null, - ); + return PathDoesNotExistRule(severity: severity, customRuleOptions: options); case AbsolutePathsRule.ruleName: return AbsolutePathsRule(severity: severity); case DescriptionLengthRule.ruleName: diff --git a/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart b/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart index 35345f5..1f7d41b 100644 --- a/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart +++ b/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart @@ -40,9 +40,6 @@ class PathDoesNotExistRule extends ConfigurableSkillRule { @override String get name => ruleName; - @override - Map get allowedOptions => const {excludeOption: String}; - @override Future> validate(SkillContext context) async { final List errors = []; diff --git a/tool/dart_skills_lint/lib/src/validation_session.dart b/tool/dart_skills_lint/lib/src/validation_session.dart index 4c8bb3c..214b052 100644 --- a/tool/dart_skills_lint/lib/src/validation_session.dart +++ b/tool/dart_skills_lint/lib/src/validation_session.dart @@ -12,6 +12,7 @@ import 'package:path/path.dart' as p; import 'config_parser.dart'; import 'fixable_rule.dart'; import 'models/analysis_severity.dart'; +import 'models/custom_rule_options.dart'; import 'models/ignore_entry.dart'; import 'models/skill_context.dart'; import 'models/skill_rule.dart'; @@ -86,7 +87,7 @@ class ValidationSession { final Configuration config; final Map resolvedRuleSeverities; - final Map> resolvedRuleOptions; + final Map resolvedRuleOptions; final String? ignoreFileOverride; final List customRules; final bool printWarnings; @@ -131,7 +132,7 @@ class ValidationSession { final Map localRuleSeverities = resolveRuleSeveritiesForPath( normalizedSkillPath, ); - final Map> localRuleOptions = resolveRuleOptionsForPath( + final Map localRuleOptions = resolveRuleOptionsForPath( normalizedSkillPath, ); final String? localIgnoreFile = resolveIgnoreFile(normalizedSkillPath); @@ -227,7 +228,7 @@ class ValidationSession { final Map localRuleSeverities = resolveRuleSeveritiesForPath( normalizedSkillPath, ); - final Map> localRuleOptions = resolveRuleOptionsForPath( + final Map localRuleOptions = resolveRuleOptionsForPath( normalizedSkillPath, ); final String? localIgnoreFile = resolveIgnoreFile(normalizedSkillPath); @@ -354,15 +355,15 @@ class ValidationSession { /// * The outer key is the rule name (e.g., `'path-does-not-exist'`). /// * The inner map contains the resolved key-value options for that rule (e.g., `{'exclude': '.*-workspace'}`). @visibleForTesting - Map> resolveRuleOptionsForPath(String path) { + Map resolveRuleOptionsForPath(String path) { final String targetPath = p.absolute(path); - final resolved = >{}; + final resolvedRaw = >{}; // Step 1: Initialize with global rule options from configuration file. - final Map>? globalOptions = config.globalRuleOptions; + final Map? globalOptions = config.globalRuleOptions; if (globalOptions != null) { - for (final MapEntry> entry in globalOptions.entries) { - resolved[entry.key] = Map.from(entry.value); + for (final MapEntry entry in globalOptions.entries) { + resolvedRaw[entry.key] = Map.from(entry.value.params); } } @@ -373,23 +374,23 @@ class ValidationSession { final bool isMatch = p.equals(configPath, targetPath) || p.isWithin(configPath, targetPath); if (isMatch && entry.config.ruleOptions != null) { - for (final MapEntry> ruleEntry + for (final MapEntry ruleEntry in entry.config.ruleOptions!.entries) { - resolved[ruleEntry.key] = Map.from(ruleEntry.value); + resolvedRaw[ruleEntry.key] = Map.from(ruleEntry.value.params); } } } // Step 3: Apply CLI overrides (command-line options take highest precedence). // An override value of `null` explicitly removes/clears the option. - for (final MapEntry> ruleOverrideEntry + for (final MapEntry ruleOverrideEntry in resolvedRuleOptions.entries) { final String ruleName = ruleOverrideEntry.key; - final Map overrides = ruleOverrideEntry.value; + final CustomRuleOptions overrides = ruleOverrideEntry.value; - final Map currentOptions = resolved[ruleName] ?? {}; + final Map currentOptions = resolvedRaw[ruleName] ?? {}; - for (final MapEntry optionEntry in overrides.entries) { + for (final MapEntry optionEntry in overrides.params.entries) { final String optionName = optionEntry.key; final Object? value = optionEntry.value; @@ -401,13 +402,13 @@ class ValidationSession { } if (currentOptions.isNotEmpty) { - resolved[ruleName] = currentOptions; + resolvedRaw[ruleName] = currentOptions; } else { - resolved.remove(ruleName); + resolvedRaw.remove(ruleName); } } - return resolved; + return resolvedRaw.map((ruleName, params) => MapEntry(ruleName, CustomRuleOptions(params))); } @visibleForTesting diff --git a/tool/dart_skills_lint/lib/src/validator.dart b/tool/dart_skills_lint/lib/src/validator.dart index 995f0fe..bef0e5f 100644 --- a/tool/dart_skills_lint/lib/src/validator.dart +++ b/tool/dart_skills_lint/lib/src/validator.dart @@ -10,6 +10,7 @@ import 'package:yaml/yaml.dart'; import 'models/analysis_severity.dart'; import 'models/check_type.dart'; +import 'models/custom_rule_options.dart'; import 'models/skill_context.dart'; import 'models/skill_rule.dart'; import 'models/validation_error.dart'; @@ -29,7 +30,7 @@ class Validator { Validator({ Map? customRuleSeverities, List? customRules, - Map>? ruleOptions, + Map? ruleOptions, }) : _customRuleSeverities = customRuleSeverities ?? {}, _rules = _buildRules(customRuleSeverities ?? {}, customRules ?? [], ruleOptions ?? {}); static const String _skillFileName = SkillContext.skillFileName; @@ -145,7 +146,7 @@ class Validator { static List _buildRules( Map customRuleSeverities, List customRules, - Map> ruleOptions, + Map ruleOptions, ) { final rules = []; final seenNames = {}; @@ -162,7 +163,7 @@ class Validator { for (final CheckType check in RuleRegistry.allChecks) { final AnalysisSeverity severity = customRuleSeverities[check.name] ?? check.defaultSeverity; - final Map? options = ruleOptions[check.name]; + final CustomRuleOptions? options = ruleOptions[check.name]; final SkillRule? rule = RuleRegistry.createRule(check.name, severity, options); if (rule != null) { addRule(rule); diff --git a/tool/dart_skills_lint/test/config_file_test.dart b/tool/dart_skills_lint/test/config_file_test.dart index 2a94f96..536547a 100644 --- a/tool/dart_skills_lint/test/config_file_test.dart +++ b/tool/dart_skills_lint/test/config_file_test.dart @@ -425,6 +425,72 @@ dart_skills_lint: await process.shouldExit(1); }); + test('fails on unrecognized option key in YAML rule definition by default', () async { + await Directory('${tempDir.path}/test-skill').create(); + await File('${tempDir.path}/test-skill/SKILL.md').writeAsString(''' +--- +name: test-skill +description: A test skill +--- +Body'''); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + rules: + path-does-not-exist: + severity: error + invalid-option-key: value +'''); + + final TestProcess process = await TestProcess.start('dart', [ + p.normalize(p.absolute('bin/cli.dart')), + '-s', + 'test-skill', + ], workingDirectory: tempDir.path); + + final List stderr = await process.stderr.rest.toList(); + expect( + stderr.join('\n'), + contains( + 'Configuration error: Global rules: Unrecognized option "invalid-option-key" for rule "path-does-not-exist".', + ), + ); + await process.shouldExit(1); + }); + + test('fails on invalid option value type in YAML rule definition by default', () async { + await Directory('${tempDir.path}/test-skill').create(); + await File('${tempDir.path}/test-skill/SKILL.md').writeAsString(''' +--- +name: test-skill +description: A test skill +--- +Body'''); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + rules: + path-does-not-exist: + severity: error + exclude: 123 +'''); + + final TestProcess process = await TestProcess.start('dart', [ + p.normalize(p.absolute('bin/cli.dart')), + '-s', + 'test-skill', + ], workingDirectory: tempDir.path); + + final List stderr = await process.stderr.rest.toList(); + expect( + stderr.join('\n'), + contains( + 'Configuration error: Global rules: Invalid type for option "exclude" in rule "path-does-not-exist". Expected String, got "123"', + ), + ); + await process.shouldExit(1); + }); + test('succeeds with warning on invalid key when --allow-misconfigured-keys passed', () async { await Directory('${tempDir.path}/test-skill').create(); await File('${tempDir.path}/test-skill/SKILL.md').writeAsString(''' @@ -683,5 +749,66 @@ dart_skills_lint: expect(output, contains('Line 5 has 1 trailing space(s)')); await process.shouldExit(0); }); + + test('obeys map-based rule options configuration', () async { + await Directory('${tempDir.path}/skills-root').create(); + await Directory('${tempDir.path}/skills-root/definition-of-done-workspace').create(); + final Directory validSkill = await Directory( + '${tempDir.path}/skills-root/valid-skill', + ).create(); + await File( + '${validSkill.path}/SKILL.md', + ).writeAsString('---\nname: valid-skill\ndescription: Valid\n---\nBody'); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + directories: + - path: "skills-root" + rules: + path-does-not-exist: + severity: error + exclude: ".*-workspace" +'''); + + final TestProcess process = await TestProcess.start('dart', [ + p.normalize(p.absolute('bin/cli.dart')), + '-d', + 'skills-root', + ], workingDirectory: tempDir.path); + + await process.shouldExit(0); + }); + + test('preserves global rule options when target overrides only severity', () async { + await Directory('${tempDir.path}/skills-root').create(); + await Directory('${tempDir.path}/skills-root/definition-of-done-workspace').create(); + final Directory validSkill = await Directory( + '${tempDir.path}/skills-root/valid-skill', + ).create(); + await File( + '${validSkill.path}/SKILL.md', + ).writeAsString('---\nname: valid-skill\ndescription: Valid\n---\nBody'); + + await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' +dart_skills_lint: + rules: + path-does-not-exist: + severity: warning + exclude: ".*-workspace" + directories: + - path: "skills-root" + rules: + path-does-not-exist: error +'''); + + final TestProcess process = await TestProcess.start('dart', [ + p.normalize(p.absolute('bin/cli.dart')), + '-d', + 'skills-root', + ], workingDirectory: tempDir.path); + + // Exits with 0 because definition-of-done-workspace is still excluded (inherited global options) + await process.shouldExit(0); + }); }); } diff --git a/tool/dart_skills_lint/test/custom_rule_test.dart b/tool/dart_skills_lint/test/custom_rule_test.dart index b1984e4..92b93ca 100644 --- a/tool/dart_skills_lint/test/custom_rule_test.dart +++ b/tool/dart_skills_lint/test/custom_rule_test.dart @@ -52,6 +52,21 @@ class MismatchRule extends SkillRule { } } +class AlwaysFailsRule extends SkillRule { + @override + final String name = 'always-fails-rule'; + + @override + final AnalysisSeverity severity = AnalysisSeverity.error; + + @override + Future> validate(SkillContext context) async { + return [ + ValidationError(ruleId: name, severity: severity, file: 'SKILL.md', message: 'Always fails'), + ]; + } +} + void main() { group('Custom Rules', () { late Directory tempDir; diff --git a/tool/dart_skills_lint/test/resolve_options_test.dart b/tool/dart_skills_lint/test/resolve_options_test.dart new file mode 100644 index 0000000..f10ccaa --- /dev/null +++ b/tool/dart_skills_lint/test/resolve_options_test.dart @@ -0,0 +1,122 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:args/args.dart'; +import 'package:dart_skills_lint/src/entry_point.dart'; +import 'package:dart_skills_lint/src/models/analysis_severity.dart'; +import 'package:dart_skills_lint/src/models/check_type.dart'; +import 'package:dart_skills_lint/src/models/custom_rule_options.dart'; +import 'package:dart_skills_lint/src/rule_registry.dart'; +import 'package:test/test.dart'; + +void main() { + group('resolveOptionsOverrides', () { + const mockCheckName = 'mock-rule'; + late CheckType mockCheck; + + setUpAll(() { + mockCheck = const CheckType( + name: mockCheckName, + defaultSeverity: AnalysisSeverity.disabled, + help: 'Mock rule for testing.', + allowedOptions: {'exclude': String, 'max': int, 'strict': bool, 'items': List}, + ); + RuleRegistry.allChecks.add(mockCheck); + }); + + tearDownAll(() { + RuleRegistry.allChecks.remove(mockCheck); + }); + + ArgParser createParser() { + final parser = ArgParser(); + for (final CheckType check in RuleRegistry.allChecks) { + parser.addFlag(check.name, defaultsTo: check.defaultSeverity != AnalysisSeverity.disabled); + for (final String optionName in check.allowedOptions.keys) { + final Type type = check.allowedOptions[optionName]!; + if (type == List || type == List) { + parser.addMultiOption('${check.name}-$optionName'); + } else { + parser.addOption('${check.name}-$optionName'); + } + } + } + return parser; + } + + test('returns empty map when no options CLI overrides are provided', () { + final ArgResults results = createParser().parse([]); + final Map overrides = resolveRuleOptionsOverrides(results); + expect(overrides, isEmpty); + }); + + test('parses and coerces String, int, and bool options correctly', () { + final ArgResults results = createParser().parse([ + '--$mockCheckName-exclude=.*-workspace', + '--$mockCheckName-max=75', + '--$mockCheckName-strict=true', + ]); + + final Map overrides = resolveRuleOptionsOverrides(results); + expect(overrides, isNotEmpty); + expect(overrides[mockCheckName], isNotNull); + + final CustomRuleOptions mockOpts = overrides[mockCheckName]!; + expect(mockOpts['exclude'], equals('.*-workspace')); + expect(mockOpts['max'], equals(75)); + expect(mockOpts['strict'], isTrue); + }); + + test('parses and coerces List option correctly', () { + final ArgResults results = createParser().parse(['--$mockCheckName-items=a,b,c']); + + final Map overrides = resolveRuleOptionsOverrides(results); + expect(overrides, isNotEmpty); + expect(overrides[mockCheckName], isNotNull); + + final CustomRuleOptions mockOpts = overrides[mockCheckName]!; + expect(mockOpts['items'], equals(['a', 'b', 'c'])); + }); + + test('clears option (sets to null) when overridden with empty string', () { + final ArgResults results = createParser().parse(['--$mockCheckName-exclude=']); + + final Map overrides = resolveRuleOptionsOverrides(results); + expect(overrides, isNotEmpty); + expect(overrides[mockCheckName], isNotNull); + + final CustomRuleOptions mockOpts = overrides[mockCheckName]!; + expect(mockOpts.containsKey('exclude'), isTrue); + expect(mockOpts['exclude'], isNull); + }); + + test('throws FormatException when int option is passed an invalid numeric string', () { + final ArgResults results = createParser().parse(['--$mockCheckName-max=abc']); + expect( + () => resolveRuleOptionsOverrides(results), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Expected an integer'), + ), + ), + ); + }); + + test('throws FormatException when bool option is passed a non-boolean string', () { + final ArgResults results = createParser().parse(['--$mockCheckName-strict=yes']); + expect( + () => resolveRuleOptionsOverrides(results), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Expected "true" or "false"'), + ), + ), + ); + }); + }); +} diff --git a/tool/dart_skills_lint/test/rules/path_does_not_exist_rule_test.dart b/tool/dart_skills_lint/test/rules/path_does_not_exist_rule_test.dart new file mode 100644 index 0000000..a9f92a6 --- /dev/null +++ b/tool/dart_skills_lint/test/rules/path_does_not_exist_rule_test.dart @@ -0,0 +1,141 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; + +import 'package:dart_skills_lint/src/models/analysis_severity.dart'; +import 'package:dart_skills_lint/src/models/custom_rule_options.dart'; +import 'package:dart_skills_lint/src/models/skill_context.dart'; +import 'package:dart_skills_lint/src/models/validation_error.dart'; +import 'package:dart_skills_lint/src/rules/path_does_not_exist_rule.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + group('PathDoesNotExistRule', () { + late Directory tempDir; + + setUp(() async { + tempDir = await Directory.systemTemp.createTemp('path_does_not_exist_test.'); + }); + + tearDown(() async { + if (tempDir.existsSync()) { + await tempDir.delete(recursive: true); + } + }); + + test('passes when directory exists and contains SKILL.md', () async { + final skillDir = Directory(p.join(tempDir.path, 'valid-skill')); + await skillDir.create(); + await File(p.join(skillDir.path, 'SKILL.md')).writeAsString('name: valid-skill'); + + final rule = PathDoesNotExistRule(severity: AnalysisSeverity.error); + final context = SkillContext(directory: skillDir, rawContent: 'name: valid-skill'); + + final List errors = await rule.validate(context); + expect(errors, isEmpty); + }); + + test('flags when SKILL.md is missing', () async { + final skillDir = Directory(p.join(tempDir.path, 'missing-skill-md')); + await skillDir.create(); + + final rule = PathDoesNotExistRule(severity: AnalysisSeverity.error); + final context = SkillContext(directory: skillDir, rawContent: ''); + + final List errors = await rule.validate(context); + expect(errors, isNotEmpty); + expect(errors.first.ruleId, equals(PathDoesNotExistRule.ruleName)); + expect(errors.first.message, contains('SKILL.md is missing')); + }); + + test('flags when directory does not exist', () async { + final skillDir = Directory(p.join(tempDir.path, 'non-existent')); + + final rule = PathDoesNotExistRule(severity: AnalysisSeverity.error); + final context = SkillContext(directory: skillDir, rawContent: ''); + + final List errors = await rule.validate(context); + expect(errors, isNotEmpty); + expect(errors.first.ruleId, equals(PathDoesNotExistRule.ruleName)); + expect(errors.first.message, contains('Directory does not exist')); + }); + + test('flags when path is a file instead of a directory', () async { + final skillDirAsFile = File(p.join(tempDir.path, 'is-a-file')); + await skillDirAsFile.create(); + + final rule = PathDoesNotExistRule(severity: AnalysisSeverity.error); + final context = SkillContext(directory: Directory(skillDirAsFile.path), rawContent: ''); + + final List errors = await rule.validate(context); + expect(errors, isNotEmpty); + expect(errors.first.ruleId, equals(PathDoesNotExistRule.ruleName)); + expect(errors.first.message, contains('Path is not a directory')); + }); + + test('bypasses validation when directory name matches exclude RegExp', () async { + final skillDir = Directory(p.join(tempDir.path, 'test-workspace')); + await skillDir.create(); // missing SKILL.md + + final rule = PathDoesNotExistRule( + severity: AnalysisSeverity.error, + customRuleOptions: const CustomRuleOptions({'exclude': '.*-workspace'}), + ); + final context = SkillContext(directory: skillDir, rawContent: ''); + + final List errors = await rule.validate(context); + expect(errors, isEmpty); + }); + + test('bypasses validation when directory name matches alternation RegExp', () async { + final skillDir1 = Directory(p.join(tempDir.path, 'test-workspace')); + final skillDir2 = Directory(p.join(tempDir.path, 'evals')); + await skillDir1.create(); + await skillDir2.create(); + + final rule = PathDoesNotExistRule( + severity: AnalysisSeverity.error, + customRuleOptions: const CustomRuleOptions({'exclude': '.*-workspace|evals'}), + ); + + final context1 = SkillContext(directory: skillDir1, rawContent: ''); + final context2 = SkillContext(directory: skillDir2, rawContent: ''); + + expect(await rule.validate(context1), isEmpty); + expect(await rule.validate(context2), isEmpty); + }); + + test('validates options keys and throws ArgumentError on unknown keys', () { + expect( + () => PathDoesNotExistRule( + severity: AnalysisSeverity.error, + customRuleOptions: const CustomRuleOptions({'unknown_option': 'val'}), + ), + throwsArgumentError, + ); + }); + + test('validates options types and throws ArgumentError on type mismatch', () { + expect( + () => PathDoesNotExistRule( + severity: AnalysisSeverity.error, + customRuleOptions: const CustomRuleOptions({'exclude': 123}), + ), + throwsArgumentError, + ); + }); + + test('throws FormatException on invalid regex option', () { + expect( + () => PathDoesNotExistRule( + severity: AnalysisSeverity.error, + customRuleOptions: const CustomRuleOptions({'exclude': '[invalid(regex'}), + ), + throwsFormatException, + ); + }); + }); +} From 292c213c66b588edc4f4af36c743c1e47624246c Mon Sep 17 00:00:00 2001 From: Reid Baker <1063596+reidbaker@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:15:45 -0400 Subject: [PATCH 6/9] Update .gitignore --- tool/dart_skills_lint/.agents/skills/.gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tool/dart_skills_lint/.agents/skills/.gitignore b/tool/dart_skills_lint/.agents/skills/.gitignore index 1471a06..466a859 100644 --- a/tool/dart_skills_lint/.agents/skills/.gitignore +++ b/tool/dart_skills_lint/.agents/skills/.gitignore @@ -6,10 +6,6 @@ !dart-skills-lint-integration/ !definition-of-done/ -# Keep evals inside un-ignored skill folders -!**/evals/ -!**/evals/** - # Keep essential configuration and docs !.gitignore !README.md From b6e5e901538aad364585433cf38475f5ff4d3292 Mon Sep 17 00:00:00 2001 From: Reid Baker <1063596+reidbaker@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:16:25 -0400 Subject: [PATCH 7/9] Update ignore.json --- tool/dart_skills_lint/.agents/skills/ignore.json | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tool/dart_skills_lint/.agents/skills/ignore.json b/tool/dart_skills_lint/.agents/skills/ignore.json index 07cac70..e1114b9 100644 --- a/tool/dart_skills_lint/.agents/skills/ignore.json +++ b/tool/dart_skills_lint/.agents/skills/ignore.json @@ -1,10 +1,3 @@ { - "skills": { - "definition-of-done-workspace": [ - { - "rule_id": "path-does-not-exist", - "file_name": ".agents/skills/definition-of-done-workspace" - } - ] - } -} \ No newline at end of file + "skills": {} +} From 7a5636f3afeeda4c5ffa2f75d7f1da17f850cf2c Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 13 Jul 2026 22:19:13 -0400 Subject: [PATCH 8/9] feat: configure path-does-not-exist exclude pattern in dart_skills_lint.yaml --- tool/dart_skills_lint/dart_skills_lint.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tool/dart_skills_lint/dart_skills_lint.yaml b/tool/dart_skills_lint/dart_skills_lint.yaml index 31191a8..62c9a5e 100644 --- a/tool/dart_skills_lint/dart_skills_lint.yaml +++ b/tool/dart_skills_lint/dart_skills_lint.yaml @@ -6,6 +6,9 @@ dart_skills_lint: - path: ".agents/skills" rules: check-trailing-whitespace: error + path-does-not-exist: + severity: error + exclude: ".*-workspace" ignore_file: ".agents/skills/ignore.json" - path: "../../skills" ignore_file: ".agents/skills/flutter_skills_ignore.json" From fae2228947760f835b51e115c1dbff6dbbb0f66e Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Mon, 13 Jul 2026 22:24:17 -0400 Subject: [PATCH 9/9] refactor: introduce type-safe getters in CustomRuleOptions and rename to optionsSchema - Add type-safe getters (getString, getInt, getBool, getStringList) to CustomRuleOptions. - Update PathDoesNotExistRule to retrieve options using the new type-safe getters. - Rename allowedOptions to optionsSchema inside CheckType to distinguish the schema definition from configuration values. - Clean up all references across EntryPoint, Registry, and tests to match optionsSchema. --- .../dart_skills_lint/lib/src/entry_point.dart | 10 +++--- .../lib/src/models/check_type.dart | 10 +++--- .../lib/src/models/custom_rule_options.dart | 35 +++++++++++++++++++ .../lib/src/rule_registry.dart | 2 +- .../src/rules/path_does_not_exist_rule.dart | 8 ++--- .../test/resolve_options_test.dart | 6 ++-- 6 files changed, 52 insertions(+), 19 deletions(-) diff --git a/tool/dart_skills_lint/lib/src/entry_point.dart b/tool/dart_skills_lint/lib/src/entry_point.dart index 5d6573c..87dd442 100644 --- a/tool/dart_skills_lint/lib/src/entry_point.dart +++ b/tool/dart_skills_lint/lib/src/entry_point.dart @@ -182,8 +182,8 @@ ArgParser _createArgParser(String helpFlag) { ); // Register namespaced delegated options - for (final String optionName in check.allowedOptions.keys) { - final Type expectedType = check.allowedOptions[optionName]!; + for (final String optionName in check.optionsSchema.keys) { + final Type expectedType = check.optionsSchema[optionName]!; if (expectedType == List || expectedType == List) { parser.addMultiOption( '${check.name}-$optionName', @@ -485,7 +485,7 @@ Map resolveRuleSeverities(ArgResults results) { /// flags matches in the format `--[rule-name]-[option-name]` (e.g. `--path-does-not-exist-exclude`), /// and returns a structured map mapping rule names to their custom key-value parameter overrides. /// -/// Applies type coercion based on the expected type defined in [CheckType.allowedOptions]: +/// Applies type coercion based on the expected type defined in [CheckType.optionsSchema]: /// * [int]: Coerced using [int.tryParse]. /// * [bool]: Coerced to `true` if matching "true" case-insensitively, `false` otherwise. /// * Other: Parsed as-is (e.g. [String] or [List] types). @@ -496,10 +496,10 @@ Map resolveRuleOptionsOverrides(ArgResults results) { for (final CheckType check in RuleRegistry.allChecks) { final Map checkOverrides = {}; - for (final String optionName in check.allowedOptions.keys) { + for (final String optionName in check.optionsSchema.keys) { final optionFlag = '${check.name}-$optionName'; if (results.wasParsed(optionFlag)) { - final Type expectedType = check.allowedOptions[optionName]!; + final Type expectedType = check.optionsSchema[optionName]!; final Object? rawValue = results[optionFlag]; if (rawValue == '') { diff --git a/tool/dart_skills_lint/lib/src/models/check_type.dart b/tool/dart_skills_lint/lib/src/models/check_type.dart index fac13d0..3f7fecc 100644 --- a/tool/dart_skills_lint/lib/src/models/check_type.dart +++ b/tool/dart_skills_lint/lib/src/models/check_type.dart @@ -11,7 +11,7 @@ class CheckType { required this.name, required this.defaultSeverity, required this.help, - this.allowedOptions = const {}, + this.optionsSchema = const {}, }); final String name; @@ -22,19 +22,19 @@ class CheckType { final String help; /// Custom configuration options supported by this check. - final Map allowedOptions; + final Map optionsSchema; - /// Validates the given [options] against this check's [allowedOptions] schema. + /// Validates the given [options] against this check's [optionsSchema] schema. /// /// Returns a list of error messages for any unrecognized options or type mismatches. List validateOptions(CustomRuleOptions options) { final List errors = []; for (final String key in options.keys) { - if (!allowedOptions.containsKey(key)) { + if (!optionsSchema.containsKey(key)) { errors.add('Unrecognized option "$key" for rule "$name".'); continue; } - final Type expectedType = allowedOptions[key]!; + final Type expectedType = optionsSchema[key]!; final Object? actualValue = options[key]; if (actualValue != null && !_isTypeValid(actualValue, expectedType)) { errors.add( diff --git a/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart b/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart index ec1251f..ca7097b 100644 --- a/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart +++ b/tool/dart_skills_lint/lib/src/models/custom_rule_options.dart @@ -26,4 +26,39 @@ class CustomRuleOptions { /// Whether the configuration contains the specified [key]. bool containsKey(String key) => params.containsKey(key); + + /// Retrieves the value of the parameter associated with [key] as a [String]. + /// + /// Returns `null` if the value is missing or not a [String]. + String? getString(String key) { + final Object? val = params[key]; + return val is String ? val : null; + } + + /// Retrieves the value of the parameter associated with [key] as an [int]. + /// + /// Returns `null` if the value is missing or not an [int]. + int? getInt(String key) { + final Object? val = params[key]; + return val is int ? val : null; + } + + /// Retrieves the value of the parameter associated with [key] as a [bool]. + /// + /// Returns `null` if the value is missing or not a [bool]. + bool? getBool(String key) { + final Object? val = params[key]; + return val is bool ? val : null; + } + + /// Retrieves the value of the parameter associated with [key] as a [List] of [String]s. + /// + /// Returns `null` if the value is missing or not a [List]. + List? getStringList(String key) { + final Object? val = params[key]; + if (val is List) { + return val.map((e) => e.toString()).toList(); + } + return null; + } } diff --git a/tool/dart_skills_lint/lib/src/rule_registry.dart b/tool/dart_skills_lint/lib/src/rule_registry.dart index d39a24d..f9590c3 100644 --- a/tool/dart_skills_lint/lib/src/rule_registry.dart +++ b/tool/dart_skills_lint/lib/src/rule_registry.dart @@ -25,7 +25,7 @@ class RuleRegistry { name: PathDoesNotExistRule.ruleName, defaultSeverity: AnalysisSeverity.error, help: 'Check if SKILL.md and directory structure are correct.', - allowedOptions: {'exclude': String}, + optionsSchema: {'exclude': String}, ), const CheckType( name: AbsolutePathsRule.ruleName, diff --git a/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart b/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart index 1f7d41b..e6fed96 100644 --- a/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart +++ b/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart @@ -19,11 +19,9 @@ import '../models/validation_error.dart'; class PathDoesNotExistRule extends ConfigurableSkillRule { PathDoesNotExistRule({required this.severity, CustomRuleOptions? customRuleOptions}) : super(customRuleOptions) { - if (customRuleOptions != null) { - final Object? excludeVal = customRuleOptions[excludeOption]; - if (excludeVal is String && excludeVal.isNotEmpty) { - _excludeRegExp = RegExp(excludeVal); - } + final String? excludeVal = customRuleOptions?.getString(excludeOption); + if (excludeVal != null && excludeVal.isNotEmpty) { + _excludeRegExp = RegExp(excludeVal); } } diff --git a/tool/dart_skills_lint/test/resolve_options_test.dart b/tool/dart_skills_lint/test/resolve_options_test.dart index f10ccaa..fe7a278 100644 --- a/tool/dart_skills_lint/test/resolve_options_test.dart +++ b/tool/dart_skills_lint/test/resolve_options_test.dart @@ -20,7 +20,7 @@ void main() { name: mockCheckName, defaultSeverity: AnalysisSeverity.disabled, help: 'Mock rule for testing.', - allowedOptions: {'exclude': String, 'max': int, 'strict': bool, 'items': List}, + optionsSchema: {'exclude': String, 'max': int, 'strict': bool, 'items': List}, ); RuleRegistry.allChecks.add(mockCheck); }); @@ -33,8 +33,8 @@ void main() { final parser = ArgParser(); for (final CheckType check in RuleRegistry.allChecks) { parser.addFlag(check.name, defaultsTo: check.defaultSeverity != AnalysisSeverity.disabled); - for (final String optionName in check.allowedOptions.keys) { - final Type type = check.allowedOptions[optionName]!; + for (final String optionName in check.optionsSchema.keys) { + final Type type = check.optionsSchema[optionName]!; if (type == List || type == List) { parser.addMultiOption('${check.name}-$optionName'); } else {