diff --git a/.agents/agents/reidbaker-agent/README.md b/.agents/agents/reidbaker-agent/README.md index 6e29e763..32e9b6e6 100644 --- a/.agents/agents/reidbaker-agent/README.md +++ b/.agents/agents/reidbaker-agent/README.md @@ -2,4 +2,5 @@ The skills located in `skills/` within this agent directory are locally maintained custom skills. -For external agent skills used during development of `dart_skills_lint`, see [`tool/dart_skills_lint/.agents/skills/README.md`](../../../tool/dart_skills_lint/.agents/skills/README.md). +For external agent skills used during development of `skills_lint`, see the [`google/skills_lint.dart`](https://github.com/google/skills_lint.dart) repository. + diff --git a/.github/workflows/dart_skills_lint_release.yaml b/.github/workflows/dart_skills_lint_release.yaml deleted file mode 100644 index f97c2ff1..00000000 --- a/.github/workflows/dart_skills_lint_release.yaml +++ /dev/null @@ -1,168 +0,0 @@ -name: dart_skills_lint release -permissions: - contents: read - -on: - push: - tags: - - 'dart_skills_lint-v*' - -jobs: - build_binaries: - strategy: - fail-fast: false - matrix: - include: - - os: macos-14 - target: macos-arm64 - - os: macos-15-intel - target: macos-x64 - - os: ubuntu-22.04 - target: linux-x64 - - os: ubuntu-22.04-arm - target: linux-arm64 - runs-on: ${{ matrix.os }} - defaults: - run: - working-directory: tool/dart_skills_lint - shell: bash - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 - with: - sdk: stable - - - name: Resolve dependencies - run: dart pub get - - - name: Compile native binary - run: | - set -euo pipefail - mkdir -p dist - dart compile exe bin/cli.dart -o "dist/dart_skills_lint-${TARGET}" - chmod +x "dist/dart_skills_lint-${TARGET}" - env: - TARGET: ${{ matrix.target }} - - - name: Smoke-test binary - run: ./dist/dart_skills_lint-${{ matrix.target }} --help - - - name: Package and hash - run: | - set -euo pipefail - cd dist - tar -czf "dart_skills_lint-${TARGET}.tar.gz" "dart_skills_lint-${TARGET}" - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "dart_skills_lint-${TARGET}.tar.gz" > "dart_skills_lint-${TARGET}.tar.gz.sha256" - else - shasum -a 256 "dart_skills_lint-${TARGET}.tar.gz" > "dart_skills_lint-${TARGET}.tar.gz.sha256" - fi - env: - TARGET: ${{ matrix.target }} - - - name: Upload binary artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: dart_skills_lint-${{ matrix.target }} - path: | - tool/dart_skills_lint/dist/dart_skills_lint-${{ matrix.target }}.tar.gz - tool/dart_skills_lint/dist/dart_skills_lint-${{ matrix.target }}.tar.gz.sha256 - if-no-files-found: error - retention-days: 7 - - release: - needs: build_binaries - runs-on: ubuntu-latest - permissions: - contents: write - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Download all binary artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: dist - merge-multiple: true - - - name: Build SHA256SUMS aggregate file - working-directory: dist - run: | - set -euo pipefail - : > SHA256SUMS - for f in *.tar.gz.sha256; do - cat "$f" >> SHA256SUMS - done - rm -f *.tar.gz.sha256 - echo "--- SHA256SUMS ---" - cat SHA256SUMS - - - name: Extract release metadata - id: meta - run: | - set -euo pipefail - # Tag pattern: dart_skills_lint-v. Strip prefix to get the version. - TAG="${GITHUB_REF_NAME}" - VERSION="${TAG#dart_skills_lint-v}" - if [ "$VERSION" = "$TAG" ]; then - echo "ERROR: tag '$TAG' does not match expected pattern 'dart_skills_lint-v'" >&2 - exit 1 - fi - # Verify pubspec matches. - PUBSPEC_VERSION=$(awk -F': *' '/^version:/ {print $2; exit}' tool/dart_skills_lint/pubspec.yaml) - if [ "$VERSION" != "$PUBSPEC_VERSION" ]; then - echo "ERROR: tag version '$VERSION' does not match pubspec.yaml version '$PUBSPEC_VERSION'" >&2 - exit 1 - fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - # Pre-release detection for the GitHub Release flag. - case "$VERSION" in - *-dev.*|*-alpha.*|*-beta.*|*-rc.*|*-preview.*) echo "prerelease=true" >> "$GITHUB_OUTPUT" ;; - *) echo "prerelease=false" >> "$GITHUB_OUTPUT" ;; - esac - - - name: Extract release notes from CHANGELOG - run: | - set -euo pipefail - awk -v ver="## ${VERSION}" ' - $0 == ver { found = 1; next } - /^## / && found { exit } - found { print } - ' tool/dart_skills_lint/CHANGELOG.md > release-notes.md - if [ ! -s release-notes.md ]; then - echo "ERROR: no CHANGELOG entry found for version '${VERSION}'" >&2 - echo "Expected a heading line: ## ${VERSION}" >&2 - exit 1 - fi - echo "--- release notes ---" - cat release-notes.md - env: - VERSION: ${{ steps.meta.outputs.version }} - - - name: Create GitHub Release - run: | - set -euo pipefail - - PRERELEASE_FLAG="" - if [ "${STEPS_META_OUTPUTS_PRERELEASE}" = "true" ]; then - PRERELEASE_FLAG="--prerelease" - fi - - gh release create "${GITHUB_REF_NAME}" \ - --title "dart_skills_lint v${STEPS_META_OUTPUTS_VERSION}" \ - --notes-file release-notes.md \ - $PRERELEASE_FLAG \ - dist/*.tar.gz \ - dist/SHA256SUMS \ - tool/dart_skills_lint/scripts/install.sh - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - STEPS_META_OUTPUTS_PRERELEASE: ${{ steps.meta.outputs.prerelease }} - STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }} diff --git a/.github/workflows/dart_skills_lint_workflow.yaml b/.github/workflows/dart_skills_lint_workflow.yaml deleted file mode 100644 index 1187d5c1..00000000 --- a/.github/workflows/dart_skills_lint_workflow.yaml +++ /dev/null @@ -1,116 +0,0 @@ -name: dart_skills_lint -permissions: - contents: read - -on: - pull_request: - paths: - - 'skills/**' - - 'tool/dart_skills_lint/**' - - '.github/workflows/dart_skills_lint_workflow.yaml' - push: - branches: [ main ] - paths: - - 'skills/**' - - 'tool/dart_skills_lint/**' - - '.github/workflows/dart_skills_lint_workflow.yaml' - schedule: - - cron: '0 0 * * 0' # weekly - -defaults: - run: - working-directory: tool/dart_skills_lint - -jobs: - analyze_and_test: - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 - with: - sdk: stable - - - run: dart pub get - - - run: dart analyze --fatal-infos - - - name: Run cognitive complexity check - run: dart run cognitive_complexity --fail-threshold 20 tool/dart_skills_lint/lib tool/dart_skills_lint/test - - - run: dart test - - - name: Verify API boundary runner example - run: | - cd example/api_boundary_runner - dart pub get - dart format --output=none --set-exit-if-changed . - dart analyze --fatal-infos . - dart run bin/main.dart - - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 - with: - sdk: stable - - - run: dart pub get - - - name: Collect coverage - run: dart test --coverage=coverage - - - name: Format coverage to LCOV - run: dart run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib - - # Action steps do not inherit defaults.run.working-directory, so this - # path is relative to the repository root. - - name: Enforce coverage threshold - uses: VeryGoodOpenSource/very_good_coverage@c953fca3e24a915e111cc6f55f03f756dcb3964c # v3 # zizmor: ignore[archived-uses] - with: - path: tool/dart_skills_lint/coverage/lcov.info - min_coverage: 73 - exclude: '**/*.g.dart' - - formatting: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 - with: - sdk: stable - - - run: dart pub get - - - run: dart format --output=none --set-exit-if-changed . - - pana_score: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 - with: - sdk: stable - - - run: dart pub get - - - name: Install pana - run: dart pub global activate pana - - # pana --exit-code-threshold N fails the step when - # (max - granted) > N. Threshold 0 means any point drop fails; - # current package score is 160/160 and we want to keep it there. - - name: Pana score gate (160/160 required) - run: dart pub global run pana --no-warning --exit-code-threshold 0 . diff --git a/.github/workflows/skills_tool.yaml b/.github/workflows/skills_tool.yaml index 182fa306..0381b617 100644 --- a/.github/workflows/skills_tool.yaml +++ b/.github/workflows/skills_tool.yaml @@ -7,11 +7,17 @@ on: pull_request: paths: - '.github/workflows/skills_tool.yaml' + - 'pubspec.yaml' + - 'resources/**' + - 'skills/**' - 'tool/generator/**' push: branches: [ main ] paths: - '.github/workflows/skills_tool.yaml' + - 'pubspec.yaml' + - 'resources/**' + - 'skills/**' - 'tool/generator/**' schedule: - cron: '0 0 * * 0' # weekly diff --git a/pubspec.yaml b/pubspec.yaml index 6f5f1e3f..f8c23f60 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,4 +6,3 @@ environment: workspace: - tool/generator - - tool/dart_skills_lint diff --git a/tool/dart_skills_lint/.agent.md b/tool/dart_skills_lint/.agent.md deleted file mode 100644 index a8b2c8ab..00000000 --- a/tool/dart_skills_lint/.agent.md +++ /dev/null @@ -1,20 +0,0 @@ -# Environment Config - -This file contains environment paths and instructions for AI agents working in this repository. - -## Dart SDK Path - -When running `dart` commands in a persistent terminal where interactive shell configs are not loaded, ensure your Dart SDK is on `PATH`: -```bash -export PATH="/path/to/dart-sdk/bin:$PATH" -dart run bin/cli.dart -``` - -## Coding Standards - -When modifying code in this repository, follow these guidelines: - -- **Extract String Literals**: Always extract string literals to top-level constants (especially CLI flags, options, keys, and log messages), rather than hardcoding them inline. -- **Group Constants by Concept**: Organize constants by usage type (e.g., CLI Flags, Messages) and separate different concepts with a newline. -- **Keep Documentation Synchronized**: When modifying CLI flags, outputs, or configuration parsing, always check if sibling documentation (e.g., `README.md`, `SPECIFICATION.md`) needs to be updated. Ensure that documented behavior always matches the runtime implementation. -- **Windows Compatibility**: Always consider Windows when writing code and tests. Never hardcode path separators (always use `package:path`'s `p.join`). When testing file operations that depend on OS-specific commands (like permissions), provide a Windows equivalent (e.g., `icacls`) or use appropriate mock libraries. diff --git a/tool/dart_skills_lint/.agents/skills/.gitignore b/tool/dart_skills_lint/.agents/skills/.gitignore deleted file mode 100644 index 2e208c84..00000000 --- a/tool/dart_skills_lint/.agents/skills/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# Ignore everything by default right in this directory -/* -*.log - -# Un-ignore specific checked-in skills -!contributor-pr-description/ -!add-dart-lint-validation-rule/ -!check-downstream-consumers/ -!dart-skills-lint-integration/ -!definition-of-done/ -!run-evals/ - -# Keep essential configuration and docs -!.gitignore -!README.md -!ignore.json -!flutter_skills_ignore.json diff --git a/tool/dart_skills_lint/.agents/skills/README.md b/tool/dart_skills_lint/.agents/skills/README.md deleted file mode 100644 index f3102fff..00000000 --- a/tool/dart_skills_lint/.agents/skills/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Dart Skills Lint - Agent Skills - -This directory (`tool/dart_skills_lint/.agents/skills/`) contains skills and configurations for agents working on the dart_skills_lint package. - -## Setup Instructions - -To set up this directory for development, you must install the remote skills using the Dart `skills` package (`dart install skills@^1.0.0`). These include general Dart development practices, testing fundamentals, and productivity tools that agents rely on. - -Run the following commands from the `tool/dart_skills_lint/` directory to fetch the dependencies: - -```sh -# Ensure the skills CLI is globally installed (version 1.0.0 or higher) -dart install skills@^1.0.0 - -# Core Dart Skills -skills add kevmoo/dash_skills \ - --skill dart-best-practices \ - --skill dart-doc-validation \ - --skill dart-long-lines \ - --skill dart-matcher-best-practices \ - --skill dart-modern-features \ - --skill dart-package-maintenance \ - --skill dart-test-coverage \ - --skill dart-test-fundamentals \ - --agent generic - -skills add dart-lang/skills \ - --skill dart-migrate-to-checks-package \ - --skill dart-build-cli-app \ - --skill dart-collect-coverage \ - --skill dart-add-unit-test \ - --skill dart-use-pattern-matching \ - --agent generic - -# Productivity and Workflows -skills add mattpocock/skills --skill grill-me --agent generic -skills add obra/superpowers --skill test-driven-development --agent generic -skills add anthropics/skills --skill skill-creator --agent generic -``` - -## Overview and Philosophy - -* **Remote Dependencies:** We prefer to leverage community-maintained skills from upstream repositories rather than duplicating them locally. This ensures we stay aligned with the broader ecosystem's best practices. -* **Internal Skills:** We maintain a few local skills directly in this repository (e.g., `add-dart-lint-validation-rule` and `dart-skills-lint-integration`). These are explicitly marked with `internal: true` in their frontmatter to prevent them from being accidentally published to the global registry, as they are specific to our local tools. - -## Contributing and Maintenance - -When adding new external skills to this directory, follow these guidelines: - -1. Use `skills add --skill --agent generic` 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 configuration file located at `.config/dart_skills/skills_config.json` 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 - skills add anthropics/skills --skill skill-creator --agent generic - ``` -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/add-dart-lint-validation-rule/SKILL.md b/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/SKILL.md deleted file mode 100644 index 208f9afb..00000000 --- a/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/SKILL.md +++ /dev/null @@ -1,206 +0,0 @@ ---- -name: add-dart-lint-validation-rule -description: > - Instructions for adding a new validation rule and CLI flag to dart_skills_lint. - Use this skill when asked to create a new rule that validates aspects of skills - (like frontmatter metadata). -metadata: - internal: true ---- - -# Add a New Validation Rule and Flag - -Use this skill when you need to add a new validation rule to the `dart_skills_lint` package, expose it as a toggleable CLI flag, and verify its behavior. - ---- - -## ๐Ÿ› ๏ธ Step-by-Step Implementation - -### 1. Create the Rule Class -Create a new file in `lib/src/rules/` extending `SkillRule`. - -> [!TIP] -> If your rule expects a specific structure in the skill's YAML frontmatter (e.g., inside `metadata`), document this structure clearly in the class Dart docstring. - -```dart -// lib/src/rules/my_new_rule.dart - -import '../models/analysis_severity.dart'; -import '../models/skill_context.dart'; -import '../models/skill_rule.dart'; -import '../models/validation_error.dart'; - -class MyNewRule extends SkillRule { - MyNewRule({super.severity}); - - @override - Future> validate(SkillContext context) async { - final errors = []; - // Add validation logic here using context.rawContent or context.directory - return errors; - } -} -``` - -#### Accessing YAML Frontmatter -If your rule needs configuration from the skill's YAML frontmatter, you can access it via `context.parsedYaml`. - -```dart - @override - Future> validate(SkillContext context) async { - final errors = []; - final yaml = context.parsedYaml; - if (yaml != null) { - final metadata = yaml['metadata']; - if (metadata is Map) { - // Read your custom config here - } - } - return errors; - } -``` - -### 2. Register the Rule in `lib/src/rule_registry.dart` - -Add a new `CheckType` instance to `RuleRegistry.allChecks` list. This automatically exposes it as a CLI flag. - -```dart -// lib/src/rule_registry.dart in allChecks list - - const CheckType( - name: MyNewRule.ruleName, - defaultSeverity: MyNewRule.defaultSeverity, - help: 'Description of what the rule does for CLI help.', - ), -``` - -Then, add a case to `RuleRegistry.createRule` to instantiate your rule: - -```dart -// lib/src/rule_registry.dart in createRule method - - static SkillRule? createRule(String name, AnalysisSeverity severity) { - switch (name) { - // ... other rules - case MyNewRule.ruleName: - return MyNewRule(severity: severity); - default: - return null; - } - } -``` - -### 3. Handle Disabled by Default Rules (If applicable) -If the rule is disabled by default (`defaultSeverity: AnalysisSeverity.disabled`), passing the flag `--check-my-new-rule` will automatically enable it with `AnalysisSeverity.error` severity (handled in `entry_point.dart`). - ---- - -## ๐Ÿงช Testing the New Rule - -You must write automated tests verifying your rule triggers when it should and skips when it shouldn't. - -### Preferred Approach: In-Memory Unit Tests -Instead of writing files to disk, test the rule directly using a mock `SkillContext`. This is faster and avoids I/O dependencies. - -```dart -// test/my_new_rule_test.dart - -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_error.dart'; -import 'package:dart_skills_lint/src/rules/my_new_rule.dart'; -import 'package:test/test.dart'; - -void main() { - group('MyNewRule', () { - test('flags invalid content', () async { - final rule = MyNewRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: 'Invalid content', - ); - - final List errors = await rule.validate(context); - - expect(errors, isNotEmpty); - expect(errors.first.message, contains('Expected error message')); - }); - - test('passes valid content', () async { - final rule = MyNewRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: 'Valid content', - ); - - final List errors = await rule.validate(context); - - expect(errors, isEmpty); - }); - }); -} -``` - -### Alternative Approach: File System Interaction -If the rule interacts with the file system or wraps an external CLI tool (like `popmark`), you should use a temporary directory for testing instead of in-memory mocks. - -```dart - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('my_rule_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('flags invalid file content', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString('Invalid content'); - - final rule = MyNewRule(severity: AnalysisSeverity.warning); - final context = SkillContext(directory: skillDir, rawContent: 'Invalid content'); - - final List errors = await rule.validate(context); - - expect(errors, isNotEmpty); - }); -``` - -### Integration Tests -If the rule interacts with CLI flags or configuration files, add a test in `test/cli_integration_test.dart` using `TestProcess`. -> [!IMPORTANT] -> When writing integration tests that use config files and `TestProcess`, ensure that paths in the config file and paths passed to the CLI match in style (both relative or both absolute) to avoid issues with path matching in `entry_point.dart`. - ---- - -## ๐Ÿ“š Documentation Updates - -When a new rule is introduced, verify that you synchronize sibling markdown files! - -1. **`README.md`:** - * Add your flag under the **Flags** section (under **Usage**) so users know it exists. - * **CRITICAL FORMATTING:** You MUST use the exact format `- \`--[no-]\`: . (Disabled by default if applicable)`. - * **CRITICAL NAMING:** Ensure the flag string matches the `ruleName` EXACTLY. For example, if the `ruleName` is `file-existence`, the flag MUST be documented as `--[no-]file-existence` (do NOT hallucinate a `check-` prefix like `--[no-]check-file-existence`). Do NOT add empty bullet points. -2. **`RULES.md`:** - * Add a new entry for your rule documenting its default severity, fixability, what it checks, diagnostic shape, auto-fix behavior, and how to disable it. This is strictly required by the `rules_md_consistency_test.dart` test. -3. **`documentation/knowledge/SPECIFICATION.md`:** - * Document the formal constraint in the specification if it defines a standard for skill files. - ---- - -## ๐Ÿšฆ Checklist Before Submitting PR - -- [ ] Rule class created in `lib/src/rules/`. -- [ ] Rule registered in `lib/src/rule_registry.dart`. -- [ ] Unit tests added in `test/` using in-memory `SkillContext`. -- [ ] **CRITICAL**: Usage flag correctly documented in `README.md` under Flags (ensure flag string matches `ruleName` EXACTLY and format is correct). -- [ ] Rule documented in `RULES.md`. -- [ ] Schema documented in `documentation/knowledge/SPECIFICATION.md` (if applicable). -- [ ] Run `dart format .` to format code. -- [ ] Run `dart analyze --fatal-infos` to ensure no issues. -- [ ] Run `dart test` to ensure tests passing. diff --git a/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/evals/evals.json b/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/evals/evals.json deleted file mode 100644 index 8039b0a9..00000000 --- a/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/evals/evals.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "repo_criteria": [ - "evals/code_quality_rubric.json" - ], - "evals": [ - { - "id": 1, - "prompt": "Author a custom dart_skills_lint rule named `RequireSpecificMetadataRule` that verifies if a skill's metadata contains a `required_version` field. Wire it up into a new test file `test/require_specific_metadata_rule_test.dart` using an in-memory SkillContext.", - "expected_chat_output": [ - "The agent identifies the skill, spawns a with-skill subagent, and successfully implements the rule." - ], - "expected_repo_state": [ - "The agent successfully authored `RequireSpecificMetadataRule` extending `SkillRule` in `lib/src/rules/require_specific_metadata_rule.dart`.", - "The rule was properly added to `RuleRegistry.allChecks` and the `createRule` switch statement in `lib/src/rule_registry.dart`.", - "The `validate` method correctly parses `context.parsedYaml` to check for `required_version` in the metadata.", - "The agent authored unit tests utilizing an in-memory raw string, `loadYaml`, and a mock `SkillContext` in `test/require_specific_metadata_rule_test.dart`.", - "The toggleable flag `--[no-]require-specific-metadata` was correctly documented in `README.md`.", - "A proper rule documentation entry was added in `RULES.md`, including severity, fixability, and diagnostic shape.", - "The agent proactively documented the rule constraints in `SPECIFICATION.md`." - ], - "agent_config": "reidbaker-agent" - }, - { - "id": 2, - "prompt": "Create a new rule `FileExistenceRule` that ensures a specific supplementary file `info.txt` exists alongside `SKILL.md`. Make sure this rule is disabled by default. Write tests using the temporary directory approach.", - "expected_chat_output": [ - "The agent creates the rule and physical directory tests." - ], - "expected_repo_state": [ - "The `FileExistenceRule` class was successfully generated extending `SkillRule` in `lib/src/rules/file_existence_rule.dart`.", - "Included correctly in `RuleRegistry.allChecks` as `disabled` in `lib/src/rule_registry.dart`.", - "The agent successfully utilized `Directory.systemTemp.createTemp` and `File.writeAsString` for integration testing in `test/rules/file_existence_rule_test.dart`.", - "The rule flag `--[no-]file-existence` was documented in `README.md`.", - "A proper rule documentation entry was added outlining the diagnostics and behavior in `RULES.md`.", - "Temporary files were safely cleaned up via `tempDir.delete(recursive: true)` in `tearDown`." - ], - "agent_config": "reidbaker-agent" - } - ] -} diff --git a/tool/dart_skills_lint/.agents/skills/check-downstream-consumers/SKILL.md b/tool/dart_skills_lint/.agents/skills/check-downstream-consumers/SKILL.md deleted file mode 100644 index 78584da8..00000000 --- a/tool/dart_skills_lint/.agents/skills/check-downstream-consumers/SKILL.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -name: check-downstream-consumers -description: > - Validates an in-progress PR or feature branch of dart_skills_lint against known downstream ecosystem consumers. - Use when assessing breaking changes across external repositories during PR evaluation, testing migrations against the changelog, or determining necessary backwards compatibility shims. -metadata: - internal: true ---- - -# Check Downstream Consumers & Evaluate Breaking Changes - -> [!IMPORTANT] -> **No Downstream Commits Allowed** -> This skill is focused entirely on evaluating breaking changes within `dart_skills_lint` during PR review and changelog validation. All modifications applied to downstream consumer repositories (such as temporarily editing `pubspec.yaml` `ref:` hashes or updating calling syntax to verify migrations) are strictly **diagnostic and transient**. You must **never stage, commit, or push** code inside external downstream repositories during this workflow. - -## 1. Preparation & Repository Verification - -1. **Verify Local Linter State (`dart_skills_lint`)** - - Ensure your working directory in `dart_skills_lint` is clean (`git status`). - - Verify all existing tests pass cleanly (`dart test`). - - Push current commits to the remote branch so downstream consumers can resolve git hashes directly via the network. - - Record the latest remote git SHA-1 commit hash (e.g., `1e1f280...`). - -2. **Locate & Check Out Downstream Consumers** - - Read [`resources/known_consumers.md`](resources/known_consumers.md) to review typical consumer repositories (`flutter/flutter`, `flutter/devtools`, `dart-lang/site-www`, etc.) and their specific consumption subdirectories. - - **Discovering Local Checkouts**: If you do not already know the exact directory paths where these consumer repositories live on disk: - 1. **Check Workspace Knowledge**: Inspect active workspace definitions and machine-specific local Knowledge Items (`KIs`), which frequently record configured system directory structures. - 2. **Inspect Adjacent Parent Directories**: Check common sibling paths right around your current repository root (for example, listing adjacent directories under `..` or running localized, depth-limited searches like `find .. -maxdepth 3 -name pubspec.yaml`). - 3. **Ask Before Running Blind Traversals**: Never execute unbounded root filesystem sweeps (`find / -name ...`). If a target repository cannot be found within adjacent workspace boundaries, immediately ask the user whether the repository is checked out locally and prompt for its path before evaluating. - - For every target checked out on disk, verify its git state is clean and resting on its primary upstream branch (`main` or `master`). Do not run tests against dirty or out-of-date branches. - ---- - -## 2. Pointing Consumers to the In-Progress Hash - -For each downstream consumer under evaluation: - -1. **Update `pubspec.yaml`** - - Locate the target's relevant dependency specification (e.g., `dev/tools/pubspec.yaml` or `tool/pubspec.yaml`). - - Update the `ref:` field under the `git` configuration for `dart_skills_lint` to exact match the in-progress commit hash: - ```yaml - dart_skills_lint: - git: - url: https://github.com/flutter/agent-plugins - path: tool/dart_skills_lint - ref: - ``` - -2. **Resolve Dependencies & Run Verification Tests (Legacy Check)** - - Execute dependency resolution according to the consumer environment (Flutter workspaces require `flutter pub get`; standard pure Dart repositories require `dart pub get`). - - Run the consumer's verification tests against their existing code (typically targeting tests like `test/validate_skills_test.dart` or running `flutter test` / `dart test`). - - Confirm that all existing tests and static analyses compile and pass cleanly when using their established calling syntax. This ensures backward-compatibility deprecation shims function properly right alongside legacy calling conventions. - -3. **Perform Diagnostic API Migration & Boundary Verification** - After verifying across each target consumer that legacy calls function properly without regressions in Step 2: - - **Migrate Consumer Calling Syntax**: For every repository in the set of downstream targets under evaluation (whether a single target, a requested subset, or all known consumers), update its codebase to remove any usage of deprecated getters, parameters, or constructors directly, replacing them with the new API surface introduced in `dart_skills_lint` (for example, transitioning `resolvedRules` arguments to `resolvedRuleConfigs`). - - **Verify Public Boundary Resolution**: Execute strict static analysis (`dart analyze --fatal-infos `) within the consumer's package directory after completing the migration. - - Confirm that all newly exposed classes and parameters resolve cleanly through the public library barrier (`import 'package:dart_skills_lint/dart_skills_lint.dart';`). - - Any syntax check reporting `Undefined class` or requiring internal implementation imports (`import 'package:dart_skills_lint/src/...';`) to compile indicates an explicit **public export deficit** inside `lib/dart_skills_lint.dart`. - - **Run Migrated Test Suite**: Re-run the complete downstream consumer test harness against the migrated code (`flutter test` / `dart test`) to guarantee exact behavioral alignment before accepting the upstream change. - ---- - -## 3. Breaking Change Evaluation & Decision Protocol - -Whenever tests fail or dependency resolution encounters API friction, you must evaluate the nature of the breakage and pause for a deliberate human-in-the-loop decision before taking action. - -### Analyzing the Failure -1. **Diagnose**: Identify exact causes (e.g., renamed public parameters, removed model types, altered getter return types, or modified severity profiles). -2. **Mitigation Options**: Determine if a backwards-compatible code layer can seamlessly bridge the change without compromising new features (e.g., `@Deprecated` getters mapping new types back to legacy structures, constructor parameter forwarding, or fallback exports). -3. **Changelog Integrity**: Verify whether the breaking behavior and its required migration steps are fully documented in `dart_skills_lint/CHANGELOG.md`. - -### The Human Collaboration Point -Present your diagnostic summary to the human and request a deliberate path forward. The choice between mitigating a break inside the linter versus making a breaking change in downstream libraries is strictly a human decision based on ecosystem trade-offs. - -#### Pathway A: Backwards Compatibility Mitigation (Approved by Human) -If the decision is to soften or eliminate the breaking change from `dart_skills_lint`: -1. Modify `dart_skills_lint` code to introduce the backward-compatible shim (such as deprecated getters/constructors or compatibility exports). -2. Add regression tests to ensure both legacy consumer calls and new patterns function properly without throwing exceptions, while verifying mutually exclusive flags fail gracefully if combined. -3. Ensure temporary shims are appropriately documented and tagged with tracking issues (`// TODO(...)`) for future removal. -4. Format code (`dart format .`), verify static analysis (`dart analyze --fatal-infos`), and confirm tests pass locally (`dart test`). -5. Commit and push the updated branch, capture the refreshed SHA-1 commit hash, update the downstream `pubspec.yaml`, and run tests again until the consumer cleanly compiles and passes. - -#### Pathway B: Downstream Migration via Changelog (Break Accepted) -If the human instructs you not to mitigate (or if mitigation is structurally impossible) and accepts the breaking change: -1. **Attempt Downstream Upgrade**: Upgrade the downstream library's calling code **strictly following the migration instructions written in `dart_skills_lint/CHANGELOG.md`**. -2. **Evaluate Changelog Quality**: If the instructions written in the `CHANGELOG.md` are incomplete, confusing, or insufficient to cleanly migrate the downstream code, treat this as an explicit evaluation failure. Immediately propose targeted additions and clarity improvements to `dart_skills_lint/CHANGELOG.md`. -3. **Verify Build & Tests**: After updating both the consumer codebase and any changelog enhancements, re-run dependency checks (`pub get`) and test suites until the downstream package runs cleanly. - -*Constraint โ€” **Do Not Commit**: Remember that any migration edits made across the downstream codebase exist purely to verify that `dart_skills_lint/CHANGELOG.md` instructions function cleanly in practice. Do **not** stage or commit these edits inside the consumer repository.* - ---- - -## 4. Iteration Loop & Repository Cleanup - -After successfully evaluating and resolving one target repository: -1. **Clean Up Consumer State**: Because downstream edits are entirely transient, restore the external consumer repository cleanly back to its initial git state (`git checkout -- .` or `git restore .` across the modified paths) before proceeding to the next candidate, unless expressly instructed by the user to leave uncommitted changes on disk for local inspection. -2. **Record Evaluation Summary**: Update and maintain clear summaries documenting which repositories passed cleanly without changes, which required local mitigations inside `dart_skills_lint`, and which proved out changelog migration workflows. -3. **Check Reviewer Intent**: Ask the human reviewer whether to proceed directly to evaluating the next remaining repository listed in [`resources/known_consumers.md`](resources/known_consumers.md) or halt execution. diff --git a/tool/dart_skills_lint/.agents/skills/check-downstream-consumers/resources/known_consumers.md b/tool/dart_skills_lint/.agents/skills/check-downstream-consumers/resources/known_consumers.md deleted file mode 100644 index c3fbb246..00000000 --- a/tool/dart_skills_lint/.agents/skills/check-downstream-consumers/resources/known_consumers.md +++ /dev/null @@ -1,42 +0,0 @@ -# Known Downstream Consumers of `dart_skills_lint` - -When evaluating the impact of pull requests on downstream repositories, check against these known ecosystem consumers. - -> [!NOTE] -> Local directory paths across individual development machines vary. Avoid assuming fixed directory locations. Locate repositories dynamically (e.g., searching relative to workspace root parent directories or checking common checkout folders) or consult machine-specific local Knowledge Items if available. - -## Repository Directory & Usage Profile - -### 1. `flutter/flutter` -- **Repository URL**: [flutter/flutter](https://github.com/flutter/flutter) -- **Primary Consumer Location**: `dev/tools/` -- **Tooling Engine**: `flutter pub get` and `flutter test` -- **Focus Areas**: Validating agent skills embedded within repository automation and development workflows. - -### 2. `flutter/devtools` -- **Repository URL**: [flutter/devtools](https://github.com/flutter/devtools) -- **Primary Consumer Location**: `tool/` -- **Tooling Engine**: `flutter pub get` and `flutter test` (or `dart test`) -- **Focus Areas**: Custom verification harnesses requiring absolute path isolation patterns (e.g., `validate_skills_test.dart`). - -### 3. `flutter/packages` -- **Repository URL**: [flutter/packages](https://github.com/flutter/packages) -- **Primary Consumer Location**: Package-specific automation tools (for instance, `packages/camera/camera_android_camerax/pubspec.yaml` or shared verification test benches). -- **Tooling Engine**: `flutter pub get` and `flutter test` -- **Focus Areas**: Custom domain-specific validation rules extending `SkillRule` directly. - -### 4. `dart-lang/site-www` -- **Repository URL**: [dart-lang/site-www](https://github.com/dart-lang/site-www) -- **Primary Consumer Location**: `site/` -- **Tooling Engine**: `dart pub get` and `dart test` -- **Focus Areas**: Web documentation generation workflows (`test/lint_skills_test.dart`). - -### 5. `dart-lang/skills` -- **Repository URL**: [dart-lang/skills](https://github.com/dart-lang/skills) -- **Primary Consumer Location**: Root skill sets or tooling harnesses. -- **Tooling Engine**: `dart pub get` and `dart test` - -### 6. `kevmoo/dash_skills` -- **Repository URL**: [kevmoo/dash_skills](https://github.com/kevmoo/dash_skills) -- **Primary Consumer Location**: `tool/` -- **Tooling Engine**: `dart pub get` and `dart test` diff --git a/tool/dart_skills_lint/.agents/skills/contributor-pr-description/SKILL.md b/tool/dart_skills_lint/.agents/skills/contributor-pr-description/SKILL.md deleted file mode 100644 index 5bd72377..00000000 --- a/tool/dart_skills_lint/.agents/skills/contributor-pr-description/SKILL.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: contributor-pr-description -description: Guidelines and format for writing pull request descriptions in this repository. Use this skill whenever the user asks you to draft a pull request description, submit a PR, or update a PR description. -metadata: - internal: true ---- - -# Pull Request Description Guidelines - -When writing a pull request (PR) description, your goal is to provide reviewers with enough context to understand what you did, why you did it, and how they can verify it. A good PR description speeds up the review process and serves as documentation for future contributors. - -## Identity Verification - -If you are in an environment where there are multiple GitHub identities (e.g., an AI agent identity and a primary user identity), **you must verify your identity before pushing or creating a PR**. - -Before committing, pushing, or opening a PR, ensure you are using the identity that the user expects based on the type of work and the repository you are working on. Verify your active GitHub CLI auth (`gh auth status`) and ensure your Git commit author (`git commit --author="..."`) matches the correct identity for the task. - -## PR Description Template - -Always use the following template (or a very similar structure) when drafting a PR description: - -```markdown -## Summary -[Provide a clear, 1-2 sentence summary of what this PR does.] - -## Motivation and Context -[Explain why this change is necessary. What problem does it solve? If it's a bug fix, what was the broken behavior?] - -## Related Issues -[If this PR fixes an open issue, link to it using keywords. e.g., "Fixes #123" or "Closes #456". If it relates to an issue but doesn't close it, use "Related to #789".] - -## What changed -[Optional: Provide a bulleted list of the most important technical changes made in the code. This is useful for larger PRs.] -- Added `FooClass` to handle XYZ. -- Updated `BarMethod` to return `Result`. - -## Testing Instructions -[Explain how reviewers can test your changes locally. Mention any manual verification steps.] -- Run `dart test` to ensure all tests pass. -- [Any specific manual testing steps] - -``` - -## Tone and Style - -1. **Be clear and concise**: Avoid rambling. Use bullet points for readability. -2. **Focus on the "Why"**: The diff shows *what* changed. The description should explain *why* it changed. -3. **Be professional**: Use natural, accessible language. - -## Examples of Bad vs Good Summaries - -**Bad**: "Fixed the bug." (Too vague, doesn't explain what bug) -**Bad**: "Changed line 42 in `main.dart` to use `foo` instead of `bar`." (Focuses too much on the code, which is visible in the diff) - -**Good**: "Fixes a crash when the user clicks 'Submit' without entering an email address by adding validation to the input form." (Explains the problem and the solution) diff --git a/tool/dart_skills_lint/.agents/skills/dart-skills-lint-integration/SKILL.md b/tool/dart_skills_lint/.agents/skills/dart-skills-lint-integration/SKILL.md deleted file mode 100644 index 314e438e..00000000 --- a/tool/dart_skills_lint/.agents/skills/dart-skills-lint-integration/SKILL.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -name: dart-skills-lint-integration -description: > - How to roll, update, and integrate the dart_skills_lint dependency into repositories - (such as flutter/flutter). Use this skill when tasked with bumping dart_skills_lint - pinned commit refs in pubspec.yaml, configuring downstream test harnesses, - managing package hashes, and generating submission pull requests. Do NOT use - this skill for day-to-day skill validation or fixing skill markdown errors - (use dart-skills-lint-validation instead). -metadata: - internal: true ---- - -# Integrating and Configuring dart_skills_lint - -Use this skill to verify repository state, update pinned references, manage -centralized configurations, implement efficient validation test suites, and -output clean pull request commands for `dart_skills_lint`. - -## Pre-Flight Repository Verification - -Before initiating any modifications or executing dependency updates, ensure -the repository is in a clean, safe state: - -1. Run `git status` to confirm the repository has no active work in progress. -2. If clean, check out the primary tracking branch (e.g., `main` or `master`). -3. Fast-forward update from the remote owned by the authoritative org. -4. If post-checkout hooks report engine updates, run necessary sync utilities - (such as `gclient sync`) to guarantee consistency before proceeding. - -## Dependency Management Workflow - -When updating `dart_skills_lint` within a workspace or standalone project: - -1. Locate the target `pubspec.yaml` defining the dependency. -2. Update the pinned Git commit reference directly in the `ref` field. -3. Synchronize the lockfile natively using the environment's package manager. - -### Example: Pinned Git Dependency -```yaml - dart_skills_lint: - git: - url: https://github.com/flutter/skills - path: tool/dart_skills_lint - ref: e4497873950727ee781fa411c1a2f624b1ec50c6 -``` - -## Centralized Configuration Schema - -Configure rules and target paths globally via `dart_skills_lint.yaml`. Always -define paths relative to the repository root execution context. Ensure that -rules at the directory level are properly oriented within a nested `rules` map. - -### Standard Schema Implementation -```yaml -dart_skills_lint: - rules: - check-relative-paths: error - check-absolute-paths: error - check-trailing-whitespace: error - directories: - - path: ".agents/skills" -``` - -## Validation Test Implementation Patterns - -To centralize rule management, load `Configuration` dynamically via -`ConfigParser.loadConfig` and supply it to `validateSkills`. - -If test suites execute under simple environments with stable execution roots, -omit the `skillDirPaths` parameter entirely to natively inherit target paths -defined within the YAML configuration. - -**Absolute Isolation Pattern**: If test harnesses manipulate runtime -execution working directories (such as CI frameworks running tests inside -sub-package folders), guarantee path resilience by resolving configuration -files absolutely using dynamic directory contexts (e.g., `repoRoot.path`). -Explicitly inject absolute `skillDirPaths` targeting; global rules defined -under `rules:` map unconditionally regardless of explicit target path usage. - -When updating an existing validation block, explicitly audit any adjacent -`TODO` or tracker comments. If the comment describes refactoring config -loading or references issues resolved by this update, delete the comment -block entirely. - -### Core Validation Workflow -```dart -import 'package:path/path.dart' as path; -import 'package:dart_skills_lint/dart_skills_lint.dart'; - -const String _configFileName = 'dart_skills_lint.yaml'; - -test('Validate Repository Skills', () async { - // Use dynamic absolute resolution references to guarantee CI stability - final Configuration config = await ConfigParser.loadConfig( - path: path.join(repoRoot.path, 'path', 'to', _configFileName), - ); - expect( - config.directoryConfigs, - isNotEmpty, - reason: 'Configuration directoryConfigs should not be empty.', - ); - final bool isValid = await validateSkills( - skillDirPaths: [skillsDirectory], // Explicit absolute targeting - config: config, - ); - expect(isValid, isTrue); -}); -``` - -### Eliminating Duplicate Overhead in Secondary Blocks - -Secondary test blocks enforcing specialized custom rules without loading the -shared configuration must supply target paths explicitly. To prevent -duplicate execution overhead, explicitly map all default registered -built-in rules to `AnalysisSeverity.disabled`. - -```dart -test('Custom Rule Validation', () async { - final bool isValid = await validateSkills( - skillDirPaths: ['path/to/skills'], - customRules: [MyCustomRule()], - resolvedRuleConfigs: { - 'check-absolute-paths': const RuleConfigPatch(severity: AnalysisSeverity.disabled), - 'check-relative-paths': const RuleConfigPatch(severity: AnalysisSeverity.disabled), - 'check-trailing-whitespace': const RuleConfigPatch(severity: AnalysisSeverity.disabled), - 'description-too-long': const RuleConfigPatch(severity: AnalysisSeverity.disabled), - 'disallowed-field': const RuleConfigPatch(severity: AnalysisSeverity.disabled), - 'invalid-skill-name': const RuleConfigPatch(severity: AnalysisSeverity.disabled), - 'valid-yaml-metadata': const RuleConfigPatch(severity: AnalysisSeverity.disabled), - }, - ); - expect(isValid, isTrue); -}); -``` - -## Expected Final Output: Pull Request Creation Command - -Conclude tasks by staging verified work on a descriptive local branch -(suffixed with the date in YYYY-MM-DD format), committing the changes with -a concise, standard commit message, and outputting a fully executable -`gh pr create` command. - -### Discovering and Populating the Pull Request Template - -To ensure formatting compliance, always look up the target repository's native -pull request template before generating the submission body: - -1. **Locate Template**: Search for template files within `.github/`, - `.github/PULL_REQUEST_TEMPLATE/`, or the project root. Common filenames - include `PULL_REQUEST_TEMPLATE.md` or `pull_request_template.md`. -2. **Extract Structure**: Read the discovered file to identify required - markdown headers, description placeholders, issue citation rules, and - checklists. -3. **Populate Content**: Replace placeholders with clear context summarizing - the dependency rolls, configurations created, and rule blocks optimized. - Check all applicable verification boxes (`[x]`). -4. **Fallback**: If no native template exists, construct a clean submission - body containing a brief summary of modifications, relevant issue links, - and static analysis/testing outcomes. - -### Output Command Structure -```bash -gh pr create \ - --title "Update dart_skills_lint dependency to and centralize config" \ - --body "" -``` - -## Tips for the Flutter Repository (`flutter/flutter`) - -When operating directly within the main Flutter codebase: - -* **Package Resolution**: Run `bin/flutter pub get` at the repository root - instead of `dart pub get` to prevent SDK version mismatch errors. -* **Checksum Integrity**: Updating dependencies natively breaks autogenerated - pubspec checksum hashes. Always recalculate and update stale hashes by - running `bin/flutter update-packages --update-hashes`. -* **Test Orchestration**: Run repository unit tests from the root context: - `bin/flutter test dev/tools/test/validate_skills_test.dart`. -* **Verification**: Ensure zero static analysis warnings using `dart analyze - --fatal-infos` and format all source code cleanly with `dart format`. 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 deleted file mode 100644 index 51783440..00000000 --- a/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: definition-of-done -description: Mandatory checks to run before completing any task that touches md files or dart code in this repository. -metadata: - internal: true ---- - -# Definition of Done - -Use this skill to ensure that all work meets the repository standards before declaring a task complete or requesting review. - -## ๐Ÿ“‹ Mandatory Verification Steps - -Before stating that a task is complete, you MUST execute and pass the following checks: - -1. **Format**: Run `dart format .` to format files, or `dart format --output=none --set-exit-if-changed .` to check without modifying. Ensure all files are formatted correctly. -2. **Analysis**: Run `dart analyze --fatal-infos` and ensure there are zero issues (including info-level issues). -3. **Metrics**: Run `dart run cognitive_complexity --fail-threshold lib test`, where `` is the `--fail-threshold` value configured in `.github/workflows/dart_skills_lint_workflow.yaml`, and ensure there are zero issues. This checks for cognitive complexity. -4. **Tests**: Run `dart test` and ensure all tests pass successfully. -5. **Skills**: If any skill files were modified, run `dart run dart_skills_lint -d .agents/skills` to ensure they are valid. -6. **Changelog**: If the task introduces user-facing CLI flags, package API changes, bug fixes, or user-facing behavioral changes, update `CHANGELOG.md`. - - **Do NOT log internal chores**: Do not add entries for internal CI workflows, dev dependency updates/migrations, test refactoring, or repository infrastructure scripts. - - **Explicit N/A**: If the task is internal-only, leave `CHANGELOG.md` untouched and output `[x] Changelog: (N/A) `. - - 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**: Ensure that code and code comments contain no relative temporal terms (e.g., 'now', 'currently', 'new', 'old', 'existing behavior'). -8. **Documentation**: Ensure that any relevant documentation is updated. - -## ๐Ÿšฆ Output Formatting - -You MUST include a text list of all mandatory verification steps in your final response to the user. Use the exact following format: -- Use `[x] : ` if the step was completed. -- Use `[ ] : ` if the step was skipped or not applicable. - -CRITICAL: Do not just copy the full step description text. You MUST use the exact bolded Identifier from the Mandatory Verification Steps list above, followed by a colon and your short explanation. - -Examples: -- `[x] Format: dart format success.` -- `[x] Analysis: Static clean (0 issues, dart analyze --fatal-infos).` -- `[ ] Skills: Skipped because dart_skills_lint is not installed.` -- `[x] Changelog: (N/A) Not necessary since we're updating internal eval fixtures.` -- `[x] Temporal: no added words.` 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 deleted file mode 100644 index 6fa3dee6..00000000 --- a/tool/dart_skills_lint/.agents/skills/definition-of-done/evals/evals.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "repo_criteria": [ - "evals/code_quality_rubric.json" - ], - "evals": [ - { - "id": 1, - "prompt": "Modify tool/dart_skills_lint/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_chat_output": [ - "The agent confirms it added an explanatory comment about how edit distance is calculated to levenshtein.dart.", - "The agent outputs a text list of all definition-of-done rules in the format '[x] Identifier: Explanation' (if done) or '[ ] Identifier: Skipped explanation' (if skipped).", - "The agent confirms it executed cognitive complexity metrics ('dart run cognitive_complexity') and found zero issues.", - "The agent's definition-of-done checklist marks Changelog as N/A (e.g. '[x] Changelog: (N/A) ...') because adding comments is non-user-facing." - ], - "expected_repo_state": [ - "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.", - "CHANGELOG.md is not modified because adding internal comments is non-user-facing." - ], - "agent_config": "reidbaker-agent" - } - ] -} \ No newline at end of file diff --git a/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json b/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json deleted file mode 100644 index 1ab118fa..00000000 --- a/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "skills": {} -} \ No newline at end of file diff --git a/tool/dart_skills_lint/.agents/skills/ignore.json b/tool/dart_skills_lint/.agents/skills/ignore.json deleted file mode 100644 index e1114b9c..00000000 --- a/tool/dart_skills_lint/.agents/skills/ignore.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "skills": {} -} diff --git a/tool/dart_skills_lint/.agents/skills/run-evals/SKILL.md b/tool/dart_skills_lint/.agents/skills/run-evals/SKILL.md deleted file mode 100644 index a7a180f4..00000000 --- a/tool/dart_skills_lint/.agents/skills/run-evals/SKILL.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: run-evals -description: Run evaluations for one, multiple, or all skills using the agent orchestration framework. Make sure to use this skill whenever the user asks to run evals, test a skill's performance, run benchmarks, or compare baseline versus with-skill execution. -metadata: - internal: true ---- - -# Run Skill Evals - -1. **Read Framework**: Read `/evals/README.md` for understanding the difference between per-skill evals and cross-skill evals (where `` is the directory containing the `.agents` or `skills` folder). -2. **Locate Targets**: Find target `evals/evals.json` files inside `.agents/skills/` and/or `skills/`. For cross-skill evaluations, look for `*_evals.json` files directly in `/evals/`. -3. **Determine Agent Configuration**: Check the `agent_config` field in the target target JSON file to determine the environment/harness to spawn. If `agent_config` is `"bare-agent"`, spawn a subagent with the `bare-agent` profile. If it is a specific contributor profile (e.g. `"reidbaker-agent"`), use that profile to provide the necessary contributor context. -4. **Orchestrate**: By default, run an Integration Test by spawning a single **With-Skill** subagent using `Workspace: branch` and the identified `agent_config`. - - Provide the task prompt. See `resources/with_skill_execution_prompt.md` for the template. When filling in ``, you MUST use a relative path from the repository root, not an absolute path. If you are running a cross-skill evaluation, fill in `` with `"none (cross-skill meta-eval)"`. Also, replace `` with the actual directory path in both templates. - - **Only if the user explicitly requests a comparison or benchmark**, also spawn a **Baseline** subagent. See `resources/baseline_execution_prompt.md` for the template. - Instruct the subagent(s) to return their `git diff` and verification outputs (`dart pub get`, `dart format`, `dart analyze`, `dart test`) without committing. Ensure you instruct them to run these commands exclusively from within the `` directory to avoid analyzing unrelated packages. - **CRITICAL**: You must explicitly warn the subagent(s) to confine all file edits strictly to their current working directory and avoid using absolute paths to modify the parent workspace. - **WORKSPACE LIMITATION WARNING**: If the user has multiple active workspaces mounted, the `Workspace: branch` feature will fail. In this situation, you MUST warn the user that running concurrent evaluations in `Workspace: inherit` mode will cause git state bleed and cross-eval pollution (e.g., changes made by a failure scenario will be visible to a success scenario running simultaneously in the same shared directory). Instruct the user to fix this by closing all workspaces except the primary package workspace, and then re-run the evaluations. Do NOT silently fallback to `Workspace: inherit` for concurrent tasks. -5. **Grade**: Parse the combined rubric (resolving `repo_criteria` + `evals.json` expectations). Use the grading instructions in `resources/agent_judge_prompt.md`. When an expectation fails, you MUST explicitly list both the expectation and what was actually found that caused the failure. -6. **Artifact**: Grade the outputs and generate a Markdown artifact (e.g., `_eval_results.md`) containing the metadata, pass/fail rationale, and raw diffs/stdout. diff --git a/tool/dart_skills_lint/.agents/skills/run-evals/evals/evals.json b/tool/dart_skills_lint/.agents/skills/run-evals/evals/evals.json deleted file mode 100644 index 0e83e89d..00000000 --- a/tool/dart_skills_lint/.agents/skills/run-evals/evals/evals.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "repo_criteria": [ - ], - "evals": [ - { - "id": 1, - "prompt": "Run the evals for the definition-of-done skill.", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "The agent successfully triggered subagents to run the test cases.", - "There is an artifact with the results.", - "No files are modified in the parent workspace after the eval is done running (all modifications must be confined to the isolated subagent worktree).", - "The evaluation artifact includes evals sourced from code_quality_rubric.json for a skill that produces code." - ], - "agent_config": "reidbaker-agent" - }, - { - "id": 2, - "prompt": "Can you test the definition-of-done skill and see if it passes its rubric?", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "There is an artifact with the results.", - "No files are modified in the parent workspace after the eval is done running (all modifications must be confined to the isolated subagent worktree).", - "The evaluation artifact includes evals sourced from code_quality_rubric.json for a skill that produces code." - ], - "agent_config": "reidbaker-agent" - }, - { - "id": 3, - "prompt": "Run an A/B benchmark evaluation for the definition-of-done skill, comparing it explicitly against a baseline without the skill.", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "The agent successfully triggered both baseline and with-skill subagents.", - "There is an artifact with the results.", - "The evaluation artifact contains explicit sections for both the 'With-Skill Agent' and the 'Baseline Agent'.", - "No files are modified in the parent workspace after the eval is done running (all modifications must be confined to the isolated subagent worktree)." - ], - "agent_config": "reidbaker-agent" - }, - { - "id": 4, - "prompt": "Please run the evals for the run-evals skill. Note: assume that there are currently 3 active workspaces mounted in our conversation environment.", - "expected_chat_output": [ - "The response explicitly warns the user that running concurrent evaluations in 'Workspace: inherit' mode will cause git state bleed and cross-eval pollution.", - "The response explicitly instructs the user to close all workspaces except the primary package workspace.", - "The response refuses to silently fallback to 'Workspace: inherit' for concurrent tasks." - ], - "expected_repo_state": [ - "No evaluation artifact is generated.", - "No subagents are spawned." - ], - "agent_config": "reidbaker-agent" - } - ] -} \ No newline at end of file diff --git a/tool/dart_skills_lint/.agents/skills/run-evals/resources/agent_judge_prompt.md b/tool/dart_skills_lint/.agents/skills/run-evals/resources/agent_judge_prompt.md deleted file mode 100644 index e5686225..00000000 --- a/tool/dart_skills_lint/.agents/skills/run-evals/resources/agent_judge_prompt.md +++ /dev/null @@ -1,13 +0,0 @@ -You are an expert evaluator. Review the following execution outputs from an AI agent against the provided combined rubric. - -Execution Outputs: -- Git Diff: -- Command Stdout: - -Combined Rubric Expectations: - - -For every single expectation, explicitly state whether it PASSED or FAILED and provide a 1-sentence justification. -IMPORTANT: When an expectation fails, explicitly list the expectation and explain exactly what you found that caused the failure. - -Finally, provide an overall PASS/FAIL grade. diff --git a/tool/dart_skills_lint/.agents/skills/run-evals/resources/baseline_execution_prompt.md b/tool/dart_skills_lint/.agents/skills/run-evals/resources/baseline_execution_prompt.md deleted file mode 100644 index 8e65b34b..00000000 --- a/tool/dart_skills_lint/.agents/skills/run-evals/resources/baseline_execution_prompt.md +++ /dev/null @@ -1,9 +0,0 @@ -Execute this task: -- Task: -- Input files: - -WARNING: You are executing in an isolated branch workspace. Confine all file modifications strictly to your current working directory. Do NOT use absolute paths to modify files in the parent workspace. - -Once you are done, do not commit. Just send me a message with the `git diff` of your changes, and the output of running verification commands (e.g., `dart pub get`, `dart format`, `dart analyze`, `dart test`). -CRITICAL: You must explicitly `cd` into the `` directory before running any verification commands to avoid analyzing unrelated packages in the workspace! -NOTE: If your task is strictly to grade, review, or evaluate code, do NOT fix the issues you find. Leave the code exactly as it is, even if verification commands fail. Your job is only to report the evaluation results. diff --git a/tool/dart_skills_lint/.agents/skills/run-evals/resources/with_skill_execution_prompt.md b/tool/dart_skills_lint/.agents/skills/run-evals/resources/with_skill_execution_prompt.md deleted file mode 100644 index 8a8b5464..00000000 --- a/tool/dart_skills_lint/.agents/skills/run-evals/resources/with_skill_execution_prompt.md +++ /dev/null @@ -1,10 +0,0 @@ -Execute this task: -- Skill path: (Please read and STRICTLY FOLLOW the instructions in this skill file before finishing) -- Task: -- Input files: - -WARNING: You are executing in an isolated branch workspace. Confine all file modifications strictly to your current working directory. Do NOT use absolute paths to modify files in the parent workspace. - -Once you are done, do not commit. Just send me a message with the `git diff` of your changes, and the output of running verification commands (e.g., `dart pub get`, `dart format`, `dart analyze`, `dart test`). -CRITICAL: You must explicitly `cd` into the `` directory before running any verification commands to avoid analyzing unrelated packages in the workspace! -NOTE: If your task is strictly to grade, review, or evaluate code, do NOT fix the issues you find. Leave the code exactly as it is, even if verification commands fail. Your job is only to report the evaluation results. diff --git a/tool/dart_skills_lint/.config/dart_skills/skills_config.json b/tool/dart_skills_lint/.config/dart_skills/skills_config.json deleted file mode 100644 index 106b2d0a..00000000 --- a/tool/dart_skills_lint/.config/dart_skills/skills_config.json +++ /dev/null @@ -1,573 +0,0 @@ -{ - "version": 2, - "installations": { - "generic": { - "https://github.com/kevmoo/dash_skills.git": { - "skills": [ - { - "name": "dart-long-lines", - "installedAt": "2026-08-18T17:27:49.436543Z", - "contentHash": "0Ow+sE4/e0GP4e0lgVpI9Q==", - "path": "skills/dart-long-lines" - }, - { - "name": "dart-doc-validation", - "installedAt": "2026-08-18T17:27:49.438564Z", - "contentHash": "n2IqfdFOc9Gvr6Hc+ncRbA==", - "path": "skills/dart-doc-validation" - }, - { - "name": "dart-multiline-strings", - "installedAt": "2026-08-18T17:27:49.438588Z", - "isInstalled": false, - "path": "skills/dart-multiline-strings" - }, - { - "name": "dart-test-coverage", - "installedAt": "2026-08-18T17:36:39.025422Z", - "contentHash": "3o+cHW7PINj+cVkKQ/hj9g==", - "path": "skills/dart-test-coverage" - }, - { - "name": "dart-modern-features", - "installedAt": "2026-08-18T17:36:39.026875Z", - "contentHash": "AnrNPoAjYf3jrPsLb2FHXA==", - "path": "skills/dart-modern-features" - }, - { - "name": "profile-dart-code", - "installedAt": "2026-08-18T17:27:49.438594Z", - "isInstalled": false, - "path": "skills/profile-dart-code" - }, - { - "name": "dart-matcher-best-practices", - "installedAt": "2026-08-18T17:27:49.440200Z", - "contentHash": "gj8wmAmQa/OoLAkNFUjhdw==", - "path": "skills/dart-matcher-best-practices" - }, - { - "name": "dart-best-practices", - "installedAt": "2026-08-18T17:27:49.441665Z", - "contentHash": "xQp+0QhvcC1FKY6hf/YUIQ==", - "path": "skills/dart-best-practices" - }, - { - "name": "dart-package-maintenance", - "installedAt": "2026-08-18T17:27:49.443160Z", - "contentHash": "0H+7OSgyv3E0/rjt8kwlEQ==", - "path": "skills/dart-package-maintenance" - }, - { - "name": "dart-test-fundamentals", - "installedAt": "2026-08-18T17:36:39.028109Z", - "contentHash": "2HW6ObOpIuGZPyW7BMmXTA==", - "path": "skills/dart-test-fundamentals" - } - ] - }, - "https://github.com/dart-lang/skills.git": { - "skills": [ - { - "name": "dart-use-pattern-matching", - "installedAt": "2026-08-18T17:27:51.134172Z", - "contentHash": "dt0YV4d0VH2QK8z8puzumQ==", - "path": "skills/dart-use-pattern-matching" - }, - { - "name": "dart-generate-test-mocks", - "installedAt": "2026-08-18T17:27:51.134340Z", - "isInstalled": false, - "path": "skills/dart-generate-test-mocks" - }, - { - "name": "dart-use-ffigen", - "installedAt": "2026-08-18T17:27:51.134344Z", - "isInstalled": false, - "path": "skills/dart-use-ffigen" - }, - { - "name": "dart-resolve-package-conflicts", - "installedAt": "2026-08-18T17:27:51.134344Z", - "isInstalled": false, - "path": "skills/dart-resolve-package-conflicts" - }, - { - "name": "dart-collect-coverage", - "installedAt": "2026-08-18T17:27:51.136213Z", - "contentHash": "B9/qZ2V7FxeX+MIy1Pc2iA==", - "path": "skills/dart-collect-coverage" - }, - { - "name": "dart-use-primary-constructors", - "installedAt": "2026-08-18T17:27:51.136228Z", - "isInstalled": false, - "path": "skills/dart-use-primary-constructors" - }, - { - "name": "dart-setup-ffi-assets", - "installedAt": "2026-08-18T17:27:51.136229Z", - "isInstalled": false, - "path": "skills/dart-setup-ffi-assets" - }, - { - "name": "dart-migrate-to-checks-package", - "installedAt": "2026-08-18T17:27:51.138287Z", - "contentHash": "qnEkEh/eW/JmKpsNQ80yRQ==", - "path": "skills/dart-migrate-to-checks-package" - }, - { - "name": "dart-fix-runtime-errors", - "installedAt": "2026-08-18T17:27:51.138306Z", - "isInstalled": false, - "path": "skills/dart-fix-runtime-errors" - }, - { - "name": "dart-add-unit-test", - "installedAt": "2026-08-18T17:27:51.139954Z", - "contentHash": "BTBQcGE2w7XxaizcWbHAmQ==", - "path": "skills/dart-add-unit-test" - }, - { - "name": "dart-run-static-analysis", - "installedAt": "2026-08-18T17:27:51.139970Z", - "isInstalled": false, - "path": "skills/dart-run-static-analysis" - }, - { - "name": "dart-build-cli-app", - "installedAt": "2026-08-18T17:27:51.141815Z", - "contentHash": "V+mcu5FkZPIidSf7qP02dQ==", - "path": "skills/dart-build-cli-app" - } - ] - }, - "https://github.com/mattpocock/skills.git": { - "skills": [ - { - "name": "setup-pre-commit", - "installedAt": "2026-08-18T17:27:52.552866Z", - "isInstalled": false, - "path": "skills/misc/setup-pre-commit" - }, - { - "name": "git-guardrails-claude-code", - "installedAt": "2026-08-18T17:27:52.552938Z", - "isInstalled": false, - "path": "skills/misc/git-guardrails-claude-code" - }, - { - "name": "scaffold-exercises", - "installedAt": "2026-08-18T17:27:52.552941Z", - "isInstalled": false, - "path": "skills/misc/scaffold-exercises" - }, - { - "name": "migrate-to-shoehorn", - "installedAt": "2026-08-18T17:27:52.552941Z", - "isInstalled": false, - "path": "skills/misc/migrate-to-shoehorn" - }, - { - "name": "setup-ts-deep-modules", - "installedAt": "2026-08-18T17:27:52.552942Z", - "isInstalled": false, - "path": "skills/in-progress/setup-ts-deep-modules" - }, - { - "name": "loop-me", - "installedAt": "2026-08-18T17:27:52.552942Z", - "isInstalled": false, - "path": "skills/in-progress/loop-me" - }, - { - "name": "writing-fragments", - "installedAt": "2026-08-18T17:27:52.552943Z", - "isInstalled": false, - "path": "skills/in-progress/writing-fragments" - }, - { - "name": "writing-shape", - "installedAt": "2026-08-18T17:27:52.552943Z", - "isInstalled": false, - "path": "skills/in-progress/writing-shape" - }, - { - "name": "claude-handoff", - "installedAt": "2026-08-18T17:27:52.552944Z", - "isInstalled": false, - "path": "skills/in-progress/claude-handoff" - }, - { - "name": "writing-beats", - "installedAt": "2026-08-18T17:27:52.552944Z", - "isInstalled": false, - "path": "skills/in-progress/writing-beats" - }, - { - "name": "research", - "installedAt": "2026-08-18T17:27:52.552945Z", - "isInstalled": false, - "path": "skills/engineering/research" - }, - { - "name": "domain-modeling", - "installedAt": "2026-08-18T17:27:52.552945Z", - "isInstalled": false, - "path": "skills/engineering/domain-modeling" - }, - { - "name": "wayfinder", - "installedAt": "2026-08-18T17:27:52.552946Z", - "isInstalled": false, - "path": "skills/engineering/wayfinder" - }, - { - "name": "setup-matt-pocock-skills", - "installedAt": "2026-08-18T17:27:52.552946Z", - "isInstalled": false, - "path": "skills/engineering/setup-matt-pocock-skills" - }, - { - "name": "diagnosing-bugs", - "installedAt": "2026-08-18T17:27:52.552947Z", - "isInstalled": false, - "path": "skills/engineering/diagnosing-bugs" - }, - { - "name": "code-review", - "installedAt": "2026-08-18T17:27:52.552947Z", - "isInstalled": false, - "path": "skills/engineering/code-review" - }, - { - "name": "to-tickets", - "installedAt": "2026-08-18T17:27:52.552948Z", - "isInstalled": false, - "path": "skills/engineering/to-tickets" - }, - { - "name": "ask-matt", - "installedAt": "2026-08-18T17:27:52.552948Z", - "isInstalled": false, - "path": "skills/engineering/ask-matt" - }, - { - "name": "wizard", - "installedAt": "2026-08-18T17:27:52.552949Z", - "isInstalled": false, - "path": "skills/engineering/wizard" - }, - { - "name": "improve-codebase-architecture", - "installedAt": "2026-08-18T17:27:52.552949Z", - "isInstalled": false, - "path": "skills/engineering/improve-codebase-architecture" - }, - { - "name": "to-spec", - "installedAt": "2026-08-18T17:27:52.552950Z", - "isInstalled": false, - "path": "skills/engineering/to-spec" - }, - { - "name": "codebase-design", - "installedAt": "2026-08-18T17:27:52.552950Z", - "isInstalled": false, - "path": "skills/engineering/codebase-design" - }, - { - "name": "triage", - "installedAt": "2026-08-18T17:27:52.552951Z", - "isInstalled": false, - "path": "skills/engineering/triage" - }, - { - "name": "prototype", - "installedAt": "2026-08-18T17:27:52.552951Z", - "isInstalled": false, - "path": "skills/engineering/prototype" - }, - { - "name": "resolving-merge-conflicts", - "installedAt": "2026-08-18T17:27:52.552952Z", - "isInstalled": false, - "path": "skills/engineering/resolving-merge-conflicts" - }, - { - "name": "grill-with-docs", - "installedAt": "2026-08-18T17:27:52.552952Z", - "isInstalled": false, - "path": "skills/engineering/grill-with-docs" - }, - { - "name": "implement", - "installedAt": "2026-08-18T17:27:52.552953Z", - "isInstalled": false, - "path": "skills/engineering/implement" - }, - { - "name": "tdd", - "installedAt": "2026-08-18T17:27:52.552953Z", - "isInstalled": false, - "path": "skills/engineering/tdd" - }, - { - "name": "writing-for-agents", - "installedAt": "2026-08-18T17:27:52.552954Z", - "isInstalled": false, - "path": "skills/productivity/writing-for-agents" - }, - { - "name": "wait-what", - "installedAt": "2026-08-18T17:27:52.552954Z", - "isInstalled": false, - "path": "skills/productivity/wait-what" - }, - { - "name": "handoff", - "installedAt": "2026-08-18T17:27:52.552955Z", - "isInstalled": false, - "path": "skills/productivity/handoff" - }, - { - "name": "to-questionnaire", - "installedAt": "2026-08-18T17:27:52.552955Z", - "isInstalled": false, - "path": "skills/productivity/to-questionnaire" - }, - { - "name": "teach", - "installedAt": "2026-08-18T17:27:52.552956Z", - "isInstalled": false, - "path": "skills/productivity/teach" - }, - { - "name": "grill-me", - "installedAt": "2026-08-18T17:27:52.556434Z", - "contentHash": "UyaCrrVTk67EppsVYI30/g==", - "path": "skills/productivity/grill-me" - }, - { - "name": "grilling", - "installedAt": "2026-08-18T17:27:52.556522Z", - "isInstalled": false, - "path": "skills/productivity/grilling" - } - ] - }, - "https://github.com/obra/superpowers.git": { - "skills": [ - { - "name": "using-git-worktrees", - "installedAt": "2026-08-18T17:27:53.861103Z", - "isInstalled": false, - "path": "skills/using-git-worktrees" - }, - { - "name": "test-driven-development", - "installedAt": "2026-08-18T17:27:53.864197Z", - "contentHash": "PCdssGu3iJFLGNY7FvIIYA==", - "path": "skills/test-driven-development" - }, - { - "name": "systematic-debugging", - "installedAt": "2026-08-18T17:27:53.864312Z", - "isInstalled": false, - "path": "skills/systematic-debugging" - }, - { - "name": "using-superpowers", - "installedAt": "2026-08-18T17:27:53.864313Z", - "isInstalled": false, - "path": "skills/using-superpowers" - }, - { - "name": "dispatching-parallel-agents", - "installedAt": "2026-08-18T17:27:53.864314Z", - "isInstalled": false, - "path": "skills/dispatching-parallel-agents" - }, - { - "name": "executing-plans", - "installedAt": "2026-08-18T17:27:53.864315Z", - "isInstalled": false, - "path": "skills/executing-plans" - }, - { - "name": "finishing-a-development-branch", - "installedAt": "2026-08-18T17:27:53.864315Z", - "isInstalled": false, - "path": "skills/finishing-a-development-branch" - }, - { - "name": "brainstorming", - "installedAt": "2026-08-18T17:27:53.864316Z", - "isInstalled": false, - "path": "skills/brainstorming" - }, - { - "name": "writing-plans", - "installedAt": "2026-08-18T17:27:53.864316Z", - "isInstalled": false, - "path": "skills/writing-plans" - }, - { - "name": "requesting-code-review", - "installedAt": "2026-08-18T17:27:53.864317Z", - "isInstalled": false, - "path": "skills/requesting-code-review" - }, - { - "name": "receiving-code-review", - "installedAt": "2026-08-18T17:27:53.864317Z", - "isInstalled": false, - "path": "skills/receiving-code-review" - }, - { - "name": "writing-skills", - "installedAt": "2026-08-18T17:27:53.864319Z", - "isInstalled": false, - "path": "skills/writing-skills" - }, - { - "name": "verification-before-completion", - "installedAt": "2026-08-18T17:27:53.864320Z", - "isInstalled": false, - "path": "skills/verification-before-completion" - }, - { - "name": "subagent-driven-development", - "installedAt": "2026-08-18T17:27:53.864320Z", - "isInstalled": false, - "path": "skills/subagent-driven-development" - } - ] - }, - "https://github.com/anthropics/skills.git": { - "skills": [ - { - "name": "template", - "installedAt": "2026-08-18T17:36:41.168548Z", - "isInstalled": false, - "path": "template" - }, - { - "name": "theme-factory", - "installedAt": "2026-08-18T17:36:41.168617Z", - "isInstalled": false, - "path": "skills/theme-factory" - }, - { - "name": "doc-coauthoring", - "installedAt": "2026-08-18T17:36:41.168618Z", - "isInstalled": false, - "path": "skills/doc-coauthoring" - }, - { - "name": "discernment-nudge", - "installedAt": "2026-08-18T17:36:41.168618Z", - "isInstalled": false, - "path": "skills/discernment-nudge" - }, - { - "name": "claude-api", - "installedAt": "2026-08-18T17:36:41.168618Z", - "isInstalled": false, - "path": "skills/claude-api" - }, - { - "name": "xlsx", - "installedAt": "2026-08-18T17:36:41.168619Z", - "isInstalled": false, - "path": "skills/xlsx" - }, - { - "name": "pdf", - "installedAt": "2026-08-18T17:36:41.168619Z", - "isInstalled": false, - "path": "skills/pdf" - }, - { - "name": "algorithmic-art", - "installedAt": "2026-08-18T17:36:41.168620Z", - "isInstalled": false, - "path": "skills/algorithmic-art" - }, - { - "name": "internal-comms", - "installedAt": "2026-08-18T17:36:41.168620Z", - "isInstalled": false, - "path": "skills/internal-comms" - }, - { - "name": "skill-creator", - "installedAt": "2026-08-18T17:36:41.186510Z", - "contentHash": "XmGQkLldYzV5whfkKaa4MA==", - "path": "skills/skill-creator" - }, - { - "name": "canvas-design", - "installedAt": "2026-08-18T17:36:41.186631Z", - "isInstalled": false, - "path": "skills/canvas-design" - }, - { - "name": "pptx", - "installedAt": "2026-08-18T17:36:41.186632Z", - "isInstalled": false, - "path": "skills/pptx" - }, - { - "name": "slack-gif-creator", - "installedAt": "2026-08-18T17:36:41.186633Z", - "isInstalled": false, - "path": "skills/slack-gif-creator" - }, - { - "name": "webapp-testing", - "installedAt": "2026-08-18T17:36:41.186634Z", - "isInstalled": false, - "path": "skills/webapp-testing" - }, - { - "name": "frontend-design", - "installedAt": "2026-08-18T17:36:41.186634Z", - "isInstalled": false, - "path": "skills/frontend-design" - }, - { - "name": "mcp-builder", - "installedAt": "2026-08-18T17:36:41.186635Z", - "isInstalled": false, - "path": "skills/mcp-builder" - }, - { - "name": "brand-guidelines", - "installedAt": "2026-08-18T17:36:41.186636Z", - "isInstalled": false, - "path": "skills/brand-guidelines" - }, - { - "name": "docx", - "installedAt": "2026-08-18T17:36:41.186636Z", - "isInstalled": false, - "path": "skills/docx" - }, - { - "name": "academy-guide", - "installedAt": "2026-08-18T17:36:41.186637Z", - "isInstalled": false, - "path": "skills/academy-guide" - }, - { - "name": "web-artifacts-builder", - "installedAt": "2026-08-18T17:36:41.186637Z", - "isInstalled": false, - "path": "skills/web-artifacts-builder" - } - ] - } - } - } -} diff --git a/tool/dart_skills_lint/.gitignore b/tool/dart_skills_lint/.gitignore deleted file mode 100644 index 3ee5e5ea..00000000 --- a/tool/dart_skills_lint/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# Dart/Flutter -.dart_tool/ -.packages -.flutter-plugins -.flutter-plugins-dependencies -build/ - -# IDE files -.vscode/ -.idea/ -*.iml -*.code-workspace - -# OS files -.DS_Store - -# You might want to ignore pubspec.lock if this is a library package -# pubspec.lock - -# Logs and local agent tool directories -.agents/*.log -/.claude/ - -# Build and test coverage artifacts -coverage/ - -# 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/AUTHORS b/tool/dart_skills_lint/AUTHORS deleted file mode 100644 index 61159efd..00000000 --- a/tool/dart_skills_lint/AUTHORS +++ /dev/null @@ -1,7 +0,0 @@ -# Below is a list of people and organizations that have contributed -# to the project. Names should be added to the list like so: -# -# Name/Organization - -Google LLC -Reid Baker diff --git a/tool/dart_skills_lint/CHANGELOG.md b/tool/dart_skills_lint/CHANGELOG.md deleted file mode 100644 index 7a1c65c4..00000000 --- a/tool/dart_skills_lint/CHANGELOG.md +++ /dev/null @@ -1,83 +0,0 @@ -## 0.5.1 - -- Migrated developer setup, integration recipes, and documentation to use `dart install skills@^1.0.0` and `dart install dart_skills_lint`. -- Removed legacy npm-based tooling artifacts (`.npmrc` and `skills-lock.json`). - -## 0.5.0 - -- Added support for rule-specific custom parameters 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 parameters (e.g. `--path-does-not-exist-exclude`) with support for empty string overrides to clear parameters. -- Implemented parameter type-coercion for `int`, `bool`, and `List` types parsed from the command line. - -### Deprecations & Refactoring - -- Refactored `ValidationResult` out of `src/validator.dart` into `src/models/validation_result.dart`. `src/validator.dart` re-exports `ValidationResult` to preserve complete backward compatibility for packages importing internal structure directly. -- Deprecated and transitioned rule configuration concepts across model structures and APIs to use consistent `ruleConfigs` naming, alongside backwards-compatible deprecation shims: - - Deprecated `Validator` constructor parameter `ruleOverrides` in favor of `ruleConfigs`. - - Deprecated `Configuration.configuredRules` and `LintTargetConfig.rules` getters in favor of `ruleConfigs`. - - Deprecated `ValidationSession` constructor parameter `resolvedRules` in favor of `resolvedRuleConfigs`. - - Deprecated `ValidationSession` method `resolveRulesForPath` in favor of `resolveRuleConfigsForPath`. - -## 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 - Dart SDK via `curl -fsSL .../install.sh | bash`, or download the - tarball directly and verify its SHA256. -- macOS binaries are not yet code-signed; clear the - quarantine flag with - `xattr -d com.apple.quarantine $(which dart_skills_lint)` on - first launch. -- The `dart pub global activate dart_skills_lint` and - `dev_dependencies:` install paths are unchanged. - -## 0.3.1 - -- `--fix` now writes fixes to disk; pair with `--dry-run` - (`--fix --dry-run`) to preview the proposed diff without writing. - The legacy `--fix-apply` flag still works but is deprecated and - emits a notice on stderr. -- Running the CLI with no arguments and no `.claude/skills` or - `.agents/skills` directory present now prints a short onboarding - guide explaining how to point the linter at a skill or a skills - root. -- `description-too-long` errors now report the actual character - count and show an excerpt with a `|HERE|` marker at the cutoff - so authors can see exactly where the text went over. The same - diagnostic shape is now used for the `compatibility` field's - 500-character limit. -- `invalid-skill-name` errors now disambiguate the frontmatter - `name:` field from the parent directory name, quote the offending - value, and suggest a normalized form. The directory-mismatch - error offers both directions of the fix (edit the field or - rename the directory). -- `check-relative-paths` errors now include the resolved absolute - path and, when a near-miss filename exists in the same - directory, surface a `Did you mean "..."?` suggestion that - preserves the link's directory prefix. -- New `example/` directory with reference `valid` and `invalid` - skill fixtures and a walkthrough. -- New "Recipes" section in `README.md` with copy-pasteable GitHub - Actions and pre-commit hook integrations. - -## 0.3.0 - -- Exposed `ConfigParser.loadConfig()` API to load configuration files programmatically. -- Supported tilde expansion (`~/`) in configuration file paths. -- Updated documentation to clarify CLI vs. Dart Test usage. - -## 0.2.0 - -- Refactored validator to a pluggable rule-based architecture. -- Added support for custom rules via `SkillRule`. -- Added runtime assertion for duplicate rule names. -- Added warning when a rule emits an error with severity different from its definition. -- Updated `README.md` with custom rules documentation. -- **Breaking Change**: Enabling a rule via CLI flag now sets its severity to `error` instead of `warning`. - -## 0.1.0 - -- Initial version. diff --git a/tool/dart_skills_lint/CONTRIBUTING.md b/tool/dart_skills_lint/CONTRIBUTING.md deleted file mode 100644 index 5e27ea3f..00000000 --- a/tool/dart_skills_lint/CONTRIBUTING.md +++ /dev/null @@ -1,152 +0,0 @@ -# How to Contribute - -We'd love to accept your patches and contributions to this project. There are -just a few small guidelines you need to follow. - -## Contributor License Agreement - -Contributions to this project must be accompanied by a Contributor License -Agreement (CLA). You (or your employer) retain the copyright to your -contribution; this simply gives us permission to use and redistribute your -contributions as part of the project. Head over to - to see your current agreements on file or -to sign a new one. - -You generally only need to submit a CLA once, so if you've already submitted one -(even if it was for a different project), you probably don't need to do it -again. - -## Code Reviews - -All submissions, including submissions by project members, require review. We -use GitHub pull requests for this purpose. Consult -[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more -information on using pull requests. - -## Coding style - -The Dart source code in this repo follows the: - - * [Dart style guide](https://dart.dev/guides/language/effective-dart/style) - -You should familiarize yourself with those guidelines. - -## File headers - -All files in the Dart project must start with the following header; if you add a -new file please also add this. The year should be a single number stating the -year the file was created (don't use a range like "2011-2012"). Additionally, if -you edit an existing file, you shouldn't update the year. - - // 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. - -## Embedding the linter in tests - -If your project already uses `dart_skills_lint`, you can also call it -from your own test suite โ€” handy when you want skill validation to fail -the same Dart-test pipeline that already gates the rest of your code: - -```dart -import 'package:dart_skills_lint/dart_skills_lint.dart'; -import 'package:test/test.dart'; - -void main() { - test('Run skills linter', () async { - // Load whatever's in dart_skills_lint.yaml so the CLI and tests - // share configuration. Pass `customRules: [...]` to inject any - // custom SkillRule implementations. - final config = await ConfigParser.loadConfig(); - expect( - config.directoryConfigs, - isNotEmpty, - reason: 'Configuration directoryConfigs should not be empty.', - ); - await validateSkills(config: config); - }); -} -``` - -`Validator` and `ValidationResult` are also exposed for tests that -need to inspect errors programmatically. Custom rule authoring lives -in the -[`dart-skills-lint-validation`](skills/dart-skills-lint-validation/SKILL.md) -skill. - -## Testing and coverage - -Run the test suite from the package root (`tool/dart_skills_lint`): - -```bash -dart test -``` - -CI enforces a minimum line-coverage threshold for `lib/` (currently 73%), -excluding generated `*.g.dart` files. To reproduce the same number locally: - -```bash -dart test --coverage=coverage -dart run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib --ignore-files='**/*.g.dart' -``` - -The `--ignore-files='**/*.g.dart'` flag drops generated files from the report so -your local total matches the threshold CI enforces (CI applies the same -exclusion via the `very_good_coverage` action's `exclude` input). Omit the flag -to include generated files. - -CI feeds `coverage/lcov.info` to the -[`very_good_coverage`](https://github.com/VeryGoodOpenSource/very_good_coverage) -GitHub Action, which fails the build when coverage falls below the threshold. -The threshold ratchets against regressions: when you raise overall coverage, -bump `min_coverage` in `.github/workflows/dart_skills_lint_workflow.yaml` to -lock in the gain. To inspect coverage locally, render `coverage/lcov.info` with -`genhtml` or an editor LCOV viewer. - -## Community Guidelines - -This project follows -[Google's Open Source Community Guidelines](https://opensource.google/conduct/). - -We pledge to maintain an open and welcoming environment. For details, see our -[code of conduct](https://dart.dev/code-of-conduct). - -## Rule-stability policy (SemVer) - -Lint rules are part of `dart_skills_lint`'s public API. Adopters wire -the linter into pre-commit hooks and CI gates, so a rule that silently -flips from "warning" to "error" can break a downstream build with no -code change of their own. We version rule changes the same way we -version code changes: - -- **Patch release (`0.3.X` โ†’ `0.3.X+1`, `1.0.X` โ†’ `1.0.X+1`)** โ€” - bug fixes to existing rules, including diagnostic message - rewording, internal refactors, and fixes that *narrow* what a rule - matches (fewer false positives). The set of error states a passing - skill needs to clear does not grow. - -- **Minor release (`0.3.X` โ†’ `0.4.0`, `1.0.X` โ†’ `1.1.0`)** โ€” new - rules, **shipping with `defaultSeverity: AnalysisSeverity.disabled`** - so existing skills keep passing. Adopters opt in by enabling the - rule via flag or YAML config. Performance improvements that don't - change diagnostics also land here. A rule's diagnostic message may - expand to include additional context. - -- **Major release (`0.X` โ†’ `1.0`, `1.X` โ†’ `2.0`)** โ€” any change that - can fail a previously-passing skill: removing a rule (so configs - referencing it stop working), upgrading a rule's default severity - (`disabled โ†’ warning`, `warning โ†’ error`), broadening what a rule - matches (more true positives = more failures), or renaming a rule. - Releases bump the major version and the CHANGELOG calls out the - exact rules affected. - -Rationale: adopters should be able to set `dart_skills_lint: ^1.0.0` -in `pubspec.yaml` and trust that a `dart pub upgrade` never turns -green CI red without their consent. Surprises belong in major -releases, and only there. - -If you're proposing a change that doesn't fit cleanly into one of the -buckets above, say so on the PR and the maintainers will decide where -it lands. New built-in rules **must** include a `## ` -entry in `RULES.md` describing default severity and behavior โ€” see -the existing entries for the expected shape. diff --git a/tool/dart_skills_lint/LICENSE b/tool/dart_skills_lint/LICENSE deleted file mode 100644 index 9035a416..00000000 --- a/tool/dart_skills_lint/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright 2026, the Dart project authors. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google LLC nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tool/dart_skills_lint/README.md b/tool/dart_skills_lint/README.md deleted file mode 100644 index 6ce084ee..00000000 --- a/tool/dart_skills_lint/README.md +++ /dev/null @@ -1,297 +0,0 @@ -# dart_skills_lint - -A static analysis linter for Agent Skills to ensure they meet the specification in presubmit checks. This project is a Dart package and can be run as a CLI tool to validate your skills directory before committing. - -## Table of Contents -- [Overview](#overview) -- [Installation](#installation) -- [Usage](#usage) - - [Rule Precedence](#rule-precedence) -- [Specification Validation](#specification-validation) -- [Recipes](#recipes) -- [Contributing](#contributing) - -## Overview - -An **Agent Skill** is a portable, self-contained directory that extends an AI agent's capabilities. Pre-submit linting ensures that your skill definitions are valid and ready for consumption by agent platforms. - -`dart_skills_lint` validates: -- Presence of mandatory `SKILL.md` file. -- YAML frontmatter constraints (naming, length, etc.). -- Directory structure (flat, no deep nesting). -- Relative path integrity. - -For a full definition of the skill standard, see the [Agent Skills Specification](documentation/knowledge/SPECIFICATION.md). - -## Installation - -`dart_skills_lint` ships as both a standalone native binary (no Dart -SDK required) and as a Dart package on pub.dev. Pick the path that -matches your environment. - -> **Homebrew note.** A `brew install dart-skills-lint` path is on the -> roadmap; it will land after `dart_skills_lint` migrates to its own -> dedicated repository. Until then, the install paths below cover all -> supported platforms. - -### 1. Dart developers โ€” pub.dev - -If you already have the Dart SDK installed, the standard pub.dev paths -still work and are unchanged. - -#### As a project dev_dependency - -Add to your `pubspec.yaml`: -```yaml -dev_dependencies: - dart_skills_lint: ^0.5.0 -``` - -Then: -```bash -dart pub get -``` - -#### Globally installed - -For multiple projects without per-project pubspec entries: -```bash -dart install dart_skills_lint -``` - -### 2. `install.sh` โ€” Linux + macOS, no Dart required - -The recommended path for CI runners and laptops without the Dart SDK -on PATH. Downloads the matching prebuilt binary from the latest GitHub -Release, verifies its SHA256, and installs to `/usr/local/bin` (with a -`sudo` fallback). Supports macOS arm64 + x64 and Linux x64 + arm64. - -```bash -curl -fsSL https://github.com/flutter/agent-plugins/releases/latest/download/install.sh | bash -``` - -Optional env vars (set before the `bash` part): -- `INSTALL_DIR` โ€” install destination (default `/usr/local/bin`). -- `VERSION` โ€” pin a specific release like `0.4.0` (default `latest`). -- `REPO` โ€” alternate source repo (default `flutter/agent-plugins`). - -#### macOS first-launch note - -macOS binaries are not yet code-signed. The first time you run the -binary, macOS Gatekeeper will block it ("cannot be opened because the -developer cannot be verified"). Remove the quarantine flag once: - -```bash -xattr -d com.apple.quarantine "$(which dart_skills_lint)" -``` - -This step goes away once notarized builds ship. - -### 3. Direct download โ€” Linux + macOS, no install script - -For environments where piping a script to `bash` isn't acceptable. -Grab the tarball for your platform from -[the latest GitHub Release](https://github.com/flutter/agent-plugins/releases/latest) -and verify its SHA256 against the release's `SHA256SUMS` asset. - -```bash -TARGET="linux-x64" # or: macos-arm64, macos-x64, linux-arm64 -VERSION="0.5.0" -BASE="https://github.com/flutter/agent-plugins/releases/download/dart_skills_lint-v${VERSION}" -curl -fsSLO "${BASE}/dart_skills_lint-${TARGET}.tar.gz" -curl -fsSLO "${BASE}/SHA256SUMS" -grep " dart_skills_lint-${TARGET}.tar.gz$" SHA256SUMS | sha256sum -c - -tar -xzf "dart_skills_lint-${TARGET}.tar.gz" -sudo install -m 0755 "dart_skills_lint-${TARGET}" /usr/local/bin/dart_skills_lint -``` - -On macOS, replace `sha256sum -c -` with `shasum -a 256 -c -`. - -## Usage - -`dart_skills_lint` runs as a command-line tool, configured by flags or by -a `dart_skills_lint.yaml` file. The CLI is the user-facing surface; it -also has a programmatic API for contributors who need to embed the -linter in their own test suite โ€” see -[`CONTRIBUTING.md`](CONTRIBUTING.md#embedding-the-linter-in-tests). - -### 1. As a Command Line Tool with Arguments -Run the linter against your skills or root skills directories by passing arguments. - -```bash -dart run dart_skills_lint --skills-directory ./path/to/skills-root -``` - -Multiple root directories can be specified: -```bash -dart run dart_skills_lint --skills-directory ./path/to/root-a --skills-directory ./path/to/root-b -``` - -Validate Individual Skills directly using `--skill` or `-s`: -```bash -dart run dart_skills_lint --skill ./path/to/my-single-skill -``` - -If no directory is specified, it automatically checks `.claude/skills` and `.agents/skills` relative to your workspace root. - -### Flags -- `-d`, `--skills-directory`: Specifies a root directory containing sub-folders of skills to validate. Can be passed multiple times. Can use home tilde expansion (ex: `~/.agents/skills`). -- `-s`, `--skill`: Specifies an individual skill directory to validate directly. Can be passed multiple times. -- `-q`, `--quiet`: Hide non-error validation output. -- `-w`, `--print-warnings`: Enable printing of warning messages. -- `--fast-fail`: Halt execution immediately on the error. -- `--ignore-config`: Ignore the YAML configuration file entirely. -- `--[no-]check-trailing-whitespace`: Enable/disable checking for trailing whitespace. (Disabled by default). -- `--fix`: Write fixes for failing lints to disk. -- `--dry-run`: When combined with `--fix`, prints the proposed diff without writing. -- `--fix-apply`: *Deprecated* alias for `--fix`. Prints a deprecation notice on use. - -### 2. As a Command Line Tool with a YAML Configuration File -You can configure the linter using a configuration file (defaulting to `dart_skills_lint.yaml` in the current directory). - -Create `dart_skills_lint.yaml` in the root of your repository: - -```yaml -# dart_skills_lint.yaml -dart_skills_lint: - rules: - check-relative-paths: error - check-absolute-paths: error - directories: - - path: "~/.agents/skills" - ignore_file: "~/.agents/skills/ignore.json" - individual_skills: - - path: "my_custom_standalone_skill" - rules: - missing_install_script: warning - ignore_file: "my_ignores.json" -``` - -Then you can simply run: -```bash -dart run dart_skills_lint -``` - -### Rule Precedence - -When resolving which severity and parameters 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 parameter overrides: Namespaced command-line parameter flags (e.g., `--path-does-not-exist-exclude=".*-workspace"`). Passing an empty string (e.g. `--path-does-not-exist-exclude=""`) explicitly clears the custom parameter. -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 parameters map completely overrides any global parameters 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 parameters map is inherited/preserved. -3. **Global Config**: Rules and parameters 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. - ---- - -### 3. Custom Rules - -Custom rule authoring lives in the -[`dart-skills-lint-validation`](skills/dart-skills-lint-validation/SKILL.md) -skill โ€” that skill walks through extending `SkillRule` and passing the -rule into the linter. - -## Specification Validation - -The linter checks each skill against the spec at -[`documentation/knowledge/SPECIFICATION.md`](documentation/knowledge/SPECIFICATION.md). -For the full list of built-in rules โ€” default severities, exact -diagnostic shapes, auto-fix behavior, and how to disable each โ€” see -[`RULES.md`](RULES.md). - -## Recipes - -Drop-in snippets for the two most common ways to wire `dart_skills_lint` -into a project's quality gates. Each recipe is exercised by -[`test/recipe_drift_test.dart`](test/recipe_drift_test.dart), so if a -flag here goes stale, CI fails. - -### Recipe: GitHub Actions - -Save the following as `.github/workflows/lint-skills.yml`. It runs on -every push and PR, installs `dart_skills_lint` globally on the runner, -and validates every skill under `.claude/skills/`. Adjust the path to -match where your skills live. - -```yaml -# .github/workflows/lint-skills.yml -name: Lint Agent Skills -on: - push: - branches: [main] - pull_request: - -permissions: read-all - -jobs: - lint-skills: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: dart-lang/setup-dart@v1 - - run: dart install dart_skills_lint - - run: dart_skills_lint --skills-directory ./.claude/skills -``` - -To validate a single skill directory instead, swap the last step: - -```yaml - - run: dart_skills_lint --skill ./.claude/skills/my-skill -``` - -### Recipe: Dart-native pre-commit hook - -A pre-commit hook that calls into the linter directly โ€” no Husky, no -Python `pre-commit` framework, just Dart and the existing -`dart install` tooling. - -Install the linter globally once per machine: - -```bash -dart install dart_skills_lint -``` - -Then install the hook into the repository (run from the repo root): - -```bash -cat > .git/hooks/pre-commit <<'HOOK' -#!/bin/sh -set -e -# Lint every skill under .claude/skills before each commit. -# Add --skill arguments for other locations as needed. -exec dart_skills_lint --skills-directory ./.claude/skills --quiet -HOOK -chmod +x .git/hooks/pre-commit -``` - -The hook exits non-zero on lint failure, blocking the commit. To -auto-apply fixable lints inside the hook, append `--fix` to the linter -invocation. - -### Recipe: have an agent set it up for you - -If you're using Claude Code, Gemini, or another agent that can read -repository-local skills, paste the following prompt to have the agent -install and validate `dart_skills_lint` for you. The agent will -follow the -[`dart-skills-lint-setup`](skills/dart-skills-lint-setup/SKILL.md) -skill for first-time wiring, then the -[`dart-skills-lint-validation`](skills/dart-skills-lint-validation/SKILL.md) -skill to run the linter and resolve any failures. - -> Set up dart_skills_lint in this project. Use the skill at -> `skills/dart-skills-lint-setup/SKILL.md` -> to add it as a dev_dependency, create the configuration file, -> and wire it into CI. Then use the skill at -> `skills/dart-skills-lint-validation/SKILL.md` -> to run the linter and resolve any failures. - -## Contributing - -Contributions are welcome! Please ensure that any PRs pass the linter themselves and align with the `documentation/knowledge/SPECIFICATION.md`. - diff --git a/tool/dart_skills_lint/RULES.md b/tool/dart_skills_lint/RULES.md deleted file mode 100644 index f9ae4d69..00000000 --- a/tool/dart_skills_lint/RULES.md +++ /dev/null @@ -1,187 +0,0 @@ -# Rules - -The full rule contract for `dart_skills_lint`. Every built-in rule listed -here is registered in -[`lib/src/rule_registry.dart`](lib/src/rule_registry.dart) and pinned to -this document by -[`test/rules_md_consistency_test.dart`](test/rules_md_consistency_test.dart). -If a rule is added, removed, renamed, or has its default severity / -fixability changed, both the registry **and** this file must be updated -in the same commit โ€” the consistency test fails otherwise. - -Severity vocabulary: - -- `error` โ€” failure exits 1 and blocks CI. -- `warning` โ€” printed but does not change exit code. -- `disabled` โ€” not run unless explicitly enabled via CLI flag or - `dart_skills_lint.yaml` `rules:` config. - -All rules are enabled / disabled / escalated the same three ways: - -- CLI: `--` (escalates to `error`), - `--no-` (disables). -- YAML config: `dart_skills_lint.rules.: error|warning|disabled`. -- Per-target YAML: `dart_skills_lint.directories[].rules.: ...` or `dart_skills_lint.individual_skills[].rules.: ...`. - -The "Disable" line under each rule below names the negated CLI flag for -quick reference. - -See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the SemVer policy that -governs how changes to these rules ship. - ---- - -## check-absolute-paths - -- **Default severity:** warning -- **Fixable:** yes -- **What it checks:** inline Markdown links in `SKILL.md` do not use - absolute filesystem paths (POSIX `/foo/bar` or Windows `C:\foo`). - Absolute paths break portability across machines. -- **Diagnostic shape:** - `Absolute filepath found in link: . Skills must use paths relative to SKILL.md so they remain portable across machines.` -- **Auto-fix behavior:** if the absolute path resolves to a file that - exists on disk, the fixer rewrites it to the equivalent POSIX-style - relative path from `SKILL.md`. If the target does not exist the - fixer leaves the link untouched. -- **Disable:** `--no-check-absolute-paths`. - -## check-relative-paths - -- **Default severity:** disabled -- **Fixable:** no -- **What it checks:** inline Markdown links in `SKILL.md` with - relative targets resolve to files that actually exist on disk. - Web URLs, anchors, `mailto:`, `javascript:`, and `data:` links are - skipped. -- **Diagnostic shape:** - `Linked file does not exist: (resolved to ). Did you mean ""?` - The `Did you mean` clause is only included when a near-miss file - is found in the same directory; it's scored by string similarity - against the missing basename. The suggestion preserves the link's - original directory prefix, normalized to forward slashes. -- **Auto-fix behavior:** none. The author is expected to pick the - intended target by hand. -- **Disable:** `--no-check-relative-paths` (also the default state). - -## check-trailing-whitespace - -- **Default severity:** disabled -- **Fixable:** yes -- **What it checks:** lines in `SKILL.md` do not have trailing - whitespace. Exactly two spaces are allowed as a CommonMark hard - line break; one space or three-or-more spaces, or any trailing tab, - is reported. -- **Diagnostic shape:** - `Line has trailing space(s). Only exactly 2 spaces are - allowed for line breaks.` - Trailing tabs report `Line has trailing whitespace containing - tabs.` instead. -- **Auto-fix behavior:** trims violating trailing whitespace from - each offending line. Lines with exactly two trailing spaces are - left alone. -- **Disable:** `--no-check-trailing-whitespace` (also the default - state). - -## description-too-long - -- **Default severity:** error -- **Fixable:** no -- **What it checks:** the YAML frontmatter `description:` field is - at most 1024 characters. -- **Diagnostic shape:** - `Description field is characters; maximum is 1024. Cutoff at - character 1024: ...<40 chars before>|HERE|<40 chars after>... (see - https://agentskills.io/specification#description-field)` - The `|HERE|` marker pins the exact cutoff point so the author can - see what slipped past the limit without having to count characters. -- **Auto-fix behavior:** none. The fix is editorial; the linter - refuses to silently truncate the author's prose. -- **Disable:** `--no-description-too-long`. - -## disallowed-field - -- **Default severity:** disabled -- **Fixable:** no -- **What it checks:** every key in the YAML frontmatter is one of the - spec-allowed fields: `name`, `description`, `license`, - `allowed-tools`, `metadata`, `compatibility`, `category`, `tags`, - `version`, `eval_task`. -- **Diagnostic shape:** - `Disallowed field: (see - https://agentskills.io/specification#frontmatter)` -- **Auto-fix behavior:** none. The fix is destructive (removing a - field) so it requires a human decision. -- **Disable:** `--no-disallowed-field` (also the default state). - -## prevent-skills-sh-publishing - -- **Default severity:** disabled -- **Fixable:** no -- **What it checks:** the YAML frontmatter contains `metadata:` with `internal: true`, which prevents the skill from being published to skills.sh. -- **Diagnostic shape:** - A multi-line message specifying the exact structural issue. It instructs the developer to remove quotes if `internal` is set to a string, or warns when `metadata` is missing, not a map, or when `internal` is not explicitly set to boolean `true`. Each diagnostic includes the expected schema: - ```yaml - metadata: - internal: true - ``` -- **Auto-fix behavior:** none. -- **Disable:** `--no-prevent-skills-sh-publishing` (also the default state). - -## invalid-skill-name - -- **Default severity:** error -- **Fixable:** yes -- **What it checks:** the frontmatter `name:` field is: - - lowercase - - 1โ€“64 characters - - only lowercase letters, digits, and hyphens - - has no leading, trailing, or consecutive hyphens - - exactly equal to the parent directory's name -- **Diagnostic shape:** each violation produces a separate error - message naming the frontmatter `name:` field explicitly, - quoting the offending value, and suggesting a normalized form - (e.g. `Frontmatter `name` "My_Skill" contains invalid characters. - Only lowercase letters, digits, and hyphens are allowed. - Suggested: "my-skill" (see - https://agentskills.io/specification#name-field)`). - The directory-mismatch error offers both fix directions (edit the - field or rename the directory). -- **Auto-fix behavior:** when the only violation is a directory - mismatch, the fixer rewrites the frontmatter `name:` value to - match the parent directory name. Other violations (invalid - characters, length, etc.) are not auto-fixed because the - normalization is a suggestion and the author may want a different - name entirely. -- **Disable:** `--no-invalid-skill-name`. - -## valid-yaml-metadata - -- **Default severity:** error -- **Fixable:** no -- **What it checks:** - - `SKILL.md` contains a YAML frontmatter block delimited by `---` - that parses without errors. - - Required fields `name` and `description` are both present. - - If `compatibility:` is present, it is at most 500 characters. -- **Diagnostic shape:** - - `Invalid YAML metadata: (see - https://agentskills.io/specification#frontmatter)` - - `Missing required field: (see ...)` - - `Compatibility field is characters; maximum is 500. Cutoff at character 500: ...|HERE|... (see https://agentskills.io/specification#compatibility-field)` - โ€” same shape as `description-too-long`, produced by the shared - `buildLengthDiagnostic` helper. -- **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/analysis_options.yaml b/tool/dart_skills_lint/analysis_options.yaml deleted file mode 100644 index 798b9c72..00000000 --- a/tool/dart_skills_lint/analysis_options.yaml +++ /dev/null @@ -1,268 +0,0 @@ -# Specify analysis options. -# -# For a list of lints, see: https://dart.dev/tools/linter-rules -# For guidelines on configuring static analysis, see: -# https://dart.dev/tools/analysis -# -# There are other similar analysis options files in the flutter repos, -# which should be kept in sync with this file: -# -# - analysis_options.yaml (this file) -# - https://github.com/flutter/packages/blob/main/analysis_options.yaml - -analyzer: - language: - strict-casts: true - strict-inference: true - strict-raw-types: true - errors: - # allow deprecated members (we do this because otherwise we have to annotate - # every member in every test, assert, etc, when we or the Dart SDK deprecates - # something (https://github.com/flutter/flutter/issues/143312) - deprecated_member_use: ignore - deprecated_member_use_from_same_package: ignore - exclude: - - "bin/cache/**" - # Ignore protoc generated files - - "dev/conductor/lib/proto/*" - - "engine/**" - -formatter: - page_width: 100 - -linter: - rules: - # This list is derived from the list of all available lints located at - # https://github.com/dart-lang/sdk/blob/main/pkg/linter/example/all.yaml - - always_declare_return_types - - always_put_control_body_on_new_line - # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 - # - always_specify_types # conflicts with omit_obvious_local_variable_types - # - always_use_package_imports # we do this commonly - - annotate_overrides - - annotate_redeclares - # - avoid_annotating_with_dynamic # conflicts with type_annotate_public_apis - - avoid_bool_literals_in_conditional_expressions - # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023 - # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/4998 - # - avoid_classes_with_only_static_members # we do this commonly for `abstract final class`es - - avoid_double_and_int_checks - - avoid_dynamic_calls - - avoid_empty_else - - avoid_equals_and_hash_code_on_mutable_classes - - avoid_escaping_inner_quotes - - avoid_field_initializers_in_const_classes - # - avoid_final_parameters # incompatible with prefer_final_parameters - - avoid_function_literals_in_foreach_calls - # - avoid_futureor_void # not yet tested - # - avoid_implementing_value_types # see https://github.com/dart-lang/linter/issues/4558 - - avoid_init_to_null - - avoid_js_rounded_ints - # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to - - avoid_null_checks_in_equality_operators - # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it - - avoid_print - # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) - - avoid_redundant_argument_values - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null_for_void - # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives - - avoid_setters_without_getters - - avoid_shadowing_type_parameters - - avoid_single_cascade_in_expression_statements - - avoid_slow_async_io - - avoid_type_to_string - - avoid_types_as_parameter_names - # - avoid_types_on_closure_parameters # not yet tested - - avoid_unnecessary_containers - - avoid_unused_constructor_parameters - - avoid_void_async - # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere - - await_only_futures - - camel_case_extensions - - camel_case_types - - cancel_subscriptions - # - cascade_invocations # doesn't match the typical style of this repo - - cast_nullable_to_non_nullable - # - close_sinks # not reliable enough - - collection_methods_unrelated_type - - combinators_ordering - # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 - - conditional_uri_does_not_exist - # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 - - control_flow_in_finally - - curly_braces_in_flow_control_structures - - dangling_library_doc_comments - - depend_on_referenced_packages - - deprecated_consistency - # - deprecated_member_use_from_same_package # we allow self-references to deprecated members - # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) - - directives_ordering - # - discarded_futures # too many false positives, similar to unawaited_futures - # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic - # - document_ignores # not yet tested - - empty_catches - - empty_constructor_bodies - - empty_statements - - eol_at_end_of_file - - exhaustive_cases - - file_names - - flutter_style_todos - - hash_and_equals - - implementation_imports - - implicit_call_tearoffs - - implicit_reopen - - invalid_case_patterns - - invalid_runtime_check_with_js_interop_types - # - join_return_with_assignment # not required by flutter style - - leading_newlines_in_multiline_strings - - library_annotations - - library_names - - library_prefixes - - library_private_types_in_public_api - # - lines_longer_than_80_chars # not required by flutter style - - literal_only_boolean_expressions - # - matching_super_parameters # blocked on https://github.com/dart-lang/language/issues/2509 - - missing_code_block_language_in_doc_comment - - missing_whitespace_between_adjacent_strings - - no_adjacent_strings_in_list - - no_default_cases - - no_duplicate_case_values - - no_leading_underscores_for_library_prefixes - - no_leading_underscores_for_local_identifiers - - no_literal_bool_comparisons - - no_logic_in_create_state - # - no_runtimeType_toString # ok in tests; we enable this only in packages/ - - no_self_assignments - - no_wildcard_variable_uses - - non_constant_identifier_names - - noop_primitive_operations - - null_check_on_nullable_type_parameter - - null_closures - # - omit_local_variable_types # superset of omit_obvious_local_variable_types - - omit_obvious_local_variable_types # not yet tested - # - omit_obvious_property_types # conflicts with type_annotate_public_apis - # - one_member_abstracts # too many false positives - - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al - - overridden_fields - - package_names - - package_prefixed_library_names - # - parameter_assignments # we do this commonly - - prefer_adjacent_string_concatenation - - prefer_asserts_in_initializer_lists - # - prefer_asserts_with_message # not required by flutter style - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_const_constructors_in_immutables - - prefer_const_declarations - - prefer_const_literals_to_create_immutables - # - prefer_constructors_over_static_methods # far too many false positives - - prefer_contains - # - prefer_double_quotes # opposite of prefer_single_quotes - # - prefer_expression_function_bodies # conflicts with ./docs/contributing/Style-guide-for-Flutter-repo.md#consider-using--for-short-functions-and-methods - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - # - prefer_final_parameters # adds too much verbosity - - prefer_for_elements_to_map_fromIterable - - prefer_foreach - - prefer_function_declarations_over_variables - - prefer_generic_function_type_aliases - - prefer_if_elements_to_conditional_expressions - - prefer_if_null_operators - - prefer_initializing_formals - - prefer_inlined_adds - # - prefer_int_literals # conflicts with ./docs/contributing/Style-guide-for-Flutter-repo.md#use-double-literals-for-double-constants - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_is_not_operator - - prefer_iterable_whereType - - prefer_mixin - # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere - - prefer_null_aware_operators - - prefer_relative_imports - - prefer_single_quotes - - prefer_spread_collections - - prefer_typing_uninitialized_variables - - prefer_void_to_null - - provide_deprecation_message - # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml - - recursive_getters - # - require_trailing_commas # would be nice, but requires a lot of manual work: 10,000+ code locations would need to be reformatted by hand after bulk fix is applied - - secure_pubspec_urls - - sized_box_for_whitespace - - sized_box_shrink_expand - - slash_for_doc_comments - - sort_child_properties_last - - sort_constructors_first - # - sort_pub_dependencies # prevents separating pinned transitive dependencies - - sort_unnamed_constructors_first - - specify_nonobvious_local_variable_types - - specify_nonobvious_property_types - - strict_top_level_inference - - test_types_in_equals - - throw_in_finally - - tighten_type_of_initializing_formals - - type_annotate_public_apis - - type_init_formals - - type_literal_in_constant_pattern - # - unawaited_futures # too many false positives, especially with the way AnimationController works - # - unintended_html_in_doc_comment # blocked on https://github.com/dart-lang/linter/issues/5065 - # - unnecessary_async # not yet tested - - unnecessary_await_in_return - - unnecessary_brace_in_string_interps - - unnecessary_breaks - - unnecessary_const - - unnecessary_constructor_name - # - unnecessary_final # conflicts with prefer_final_locals - - unnecessary_getters_setters - # - unnecessary_ignore # Disabled by default to simplify migrations; should be periodically enabled locally to clean up offenders - # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 - - unnecessary_late - - unnecessary_library_directive - # - unnecessary_library_name # blocked on https://github.com/dart-lang/dartdoc/issues/3882 - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_null_aware_operator_on_extension_on_nullable - - unnecessary_null_checks - - unnecessary_null_in_if_null_operators - - unnecessary_nullable_for_final_variable_declarations - - unnecessary_overrides - - unnecessary_parenthesis - # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint - - unnecessary_statements - - unnecessary_string_escapes - - unnecessary_string_interpolations - - unnecessary_this - - unnecessary_to_list_in_spreads - - unnecessary_underscores - - unreachable_from_main - - unrelated_type_equality_checks - # - unsafe_variance # not yet tested - - use_build_context_synchronously - - use_colored_box - # - use_decorated_box # leads to bugs: DecoratedBox and Container are not equivalent (Container inserts extra padding) - - use_enums - - use_full_hex_values_for_flutter_colors - - use_function_type_syntax_for_parameters - - use_if_null_to_convert_nulls_to_bools - - use_is_even_rather_than_modulo - - use_key_in_widget_constructors - - use_late_for_private_fields_and_variables - - use_named_constants - - use_raw_strings - - use_rethrow_when_possible - - use_setters_to_change_properties - # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 - - use_string_in_part_of_directives - - use_super_parameters - - use_test_throws_matchers - # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review - - use_truncating_division - - valid_regexps - - void_checks - diff --git a/tool/dart_skills_lint/bench/README.md b/tool/dart_skills_lint/bench/README.md deleted file mode 100644 index 8c9fd479..00000000 --- a/tool/dart_skills_lint/bench/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Performance benchmarks - -When changing the validation loop, baseline I/O, or path-normalization code, -run the throughput benchmark to make sure you haven't regressed: - -```bash -dart run bench/baseline_throughput.dart -``` - -This generates synthetic skills at multiple sizes, runs them through -`validateSkills` with `--generate-baseline`, and prints a wall-clock table. -The benchmark is intentionally not run in CI โ€” wall-clock on hosted runners -is too noisy to enforce. Use it locally and compare your branch's table -against `main` before submitting changes. - -## Options - -``` ---sizes Comma-separated list of N values (default: 10,100,1000) ---errors-per-skill Baseline-recordable errors per synthetic skill, 1-3 (default: 1) ---runs Timed runs per cell (default: 3) ---warmup Untimed warmup runs (default: 1) -``` diff --git a/tool/dart_skills_lint/bench/baseline_throughput.dart b/tool/dart_skills_lint/bench/baseline_throughput.dart deleted file mode 100644 index 0563e85c..00000000 --- a/tool/dart_skills_lint/bench/baseline_throughput.dart +++ /dev/null @@ -1,230 +0,0 @@ -// 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. - -/// Baseline-generation throughput benchmark for `dart_skills_lint`. -/// -/// Generates synthetic skill directories at multiple sizes, runs them through -/// `validateSkills` with `--generate-baseline`, and prints a wall-clock table. -/// Intentionally not run in CI โ€” see `bench/README.md`. -library; - -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:dart_skills_lint/dart_skills_lint.dart'; -import 'package:path/path.dart' as p; - -const String _sizesFlag = 'sizes'; -const String _errorsPerSkillFlag = 'errors-per-skill'; -const String _runsFlag = 'runs'; -const String _warmupFlag = 'warmup'; -const String _helpFlag = 'help'; - -Future main(List args) async { - final parser = ArgParser() - ..addOption( - _sizesFlag, - defaultsTo: '10,100,1000', - help: 'Comma-separated list of N values (number of synthetic skills) to benchmark.', - ) - ..addOption( - _errorsPerSkillFlag, - defaultsTo: '1', - help: 'Number of baseline-recordable errors each synthetic skill should produce (1-3).', - ) - ..addOption(_runsFlag, defaultsTo: '3', help: 'Number of timed runs per cell.') - ..addOption(_warmupFlag, defaultsTo: '1', help: 'Number of untimed warmup runs before timing.') - ..addFlag(_helpFlag, abbr: 'h', negatable: false, help: 'Show usage information.'); - - try { - final ArgResults results = parser.parse(args); - - if (results[_helpFlag] as bool) { - stdout.writeln('Usage: dart run bench/baseline_throughput.dart [options]'); - stdout.writeln(parser.usage); - return; - } - - final List sizes = _parseSizes(results[_sizesFlag] as String); - final int errorsPerSkill = _clampErrorsPerSkill( - _parsePositiveInt(results[_errorsPerSkillFlag] as String, _errorsPerSkillFlag), - ); - final int runs = _parsePositiveInt(results[_runsFlag] as String, _runsFlag); - final int warmup = _parseNonNegativeInt(results[_warmupFlag] as String, _warmupFlag); - - final rows = <_BenchResult>[]; - for (final n in sizes) { - final _BenchResult row = await _benchSize( - n: n, - errorsPerSkill: errorsPerSkill, - runs: runs, - warmup: warmup, - ); - rows.add(row); - } - - _printTable(rows); - } on FormatException catch (e) { - stderr.writeln('Error: ${e.message}'); - stderr.writeln(parser.usage); - } -} - -int _parsePositiveInt(String raw, String flag) { - final int value = - int.tryParse(raw) ?? (throw FormatException('--$flag must be an integer (got "$raw").')); - if (value < 1) { - throw FormatException('--$flag must be >= 1 (got $value).'); - } - return value; -} - -int _parseNonNegativeInt(String raw, String flag) { - final int value = - int.tryParse(raw) ?? (throw FormatException('--$flag must be an integer (got "$raw").')); - if (value < 0) { - throw FormatException('--$flag must be >= 0 (got $value).'); - } - return value; -} - -List _parseSizes(String raw) { - final sizes = []; - for (final String token in raw.split(',').map((String s) => s.trim())) { - if (token.isEmpty) { - continue; - } - final int? n = int.tryParse(token); - if (n == null || n < 1) { - throw FormatException('--$_sizesFlag entries must be positive integers (got "$token").'); - } - sizes.add(n); - } - if (sizes.isEmpty) { - throw const FormatException('--sizes must contain at least one positive integer.'); - } - return sizes; -} - -int _clampErrorsPerSkill(int requested) { - const maxSupported = 3; - if (requested < 1) { - stderr.writeln('errors-per-skill must be >= 1; using 1.'); - return 1; - } - if (requested > maxSupported) { - stderr.writeln( - 'errors-per-skill > $maxSupported is not supported (only $maxSupported distinct ' - 'baseline-recordable rules trigger per skill); clamping to $maxSupported.', - ); - return maxSupported; - } - return requested; -} - -Future<_BenchResult> _benchSize({ - required int n, - required int errorsPerSkill, - required int runs, - required int warmup, -}) async { - final Directory tempDir = Directory.systemTemp.createTempSync('dskl_bench_'); - try { - final skillsRoot = Directory(p.join(tempDir.path, 'skills'))..createSync(); - for (var i = 0; i < n; i++) { - _writeSyntheticSkill(skillsRoot, i, errorsPerSkill); - } - final String ignorePath = p.join(tempDir.path, 'ignore.json'); - - for (var i = 0; i < warmup; i++) { - await _runOnce(skillsRoot.path, ignorePath); - } - - final samples = []; - for (var i = 0; i < runs; i++) { - final int ms = await _runOnce(skillsRoot.path, ignorePath); - samples.add(ms); - } - - samples.sort(); - final int min = samples.first; - final int max = samples.last; - final int median = samples[samples.length ~/ 2]; - return (n: n, minMs: min, medianMs: median, maxMs: max, runs: runs); - } finally { - if (tempDir.existsSync()) { - tempDir.deleteSync(recursive: true); - } - } -} - -Future _runOnce(String skillsRootPath, String ignorePath) async { - final ignoreFile = File(ignorePath); - if (ignoreFile.existsSync()) { - ignoreFile.deleteSync(); - } - final sw = Stopwatch()..start(); - await validateSkills( - skillDirPaths: [skillsRootPath], - generateBaseline: true, - quiet: true, - ignoreFileOverride: ignorePath, - ); - sw.stop(); - return sw.elapsedMilliseconds; -} - -void _writeSyntheticSkill(Directory skillsRoot, int index, int errorsPerSkill) { - final dirName = 'skill-$index'; - final skillDir = Directory(p.join(skillsRoot.path, dirName))..createSync(); - - // Error 1: name mismatch โ€” `name:` does not match the directory name. - // This always triggers `invalid-skill-name`. - const name = 'wrong-name-on-purpose'; - - // Error 2 (when errorsPerSkill >= 2): description longer than 1024 chars - // triggers `description-too-long`. - final String description = errorsPerSkill >= 2 - ? 'x' * 1100 - : 'Synthetic skill for benchmarking; ' - 'the yaml name does not match the directory name ' - 'so the linter records a name-format error.'; - - // Error 3 (when errorsPerSkill >= 3): an absolute-path link in the body - // triggers `check-absolute-paths` (warning, but baseline-recordable). Using - // `p.absolute(...)` guarantees the link is absolute on the host OS regardless - // of platform (POSIX or Windows). - final body = errorsPerSkill >= 3 - ? '# Test skill\n\n[abs](${p.absolute('synthetic-abs-path')})\n' - : '# Test skill\n'; - - final sb = StringBuffer() - ..writeln('---') - ..writeln('name: $name') - ..writeln('description: $description') - ..writeln('---') - ..writeln() - ..write(body); - - File(p.join(skillDir.path, 'SKILL.md')).writeAsStringSync(sb.toString()); -} - -void _printTable(List<_BenchResult> rows) { - stdout.writeln('N | min | median | max | runs'); - stdout.writeln('-------|-------|--------|-------|-----'); - for (final row in rows) { - stdout.writeln( - '${_padRight(row.n.toString(), 6)} ' - '| ${_padLeft('${row.minMs}ms', 5)} ' - '| ${_padLeft('${row.medianMs}ms', 6)} ' - '| ${_padLeft('${row.maxMs}ms', 5)} ' - '| ${row.runs}', - ); - } -} - -String _padRight(String s, int width) => s.padRight(width); -String _padLeft(String s, int width) => s.padLeft(width); - -typedef _BenchResult = ({int n, int minMs, int medianMs, int maxMs, int runs}); diff --git a/tool/dart_skills_lint/bin/cli.dart b/tool/dart_skills_lint/bin/cli.dart deleted file mode 100644 index d519944f..00000000 --- a/tool/dart_skills_lint/bin/cli.dart +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env dart - -// 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:dart_skills_lint/src/entry_point.dart'; - -Future main(List arguments) async { - await runApp(arguments); -} diff --git a/tool/dart_skills_lint/dart_skills_lint.yaml b/tool/dart_skills_lint/dart_skills_lint.yaml deleted file mode 100644 index bcee3ef4..00000000 --- a/tool/dart_skills_lint/dart_skills_lint.yaml +++ /dev/null @@ -1,44 +0,0 @@ -dart_skills_lint: - rules: - check-relative-paths: error - check-absolute-paths: error - directories: - - 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" - - path: "skills" - rules: - check-trailing-whitespace: error - prevent-skills-sh-publishing: error - - path: "example/skills/invalid" - rules: - prevent-skills-sh-publishing: error - - path: "example/skills/valid" - rules: - prevent-skills-sh-publishing: error - individual_skills: - - path: ".agents/skills/add-dart-lint-validation-rule" - rules: - prevent-skills-sh-publishing: error - - path: ".agents/skills/check-downstream-consumers" - rules: - prevent-skills-sh-publishing: error - - path: ".agents/skills/dart-skills-lint-integration" - rules: - prevent-skills-sh-publishing: error - - path: ".agents/skills/definition-of-done" - rules: - prevent-skills-sh-publishing: error - - path: ".agents/skills/run-evals" - rules: - prevent-skills-sh-publishing: error - - path: ".agents/skills/contributor-pr-description" - rules: - prevent-skills-sh-publishing: error - diff --git a/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md b/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md deleted file mode 100644 index 1e45ce9b..00000000 --- a/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md +++ /dev/null @@ -1,79 +0,0 @@ -# Agent Skills Specification - -This document defines the technical requirements and architectural priorities for implementing Agent Skills. It serves as a self-contained reference for engineers building or integrating skills into AI agent environments. - -## 1. Overview -An **Agent Skill** is a portable, self-contained directory that extends an AI agent's capabilities. It provides the agent with specific instructions, tools, and domain-specific knowledge required to perform a specialized task. - -## 2. Directory Structure -A skill directory must follow a flat and predictable structure. The only mandatory file is `SKILL.md` at the root. - -```text -skill-name/ -โ”œโ”€โ”€ SKILL.md # Required: Metadata + Instructions -โ”œโ”€โ”€ scripts/ # Optional: Executable code (Python, Bash, JS, etc.) -โ”œโ”€โ”€ references/ # Optional: Deep-dive documentation and templates -โ””โ”€โ”€ assets/ # Optional: Static resources (images, schemas, etc.) -``` - -## 3. The `SKILL.md` File -The `SKILL.md` file uses YAML frontmatter for machine-readable metadata, followed by Markdown-formatted instructions for the agent. - -### 3.1 Metadata (YAML Frontmatter) -| Field | Required | Constraints | -| :--- | :--- | :--- | -| `name` | Yes | 1-64 chars; lowercase alphanumeric and hyphens (`-`) only; no leading/trailing/consecutive hyphens. **Must match the parent directory name.** | -| `description` | Yes | 1-1024 chars. A concise summary used by agents to determine when to activate the skill. | -| `license` | No | Short name (e.g., MIT, Apache-2.0) or reference to a bundled license file. | -| `compatibility` | No | 1-500 chars; specifies environment requirements (e.g., `Requires Python 3.10+`, `Node.js 18`). | -| `metadata` | No | Arbitrary key-value mapping for client-specific properties (e.g., `version`, `author`). | -| `allowed-tools` | No | (Experimental) Space-delimited list of pre-approved tools (e.g., `Bash(git:*)`). | - -### 3.2 Instructions (Markdown Body) -The body should contain the "expert knowledge" for the agent. -- **Referencing:** Use relative paths to files within the skill directory (e.g., `[See technical details](references/DETAILS.md)`). - - -## 4. Implementation Requirements - -### 4.1 Validation -Validation ensures that a skill directory and its `SKILL.md` file adhere to the specification. A linter or validator must check the following rules: - -#### 4.1.1 Directory and File Structure -- **Existence**: The target path must exist and be a directory. -- **Mandatory File**: The root directory must contain a `SKILL.md` file. - -#### 4.1.2 Metadata (YAML Frontmatter) -- **YAML Integrity**: The frontmatter must be valid YAML. -- **Allowed Fields**: Only the following fields are allowed: `name`, `description`, `license`, `allowed-tools`, `metadata`, `compatibility`. -- **Required Fields**: `name` and `description` are mandatory. - -#### 4.1.3 Field Specific Constraints -- **Skill Name (`name`)**: - - Must be lowercase. - - Length: Maximum 64 characters. - - Characters: Only lowercase letters, digits, and hyphens (`-`). - - No leading or trailing hyphens. - - No consecutive hyphens (`--`). - - **Directory Name Match**: The skill `name` must exactly match the name of its parent directory. -- **Description (`description`)**: - - Length: Maximum 1024 characters. -- **Compatibility (`compatibility`)**: - - Length: Maximum 500 characters. - -#### 4.1.4 Content Constraints -- **Trailing Whitespace**: Lines in `SKILL.md` should not have trailing whitespace. Exactly 2 spaces at the end of a line are allowed to support Markdown hard line breaks, per the [CommonMark Spec](https://spec.commonmark.org/0.31.2/#hard-line-breaks). -- **Path Constraints**: **Inline** Markdown links must not use absolute paths to enforce portability. Can optionally be configured to check that relative paths point to valid, existing files (disabled by default). *Note: Validation only applies to inline Markdown links; HTML and reference-style links are not supported.* - -## 5. Scripts & Tools -- Scripts in the `scripts/` directory should be self-documenting and provide clear error messages. - -## 6. Versioning -- Use the `metadata` field in `SKILL.md` to track versions: - ```yaml - metadata: - version: "1.0.0" - ``` - -## 7. Best Practices -- **Avoid Deep Nesting:** Keep the directory structure as flat as possible. References should ideally be only one level deep from the root. diff --git a/tool/dart_skills_lint/documentation/knowledge/architecture_overview.md b/tool/dart_skills_lint/documentation/knowledge/architecture_overview.md deleted file mode 100644 index fdf8ea41..00000000 --- a/tool/dart_skills_lint/documentation/knowledge/architecture_overview.md +++ /dev/null @@ -1,64 +0,0 @@ -# Architecture Overview: Agent Skills Linter (`dart_skills_lint`) - -This document provides a high-level architectural overview of the `dart_skills_lint` codebase. It outlines the project's components, execution flow, and design patterns used to validate Agent Skill specifications. - -## ๐Ÿงฑ Key Components - -The codebase is organized into standard Dart package layers, separating CLI handling, validation logic, and data models. - -### ๐Ÿš— 1. CLI Entry Point (`lib/src/entry_point.dart`) -The `entry_point.dart` file serves as the command-line interface. -- **Argument Parsing:** Uses standard `package:args` to parse input flags (`--skills-directory`, `--generate-baseline`, rules toggles, etc.). -- **Workspace Discovery:** Resolves target folders by searching standard locations (e.g., `.agents/skills`, `.claude/skills`) or parsing user arguments. -- **Ignore System Integration:** Handlers for baseline ignore files (via `--ignore-file` or `dart_skills_lint.yaml`) to filter out known failures without failing the build. -- **Log Management:** Consumes validation reports and standardizes format for terminal display (stdout vs stderr). - -### โš™๏ธ 2. Configuration Parser (`lib/src/config_parser.dart`) -- Loads user-defined custom settings from `dart_skills_lint.yaml` if it exists. -- Maps directory-specific rules overrides and toggles severity defaults. - -### ๐Ÿ›ก๏ธ 3. Validation Engine (`lib/src/validator.dart`) -The core motor of the package. -- Scans `SKILL.md` using regular expressions (`dotAll: true`) to extract Frontmatter. -- Delegates to sub-routines for checking: - - **Directory structure:** Correct place and flat tree constraints. - - **Field constraints:** Descriptions vs name match. - - **Relational properties:** Verifies relative links resolve correctly on disk. -- Outputs `ValidationResult` objects wrapping aggregates of `ValidationError`. - -### ๐Ÿ“œ 4. Predefined Rules templates (`lib/src/rule_registry.dart` and `lib/src/rules/`) -Contains global definitions for standard checks. Uses standard types (`CheckType`) allowing toggling and severity states. - -### ๐Ÿ“ฆ 5. Core Data Models (`lib/src/models/`) -- **`ValidationError`:** Complex error object recording rule IDs, messages, severity contexts, and ignore statuses. -- **`CheckType`:** A schema binding check descriptions to severity settings. -- **`IgnoreEntry`:** Structure for serializing/deserializing file suppressions to/from JSON. - ---- - -## โณ Execution Lifecycle - -A typical run of the linter follows this sequential graph: - -```mermaid -graph TD - Start([CLI - runApp]) --> ArgParse[Parse ARGs & Load config.yaml] - ArgParse --> Discover[Resolve Target Directories] - Discover --> ValidatorLoad[Instantiate Validator w/ Settings] - ValidatorLoad --> DirLoop[Iterate Directories] - DirLoop --> Validation[Run validator.validate] - Validation --> ParseFront[YAML Frontmatter Parsing] - ParseFront --> RuleChecks[Run Field + Path Checks] - RuleChecks --> ResultAggregate[Compile ValidationError List] - ResultAggregate --> IgnoreFilter[Apply baseline ignore file filters] - IgnoreFilter --> LogOutput[Print to console & Set exitCode] - LogOutput --> Next{Next Dir?} - Next -- Yes --> DirLoop - Next -- No --> Finish([Exit Term]) -``` - -## ๐Ÿง  Design Principles - -- **Separation of Concerns:** CLI runners are isolated from pure validation units. The `Validator` takes objects and doesn't know about `ArgResults`. -- **Stateless Configuration vs Overrides:** When running against a workspace with specific subdirectory traits, the `Validator` instances are re-instantiated with isolated context bindings so properties do not leak. -- **Context Preservation:** Standardizing lint exceptions (Ignore Systems) as structural objects rather than ad-hoc string matches prevent brittle regressions. diff --git a/tool/dart_skills_lint/evals/README.md b/tool/dart_skills_lint/evals/README.md deleted file mode 100644 index d55dc479..00000000 --- a/tool/dart_skills_lint/evals/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Skill Evaluations - -Architecture, rubrics, and instructions for evaluating AI agent skills authored and maintained in this repository. -**Note:** These evaluations are essentially unit tests for the skills within the `dart_skills_lint` package and its internal ecosystem. They are *not* intended to be a generic evaluation framework for other agent client plugins or tools outside of this specific domain. - -## What Should (and Shouldn't) Be Evaluated - -**DO Evaluate:** -- Core workflows of a skill (e.g., adding a dependency, running validation checks). -- Specific edge cases that a skill claims to handle (e.g., legacy integration paths without `--fix`). -- Whether a skill correctly leaves the repository in a compilable, passing state. - -**DO NOT Evaluate:** -- Trivial syntax formatting that `dart format` already fixes perfectly. -- Complete system architectures that take longer than a few minutes to generate and verify. -- Skills that are outside the scope of `dart_skills_lint` (e.g. general flutter app creation). - -## Core Principles & Architecture - -Evaluations in this repository use a **Two-Tiered Architecture** to separate domain-specific skill requirements from universal skill quality standards. - -### 1. Per-Skill Evals (`/evals/evals.json`) -Each skill maintains an `evals/evals.json` file containing target task prompts and expectations: -- **`prompt`**: Realistic user prompt testing primary or edge-case workflows. -- **`expected_chat_output`**: High-level narrative summary of what the LLM should say/give to the user. -- **`expected_repo_state`**: Array of discrete, testable assertions regarding the end state of the repository and tracked files. -- **`repo_criteria`**: Array of relative file paths to shared universal quality rubrics (e.g., `["evals/code_quality_rubric.json"]`). -- **`agent_config`**: The model configuration/harness used when executing the eval against the skill. For published skills, use `"bare-agent"`. For internal contributor skills, use the internal agent profile (e.g., `"reidbaker-agent"`). - -### 2. Cross-Skill Evals (`evals/*_rubric.json`) -Universal skill quality expectations are structured into modular rubric classes that apply broadly across skills. - -## Cross-Cutting Rules -Skills that author or modify code MUST adhere to the universal code quality expectations defined in `code_quality_rubric.json`. This ensures that generated code compiles cleanly, adheres to Effective Dart, works across platforms, and is placed in standard canonical directories. - -## ๐Ÿš€ Running & Validating Evals Locally - -### 1. Validate Evals Structural Consistency -Run the unit test that checks all `evals.json` files for structural consistency across the repository: - -```bash -dart test test/skills_evals_test.dart -``` - -### 2. Running Evals via Agent Orchestration -You should use the `/run-evals` skill to run evaluations. The bulk of the execution logic and prompts are located within the `run-evals` skill itself (`.agents/skills/run-evals/SKILL.md`). The environment, model, and harness are determined by the `agent_config` specified in the corresponding `evals.json` file. - -### 3. Testing Meta-Evals (Testing the Rubrics) -To ensure our universal rubrics correctly catch anti-patterns (and permit clean code), we use meta-evaluations. Standalone cross-skill evaluations are defined as `evals/*_evals.json` files (e.g., `evals/code_quality_rubric_evals.json`). These files contain evals strictly intended to grade static fixtures located in `evals/test_data/`. - -To run the meta-evals and verify the rubrics, invoke the `/run-evals` skill and ask the agent to run the standalone `code_quality_rubric_evals.json` file. diff --git a/tool/dart_skills_lint/evals/code_quality_rubric.json b/tool/dart_skills_lint/evals/code_quality_rubric.json deleted file mode 100644 index 02d21e6f..00000000 --- a/tool/dart_skills_lint/evals/code_quality_rubric.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "evals": [ - { - "id": "compilation_and_health", - "expected_repo_state": [ - "The generated code compiles with zero syntax errors.", - "Static analysis passes cleanly ('dart analyze --fatal-infos' returns zero errors or warnings).", - "Existing and newly added unit, widget, or integration tests execute and pass successfully." - ] - }, - { - "id": "effective_dart_and_idioms", - "expected_repo_state": [ - "Follows Effective Dart style guidelines as enforced by standard 'core' and 'recommended' linter rules.", - "Leverages Dart 3 idioms such as switch expressions and exhaustive pattern matching when conditional logic branches on algebraic types.", - "Avoids anti-patterns such as unnecessary dynamic types, raw Strings instead of string interpolation, or unhandled Futures." - ] - }, - { - "id": "cross_platform_compatibility", - "expected_repo_state": [ - "File paths and CLI path parameters use platform-agnostic formatters (package:path or forward slashes '/').", - "CLI commands and shell invocation scripts use cross-platform compatible syntax.", - "Avoids hardcoded platform-specific environment paths (such as C:\\ or /usr/local/bin directly)." - ] - }, - { - "id": "directory_and_placement_hygiene", - "expected_repo_state": [ - "Source code must be placed in 'lib/', tests in 'test/', and executable scripts in 'bin/' or 'tool/'.", - "No temporary scratch files or orphaned directories are left behind after execution." - ] - }, - { - "id": "lint_cheating", - "expected_repo_state": [ - "No lint configurations (e.g., analysis_options.yaml or its rules) are removed or disabled.", - "No file-level Dart ignores (e.g., // ignore_for_file:) are added to any Dart files." - ] - } - ] -} \ No newline at end of file diff --git a/tool/dart_skills_lint/evals/code_quality_rubric_evals.json b/tool/dart_skills_lint/evals/code_quality_rubric_evals.json deleted file mode 100644 index 60fbab09..00000000 --- a/tool/dart_skills_lint/evals/code_quality_rubric_evals.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "repo_criteria": [ - "evals/code_quality_rubric.json" - ], - "evals": [ - { - "id": 1, - "prompt": "Grade the code in 'evals/test_data/bad_code/tmp/bad_script.dart' using the code_quality_rubric.json.", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "The evaluation artifact explicitly flags a failure for directory placement hygiene.", - "The evaluation artifact explicitly flags a failure for effective Dart idioms.", - "The evaluation artifact explicitly flags a failure for cross platform compatibility.", - "The evaluation artifact explicitly flags a failure for lint cheating." - ], - "agent_config": "reidbaker-agent" - }, - { - "id": 2, - "prompt": "Grade the code in 'evals/test_data/ok_code/bin/good_script.dart' using the code_quality_rubric.json.", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "The evaluation artifact explicitly flags that all code quality criteria passed successfully without any failures." - ], - "agent_config": "reidbaker-agent" - } - ] -} diff --git a/tool/dart_skills_lint/evals/test_data/bad_code/tmp/bad_script.dart b/tool/dart_skills_lint/evals/test_data/bad_code/tmp/bad_script.dart deleted file mode 100644 index ffc0a5ae..00000000 --- a/tool/dart_skills_lint/evals/test_data/bad_code/tmp/bad_script.dart +++ /dev/null @@ -1,19 +0,0 @@ -// 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. - -// ignore_for_file: unused_local_variable, prefer_final_locals, avoid_print, use_raw_strings - -// Violates placement hygiene (in tmp/ instead of bin/ or lib/) -// Violates cross-platform compatibility (hardcoded Windows path) -// Violates effective Dart idioms (raw strings instead of interpolation) - -void main() { - var user = 'Test'; - - // Anti-pattern: Raw string concatenation instead of interpolation - print(r'Hello ' + user); - - // Anti-pattern: Hardcoded platform specific path - var path = 'C:\\my\\path\\data.txt'; -} diff --git a/tool/dart_skills_lint/evals/test_data/ok_code/bin/good_script.dart b/tool/dart_skills_lint/evals/test_data/ok_code/bin/good_script.dart deleted file mode 100644 index 41eccf4a..00000000 --- a/tool/dart_skills_lint/evals/test_data/ok_code/bin/good_script.dart +++ /dev/null @@ -1,19 +0,0 @@ -// 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:path/path.dart' as p; - -void main() { - const user = 'Test'; - - // Interpolation (clean Dart idiom) - const message = 'Hello $user'; - - // Cross platform path - final String path = p.join('my', 'path', 'data.txt'); - - if (message.isEmpty || path.isEmpty) { - throw Exception('Validation failed'); - } -} diff --git a/tool/dart_skills_lint/example/README.md b/tool/dart_skills_lint/example/README.md deleted file mode 100644 index 13a22606..00000000 --- a/tool/dart_skills_lint/example/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# dart_skills_lint examples - -Two reference fixtures live in this directory: - -| Fixture | Expected outcome | -| --- | --- | -| [`valid/`](skills/valid/SKILL.md) | All rules pass; the CLI exits 0. | -| [`invalid/`](skills/invalid/SKILL.md) | Multiple rules fail; the CLI exits 1. | - -Use them to take the linter for a spin without writing your own skill -first, and to see exactly what real diagnostic output looks like. - -## Run the valid fixture - -```bash -dart run dart_skills_lint --skill ./example/skills/valid -``` - -You should see: - -``` -Evaluating directory: example/skills/valid ---- Validating skill: valid --- - Skill is valid. -``` - -Exit code: `0`. - -## Run the invalid fixture - -With default rule severities, only `invalid-skill-name` fires (the other -two violations are below their default threshold): - -```bash -dart run dart_skills_lint --skill ./example/skills/invalid -``` - -Exit code: `1`. To see every violation surface as an error, escalate the -other two rules with explicit flags: - -```bash -dart run dart_skills_lint --skill ./example/skills/invalid \ - --disallowed-field --check-absolute-paths -``` - -Three rules now report failures: - -- `invalid-skill-name` โ€” names the offending frontmatter value, calls - out the directory mismatch, and suggests a corrected form. -- `disallowed-field` โ€” names the unknown field (`secret_field`) and - links to the spec's allowed-field list. -- `check-absolute-paths` โ€” flags the `/tmp/...` link as non-portable - and links to the spec section on relative paths. - -The exact wording is asserted by -[`test/example_fixtures_test.dart`](../test/example_fixtures_test.dart), -so the fixtures and their expected diagnostics cannot drift apart. - -## Trying out --fix - -The invalid fixture's `check-absolute-paths` violation is auto-fixable -when the target file exists. To experiment, point it at a real local -file: - -```bash -dart run dart_skills_lint --skill ./example/skills/invalid --fix --dry-run -``` - -`--dry-run` shows the proposed diff without writing; drop it to apply -the change. diff --git a/tool/dart_skills_lint/example/api_boundary_runner/bin/main.dart b/tool/dart_skills_lint/example/api_boundary_runner/bin/main.dart deleted file mode 100644 index 6c54d409..00000000 --- a/tool/dart_skills_lint/example/api_boundary_runner/bin/main.dart +++ /dev/null @@ -1,69 +0,0 @@ -// 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. - -// ignore_for_file: avoid_print - -import 'dart:io'; - -import 'package:dart_skills_lint/dart_skills_lint.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as p; - -Future main(List args) async { - Logger.root.level = Level.ALL; - Logger.root.onRecord.listen((record) => print(record.message)); - - print('Running API boundary validation runner...'); - - String findPath(String relativeSuffix) { - final pathsToTry = [ - p.join('example', 'skills', relativeSuffix), - p.join('..', 'skills', relativeSuffix), - if (Platform.script.scheme == 'file') - p.join(p.dirname(Platform.script.toFilePath()), '..', '..', 'skills', relativeSuffix), - ]; - for (final path in pathsToTry) { - final String absolutePath = p.absolute(path); - if (Directory(absolutePath).existsSync()) { - return p.normalize(absolutePath); - } - } - throw StateError('Could not locate skills/$relativeSuffix directory.'); - } - - final String validSkillPath = findPath('valid'); - final String invalidSkillPath = findPath('invalid'); - - print('Validating valid skill at: $validSkillPath'); - final bool validResult = await validateSkills( - individualSkillPaths: [validSkillPath], - resolvedRuleConfigs: { - 'check-absolute-paths': const RuleConfigPatch(severity: AnalysisSeverity.disabled), - }, - ); - - if (!validResult) { - print('Error: Valid skill fixture failed validation!'); - exitCode = 1; - return; - } - print('Success: Valid skill fixture validated cleanly.'); - - print('Validating invalid skill at: $invalidSkillPath'); - // Since this is invalid, we expect it to fail under standard rules. - final bool invalidResult = await validateSkills( - individualSkillPaths: [invalidSkillPath], - printWarnings: false, - quiet: true, - ); - - if (invalidResult) { - print('Error: Invalid skill fixture unexpectedly passed validation!'); - exitCode = 1; - return; - } - print('Success: Invalid skill fixture failed validation as expected.'); - - print('API boundary verification completed successfully.'); -} diff --git a/tool/dart_skills_lint/example/api_boundary_runner/pubspec.yaml b/tool/dart_skills_lint/example/api_boundary_runner/pubspec.yaml deleted file mode 100644 index 601d1dcb..00000000 --- a/tool/dart_skills_lint/example/api_boundary_runner/pubspec.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: api_boundary_runner -description: An example code runner that validates the public API boundary. -environment: - sdk: ^3.11.0-0 -dependencies: - path: ^1.9.0 - logging: ^1.2.0 - dart_skills_lint: - path: ../../ diff --git a/tool/dart_skills_lint/example/skills/invalid/SKILL.md b/tool/dart_skills_lint/example/skills/invalid/SKILL.md deleted file mode 100644 index 583366b5..00000000 --- a/tool/dart_skills_lint/example/skills/invalid/SKILL.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: NotInvalid -description: A deliberately broken fixture used by example/README.md to show what each rule's error output looks like. -secret_field: not allowed by the spec -metadata: - internal: true ---- - -# Invalid example skill - -This skill deliberately trips three rules so the CLI's diagnostic output -can be inspected end-to-end. One fires under defaults; two need to be -enabled to surface as errors (the spec ships them at lower severities). - -1. `invalid-skill-name` *(error by default)* โ€” the frontmatter `name:` - is `NotInvalid`, which is not lowercase **and** does not match the - parent directory `invalid`. -2. `disallowed-field` *(disabled by default; enable via - `--disallowed-field` or YAML config)* โ€” `secret_field:` is not in - the spec's allowed field list. -3. `check-absolute-paths` *(warning by default; escalate to error via - `--check-absolute-paths` or YAML config)* โ€” the link below uses an - absolute filesystem path, which is not portable across machines. - -The broken link: [absolute link](/tmp/this/does/not/exist.md) - -Run it with default rules: - -```bash -dart run dart_skills_lint --skill ./example/skills/invalid -``` - -โ€ฆand again with every rule turned up to error: - -```bash -dart run dart_skills_lint --skill ./example/skills/invalid \ - --disallowed-field --check-absolute-paths -``` - -Expected: non-zero exit, error messages naming each rule that is enabled. diff --git a/tool/dart_skills_lint/example/skills/valid/SKILL.md b/tool/dart_skills_lint/example/skills/valid/SKILL.md deleted file mode 100644 index 5846630d..00000000 --- a/tool/dart_skills_lint/example/skills/valid/SKILL.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: valid -description: >- - Reference fixture for dart_skills_lint. Demonstrates a SKILL.md that - passes every default rule: hyphen-lowercase name matching the parent - directory, a properly sized description, and no other frontmatter fields - that would trigger the disallowed-field check. -metadata: - internal: true ---- - -# Valid example skill - -This skill exists so the linter has a known-good fixture to validate -against. It deliberately does nothing useful โ€” it's documentation. - -Run it with: - -```bash -dart run dart_skills_lint --skill ./example/skills/valid -``` - -Expected output: `Skill is valid.` and exit code 0. diff --git a/tool/dart_skills_lint/lib/dart_skills_lint.dart b/tool/dart_skills_lint/lib/dart_skills_lint.dart deleted file mode 100644 index d49ad6a6..00000000 --- a/tool/dart_skills_lint/lib/dart_skills_lint.dart +++ /dev/null @@ -1,14 +0,0 @@ -// 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. - -export 'src/config_parser.dart'; -export 'src/entry_point.dart'; -export 'src/models/analysis_severity.dart'; -export 'src/models/custom_rule_parameters.dart'; -export 'src/models/rule_config.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 deleted file mode 100644 index e0a83e35..00000000 --- a/tool/dart_skills_lint/lib/src/config_parser.dart +++ /dev/null @@ -1,379 +0,0 @@ -// 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. - -// ignore_for_file: specify_nonobvious_local_variable_types yaml parsing has dynamic types. - -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:yaml/yaml.dart'; - -import 'models/analysis_severity.dart'; -import 'models/check_type.dart'; -import 'models/custom_rule_parameters.dart'; -import 'models/rule_config.dart'; -import 'path_utils.dart'; -import 'rule_registry.dart'; - -final _log = Logger('dart_skills_lint'); - -class ConfigParser { - static const _dartSkillsLintKey = 'dart_skills_lint'; - static const _rulesKey = 'rules'; - static const _directoriesKey = 'directories'; - static const _individualSkillsKey = 'individual_skills'; - static const _pathKey = 'path'; - static const _ignoreFileKey = 'ignore_file'; - static const _severityKey = 'severity'; - - static const Set _allowedTopLevelKeys = { - _rulesKey, - _directoriesKey, - _individualSkillsKey, - }; - static const Set _allowedDirectoryKeys = {_pathKey, _rulesKey, _ignoreFileKey}; - - static AnalysisSeverity _parseSeverity(String value) { - if (value == 'error') { - return AnalysisSeverity.error; - } - if (value == 'warning') { - return AnalysisSeverity.warning; - } - if (value == 'disabled') { - return AnalysisSeverity.disabled; - } - return AnalysisSeverity.disabled; // Default if unknown - } - - /// Loads the configuration from the specified [path], or from the default - /// `dart_skills_lint.yaml` if no path is provided. - /// - /// If a [path] is explicitly provided and the file does not exist, this - /// method throws a [FileSystemException]. If no path is provided and the - /// default file is missing, it returns an empty [Configuration]. - static Future loadConfig({String? path}) async { - final String resolvedPath = expandPath(path ?? 'dart_skills_lint.yaml'); - final configFile = File(resolvedPath); - - if (!configFile.existsSync()) { - if (path != null) { - throw FileSystemException('Configuration file not found', resolvedPath); - } - return Configuration(); - } - - try { - final String content = await configFile.readAsString(); - final yaml = loadYaml(content); - if (yaml is YamlMap && yaml.containsKey(_dartSkillsLintKey)) { - final toolConfig = yaml[_dartSkillsLintKey]; - if (toolConfig is YamlMap) { - final parsingErrors = []; - - _validateTopLevelKeys(toolConfig, parsingErrors); - final rulesResult = _parseDefaultRules(toolConfig, parsingErrors); - final directoryConfigs = _parseConfigList(toolConfig, _directoriesKey, parsingErrors); - final individualSkillConfigs = _parseConfigList( - toolConfig, - _individualSkillsKey, - parsingErrors, - ); - - return Configuration( - directoryConfigs: directoryConfigs, - individualSkillConfigs: individualSkillConfigs, - ruleConfigs: rulesResult, - parsingErrors: parsingErrors, - ); - } - } - } catch (e) { - final message = 'Failed to parse $resolvedPath: $e'; - _log.severe(message); - return Configuration(parsingErrors: [message]); - } - return Configuration(); - } - - /// Validates that all keys at the top level of the `dart_skills_lint` configuration map are recognized. - /// Appends error messages to `parsingErrors` for any unrecognized keys. - static void _validateTopLevelKeys(YamlMap toolConfig, List parsingErrors) { - for (final key in toolConfig.keys) { - if (!_allowedTopLevelKeys.contains(key.toString())) { - parsingErrors.add('Unrecognized top-level key "$key" in dart_skills_lint configuration.'); - } - } - } - - /// Parses the project-wide default rule configurations from the top-level `rules` map. - /// - /// 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 parameters, appending any parameter type or key - /// validation errors to [parsingErrors]. - static Map _parseDefaultRules( - YamlMap toolConfig, - List parsingErrors, - ) { - if (toolConfig.containsKey(_rulesKey)) { - final rules = toolConfig[_rulesKey]; - if (rules is YamlMap) { - return _parseRulesMap(rules, parsingErrors, 'Global rules'); - } - } - return const {}; - } - - /// Iterates a YAML rules map and converts each entry into a [RuleConfigPatch]. - /// - /// Validates that parameter keys and value types match their definitions in the registry, - /// appending any validation errors to [parsingErrors] labeled by [contextLabel]. - static Map _parseRulesMap( - YamlMap rulesMap, - List parsingErrors, - String contextLabel, - ) { - final ruleConfigs = {}; - - 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; - - ruleConfigs[ruleName] = _parseRuleConfigPatch(value, check, parsingErrors, contextLabel); - } - - return ruleConfigs; - } - - /// Parses a single rule's configuration value into a [RuleConfigPatch]. - /// - /// Supports simple scalar severity declarations (e.g., `rule-name: error`) as - /// well as map declarations containing custom parameter overrides and severity - /// settings (e.g., `rule-name: { severity: error, param: value }`). Validates - /// any custom parameters against [check], appending schema validation errors - /// to [parsingErrors] labeled with [contextLabel]. - static RuleConfigPatch _parseRuleConfigPatch( - Object? value, - CheckType? check, - List parsingErrors, - String contextLabel, - ) { - if (value is! YamlMap) { - final severity = _parseSeverity(value?.toString() ?? ''); - return RuleConfigPatch(severity: severity); - } - - final severity = value.containsKey(_severityKey) - ? _parseSeverity(value[_severityKey]?.toString() ?? '') - : null; - - final parameters = {}; - for (final paramKey in value.keys) { - final paramName = paramKey.toString(); - if (paramName != _severityKey) { - parameters[paramName] = value[paramKey]; - } - } - - final customParams = parameters.isNotEmpty ? CustomRuleParameters(parameters) : null; - - if (customParams != null && check != null) { - final errors = check.validateParameters(customParams); - for (final error in errors) { - parsingErrors.add('$contextLabel: $error'); - } - } - - return RuleConfigPatch(severity: severity, parameters: customParams); - } - - /// Iterates a top-level YAML target list (`directories` or `individual_skills`) - /// and parses each element into a [LintTargetConfig]. - /// - /// Delegates validation of an individual list element to [_parseTargetEntry]. - /// Returns an empty list if [configKey] is omitted or not a list. - static List _parseConfigList( - YamlMap toolConfig, - String configKey, - List parsingErrors, - ) { - if (!toolConfig.containsKey(configKey)) { - return const []; - } - final items = toolConfig[configKey]; - if (items is! YamlList) { - return const []; - } - - final entryLabelCap = configKey == _directoriesKey - ? 'Directory entry' - : 'Individual skill entry'; - final entryLabelLower = configKey == _directoriesKey - ? 'directory entry' - : 'individual skill entry'; - - final configs = []; - for (final dir in items) { - if (dir is! YamlMap || !dir.containsKey(_pathKey)) { - continue; - } - final config = _parseTargetEntry(dir, entryLabelCap, entryLabelLower, parsingErrors); - if (config != null) { - configs.add(config); - } - } - return configs; - } - - /// Parses a single dictionary element from a target list (`directories` or `individual_skills`). - /// - /// Validates the `path` string and checks for unrecognized keys. Delegates - /// parsing of sub-keys to [_parseLocalRulesForTarget] (`rules`) and - /// [_parseIgnoreFileForTarget] (`ignore_file`). Returns `null` if `path` is - /// invalid or missing. - static LintTargetConfig? _parseTargetEntry( - YamlMap dir, - String entryLabelCap, - String entryLabelLower, - List parsingErrors, - ) { - final pathValue = dir[_pathKey]; - if (pathValue is! String) { - parsingErrors.add( - '$entryLabelCap "$_pathKey" must be a string; got "$pathValue" ' - '(${pathValue.runtimeType}). Skipping entry.', - ); - return null; - } - final String path = pathValue; - - for (final key in dir.keys) { - if (!_allowedDirectoryKeys.contains(key.toString())) { - parsingErrors.add('Unrecognized key "$key" in $entryLabelLower for "$path".'); - } - } - - final ruleConfigs = _parseLocalRulesForTarget(dir, path, entryLabelCap, parsingErrors); - - final ignoreFile = _parseIgnoreFileForTarget(dir, path, entryLabelCap, parsingErrors); - - return LintTargetConfig(path: path, ruleConfigs: ruleConfigs, ignoreFile: ignoreFile); - } - - /// Parses path-specific rule overrides under a target entry's `rules` key. - /// - /// Unlike [_parseDefaultRules], which sets global baselines, configurations - /// parsed here apply only to skills within this specific target path. - /// Delegates to [_parseRulesMap]. - static Map _parseLocalRulesForTarget( - YamlMap dir, - String path, - String entryLabelCap, - List parsingErrors, - ) { - if (!dir.containsKey(_rulesKey)) { - return const {}; - } - final localRules = dir[_rulesKey]; - if (localRules is YamlMap) { - return _parseRulesMap(localRules, parsingErrors, '$entryLabelCap rules for "$path"'); - } - parsingErrors.add( - '$entryLabelCap "$_rulesKey" for "$path" must be a map; ' - 'got "$localRules" (${localRules.runtimeType}). Ignoring local rules.', - ); - return const {}; - } - - /// Parses the custom ignore file path under a target entry's `ignore_file` key. - /// - /// Returns `null` if omitted. If present but not a string, appends a type - /// error to [parsingErrors] and returns `null` to fall back to the default - /// ignore file. - static String? _parseIgnoreFileForTarget( - YamlMap dir, - String path, - String entryLabelCap, - List parsingErrors, - ) { - if (!dir.containsKey(_ignoreFileKey)) { - return null; - } - final ignoreFileValue = dir[_ignoreFileKey]; - if (ignoreFileValue is String) { - return ignoreFileValue; - } - if (ignoreFileValue != null) { - parsingErrors.add( - '$entryLabelCap "$_ignoreFileKey" for "$path" must be a string; ' - 'got "$ignoreFileValue" (${ignoreFileValue.runtimeType}). ' - 'Falling back to the default ignore file.', - ); - } - return null; - } -} - -/// Configuration for a specific directory containing skills, or an individual skill. -/// -/// 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.ruleConfigs, 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 ruleConfigs; - final String? ignoreFile; - - // TODO(reidbaker): https://github.com/flutter/agent-plugins/issues/179 - @Deprecated('Use ruleConfigs instead') - Map get rules { - final resolvedSeverities = {}; - for (final entry in ruleConfigs.entries) { - final AnalysisSeverity? severity = entry.value.severity; - if (severity != null) { - resolvedSeverities[entry.key] = severity; - } - } - return resolvedSeverities; - } -} - -/// Structured configuration for the linter. -class Configuration { - Configuration({ - this.directoryConfigs = const [], - this.individualSkillConfigs = const [], - this.ruleConfigs = const {}, - this.parsingErrors = const [], - }); - final List directoryConfigs; - final List individualSkillConfigs; - final Map ruleConfigs; - final List parsingErrors; - - // TODO(reidbaker): https://github.com/flutter/agent-plugins/issues/179 - @Deprecated('Use ruleConfigs instead') - Map get configuredRules { - final resolvedSeverities = {}; - for (final entry in ruleConfigs.entries) { - final AnalysisSeverity? severity = entry.value.severity; - if (severity != null) { - resolvedSeverities[entry.key] = severity; - } - } - return resolvedSeverities; - } -} diff --git a/tool/dart_skills_lint/lib/src/cutoff_excerpt.dart b/tool/dart_skills_lint/lib/src/cutoff_excerpt.dart deleted file mode 100644 index 08e5e750..00000000 --- a/tool/dart_skills_lint/lib/src/cutoff_excerpt.dart +++ /dev/null @@ -1,56 +0,0 @@ -// 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. - -// Shared helper for "field is N characters; max is M" diagnostics that -// also show a |HERE| cutoff excerpt so the author can see exactly -// where the value went over. -// -// Used by both DescriptionLengthRule and the compatibility-length -// check in ValidYamlMetadataRule. Keep the message shape consistent -// across rules so downstream tooling that parses lint output doesn't -// have to learn two formats. - -/// Number of characters of context to show on either side of the cutoff. -const int _excerptContextChars = 40; - -/// Builds a length-overflow diagnostic for a frontmatter field whose -/// value is longer than [maxLength]. -/// -/// Output shape (placeholders shown in backticks): -/// -/// `fieldName` field is `N` characters; maximum is `maxLength`. -/// Cutoff at character `maxLength`: ...`context`|HERE|`context`... -/// (see `docUrl`) -/// -/// The `(see ...)` clause is omitted when [docUrl] is null. Newlines in -/// the excerpt are escaped to `\n` so the message stays on one line. -String buildLengthDiagnostic({ - required String fieldName, - required String value, - required int maxLength, - String? docUrl, -}) { - final String excerpt = _buildCutoffExcerpt(value, maxLength); - final docsClause = docUrl != null ? ' (see $docUrl)' : ''; - return '$fieldName field is ${value.length} characters; ' - 'maximum is $maxLength. ' - 'Cutoff at character $maxLength: $excerpt' - '$docsClause'; -} - -String _buildCutoffExcerpt(String value, int maxLength) { - final int start = (maxLength - _excerptContextChars).clamp(0, value.length); - final int end = (maxLength + _excerptContextChars).clamp(0, value.length); - final String before = value.substring(start, maxLength); - final String after = value.substring(maxLength, end); - final leadingEllipsis = start > 0 ? '...' : ''; - final trailingEllipsis = end < value.length ? '...' : ''; - final String escapedBefore = _escapeForOneLine(before); - final String escapedAfter = _escapeForOneLine(after); - return '$leadingEllipsis$escapedBefore|HERE|$escapedAfter$trailingEllipsis'; -} - -String _escapeForOneLine(String s) { - return s.replaceAll('\n', r'\n').replaceAll('\r', r'\r'); -} diff --git a/tool/dart_skills_lint/lib/src/entry_point.dart b/tool/dart_skills_lint/lib/src/entry_point.dart deleted file mode 100644 index 9599238a..00000000 --- a/tool/dart_skills_lint/lib/src/entry_point.dart +++ /dev/null @@ -1,554 +0,0 @@ -// 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:args/args.dart'; -import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; - -import 'config_parser.dart'; -import 'missing_defaults_exception.dart'; -import 'models/analysis_severity.dart'; -import 'models/check_type.dart'; -import 'models/custom_rule_parameters.dart'; -import 'models/rule_config.dart'; -import 'models/rule_parameter_type.dart'; -import 'models/skill_rule.dart'; -import 'rule_registry.dart'; -import 'validation_session.dart'; - -export 'validation_session.dart'; - -final _log = Logger('dart_skills_lint'); - -const _printWarningsFlag = 'print-warnings'; -const _fastFailFlag = 'fast-fail'; -const _quietFlag = 'quiet'; -const _skillsDirectoryFlag = 'skills-directory'; -const _skillOption = 'skill'; -const _ignoreFileOption = 'ignore-file'; -const _ignoreConfigFlag = 'ignore-config'; -const _generateBaselineFlag = 'generate-baseline'; -const _fixFlag = 'fix'; -const _dryRunFlag = 'dry-run'; -const _fixApplyFlag = 'fix-apply'; -const _allowMisconfiguredKeysFlag = 'allow-misconfigured-keys'; -const _configOption = 'config'; - -/// User-visible deprecation notice for the legacy `--fix-apply` alias. -/// -/// Exposed (not `_`-prefixed) so integration tests can assert it appears on -/// stderr when the alias is used. -const fixApplyDeprecationMsg = - '--fix-apply is deprecated; use --fix instead. ' - 'Pass --fix --dry-run to preview changes without writing.'; - -/// Welcoming first-run guide shown when no args are passed and no default -/// skills directory exists. Exposed so integration tests can assert the -/// exact greeting (drift here changes the new-user experience). -const firstRunGuideMsg = ''' -dart_skills_lint: a linter for Agent Skills (SKILL.md). - -No skills were found to validate. Get started in one of three ways: - - 1. Lint a single skill directory: - dart run dart_skills_lint --skill ./path/to/my-skill - - 2. Lint every skill under a root directory: - dart run dart_skills_lint --skills-directory ./path/to/skills-root - - 3. Drop a skill into one of the auto-discovered default paths - (relative to the current directory) and re-run with no flags: - .claude/skills//SKILL.md - .agents/skills//SKILL.md - -For repo-wide config, create dart_skills_lint.yaml with a -`dart_skills_lint.directories` entry. - -Spec: https://agentskills.io/specification -Run with --help to see every flag.'''; - -/// Main entrypoint execution logic for the CLI tool. -/// -/// Parses arguments and runs validation on the specified directory. -Future runApp(List args) async { - // Setup logger to print to stdout/stderr - Logger.root.level = Level.ALL; - Logger.root.onRecord.listen((record) { - if (record.level >= Level.SEVERE) { - stderr.writeln(record.message); - } else { - stdout.writeln(record.message); - } - }); - - const helpFlag = 'help'; - - final ArgParser parser = _createArgParser(helpFlag); - - final ArgResults results; - final Map resolvedRuleConfigs; - - try { - results = parser.parse(args); - if (results[helpFlag] as bool) { - _printUsage(parser); - return; - } - resolvedRuleConfigs = resolveRuleConfigsFromCli(results); - } catch (e) { - _printUsage(parser, e.toString()); - exitCode = 64; // Bad usage - return; - } - - final Configuration? config = await _loadConfig(results); - if (config == null) { - exitCode = 1; - return; - } - - final skillDirPaths = results[_skillsDirectoryFlag] as List; - final individualSkillPaths = results[_skillOption] as List; - - final printWarnings = results[_printWarningsFlag] as bool; - final fastFail = results[_fastFailFlag] as bool; - final quiet = results[_quietFlag] as bool; - final generateBaseline = results[_generateBaselineFlag] as bool; - final fixFlag = results[_fixFlag] as bool; - final dryRun = results[_dryRunFlag] as bool; - final fixApplyAlias = results[_fixApplyFlag] as bool; - - if (fixApplyAlias) { - stderr.writeln(fixApplyDeprecationMsg); - } - - // --fix writes fixes to disk; pair with --dry-run to preview without - // writing. --fix-apply is a deprecated alias for --fix that still - // writes (with a deprecation notice on stderr above). - final bool fix = fixFlag && dryRun; - final bool fixApply = (fixFlag && !dryRun) || fixApplyAlias; - - String? ignoreFileOverride; - if (results.wasParsed(_ignoreFileOption)) { - ignoreFileOverride = results[_ignoreFileOption] as String?; - } else { - ignoreFileOverride = null; - } - - var success = false; - try { - success = await validateSkillsInternal( - skillDirPaths: skillDirPaths, - individualSkillPaths: individualSkillPaths, - resolvedRuleConfigs: resolvedRuleConfigs, - printWarnings: printWarnings, - fastFail: fastFail, - quiet: quiet, - generateBaseline: generateBaseline, - fix: fix, - fixApply: fixApply, - ignoreFileOverride: ignoreFileOverride, - config: config, - ); - if (success) { - exitCode = 0; - } else { - exitCode = 1; - } - } on MissingDefaultsException catch (_) { - stdout.writeln(firstRunGuideMsg); - exitCode = 64; - } -} - -/// Creates the [ArgParser] for the CLI, adding all supported flags and options. -/// -/// Dynamically adds flags for all registered rules in [RuleRegistry]. -ArgParser _createArgParser(String helpFlag) { - final parser = ArgParser() - ..addFlag(helpFlag, abbr: 'h', negatable: false, help: 'Show usage information.') - ..addFlag(_printWarningsFlag, abbr: 'w', defaultsTo: true, help: 'Print validation warnings.'); - - // Dynamically add flags for all registered rules. - for (final CheckType check in RuleRegistry.allChecks) { - parser.addFlag( - check.name, - defaultsTo: check.defaultSeverity != AnalysisSeverity.disabled, - help: check.help, - ); - - // Register namespaced delegated parameters - for (final String paramName in check.parameterSchema.keys) { - final RuleParameterType expectedType = check.parameterSchema[paramName]!; - if (expectedType == RuleParameterType.stringList) { - parser.addMultiOption( - '${check.name}-$paramName', - help: "Override parameter '$paramName' list for rule '${check.name}'.", - ); - } else { - parser.addOption( - '${check.name}-$paramName', - help: "Override parameter '$paramName' for rule '${check.name}'.", - ); - } - } - } - - parser - ..addFlag( - _fastFailFlag, - negatable: false, - help: 'Fail immediately on the first skill validation error.', - ) - ..addFlag( - _quietFlag, - abbr: 'q', - negatable: false, - help: 'Quiet mode (only print errors and warnings).', - ) - ..addMultiOption( - _skillsDirectoryFlag, - abbr: 'd', - help: 'Path to a skills directory to validate. Can be specified multiple times.', - ) - ..addMultiOption( - _skillOption, - abbr: 's', - help: 'Path to an individual skill directory to validate. Can be specified multiple times.', - ) - ..addOption(_ignoreFileOption, help: 'Path to a JSON file listing lints to ignore for the run.') - ..addFlag( - _generateBaselineFlag, - negatable: false, - help: 'Write all current errors into $defaultIgnoreFileName to ignore on future runs.', - ) - ..addFlag( - _ignoreConfigFlag, - negatable: false, - help: 'Ignore the YAML configuration file entirely.', - ) - ..addFlag( - _fixFlag, - negatable: false, - help: 'Write fixes for failing lints to disk. Combine with --dry-run to preview.', - ) - ..addFlag( - _dryRunFlag, - negatable: false, - help: 'When passed with --fix, preview proposed changes without writing.', - ) - // help: omitted โ€” flag is hide: true so --help skips it anyway. - // Adopters who hit it still get the runtime deprecation notice - // on stderr (see fixApplyDeprecationMsg above). - ..addFlag(_fixApplyFlag, negatable: false, hide: true) - ..addFlag( - _allowMisconfiguredKeysFlag, - negatable: false, - hide: true, - help: 'Allow misconfigured keys in dart_skills_lint.yaml.', - ) - ..addOption( - _configOption, - abbr: 'c', - help: 'Path to a custom configuration file (defaults to dart_skills_lint.yaml).', - ); - - return parser; -} - -Future _loadConfig(ArgResults results) async { - final ignoreConfig = results[_ignoreConfigFlag] as bool; - final Configuration config; - if (ignoreConfig) { - config = Configuration(); - } else { - try { - final configPath = results[_configOption] as String?; - config = await ConfigParser.loadConfig(path: configPath); - } on FileSystemException catch (e) { - _log.severe('Error: ${e.message} (${e.path})'); - return null; - } catch (e) { - _log.severe('Error loading configuration: $e'); - return null; - } - } - if (ignoreConfig && !(results[_quietFlag] as bool)) { - _log.info('Ignoring configuration file due to $_ignoreConfigFlag flag'); - } - - if (config.parsingErrors.isNotEmpty) { - final allowMisconfiguredKeys = results[_allowMisconfiguredKeysFlag] as bool; - if (allowMisconfiguredKeys) { - _log.warning( - 'DEPRECATION WARNING: --allow-misconfigured-keys is deprecated and will be removed in a future release. Fix misconfigured configuration keys rather than bypassing validation.', - ); - for (final String error in config.parsingErrors) { - _log.warning('Configuration warning: $error'); - } - } else { - for (final String error in config.parsingErrors) { - _log.severe('Configuration error: $error'); - } - _log.severe('Use --$_allowMisconfiguredKeysFlag to ignore these errors.'); - return null; - } - } - return config; -} - -/// Validates skills based on the provided configuration. -/// -/// This is the public API for validating skills. It does not support fixing -/// lints as that feature is considered internal to the CLI. -/// -/// [skillDirPaths] is a list of directories containing multiple skills. -/// [individualSkillPaths] is a list of paths to individual skill directories. -/// [resolvedRules] is a map of rule names to their severity overrides. -/// [printWarnings] controls whether to print validation warnings. -/// [fastFail] causes validation to stop on the first error. -/// [quiet] suppresses non-error/warning output. -/// [generateBaseline] writes current errors to a baseline file instead of reporting them. -/// [ignoreFileOverride] is an optional path to a baseline file to use. -/// [config] is the loaded configuration. -/// -/// Returns a [Future] that resolves to `true` if all skills validated successfully -/// (or if [generateBaseline] is true), and `false` if any validation failures -/// were encountered. -Future validateSkills({ - List skillDirPaths = const [], - List individualSkillPaths = const [], - @Deprecated('Use resolvedRuleConfigs instead') - Map resolvedRules = const {}, - Map resolvedRuleConfigs = const {}, - bool printWarnings = true, - bool fastFail = false, - bool quiet = false, - bool generateBaseline = false, - String? ignoreFileOverride, - Configuration? config, - List customRules = const [], -}) { - if (resolvedRules.isNotEmpty && resolvedRuleConfigs.isNotEmpty) { - throw ArgumentError( - 'Cannot specify both deprecated resolvedRules and new resolvedRuleConfigs. ' - 'Please migrate all overrides to resolvedRuleConfigs.', - ); - } - - final Map mergedConfigs = Map.from(resolvedRuleConfigs); - if (resolvedRules.isNotEmpty) { - for (final String ruleName in resolvedRules.keys) { - mergedConfigs[ruleName] = RuleConfigPatch(severity: resolvedRules[ruleName]); - } - } - - return validateSkillsInternal( - skillDirPaths: skillDirPaths, - individualSkillPaths: individualSkillPaths, - resolvedRuleConfigs: mergedConfigs, - printWarnings: printWarnings, - fastFail: fastFail, - quiet: quiet, - generateBaseline: generateBaseline, - ignoreFileOverride: ignoreFileOverride, - config: config, - customRules: customRules, - ); -} - -/// Internal implementation of skill validation that supports fixing. -/// -/// Kept internal to avoid exposing experimental fix parameters in the public API. -/// -/// Returns `true` if all validations passed (or if generating a baseline), `false` otherwise. -@visibleForTesting -Future validateSkillsInternal({ - List skillDirPaths = const [], - List individualSkillPaths = const [], - Map resolvedRuleConfigs = const {}, - bool printWarnings = true, - bool fastFail = false, - bool quiet = false, - bool generateBaseline = false, - bool fix = false, - bool fixApply = false, - String? ignoreFileOverride, - Configuration? config, - List customRules = const [], -}) async { - final bool hasCliTargets = skillDirPaths.isNotEmpty || individualSkillPaths.isNotEmpty; - final List effectiveIndividualSkillPaths = [ - ...individualSkillPaths, - if (config != null && !hasCliTargets) ...config.individualSkillConfigs.map((e) => e.path), - ]; - - final List effectiveSkillDirPaths = _getEffectiveSkillDirPaths( - skillDirPaths: skillDirPaths, - individualSkillPaths: individualSkillPaths, - config: config, - ); - - final session = ValidationSession( - config: config ?? Configuration(), - resolvedRuleConfigs: resolvedRuleConfigs, - ignoreFileOverride: ignoreFileOverride, - customRules: customRules, - printWarnings: printWarnings, - fastFail: fastFail, - quiet: quiet, - generateBaseline: generateBaseline, - fix: fix, - fixApply: fixApply, - ); - - for (final skillPath in effectiveIndividualSkillPaths) { - final bool keepGoing = await session.processIndividualSkill(skillPath); - if (!keepGoing) { - break; - } - } - if (session.anyFailed && fastFail) { - return false; - } - - for (final rootPath in effectiveSkillDirPaths) { - final bool keepGoing = await session.processSkillRoot(rootPath); - if (!keepGoing) { - break; - } - } - - session.reportNoSkillsValidated(effectiveSkillDirPaths); - - if (generateBaseline) { - return true; - } - return !session.anyFailed; -} - -/// Computes the list of skill directory paths to validate. -/// -/// If paths are not explicitly provided, falls back to configured directory -/// paths, and then to default locations (`.claude/skills`, `.agents/skills`). -/// Throws [MissingDefaultsException] if no directories are found. -List _getEffectiveSkillDirPaths({ - required List skillDirPaths, - required List individualSkillPaths, - Configuration? config, -}) { - final effectiveSkillDirPaths = List.from(skillDirPaths); - - if (effectiveSkillDirPaths.isEmpty && individualSkillPaths.isEmpty) { - // If the config specifies any targets (even if it's only individual_skills - // and directories is empty), we avoid the default directory fallback. - if (config != null && - (config.directoryConfigs.isNotEmpty || config.individualSkillConfigs.isNotEmpty)) { - return config.directoryConfigs.map((e) => e.path).toList(); - } else { - final defaults = ['.claude/skills', '.agents/skills']; - final existingDefaults = []; - for (final path in defaults) { - if (Directory(path).existsSync()) { - existingDefaults.add(path); - } - } - if (existingDefaults.isEmpty) { - throw MissingDefaultsException(defaults); - } - return existingDefaults; - } - } - - return effectiveSkillDirPaths; -} - -@visibleForTesting -Map resolveRuleConfigsFromCli(ArgResults results) { - final configs = {}; - - // 1. Resolve severities from CLI flags (e.g. --path-does-not-exist) - final severityOverrides = {}; - for (final CheckType check in RuleRegistry.allChecks) { - final String name = check.name; - if (results.options.contains(name) && results.wasParsed(name)) { - final Object? value = results[name]; - if (value is bool) { - severityOverrides[name] = value ? AnalysisSeverity.error : AnalysisSeverity.disabled; - } - } - } - - // 2. Resolve parameter overrides from CLI flags (e.g. --path-does-not-exist-exclude) - final parameterOverrides = >{}; - for (final CheckType check in RuleRegistry.allChecks) { - final Map checkOverrides = _resolveParametersForCheck(check, results); - if (checkOverrides.isNotEmpty) { - parameterOverrides[check.name] = checkOverrides; - } - } - - // 3. Combine into RuleConfigPatch overrides - final Set allRuleNames = {...severityOverrides.keys, ...parameterOverrides.keys}; - for (final ruleName in allRuleNames) { - configs[ruleName] = RuleConfigPatch( - severity: severityOverrides[ruleName], - parameters: parameterOverrides.containsKey(ruleName) - ? CustomRuleParameters(parameterOverrides[ruleName]!) - : null, - ); - } - - return configs; -} - -Map _resolveParametersForCheck(CheckType check, ArgResults results) { - final Map checkOverrides = {}; - for (final String paramName in check.parameterSchema.keys) { - final paramFlag = '${check.name}-$paramName'; - if (results.options.contains(paramFlag) && results.wasParsed(paramFlag)) { - final RuleParameterType expectedType = check.parameterSchema[paramName]!; - checkOverrides[paramName] = _parseParameterValue(paramFlag, results[paramFlag], expectedType); - } - } - return checkOverrides; -} - -Object? _parseParameterValue(String paramFlag, Object? rawValue, RuleParameterType expectedType) { - if (rawValue == '') { - return null; - } - - if (expectedType == RuleParameterType.integer) { - final int? parsedInt = int.tryParse(rawValue.toString()); - if (parsedInt == null) { - throw FormatException( - 'Invalid value "$rawValue" for parameter "$paramFlag". Expected an integer.', - ); - } - return parsedInt; - } - - if (expectedType == RuleParameterType.boolean) { - final String lower = rawValue.toString().toLowerCase(); - if (lower != 'true' && lower != 'false') { - throw FormatException( - 'Invalid value "$rawValue" for parameter "$paramFlag". Expected "true" or "false".', - ); - } - return lower == 'true'; - } - - return rawValue; -} - -void _printUsage(ArgParser parser, [String? error]) { - if (error != null) { - _log.severe('Error: $error'); - } - _log.info('Usage: dart_skills_lint [options] --$_skillsDirectoryFlag <$_skillsDirectoryFlag>'); - _log.info(parser.usage); -} diff --git a/tool/dart_skills_lint/lib/src/fixable_rule.dart b/tool/dart_skills_lint/lib/src/fixable_rule.dart deleted file mode 100644 index 477df1a9..00000000 --- a/tool/dart_skills_lint/lib/src/fixable_rule.dart +++ /dev/null @@ -1,20 +0,0 @@ -// 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 'models/skill_rule.dart'; - -/// Interface for rules that support fixes. -/// Kept internal to the package. -abstract class FixableRule extends SkillRule { - /// Returns the updated content of the file at [filePath]. - /// [currentContent] is the content after previous fixes have been applied. - /// If the rule does not support fixing the file at [filePath], it should return [currentContent]. - /// - /// Rules should rely on [currentContent] for the current state of the file. - /// If a rule needs structured access (like parsed YAML), it should parse - /// [currentContent] itself, as structured data in [SkillContext] may be stale - /// if previous rules applied fixes. - Future fix(String filePath, String currentContent, Directory directory); -} diff --git a/tool/dart_skills_lint/lib/src/levenshtein.dart b/tool/dart_skills_lint/lib/src/levenshtein.dart deleted file mode 100644 index d9668c0b..00000000 --- a/tool/dart_skills_lint/lib/src/levenshtein.dart +++ /dev/null @@ -1,43 +0,0 @@ -// 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:math' as math; - -/// Plain Levenshtein edit distance over runes. O(n*m) time, O(m) space. -/// -/// Used by sibling-suggestion logic to score how close an existing filename -/// is to a missing one. Lifted into its own file so the rule that consumes -/// it stays focused on the rule contract and so the function is easy to -/// unit-test in isolation. -int levenshtein(String a, String b) { - if (a == b) { - return 0; - } - if (a.isEmpty) { - return b.length; - } - if (b.isEmpty) { - return a.length; - } - - final List aCodes = a.runes.toList(); - final List bCodes = b.runes.toList(); - - var previous = List.generate(bCodes.length + 1, (j) => j); - var current = List.filled(bCodes.length + 1, 0); - for (var i = 1; i <= aCodes.length; i++) { - current[0] = i; - for (var j = 1; j <= bCodes.length; j++) { - final cost = aCodes[i - 1] == bCodes[j - 1] ? 0 : 1; - final int del = previous[j] + 1; - final int ins = current[j - 1] + 1; - final int sub = previous[j - 1] + cost; - current[j] = math.min(math.min(del, ins), sub); - } - final swap = previous; - previous = current; - current = swap; - } - return previous[bCodes.length]; -} diff --git a/tool/dart_skills_lint/lib/src/missing_defaults_exception.dart b/tool/dart_skills_lint/lib/src/missing_defaults_exception.dart deleted file mode 100644 index 501d2141..00000000 --- a/tool/dart_skills_lint/lib/src/missing_defaults_exception.dart +++ /dev/null @@ -1,8 +0,0 @@ -// 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. - -class MissingDefaultsException implements Exception { - MissingDefaultsException(this.defaults); - final List defaults; -} diff --git a/tool/dart_skills_lint/lib/src/models/analysis_severity.dart b/tool/dart_skills_lint/lib/src/models/analysis_severity.dart deleted file mode 100644 index 1d50f98d..00000000 --- a/tool/dart_skills_lint/lib/src/models/analysis_severity.dart +++ /dev/null @@ -1,15 +0,0 @@ -// 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. - -/// Severity level for a specific analysis rule. -enum AnalysisSeverity { - /// Check is completely disabled. - disabled, - - /// Failures are reported as warnings and do not fail the overall validation. - warning, - - /// Failures are reported as errors and fail the overall validation. - 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 deleted file mode 100644 index bfa7a41f..00000000 --- a/tool/dart_skills_lint/lib/src/models/check_type.dart +++ /dev/null @@ -1,49 +0,0 @@ -// 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 'custom_rule_parameters.dart'; -import 'rule_parameter_type.dart'; - -/// Encapsulates metadata and severity state for a specific validation rule. -class CheckType { - const CheckType({ - required this.name, - required this.defaultSeverity, - required this.help, - this.parameterSchema = const {}, - }); - final String name; - - /// The default severity if not overridden by config or flags. - final AnalysisSeverity defaultSeverity; - - /// The help message displayed by the CLI. - final String help; - - /// Custom configuration options supported by this check. - final Map parameterSchema; - - /// Validates the given [options] against this check's [parameterSchema] schema. - /// - /// Returns a list of error messages for any unrecognized options or type mismatches. - List validateParameters(CustomRuleParameters parameters) { - final List errors = []; - for (final String key in parameters.params.keys) { - if (!parameterSchema.containsKey(key)) { - errors.add('Unrecognized parameter "$key" for rule "$name".'); - continue; - } - final RuleParameterType expectedType = parameterSchema[key]!; - final Object? actualValue = parameters.params[key]; - if (actualValue != null && !expectedType.isValid(actualValue)) { - errors.add( - 'Invalid value/type for parameter "$key" in rule "$name". ' - 'Expected ${expectedType.description}, got "$actualValue".', - ); - } - } - return errors; - } -} diff --git a/tool/dart_skills_lint/lib/src/models/custom_rule_parameters.dart b/tool/dart_skills_lint/lib/src/models/custom_rule_parameters.dart deleted file mode 100644 index 99b2ca75..00000000 --- a/tool/dart_skills_lint/lib/src/models/custom_rule_parameters.dart +++ /dev/null @@ -1,61 +0,0 @@ -// 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 parameters. -/// -/// Prevents exposing raw [Map] APIs directly inside rule logic, and provides -/// standard lookups and properties for rule configuration parameters. -class CustomRuleParameters { - /// Creates a new configuration with the provided [params]. - CustomRuleParameters(Map params) - : params = Map.unmodifiable(params); - - /// The underlying map containing the parameters. - final Map params; - - bool get isEmpty => params.isEmpty; - - bool get isNotEmpty => params.isNotEmpty; - - Object? operator [](String key) => params[key]; - - Iterable get keys => params.keys; - - 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/models/ignore_entry.dart b/tool/dart_skills_lint/lib/src/models/ignore_entry.dart deleted file mode 100644 index 0e0b9170..00000000 --- a/tool/dart_skills_lint/lib/src/models/ignore_entry.dart +++ /dev/null @@ -1,34 +0,0 @@ -// 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:json_annotation/json_annotation.dart'; - -part 'ignore_entry.g.dart'; - -/// Represents a single ignored rule entry for a specific file. -@JsonSerializable() -class IgnoreEntry { - IgnoreEntry({required this.ruleId, required this.fileName, this.used = false}); - - /// Creates an IgnoreEntry from a JSON map. - factory IgnoreEntry.fromJson(Map json) => _$IgnoreEntryFromJson(json); - - static const String ruleIdKey = 'rule_id'; - static const String fileNameKey = 'file_name'; - - /// The rule ID that should be suppressed (e.g., 'description_too_long'). - @JsonKey(name: ruleIdKey) - final String ruleId; - - /// The file name to apply this suppression to. - @JsonKey(name: fileNameKey) - final String fileName; - - /// Whether this entry has been used during the run. - @JsonKey(includeFromJson: false, includeToJson: false) - bool used; - - /// Converts an IgnoreEntry to a JSON map. - Map toJson() => _$IgnoreEntryToJson(this); -} diff --git a/tool/dart_skills_lint/lib/src/models/ignore_entry.g.dart b/tool/dart_skills_lint/lib/src/models/ignore_entry.g.dart deleted file mode 100644 index 71ad5ef2..00000000 --- a/tool/dart_skills_lint/lib/src/models/ignore_entry.g.dart +++ /dev/null @@ -1,19 +0,0 @@ -// 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. - -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'ignore_entry.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -IgnoreEntry _$IgnoreEntryFromJson(Map json) => - IgnoreEntry(ruleId: json['rule_id'] as String, fileName: json['file_name'] as String); - -Map _$IgnoreEntryToJson(IgnoreEntry instance) => { - 'rule_id': instance.ruleId, - 'file_name': instance.fileName, -}; diff --git a/tool/dart_skills_lint/lib/src/models/rule_config.dart b/tool/dart_skills_lint/lib/src/models/rule_config.dart deleted file mode 100644 index 11fe6ee0..00000000 --- a/tool/dart_skills_lint/lib/src/models/rule_config.dart +++ /dev/null @@ -1,56 +0,0 @@ -// 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 'custom_rule_parameters.dart'; - -/// Represents the resolved, active configuration for a validation rule, -/// bundling both orchestration (severity) and execution parameters. -class RuleConfig { - RuleConfig({required this.severity, CustomRuleParameters? parameters}) - : parameters = parameters ?? CustomRuleParameters({}); - - final AnalysisSeverity severity; - - final CustomRuleParameters parameters; -} - -/// Represents a configuration override patch containing nullable parameters. -/// Used during validation session configuration inheritance to resolve target-specific -/// overrides without wiping out unspecified base/global parameters. -class RuleConfigPatch { - const RuleConfigPatch({this.severity, this.parameters}); - - /// The overridden severity value. If null, the base configuration's severity is preserved. - final AnalysisSeverity? severity; - - /// The overridden parameters. Keys containing null values (e.g. from YAML `~`) will remove - /// the parameter from the base configuration during merging. - final CustomRuleParameters? parameters; - - /// Creates a new [RuleConfig] by layering this patch's overrides over a [base] configuration. - RuleConfig applyTo(RuleConfig base) { - return RuleConfig( - severity: severity ?? base.severity, - parameters: parameters != null - ? _mergeParameters(base.parameters, parameters!) - : base.parameters, - ); - } - - static CustomRuleParameters _mergeParameters( - CustomRuleParameters base, - CustomRuleParameters patch, - ) { - final merged = Map.from(base.params); - for (final MapEntry entry in patch.params.entries) { - if (entry.value == null) { - merged.remove(entry.key); - } else { - merged[entry.key] = entry.value; - } - } - return CustomRuleParameters(merged); - } -} diff --git a/tool/dart_skills_lint/lib/src/models/rule_parameter_type.dart b/tool/dart_skills_lint/lib/src/models/rule_parameter_type.dart deleted file mode 100644 index fc79072b..00000000 --- a/tool/dart_skills_lint/lib/src/models/rule_parameter_type.dart +++ /dev/null @@ -1,52 +0,0 @@ -// 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. - -/// Defines the expected schema types for custom rule configuration parameters. -enum RuleParameterType { - string, - integer, - boolean, - stringList, - regExp; - - /// Returns whether [value] matches this schema type constraint and syntax format. - bool isValid(Object? value) { - switch (this) { - case RuleParameterType.string: - return value is String; - case RuleParameterType.integer: - return value is int; - case RuleParameterType.boolean: - return value is bool; - case RuleParameterType.stringList: - return value is List && value.every((e) => e is String); - case RuleParameterType.regExp: - if (value is! String) { - return false; - } - try { - RegExp(value); - return true; - } on FormatException { - return false; - } - } - } - - /// User-facing type description. - String get description { - switch (this) { - case RuleParameterType.string: - return 'String'; - case RuleParameterType.integer: - return 'int'; - case RuleParameterType.boolean: - return 'bool'; - case RuleParameterType.stringList: - return 'List'; - case RuleParameterType.regExp: - return 'RegExp (valid regular expression string)'; - } - } -} diff --git a/tool/dart_skills_lint/lib/src/models/skill_context.dart b/tool/dart_skills_lint/lib/src/models/skill_context.dart deleted file mode 100644 index 8d7c0108..00000000 --- a/tool/dart_skills_lint/lib/src/models/skill_context.dart +++ /dev/null @@ -1,36 +0,0 @@ -// 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:yaml/yaml.dart'; - -/// Context provided to [SkillRule]s during validation. -class SkillContext { - SkillContext({ - required this.directory, - required this.rawContent, - this.parsedYaml, - this.yamlParsingError, - }); - - /// The required filename for skill documentation. - static const String skillFileName = 'SKILL.md'; - - /// Regex to match the YAML frontmatter in SKILL.md. - static final RegExp skillStartRegex = RegExp(r'^---\s*\n(.*?)\n---\s*\n', dotAll: true); - - /// Regex to match inline Markdown links (`[text](target)`). The capture - /// group is the link target. Rules that inspect SKILL.md link targets - /// import this rather than re-defining the pattern. - static final RegExp markdownLinkRegex = RegExp(r'\[.*?\]\((.*?)\)'); - - final Directory directory; - - /// Guaranteed to be non-null because we only run rules if SKILL.md exists. - final String rawContent; - - final YamlMap? parsedYaml; - - final String? yamlParsingError; -} diff --git a/tool/dart_skills_lint/lib/src/models/skill_rule.dart b/tool/dart_skills_lint/lib/src/models/skill_rule.dart deleted file mode 100644 index 041f127d..00000000 --- a/tool/dart_skills_lint/lib/src/models/skill_rule.dart +++ /dev/null @@ -1,31 +0,0 @@ -// 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'; - -/// Abstract base class for all skill validation rules. -/// -/// Custom rules should follow these guidelines to play nice with others: -/// 1. **Unique Name**: The [name] must be unique to allow for overrides in -/// configuration. -/// 2. **Statelessness**: Rules should not maintain state between [validate] calls. -/// 3. **Use Context**: Prefer using data in [SkillContext] (like [context.parsedYaml]) -/// rather than reading files manually to avoid duplicate I/O. -/// 4. **Handle Parsing Errors**: If [context.parsedYaml] is null, check -/// [context.yamlParsingError]. Rules that require valid YAML should return -/// quickly if parsing failed. -/// 5. **Respect Severity**: The rule should use its [severity] when creating -/// [ValidationError]s unless there is a good reason not to. -abstract class SkillRule { - /// The unique name of the rule (e.g., 'check-relative-paths'). - /// Used in configuration and flags. - String get name; - - AnalysisSeverity get severity; - - /// Validates the skill provided in [context]. - Future> validate(SkillContext context); -} diff --git a/tool/dart_skills_lint/lib/src/models/skills_ignores.dart b/tool/dart_skills_lint/lib/src/models/skills_ignores.dart deleted file mode 100644 index 1f0935fd..00000000 --- a/tool/dart_skills_lint/lib/src/models/skills_ignores.dart +++ /dev/null @@ -1,26 +0,0 @@ -// 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:json_annotation/json_annotation.dart'; -import 'ignore_entry.dart'; - -part 'skills_ignores.g.dart'; - -/// Represents the top-level structure of the skills ignore JSON file. -@JsonSerializable(explicitToJson: true) -class SkillsIgnores { - SkillsIgnores({required this.skills}); - - /// Creates a SkillsIgnores from a JSON map. - factory SkillsIgnores.fromJson(Map json) => _$SkillsIgnoresFromJson(json); - - static const String skillsKey = 'skills'; - - /// Map of skill names to their list of ignore entries. - @JsonKey(name: skillsKey) - final Map> skills; - - /// Converts a SkillsIgnores to a JSON map. - Map toJson() => _$SkillsIgnoresToJson(this); -} diff --git a/tool/dart_skills_lint/lib/src/models/skills_ignores.g.dart b/tool/dart_skills_lint/lib/src/models/skills_ignores.g.dart deleted file mode 100644 index 6349c11a..00000000 --- a/tool/dart_skills_lint/lib/src/models/skills_ignores.g.dart +++ /dev/null @@ -1,24 +0,0 @@ -// 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. - -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'skills_ignores.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SkillsIgnores _$SkillsIgnoresFromJson(Map json) => SkillsIgnores( - skills: (json['skills'] as Map).map( - (k, e) => MapEntry( - k, - (e as List).map((e) => IgnoreEntry.fromJson(e as Map)).toList(), - ), - ), -); - -Map _$SkillsIgnoresToJson(SkillsIgnores instance) => { - 'skills': instance.skills.map((k, e) => MapEntry(k, e.map((e) => e.toJson()).toList())), -}; diff --git a/tool/dart_skills_lint/lib/src/models/validation_error.dart b/tool/dart_skills_lint/lib/src/models/validation_error.dart deleted file mode 100644 index 97a40bf6..00000000 --- a/tool/dart_skills_lint/lib/src/models/validation_error.dart +++ /dev/null @@ -1,31 +0,0 @@ -// 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'; - -/// Represents a single validation error found during analysis. -class ValidationError { - ValidationError({ - required this.ruleId, - required this.file, - required this.message, - required this.severity, - this.isIgnored = false, - }); - - /// The unique rule ID (e.g., 'description_too_long'). - final String ruleId; - - /// The file name context (e.g., 'SKILL.md' or relative path). - final String file; - - /// The human-readable error message. - final String message; - - /// The severity of the error. - final AnalysisSeverity severity; - - /// Whether this error has been ignored via configuration. - bool isIgnored; -} diff --git a/tool/dart_skills_lint/lib/src/models/validation_result.dart b/tool/dart_skills_lint/lib/src/models/validation_result.dart deleted file mode 100644 index 93f0145d..00000000 --- a/tool/dart_skills_lint/lib/src/models/validation_result.dart +++ /dev/null @@ -1,42 +0,0 @@ -// 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/path_utils.dart b/tool/dart_skills_lint/lib/src/path_utils.dart deleted file mode 100644 index 67d93095..00000000 --- a/tool/dart_skills_lint/lib/src/path_utils.dart +++ /dev/null @@ -1,20 +0,0 @@ -// 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; - -/// Expands tilde (`~/`) at the start of a path to the user's home directory. -/// -/// If the path does not start with `~/` or if the home directory cannot be -/// determined from the environment, the original path is returned. -String expandPath(String path) { - if (path.startsWith('~/')) { - final String? homeDir = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; - if (homeDir != null) { - return p.join(homeDir, path.substring(2)); - } - } - return path; -} diff --git a/tool/dart_skills_lint/lib/src/rule_registry.dart b/tool/dart_skills_lint/lib/src/rule_registry.dart deleted file mode 100644 index 0d9efb65..00000000 --- a/tool/dart_skills_lint/lib/src/rule_registry.dart +++ /dev/null @@ -1,107 +0,0 @@ -// 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 'models/analysis_severity.dart'; -import 'models/check_type.dart'; -import 'models/custom_rule_parameters.dart'; -import 'models/rule_parameter_type.dart'; -import 'models/skill_rule.dart'; -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'; -import 'rules/valid_yaml_metadata_rule.dart'; - -/// Registry of all built-in rules. -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.', - parameterSchema: {PathDoesNotExistRule.excludeParameter: RuleParameterType.regExp}, - ), - const CheckType( - name: AbsolutePathsRule.ruleName, - defaultSeverity: AbsolutePathsRule.defaultSeverity, - help: 'Check if absolute paths exist.', - ), - const CheckType( - name: DescriptionLengthRule.ruleName, - defaultSeverity: DescriptionLengthRule.defaultSeverity, - help: 'Check if description is too long.', - ), - const CheckType( - name: DisallowedFieldRule.ruleName, - defaultSeverity: DisallowedFieldRule.defaultSeverity, - help: 'Check for disallowed fields in YAML metadata.', - ), - const CheckType( - name: PreventSkillsShPublishingRule.ruleName, - defaultSeverity: PreventSkillsShPublishingRule.defaultSeverity, - help: 'Check if skill has metadata: internal: true to prevent publishing.', - ), - const CheckType( - name: NameFormatRule.ruleName, - defaultSeverity: NameFormatRule.defaultSeverity, - help: 'Check if skill name is invalid.', - ), - const CheckType( - name: RelativePathsRule.ruleName, - defaultSeverity: RelativePathsRule.defaultSeverity, - help: 'Check if relative paths exist.', - ), - const CheckType( - name: TrailingWhitespaceRule.ruleName, - defaultSeverity: TrailingWhitespaceRule.defaultSeverity, - help: 'Check for trailing whitespace (allows exactly 2 spaces for line breaks).', - ), - const CheckType( - name: ValidYamlMetadataRule.ruleName, - defaultSeverity: ValidYamlMetadataRule.defaultSeverity, - help: 'Check if YAML metadata is valid.', - ), - ]; - - /// Creates a rule instance by name, or returns null if not a class-based rule. - static SkillRule? createRule( - String name, - AnalysisSeverity severity, [ - CustomRuleParameters? parameters, - ]) { - switch (name) { - case PathDoesNotExistRule.ruleName: - RegExp? excludeRegExp; - final String? excludePattern = parameters?.getString(PathDoesNotExistRule.excludeParameter); - if (excludePattern != null && excludePattern.isNotEmpty) { - excludeRegExp = RegExp(excludePattern); - } - return PathDoesNotExistRule(severity: severity, excludeRegExp: excludeRegExp); - case AbsolutePathsRule.ruleName: - return AbsolutePathsRule(severity: severity); - case DescriptionLengthRule.ruleName: - return DescriptionLengthRule(severity: severity); - case DisallowedFieldRule.ruleName: - return DisallowedFieldRule(severity: severity); - case PreventSkillsShPublishingRule.ruleName: - return PreventSkillsShPublishingRule(severity: severity); - case NameFormatRule.ruleName: - return NameFormatRule(severity: severity); - case RelativePathsRule.ruleName: - return RelativePathsRule(severity: severity); - case TrailingWhitespaceRule.ruleName: - return TrailingWhitespaceRule(severity: severity); - case ValidYamlMetadataRule.ruleName: - return ValidYamlMetadataRule(severity: severity); - default: - return null; - } - } -} diff --git a/tool/dart_skills_lint/lib/src/rules/absolute_paths_rule.dart b/tool/dart_skills_lint/lib/src/rules/absolute_paths_rule.dart deleted file mode 100644 index e8c0621d..00000000 --- a/tool/dart_skills_lint/lib/src/rules/absolute_paths_rule.dart +++ /dev/null @@ -1,81 +0,0 @@ -// 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'; -import '../fixable_rule.dart'; -import '../models/analysis_severity.dart'; -import '../models/skill_context.dart'; -import '../models/skill_rule.dart'; -import '../models/validation_error.dart'; - -/// Enforces that links in SKILL.md do not use absolute paths. -class AbsolutePathsRule extends SkillRule implements FixableRule { - AbsolutePathsRule({this.severity = defaultSeverity}); - - static const String ruleName = 'check-absolute-paths'; - static const AnalysisSeverity defaultSeverity = AnalysisSeverity.warning; - - @override - String get name => ruleName; - - @override - final AnalysisSeverity severity; - - static const String _skillFileName = SkillContext.skillFileName; - - @override - Future> validate(SkillContext context) async { - final errors = []; - - // Extract content after YAML frontmatter - final RegExpMatch? match = SkillContext.skillStartRegex.firstMatch(context.rawContent); - final String markdownContent = match != null - ? context.rawContent.substring(match.end) - : context.rawContent; - - for (final RegExpMatch linkMatch in SkillContext.markdownLinkRegex.allMatches( - markdownContent, - )) { - final String path = linkMatch.group(1)!; - if (isAbsolute(path) || windows.isAbsolute(path)) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: - 'Absolute filepath found in link: $path. ' - 'Skills must use paths relative to SKILL.md so they remain ' - 'portable across machines.', - ), - ); - } - } - - return errors; - } - - @override - Future fix(String filePath, String currentContent, Directory directory) async { - if (filePath != SkillContext.skillFileName) { - return currentContent; - } - - return currentContent.replaceAllMapped(SkillContext.markdownLinkRegex, (match) { - final String path = match.group(1)!; - if (isAbsolute(path) || windows.isAbsolute(path)) { - final file = File(path); - if (file.existsSync()) { - final String relativePath = relative(path, from: directory.path); - final String posixRelativePath = relativePath.replaceAll(r'\', '/'); - final String fullMatch = match.group(0)!; - final int lastParen = fullMatch.lastIndexOf('('); - return '${fullMatch.substring(0, lastParen + 1)}$posixRelativePath)'; - } - } - return match.group(0)!; - }); - } -} diff --git a/tool/dart_skills_lint/lib/src/rules/description_length_rule.dart b/tool/dart_skills_lint/lib/src/rules/description_length_rule.dart deleted file mode 100644 index 3dd1a918..00000000 --- a/tool/dart_skills_lint/lib/src/rules/description_length_rule.dart +++ /dev/null @@ -1,58 +0,0 @@ -// 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:yaml/yaml.dart'; -import '../cutoff_excerpt.dart'; -import '../models/analysis_severity.dart'; -import '../models/skill_context.dart'; -import '../models/skill_rule.dart'; -import '../models/validation_error.dart'; - -/// Enforces that the description field is not too long. -class DescriptionLengthRule extends SkillRule { - DescriptionLengthRule({this.severity = defaultSeverity}); - - static const String ruleName = 'description-too-long'; - static const AnalysisSeverity defaultSeverity = AnalysisSeverity.error; - - @override - String get name => ruleName; - - @override - final AnalysisSeverity severity; - - static const maxDescriptionLength = 1024; - static const _skillFileName = 'SKILL.md'; - static const _descriptionFieldUrl = 'https://agentskills.io/specification#description-field'; - - @override - Future> validate(SkillContext context) async { - final errors = []; - - if (context.parsedYaml == null) { - return errors; - } - - final YamlMap yaml = context.parsedYaml!; - final String description = yaml['description']?.toString() ?? ''; - - if (description.length > maxDescriptionLength) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: buildLengthDiagnostic( - fieldName: 'Description', - value: description, - maxLength: maxDescriptionLength, - docUrl: _descriptionFieldUrl, - ), - ), - ); - } - - return errors; - } -} diff --git a/tool/dart_skills_lint/lib/src/rules/disallowed_field_rule.dart b/tool/dart_skills_lint/lib/src/rules/disallowed_field_rule.dart deleted file mode 100644 index d4bab489..00000000 --- a/tool/dart_skills_lint/lib/src/rules/disallowed_field_rule.dart +++ /dev/null @@ -1,65 +0,0 @@ -// 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:yaml/yaml.dart'; -import '../models/analysis_severity.dart'; -import '../models/skill_context.dart'; -import '../models/skill_rule.dart'; -import '../models/validation_error.dart'; - -/// Enforces that only allowed fields are present in YAML metadata. -class DisallowedFieldRule extends SkillRule { - DisallowedFieldRule({this.severity = defaultSeverity}); - - static const String ruleName = 'disallowed-field'; - static const AnalysisSeverity defaultSeverity = AnalysisSeverity.disabled; - - @override - String get name => ruleName; - - @override - final AnalysisSeverity severity; - - static const _allowedFields = { - 'name', - 'description', - 'license', - 'allowed-tools', - 'metadata', - 'compatibility', - 'category', - 'tags', - 'version', - 'eval_task', - }; - - static const _skillFileName = 'SKILL.md'; - static const _metadataUrl = 'https://agentskills.io/specification#frontmatter'; - - @override - Future> validate(SkillContext context) async { - final errors = []; - - if (context.parsedYaml == null) { - return errors; - } - - final YamlMap yaml = context.parsedYaml!; - for (final Object? key in yaml.keys) { - final bool isDisallowed = key is! String || !_allowedFields.contains(key); - if (isDisallowed) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: 'Disallowed field: $key (see $_metadataUrl)', - ), - ); - } - } - - return errors; - } -} diff --git a/tool/dart_skills_lint/lib/src/rules/name_format_rule.dart b/tool/dart_skills_lint/lib/src/rules/name_format_rule.dart deleted file mode 100644 index 7fe79f15..00000000 --- a/tool/dart_skills_lint/lib/src/rules/name_format_rule.dart +++ /dev/null @@ -1,189 +0,0 @@ -// 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:meta/meta.dart'; -import 'package:path/path.dart'; -import 'package:yaml/yaml.dart'; -import '../fixable_rule.dart'; -import '../models/analysis_severity.dart'; -import '../models/skill_context.dart'; -import '../models/skill_rule.dart'; -import '../models/validation_error.dart'; - -/// Enforces constraints on the skill name field. -class NameFormatRule extends SkillRule implements FixableRule { - NameFormatRule({this.severity = defaultSeverity}); - - static const String ruleName = 'invalid-skill-name'; - static const AnalysisSeverity defaultSeverity = AnalysisSeverity.error; - - @override - String get name => ruleName; - - @override - final AnalysisSeverity severity; - - static const maxNameLength = 64; - static final _validNameRegex = RegExp(r'^[a-z0-9\-]+$'); - static const String _skillFileName = SkillContext.skillFileName; - static const _nameFieldUrl = 'https://agentskills.io/specification#name-field'; - - @override - Future> validate(SkillContext context) async { - final errors = []; - - if (context.parsedYaml == null) { - return errors; - } - - final YamlMap yaml = context.parsedYaml!; - final String skillName = getNameNode(yaml)?.value.toString() ?? ''; - - if (skillName.isEmpty) { - return errors; // Handled by required fields check - } - - final String suggestion = suggestNormalizedName(skillName); - - if (skillName != skillName.toLowerCase()) { - errors.add( - _buildNameFormatError( - 'Frontmatter `name` "$skillName" must be lowercase. ' - 'Suggested: "$suggestion"', - ), - ); - } - - if (skillName.length > maxNameLength) { - errors.add( - _buildNameFormatError( - 'Frontmatter `name` is ${skillName.length} characters; ' - 'maximum is $maxNameLength. ' - 'Shorten the `name:` field in SKILL.md.', - ), - ); - } - - if (!_validNameRegex.hasMatch(skillName)) { - errors.add( - _buildNameFormatError( - 'Frontmatter `name` "$skillName" contains invalid characters. ' - 'Only lowercase letters, digits, and hyphens are allowed. ' - 'Suggested: "$suggestion"', - ), - ); - } - - if (skillName.startsWith('-') || skillName.endsWith('-')) { - errors.add( - _buildNameFormatError( - 'Frontmatter `name` "$skillName" has leading or trailing hyphens. ' - 'Suggested: "$suggestion"', - ), - ); - } - - if (skillName.contains('--')) { - errors.add( - _buildNameFormatError( - 'Frontmatter `name` "$skillName" has consecutive hyphens. ' - 'Suggested: "$suggestion"', - ), - ); - } - - final String dirName = basename(context.directory.path); - if (skillName != dirName) { - errors.add( - _buildNameFormatError( - 'Frontmatter `name` "$skillName" does not match the parent ' - 'directory name "$dirName". ' - 'Fix by either setting `name: $dirName` in SKILL.md ' - 'or renaming the directory from "$dirName" to "$skillName".', - ), - ); - } - - return errors; - } - - ValidationError _buildNameFormatError(String message) => ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: '$message (see $_nameFieldUrl)', - ); - - /// Returns a best-effort normalization of [input] that conforms to the - /// skill name format: lowercase, hyphens only, no consecutive/leading/ - /// trailing hyphens, truncated to [maxNameLength]. - /// - /// This is intentionally a *suggestion* โ€” the author still picks the final - /// name. The output is not guaranteed to match a directory name. - @visibleForTesting - static String suggestNormalizedName(String input) { - String s = input.toLowerCase(); - s = s.replaceAll(RegExp(r'[^a-z0-9\-]+'), '-'); - s = s.replaceAll(RegExp(r'-+'), '-'); - s = s.replaceAll(RegExp(r'^-+|-+$'), ''); - if (s.length > maxNameLength) { - s = s.substring(0, maxNameLength); - s = s.replaceAll(RegExp(r'-+$'), ''); - } - return s; - } - - @override - Future fix(String filePath, String currentContent, Directory directory) async { - if (filePath != SkillContext.skillFileName) { - return currentContent; - } - - final RegExpMatch? match = SkillContext.skillStartRegex.firstMatch(currentContent); - if (match == null) { - return currentContent; - } - final String yamlStr = match.group(1)!; - - final Object? yamlObj; - try { - yamlObj = loadYaml(yamlStr); - } catch (e) { - return currentContent; - } - - if (yamlObj is! YamlMap) { - return currentContent; - } - - final YamlMap yaml = yamlObj; - final YamlNode? nameNode = getNameNode(yaml); - if (nameNode == null) { - return currentContent; - } - - final String dirName = basename(directory.path); - - final currentName = nameNode.value.toString(); - if (currentName == dirName) { - return currentContent; - } - - final int yamlOffset = currentContent.indexOf(yamlStr, match.start); - - // ignore: specify_nonobvious_local_variable_types - final span = nameNode.span; - final String before = currentContent.substring(0, yamlOffset + span.start.offset); - final String after = currentContent.substring(yamlOffset + span.end.offset); - - return '$before$dirName$after'; - } - - /// Returns the YAML node for the skill name. - @visibleForTesting - static YamlNode? getNameNode(YamlMap yaml) { - return yaml.nodes['name']; - } -} 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 deleted file mode 100644 index bf1734c2..00000000 --- a/tool/dart_skills_lint/lib/src/rules/path_does_not_exist_rule.dart +++ /dev/null @@ -1,85 +0,0 @@ -// 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/skill_context.dart'; -import '../models/skill_rule.dart'; -import '../models/validation_error.dart'; - -/// Checks that a skill directory exists and contains a SKILL.md file. -/// -/// If [excludeRegExp] is specified, it skips validation if the normalized -/// directory path matches the pattern. -/// Note on `exclude` regular expressions: To guarantee cross-platform portability across macOS, Linux, and Windows, path separators across evaluated absolute paths are **always normalized to forward slashes (`/`) prior to matching**. Always write `/` instead of `\` when separating directories within your regular expression exclusions. -class PathDoesNotExistRule extends SkillRule { - PathDoesNotExistRule({required this.severity, this.excludeRegExp}); - - static const String ruleName = 'path-does-not-exist'; - static const String excludeParameter = 'exclude'; - static const String _skillFileName = SkillContext.skillFileName; - static const String _dirStructureUrl = 'https://agentskills.io/specification#directory-structure'; - - @override - final AnalysisSeverity severity; - - /// Optional regex pattern to exclude matching directories. - /// Note: Target paths evaluated against this regex always normalize path - /// separators to forward slashes (`/`), even on Windows. - final RegExp? excludeRegExp; - - @override - String get name => ruleName; - - @override - Future> validate(SkillContext context) async { - final List errors = []; - final Directory dir = context.directory; - final String normalizedPath = dir.path.replaceAll(r'\', '/'); - - if (excludeRegExp != null && excludeRegExp!.hasMatch(normalizedPath)) { - 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; - } -} diff --git a/tool/dart_skills_lint/lib/src/rules/prevent_skills_sh_publishing_rule.dart b/tool/dart_skills_lint/lib/src/rules/prevent_skills_sh_publishing_rule.dart deleted file mode 100644 index 18ced959..00000000 --- a/tool/dart_skills_lint/lib/src/rules/prevent_skills_sh_publishing_rule.dart +++ /dev/null @@ -1,114 +0,0 @@ -// 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:yaml/yaml.dart'; -import '../models/analysis_severity.dart'; -import '../models/skill_context.dart'; -import '../models/skill_rule.dart'; -import '../models/validation_error.dart'; - -/// Enforces that skills are marked as internal to prevent accidental publishing to the public skills.sh registry. -/// This rule requires `metadata.internal` to be explicitly set to `true` in the SKILL.md YAML frontmatter. -class PreventSkillsShPublishingRule extends SkillRule { - PreventSkillsShPublishingRule({this.severity = defaultSeverity}); - - static const String ruleName = 'prevent-skills-sh-publishing'; - static const AnalysisSeverity defaultSeverity = AnalysisSeverity.disabled; - - @override - String get name => ruleName; - - @override - final AnalysisSeverity severity; - - static const _skillFileName = 'SKILL.md'; - - @override - Future> validate(SkillContext context) async { - final errors = []; - - if (context.yamlParsingError != null) { - return errors; - } - - if (context.parsedYaml == null) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: - 'Missing YAML frontmatter. Expected:\n' - 'metadata:\n' - ' internal: true', - ), - ); - return errors; - } - - final YamlMap yaml = context.parsedYaml!; - final Object? metadata = yaml['metadata']; - - if (metadata == null) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: - 'Missing "metadata" block in YAML frontmatter. Expected:\n' - 'metadata:\n' - ' internal: true', - ), - ); - return errors; - } - - if (metadata is! YamlMap) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: - '"metadata" must be a YAML mapping (dictionary). Expected:\n' - 'metadata:\n' - ' internal: true', - ), - ); - return errors; - } - - final Object? internalVal = metadata['internal']; - - if (internalVal is String && internalVal.trim().toLowerCase() == 'true') { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: - 'The "internal" field under "metadata" is set to a string "$internalVal". Please remove the quotes so it is parsed as a boolean.', - ), - ); - return errors; - } - - if (internalVal != true) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: - 'The "internal" field under "metadata" must be explicitly set to boolean true to prevent accidental publishing. Expected:\n' - 'metadata:\n' - ' internal: true', - ), - ); - } - - return errors; - } -} diff --git a/tool/dart_skills_lint/lib/src/rules/relative_paths_rule.dart b/tool/dart_skills_lint/lib/src/rules/relative_paths_rule.dart deleted file mode 100644 index ebedbcf9..00000000 --- a/tool/dart_skills_lint/lib/src/rules/relative_paths_rule.dart +++ /dev/null @@ -1,155 +0,0 @@ -// 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:meta/meta.dart'; -import 'package:path/path.dart'; -import '../levenshtein.dart'; -import '../models/analysis_severity.dart'; -import '../models/skill_context.dart'; -import '../models/skill_rule.dart'; -import '../models/validation_error.dart'; - -/// Enforces that relative links in SKILL.md point to existing files. -class RelativePathsRule extends SkillRule { - RelativePathsRule({this.severity = defaultSeverity}); - - static const String ruleName = 'check-relative-paths'; - static const AnalysisSeverity defaultSeverity = AnalysisSeverity.disabled; - - @override - String get name => ruleName; - - @override - final AnalysisSeverity severity; - - static const _skillFileName = 'SKILL.md'; - - @override - Future> validate(SkillContext context) async { - final errors = []; - - // Extract content after YAML frontmatter - final RegExpMatch? match = SkillContext.skillStartRegex.firstMatch(context.rawContent); - final String markdownContent = match != null - ? context.rawContent.substring(match.end) - : context.rawContent; - - for (final RegExpMatch linkMatch in SkillContext.markdownLinkRegex.allMatches( - markdownContent, - )) { - final String fullPath = linkMatch.group(1)!; - // Markdown links can have a title after the URL, separated by spaces. - // e.g. [text](url "title") - final String path = fullPath.trim().split(RegExp(r'\s+')).first; - - // Skip absolute paths (handled by AbsolutePathsRule) - if (isAbsolute(path) || windows.isAbsolute(path)) { - continue; - } - - var effectivePath = path; - try { - final Uri uri = Uri.parse(path); - if (uri.hasScheme || path.startsWith('#')) { - continue; // Ignore web URLs, email links, anchors, etc. - } - effectivePath = uri.path; - } catch (_) { - // If Uri parsing fails, treat it as a potential filepath. - } - - final String resolvedPath = absolute(normalize(join(context.directory.path, effectivePath))); - final linkedFile = File(resolvedPath); - if (!linkedFile.existsSync()) { - final String? suggestion = findSiblingSuggestion( - originalLink: path, - resolvedPath: resolvedPath, - ); - final suggestionClause = suggestion != null ? ' Did you mean "$suggestion"?' : ''; - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: - 'Linked file does not exist: $path (resolved to $resolvedPath).' - '$suggestionClause', - ), - ); - } - } - - return errors; - } -} - -/// Looks for a near-miss sibling **file** next to the missing -/// [resolvedPath] and, if one exists, returns the full suggested link as -/// it should appear in the SKILL.md author's markdown โ€” the original -/// link's directory prefix joined to the matched basename, normalized to -/// forward slashes so the suggestion is portable across platforms. -/// -/// Returns `null` when: -/// - the original link has no parent dir on disk, -/// - the parent dir can't be listed (e.g. permission error), -/// - or no candidate is close enough to the missing basename. -/// -/// [originalLink] is the link text as written in the SKILL.md -/// (`docs/DEATILS.md`); [resolvedPath] is the same link resolved -/// against the skill directory (`/abs/path/skill/docs/DEATILS.md`). -/// -/// Subdirectories of the parent are intentionally excluded from the -/// candidate set โ€” links almost always point at files, and suggesting -/// a directory would be misleading. -@visibleForTesting -String? findSiblingSuggestion({required String originalLink, required String resolvedPath}) { - final String parentPath = dirname(resolvedPath); - final parentDir = Directory(parentPath); - if (!parentDir.existsSync()) { - return null; - } - - final String missingBase = basename(resolvedPath).toLowerCase(); - if (missingBase.isEmpty) { - return null; - } - - // Tunable; chosen to balance typo recall against false positives. - final int threshold = (missingBase.length ~/ 3).clamp(1, missingBase.length); - - final List entries; - try { - entries = parentDir.listSync(); - } on FileSystemException { - return null; - } - - String? best; - int bestDistance = threshold + 1; - for (final entity in entries) { - if (entity is Directory) { - continue; - } - final String candidate = basename(entity.path); - if (candidate == basename(resolvedPath)) { - continue; - } - final int distance = levenshtein(missingBase, candidate.toLowerCase()); - if (distance < bestDistance) { - bestDistance = distance; - best = candidate; - } - } - - if (best == null || bestDistance > threshold) { - return null; - } - - final String dir = dirname(originalLink); - if (dir == '.' || dir.isEmpty) { - return best; - } - return join(dir, best).replaceAll(r'\', '/'); -} diff --git a/tool/dart_skills_lint/lib/src/rules/trailing_whitespace_rule.dart b/tool/dart_skills_lint/lib/src/rules/trailing_whitespace_rule.dart deleted file mode 100644 index c2e7e472..00000000 --- a/tool/dart_skills_lint/lib/src/rules/trailing_whitespace_rule.dart +++ /dev/null @@ -1,93 +0,0 @@ -// 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:meta/meta.dart'; - -import '../fixable_rule.dart'; -import '../models/analysis_severity.dart'; -import '../models/skill_context.dart'; -import '../models/skill_rule.dart'; -import '../models/validation_error.dart'; - -/// Enforces that lines in SKILL.md do not have trailing whitespace, -/// except for exactly two spaces which indicate a hard line break. -class TrailingWhitespaceRule extends SkillRule implements FixableRule { - TrailingWhitespaceRule({this.severity = defaultSeverity}); - - static const String ruleName = 'check-trailing-whitespace'; - static const AnalysisSeverity defaultSeverity = AnalysisSeverity.disabled; - static final RegExp _whitespaceRegExp = RegExp(r'([ \t]+)$'); - - @override - String get name => ruleName; - - @override - final AnalysisSeverity severity; - - @override - Future> validate(SkillContext context) async { - final errors = []; - final List lines = context.rawContent.split('\n'); - - for (var i = 0; i < lines.length; i++) { - final String line = lines[i]; - - // Remove carriage return if present (Windows line endings) - final String trimmedLine = line.endsWith('\r') ? line.substring(0, line.length - 1) : line; - - final RegExpMatch? match = _whitespaceRegExp.firstMatch(trimmedLine); - if (match != null) { - final String whitespace = match.group(1)!; - String? message; - - if (whitespace.contains('\t')) { - message = 'Line ${i + 1} has trailing whitespace containing tabs.'; - } else { - final int spacesCount = whitespace.length; - if (spacesCount == 1 || spacesCount >= 3) { - message = - 'Line ${i + 1} has $spacesCount trailing space(s). Only exactly 2 spaces are allowed for line breaks.'; - } - } - - if (message != null) { - errors.add( - ValidationError(ruleId: name, severity: severity, file: 'SKILL.md', message: message), - ); - } - } - } - - return errors; - } - - @override - Future fix(String filePath, String currentContent, Directory directory) async { - if (filePath != 'SKILL.md') { - return currentContent; - } - - return currentContent.split('\n').map(fixLine).join('\n'); - } - - @visibleForTesting - String fixLine(String line) { - final bool hasCR = line.endsWith('\r'); - final String lineWithoutCR = hasCR ? line.substring(0, line.length - 1) : line; - - final RegExpMatch? match = _whitespaceRegExp.firstMatch(lineWithoutCR); - if (match == null) { - return line; - } - - final String whitespace = match.group(1)!; - if (whitespace == ' ') { - return line; // Keep the 2 space hard line break. - } - - final String fixedLine = lineWithoutCR.replaceAll(_whitespaceRegExp, ''); - return hasCR ? '$fixedLine\r' : fixedLine; - } -} diff --git a/tool/dart_skills_lint/lib/src/rules/valid_yaml_metadata_rule.dart b/tool/dart_skills_lint/lib/src/rules/valid_yaml_metadata_rule.dart deleted file mode 100644 index 868a1ffa..00000000 --- a/tool/dart_skills_lint/lib/src/rules/valid_yaml_metadata_rule.dart +++ /dev/null @@ -1,83 +0,0 @@ -// 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:yaml/yaml.dart'; -import '../cutoff_excerpt.dart'; -import '../models/analysis_severity.dart'; -import '../models/skill_context.dart'; -import '../models/skill_rule.dart'; -import '../models/validation_error.dart'; - -/// Enforces that SKILL.md has valid YAML frontmatter and required fields. -class ValidYamlMetadataRule extends SkillRule { - ValidYamlMetadataRule({this.severity = defaultSeverity}); - - static const String ruleName = 'valid-yaml-metadata'; - static const AnalysisSeverity defaultSeverity = AnalysisSeverity.error; - - @override - String get name => ruleName; - - @override - final AnalysisSeverity severity; - - static const _requiredFields = {'name', 'description'}; - static const _skillFileName = 'SKILL.md'; - static const _metadataUrl = 'https://agentskills.io/specification#frontmatter'; - static const maxCompatibilityLength = 500; - static const _compatibilityFieldUrl = 'https://agentskills.io/specification#compatibility-field'; - - @override - Future> validate(SkillContext context) async { - final errors = []; - - if (context.parsedYaml == null) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: - 'Invalid YAML metadata: ${context.yamlParsingError ?? 'Missing or invalid'} (see $_metadataUrl)', - ), - ); - return errors; - } - - final YamlMap yaml = context.parsedYaml!; - for (final String field in _requiredFields) { - if (!yaml.containsKey(field)) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: 'Missing required field: $field (see $_metadataUrl)', - ), - ); - } - } - - if (yaml.containsKey('compatibility')) { - final String compatibility = yaml['compatibility']?.toString() ?? ''; - if (compatibility.length > maxCompatibilityLength) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: _skillFileName, - message: buildLengthDiagnostic( - fieldName: 'Compatibility', - value: compatibility, - maxLength: maxCompatibilityLength, - docUrl: _compatibilityFieldUrl, - ), - ), - ); - } - } - - return errors; - } -} diff --git a/tool/dart_skills_lint/lib/src/skills_ignores_storage.dart b/tool/dart_skills_lint/lib/src/skills_ignores_storage.dart deleted file mode 100644 index ac5da966..00000000 --- a/tool/dart_skills_lint/lib/src/skills_ignores_storage.dart +++ /dev/null @@ -1,36 +0,0 @@ -// 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:convert'; -import 'dart:io'; - -import 'models/skills_ignores.dart'; - -/// Service class for reading and writing the `SkillsIgnores` model to/from disk. -class SkillsIgnoresStorage { - /// Loads `SkillsIgnores` from the specified path. - /// - /// Returns an empty `SkillsIgnores` if the file does not exist or fails to parse. - Future load(String path) async { - final file = File(path); - if (!file.existsSync()) { - return SkillsIgnores(skills: {}); - } - - try { - final String content = await file.readAsString(); - final json = jsonDecode(content) as Map; - return SkillsIgnores.fromJson(json); - } catch (_) { - return SkillsIgnores(skills: {}); - } - } - - /// Saves `SkillsIgnores` to the specified path. - Future save(String path, SkillsIgnores ignores) async { - final file = File(path); - final String jsonString = const JsonEncoder.withIndent(' ').convert(ignores.toJson()); - await file.writeAsString(jsonString); - } -} diff --git a/tool/dart_skills_lint/lib/src/validation_session.dart b/tool/dart_skills_lint/lib/src/validation_session.dart deleted file mode 100644 index b7a8f703..00000000 --- a/tool/dart_skills_lint/lib/src/validation_session.dart +++ /dev/null @@ -1,727 +0,0 @@ -// 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:convert'; -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; - -import 'config_parser.dart'; -import 'fixable_rule.dart'; -import 'models/analysis_severity.dart'; -import 'models/check_type.dart'; -import 'models/ignore_entry.dart'; -import 'models/rule_config.dart'; -import 'models/skill_context.dart'; -import 'models/skill_rule.dart'; -import 'models/skills_ignores.dart'; -import 'models/validation_error.dart'; -import 'path_utils.dart'; -import 'rule_registry.dart'; -import 'skills_ignores_storage.dart'; -import 'validator.dart'; - -final _log = Logger('dart_skills_lint'); - -/// Default filename for the per-run ignore baseline file. -/// -/// Referenced both by production code (the `--generate-baseline` help text in -/// the CLI) and by tests, so this is intentionally not `@visibleForTesting`. -const defaultIgnoreFileName = 'dart_skills_lint_ignore.json'; - -@visibleForTesting -const skillIsValidMsg = ' Skill is valid.'; -@visibleForTesting -const skillIsInvalidMsg = ' Skill is invalid:'; -@visibleForTesting -const warningsMsg = 'Warnings:'; - -@visibleForTesting -const evaluatingDirMsg = 'Evaluating directory:'; - -@visibleForTesting -const directoryErrorMsg = 'Directory error:'; - -/// Per-invocation state and orchestration for skill validation. -/// -/// One session is constructed per CLI invocation (or embedded call). The -/// session aggregates configuration parameters, 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. -/// 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. - /// * [resolvedRuleConfigs] maps rule names to rule configuration patches (e.g., CLI-passed flags). - /// * [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, - // TODO(reidbaker): https://github.com/flutter/agent-plugins/issues/179 - @Deprecated('Use resolvedRuleConfigs instead') - Map resolvedRules = const {}, - Map resolvedRuleConfigs = const {}, - required this.ignoreFileOverride, - required this.customRules, - required this.printWarnings, - required this.fastFail, - required this.quiet, - required this.generateBaseline, - required this.fix, - required this.fixApply, - }) : resolvedRuleConfigs = _mergeDeprecatedRules(resolvedRules, resolvedRuleConfigs), - _normalizedDirectoryConfigs = [ - for (final dc in [...config.directoryConfigs, ...config.individualSkillConfigs]) - (normalizedPath: p.absolute(p.normalize(expandPath(dc.path))), config: dc), - ]; - - static Map _mergeDeprecatedRules( - Map deprecatedRules, - Map configPatches, - ) { - if (deprecatedRules.isEmpty && configPatches.isEmpty) { - return const {}; - } - if (deprecatedRules.isNotEmpty && configPatches.isNotEmpty) { - throw ArgumentError( - 'Cannot specify both deprecated resolvedRules and new resolvedRuleConfigs. ' - 'Please migrate all overrides to resolvedRuleConfigs.', - ); - } - final merged = Map.from(configPatches); - for (final MapEntry entry in deprecatedRules.entries) { - merged[entry.key] = RuleConfigPatch(severity: entry.value); - } - return merged; - } - - final Configuration config; - final Map resolvedRuleConfigs; - final String? ignoreFileOverride; - final List customRules; - final bool printWarnings; - final bool fastFail; - final bool quiet; - final bool generateBaseline; - final bool fix; - final bool fixApply; - - /// [config.directoryConfigs] with each `path` pre-normalized once. - /// - /// `config` is static for the lifetime of a session, so we pay the - /// `p.normalize` cost up front instead of once per skill in - /// [_resolveRulesForPath] and [_resolveIgnoreFile]. - final List<({String normalizedPath, LintTargetConfig config})> _normalizedDirectoryConfigs; - - bool _anyFailed = false; - bool _anySkillsValidated = false; - - bool get anyFailed => _anyFailed; - bool get anySkillsValidated => _anySkillsValidated; - - /// Validates a single skill directory passed via `--skill` / `-s`. - /// - /// Returns `true` if the caller should continue iterating, `false` to - /// stop. Only a real validation failure under [fastFail] returns `false`; - /// a missing directory contributes to [anyFailed] but still allows the - /// caller to continue. - Future processIndividualSkill(String skillPath) async { - final String normalizedSkillPath = p.normalize(expandPath(skillPath)); - if (!quiet) { - _log.info('$evaluatingDirMsg $normalizedSkillPath'); - } - final skillDir = Directory(normalizedSkillPath); - - if (!skillDir.existsSync()) { - _log.severe('Specified skill directory does not exist: $normalizedSkillPath'); - _anyFailed = true; - return true; - } - - final Map resolvedConfigs = resolveRuleConfigsForPath(normalizedSkillPath); - final String? localIgnoreFile = resolveIgnoreFile(normalizedSkillPath); - final validator = Validator(ruleConfigs: resolvedConfigs, customRules: customRules); - - final String ignorePath = _resolveIgnorePath(localIgnoreFile, skillDir); - final SkillsIgnores ignores = await _loadIgnores( - ignorePath, - isCustomIgnoreFile: localIgnoreFile != null, - ); - final String skillName = p.basename(skillDir.path); - final List skillIgnores = ignores.skills[skillName] ?? []; - - _anySkillsValidated = true; - final ValidationResult finalResult = await _runValidationWorkflow( - skillDir: skillDir, - validator: validator, - ignores: ignores, - ); - - if (generateBaseline) { - await _saveBaseline(ignorePath, ignores); - } else { - final String fullPath = p.absolute(skillDir.path); - for (final ignore in skillIgnores) { - if (!ignore.used) { - _log.info( - "Stale ignore entry found for rule '${ignore.ruleId}' in skill " - "'$skillName' at '$fullPath'. Consider removing it.", - ); - } - } - } - - if (!finalResult.isValid) { - _anyFailed = true; - if (fastFail) { - return false; - } - } - return true; - } - - /// Validates every skill directory under a root passed via - /// `--skills-directory` / `-d`. - /// - /// Returns `true` if the caller should continue iterating, `false` to - /// stop. Missing-root and listing-failure errors contribute to [anyFailed] - /// but allow the caller to continue. After a successful iteration, returns - /// `false` if [fastFail] is set and any failure has accumulated across the - /// run so far. - Future processSkillRoot(String rootPath) async { - final String normalizedRootPath = p.normalize(expandPath(rootPath)); - if (!quiet) { - _log.info('$evaluatingDirMsg $normalizedRootPath'); - } - final rootDir = Directory(normalizedRootPath); - - if (!rootDir.existsSync()) { - _log.severe('Specified root directory does not exist: $normalizedRootPath'); - _anyFailed = true; - return true; - } - - List entities; - try { - entities = await rootDir.list().toList(); - } catch (_) { - _log.severe(' $directoryErrorMsg'); - _log.severe(' - Failed to list children of: $normalizedRootPath'); - _anyFailed = true; - return true; - } - entities.sort((a, b) => a.path.compareTo(b.path)); - - // Keep a cache of loaded ignores to avoid loading/saving the same ignore file multiple times, - // and to accumulate ignore usages correctly across all skills. - final Map loadedIgnoresCache = {}; - - for (final entity in entities) { - if (entity is! Directory || p.basename(entity.path).startsWith('.')) { - continue; - } - - final bool shouldContinue = await _processRootSkillEntity( - entity, - rootDir, - loadedIgnoresCache, - ); - if (!shouldContinue) { - break; - } - } - - await _finalizeIgnoresForRoot(loadedIgnoresCache, rootDir); - - return !(_anyFailed && fastFail); - } - - /// Processes and validates a single skill directory ([entity]) located - /// immediately inside a skills root directory ([rootDir]). - /// - /// In this context, "root" refers to the container directory passed via - /// `--skills-directory` / `-d` (represented by [rootDir]), which holds one or - /// more child skill folders. [entity] is an individual skill folder within - /// that root container. - /// - /// Returns `true` if iteration over the remaining skills in [rootDir] should - /// continue, or `false` to abort early when [fastFail] is enabled and this - /// skill failed validation. - Future _processRootSkillEntity( - Directory entity, - Directory rootDir, - Map loadedIgnoresCache, - ) async { - final String normalizedSkillPath = p.normalize(entity.path); - final Map resolvedConfigs = resolveRuleConfigsForPath(normalizedSkillPath); - final String? localIgnoreFile = resolveIgnoreFile(normalizedSkillPath); - final validator = Validator(ruleConfigs: resolvedConfigs, customRules: customRules); - - final SkillsIgnores ignores = await _getIgnoresForSkill( - localIgnoreFile, - rootDir, - loadedIgnoresCache, - ); - - _anySkillsValidated = true; - final ValidationResult finalResult = await _runValidationWorkflow( - skillDir: entity, - validator: validator, - ignores: ignores, - ); - - if (!finalResult.isValid) { - _anyFailed = true; - if (fastFail) { - return false; - } - } - return true; - } - - Future _getIgnoresForSkill( - String? localIgnoreFile, - Directory rootDir, - Map loadedIgnoresCache, - ) async { - final String ignorePath = _resolveIgnorePath(localIgnoreFile, rootDir); - - if (loadedIgnoresCache.containsKey(ignorePath)) { - return loadedIgnoresCache[ignorePath]!; - } - - final SkillsIgnores ignores = await _loadIgnores( - ignorePath, - isCustomIgnoreFile: localIgnoreFile != null, - ); - loadedIgnoresCache[ignorePath] = ignores; - return ignores; - } - - Future _finalizeIgnoresForRoot( - Map loadedIgnoresCache, - Directory rootDir, - ) async { - for (final MapEntry entry in loadedIgnoresCache.entries) { - final String ignorePath = entry.key; - final SkillsIgnores ignores = entry.value; - - if (generateBaseline) { - await _saveBaseline(ignorePath, ignores); - } else { - _reportStaleIgnores(ignores, rootDir); - } - } - } - - void _reportStaleIgnores(SkillsIgnores ignores, Directory rootDir) { - for (final MapEntry> skillEntry in ignores.skills.entries) { - final String skillName = skillEntry.key; - for (final IgnoreEntry ignore in skillEntry.value) { - if (!ignore.used) { - final String fullPath = p.absolute(p.join(rootDir.path, skillName)); - _log.info( - "Stale ignore entry found for rule '${ignore.ruleId}' in skill " - "'$skillName' at '$fullPath'. Consider removing it.", - ); - } - } - } - } - - /// If no skills were validated across the whole run, emit appropriate - /// diagnostics and mark the session as failed. - void reportNoSkillsValidated(List rootPaths) { - if (_anySkillsValidated) { - return; - } - - var foundSingleSkillPassedToD = false; - for (final rootPath in rootPaths) { - final String expandedRootPath = expandPath(rootPath); - final skillMdFile = File(p.join(expandedRootPath, SkillContext.skillFileName)); - if (skillMdFile.existsSync()) { - _log.severe( - 'Directory "$expandedRootPath" appears to be an individual skill. ' - 'Use --skill / -s instead of -d / --skills-directory.', - ); - foundSingleSkillPassedToD = true; - } - } - if (!foundSingleSkillPassedToD) { - _log.severe('No skills found to validate in the specified directories.'); - } - _anyFailed = true; - } - - // TODO(reidbaker): https://github.com/flutter/agent-plugins/issues/179 - @Deprecated('Use resolveRuleConfigsForPath instead') - Map resolveRulesForPath(String path) { - return resolveRuleConfigsForPath( - path, - ).map((String key, RuleConfig value) => MapEntry(key, value.severity)); - } - - @visibleForTesting - Map resolveRuleConfigsForPath(String path) { - final String normalizedPath = p.absolute(path); - final resolvedConfigs = {}; - - // Initialize with all checks defaults - for (final CheckType check in RuleRegistry.allChecks) { - resolvedConfigs[check.name] = RuleConfig(severity: check.defaultSeverity); - } - - void applyPatchMap(Map patches) { - for (final MapEntry entry in patches.entries) { - final String ruleName = entry.key; - final RuleConfigPatch patch = entry.value; - - final RuleConfig base = - resolvedConfigs[ruleName] ?? RuleConfig(severity: AnalysisSeverity.disabled); - resolvedConfigs[ruleName] = patch.applyTo(base); - } - } - - // 1. Global Config (from YAML) - applyPatchMap(config.ruleConfigs); - - // 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)) { - applyPatchMap(entry.config.ruleConfigs); - } - } - - // 3. Overrides (CLI flags or API caller) take highest precedence - applyPatchMap(resolvedRuleConfigs); - - return resolvedConfigs; - } - - @visibleForTesting - String? resolveIgnoreFile(String path) { - final String normalizedPath = p.absolute(path); - if (ignoreFileOverride != null) { - return ignoreFileOverride; - } - String? resolvedIgnoreFile; - for (final ({String normalizedPath, LintTargetConfig config}) entry - in _normalizedDirectoryConfigs) { - final String configPath = entry.normalizedPath; - if (p.equals(configPath, normalizedPath) || p.isWithin(configPath, normalizedPath)) { - final String? ignoreFile = entry.config.ignoreFile; - if (ignoreFile != null) { - resolvedIgnoreFile = ignoreFile; - } - } - } - return resolvedIgnoreFile; - } - - String _resolveIgnorePath(String? localIgnoreFile, Directory rootDir) { - return localIgnoreFile != null - ? p.normalize(expandPath(localIgnoreFile)) - : p.join(rootDir.path, defaultIgnoreFileName); - } - - /// Loads the ignore JSON from [ignorePath], returning the parsed [SkillsIgnores]. - /// - /// If [isCustomIgnoreFile] is true and the file does not exist, generates an - /// empty baseline file on disk. - Future _loadIgnores(String ignorePath, {required bool isCustomIgnoreFile}) async { - final file = File(ignorePath); - - if (file.existsSync()) { - final storage = SkillsIgnoresStorage(); - return storage.load(ignorePath); - } - - // If a custom ignore file was specified but not found, create an empty one - // so the user can start adding ignores to it. - if (isCustomIgnoreFile) { - _log.warning('File not found generating-baseline'); - try { - await file.writeAsString(jsonEncode({SkillsIgnores.skillsKey: {}})); - } catch (_) { - // Ignore write errors, we will just return empty ignores. - } - } - - return SkillsIgnores(skills: {}); - } - - void _applyIgnores(ValidationResult result, List ignores) { - // Pre-normalize ignore filenames once so the inner loop below is a - // straight string comparison instead of repeated path normalization. - final List<({IgnoreEntry entry, String normalizedFileName})> preNormalizedIgnores = [ - for (final ignore in ignores) - (entry: ignore, normalizedFileName: p.normalize(ignore.fileName)), - ]; - - for (final ValidationError error in result.validationErrors) { - if (error.isIgnored) { - continue; - } - final String normalizedErrorFile = p.normalize(error.file); - for (final pair in preNormalizedIgnores) { - final IgnoreEntry ignore = pair.entry; - if (ignore.ruleId == error.ruleId && pair.normalizedFileName == normalizedErrorFile) { - error.isIgnored = true; - ignore.used = true; - break; - } - } - } - } - - /// Validates [skillDir], applies fixes if requested, and (when - /// [generateBaseline] is set) updates [ignores] in memory with any new - /// baseline entries for this skill. The caller is responsible for - /// persisting [ignores] to disk once after all skills are processed โ€” - /// see [_saveBaseline]. - Future _runValidationWorkflow({ - required Directory skillDir, - required Validator validator, - required SkillsIgnores ignores, - }) async { - final String skillName = p.basename(skillDir.path); - final List skillIgnores = ignores.skills[skillName] ?? []; - - final ValidationResult result = await _validateSingleSkill( - skillDir: skillDir, - validator: validator, - skillIgnores: skillIgnores, - ); - - final ValidationResult finalResult = await _applyFixesIfNeeded( - skillDir: skillDir, - result: result, - validator: validator, - skillIgnores: skillIgnores, - ); - - if (generateBaseline) { - _updateBaselineForSkill(ignores, finalResult, skillName); - } - - return finalResult; - } - - Future _validateSingleSkill({ - required Directory skillDir, - required Validator validator, - required List skillIgnores, - }) async { - final String skillName = p.basename(skillDir.path); - if (!quiet) { - _log.info('--- Validating skill: $skillName ---'); - } - final ValidationResult result = await validator.validate(skillDir); - _applyIgnores(result, skillIgnores); - _printValidationResult(result); - return result; - } - - Future _applyFixesIfNeeded({ - required Directory skillDir, - required ValidationResult result, - required Validator validator, - required List skillIgnores, - }) async { - if (!fix && !fixApply) { - return result; - } - - final SkillContext? context = result.context; - if (context == null) { - return result; - } - - final skillMdFile = File(p.join(skillDir.path, SkillContext.skillFileName)); - if (!skillMdFile.existsSync()) { - return result; - } - - final String fixedContent = await _runFixableRules( - context: context, - result: result, - validator: validator, - ); - - if (fixedContent == context.rawContent) { - return result; - } - - return _handleFixResult( - skillDir: skillDir, - skillMdFile: skillMdFile, - originalContent: context.rawContent, - currentContent: fixedContent, - validator: validator, - skillIgnores: skillIgnores, - fallbackResult: result, - ); - } - - /// Runs all fixable rules against [context.rawContent] sequentially and - /// returns the resulting content string. - Future _runFixableRules({ - required SkillContext context, - required ValidationResult result, - required Validator validator, - }) async { - String currentContent = context.rawContent; - - for (final SkillRule rule in validator.rules) { - if (rule is! FixableRule) { - continue; - } - final bool hasErrors = result.validationErrors.any( - (e) => e.ruleId == rule.name && !e.isIgnored, - ); - if (!hasErrors) { - continue; - } - - try { - final String newContent = await rule.fix( - SkillContext.skillFileName, - currentContent, - context.directory, - ); - currentContent = newContent; - } catch (e) { - _log.severe(" Failed to apply fix for rule '${rule.name}': $e"); - } - } - - return currentContent; - } - - Future _handleFixResult({ - required Directory skillDir, - required File skillMdFile, - required String originalContent, - required String currentContent, - required Validator validator, - required List skillIgnores, - required ValidationResult fallbackResult, - }) async { - final String skillName = p.basename(skillDir.path); - if (fixApply) { - await skillMdFile.writeAsString(currentContent); - if (!quiet) { - _log.info(' Applied fixes for $skillName'); - } - final ValidationResult newResult = await validator.validate(skillDir); - _applyIgnores(newResult, skillIgnores); - return newResult; - } - if (fix && !quiet) { - _log.info(' [Dry Run] Proposed changes for $skillName (SKILL.md):'); - _printDiff(originalContent, currentContent); - } - return fallbackResult; - } - - /// Prints a simple line-by-line diff between [original] and [modified]. - /// - /// **Limitation**: This naive diff algorithm does not handle line additions - /// or removals well, as it compares lines at the same index. It is - /// sufficient for current fixers that only modify existing lines, but - /// should be replaced with a more robust diffing solution (e.g., - /// `package:diff`) if future fixers add or remove lines. - void _printDiff(String original, String modified) { - final List origLines = original.split('\n'); - final List modLines = modified.split('\n'); - final int maxLines = origLines.length > modLines.length ? origLines.length : modLines.length; - for (var i = 0; i < maxLines; i++) { - final String orig = i < origLines.length ? origLines[i] : ''; - final String mod = i < modLines.length ? modLines[i] : ''; - if (orig != mod) { - if (orig.isNotEmpty) { - _log.info('- Line ${i + 1}: $orig'); - } - if (mod.isNotEmpty) { - _log.info('+ Line ${i + 1}: $mod'); - } - } - } - } - - /// Mutates [ignores] in place to add baseline entries for any non-ignored - /// errors in [result] under the [skillName] key. Pure in-memory operation - /// โ€” pair with [_saveBaseline] to persist changes. - void _updateBaselineForSkill(SkillsIgnores ignores, ValidationResult result, String skillName) { - final List currentSkillIgnores = ignores.skills[skillName] ?? []; - final currentSkillSeen = {}; - for (final ignore in currentSkillIgnores) { - currentSkillSeen.add('${ignore.ruleId}:${ignore.fileName}'); - } - - for (final ValidationError error in result.validationErrors) { - if (!error.isIgnored) { - final key = '${error.ruleId}:${error.file}'; - if (currentSkillSeen.contains(key)) { - continue; - } - currentSkillSeen.add(key); - - currentSkillIgnores.add(IgnoreEntry(ruleId: error.ruleId, fileName: error.file)); - } - } - - if (currentSkillIgnores.isNotEmpty) { - ignores.skills[skillName] = currentSkillIgnores; - } else { - ignores.skills.remove(skillName); - } - } - - /// Writes [ignores] to [ignorePath]. Write failures are logged at warning - /// level and otherwise swallowed so a single I/O error does not abort the - /// rest of the run. - Future _saveBaseline(String ignorePath, SkillsIgnores ignores) async { - try { - await SkillsIgnoresStorage().save(ignorePath, ignores); - } catch (e) { - _log.warning('Failed to generate baseline file at $ignorePath: $e'); - } - } - - void _printValidationResult(ValidationResult result) { - if (result.isValid) { - if (!quiet) { - _log.info(' $skillIsValidMsg'); - } - } else { - _log.severe(' $skillIsInvalidMsg'); - for (final String error in result.errors) { - _log.severe(' - $error'); - } - } - - if (printWarnings && result.warnings.isNotEmpty) { - _log.warning(' $warningsMsg'); - for (final String warning in result.warnings) { - _log.warning(' - $warning'); - } - } - } -} diff --git a/tool/dart_skills_lint/lib/src/validator.dart b/tool/dart_skills_lint/lib/src/validator.dart deleted file mode 100644 index 262b073a..00000000 --- a/tool/dart_skills_lint/lib/src/validator.dart +++ /dev/null @@ -1,236 +0,0 @@ -// 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:logging/logging.dart'; -import 'package:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -import 'models/analysis_severity.dart'; -import 'models/check_type.dart'; -import 'models/rule_config.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'; -import 'rules/path_does_not_exist_rule.dart'; - -// TODO(reidbaker): https://github.com/flutter/agent-plugins/issues/179 -export 'models/validation_result.dart'; - -final _log = Logger('dart_skills_lint'); - -/// Validates agent skill directories against the Agent Skills specification. -class Validator { - /// Creates a validator with optional rule configurations and custom rules. - /// - /// * [ruleConfigs] defines resolved severity and options for the validation rules. - /// * [customRules] specifies custom rules to be included in the validation. - Validator({ - // TODO(reidbaker): https://github.com/flutter/agent-plugins/issues/179 - @Deprecated('Use ruleConfigs instead') Map? ruleOverrides, - Map? ruleConfigs, - List? customRules, - }) : _ruleConfigs = _mergeOverrides(ruleOverrides, ruleConfigs), - _rules = _buildRules(_mergeOverrides(ruleOverrides, ruleConfigs), customRules ?? []); - - static Map _mergeOverrides( - Map? deprecatedOverrides, - Map? configOverrides, - ) { - if (deprecatedOverrides == null && configOverrides == null) { - return {}; - } - if (deprecatedOverrides != null && - deprecatedOverrides.isNotEmpty && - configOverrides != null && - configOverrides.isNotEmpty) { - throw ArgumentError( - 'Cannot specify both deprecated ruleOverrides and new ruleConfigs. ' - 'Please migrate all overrides to ruleConfigs.', - ); - } - final merged = Map.from(configOverrides ?? {}); - if (deprecatedOverrides != null) { - for (final MapEntry entry in deprecatedOverrides.entries) { - merged[entry.key] = RuleConfig(severity: entry.value); - } - } - return merged; - } - - static const String _skillFileName = SkillContext.skillFileName; - - /// The name of the special check for missing files or directories. - static const String pathDoesNotExist = 'path-does-not-exist'; - - /// The name of the special check for inaccessible files. - static const String skillFileInaccessible = 'skill-file-inaccessible'; - - /// The name of the special check for unexpected errors. - static const String unexpectedError = 'unexpected-error'; - - final Map _ruleConfigs; - final List _rules; - - /// Returns the rules used by this validator. - List get rules => _rules; - - AnalysisSeverity _getSeverity(String name, AnalysisSeverity defaultSeverity) { - return _ruleConfigs[name]?.severity ?? defaultSeverity; - } - - /// Validates a single skill directory. - /// - /// Scans the directory for `SKILL.md`, parses its YAML metadata, and validates - /// constraints like name format and field lengths using registered rules. - Future validate(Directory dir) async { - final skillMdFile = File(p.join(dir.path, _skillFileName)); - final bool skillMdExists = dir.existsSync() && skillMdFile.existsSync(); - - final fatalErrors = []; - final SkillContext? context = await _buildContext(dir, skillMdFile, skillMdExists, fatalErrors); - if (context == null) { - return ValidationResult(validationErrors: fatalErrors); - } - - final validationErrors = []; - - for (final SkillRule rule in _rules) { - // If SKILL.md or the directory does not exist or is inaccessible, running content validation rules - // against empty or non-existent content produces redundant cascading errors. We run solely PathDoesNotExistRule - // to report the missing structure cleanly, skipping subsequent rules. - if (!skillMdExists && rule.name != PathDoesNotExistRule.ruleName) { - continue; - } - final List errors = await rule.validate(context); - _checkSeverityWarnings(rule, errors); - validationErrors.addAll(errors); - } - - return ValidationResult(validationErrors: validationErrors, context: context); - } - - /// Reads the skill file content and parses its YAML frontmatter to build a [SkillContext]. - /// - /// Appends any disk-read exception to [fatalErrors] and returns `null` if - /// [skillMdFile] cannot be read from disk. - Future _buildContext( - Directory dir, - File skillMdFile, - bool skillMdExists, - List fatalErrors, - ) async { - if (!skillMdExists) { - return SkillContext(directory: dir, rawContent: ''); - } - - final String content; - try { - content = await skillMdFile.readAsString(); - } on FileSystemException catch (e) { - fatalErrors.add( - ValidationError( - ruleId: skillFileInaccessible, - file: skillMdFile.path, - message: 'Failed to read $_skillFileName: $e', - severity: _getSeverity(skillFileInaccessible, AnalysisSeverity.error), - ), - ); - return null; - } catch (e) { - fatalErrors.add( - ValidationError( - ruleId: unexpectedError, - file: skillMdFile.path, - message: 'Unexpected error reading $_skillFileName: $e', - severity: _getSeverity(unexpectedError, AnalysisSeverity.error), - ), - ); - return null; - } - - YamlMap? parsedYaml; - String? yamlParsingError; - try { - final RegExpMatch? match = SkillContext.skillStartRegex.firstMatch(content); - if (match == null) { - yamlParsingError = 'Missing YAML metadata in $_skillFileName'; - } else { - 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'; - } - } - } catch (e) { - yamlParsingError = 'Failed to parse YAML: $e'; - } - - return SkillContext( - directory: dir, - rawContent: content, - parsedYaml: parsedYaml, - yamlParsingError: yamlParsingError, - ); - } - - void _checkSeverityWarnings(SkillRule rule, List errors) { - for (final error in errors) { - if (error.severity != rule.severity) { - _log.warning( - 'Rule "${rule.name}" used severity ${error.severity} instead of defined ${rule.severity}.', - ); - } - } - } - - /// Compiles the final list of active rules for the validator. - /// - /// * [ruleConfigs] resolved rules configurations mapping. - /// * [customRules] specifies custom rules to be included in the validation. - /// - /// Rules configured with [AnalysisSeverity.disabled] are excluded. - /// Throws an [ArgumentError] if a duplicate rule name is encountered. - static List _buildRules( - Map ruleConfigs, - List customRules, - ) { - final rules = []; - final seenNames = {}; - - void addRule(SkillRule rule) { - if (rule.severity != AnalysisSeverity.disabled) { - if (seenNames.contains(rule.name)) { - throw ArgumentError('Duplicate rule name detected: ${rule.name}'); - } - seenNames.add(rule.name); - rules.add(rule); - } - } - - for (final CheckType check in RuleRegistry.allChecks) { - final RuleConfig config = - ruleConfigs[check.name] ?? RuleConfig(severity: check.defaultSeverity); - if (config.severity != AnalysisSeverity.disabled) { - final SkillRule? rule = RuleRegistry.createRule( - check.name, - config.severity, - config.parameters, - ); - if (rule != null) { - addRule(rule); - } - } - } - - customRules.forEach(addRule); - - return rules; - } -} diff --git a/tool/dart_skills_lint/pubspec.yaml b/tool/dart_skills_lint/pubspec.yaml deleted file mode 100644 index 1c6fd45f..00000000 --- a/tool/dart_skills_lint/pubspec.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: dart_skills_lint -description: >- - A static analysis linter for Agent Skills (SKILL.md) written in Dart. - Validates frontmatter, naming, paths, and structure for use in CI and - pre-commit hooks. -version: 0.5.1 -resolution: workspace -repository: https://github.com/flutter/agent-plugins -issue_tracker: https://github.com/flutter/agent-plugins/issues - -topics: - - agent-skills - - linter - - static-analysis - - cli - - validation - -environment: - sdk: ^3.10.8 - -dependencies: - args: ^2.4.0 - io: ^1.0.4 - logging: ^1.2.0 - meta: ^1.11.0 - path: ^1.9.1 - yaml: ^3.1.3 - json_annotation: ^4.8.0 - -dev_dependencies: - lints: ^6.0.0 - test: ^1.24.0 - test_process: ^2.1.1 - json_serializable: ^6.7.0 - build_runner: ^2.4.0 - cognitive_complexity: ^0.2.0 - coverage: ^1.15.0 - -executables: - dart_skills_lint: cli diff --git a/tool/dart_skills_lint/scripts/install.sh b/tool/dart_skills_lint/scripts/install.sh deleted file mode 100755 index ff9ffcc2..00000000 --- a/tool/dart_skills_lint/scripts/install.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash -# -# 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. -# -# install.sh โ€” Install the dart_skills_lint native binary. -# -# Usage (default repo + latest version): -# curl -fsSL https://github.com/flutter/agent-plugins/releases/latest/download/install.sh | bash -# -# Pin a specific version or alternate repo: -# curl -fsSL .../install.sh | REPO=other-org/other-repo VERSION=0.4.0-dev.1 bash -# -# Env vars: -# REPO GitHub owner/repo (default: flutter/agent-plugins). -# VERSION "latest" or a specific version like 0.4.0-dev.1 (default: latest). -# INSTALL_DIR Install destination (default: /usr/local/bin). - -set -euo pipefail - -REPO="${REPO:-flutter/agent-plugins}" -VERSION="${VERSION:-latest}" -INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" -BIN_NAME="dart_skills_lint" - -err() { echo "install.sh: error: $*" >&2; exit 1; } -info() { echo "install.sh: $*"; } - -# --- Detect platform --------------------------------------------------------- -# Single source of truth: every "Supported: ..." message and the final -# platform check derive from this list, so adding a build target only -# requires touching one constant. -SUPPORTED_TARGETS="macos-arm64 macos-x64 linux-x64 linux-arm64" -err_unsupported() { err "$1. Supported platforms: ${SUPPORTED_TARGETS// /, }."; } - -case "$(uname -s)" in - Darwin) os="macos" ;; - Linux) os="linux" ;; - *) err_unsupported "unsupported OS '$(uname -s)'" ;; -esac - -case "$(uname -m)" in - arm64|aarch64) arch="arm64" ;; - x86_64|amd64) arch="x64" ;; - *) err_unsupported "unsupported architecture '$(uname -m)'" ;; -esac - -target="${os}-${arch}" -case " $SUPPORTED_TARGETS " in - *" $target "*) ;; - *) err_unsupported "no published binary for platform '${target}'" ;; -esac - -# --- Required tools --------------------------------------------------------- -require() { command -v "$1" >/dev/null 2>&1 || err "required tool '$1' not found on PATH."; } -require curl -require tar -require awk - -if command -v sha256sum >/dev/null 2>&1; then - shasum_cmd() { sha256sum "$@"; } -elif command -v shasum >/dev/null 2>&1; then - shasum_cmd() { shasum -a 256 "$@"; } -else - err "required tools 'sha256sum' or 'shasum' not found on PATH. Install one to verify the binary." -fi - -# --- Resolve URLs ---------------------------------------------------------- -if [ "$VERSION" = "latest" ]; then - base_url="https://github.com/${REPO}/releases/latest/download" -else - tag="dart_skills_lint-v${VERSION}" - base_url="https://github.com/${REPO}/releases/download/${tag}" -fi -archive="${BIN_NAME}-${target}.tar.gz" -archive_url="${base_url}/${archive}" -sums_url="${base_url}/SHA256SUMS" - -# --- Download into a tempdir, cleaned up on exit ----------------------------- -tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/dart-skills-lint-install.XXXXXX")" -# Guard trap to prevent running rm -rf on empty/unbound tmpdir if trap triggers prematurely. -trap '[ -n "${tmpdir:-}" ] && rm -rf "$tmpdir"' EXIT INT TERM - -info "downloading ${archive} from ${REPO} (${VERSION})" -curl -fsSL --retry 3 -o "${tmpdir}/${archive}" "$archive_url" \ - || err "could not download ${archive_url}" -curl -fsSL --retry 3 -o "${tmpdir}/SHA256SUMS" "$sums_url" \ - || err "could not download ${sums_url}" - -# --- Verify SHA256 ---------------------------------------------------------- -# Strip the optional leading '*' that `sha256sum -b` (binary mode) puts before -# the filename, so SHA256SUMS files from either text or binary mode work. -expected_sha="$(awk -v fname="$archive" ' - { sub(/^\*/, "", $2) } - $2 == fname { print $1; exit } -' "${tmpdir}/SHA256SUMS")" -[ -n "$expected_sha" ] || err "no SHA256 entry for '${archive}' in SHA256SUMS." - -actual_sha="$(shasum_cmd "${tmpdir}/${archive}" | awk '{print $1}')" -if [ "$expected_sha" != "$actual_sha" ]; then - err "SHA256 mismatch for ${archive}. Expected ${expected_sha}, got ${actual_sha}." -fi -info "checksum verified" - -# --- Extract ---------------------------------------------------------------- -( cd "$tmpdir" && tar -xzf "$archive" ) -extracted="${tmpdir}/${BIN_NAME}-${target}" -[ -x "$extracted" ] || err "extracted file ${extracted} not found or not executable." - -# --- Install ---------------------------------------------------------------- -install_path="${INSTALL_DIR}/${BIN_NAME}" - -needs_sudo=0 -if [ -d "$INSTALL_DIR" ]; then - [ -w "$INSTALL_DIR" ] || needs_sudo=1 -else - parent="$(dirname "$INSTALL_DIR")" - [ -d "$parent" ] && [ -w "$parent" ] || needs_sudo=1 -fi - -if [ "$needs_sudo" = "0" ]; then - mkdir -p "$INSTALL_DIR" - install -m 0755 "$extracted" "$install_path" -elif command -v sudo >/dev/null 2>&1; then - info "${INSTALL_DIR} is not writable; using sudo" - sudo mkdir -p "$INSTALL_DIR" - sudo install -m 0755 "$extracted" "$install_path" -else - err "${INSTALL_DIR} is not writable and 'sudo' is not available. Set INSTALL_DIR to a writable path and re-run." -fi - -# --- macOS Gatekeeper note (preview binaries are unsigned) ------------------ -# Print BEFORE the launch check so users see the workaround even if Gatekeeper -# blocks the --help invocation below. -if [ "$os" = "macos" ]; then - cat </dev/null 2>&1; then - info "installed ${BIN_NAME} โ†’ ${install_path}" - info "run '${BIN_NAME} --help' to get started" -elif [ "$os" = "macos" ]; then - info "installed ${BIN_NAME} โ†’ ${install_path}" - info "launch check failed โ€” likely Gatekeeper. See the note above to clear quarantine, then run '${BIN_NAME} --help'." -else - err "installed binary at ${install_path} failed to launch." -fi diff --git a/tool/dart_skills_lint/skills/README.md b/tool/dart_skills_lint/skills/README.md deleted file mode 100644 index 1f365c3d..00000000 --- a/tool/dart_skills_lint/skills/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Skills shipped with dart_skills_lint - -The skills in this directory are shipped with the `dart_skills_lint` package. -They are intended for users of the package to help them use it effectively. - -To install these skills into your IDE, you can use the [skills](https://pub.dev/packages/skills) package on pub: -```bash -dart install skills@^1.0.0 -skills get -``` diff --git a/tool/dart_skills_lint/skills/dart-skills-lint-setup/SKILL.md b/tool/dart_skills_lint/skills/dart-skills-lint-setup/SKILL.md deleted file mode 100644 index 0c824448..00000000 --- a/tool/dart_skills_lint/skills/dart-skills-lint-setup/SKILL.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -name: dart-skills-lint-setup -description: |- - Use this skill when you need to set up validation for AI agent skills in a Dart project for the first time. - Adds the linter as a dev_dependency, creates a configuration file, and generates a baseline for legacy repos. -metadata: - internal: true ---- - -# Setting up Skill Validation with dart_skills_lint - -This skill covers **first-time wiring** of `dart_skills_lint` into a -repository. For ongoing use โ€” running the linter, interpreting -output, and writing custom rules โ€” see the -[`dart-skills-lint-validation`](../dart-skills-lint-validation/SKILL.md) -skill. For copy-pasteable CI workflow and pre-commit hook recipes, -see the [`Recipes` section of the README](../../README.md#recipes). - -## Steps - -1. **Add `dart_skills_lint` as a `dev_dependency`.** - - ```yaml - dev_dependencies: - dart_skills_lint: ^0.5.0 - ``` - - **Isolate the dependency** in a `tool/` package when you can, - instead of putting it on the root `pubspec.yaml` โ€” keeps the - linter's deps out of your runtime closure. If you must add it - to multiple `pubspec.yaml` files, ensure the `ref:` (commit - hash) is identical across all of them so resolution doesn't - diverge. - -2. **Create `dart_skills_lint.yaml`** at the repository root so both - the CLI and any embedded test invocation share the same config: - - ```yaml - dart_skills_lint: - rules: - check-relative-paths: error - check-trailing-whitespace: error - directories: - - path: ".agents/skills" - ``` - - Rules enabled by default โ€” `check-absolute-paths`, - `valid-yaml-metadata`, `invalid-skill-name`, - `description-too-long` โ€” only need to be listed if you want to - change their severity. See [`RULES.md`](../../RULES.md) for the - full list. - -3. **Generate a baseline** if you're integrating into a repository - with pre-existing skills that have legacy violations you don't - want to fix immediately: - - ```bash - dart run dart_skills_lint:cli --skills-directory=.agents/skills --generate-baseline - ``` - - This writes the current set of failures into an ignore file so - the next run exits clean. New violations introduced after the - baseline still surface as errors. - -4. **Wire it into CI.** Use the - [GitHub Actions recipe](../../README.md#recipes) from the README - verbatim, or follow the - [pre-commit hook recipe](../../README.md#recipes) below it. - -## When you're done - -The dart-skills-lint-validation skill takes over from here for -day-to-day use. diff --git a/tool/dart_skills_lint/skills/dart-skills-lint-setup/evals/evals.json b/tool/dart_skills_lint/skills/dart-skills-lint-setup/evals/evals.json deleted file mode 100644 index ddad1753..00000000 --- a/tool/dart_skills_lint/skills/dart-skills-lint-setup/evals/evals.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "repo_criteria": [ - "evals/code_quality_rubric.json" - ], - "evals": [ - { - "id": 1, - "prompt": "Set up dart_skills_lint validation for the first time in our Dart project to lint skills located in .agents/skills.", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "dart_skills_lint is added under dev_dependencies in pubspec.yaml as a git dependency targeting https://github.com/flutter/agent-plugins.git with path tool/dart_skills_lint.", - "The dart_skills_lint.yaml file is created at the repository root so both CLI and Dart test invocations share the configuration.", - "dart_skills_lint.yaml specifies .agents/skills under directories.", - "Configures check-relative-paths: error under rules in dart_skills_lint.yaml to enforce portable relative links across skill files.", - "Guidance or recipe for CI integration or pre-commit hook is provided.", - "NOT added under runtime dependencies in pubspec.yaml.", - "NOT placed inside .agents/skills/ or a sub-folder where root CLI commands will fail to locate the config.", - "No deprecated code from dart_skills_lint is used in the generated code.", - "There is a dart test created that fails when a skill defined in dart_skills_lint.yaml has an error." - ], - "agent_config": "bare-agent" - }, - { - "id": 2, - "prompt": "A repository has pre-existing skill files in .agents/skills with lint violations. Set up dart_skills_lint without failing CI or modifying legacy skill files by generating a baseline ignore file.", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "dart_skills_lint is configured under dev_dependencies in pubspec.yaml and dart_skills_lint.yaml is created at the repository root.", - "Configures check-relative-paths: error in dart_skills_lint.yaml to enforce portable path references.", - "The CLI command dart run dart_skills_lint:cli is run with --skills-directory=.agents/skills --generate-baseline to write legacy failures to an ignore file.", - "An explanation is provided that pre-existing violations exit clean while violations introduced after baseline generation will still fail validation.", - "Does NOT modify, delete, or reformat any pre-existing SKILL.md files in .agents/skills during baseline generation.", - "Does NOT pass --fix during baseline generation, keeping legacy skill files intact." - ], - "agent_config": "bare-agent" - }, - { - "id": 3, - "prompt": "Set up dart_skills_lint to validate agent skills in .agents/skills, while isolating the package dependency inside tool/pubspec.yaml instead of adding it to the root pubspec.yaml.", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "dart_skills_lint is added under dev_dependencies in tool/pubspec.yaml rather than the root pubspec.yaml.", - "dart_skills_lint.yaml is created at the repository root directory rather than inside tool/ so both CLI and test invocations share the configuration.", - "Configures check-relative-paths: error in dart_skills_lint.yaml to enforce portable path references.", - "NOT placed inside tool/, which would prevent root-level CLI invocations from discovering the configuration file.", - "NOT added to the root pubspec.yaml dependencies or dev_dependencies." - ], - "agent_config": "bare-agent" - } - ] -} \ No newline at end of file diff --git a/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md b/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md deleted file mode 100644 index f0b376cb..00000000 --- a/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -name: dart-skills-lint-validation -description: |- - Use this skill when you need to validate AI agent skills with dart_skills_lint โ€” running the linter, interpreting failures, fixing violations, and authoring custom rules. -metadata: - internal: true ---- - -# Validating Skills with dart_skills_lint - -This skill covers **day-to-day use**: running the linter, walking -through a failing run, and writing a custom rule when defaults -aren't enough. For first-time wiring (adding the dep, creating the -config file, generating a baseline) see -[`dart-skills-lint-setup`](../dart-skills-lint-setup/SKILL.md). The -full rule reference (default severities, diagnostic shapes, -fixability) lives in [`RULES.md`](../../RULES.md). - -## Running the linter - -If `dart_skills_lint` is in `pubspec.yaml`: - -```bash -dart run dart_skills_lint:cli -d .agents/skills -``` - -If it's installed globally with `dart install`: - -```bash -dart_skills_lint -d .agents/skills -``` - -If it's activated globally with `dart pub global activate`: - -```bash -dart pub global run dart_skills_lint:cli -d .agents/skills -``` - -Run `dart run dart_skills_lint:cli --help` (or `dart_skills_lint --help`) for the full flag list -(skip the inline duplicate so it never goes stale). - -## Workflow for a failing run - -1. **Run the validator.** -2. **Read the errors.** Each diagnostic names the rule that fired, - the offending value, and a suggested fix when one applies. -3. **Fix the violations.** For fixable rules - (`check-absolute-paths`, `check-trailing-whitespace`, - `invalid-skill-name`), pass `--fix` to write the corrections - to disk; add `--dry-run` to preview the diff first. -4. **Re-run** to confirm the run is clean. - -### Task progress - -- [ ] Run validator -- [ ] Read errors -- [ ] Fix violations (manual or `--fix` / `--fix --dry-run`) -- [ ] Verify clean run - -## Authoring a custom rule - -Extend `SkillRule` and pass the rule into `validateSkills`: - -```dart -import 'package:dart_skills_lint/dart_skills_lint.dart'; - -class DeprecatedSkillRule extends SkillRule { - @override - final String name = 'deprecated-skill'; - - @override - final AnalysisSeverity severity = AnalysisSeverity.warning; - - @override - Future> validate(SkillContext context) async { - final errors = []; - final yaml = context.parsedYaml; - if (yaml == null) return errors; - - if (yaml['metadata']?['deprecated'] == true) { - errors.add(ValidationError( - ruleId: name, - severity: severity, - file: 'SKILL.md', - message: 'This skill is marked as deprecated.', - )); - } - return errors; - } -} -``` - -Wire it up in a Dart test: - -```dart -import 'package:dart_skills_lint/dart_skills_lint.dart'; -import 'package:test/test.dart'; - -void main() { - test('skills pass with deprecated-skill custom rule', () async { - final config = await ConfigParser.loadConfig(); - expect( - config.directoryConfigs, - isNotEmpty, - reason: 'Configuration directoryConfigs should not be empty.', - ); - await validateSkills( - config: config, - customRules: [DeprecatedSkillRule()], - ); - }); -} -``` - -## Related - -- [`dart-skills-lint-setup`](../dart-skills-lint-setup/SKILL.md) โ€” - first-time wiring. -- [`RULES.md`](../../RULES.md) โ€” canonical rule reference. -- [`README.md`](../../README.md) โ€” installation, configuration, - integration recipes. diff --git a/tool/dart_skills_lint/skills/dart-skills-lint-validation/evals/evals.json b/tool/dart_skills_lint/skills/dart-skills-lint-validation/evals/evals.json deleted file mode 100644 index cc58775f..00000000 --- a/tool/dart_skills_lint/skills/dart-skills-lint-validation/evals/evals.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "repo_criteria": [ - "evals/code_quality_rubric.json" - ], - "evals": [ - { - "id": 1, - "prompt": "Run dart_skills_lint validation on `example/skills/invalid` with the `--check-absolute-paths` flag. The fixture points to `/tmp/this/does/not/exist.md`, which cannot be auto-fixed since it does not exist. First, create a dummy file at an absolute path (e.g., `/tmp/dummy.md`), update the link in `example/skills/invalid/SKILL.md` to point to it, and then run `--fix --dry-run` to preview the auto-fix. Finally, run `--fix` to apply the fix and verify that the link was automatically converted to a relative path.", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "The agent updated the SKILL.md link to point to an absolute path that actually exists on disk.", - "The agent ran the CLI with `--check-absolute-paths` and explicitly verified that a fixable path violation was found.", - "The agent used `--fix --dry-run` to preview the fix.", - "The agent successfully applied the fix using `--fix`, converting the absolute link into a relative one." - ], - "agent_config": "bare-agent" - }, - { - "id": 2, - "prompt": "Run dart_skills_lint validation on `example/skills/invalid` with `--disallowed-field` and `--check-absolute-paths` flags to surface all violations. Interpret the failures and manually fix all three violations (skill name mismatch, disallowed field, absolute path) without using the `--fix` flag. Ensure the final run is clean.", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "The agent executes the linter with the required flags to surface all violations.", - "The agent manually edits the skill files to fix the invalid skill name violation.", - "The agent manually edits the skill files to fix the disallowed field violation.", - "The agent manually edits the skill files to fix the absolute path violation.", - "The agent validates the directory again to confirm a clean run." - ], - "agent_config": "bare-agent" - }, - { - "id": 3, - "prompt": "Author a custom dart_skills_lint rule named `RequireInternalMetadataRule` that verifies if a skill's metadata contains `internal: true`. Wire it up into a new test file `test/custom_rule_test.dart`.", - "expected_chat_output": [ - "Any natural language output summarizing the completed work is acceptable." - ], - "expected_repo_state": [ - "The agent created a class extending `SkillRule`.", - "The agent implemented the `name` and `severity` overrides.", - "The `validate` method correctly parses the `parsedYaml` to check for `internal: true`.", - "The agent created a Dart test file that imports the custom rule and passes it to `validateSkills`.", - "The newly created test compiles and passes static analysis." - ], - "agent_config": "bare-agent" - } - ] -} \ No newline at end of file diff --git a/tool/dart_skills_lint/test/absolute_paths_test.dart b/tool/dart_skills_lint/test/absolute_paths_test.dart deleted file mode 100644 index 8ed41523..00000000 --- a/tool/dart_skills_lint/test/absolute_paths_test.dart +++ /dev/null @@ -1,168 +0,0 @@ -// 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/rule_config.dart'; -import 'package:dart_skills_lint/src/models/skill_context.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'; -import 'package:test/test.dart'; - -import 'test_utils.dart'; - -void main() { - group('Absolute Paths Validation', () { - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('absolute_path_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('flags absolute path starting with / as warning by default', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}[Absolute link](/absolute/path.md)\n', - ); - - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect( - result.warnings, - contains(contains('Absolute filepath found in link: /absolute/path.md')), - ); - }); - - test('flags windows absolute path starting with drive letter as error', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}[Windows absolute link](C:\\absolute\\path.md)\n', - ); - - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect( - result.warnings, - contains(contains(r'Absolute filepath found in link: C:\absolute\path.md')), - ); - }); - - test('ignores valid relative paths resembling windows drives', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}[Relative link](C:relative.md)\n'); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.disabled)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - expect(result.warnings, isEmpty); - }); - - test('ignores ordinary file links', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}[Relative link](file.md)\n'); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.disabled)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - expect(result.warnings, isEmpty); - }); - test('ignores absolute paths when disabled', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}Body with [broken link](missing.md) and [absolute link](/absolute/path.md)', - ); - - final validator = Validator( - ruleConfigs: {AbsolutePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.disabled)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - expect(result.warnings, isEmpty); - }); - - test( - 'flags absolute path as warning when absolutePathsSeverity: AnalysisSeverity.warning', - () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}Body with [absolute link](/absolute/path.md)', - ); - - final validator = Validator( - ruleConfigs: {AbsolutePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); // Warnings don't fail validation - expect(result.errors, isEmpty); - expect( - result.warnings, - contains(contains('Absolute filepath found in link: /absolute/path.md')), - ); - }, - ); - - test('fixes absolute path to relative if file exists', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - final File targetFile = await File('${tempDir.path}/target.md').create(); - - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}[Link](${targetFile.path})\n'); - - final rule = AbsolutePathsRule(); - final file = File('${skillDir.path}/SKILL.md'); - final String content = await file.readAsString(); - final context = SkillContext(directory: skillDir, rawContent: content); - - final String fixedContent = await rule.fix('SKILL.md', content, context.directory); - - expect(fixedContent, contains('(../target.md)')); - }); - - test('does not fix absolute path if file does not exist', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}[Link](/non/existent/file.md)\n'); - - final rule = AbsolutePathsRule(); - final file = File('${skillDir.path}/SKILL.md'); - final String content = await file.readAsString(); - final context = SkillContext(directory: skillDir, rawContent: content); - - final String fixedContent = await rule.fix('SKILL.md', content, context.directory); - - expect(fixedContent, contains('(/non/existent/file.md)')); - }); - }); -} diff --git a/tool/dart_skills_lint/test/api_defaults_test.dart b/tool/dart_skills_lint/test/api_defaults_test.dart deleted file mode 100644 index 3401ac17..00000000 --- a/tool/dart_skills_lint/test/api_defaults_test.dart +++ /dev/null @@ -1,301 +0,0 @@ -// 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/dart_skills_lint.dart'; -import 'package:dart_skills_lint/src/rules/description_length_rule.dart'; -import 'package:dart_skills_lint/src/rules/trailing_whitespace_rule.dart'; -import 'package:dart_skills_lint/src/rules/valid_yaml_metadata_rule.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; -import 'test_utils.dart'; - -void main() { - test('validateSkills applies default rules when not specified', () async { - await withTempDir((tempDir) async { - final Directory skillDir = await createDummySkill( - tempDir, - name: 'test-skill', - skillContent: 'Invalid YAML No Frontmatter', - ); - - // Call validateSkills with empty overrides. - // It should apply default rules, including valid-yaml-metadata. - final bool isValid = await validateSkills(individualSkillPaths: [skillDir.path]); - - expect(isValid, isFalse, reason: 'Should fail due to default rule valid-yaml-metadata.'); - }); - }); - - test('Validator skips disabled rules', () async { - await withTempDir((tempDir) async { - final Directory skillDir = await createDummySkill( - tempDir, - name: 'test-skill', - skillContent: 'Invalid YAML No Frontmatter', - ); - - // Create validator with the rule disabled. - final validator = Validator( - ruleConfigs: { - ValidYamlMetadataRule.ruleName: RuleConfig(severity: AnalysisSeverity.disabled), - }, - ); - final ValidationResult result = await validator.validate(skillDir); - - final bool hasYamlError = result.validationErrors.any( - (e) => e.ruleId == ValidYamlMetadataRule.ruleName, - ); - expect( - hasYamlError, - isFalse, - reason: 'Should not have valid-yaml-metadata error when disabled.', - ); - }); - }); - - test('loadConfig resolves tilde in custom config path', () async { - final String? home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; - expect(home, isNotNull, reason: 'HOME or USERPROFILE environment variable must be set.'); - - final tempFile = File(p.join(home!, 'dart_skills_lint_temp_test.yaml')); - await tempFile.writeAsString(''' -dart_skills_lint: - rules: - check-relative-paths: error -'''); - - try { - // Under the current code, this will fail because loadConfig does not do tilde expansion. - final Configuration config = await ConfigParser.loadConfig( - path: '~/dart_skills_lint_temp_test.yaml', - ); - expect(config.ruleConfigs, contains('check-relative-paths')); - } finally { - if (tempFile.existsSync()) { - await tempFile.delete(); - } - } - }); - - test('Path resolution avoids collision with prefix-sharing directories', () async { - await withTempDir((tempDir) async { - // We create two directories: 'skills-tests/test-skill' (the one being evaluated) - // and 'skills' (the one defined in config) - final configDir = Directory(p.join(tempDir.path, 'skills')); - final Directory skillDir = await createDummySkill( - tempDir, - name: 'skills-tests/test-skill', - skillContent: ''' ---- -name: test-skill -description: A test skill ---- -Line with space -''', // Trailing space - ); - - // Create a Configuration with rules enabled specifically for 'skills' - final config = Configuration( - directoryConfigs: [ - LintTargetConfig( - path: configDir.path, - ruleConfigs: { - TrailingWhitespaceRule.ruleName: const RuleConfigPatch( - severity: AnalysisSeverity.error, - ), - }, - ), - ], - ); - - // Call validateSkills. Under unsafe prefix-matching, 'skills-tests' - // starts with 'skills' (prefix collision) and enables trailing whitespace checks as error. - // It should pass because 'skills-tests' is NOT the same directory as 'skills'. - final bool isValid = await validateSkills( - individualSkillPaths: [skillDir.path], - config: config, - ); - - expect( - isValid, - isTrue, - reason: 'Should pass because skills-tests does not match configuration for skills.', - ); - }); - }); - - test('loadConfig captures YAML parsing errors in Configuration.parsingErrors', () async { - await withTempDir((tempDir) async { - final configFile = File(p.join(tempDir.path, 'dart_skills_lint.yaml')); - await configFile.writeAsString(''' -dart_skills_lint: - rules: - check-trailing-whitespace: [error, warning -'''); // unclosed bracket YAML syntax error - - final Configuration config = await ConfigParser.loadConfig(path: configFile.path); - - expect(config.parsingErrors, isNotEmpty); - expect(config.parsingErrors.first, contains('Failed to parse')); - }); - }); - - test('Nested Directory Rule Inheritance merging and precedence override', () async { - await withTempDir((tempDir) { - // parent path: 'skills' (enables check-trailing-whitespace: error, description-length: warning) - // child path: 'skills/nested' (enables description-length: error, check-trailing-whitespace: disabled) - final config = Configuration( - directoryConfigs: [ - LintTargetConfig( - path: p.join(tempDir.path, 'skills'), - ruleConfigs: { - TrailingWhitespaceRule.ruleName: const RuleConfigPatch( - severity: AnalysisSeverity.error, - ), - DescriptionLengthRule.ruleName: const RuleConfigPatch( - severity: AnalysisSeverity.warning, - ), - }, - ), - LintTargetConfig( - path: p.join(tempDir.path, 'skills/nested'), - ruleConfigs: { - DescriptionLengthRule.ruleName: const RuleConfigPatch( - severity: AnalysisSeverity.error, - ), - TrailingWhitespaceRule.ruleName: const RuleConfigPatch( - severity: AnalysisSeverity.disabled, - ), - }, - ), - ], - ); - - final session = ValidationSession( - config: config, - ignoreFileOverride: null, - customRules: [], - printWarnings: true, - fastFail: false, - quiet: true, - generateBaseline: false, - fix: false, - fixApply: false, - ); - - // Parent path should have parent rules applied - final Map parentRules = session.resolveRuleConfigsForPath( - p.join(tempDir.path, 'skills/some-skill'), - ); - expect( - parentRules[TrailingWhitespaceRule.ruleName]?.severity, - equals(AnalysisSeverity.error), - ); - expect( - parentRules[DescriptionLengthRule.ruleName]?.severity, - equals(AnalysisSeverity.warning), - ); - - // Child path should merge and override parent rules - final Map childRules = session.resolveRuleConfigsForPath( - p.join(tempDir.path, 'skills/nested/nested-skill'), - ); - expect( - childRules[TrailingWhitespaceRule.ruleName]?.severity, - equals(AnalysisSeverity.disabled), - ); // child override - expect( - childRules[DescriptionLengthRule.ruleName]?.severity, - equals(AnalysisSeverity.error), - ); // child override - }); - }); - - test('Nested Directory Ignore File Inheritance', () async { - await withTempDir((tempDir) { - // parent path: 'skills' (defines ignoreFile) - // child path: 'skills/nested' (does not define ignoreFile, should inherit) - final config = Configuration( - directoryConfigs: [ - LintTargetConfig( - path: p.join(tempDir.path, 'skills'), - ignoreFile: 'parent_ignores.json', - ruleConfigs: const {}, - ), - LintTargetConfig(path: p.join(tempDir.path, 'skills/nested'), ruleConfigs: const {}), - ], - ); - - final session = ValidationSession( - config: config, - ignoreFileOverride: null, - customRules: [], - printWarnings: true, - fastFail: false, - quiet: true, - generateBaseline: false, - fix: false, - fixApply: false, - ); - - // Parent ignore file should match - expect( - session.resolveIgnoreFile(p.join(tempDir.path, 'skills/some-skill')), - equals('parent_ignores.json'), - ); - - // Child should inherit parent ignore file - expect( - session.resolveIgnoreFile(p.join(tempDir.path, 'skills/nested/nested-skill')), - equals('parent_ignores.json'), - ); - }); - }); - - test('Absolute vs. Relative Path Resolution matching', () { - // Config defines path as relative 'skills' - final config = Configuration( - directoryConfigs: [ - LintTargetConfig( - path: 'skills', - ruleConfigs: { - TrailingWhitespaceRule.ruleName: const RuleConfigPatch( - severity: AnalysisSeverity.error, - ), - }, - ), - ], - ); - - final session = ValidationSession( - config: config, - ignoreFileOverride: null, - customRules: [], - printWarnings: true, - fastFail: false, - quiet: true, - generateBaseline: false, - fix: false, - fixApply: false, - ); - - // 1. Evaluate relative input: 'skills/my-skill' - final Map relativeRules = session.resolveRuleConfigsForPath( - 'skills/my-skill', - ); - expect( - relativeRules[TrailingWhitespaceRule.ruleName]?.severity, - equals(AnalysisSeverity.error), - ); - - // 2. Evaluate absolute input - final String absoluteInput = p.absolute('skills/my-skill'); - final Map absoluteRules = session.resolveRuleConfigsForPath(absoluteInput); - expect( - absoluteRules[TrailingWhitespaceRule.ruleName]?.severity, - 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 deleted file mode 100644 index 3998da81..00000000 --- a/tool/dart_skills_lint/test/cli_integration_test.dart +++ /dev/null @@ -1,717 +0,0 @@ -// 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:convert'; -import 'dart:io'; - -import 'package:dart_skills_lint/src/entry_point.dart'; -import 'package:dart_skills_lint/src/models/check_type.dart'; -import 'package:dart_skills_lint/src/models/ignore_entry.dart'; -import 'package:dart_skills_lint/src/models/skills_ignores.dart'; -import 'package:dart_skills_lint/src/rule_registry.dart'; -import 'package:dart_skills_lint/src/validator.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; -import 'package:test_process/test_process.dart'; - -import 'test_utils.dart'; - -void main() { - group('CLI Integration', () { - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('cli_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('de-duplicates baseline entries for multiple identical rule failures', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}[Link 1](missing1.md)\n[Link 2](missing2.md)\n', - ); - - // Run with --generate-baseline - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - '--generate-baseline', - ]); - await process.shouldExit(0); - - final ignoreFile = File('${skillDir.path}/$defaultIgnoreFileName'); - expect(ignoreFile.existsSync(), isTrue); - - final String content = await ignoreFile.readAsString(); - final json = jsonDecode(content) as Map; - final skills = json[SkillsIgnores.skillsKey] as Map; - final ignores = skills['test-skill'] as List; - - // Should be 1 entry only! Both relative link failures utilize the same ruleId/fileName de-duplication. - expect(ignores.length, equals(1)); - }); - - test('individual skill baseline is loaded on subsequent runs', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}[Link](missing.md)\n'); - - final TestProcess genProcess = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - '--generate-baseline', - ]); - await genProcess.shouldExit(0); - - final ignoreFile = File('${skillDir.path}/$defaultIgnoreFileName'); - expect(ignoreFile.existsSync(), isTrue); - - final TestProcess runProcess = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - ]); - await runProcess.shouldExit(0); - }); - - test( - 'cross-skill baseline de-duplicates and suppresses all errors across different skills', - () async { - final Directory skillsDir = await Directory('${tempDir.path}/skills').create(); - - // Create skill-one with a broken link - final Directory skill1Dir = await Directory('${skillsDir.path}/skill-one').create(); - await File('${skill1Dir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'skill-one', description: 'Skill one with a broken link')}[Link to nowhere](../nowhere/SKILL.md)\n', - ); - - // Create skill-two with a broken link - final Directory skill2Dir = await Directory('${skillsDir.path}/skill-two').create(); - await File('${skill2Dir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'skill-two', description: 'Skill two with a broken link')}[Link to nowhere](../nowhere/SKILL.md)\n', - ); - - final configFile = File('${tempDir.path}/dart_skills_lint.yaml'); - await configFile.writeAsString(''' -dart_skills_lint: - directories: - - path: "skills" - rules: - check-relative-paths: error - ignore_file: "$defaultIgnoreFileName" -'''); - - // 1. Run with --generate-baseline. It should evaluate all skills and write both to the baseline! - final TestProcess genProcess = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-d', - 'skills', - '--generate-baseline', - ], workingDirectory: tempDir.path); - await genProcess.shouldExit(0); // Exits 0 if --generate-baseline is passed - - final ignoreFile = File('${tempDir.path}/$defaultIgnoreFileName'); - expect(ignoreFile.existsSync(), isTrue); - - final String content = await ignoreFile.readAsString(); - final json = jsonDecode(content) as Map; - final skills = json[SkillsIgnores.skillsKey] as Map; - - expect(skills.containsKey('skill-one'), isTrue); - expect(skills.containsKey('skill-two'), isTrue); - - // 2. Run again silently. It should succeed with exit 0 because all errors are ignored! - final TestProcess runProcess = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-d', - 'skills', - '-q', - ], workingDirectory: tempDir.path); - await runProcess.shouldExit(0); - }, - ); - - test('exits with 0 and success message for valid skill', () async { - final Directory skillDir = await Directory('${tempDir.path}/valid-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'valid-skill', description: 'A valid skill')}Body'); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - ]); - - final List stdout = await process.stdout.rest.toList(); - expect(stdout.join('\n'), contains(skillIsValidMsg)); - await process.shouldExit(0); - }); - - test('exits with 1 and error message for invalid skill', () async { - final Directory skillDir = await Directory('${tempDir.path}/invalid-skill').create(); - // SKILL.md is missing - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - ]); - - final List stderr = await process.stderr.rest.toList(); - final String stderrStr = stderr.join('\n'); - expect(stderrStr, contains(skillIsInvalidMsg)); - expect(stderrStr, contains('SKILL.md is missing')); - await process.shouldExit(1); - }); - - test('exits with 0 and validates subdirectories if named "skills"', () async { - final Directory skillsDir = await Directory('${tempDir.path}/skills').create(); - final Directory skill1 = await Directory('${skillsDir.path}/skill-a').create(); - await File( - '${skill1.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-a', description: 'Skill A')}Body'); - - final Directory skill2 = await Directory('${skillsDir.path}/skill-b').create(); - await File( - '${skill2.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-b', description: 'Skill B')}Body'); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-d', - skillsDir.path, - ]); - - // Verify outputs for both skills (sorted order) - final List stdout = await process.stdout.rest.toList(); - final String stdoutStr = stdout.join('\n'); - expect(stdoutStr, contains(evaluatingDirMsg)); - expect(stdoutStr, contains('--- Validating skill: skill-a ---')); - expect(stdoutStr, contains(skillIsValidMsg)); - - expect(stdoutStr, contains('--- Validating skill: skill-b ---')); - expect(stdoutStr, contains(skillIsValidMsg)); - - await process.shouldExit(0); - }); - - test('ignores subdirectories starting with a dot "." in "skills" folder', () async { - final Directory skillsDir = await Directory('${tempDir.path}/skills').create(); - final Directory skill1 = await Directory('${skillsDir.path}/skill-a').create(); - await File( - '${skill1.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-a', description: 'Skill A')}Body'); - - await Directory('${skillsDir.path}/.dart_tool').create(); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-d', - skillsDir.path, - ]); - - final List stdout = await process.stdout.rest.toList(); - final String stdoutStr = stdout.join('\n'); - expect(stdoutStr, contains('--- Validating skill: skill-a ---')); - expect(stdoutStr, contains(skillIsValidMsg)); - expect(stdoutStr, isNot(contains('.dart_tool'))); - - await process.shouldExit(0); - }); - - test('exits with 1 if any subdirectory skill fails in "skills" folder', () async { - final Directory skillsDir = await Directory('${tempDir.path}/skills').create(); - final Directory skill1 = await Directory('${skillsDir.path}/skill-a').create(); - await File( - '${skill1.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-a', description: 'Skill A')}Body'); - - await Directory('${skillsDir.path}/skill-b').create(); // No SKILL.md - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-d', - skillsDir.path, - ]); - - // Verify outputs - final List stdout = await process.stdout.rest.toList(); - expect(stdout.join('\n'), contains('--- Validating skill: skill-a ---')); - expect(stdout.join('\n'), contains(skillIsValidMsg)); - - expect(stdout.join('\n'), contains('--- Validating skill: skill-b ---')); - final List stderr = await process.stderr.rest.toList(); - expect(stderr.join('\n'), contains(skillIsInvalidMsg)); - await process.shouldExit(1); - }); - - test( - 'exits with 1 early and does not process subsequent skills if --fast-fail is passed', - () async { - final Directory skillsDir = await Directory('${tempDir.path}/skills').create(); - - await Directory('${skillsDir.path}/skill-a').create(); - // skill-a does not create SKILL.md, so it is invalid and will fail first (sorted order) - - await Directory('${skillsDir.path}/skill-b').create(); - await File( - '${p.join(tempDir.path, 'skills', 'skill-b')}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-b', description: 'Skill B')}Body'); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-d', - skillsDir.path, - '--fast-fail', - ]); - - // Verify outputs for skill-a - final List stdout = await process.stdout.rest.toList(); - final String stdoutStr = stdout.join('\n'); - expect(stdoutStr, contains(evaluatingDirMsg)); - expect(stdoutStr, contains('--- Validating skill: skill-a ---')); - - final List stderr = await process.stderr.rest.toList(); - expect(stderr.join('\n'), contains(skillIsInvalidMsg)); - - // Since process exits after skill-a, stdout should be closed and no further lines (like skill-b) should appear. - await process.shouldExit(1); - }, - ); - - test('exits with 0 and suppresses success messages if --quiet is passed', () async { - final Directory skillDir = await Directory('${tempDir.path}/valid-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'valid-skill', description: 'A valid skill')}Body'); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - '--quiet', - ]); - - await process.shouldExit(0); - - // Stdout should be empty for a valid skill in quiet mode - final List rest = await process.stdout.rest.toList(); - expect(rest, isEmpty); - }); - test('prints a first-run guide to stdout and exits 64 when no defaults exist', () async { - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - ], workingDirectory: tempDir.path); - - final List stdout = await process.stdout.rest.toList(); - final String stdoutStr = stdout.join('\n'); - expect(stdoutStr, contains('dart_skills_lint: a linter for Agent Skills')); - expect(stdoutStr, contains('--skill ./path/to/my-skill')); - expect(stdoutStr, contains('--skills-directory ./path/to/skills-root')); - expect(stdoutStr, contains('.claude/skills//SKILL.md')); - expect(stdoutStr, contains('.agents/skills//SKILL.md')); - expect(stdoutStr, contains('agentskills.io/specification')); - expect(stdoutStr, contains('--help')); - await process.shouldExit(64); - }); - - test('picks up .claude/skills when no flags passed and it exists', () async { - final Directory claudeDir = await Directory( - '${tempDir.path}/.claude/skills', - ).create(recursive: true); - final Directory skillDir = await Directory('${claudeDir.path}/valid-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'valid-skill', description: 'A valid skill')}Body'); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - ], workingDirectory: tempDir.path); - - final List stdout = await process.stdout.rest.toList(); - expect(stdout.join('\n'), contains('Skill is valid.')); - await process.shouldExit(0); - }); - test('expands ~/ to HOME environment variable', () async { - final Directory skillDir = await Directory('${tempDir.path}/some-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'some-skill')}Body'); - - final TestProcess process = await TestProcess.start( - 'dart', - [p.normalize(p.absolute('bin/cli.dart')), '-s', '~/some-skill'], - environment: {'HOME': tempDir.path}, - ); - - final List stdout = await process.stdout.rest.toList(); - expect(stdout.join('\n'), contains('Skill is valid.')); - await process.shouldExit(0); - }); - - test('overrides valid-yaml-metadata flag to disabled', () async { - final Directory skillDir = await Directory('${tempDir.path}/invalid-yaml').create(); - await File('${skillDir.path}/SKILL.md').writeAsString('Invalid YAML No Frontmatter'); - - // 1. Run normally. Should fail because valid-yaml-metadata defaults to true (error). - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - ]); - await process.shouldExit(1); - - // 2. Run with --no-valid-yaml-metadata. Should pass because the check is disabled! - final TestProcess noYamlProcess = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - '--no-valid-yaml-metadata', - ]); - await noYamlProcess.shouldExit(0); - }); - - test('fails if -d specifies a directory with zero skills', () async { - final Directory emptyDir = await Directory('${tempDir.path}/empty-root').create(); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-d', - emptyDir.path, - ]); - - await process.shouldExit(1); - final List stderr = await process.stderr.rest.toList(); - expect( - stderr.join('\n'), - contains('No skills found to validate in the specified directories.'), - ); - }); - - test('fails if -d specifies a single skill directory (no sub-folders found)', () async { - final Directory skillAsRoot = await Directory('${tempDir.path}/single-skill-root').create(); - await File('${skillAsRoot.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'single-skill-root', description: 'Not a root, but a skill folder.')}Body', - ); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-d', - skillAsRoot.path, - ]); - - await process.shouldExit(1); - final List stderr = await process.stderr.rest.toList(); - expect( - stderr.join('\n'), - contains( - 'appears to be an individual skill. Use --skill / -s instead of -d / --skills-directory.', - ), - ); - }); - - test('validates multiple skills with multiple -s flags', () async { - final Directory skill1 = await Directory('${tempDir.path}/skill-1').create(); - await File( - '${skill1.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-1', description: 'Skill 1')}Body'); - - final Directory skill2 = await Directory('${tempDir.path}/skill-2').create(); - await File( - '${skill2.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-2', description: 'Skill 2')}Body'); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skill1.path, - '-s', - skill2.path, - ]); - - await process.shouldExit(0); - final List stdout = await process.stdout.rest.toList(); - final String stdoutStr = stdout.join('\n'); - expect(stdoutStr, contains('--- Validating skill: skill-1 ---')); - expect(stdoutStr, contains('--- Validating skill: skill-2 ---')); - }); - - test('handles malformed JSON ignore-file gracefully by falling back', () async { - final malformedFile = File('${tempDir.path}/malformed.json'); - await malformedFile.writeAsString('{ malformed json }'); - - final Directory skillFolder = await Directory('${tempDir.path}/skill-x').create(); - await File( - '${skillFolder.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-x', description: 'Valid skill')}Body'); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillFolder.path, - '--ignore-file', - malformedFile.path, - ]); - - await process.shouldExit(0); // Valid skill should still pass - final List stdout = await process.stdout.rest.toList(); - expect(stdout.join('\n'), contains('Evaluating directory:')); - }); - - test('CLI help displays all registered rules', () async { - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '--help', - ]); - await process.shouldExit(0); - final List stdout = await process.stdout.rest.toList(); - final String stdoutStr = stdout.join('\n'); - - for (final CheckType check in RuleRegistry.allChecks) { - expect(stdoutStr, contains(check.name)); - } - }); - - test('CLI help displays path-does-not-exist', () async { - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '--help', - ]); - await process.shouldExit(0); - final List stdout = await process.stdout.rest.toList(); - final String stdoutStr = stdout.join('\n'); - - expect(stdoutStr, contains(Validator.pathDoesNotExist)); - }); - - test('ignores directory missing SKILL.md if listed in ignore file', () async { - final Directory skillsDir = await Directory('${tempDir.path}/skills').create(); - - // Create a valid skill - final Directory skillDir = await Directory('${skillsDir.path}/valid-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('---\nname: valid-skill\ndescription: A valid skill\n---\nBody'); - - // Create a non-skill directory - await Directory('${skillsDir.path}/contributing').create(); - - // Create ignore file - final ignoreFile = File('${tempDir.path}/$defaultIgnoreFileName'); - await ignoreFile.writeAsString( - jsonEncode({ - SkillsIgnores.skillsKey: { - 'contributing': [ - { - IgnoreEntry.ruleIdKey: Validator.pathDoesNotExist, - IgnoreEntry.fileNameKey: 'skills/contributing', - }, - ], - }, - }), - ); - - final configFile = File('${tempDir.path}/dart_skills_lint.yaml'); - await configFile.writeAsString(''' -dart_skills_lint: - directories: - - path: "skills" - ignore_file: "$defaultIgnoreFileName" -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-d', - 'skills', - ], workingDirectory: tempDir.path); - - await process.shouldExit(0); - - final List stdout = await process.stdout.rest.toList(); - final String stdoutStr = stdout.join('\n'); - expect(stdoutStr, contains('--- Validating skill: valid-skill ---')); - expect(stdoutStr, contains('--- Validating skill: contributing ---')); - }); - - test('CLI reports trailing whitespace as error when enabled via config', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}Line with 1 space \n'); - - final configFile = File('${tempDir.path}/dart_skills_lint.yaml'); - await configFile.writeAsString(''' -dart_skills_lint: - directories: - - path: "test-skill" - rules: - check-trailing-whitespace: error -'''); - - 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(); - final String stderrStr = stderr.join('\n'); - expect(stderrStr, contains('has 1 trailing space(s)')); - await process.shouldExit(1); - }); - - test('--fix --dry-run shows diff but does not modify file', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}Line with 1 space \n'); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - '--fix', - '--dry-run', - '--check-trailing-whitespace', - ]); - - final List stdout = await process.stdout.rest.toList(); - final String stdoutStr = stdout.join('\n'); - expect(stdoutStr, contains('[Dry Run] Proposed changes for test-skill (SKILL.md):')); - - await process.shouldExit(1); - - // Verify file was not modified - final String content = await File('${skillDir.path}/SKILL.md').readAsString(); - expect(content, contains('Line with 1 space \n')); - }); - - test('--fix without --dry-run writes fixes to disk', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}Line with 1 space \n'); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - '--fix', - '--check-trailing-whitespace', - ]); - - final List stdout = await process.stdout.rest.toList(); - expect(stdout.join('\n'), contains('Applied fixes for test-skill')); - - await process.shouldExit(0); - - // Verify file was modified - final String content = await File('${skillDir.path}/SKILL.md').readAsString(); - expect(content, isNot(contains('Line with 1 space \n'))); - expect(content, contains('Line with 1 space\n')); - }); - - test('--fix-apply alias still works but prints a deprecation notice', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}Line with 1 space \n'); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - '--fix-apply', - '--check-trailing-whitespace', - ]); - - final List stdout = await process.stdout.rest.toList(); - final List stderr = await process.stderr.rest.toList(); - expect(stderr.join('\n'), contains(fixApplyDeprecationMsg)); - expect(stdout.join('\n'), contains('Applied fixes for test-skill')); - - await process.shouldExit(0); - - // File still modified โ€” alias preserves behavior. - final String content = await File('${skillDir.path}/SKILL.md').readAsString(); - expect(content, contains('Line with 1 space\n')); - }); - - test('--fix does not modify file if lint is ignored', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}Line with 1 space \n'); - - final ignoreFile = File('${skillDir.path}/$defaultIgnoreFileName'); - await ignoreFile.writeAsString( - jsonEncode({ - SkillsIgnores.skillsKey: { - 'test-skill': [ - { - IgnoreEntry.ruleIdKey: 'check-trailing-whitespace', - IgnoreEntry.fileNameKey: 'SKILL.md', - }, - ], - }, - }), - ); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - '--fix', - '--check-trailing-whitespace', - ]); - - await process.shouldExit(0); - - final String content = await File('${skillDir.path}/SKILL.md').readAsString(); - expect(content, contains('Line with 1 space \n')); - }); - - test('--fix does not modify file if invalid-skill-name is ignored', () async { - final Directory skillDir = await Directory('${tempDir.path}/my_skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: wrong-name -description: A test skill ---- -Body'''); - - final ignoreFile = File('${skillDir.path}/$defaultIgnoreFileName'); - await ignoreFile.writeAsString( - jsonEncode({ - SkillsIgnores.skillsKey: { - 'my_skill': [ - {IgnoreEntry.ruleIdKey: 'invalid-skill-name', IgnoreEntry.fileNameKey: 'SKILL.md'}, - ], - }, - }), - ); - - final TestProcess process = await TestProcess.start('dart', [ - 'bin/cli.dart', - '-s', - skillDir.path, - '--fix', - ]); - - await process.shouldExit(0); - - final String content = await File('${skillDir.path}/SKILL.md').readAsString(); - expect(content, contains('name: wrong-name')); - }); - }); -} diff --git a/tool/dart_skills_lint/test/config_file_test.dart b/tool/dart_skills_lint/test/config_file_test.dart deleted file mode 100644 index ee2fee9a..00000000 --- a/tool/dart_skills_lint/test/config_file_test.dart +++ /dev/null @@ -1,924 +0,0 @@ -// 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/entry_point.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; -import 'package:test_process/test_process.dart'; - -void main() { - group('Configuration File Integration', () { - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('config_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('obeys disabled relative paths in config', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: test-skill -description: A test skill ---- -[broken](missing.md)'''); - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - rules: - check-relative-paths: disabled -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'test-skill', - ], workingDirectory: tempDir.path); - - final List stdout = await process.stdout.rest.toList(); - expect(stdout.join('\n'), contains('Skill is valid.')); - await process.shouldExit(0); - }); - - test('obeys warning absolute paths in config', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: test-skill -description: A test skill ---- -[absolute](/absolute/path.md)'''); - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - rules: - check-absolute-paths: warning -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'test-skill', - ], workingDirectory: tempDir.path); - - final List stdout = await process.stdout.rest.toList(); - expect(stdout.join('\n'), contains('Warnings:')); - await process.shouldExit(0); - }); - - test('obeys path-specific rules with tilde in config', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: test-skill -description: A test skill ---- -Line with 1 space -'''); // Trailing space - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - directories: - - path: "~/test-skill" - rules: - check-trailing-whitespace: error -'''); - - final TestProcess process = await TestProcess.start( - 'dart', - [p.normalize(p.absolute('bin/cli.dart')), '-s', '~/test-skill'], - environment: {'HOME': tempDir.path}, - workingDirectory: tempDir.path, - ); - - final List stderr = await process.stderr.rest.toList(); - expect(stderr.join('\n'), contains('has 1 trailing space(s)')); - await process.shouldExit(1); - }); - - test('CLI flags override path-specific config', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: test-skill -description: A test skill ---- -Line with 1 space -'''); // Trailing space - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - directories: - - path: "test-skill" - rules: - check-trailing-whitespace: error -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'test-skill', - '--no-check-trailing-whitespace', - ], workingDirectory: tempDir.path); - - await process.shouldExit(0); - }); - - test('obeys individual_skills block in config', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: test-skill -description: A test skill ---- -Line with 1 space -'''); // Trailing space - - // Create a second skill not listed in the config to act as a negative test. - // This ensures the rule is applied strictly to `test-skill` and hasn't accidentally bled globally. - final Directory otherSkillDir = await Directory('${tempDir.path}/other-skill').create(); - await File('${otherSkillDir.path}/SKILL.md').writeAsString(''' ---- -name: other-skill -description: Another test skill ---- -Line with 1 space -'''); // Trailing space - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - individual_skills: - - path: "test-skill" - rules: - check-trailing-whitespace: error -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'test-skill', - '-s', - 'other-skill', - ], workingDirectory: tempDir.path); - - final List stderr = await process.stderr.rest.toList(); - final String output = stderr.join('\n'); - expect(output, contains('has 1 trailing space(s)')); - expect(output, isNot(contains('other-skill'))); - await process.shouldExit(1); - }); - - test('succeeds on non-overlapping individual_skills and directories paths', () async { - await Directory('${tempDir.path}/dir1').create(); - await File('${tempDir.path}/dir1/SKILL.md').writeAsString(''' ---- -name: dir1 -description: A test skill ---- -Body'''); - - await Directory('${tempDir.path}/dir2').create(); - await File('${tempDir.path}/dir2/SKILL.md').writeAsString(''' ---- -name: dir2 -description: A test skill ---- -Body'''); - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - directories: - - path: "dir1" - individual_skills: - - path: "dir2" -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - ], workingDirectory: tempDir.path); - - await process.shouldExit(0); - }); - - test('CLI flags override config', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: test-skill -description: A test skill ---- -[broken](missing.md)'''); - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - rules: - check-relative-paths: disabled -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'test-skill', - '--check-relative-paths', - ], workingDirectory: tempDir.path); - - final List stderr = await process.stderr.rest.toList(); - expect(stderr.join('\n'), contains('Skill is invalid:')); - await process.shouldExit(1); - }); - - test('writes empty ignore-file if missing and specified in config', () 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'''); - - const ignorePath = 'custom_ignore.json'; - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - directories: - - path: "test-skill" - ignore_file: "$ignorePath" -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'test-skill', - ], workingDirectory: tempDir.path); - - final List stdout = await process.stdout.rest.toList(); - expect(stdout.join('\n'), contains('File not found generating-baseline')); - await process.shouldExit(0); - - final writtenFile = File('${tempDir.path}/$ignorePath'); - expect(writtenFile.existsSync(), isTrue); - final String fileContent = await writtenFile.readAsString(); - expect(fileContent, contains('"skills":')); - }); - - test('ignores config when --ignore-config is passed', () async { - final Directory skillDir = await Directory('${tempDir.path}/TEST-SKILL').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: TEST-SKILL -description: A test skill -license: MIT ---- -Body'''); - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - rules: - invalid-skill-name: disabled -'''); - - // 1. Run without --ignore-config. Should pass because config disables the check. - final TestProcess passProcess = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'TEST-SKILL', - ], workingDirectory: tempDir.path); - await passProcess.shouldExit(0); - - // 2. Run with --ignore-config. Should fail because config is ignored and default is used. - final TestProcess failProcess = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'TEST-SKILL', - '--ignore-config', - ], workingDirectory: tempDir.path); - await failProcess.shouldExit(1); - }); - - test('ignores config when generating baseline with --ignore-config', () async { - final Directory skillDir = await Directory('${tempDir.path}/TEST-SKILL').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: TEST-SKILL -description: A test skill -license: MIT ---- -Body'''); - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - rules: - invalid-skill-name: disabled -'''); - - // 1. Generate baseline with --ignore-config. It should ignore config (so the rule is enabled) and find violations to generate baseline for! - final TestProcess genProcess = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'TEST-SKILL', - '--generate-baseline', - '--ignore-config', - ], workingDirectory: tempDir.path); - await genProcess.shouldExit(0); // Exits 0 if --generate-baseline passed - - final ignoreFile = File('${skillDir.path}/$defaultIgnoreFileName'); - expect(ignoreFile.existsSync(), isTrue); - - final String content = await ignoreFile.readAsString(); - expect(content, contains('invalid-skill-name')); // It should generate baseline for it! - }); - - test('fails on invalid top-level key in config 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: - invalid-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: Unrecognized top-level key "invalid-key"'), - ); - await process.shouldExit(1); - }); - - test('bad path: type emits parsing error and lets later entries through', () async { - // First entry has path: 123 (not a string). Second entry is well-formed. - // The bad-type entry should produce a parsingErrors line but must not - // prevent the second entry from being parsed. - await Directory('${tempDir.path}/good-skill').create(); - await File('${tempDir.path}/good-skill/SKILL.md').writeAsString(''' ---- -name: good-skill -description: A valid skill ---- -Body'''); - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - directories: - - path: 123 - - path: "good-skill" -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - ], workingDirectory: tempDir.path); - - final List stderr = await process.stderr.rest.toList(); - final String stderrStr = stderr.join('\n'); - expect(stderrStr, contains('Configuration error: Directory entry "path" must be a string')); - // Without the fix, the unchecked cast would throw inside the - // top-level try/catch and 'good-skill' would never run. - await process.shouldExit(1); // exits 1 due to parsing error - }); - - test('fails on invalid directory key in config 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: - directories: - - path: "test-skill" - invalid-dir-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: Unrecognized key "invalid-dir-key"'), - ); - await process.shouldExit(1); - }); - - test('fails on unrecognized parameter 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-parameter-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 parameter "invalid-parameter-key" for rule "path-does-not-exist".', - ), - ); - await process.shouldExit(1); - }); - - test('fails on invalid parameter 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 value/type for parameter "exclude" in rule "path-does-not-exist". Expected RegExp (valid regular expression string), got "123".', - ), - ); - await process.shouldExit(1); - }); - - test( - 'succeeds with warning on invalid key and prints deprecation when --allow-misconfigured-keys passed', - () 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: - invalid-key: value -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'test-skill', - '--allow-misconfigured-keys', - ], workingDirectory: tempDir.path); - - final List stdout = await process.stdout.rest.toList(); - final String output = stdout.join('\n'); - expect(output, contains('Configuration warning: Unrecognized top-level key "invalid-key"')); - expect(output, contains('DEPRECATION WARNING: --allow-misconfigured-keys is deprecated')); - await process.shouldExit(0); - }, - ); - - test('obeys custom configuration file path via --config', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: test-skill -description: A test skill ---- -[broken](missing.md)'''); - - await File('${tempDir.path}/custom_config.yaml').writeAsString(''' -dart_skills_lint: - rules: - check-relative-paths: disabled -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'test-skill', - '--config', - 'custom_config.yaml', - ], workingDirectory: tempDir.path); - - final List stdout = await process.stdout.rest.toList(); - expect(stdout.join('\n'), contains('Skill is valid.')); - await process.shouldExit(0); - }); - - test('exits with 1 and prints error message if --config points to non-existent file', () 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'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'test-skill', - '--config', - 'non_existent_config.yaml', - ], workingDirectory: tempDir.path); - - final List stderr = await process.stderr.rest.toList(); - expect(stderr.join('\n'), contains('Configuration file not found')); - expect(stderr.join('\n'), contains('non_existent_config.yaml')); - await process.shouldExit(1); - }); - - test('ignores config when both --config and --ignore-config are passed', () async { - final Directory skillDir = await Directory('${tempDir.path}/TEST-SKILL').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: TEST-SKILL -description: A test skill -license: MIT ---- -Body'''); - - await File('${tempDir.path}/custom_config.yaml').writeAsString(''' -dart_skills_lint: - rules: - invalid-skill-name: disabled -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'TEST-SKILL', - '--config', - 'custom_config.yaml', - '--ignore-config', - ], workingDirectory: tempDir.path); - - await process.shouldExit(1); - }); - - test('fails on invalid individual_skills key in config 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: - individual_skills: - - path: "test-skill" - invalid-ind-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: Unrecognized key "invalid-ind-key" in individual skill entry for "test-skill".', - ), - ); - await process.shouldExit(1); - }); - - test( - 'processes both configured directories and individual skills when no arguments are passed', - () async { - // 1. Create a directory target with a nested skill - await Directory('${tempDir.path}/dir-target/dir-skill').create(recursive: true); - await File('${tempDir.path}/dir-target/dir-skill/SKILL.md').writeAsString(''' ---- -name: dir-skill -description: A directory skill ---- -Body'''); - - // 2. Create an individual skill target - await Directory('${tempDir.path}/ind-skill').create(); - await File('${tempDir.path}/ind-skill/SKILL.md').writeAsString(''' ---- -name: ind-skill -description: An individual skill ---- -Body'''); - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - directories: - - path: "dir-target" - individual_skills: - - path: "ind-skill" -'''); - - // Run with NO arguments (no -s or -d) - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - ], workingDirectory: tempDir.path); - - final List stdout = await process.stdout.rest.toList(); - final String output = stdout.join('\n'); - - // Should validate both exactly once - expect('Validating skill: dir-skill'.allMatches(output).length, 1); - expect('Validating skill: ind-skill'.allMatches(output).length, 1); - await process.shouldExit(0); - }, - ); - - test('CLI targets override configured individual_skills', () async { - final Directory cliSkillDir = await Directory('${tempDir.path}/cli-skill').create(); - await File('${cliSkillDir.path}/SKILL.md').writeAsString(''' ---- -name: cli-skill -description: A test skill passed via CLI ---- -Body'''); - - final Directory configSkillDir = await Directory('${tempDir.path}/config-skill').create(); - await File('${configSkillDir.path}/SKILL.md').writeAsString(''' ---- -name: config-skill -description: A test skill in config ---- -Body'''); - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - individual_skills: - - path: "config-skill" -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-s', - 'cli-skill', - ], workingDirectory: tempDir.path); - - final List stdout = await process.stdout.rest.toList(); - final String output = stdout.join('\n'); - - // The CLI target should be validated - expect(output, contains('Validating skill: cli-skill')); - // The config target should NOT be validated because the CLI target overrides it - expect(output, isNot(contains('Validating skill: config-skill'))); - - await process.shouldExit(0); - }); - - test('later config entries override earlier ones for overlapping paths', () async { - await Directory('${tempDir.path}/dir1').create(); - await Directory('${tempDir.path}/dir1/test-skill').create(); - // Add trailing whitespace to trigger a lint rule - await File( - '${tempDir.path}/dir1/test-skill/SKILL.md', - ).writeAsString('---\nname: test-skill\ndescription: A test skill\n---\nBody \n'); - - await File('${tempDir.path}/dart_skills_lint.yaml').writeAsString(''' -dart_skills_lint: - directories: - - path: "dir1" - rules: - check-trailing-whitespace: error - individual_skills: - - path: "dir1/test-skill" - rules: - check-trailing-whitespace: warning -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-d', - 'dir1', - ], workingDirectory: tempDir.path); - - final List stdout = await process.stdout.rest.toList(); - final String output = stdout.join('\n'); - - // Should show a warning, not an error. Exit code 0 for warnings. - expect(output, contains('Warnings:')); - expect(output, contains('Line 5 has 1 trailing space(s)')); - await process.shouldExit(0); - }); - - test('obeys map-based rule parameters 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 parameters 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 parameters) - await process.shouldExit(0); - }); - - test( - 'clears inherited rule parameters when target overrides key with tilde (~) null value', - () 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: error - exclude: ".*-workspace" - directories: - - path: "skills-root" - rules: - path-does-not-exist: - exclude: ~ -'''); - - final TestProcess process = await TestProcess.start('dart', [ - p.normalize(p.absolute('bin/cli.dart')), - '-d', - 'skills-root', - ], workingDirectory: tempDir.path); - - // Exits with 1 because exclude was nullified by ~, so definition-of-done-workspace is evaluated - // and fails due to missing SKILL.md. - await process.shouldExit(1); - }, - ); - - test('yields RuleParameterType schema validation error for nested collections', () 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: - - ".*-workspace" -'''); - - 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 value/type for parameter "exclude" in rule "path-does-not-exist"', - ), - ); - await process.shouldExit(1); - }); - - test( - 'yields RuleParameterType schema validation error for malformed regular expression', - () 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: "[a-z" -'''); - - 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 value/type for parameter "exclude" in rule "path-does-not-exist"', - ), - ); - expect(stderr.join('\n'), contains('Expected RegExp (valid regular expression string)')); - await process.shouldExit(1); - }, - ); - }); -} diff --git a/tool/dart_skills_lint/test/copyright_header_test.dart b/tool/dart_skills_lint/test/copyright_header_test.dart deleted file mode 100644 index 988caf5e..00000000 --- a/tool/dart_skills_lint/test/copyright_header_test.dart +++ /dev/null @@ -1,76 +0,0 @@ -// 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 'package:test/test.dart'; - -/// Pins the BSD copyright header to every Dart source file in the package. -/// -/// Every `.dart` file in `lib/`, `bin/`, and `test/` must begin with the -/// standard three-line copyright block. The year is not pinned โ€” any four-digit -/// year is accepted โ€” but the rest of the text is matched exactly. - -/// The canonical copyright header. Used in both the check and the error message -/// so the two never drift apart. -const String _copyrightHeader = - '// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file\n' - '// for details. All rights reserved. Use of this source code is governed by a\n' - '// BSD-style license that can be found in the LICENSE file.'; - -/// Directories to scan for Dart source files, relative to the package root -/// (i.e. the directory that contains `pubspec.yaml`, which is also the working -/// directory when `dart test` is invoked from the package). -const Set _sourceDirs = {'bin', 'lib', 'test'}; - -void main() { - test('every Dart file has a BSD copyright header', () { - final String packageRoot = p.normalize(p.absolute('.')); - final List missing = []; - - for (final String dir in _sourceDirs) { - final source = Directory(p.join(packageRoot, dir)); - if (!source.existsSync()) { - continue; - } - for (final FileSystemEntity entity in source.listSync(recursive: true)) { - if (entity is File && entity.path.endsWith('.dart')) { - final String content = entity.readAsStringSync(); - if (!_hasCopyrightHeader(content)) { - missing.add(p.relative(entity.path, from: packageRoot)); - } - } - } - } - - expect( - missing, - isEmpty, - reason: - 'The following Dart files are missing the BSD copyright header:\n' - ' ${missing.join('\n ')}\n\n' - 'Add the following block as the first three lines of each file:\n' - '$_copyrightHeader', - ); - }); -} - -final RegExp _copyrightPattern = RegExp( - r'^// Copyright \(c\) \d{4}, the Dart project authors\. {2}Please see the AUTHORS file\n' - r'// for details\. All rights reserved\. Use of this source code is governed by a\n' - r'// BSD-style license that can be found in the LICENSE file\.', -); - -bool _hasCopyrightHeader(String content) { - // Normalize Windows line endings so the regex works on files checked out - // with core.autocrlf enabled. - final String normalized = content.replaceAll('\r\n', '\n'); - // Allow an optional shebang line (and any following blank lines) before the - // copyright header. - final String checkContent = normalized.startsWith('#!') - ? normalized.substring(normalized.indexOf('\n') + 1).replaceFirst(RegExp(r'^\n*'), '') - : normalized; - return _copyrightPattern.hasMatch(checkContent); -} diff --git a/tool/dart_skills_lint/test/custom_rule_parameters_test.dart b/tool/dart_skills_lint/test/custom_rule_parameters_test.dart deleted file mode 100644 index 2a50b8e1..00000000 --- a/tool/dart_skills_lint/test/custom_rule_parameters_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -// 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:dart_skills_lint/src/models/custom_rule_parameters.dart'; -import 'package:test/test.dart'; - -void main() { - test('CustomRuleParameters params map is unmodifiable', () { - final parameters = CustomRuleParameters({'key': 'value'}); - - expect(() => parameters.params['new_key'] = 'new_value', throwsA(isUnsupportedError)); - expect(() => parameters.params.remove('key'), throwsA(isUnsupportedError)); - expect(() => parameters.params.clear(), throwsA(isUnsupportedError)); - }); -} diff --git a/tool/dart_skills_lint/test/custom_rule_test.dart b/tool/dart_skills_lint/test/custom_rule_test.dart deleted file mode 100644 index 92b93cae..00000000 --- a/tool/dart_skills_lint/test/custom_rule_test.dart +++ /dev/null @@ -1,158 +0,0 @@ -// 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:async'; -import 'dart:io'; -import 'package:dart_skills_lint/dart_skills_lint.dart'; -import 'package:logging/logging.dart'; -import 'package:test/test.dart'; - -class CustomRule extends SkillRule { - @override - final String name = 'custom-rule'; - - @override - final AnalysisSeverity severity = AnalysisSeverity.error; - - @override - Future> validate(SkillContext context) async { - final errors = []; - if (context.rawContent.contains('TRIGGER_ERROR')) { - errors.add( - ValidationError( - ruleId: name, - severity: severity, - file: 'SKILL.md', - message: 'Custom rule triggered', - ), - ); - } - return errors; - } -} - -class MismatchRule extends SkillRule { - @override - final String name = 'mismatch-rule'; - - @override - final AnalysisSeverity severity = AnalysisSeverity.warning; - - @override - Future> validate(SkillContext context) async { - return [ - ValidationError( - ruleId: name, - severity: AnalysisSeverity.error, // Mismatch! - file: 'SKILL.md', - message: 'Triggered', - ), - ]; - } -} - -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; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('custom_rule_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('Validator runs custom rule', () async { - final Directory skillDir = await Directory('${tempDir.path}/skill-name').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: skill-name -description: A test skill ---- -TRIGGER_ERROR'''); - - final validator = Validator(customRules: [CustomRule()]); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('Custom rule triggered'))); - }); - - test('Validator logs warning on severity mismatch', () async { - final Directory skillDir = await Directory('${tempDir.path}/skill-name-3').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: skill-name-3 -description: A test skill ---- -Body'''); - - final validator = Validator(customRules: [MismatchRule()]); - - final logs = []; - final StreamSubscription subscription = Logger('dart_skills_lint').onRecord.listen( - (record) { - logs.add(record.message); - }, - ); - - try { - await validator.validate(skillDir); - } finally { - await subscription.cancel(); - } - - expect( - logs, - contains( - contains( - 'Rule "mismatch-rule" used severity AnalysisSeverity.error instead of defined AnalysisSeverity.warning', - ), - ), - ); - }); - - test('Validator throws ArgumentError on duplicate rule names', () { - final rule1 = CustomRule(); - final rule2 = CustomRule(); // Same name 'custom-rule' - - 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/dart_skills_lint_skills_test.dart b/tool/dart_skills_lint/test/dart_skills_lint_skills_test.dart deleted file mode 100644 index f7002298..00000000 --- a/tool/dart_skills_lint/test/dart_skills_lint_skills_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -// 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:async'; -import 'dart:io'; -import 'package:dart_skills_lint/dart_skills_lint.dart'; -import 'package:logging/logging.dart'; -import 'package:test/test.dart'; - -void main() { - test('Run skills linter mirroring config', () async { - final Level oldLevel = Logger.root.level; - Logger.root.level = Level.ALL; - final StreamSubscription subscription = Logger.root.onRecord.listen( - (record) => stdout.writeln(record.message), - ); - - try { - // Load configuration from the default file (dart_skills_lint.yaml) - // to mirror what is configured in the repository. - final Configuration config = await ConfigParser.loadConfig(); - expect( - config.directoryConfigs, - isNotEmpty, - reason: 'Configuration directoryConfigs should not be empty.', - ); - - final bool isValid = await validateSkills(config: config); - expect(isValid, isTrue, reason: 'Skills validation failed. See above for details.'); - } finally { - Logger.root.level = oldLevel; - await subscription.cancel(); - } - }); -} diff --git a/tool/dart_skills_lint/test/directory_structure_test.dart b/tool/dart_skills_lint/test/directory_structure_test.dart deleted file mode 100644 index 964d61f1..00000000 --- a/tool/dart_skills_lint/test/directory_structure_test.dart +++ /dev/null @@ -1,166 +0,0 @@ -// 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:convert'; -import 'dart:io'; - -import 'package:dart_skills_lint/src/models/analysis_severity.dart'; -import 'package:dart_skills_lint/src/models/rule_config.dart'; -import 'package:dart_skills_lint/src/validator.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -class MockInaccessibleFile implements File { - MockInaccessibleFile(this._path); - final String _path; - - @override - String get path => _path; - - @override - bool existsSync() => true; - - @override - Future readAsString({Encoding encoding = utf8}) async { - throw FileSystemException('File is inaccessible', _path); - } - - @override - Object? noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - -base class TestIOOverrides extends IOOverrides { - TestIOOverrides(this.targetPath); - final String targetPath; - - @override - File createFile(String path) { - if (path == targetPath) { - return MockInaccessibleFile(path); - } - return super.createFile(path); - } -} - -void main() { - group('Directory Structure Validation', () { - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('skill_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('fails if directory does not exist', () async { - final nonExistentDir = Directory('path/to/nothing'); - final validator = Validator(); - final ValidationResult result = await validator.validate(nonExistentDir); - - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('Directory does not exist'))); - }); - - test('fails if path is a file', () async { - final file = File('${tempDir.path}/some_file'); - await file.create(); - final validator = Validator(); - final ValidationResult result = await validator.validate(Directory(file.path)); - - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('is not a directory'))); - }); - - test('fails if SKILL.md is missing', () async { - final validator = Validator(); - final ValidationResult result = await validator.validate(tempDir); - - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('SKILL.md is missing'))); - }); - - test('fails if SKILL.md cannot be read', () async { - final skillDir = Directory(p.join(tempDir.path, 'test-skill-inaccessible')); - await skillDir.create(); - final String filePath = p.join(skillDir.path, 'SKILL.md'); - - final overrides = TestIOOverrides(filePath); - await IOOverrides.runWithIOOverrides(() async { - try { - final validator = Validator(); - final ValidationResult validationResult = await validator.validate(skillDir); - - // ignore: avoid_print - print( - 'DEBUG errors: ${validationResult.validationErrors.map((e) => "${e.ruleId}: ${e.message}").toList()}', - ); - expect(validationResult.isValid, isFalse); - expect( - validationResult.validationErrors.any( - (e) => e.ruleId == Validator.skillFileInaccessible, - ), - isTrue, - ); - } catch (e, s) { - fail('Unexpected exception during validation: $e\n$s'); - } - }, overrides); - }); - - test('obeys skill-file-inaccessible severity override', () async { - final skillDir = Directory(p.join(tempDir.path, 'test-skill-override')); - await skillDir.create(); - final String filePath = p.join(skillDir.path, 'SKILL.md'); - - final overrides = TestIOOverrides(filePath); - await IOOverrides.runWithIOOverrides(() async { - try { - final validator = Validator( - ruleConfigs: { - Validator.skillFileInaccessible: RuleConfig(severity: AnalysisSeverity.warning), - }, - ); - final ValidationResult validationResult = await validator.validate(skillDir); - - // ignore: avoid_print - print( - 'DEBUG errors (override): ${validationResult.validationErrors.map((e) => "${e.ruleId}: ${e.message}").toList()}', - ); - expect(validationResult.isValid, isTrue); - expect( - validationResult.validationErrors.any( - (e) => - e.ruleId == Validator.skillFileInaccessible && - e.severity == AnalysisSeverity.warning, - ), - isTrue, - ); - } catch (e, s) { - fail('Unexpected exception during validation: $e\n$s'); - } - }, overrides); - }); - - test('passes if directory exists and contains SKILL.md', () async { - final skillDir = Directory(p.join(tempDir.path, 'test-skill')); - await skillDir.create(); - await File(p.join(skillDir.path, 'SKILL.md')).writeAsString(''' ---- -name: test-skill -description: A test skill ---- -Body'''); - - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue, reason: result.errors.isEmpty ? '' : result.errors.first); - expect(result.errors, isEmpty); - }); - }); -} diff --git a/tool/dart_skills_lint/test/example_fixtures_test.dart b/tool/dart_skills_lint/test/example_fixtures_test.dart deleted file mode 100644 index dd91fe23..00000000 --- a/tool/dart_skills_lint/test/example_fixtures_test.dart +++ /dev/null @@ -1,84 +0,0 @@ -// 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:path/path.dart' as p; -import 'package:test/test.dart'; -import 'package:test_process/test_process.dart'; - -/// Drift guard for the `example/valid` and `example/invalid` fixtures. -/// -/// The fixtures and `example/README.md` make precise claims about which -/// rules fire and what their diagnostics look like. This test pins both: -/// -/// - `example/valid` must exit 0 with no error output under default rules. -/// - `example/invalid` must exit 1 with `invalid-skill-name` under default -/// rules, and must surface all three intended rules when the other two -/// are escalated. -/// -/// Failures here mean either the fixtures have drifted from the README, -/// or a rule's diagnostic wording has changed without the README catching -/// up. Fix one or the other โ€” do not silence the test. -void main() { - group('example fixtures', () { - final String cliPath = p.normalize(p.absolute('bin/cli.dart')); - final String validPath = p.normalize(p.absolute('example/skills/valid')); - final String invalidPath = p.normalize(p.absolute('example/skills/invalid')); - - test('example/valid passes with default rules', () async { - final TestProcess process = await TestProcess.start('dart', [cliPath, '--skill', validPath]); - - final List stdout = await process.stdout.rest.toList(); - final String stdoutStr = stdout.join('\n'); - expect(stdoutStr, contains('--- Validating skill: valid ---')); - expect(stdoutStr, contains('Skill is valid.')); - await process.shouldExit(0); - }); - - test('example/invalid fails on invalid-skill-name with default rules', () async { - final TestProcess process = await TestProcess.start('dart', [ - cliPath, - '--skill', - invalidPath, - ]); - - final List stderr = await process.stderr.rest.toList(); - final String stderrStr = stderr.join('\n'); - - // Disambiguated frontmatter-vs-dir wording plus a normalized - // suggestion โ€” exercises the diagnostic shape from name_format_rule. - expect(stderrStr, contains('Frontmatter `name` "NotInvalid" must be lowercase')); - expect(stderrStr, contains('does not match the parent directory name "invalid"')); - expect(stderrStr, contains('Suggested: "notinvalid"')); - - await process.shouldExit(1); - }); - - test( - 'example/invalid surfaces disallowed-field and check-absolute-paths when escalated', - () async { - final TestProcess process = await TestProcess.start('dart', [ - cliPath, - '--skill', - invalidPath, - '--disallowed-field', - '--check-absolute-paths', - ]); - - final List stderr = await process.stderr.rest.toList(); - final String stderrStr = stderr.join('\n'); - - // disallowed-field - expect(stderrStr, contains('Disallowed field: secret_field')); - // check-absolute-paths now spells out the portability rationale - // in the error message itself. - expect(stderrStr, contains('Absolute filepath found in link: /tmp/this/does/not/exist.md')); - expect(stderrStr, contains('portable')); - // invalid-skill-name still fires. - expect(stderrStr, contains('Frontmatter `name`')); - - await process.shouldExit(1); - }, - ); - }); -} diff --git a/tool/dart_skills_lint/test/field_constraints_test.dart b/tool/dart_skills_lint/test/field_constraints_test.dart deleted file mode 100644 index 1321cda9..00000000 --- a/tool/dart_skills_lint/test/field_constraints_test.dart +++ /dev/null @@ -1,251 +0,0 @@ -// 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/skill_context.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'; -import 'package:dart_skills_lint/src/validator.dart'; -import 'package:test/test.dart'; -import 'package:yaml/yaml.dart'; - -import 'test_utils.dart'; - -void main() { - group('Field Specific Constraints Validation', () { - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('fields_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - group('Skill Name', () { - test('fails if not lowercase, error names the frontmatter field', () async { - final Directory skillDir = await Directory('${tempDir.path}/Skill-Name').create(); - await File('${skillDir.path}/SKILL.md').writeAsString('${buildFrontmatter()}Body'); - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isFalse); - expect( - result.errors, - contains(contains('Frontmatter `name` "Skill-Name" must be lowercase')), - ); - expect(result.errors, contains(contains('Suggested: "skill-name"'))); - }); - - test('fails if too long, error reports both lengths and names the field', () async { - final String longName = 'a' * (NameFormatRule.maxNameLength + 1); - final Directory skillDir = await Directory('${tempDir.path}/$longName').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: longName)}Body'); - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isFalse); - expect( - result.errors, - contains(contains('Frontmatter `name` is ${longName.length} characters')), - ); - expect(result.errors, contains(contains('maximum is ${NameFormatRule.maxNameLength}'))); - }); - - test('fails if contains invalid characters; suggests hyphen-normalized form', () async { - final Directory skillDir = await Directory('${tempDir.path}/skill_name').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill_name')}Body'); - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isFalse); - expect( - result.errors, - contains(contains('Frontmatter `name` "skill_name" contains invalid characters')), - ); - expect(result.errors, contains(contains('Suggested: "skill-name"'))); - }); - - test('fails if has leading hyphen; suggests stripped form', () async { - final Directory skillDir = await Directory('${tempDir.path}/-skill-name').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: '-skill-name')}Body'); - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('"-skill-name" has leading or trailing hyphens'))); - expect(result.errors, contains(contains('Suggested: "skill-name"'))); - }); - - test('fails if has trailing hyphen; suggests stripped form', () async { - final Directory skillDir = await Directory('${tempDir.path}/skill-name-').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-name-')}Body'); - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('"skill-name-" has leading or trailing hyphens'))); - expect(result.errors, contains(contains('Suggested: "skill-name"'))); - }); - - test('fails if has consecutive hyphens; suggests collapsed form', () async { - final Directory skillDir = await Directory('${tempDir.path}/skill--name').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill--name')}Body'); - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('"skill--name" has consecutive hyphens'))); - expect(result.errors, contains(contains('Suggested: "skill-name"'))); - }); - - test('mismatched name vs dir: error offers both directions to fix', () async { - final Directory skillDir = await Directory('${tempDir.path}/wrong-name').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'right-name')}Body'); - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isFalse); - expect( - result.errors, - contains( - contains( - 'Frontmatter `name` "right-name" does not match the parent ' - 'directory name "wrong-name"', - ), - ), - ); - expect(result.errors, contains(contains('setting `name: wrong-name` in SKILL.md'))); - expect( - result.errors, - contains(contains('renaming the directory from "wrong-name" to "right-name"')), - ); - }); - - test('suggestNormalizedName normalizes case, separators, edges, length', () { - expect(NameFormatRule.suggestNormalizedName('My_Cool Skill!'), 'my-cool-skill'); - expect(NameFormatRule.suggestNormalizedName('--leading--double--'), 'leading-double'); - expect( - NameFormatRule.suggestNormalizedName('a' * (NameFormatRule.maxNameLength + 10)), - 'a' * NameFormatRule.maxNameLength, - ); - }); - - test('fixes name to match directory name (not replacing underscores)', () async { - final Directory skillDir = await Directory('${tempDir.path}/my_skill').create(); - final file = File('${skillDir.path}/SKILL.md'); - await file.writeAsString(''' ---- -name: wrong-name -description: A test skill ---- -Body'''); - - final rule = NameFormatRule(); - final String content = await file.readAsString(); - final RegExpMatch? match = RegExp( - r'^---\s*\n(.*?)\n---\s*\n', - dotAll: true, - ).firstMatch(content); - final parsedYaml = loadYaml(match!.group(1)!) as YamlMap?; - final context = SkillContext( - directory: skillDir, - rawContent: content, - parsedYaml: parsedYaml, - ); - - final String fixedContent = await rule.fix('SKILL.md', content, context.directory); - - expect(fixedContent, contains('name: my_skill')); - }); - }); - - group('Description', () { - test('fails if too long (> ${DescriptionLengthRule.maxDescriptionLength} chars)', () async { - final String longDesc = 'a' * (DescriptionLengthRule.maxDescriptionLength + 1); - final Directory skillDir = await Directory('${tempDir.path}/skill-name').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-name', description: longDesc)}Body'); - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isFalse); - expect( - result.errors, - contains(contains('maximum is ${DescriptionLengthRule.maxDescriptionLength}')), - ); - }); - - test('error message includes char count and |HERE| cutoff excerpt', () async { - // 50 chars before, 50 chars after the cutoff for a distinctive excerpt. - final String before = 'B' * 50; - final String after = 'A' * 50; - final String longDesc = - 'P' * (DescriptionLengthRule.maxDescriptionLength - 50) + before + after; - expect(longDesc.length, DescriptionLengthRule.maxDescriptionLength + 50); - - final Directory skillDir = await Directory('${tempDir.path}/skill-name').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'skill-name', description: longDesc)}Body'); - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isFalse); - - final String error = result.errors.firstWhere((e) => e.contains('Description field is')); - expect(error, contains('Description field is ${longDesc.length} characters')); - expect(error, contains('maximum is ${DescriptionLengthRule.maxDescriptionLength}')); - expect( - error, - contains('Cutoff at character ${DescriptionLengthRule.maxDescriptionLength}'), - ); - expect(error, contains('|HERE|')); - // The chars right before/after the cutoff should appear in the excerpt. - expect(error, contains('BBBBB|HERE|AAAAA')); - }); - }); - - group('Compatibility', () { - test('fails if too long with shared char-count + |HERE| excerpt shape', () async { - // Put a distinctive run of characters straddling the cutoff so the - // excerpt is visible in the assertion. - final String before = 'B' * 50; - final String after = 'A' * 50; - final String longComp = - 'P' * (ValidYamlMetadataRule.maxCompatibilityLength - 50) + before + after; - final Directory skillDir = await Directory('${tempDir.path}/skill-name').create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: skill-name -description: A test skill -compatibility: $longComp ---- -Body'''); - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isFalse); - final String error = result.errors.firstWhere((e) => e.contains('Compatibility field')); - // Same diagnostic shape as description-too-long, generated by the - // shared buildLengthDiagnostic helper. - expect(error, contains('Compatibility field is ${longComp.length} characters')); - expect(error, contains('maximum is ${ValidYamlMetadataRule.maxCompatibilityLength}')); - expect( - error, - contains('Cutoff at character ${ValidYamlMetadataRule.maxCompatibilityLength}'), - ); - expect(error, contains('BBBBB|HERE|AAAAA')); - }); - }); - }); -} diff --git a/tool/dart_skills_lint/test/fixer_test.dart b/tool/dart_skills_lint/test/fixer_test.dart deleted file mode 100644 index 75e173f9..00000000 --- a/tool/dart_skills_lint/test/fixer_test.dart +++ /dev/null @@ -1,172 +0,0 @@ -// 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/entry_point.dart'; -import 'package:dart_skills_lint/src/fixable_rule.dart'; -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/skill_rule.dart'; -import 'package:dart_skills_lint/src/models/validation_error.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -class RuleA extends SkillRule implements FixableRule { - @override - String get name => 'rule-a'; - - @override - AnalysisSeverity get severity => AnalysisSeverity.warning; - - @override - Future> validate(SkillContext context) async { - return [ - ValidationError( - ruleId: name, - message: 'Error A', - severity: AnalysisSeverity.warning, - file: 'SKILL.md', - ), - ]; - } - - @override - Future fix(String filePath, String currentContent, Directory directory) async { - return '$currentContent A'; - } -} - -class RuleB extends SkillRule implements FixableRule { - @override - String get name => 'rule-b'; - - @override - AnalysisSeverity get severity => AnalysisSeverity.warning; - - @override - Future> validate(SkillContext context) async { - return [ - ValidationError( - ruleId: name, - message: 'Error B', - severity: AnalysisSeverity.warning, - file: 'SKILL.md', - ), - ]; - } - - @override - Future fix(String filePath, String currentContent, Directory directory) async { - return '$currentContent B'; - } -} - -class RuleThrows extends SkillRule implements FixableRule { - @override - String get name => 'rule-throws'; - - @override - AnalysisSeverity get severity => AnalysisSeverity.warning; - - @override - Future> validate(SkillContext context) async { - return [ - ValidationError( - ruleId: name, - message: 'Error Throws', - severity: AnalysisSeverity.warning, - file: 'SKILL.md', - ), - ]; - } - - @override - Future fix(String filePath, String currentContent, Directory directory) async { - throw Exception('Fix failed'); - } -} - -void main() { - group('Fixer Sequential Execution', () { - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('fixer_test.'); - }); - - tearDown(() async { - await tempDir.delete(recursive: true); - }); - - test('applies fixes in order', () async { - final skillDir = Directory(p.join(tempDir.path, 'test-skill')); - await skillDir.create(); - final skillFile = File(p.join(skillDir.path, 'SKILL.md')); - await skillFile.writeAsString('Original'); - - final bool success = await validateSkillsInternal( - individualSkillPaths: [skillDir.path], - fixApply: true, - quiet: true, - customRules: [RuleA(), RuleB()], - ); - - expect(success, isFalse); - - final String content = await skillFile.readAsString(); - expect(content, 'Original A B'); - }); - - test( - '--fast-fail stops processing subsequent skills but completes current skill fixes', - () async { - final skillDir1 = Directory(p.join(tempDir.path, 'test-skill-1')); - await skillDir1.create(); - final skillFile1 = File(p.join(skillDir1.path, 'SKILL.md')); - await skillFile1.writeAsString('Original1'); - - final skillDir2 = Directory(p.join(tempDir.path, 'test-skill-2')); - await skillDir2.create(); - final skillFile2 = File(p.join(skillDir2.path, 'SKILL.md')); - await skillFile2.writeAsString('Original2'); - - final bool success = await validateSkillsInternal( - individualSkillPaths: [skillDir1.path, skillDir2.path], - fixApply: true, - fastFail: true, - quiet: true, - customRules: [RuleA()], - ); - - expect(success, isFalse); - - final String content1 = await skillFile1.readAsString(); - expect(content1, 'Original1 A'); - - final String content2 = await skillFile2.readAsString(); - expect(content2, 'Original2'); - }, - ); - - test('handles exceptions in fix method gracefully', () async { - final skillDir = Directory(p.join(tempDir.path, 'test-skill')); - await skillDir.create(); - final skillFile = File(p.join(skillDir.path, 'SKILL.md')); - await skillFile.writeAsString('Original'); - - final bool success = await validateSkillsInternal( - individualSkillPaths: [skillDir.path], - fixApply: true, - quiet: true, - customRules: [RuleThrows()], - ); - - expect(success, isFalse); - - final String content = await skillFile.readAsString(); - expect(content, 'Original'); - }); - }); -} diff --git a/tool/dart_skills_lint/test/ignore_models_test.dart b/tool/dart_skills_lint/test/ignore_models_test.dart deleted file mode 100644 index 051360ba..00000000 --- a/tool/dart_skills_lint/test/ignore_models_test.dart +++ /dev/null @@ -1,63 +0,0 @@ -// 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:dart_skills_lint/src/models/ignore_entry.dart'; -import 'package:dart_skills_lint/src/models/skills_ignores.dart'; -import 'package:test/test.dart'; - -void main() { - group('IgnoreEntry Serialization', () { - test('fromJson parses rule_id and file_name', () { - final Map json = { - IgnoreEntry.ruleIdKey: 'description_too_long', - IgnoreEntry.fileNameKey: 'SKILL.md', - }; - final entry = IgnoreEntry.fromJson(json); - expect(entry.ruleId, equals('description_too_long')); - expect(entry.fileName, equals('SKILL.md')); - expect(entry.used, isFalse); // Default - }); - - test('toJson serializes rule_id and file_name', () { - final entry = IgnoreEntry(ruleId: 'description_too_long', fileName: 'SKILL.md'); - final Map json = entry.toJson(); - expect(json[IgnoreEntry.ruleIdKey], equals('description_too_long')); - expect(json[IgnoreEntry.fileNameKey], equals('SKILL.md')); - expect(json.containsKey('used'), isFalse); // Suppressed - }); - }); - - group('SkillsIgnores Serialization', () { - test('fromJson parses nested skills map', () { - final Map json = { - SkillsIgnores.skillsKey: { - 'skill-a': [ - {IgnoreEntry.ruleIdKey: 'rule1', IgnoreEntry.fileNameKey: 'file1.md'}, - ], - }, - }; - final ignores = SkillsIgnores.fromJson(json); - expect(ignores.skills.containsKey('skill-a'), isTrue); - expect(ignores.skills['skill-a']!.length, equals(1)); - expect(ignores.skills['skill-a']![0].ruleId, equals('rule1')); - }); - - test('toJson serializes nested skills map', () { - final entry = IgnoreEntry(ruleId: 'rule1', fileName: 'file1.md'); - final ignores = SkillsIgnores( - skills: { - 'skill-a': [entry], - }, - ); - final Map json = ignores.toJson(); - - expect(json.containsKey(SkillsIgnores.skillsKey), isTrue); - final skillsJson = json[SkillsIgnores.skillsKey] as Map; - expect(skillsJson.containsKey('skill-a'), isTrue); - final skillAList = skillsJson['skill-a'] as List; - final firstItem = skillAList[0] as Map; - expect(firstItem[IgnoreEntry.ruleIdKey], equals('rule1')); - }); - }); -} diff --git a/tool/dart_skills_lint/test/install_script_test.dart b/tool/dart_skills_lint/test/install_script_test.dart deleted file mode 100644 index 7e955cf6..00000000 --- a/tool/dart_skills_lint/test/install_script_test.dart +++ /dev/null @@ -1,491 +0,0 @@ -// 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 'package:test/test.dart'; -import 'package:test_process/test_process.dart'; - -/// A dummy SHA256 checksum that represents an invalid/corrupted release file hash. -const _corruptedHash = '0000000000000000000000000000000000000000000000000000000000000000'; - -/// A mock implementation of the `curl` command line utility. -/// -/// Simulates downloading a release artifact by copying the source file from -/// `MOCK_RELEASE_DIR` to the target output path specified by `-o`. -/// -/// It ignores other standard `curl` flags. -const _mockCurlScript = r''' -#!/bin/bash -set -eu -outfile="" -url="" -while [ $# -gt 0 ]; do - case "$1" in - -o) outfile="$2"; shift ;; - --retry) shift ;; - -*) ;; - *) url="$1" ;; - esac - shift -done -filename="$(basename "$url")" -src_file="${MOCK_RELEASE_DIR}/$filename" -if [ -f "$src_file" ]; then - cp "$src_file" "$outfile" -else - echo "mock curl: error: source file $src_file not found ($url)" >&2 - exit 1 -fi -'''; - -/// A mock implementation of the `uname` command line utility. -/// -/// Outputs the mocked OS (`MOCK_UNAME_S`) or CPU architecture (`MOCK_UNAME_M`) -/// depending on whether it is executed with `-s` or `-m` flags. -const _mockUnameScript = r''' -#!/bin/bash -if [ "$1" = "-s" ]; then - echo "${MOCK_UNAME_S:-Darwin}" -elif [ "$1" = "-m" ]; then - echo "${MOCK_UNAME_M:-arm64}" -fi -'''; - -void main() { - group('install.sh integration', () { - late Directory tempDir; - late Directory mockBinDir; - late Directory mockReleaseDir; - late Directory installDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('install_sh_test.'); - mockBinDir = await Directory(p.join(tempDir.path, 'bin')).create(); - mockReleaseDir = await Directory(p.join(tempDir.path, 'mock_release')).create(); - installDir = await Directory(p.join(tempDir.path, 'install')).create(); - - // Write mock uname script - final unameFile = File(p.join(mockBinDir.path, 'uname')); - await unameFile.writeAsString(_mockUnameScript); - final ProcessResult chmodUnameResult = await Process.run('chmod', ['+x', unameFile.path]); - expect( - chmodUnameResult.exitCode, - 0, - reason: 'chmod failed for uname mock: ${chmodUnameResult.stderr}', - ); - - // Write mock curl script - final curlFile = File(p.join(mockBinDir.path, 'curl')); - await curlFile.writeAsString(_mockCurlScript); - final ProcessResult chmodCurlResult = await Process.run('chmod', ['+x', curlFile.path]); - expect( - chmodCurlResult.exitCode, - 0, - reason: 'chmod failed for curl mock: ${chmodCurlResult.stderr}', - ); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('successful installation on macos-arm64', () async { - await _runInstallScriptTest( - tempDir: tempDir, - mockBinDir: mockBinDir, - mockReleaseDir: mockReleaseDir, - installDir: installDir, - os: 'macos', - arch: 'arm64', - mockUnameS: 'Darwin', - mockUnameM: 'arm64', - simulateLaunchFailure: false, - expectedExitCode: 0, - expectInstalled: true, - ); - }); - - test('successful installation on linux-x64', () async { - await _runInstallScriptTest( - tempDir: tempDir, - mockBinDir: mockBinDir, - mockReleaseDir: mockReleaseDir, - installDir: installDir, - os: 'linux', - arch: 'x64', - mockUnameS: 'Linux', - mockUnameM: 'x86_64', - simulateLaunchFailure: false, - expectedExitCode: 0, - expectInstalled: true, - ); - }); - - test('fails on linux if installed binary fails launch check', () async { - await _runInstallScriptTest( - tempDir: tempDir, - mockBinDir: mockBinDir, - mockReleaseDir: mockReleaseDir, - installDir: installDir, - os: 'linux', - arch: 'x64', - mockUnameS: 'Linux', - mockUnameM: 'x86_64', - simulateLaunchFailure: true, - expectedExitCode: 1, - expectInstalled: true, - ); - }); - - test('succeeds on macos even if installed binary fails launch check', () async { - await _runInstallScriptTest( - tempDir: tempDir, - mockBinDir: mockBinDir, - mockReleaseDir: mockReleaseDir, - installDir: installDir, - os: 'macos', - arch: 'arm64', - mockUnameS: 'Darwin', - mockUnameM: 'arm64', - simulateLaunchFailure: true, - expectedExitCode: 0, - expectInstalled: true, - ); - }); - - test('fails if checksum mismatch', () async { - const version = '0.4.0-test'; - await _createMockRelease( - tempDir: tempDir, - mockReleaseDir: mockReleaseDir, - os: 'macos', - arch: 'arm64', - binaryContent: 'dummy', - shouldCorruptHash: true, - ); - - // TODO(reidbaker): Use Windows path separator (;) when running on Windows hosts. https://github.com/flutter/agent-plugins/issues/164 - final newPath = '${mockBinDir.path}:${Platform.environment['PATH']}'; - final String packageRoot = _getPackageRoot(); - final String scriptPath = p.join(packageRoot, 'scripts', 'install.sh'); - - final TestProcess process = await TestProcess.start( - 'bash', - [scriptPath], - environment: { - 'PATH': newPath, - 'MOCK_UNAME_S': 'Darwin', - 'MOCK_UNAME_M': 'arm64', - 'MOCK_RELEASE_DIR': mockReleaseDir.path, - 'INSTALL_DIR': installDir.path, - 'VERSION': version, - }, - ); - - final List stderr = await process.stderr.rest.toList(); - expect(stderr.any((line) => line.contains('SHA256 mismatch')), isTrue); - await process.shouldExit(1); - }); - - group('missing required tools', () { - final requiredTools = ['curl', 'tar', 'awk']; - - for (var i = 0; i < requiredTools.length; i++) { - final String missingTool = requiredTools[i]; - - test('fails if required tool $missingTool is missing', () async { - // Create a directory containing mock uname and all required tools before this one - final Directory testBinDir = await Directory( - p.join(tempDir.path, 'bin_$missingTool'), - ).create(); - - // Always copy mock uname - final mockUnameFile = File(p.join(mockBinDir.path, 'uname')); - await mockUnameFile.copy(p.join(testBinDir.path, 'uname')); - final ProcessResult chmodUnameResult = await Process.run('chmod', [ - '+x', - p.join(testBinDir.path, 'uname'), - ]); - expect(chmodUnameResult.exitCode, 0); - - // Copy all mock tools prior to this one in the dependency order - for (var j = 0; j < i; j++) { - final String toolToCopy = requiredTools[j]; - if (toolToCopy == 'curl') { - final mockCurlFile = File(p.join(mockBinDir.path, 'curl')); - await mockCurlFile.copy(p.join(testBinDir.path, 'curl')); - final ProcessResult chmodCurlResult = await Process.run('chmod', [ - '+x', - p.join(testBinDir.path, 'curl'), - ]); - expect(chmodCurlResult.exitCode, 0); - } else { - // Write a dummy script for other tools (like tar) so they pass command -v check - final dummyFile = File(p.join(testBinDir.path, toolToCopy)); - await dummyFile.writeAsString('#!/bin/bash\nexit 0\n'); - final ProcessResult chmodDummyResult = await Process.run('chmod', [ - '+x', - dummyFile.path, - ]); - expect(chmodDummyResult.exitCode, 0); - } - } - - final String packageRoot = _getPackageRoot(); - final String scriptPath = p.join(packageRoot, 'scripts', 'install.sh'); - - final TestProcess process = await TestProcess.start( - '/bin/bash', - [scriptPath], - environment: { - 'PATH': testBinDir.path, - 'MOCK_UNAME_S': 'Darwin', - 'MOCK_UNAME_M': 'arm64', - 'INSTALL_DIR': installDir.path, - }, - ); - - final List stderr = await process.stderr.rest.toList(); - expect( - stderr.any((line) => line.contains("required tool '$missingTool' not found on PATH")), - isTrue, - ); - await process.shouldExit(1); - }); - } - - test('fails if both sha256sum and shasum are missing', () async { - final Directory testBinDir = await Directory(p.join(tempDir.path, 'bin_no_hash')).create(); - - // Copy mock uname, curl, and dummy tar, awk - final mockUnameFile = File(p.join(mockBinDir.path, 'uname')); - await mockUnameFile.copy(p.join(testBinDir.path, 'uname')); - final ProcessResult chmodUname = await Process.run('chmod', [ - '+x', - p.join(testBinDir.path, 'uname'), - ]); - expect(chmodUname.exitCode, 0); - - final mockCurlFile = File(p.join(mockBinDir.path, 'curl')); - await mockCurlFile.copy(p.join(testBinDir.path, 'curl')); - final ProcessResult chmodCurl = await Process.run('chmod', [ - '+x', - p.join(testBinDir.path, 'curl'), - ]); - expect(chmodCurl.exitCode, 0); - - final dummyTar = File(p.join(testBinDir.path, 'tar')); - await dummyTar.writeAsString('#!/bin/bash\nexit 0\n'); - final ProcessResult chmodTar = await Process.run('chmod', ['+x', dummyTar.path]); - expect(chmodTar.exitCode, 0); - - final dummyAwk = File(p.join(testBinDir.path, 'awk')); - await dummyAwk.writeAsString('#!/bin/bash\nexit 0\n'); - final ProcessResult chmodAwk = await Process.run('chmod', ['+x', dummyAwk.path]); - expect(chmodAwk.exitCode, 0); - - final String packageRoot = _getPackageRoot(); - final String scriptPath = p.join(packageRoot, 'scripts', 'install.sh'); - - final TestProcess process = await TestProcess.start( - '/bin/bash', - [scriptPath], - environment: { - 'PATH': testBinDir.path, - 'MOCK_UNAME_S': 'Darwin', - 'MOCK_UNAME_M': 'arm64', - 'INSTALL_DIR': installDir.path, - }, - ); - - final List stderr = await process.stderr.rest.toList(); - expect( - stderr.any( - (line) => - line.contains('sha256sum') && line.contains('shasum') && line.contains('not found'), - ), - isTrue, - ); - await process.shouldExit(1); - }); - }); - - test('fails on unsupported architecture', () async { - // TODO(reidbaker): Use Windows path separator (;) when running on Windows hosts. https://github.com/flutter/agent-plugins/issues/164 - final newPath = '${mockBinDir.path}:${Platform.environment['PATH']}'; - final String packageRoot = _getPackageRoot(); - final String scriptPath = p.join(packageRoot, 'scripts', 'install.sh'); - - final TestProcess process = await TestProcess.start( - 'bash', - [scriptPath], - environment: { - 'PATH': newPath, - 'MOCK_UNAME_S': 'Linux', - 'MOCK_UNAME_M': 'i386', - 'INSTALL_DIR': installDir.path, - }, - ); - - final List stderr = await process.stderr.rest.toList(); - expect(stderr.any((line) => line.contains('unsupported architecture')), isTrue); - await process.shouldExit(1); - }); - // TODO(reidbaker): Support running install.sh tests on Windows hosts. https://github.com/flutter/agent-plugins/issues/164 - }, skip: Platform.isWindows ? 'install.sh is not supported on Windows' : null); -} - -String _getPackageRoot() { - final String currentPath = Directory.current.path; - var dir = Directory(currentPath); - while (dir.path != '/' && dir.path.isNotEmpty) { - final pubspec = File(p.join(dir.path, 'pubspec.yaml')); - if (pubspec.existsSync() && pubspec.readAsStringSync().contains('name: dart_skills_lint')) { - return dir.path; - } - dir = dir.parent; - } - // Fallback to searching subdirectories - final subdir = Directory(p.join(currentPath, 'tool', 'dart_skills_lint')); - if (subdir.existsSync()) { - return subdir.path; - } - return currentPath; -} - -/// Simulates a packaged GitHub release asset by writing a dummy binary, -/// compressing it to a `.tar.gz` archive in [mockReleaseDir], and generating -/// the corresponding `SHA256SUMS` checksum file. -/// -/// If [shouldCorruptHash] is true, the `SHA256SUMS` file will be written with -/// an invalid hash to test checksum verification failure paths. -Future _createMockRelease({ - required Directory tempDir, - required Directory mockReleaseDir, - required String os, - required String arch, - required String binaryContent, - bool shouldCorruptHash = false, -}) async { - final target = '$os-$arch'; - final binaryName = 'dart_skills_lint-$target'; - final archiveName = 'dart_skills_lint-$target.tar.gz'; - - // Create dummy binary file - final dummyBin = File(p.join(tempDir.path, binaryName)); - await dummyBin.writeAsString(binaryContent); - final ProcessResult chmodBinResult = await Process.run('chmod', ['+x', dummyBin.path]); - expect( - chmodBinResult.exitCode, - 0, - reason: 'chmod failed for dummy binary: ${chmodBinResult.stderr}', - ); - - // Package it into tar.gz - final ProcessResult tarResult = await Process.run('tar', [ - '-czf', - p.join(mockReleaseDir.path, archiveName), - '-C', - tempDir.path, - binaryName, - ]); - expect(tarResult.exitCode, 0, reason: 'tar packaging failed: ${tarResult.stderr}'); - - // Get SHA256 sum - var hash = ''; - // TODO(reidbaker): Re-add CertUtil checksum verification for Windows hosts. https://github.com/flutter/agent-plugins/issues/164 - final ProcessResult shaProcess = await Process.run('shasum', [ - '-a', - '256', - p.join(mockReleaseDir.path, archiveName), - ]); - if (shaProcess.exitCode == 0) { - hash = shaProcess.stdout.toString().trim().split(' ')[0]; - } else { - final ProcessResult sha256Process = await Process.run('sha256sum', [ - p.join(mockReleaseDir.path, archiveName), - ]); - if (sha256Process.exitCode == 0) { - hash = sha256Process.stdout.toString().trim().split(' ')[0]; - } - } - - if (hash.isEmpty) { - throw StateError('Could not calculate SHA256 hash using shasum or sha256sum.'); - } - - final String finalHash = shouldCorruptHash ? _corruptedHash : hash; - - final sha256sums = File(p.join(mockReleaseDir.path, 'SHA256SUMS')); - await sha256sums.writeAsString('$finalHash $archiveName\n'); -} - -Future _runInstallScriptTest({ - required Directory tempDir, - required Directory mockBinDir, - required Directory mockReleaseDir, - required Directory installDir, - required String os, - required String arch, - required String mockUnameS, - required String mockUnameM, - required bool simulateLaunchFailure, - required int expectedExitCode, - required bool expectInstalled, -}) async { - const version = '0.4.0-test'; - final binaryContent = simulateLaunchFailure - ? '#!/usr/bin/env bash\nexit 1\n' - : '#!/usr/bin/env bash\necho "mock-cli-help"\n'; - - await _createMockRelease( - tempDir: tempDir, - mockReleaseDir: mockReleaseDir, - os: os, - arch: arch, - binaryContent: binaryContent, - ); - - // TODO(reidbaker): Use Windows path separator (;) when running on Windows hosts. https://github.com/flutter/agent-plugins/issues/164 - final newPath = '${mockBinDir.path}:${Platform.environment['PATH']}'; - final String packageRoot = _getPackageRoot(); - final String scriptPath = p.join(packageRoot, 'scripts', 'install.sh'); - - final TestProcess process = await TestProcess.start( - 'bash', - [scriptPath], - environment: { - 'PATH': newPath, - 'MOCK_UNAME_S': mockUnameS, - 'MOCK_UNAME_M': mockUnameM, - 'MOCK_RELEASE_DIR': mockReleaseDir.path, - 'INSTALL_DIR': installDir.path, - 'VERSION': version, - }, - ); - - await process.shouldExit(expectedExitCode); - - final installedFile = File(p.join(installDir.path, 'dart_skills_lint')); - expect(installedFile.existsSync(), equals(expectInstalled)); - - if (expectInstalled && expectedExitCode == 0) { - if (simulateLaunchFailure) { - final List stdout = await process.stdout.rest.toList(); - expect( - stdout.any((line) => line.contains('launch check failed โ€” likely Gatekeeper')), - isTrue, - ); - } else { - final ProcessResult runResult = await Process.run(installedFile.path, ['--help']); - expect(runResult.stdout.toString().trim(), equals('mock-cli-help')); - } - } else if (expectedExitCode == 1 && simulateLaunchFailure) { - final List stderr = await process.stderr.rest.toList(); - expect(stderr.any((line) => line.contains('failed to launch')), isTrue); - } -} diff --git a/tool/dart_skills_lint/test/metadata_validation_test.dart b/tool/dart_skills_lint/test/metadata_validation_test.dart deleted file mode 100644 index cd807a7d..00000000 --- a/tool/dart_skills_lint/test/metadata_validation_test.dart +++ /dev/null @@ -1,116 +0,0 @@ -// 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/validation_error.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'; - -import 'test_utils.dart'; - -void main() { - group('Metadata (YAML) Validation', () { - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('metadata_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('fails if YAML metadata is invalid', () async { - await File('${tempDir.path}/SKILL.md').writeAsString(''' ---- -invalid: yaml: frontmatter ---- -Body'''); - final validator = Validator(); - final ValidationResult result = await validator.validate(tempDir); - - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('Invalid YAML metadata'))); - }); - - test('fails if required field "name" is missing', () async { - await File('${tempDir.path}/SKILL.md').writeAsString(''' ---- -description: A test skill ---- -Body'''); - final validator = Validator(); - final ValidationResult result = await validator.validate(tempDir); - - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('Missing required field: name'))); - }); - - test('fails if required field "description" is missing', () async { - await File('${tempDir.path}/SKILL.md').writeAsString(''' ---- -name: metadata-test ---- -Body'''); - final validator = Validator(); - final ValidationResult result = await validator.validate(tempDir); - - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('Missing required field: description'))); - }); - - test('passes without warning if disallowed fields are present', () async { - final skillDir = Directory('${tempDir.path}/metadata-test'); - await skillDir.create(); - await File('${skillDir.path}/SKILL.md').writeAsString(''' ---- -name: metadata-test -description: A test skill -extra-field: not allowed ---- -Body'''); - - final validator = Validator(); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.warnings, isEmpty); - - final Iterable disallowedErrors = result.validationErrors.where( - (e) => e.ruleId == DisallowedFieldRule.ruleName, - ); - expect(disallowedErrors, isEmpty); - }); - - test('passes with all allowed fields and valid YAML', () async { - await File('${tempDir.path}/SKILL.md').writeAsString(''' ---- -name: metadata-test -description: A test skill -license: MIT -compatibility: Python 3.10 -metadata: - version: 1.0.0 -allowed-tools: git ---- -Body'''); - final validator = Validator(); - // We need to make sure directory name matches name in metadata - final skillDir = Directory('${tempDir.path}/metadata-test'); - await skillDir.create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'metadata-test')}Body'); - - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue, reason: result.errors.isEmpty ? '' : result.errors.first); - expect(result.errors, isEmpty); - }); - }); -} diff --git a/tool/dart_skills_lint/test/path_utils_test.dart b/tool/dart_skills_lint/test/path_utils_test.dart deleted file mode 100644 index d7986433..00000000 --- a/tool/dart_skills_lint/test/path_utils_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -// 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/path_utils.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -void main() { - group('expandPath', () { - test('expands tilde at start of path', () { - final String? home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; - if (home != null) { - expect(expandPath('~/some/path'), equals(p.join(home, 'some/path'))); - } else { - // If home is null, it should return the path as is. - expect(expandPath('~/some/path'), equals('~/some/path')); - } - }); - - test('does not expand tilde not at start of path', () { - expect(expandPath('some/~/path'), equals('some/~/path')); - }); - - test('returns path as is if it does not start with tilde', () { - expect(expandPath('some/path'), equals('some/path')); - expect(expandPath('/absolute/path'), equals('/absolute/path')); - }); - }); -} diff --git a/tool/dart_skills_lint/test/prevent_skills_sh_publishing_rule_test.dart b/tool/dart_skills_lint/test/prevent_skills_sh_publishing_rule_test.dart deleted file mode 100644 index 9ebbc27d..00000000 --- a/tool/dart_skills_lint/test/prevent_skills_sh_publishing_rule_test.dart +++ /dev/null @@ -1,140 +0,0 @@ -// 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/skill_context.dart'; -import 'package:dart_skills_lint/src/models/validation_error.dart'; -import 'package:dart_skills_lint/src/rules/prevent_skills_sh_publishing_rule.dart'; -import 'package:test/test.dart'; -import 'package:yaml/yaml.dart'; - -void main() { - group('PreventSkillsShPublishingRule', () { - test('flags when YAML frontmatter is completely missing', () async { - final rule = PreventSkillsShPublishingRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: 'Just some text, no frontmatter.', - ); - - final List errors = await rule.validate(context); - - expect(errors, isNotEmpty); - expect(errors.first.message, contains('Missing YAML frontmatter')); - }); - - test('returns no errors when there is a YAML parsing error', () async { - final rule = PreventSkillsShPublishingRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '---\ninvalid: yaml: : mapping\n---\n', - yamlParsingError: 'YAML parsing error details', - ); - - final List errors = await rule.validate(context); - - expect(errors, isEmpty); - }); - - test('flags when metadata field is missing', () async { - final rule = PreventSkillsShPublishingRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '---\nname: my-skill\ndescription: Test\n---\n', - parsedYaml: loadYaml('name: my-skill\ndescription: Test\n') as YamlMap, - ); - - final List errors = await rule.validate(context); - - expect(errors, isNotEmpty); - expect(errors.first.message, contains('Missing "metadata" block in YAML frontmatter.')); - }); - - test('flags when metadata is not a map', () async { - final rule = PreventSkillsShPublishingRule(severity: AnalysisSeverity.warning); - final parsed = - loadYaml('name: my-skill\ndescription: Test\nmetadata: "some string"\n') as YamlMap; - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '---\nname: my-skill\ndescription: Test\nmetadata: "some string"\n---\n', - parsedYaml: parsed, - ); - - final List errors = await rule.validate(context); - - expect(errors, isNotEmpty); - expect(errors.first.message, contains('"metadata" must be a YAML mapping (dictionary).')); - }); - - test('flags when metadata internal is false', () async { - final rule = PreventSkillsShPublishingRule(severity: AnalysisSeverity.warning); - final parsed = - loadYaml('name: my-skill\ndescription: Test\nmetadata:\n internal: false\n') as YamlMap; - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '---\nname: my-skill\ndescription: Test\nmetadata:\n internal: false\n---\n', - parsedYaml: parsed, - ); - - final List errors = await rule.validate(context); - - expect(errors, isNotEmpty); - expect( - errors.first.message, - contains('The "internal" field under "metadata" must be explicitly set to boolean true'), - ); - }); - - test('flags when metadata internal is a string', () async { - final rule = PreventSkillsShPublishingRule(severity: AnalysisSeverity.warning); - final parsed = - loadYaml('name: my-skill\ndescription: Test\nmetadata:\n internal: "True"\n') as YamlMap; - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '---\nname: my-skill\ndescription: Test\nmetadata:\n internal: "True"\n---\n', - parsedYaml: parsed, - ); - - final List errors = await rule.validate(context); - - expect(errors, isNotEmpty); - expect(errors.first.message, contains('is set to a string "True"')); - }); - - test('flags when metadata internal is a string with whitespace', () async { - final rule = PreventSkillsShPublishingRule(severity: AnalysisSeverity.warning); - final parsed = - loadYaml('name: my-skill\ndescription: Test\nmetadata:\n internal: " True "\n') - as YamlMap; - final context = SkillContext( - directory: Directory('dummy'), - rawContent: - '---\nname: my-skill\ndescription: Test\nmetadata:\n internal: " True "\n---\n', - parsedYaml: parsed, - ); - - final List errors = await rule.validate(context); - - expect(errors, isNotEmpty); - expect(errors.first.message, contains('is set to a string " True "')); - }); - - test('passes when metadata internal is true', () async { - final rule = PreventSkillsShPublishingRule(severity: AnalysisSeverity.warning); - final parsed = - loadYaml('name: my-skill\ndescription: Test\nmetadata:\n internal: true\n') as YamlMap; - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '---\nname: my-skill\ndescription: Test\nmetadata:\n internal: true\n---\n', - parsedYaml: parsed, - ); - - final List errors = await rule.validate(context); - - expect(errors, isEmpty); - }); - }); -} diff --git a/tool/dart_skills_lint/test/recipe_drift_test.dart b/tool/dart_skills_lint/test/recipe_drift_test.dart deleted file mode 100644 index 34989181..00000000 --- a/tool/dart_skills_lint/test/recipe_drift_test.dart +++ /dev/null @@ -1,240 +0,0 @@ -// 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 'package:test/test.dart'; -import 'package:test_process/test_process.dart'; -import 'package:yaml/yaml.dart'; - -/// Drift guard for the `## Recipes` section of README.md. -/// -/// The README ships copy-pasteable integration recipes. When a flag or -/// command in them goes stale, downstream adopters silently run a -/// broken pipeline. This test reads the README at test time and -/// asserts each recipe is still well-formed. -/// -/// Three checks: -/// 1. The README has recipe code blocks with non-empty bodies. -/// 2. The GitHub Actions YAML parses and wires up the expected -/// setup-dart, install, and invocation steps. -/// 3. The pre-commit hook body runs end-to-end against the valid and -/// invalid example fixtures and exits with the expected codes. -void main() { - group('README Recipes drift', () { - late _RecipeReader reader; - final String cliPath = p.normalize(p.absolute('bin/cli.dart')); - final String validFixture = p.normalize(p.absolute('example/skills/valid')); - final String invalidFixture = p.normalize(p.absolute('example/skills/invalid')); - - setUpAll(() { - reader = _RecipeReader.fromFile(p.normalize(p.absolute('README.md'))); - }); - - test('README has all expected recipes with non-empty bodies', () { - expect(reader.yamlBlocks, isNotEmpty, reason: 'GitHub Actions YAML recipe missing'); - expect(reader.shellBlocks, isNotEmpty, reason: 'pre-commit hook shell recipe missing'); - for (final _RecipeBlock block in reader.allBlocks) { - expect(block.body.trim(), isNotEmpty); - } - }); - - test('agent recipe references both setup and validation skills by path', () { - // The "have an agent set it up for you" recipe is plain prose - // inside a blockquote, not a fenced code block, so check the raw - // README text for the skill paths it should point at. - final String readme = File(p.normalize(p.absolute('README.md'))).readAsStringSync(); - final int recipesIdx = readme.indexOf('## Recipes'); - expect(recipesIdx, isNonNegative, reason: 'README has no Recipes section'); - final String recipesSection = readme.substring(recipesIdx); - expect( - recipesSection, - contains('skills/dart-skills-lint-setup/SKILL.md'), - reason: 'agent recipe lost its pointer to the setup skill', - ); - expect( - recipesSection, - contains('skills/dart-skills-lint-validation/SKILL.md'), - reason: 'agent recipe lost its pointer to the validation skill', - ); - }); - - test('GitHub Actions recipe parses and wires up setup-dart + install + invocation', () { - final YamlMap doc = reader.workflowYaml; - expect(doc['name'], 'Lint Agent Skills'); - - final jobs = doc['jobs'] as YamlMap; - expect(jobs.keys, contains('lint-skills')); - final lintJob = jobs['lint-skills'] as YamlMap; - final steps = lintJob['steps'] as YamlList; - - expect(reader.stepsUsing(steps), contains('dart-lang/setup-dart@v1')); - - final List runs = reader.stepsRunning(steps); - expect( - runs.any((r) => r.contains('dart install dart_skills_lint')), - isTrue, - reason: 'workflow no longer installs dart_skills_lint', - ); - expect( - runs.any((r) => r.contains('dart_skills_lint --skills-directory')), - isTrue, - reason: 'workflow no longer runs the linter against a skills directory', - ); - expect( - runs.any((r) => r.contains('dart pub global')), - isFalse, - reason: 'workflow still references legacy dart pub global commands', - ); - }); - - test('pre-commit hook body exits 0 on a valid fixture, non-zero on an invalid one', () async { - // Run the actual hook (rewritten to call bin/cli.dart instead of a - // globally-installed linter) against both example fixtures. This - // catches drift in the hook's exec line, exit-code propagation, and - // the linter's response to a known-good vs known-bad skill โ€” all in - // one place. - final String hookBody = reader.preCommitHookBody.replaceAll( - 'dart_skills_lint --skills-directory', - 'dart "$cliPath" --skills-directory', - ); - - await _runHookAgainst(hookBody, validFixture, expectZeroExit: true); - await _runHookAgainst(hookBody, invalidFixture, expectZeroExit: false); - }); - }, skip: Platform.isWindows ? 'recipe drift uses POSIX shell' : null); -} - -Future _runHookAgainst( - String hookBody, - String fixturePath, { - required bool expectZeroExit, -}) async { - // The recipe targets a roots-directory (--skills-directory); fixtures - // are individual skills, so swap the flag to --skill and substitute - // the fixture path in for the placeholder ./.claude/skills. - final String runnable = hookBody - .replaceAll('--skills-directory', '--skill') - .replaceAll('./.claude/skills', fixturePath); - - final Directory tmp = await Directory.systemTemp.createTemp('recipe_hook.'); - try { - final hookFile = File(p.join(tmp.path, 'pre-commit')); - await hookFile.writeAsString(runnable); - final ProcessResult chmod = await Process.run('chmod', ['+x', hookFile.path]); - expect(chmod.exitCode, 0); - - final TestProcess process = await TestProcess.start(hookFile.path, const []); - final int exit = await process.exitCode; - if (expectZeroExit) { - expect(exit, 0, reason: 'hook should exit 0 against fixture $fixturePath'); - } else { - expect(exit, isNonZero, reason: 'hook should exit non-zero against fixture $fixturePath'); - } - } finally { - if (tmp.existsSync()) { - await tmp.delete(recursive: true); - } - } -} - -/// Small parser-and-accessor for the recipe section of README.md. The -/// tests above read like a list of assertions; the parsing lives here. -class _RecipeReader { - _RecipeReader._(this.allBlocks); - - factory _RecipeReader.fromFile(String readmePath) { - final String content = File(readmePath).readAsStringSync(); - return _RecipeReader._(_extractBlocks(content)); - } - - final List<_RecipeBlock> allBlocks; - - List<_RecipeBlock> get yamlBlocks => - allBlocks.where((b) => b.language == 'yaml').toList(growable: false); - - List<_RecipeBlock> get shellBlocks => - allBlocks.where((b) => b.language == 'bash').toList(growable: false); - - /// The first YAML block that contains a `jobs:` key โ€” the actual - /// workflow file the recipe documents (vs. small snippet variants). - YamlMap get workflowYaml { - final _RecipeBlock block = yamlBlocks.firstWhere( - (b) => b.body.contains('jobs:'), - orElse: () => fail('no full workflow YAML block found under Recipes'), - ); - final Object? doc = loadYaml(block.body); - expect(doc, isA(), reason: 'workflow YAML failed to parse as a map'); - return doc! as YamlMap; - } - - /// The body between `<<'HOOK'` and `HOOK` markers in the pre-commit - /// shell recipe โ€” the executable hook itself, sans wrapping `cat >` / - /// `chmod +x` plumbing. - String get preCommitHookBody { - final _RecipeBlock block = shellBlocks.firstWhere( - (b) => b.body.contains('.git/hooks/pre-commit') && b.body.contains('HOOK'), - orElse: () => fail('pre-commit HEREDOC recipe missing'), - ); - // Matches a shell HEREDOC of the form - // <<'HOOK' - // ...body lines... - // HOOK - // capturing the body (everything between the opening `<<'HOOK'` - // newline and the closing `HOOK` line, exclusive). dotAll lets `.` - // span newlines so the body matches across lines; the inner `.*?` - // is non-greedy so we stop at the first closing `HOOK`. - final heredoc = RegExp(r"<<'HOOK'\n(.*?)\nHOOK", dotAll: true); - final RegExpMatch? match = heredoc.firstMatch(block.body); - expect(match, isNotNull, reason: 'HEREDOC body could not be parsed'); - return match!.group(1)!; - } - - List stepsUsing(YamlList steps) => steps - .whereType() - .where((s) => s.containsKey('uses')) - .map((s) => s['uses'] as String) - .toList(growable: false); - - List stepsRunning(YamlList steps) => steps - .whereType() - .where((s) => s.containsKey('run')) - .map((s) => s['run'] as String) - .toList(growable: false); - - static List<_RecipeBlock> _extractBlocks(String readme) { - // Matches the README's `## Recipes` heading and captures everything - // from the line after the heading up to (but not including) the - // next `## ` heading. multiLine makes `^` anchor at line starts so - // the lookahead picks up sibling H2 headings; dotAll lets the - // non-greedy body span line breaks. - final section = RegExp(r'^## Recipes\s*\n(.*?)(?=^## )', multiLine: true, dotAll: true); - final RegExpMatch? match = section.firstMatch(readme); - if (match == null) { - return const []; - } - final String body = match.group(1)!; - // Matches a fenced code block of the form - // ``` - // ...body... - // ``` - // capturing the language tag (group 1, may be empty) and the body - // (group 2). The language tag is [a-zA-Z0-9_-]* so we accept - // ```yaml, ```bash, ```dart, etc. multiLine + dotAll let the - // opening/closing backticks anchor to line starts and the inner - // body span newlines. - final fence = RegExp(r'^```([a-zA-Z0-9_-]*)\s*\n(.*?)^```', multiLine: true, dotAll: true); - return [ - for (final RegExpMatch m in fence.allMatches(body)) - _RecipeBlock((m.group(1) ?? '').trim(), m.group(2)!), - ]; - } -} - -class _RecipeBlock { - _RecipeBlock(this.language, this.body); - final String language; - final String body; -} diff --git a/tool/dart_skills_lint/test/relative_path_flag_test.dart b/tool/dart_skills_lint/test/relative_path_flag_test.dart deleted file mode 100644 index 9fd9b186..00000000 --- a/tool/dart_skills_lint/test/relative_path_flag_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -// 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/rule_config.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'; -import 'package:test/test.dart'; - -import 'test_utils.dart'; - -void main() { - group('Relative Path Flag Validation', () { - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('relative_path_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('validates links when relativePathsSeverity = warning', () async { - final skillDir = Directory('${tempDir.path}/test-skill'); - await skillDir.create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}Body with [broken link](missing.md) and [absolute link](/absolute/path.md)', - ); - - final validator = Validator( - ruleConfigs: { - RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning), - AbsolutePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.error), - }, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isFalse); - expect( - result.errors, - contains(contains('Absolute filepath found in link: /absolute/path.md')), - ); - expect(result.warnings, contains(contains('Linked file does not exist: missing.md'))); - }); - - test('passes when relativePathsSeverity = warning and links are valid', () async { - final skillDir = Directory('${tempDir.path}/test-skill'); - await skillDir.create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}Body with [valid relative link](valid.md)', - ); - await File('${skillDir.path}/valid.md').writeAsString('Valid file content'); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - expect(result.warnings, isEmpty); - }); - }); -} diff --git a/tool/dart_skills_lint/test/relative_paths_test.dart b/tool/dart_skills_lint/test/relative_paths_test.dart deleted file mode 100644 index 3ac88021..00000000 --- a/tool/dart_skills_lint/test/relative_paths_test.dart +++ /dev/null @@ -1,217 +0,0 @@ -// 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/rule_config.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'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -import 'test_utils.dart'; - -void main() { - group('Relative Paths Validation', () { - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('paths_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('passes with valid relative file path (existing file)', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}[Link to a reference](references/DETAILS.md)\n', - ); - - final Directory refDir = await Directory('${skillDir.path}/references').create(); - await File('${refDir.path}/DETAILS.md').writeAsString('Details here'); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - expect(result.warnings, isEmpty); - }); - - test('warns with missing relative file path and reports resolved path', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}[Link to a references file missing](references/MISSING.md)\n', - ); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.warnings, contains(contains('Linked file does not exist'))); - expect(result.warnings, contains(contains('references/MISSING.md'))); - // The diagnostic includes the resolved absolute path. The exact - // shape differs by platform (POSIX `/...` vs Windows `C:\...`), - // so just assert the prefix and that what follows is absolute. - final String warning = result.warnings.firstWhere((w) => w.contains('resolved to ')); - final int prefixIdx = warning.indexOf('resolved to '); - final String resolved = warning.substring(prefixIdx + 'resolved to '.length); - expect(p.isAbsolute(resolved), isTrue, reason: 'resolved path "$resolved" is not absolute'); - }); - - test('did-you-mean: suggests near-miss sibling file when one exists', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}[Link](references/DEATILS.md)\n'); - final Directory refs = await Directory('${skillDir.path}/references').create(); - await File('${refs.path}/DETAILS.md').writeAsString('Details'); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isTrue); - // Suggestion preserves the link's directory prefix so the user - // gets back a copy-pasteable replacement, not just a basename. - expect(result.warnings, contains(contains('Did you mean "references/DETAILS.md"?'))); - }); - - test('did-you-mean: stays silent when nothing in the sibling dir is close', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}[Link](references/MISSING.md)\n'); - final Directory refs = await Directory('${skillDir.path}/references').create(); - await File('${refs.path}/UNRELATED.txt').writeAsString('Nope'); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - expect(result.isValid, isTrue); - expect(result.warnings, contains(contains('Linked file does not exist'))); - expect(result.warnings.any((w) => w.contains('Did you mean')), isFalse); - }); - - test('fails with absolute file path', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}[Absolute path link](/tmp/some_absolute_path/file.md)\n', - ); - - final validator = Validator( - ruleConfigs: { - RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning), - AbsolutePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.error), - }, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isFalse); - expect(result.errors, contains(contains('Absolute filepath found in link'))); - }); - - test('ignores web URLs, emails, javascript, data URIs, and anchors', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}- [Web link](http://example.com)\n- [Web TLS link](https://example.com)\n- [Email link](mailto:user@domain.com)\n- [JS link](javascript:alert(1))\n- [Data URI](data:image/png;base64,iVBORw)\n- [Anchor link](#section-name)\n', - ); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - expect(result.warnings, isEmpty); // None of these should trigger local file checks - }); - - test('passes with valid relative image path and title', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}![Accessible description](images/screenshot.png "Hover description")\n', - ); - - final Directory imgDir = await Directory('${skillDir.path}/images').create(); - await File('${imgDir.path}/screenshot.png').writeAsString('image content'); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - expect(result.warnings, isEmpty); - }); - - test('passes with relative path containing line fragments', () async { - final Directory skillDir = await Directory( - '${tempDir.path}/a/b/c/test-skill', - ).create(recursive: true); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}[Link to lines](../../../CONTRIBUTING.md#L64-L80)\n', - ); - - await File('${tempDir.path}/a/CONTRIBUTING.md').create(recursive: true); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - expect(result.warnings, isEmpty); - }); - - test('passes with relative path containing anchor fragments', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}[Link to section](styleguide.md#miscellaneous-languages)\n', - ); - - await File('${skillDir.path}/styleguide.md').writeAsString('Styleguide content'); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - expect(result.warnings, isEmpty); - }); - - test('passes with leading and trailing whitespace in link', () async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File('${skillDir.path}/SKILL.md').writeAsString( - '${buildFrontmatter(name: 'test-skill')}[Link with whitespace]( styleguide.md )\n', - ); - - await File('${skillDir.path}/styleguide.md').writeAsString('Styleguide content'); - - final validator = Validator( - ruleConfigs: {RelativePathsRule.ruleName: RuleConfig(severity: AnalysisSeverity.warning)}, - ); - final ValidationResult result = await validator.validate(skillDir); - - expect(result.isValid, isTrue); - expect(result.errors, isEmpty); - expect(result.warnings, isEmpty); - }); - }); -} diff --git a/tool/dart_skills_lint/test/resolve_cli_configs_test.dart b/tool/dart_skills_lint/test/resolve_cli_configs_test.dart deleted file mode 100644 index b2ca6254..00000000 --- a/tool/dart_skills_lint/test/resolve_cli_configs_test.dart +++ /dev/null @@ -1,176 +0,0 @@ -// 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_parameters.dart'; -import 'package:dart_skills_lint/src/models/rule_config.dart'; -import 'package:dart_skills_lint/src/models/rule_parameter_type.dart'; -import 'package:dart_skills_lint/src/rule_registry.dart'; -import 'package:dart_skills_lint/src/rules/relative_paths_rule.dart'; -import 'package:dart_skills_lint/src/rules/valid_yaml_metadata_rule.dart'; -import 'package:test/test.dart'; - -void main() { - group('resolveRuleConfigsFromCli - severity overrides', () { - ArgParser createParser() { - final parser = ArgParser(); - for (final CheckType check in RuleRegistry.allChecks) { - parser.addFlag(check.name, defaultsTo: check.defaultSeverity != AnalysisSeverity.disabled); - } - return parser; - } - - test('returns empty map when no CLI overrides are provided', () { - final ArgResults results = createParser().parse([]); - - final Map resolved = resolveRuleConfigsFromCli(results); - - expect( - resolved, - isEmpty, - reason: - 'resolveRuleConfigsFromCli 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 = resolveRuleConfigsFromCli(results); - - expect(resolved[RelativePathsRule.ruleName]?.severity, AnalysisSeverity.error); - }); - - test('CLI flag disabled overrides defaults', () { - final ArgResults results = createParser().parse(['--no-${ValidYamlMetadataRule.ruleName}']); - - final Map resolved = resolveRuleConfigsFromCli(results); - - expect(resolved[ValidYamlMetadataRule.ruleName]?.severity, AnalysisSeverity.disabled); - }); - }); - - group('resolveRuleConfigsFromCli - parameter overrides', () { - const mockCheckName = 'mock-rule'; - late CheckType mockCheck; - - setUpAll(() { - mockCheck = const CheckType( - name: mockCheckName, - defaultSeverity: AnalysisSeverity.disabled, - help: 'Mock rule for testing.', - parameterSchema: { - 'exclude': RuleParameterType.string, - 'max': RuleParameterType.integer, - 'strict': RuleParameterType.boolean, - 'items': RuleParameterType.stringList, - 'pattern': RuleParameterType.regExp, - }, - ); - 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 paramName in check.parameterSchema.keys) { - final RuleParameterType type = check.parameterSchema[paramName]!; - if (type == RuleParameterType.stringList) { - parser.addMultiOption('${check.name}-$paramName'); - } else { - parser.addOption('${check.name}-$paramName'); - } - } - } - return parser; - } - - test('returns empty map when no parameter CLI overrides are provided', () { - final ArgResults results = createParser().parse([]); - final Map configs = resolveRuleConfigsFromCli(results); - expect(configs, isEmpty); - }); - - test('parses and coerces String, RegExp, int, and bool parameters correctly', () { - final ArgResults results = createParser().parse([ - '--$mockCheckName-exclude=.*-workspace', - '--$mockCheckName-max=75', - '--$mockCheckName-strict=true', - '--$mockCheckName-pattern=^[a-z]+\$', - ]); - - final Map configs = resolveRuleConfigsFromCli(results); - expect(configs, isNotEmpty); - expect(configs[mockCheckName], isNotNull); - - final CustomRuleParameters? mockParams = configs[mockCheckName]!.parameters; - expect(mockParams, isNotNull); - expect(mockParams!['exclude'], equals('.*-workspace')); - expect(mockParams['max'], equals(75)); - expect(mockParams['strict'], isTrue); - expect(mockParams['pattern'], equals(r'^[a-z]+$')); - }); - - test('parses and coerces List parameter correctly', () { - final ArgResults results = createParser().parse(['--$mockCheckName-items=a,b,c']); - - final Map configs = resolveRuleConfigsFromCli(results); - expect(configs, isNotEmpty); - expect(configs[mockCheckName], isNotNull); - - final CustomRuleParameters? mockParams = configs[mockCheckName]!.parameters; - expect(mockParams, isNotNull); - expect(mockParams!['items'], equals(['a', 'b', 'c'])); - }); - - test('clears parameter (sets to null) when overridden with empty string', () { - final ArgResults results = createParser().parse(['--$mockCheckName-exclude=']); - - final Map configs = resolveRuleConfigsFromCli(results); - expect(configs, isNotEmpty); - expect(configs[mockCheckName], isNotNull); - - final CustomRuleParameters? mockParams = configs[mockCheckName]!.parameters; - expect(mockParams, isNotNull); - expect(mockParams!.containsKey('exclude'), isTrue); - expect(mockParams['exclude'], isNull); - }); - - test('throws FormatException when int parameter is passed an invalid numeric string', () { - final ArgResults results = createParser().parse(['--$mockCheckName-max=abc']); - expect( - () => resolveRuleConfigsFromCli(results), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('Expected an integer'), - ), - ), - ); - }); - - test('throws FormatException when bool parameter is passed a non-boolean string', () { - final ArgResults results = createParser().parse(['--$mockCheckName-strict=yes']); - expect( - () => resolveRuleConfigsFromCli(results), - throwsA( - isA().having( - (e) => e.message, - 'message', - contains('Expected "true" or "false"'), - ), - ), - ); - }); - }); -} diff --git a/tool/dart_skills_lint/test/rule_config_test.dart b/tool/dart_skills_lint/test/rule_config_test.dart deleted file mode 100644 index 362de9cb..00000000 --- a/tool/dart_skills_lint/test/rule_config_test.dart +++ /dev/null @@ -1,156 +0,0 @@ -// 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/dart_skills_lint.dart'; -import 'package:test/test.dart'; -import 'test_utils.dart'; - -void main() { - group('RuleConfig & RuleConfigPatch Merging', () { - test('RuleConfig initialization defaults', () { - final config = RuleConfig(severity: AnalysisSeverity.error); - expect(config.severity, equals(AnalysisSeverity.error)); - expect(config.parameters.params, isEmpty); - expect(config.severity != AnalysisSeverity.disabled, isTrue); - }); - - test('RuleConfigPatch overrides severity only', () { - final base = RuleConfig( - severity: AnalysisSeverity.warning, - parameters: CustomRuleParameters({'exclude': '.*-workspace', 'max': 50}), - ); - const patch = RuleConfigPatch(severity: AnalysisSeverity.error); - - final RuleConfig merged = patch.applyTo(base); - expect(merged.severity, equals(AnalysisSeverity.error)); - expect(merged.parameters.params, equals({'exclude': '.*-workspace', 'max': 50})); - }); - - test('RuleConfigPatch overrides parameters only', () { - final base = RuleConfig( - severity: AnalysisSeverity.warning, - parameters: CustomRuleParameters({'exclude': '.*-workspace', 'max': 50}), - ); - final patch = RuleConfigPatch(parameters: CustomRuleParameters({'max': 100, 'strict': true})); - - final RuleConfig merged = patch.applyTo(base); - expect(merged.severity, equals(AnalysisSeverity.warning)); - expect( - merged.parameters.params, - equals({'exclude': '.*-workspace', 'max': 100, 'strict': true}), - ); - }); - - test('RuleConfigPatch nullifies keys via null value overrides', () { - final base = RuleConfig( - severity: AnalysisSeverity.warning, - parameters: CustomRuleParameters({'exclude': '.*-workspace', 'max': 50}), - ); - final patch = RuleConfigPatch( - parameters: CustomRuleParameters({'exclude': null, 'max': 100}), - ); - - final RuleConfig merged = patch.applyTo(base); - expect(merged.severity, equals(AnalysisSeverity.warning)); - expect(merged.parameters.params, equals({'max': 100})); - }); - }); - - group('Backwards Compatibility & API Guard Rails', () { - test( - 'validateSkills throws ArgumentError when passing both resolvedRules and resolvedRuleConfigs', - () async { - await withTempDir((tempDir) async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}Body content'); - - expect( - () => validateSkills( - individualSkillPaths: [skillDir.path], - // ignore: deprecated_member_use_from_same_package - resolvedRules: {'valid-yaml-metadata': AnalysisSeverity.warning}, - resolvedRuleConfigs: { - 'valid-yaml-metadata': const RuleConfigPatch(severity: AnalysisSeverity.error), - }, - ), - throwsArgumentError, - ); - }); - }, - ); - - test( - 'validateSkills successfully processes deprecated resolvedRules API backwards compatibly', - () async { - await withTempDir((tempDir) async { - final Directory skillDir = await Directory('${tempDir.path}/test-skill').create(); - await File( - '${skillDir.path}/SKILL.md', - ).writeAsString('${buildFrontmatter(name: 'test-skill')}Body content'); - - // ignore: deprecated_member_use_from_same_package - final bool isValid = await validateSkills( - individualSkillPaths: [skillDir.path], - // ignore: deprecated_member_use_from_same_package - resolvedRules: {'valid-yaml-metadata': AnalysisSeverity.warning}, - ); - // Just confirming it runs without throwing the ArgumentError - expect(isValid, isTrue); - }); - }, - ); - test('Validator throws ArgumentError when passing both ruleOverrides and ruleConfigs', () { - expect( - () => Validator( - // ignore: deprecated_member_use_from_same_package - ruleOverrides: {'foo': AnalysisSeverity.warning}, - ruleConfigs: {'foo': RuleConfig(severity: AnalysisSeverity.error)}, - ), - throwsArgumentError, - ); - }); - - test('Validator maps deprecated ruleOverrides properly', () { - // ignore: deprecated_member_use_from_same_package - final validator = Validator(ruleOverrides: {'foo': AnalysisSeverity.warning}); - // Ensure the mapping happened without error. Validation runs successfully. - expect(validator, isNotNull); - }); - - test('LintTargetConfig deprecated rules getter maps correctly', () { - final config = LintTargetConfig( - path: 'foo', - ruleConfigs: {'foo': const RuleConfigPatch(severity: AnalysisSeverity.warning)}, - ); - // ignore: deprecated_member_use_from_same_package - expect(config.rules['foo'], equals(AnalysisSeverity.warning)); - }); - - test('Configuration deprecated configuredRules getter maps correctly', () { - final config = Configuration( - ruleConfigs: {'bar': const RuleConfigPatch(severity: AnalysisSeverity.error)}, - ); - // ignore: deprecated_member_use_from_same_package - expect(config.configuredRules['bar'], equals(AnalysisSeverity.error)); - }); - - test('deprecated rules and configuredRules getters omit patches without explicit severity', () { - const patchWithoutSeverity = RuleConfigPatch(); - final targetConfig = LintTargetConfig( - path: 'foo', - ruleConfigs: {'path-does-not-exist': patchWithoutSeverity}, - ); - final topConfig = Configuration(ruleConfigs: {'path-does-not-exist': patchWithoutSeverity}); - - // ignore: deprecated_member_use_from_same_package - expect(targetConfig.rules.containsKey('path-does-not-exist'), isFalse); - // ignore: deprecated_member_use_from_same_package - expect(topConfig.configuredRules.containsKey('path-does-not-exist'), isFalse); - }); - }); -} diff --git a/tool/dart_skills_lint/test/rule_naming_test.dart b/tool/dart_skills_lint/test/rule_naming_test.dart deleted file mode 100644 index 2e33dc51..00000000 --- a/tool/dart_skills_lint/test/rule_naming_test.dart +++ /dev/null @@ -1,33 +0,0 @@ -// 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:dart_skills_lint/dart_skills_lint.dart'; -import 'package:dart_skills_lint/src/rules/absolute_paths_rule.dart'; -import 'package:dart_skills_lint/src/rules/description_length_rule.dart'; -import 'package:dart_skills_lint/src/rules/disallowed_field_rule.dart'; -import 'package:dart_skills_lint/src/rules/name_format_rule.dart'; -import 'package:dart_skills_lint/src/rules/relative_paths_rule.dart'; -import 'package:dart_skills_lint/src/rules/valid_yaml_metadata_rule.dart'; -import 'package:test/test.dart'; - -void main() { - group('Rule Naming Conventions', () { - final List rules = [ - AbsolutePathsRule(), - DescriptionLengthRule(), - DisallowedFieldRule(), - NameFormatRule(), - RelativePathsRule(), - ValidYamlMetadataRule(), - ]; - - final kebabCaseRegex = RegExp(r'^[a-z0-9]+(-[a-z0-9]+)*$'); - - for (final rule in rules) { - test('Rule "${rule.runtimeType}" has valid kebab-case name', () { - expect(rule.name, matches(kebabCaseRegex)); - }); - } - }); -} 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 deleted file mode 100644 index edde2b5d..00000000 --- a/tool/dart_skills_lint/test/rules/path_does_not_exist_rule_test.dart +++ /dev/null @@ -1,128 +0,0 @@ -// 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/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 full directory path matches exclude RegExp', () async { - final skillDir = Directory(p.join(tempDir.path, 'nested', 'target-workspace')); - await skillDir.create(recursive: true); // missing SKILL.md - - final rule = PathDoesNotExistRule( - severity: AnalysisSeverity.error, - excludeRegExp: RegExp(r'nested/target-workspace'), - ); - final context = SkillContext(directory: skillDir, rawContent: ''); - - final List errors = await rule.validate(context); - expect(errors, isEmpty); - }); - - test('bypasses validation when full directory path with backslashes is normalized', () async { - final skillDir = Directory(p.join(tempDir.path, 'target-workspace')); - await skillDir.create(); - - // Simulate Windows path structure intentionally using backslashes - final String windowsStylePath = skillDir.path.replaceAll('/', r'\'); - final windowsDir = Directory(windowsStylePath); - - final rule = PathDoesNotExistRule( - severity: AnalysisSeverity.error, - excludeRegExp: RegExp(r'/target-workspace'), - ); - final context = SkillContext(directory: windowsDir, 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, - excludeRegExp: RegExp(r'.*-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); - }); - }); -} diff --git a/tool/dart_skills_lint/test/rules_md_consistency_test.dart b/tool/dart_skills_lint/test/rules_md_consistency_test.dart deleted file mode 100644 index 94b675aa..00000000 --- a/tool/dart_skills_lint/test/rules_md_consistency_test.dart +++ /dev/null @@ -1,222 +0,0 @@ -// 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/fixable_rule.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/skill_rule.dart'; -import 'package:dart_skills_lint/src/rule_registry.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -/// Pins [RULES.md](../RULES.md) to [RuleRegistry] so a rule cannot be -/// added, removed, renamed, or have its default severity / fixability -/// changed without the docs catching up in the same commit. -/// -/// Asserts four invariants between the doc and the registry: -/// 1. Every registered rule has a RULES.md entry (catches missing docs). -/// 2. Every RULES.md entry maps to a registered rule (catches stale -/// docs after a rule is removed or renamed). -/// 3. The documented `Default severity:` value equals the rule's -/// `CheckType.defaultSeverity` (catches silent severity changes -/// that should have been a major version bump per -/// `CONTRIBUTING.md`). -/// 4. The documented `Fixable:` value matches whether the rule's class -/// actually implements `FixableRule`. -/// -/// Each failure prints which rule and which field diverged so the fix -/// is obvious. -void main() { - group('RULES.md consistency', () { - late Map docRules; - late Map registryByName; - - setUpAll(() { - final String rulesPath = p.normalize(p.absolute('RULES.md')); - final String content = File(rulesPath).readAsStringSync(); - docRules = _parseRulesDoc(content); - registryByName = {for (final c in RuleRegistry.allChecks) c.name: c}; - }); - - test('every registered rule has a RULES.md entry', () { - final Set missing = registryByName.keys.toSet()..removeAll(docRules.keys); - expect( - missing, - isEmpty, - reason: - 'RuleRegistry contains rules with no RULES.md entry: $missing. ' - 'Add a `## ` section to RULES.md.', - ); - }); - - test('every RULES.md entry maps to a registered rule', () { - final Set orphans = docRules.keys.toSet()..removeAll(registryByName.keys); - expect( - orphans, - isEmpty, - reason: - 'RULES.md documents rules that are not in RuleRegistry: $orphans. ' - 'Either re-register them or remove the section.', - ); - }); - - test('RULES.md "Default severity:" matches CheckType.defaultSeverity', () { - final List mismatches = _findSeverityMismatches(docRules, registryByName); - expect( - mismatches, - isEmpty, - reason: - 'Default severity drifted between RULES.md and RuleRegistry:\n' - ' ${mismatches.join('\n ')}', - ); - }); - - test('RULES.md "Fixable:" matches whether the rule implements FixableRule', () { - final List mismatches = _findFixableMismatches(docRules, registryByName); - expect( - mismatches, - isEmpty, - reason: - 'Fixable claim drifted between RULES.md and the rule class:\n' - ' ${mismatches.join('\n ')}', - ); - }); - }); -} - -/// Returns descriptive mismatch strings for rules whose documented -/// `Default severity:` in `RULES.md` differs from [CheckType.defaultSeverity]. -List _findSeverityMismatches( - Map docRules, - Map registryByName, -) { - final List mismatches = []; - for (final MapEntry entry in docRules.entries) { - final String name = entry.key; - final CheckType? check = registryByName[name]; - if (check == null) { - continue; - } - if (entry.value.defaultSeverity != check.defaultSeverity) { - mismatches.add( - '$name: RULES.md says ${entry.value.defaultSeverity.name}, ' - 'registry says ${check.defaultSeverity.name}', - ); - } - } - return mismatches; -} - -/// Returns descriptive mismatch strings for rules whose documented -/// `Fixable:` claim in `RULES.md` differs from whether the rule class -/// implements [FixableRule]. -List _findFixableMismatches( - Map docRules, - Map registryByName, -) { - final List mismatches = []; - for (final MapEntry entry in docRules.entries) { - final String name = entry.key; - final CheckType? check = registryByName[name]; - if (check == null) { - continue; - } - final SkillRule? rule = RuleRegistry.createRule(name, check.defaultSeverity); - if (rule == null) { - mismatches.add('$name: RuleRegistry.createRule returned null'); - continue; - } - final actuallyFixable = rule is FixableRule; - if (entry.value.fixable != actuallyFixable) { - mismatches.add( - '$name: RULES.md says fixable=${entry.value.fixable}, ' - 'class is FixableRule=$actuallyFixable', - ); - } - } - return mismatches; -} - -class _DocRule { - _DocRule({required this.defaultSeverity, required this.fixable}); - - final AnalysisSeverity defaultSeverity; - final bool fixable; -} - -/// Parses every `## ` section in RULES.md and extracts the -/// `Default severity:` and `Fixable:` lines. The format the test -/// enforces: -/// -/// ## -/// -/// - **Default severity:** -/// - **Fixable:** -/// ... -/// -/// Sections whose heading does not look like a kebab-case rule name -/// (e.g. the introductory "Rules" `#` heading) are ignored. -Map _parseRulesDoc(String content) { - // Append a sentinel `## ` heading so the last real section terminates - // cleanly. Dart's RegExp doesn't support `\Z`, and a multiline `$` - // matches every newline, so we avoid both by feeding the parser a - // synthetic trailing heading. - final padded = '$content\n## __end__\n'; - final section = RegExp( - r'^## ([a-z][a-z0-9-_]*)\s*\n(.*?)(?=^## )', - multiLine: true, - dotAll: true, - ); - final Map out = {}; - for (final Match m in section.allMatches(padded)) { - final String name = m.group(1)!; - if (name == '__end__') { - continue; - } - final String body = m.group(2)!; - final AnalysisSeverity? severity = _parseSeverity(body); - final bool? fixable = _parseFixable(body); - if (severity == null || fixable == null) { - throw StateError( - 'RULES.md section "$name" is missing a "**Default severity:**" or ' - '"**Fixable:**" line. Found:\n$body', - ); - } - out[name] = _DocRule(defaultSeverity: severity, fixable: fixable); - } - return out; -} - -AnalysisSeverity? _parseSeverity(String body) { - final r = RegExp(r'\*\*Default severity:\*\*\s+(\w+)'); - final RegExpMatch? m = r.firstMatch(body); - if (m == null) { - return null; - } - final String raw = m.group(1)!.toLowerCase(); - for (final AnalysisSeverity s in AnalysisSeverity.values) { - if (s.name == raw) { - return s; - } - } - return null; -} - -bool? _parseFixable(String body) { - final r = RegExp(r'\*\*Fixable:\*\*\s+(\w+)'); - final RegExpMatch? m = r.firstMatch(body); - if (m == null) { - return null; - } - switch (m.group(1)!.toLowerCase()) { - case 'yes': - return true; - case 'no': - return false; - default: - return null; - } -} diff --git a/tool/dart_skills_lint/test/sibling_suggestion_test.dart b/tool/dart_skills_lint/test/sibling_suggestion_test.dart deleted file mode 100644 index 431c49de..00000000 --- a/tool/dart_skills_lint/test/sibling_suggestion_test.dart +++ /dev/null @@ -1,81 +0,0 @@ -// 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/rules/relative_paths_rule.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -/// Unit tests for findSiblingSuggestion. The full path-rule integration is -/// covered in relative_paths_test.dart; these tests exercise the -/// suggestion logic directly so failure messages point at the algorithm -/// rather than at the rule plumbing. -void main() { - group('findSiblingSuggestion', () { - late Directory tempDir; - - setUp(() async { - tempDir = await Directory.systemTemp.createTemp('sibling_suggestion_test.'); - }); - - tearDown(() async { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - }); - - test('returns just the basename when the link had no directory prefix', () async { - // Missing target: DETAILS.md; actual file on disk: DETAILS.md (typo). - await File(p.join(tempDir.path, 'DETAILS.md')).writeAsString('details'); - // Original link was just `DEATILS.md` โ€” no parent dir to preserve. - final String? result = findSiblingSuggestion( - originalLink: 'DEATILS.md', - resolvedPath: p.join(tempDir.path, 'DEATILS.md'), - ); - expect(result, 'DETAILS.md'); - }); - - test('preserves the directory prefix when the link had one', () async { - final Directory refs = await Directory(p.join(tempDir.path, 'references')).create(); - await File(p.join(refs.path, 'DETAILS.md')).writeAsString('details'); - - // Original link was `references/DEATILS.md` โ€” the suggestion should - // include the same prefix so the user can paste it back verbatim. - final String? result = findSiblingSuggestion( - originalLink: 'references/DEATILS.md', - resolvedPath: p.join(refs.path, 'DEATILS.md'), - ); - expect(result, 'references/DETAILS.md'); - }); - - test('returns null when no candidate is close to the missing basename', () async { - await File(p.join(tempDir.path, 'COMPLETELY_UNRELATED.txt')).writeAsString('nope'); - final String? result = findSiblingSuggestion( - originalLink: 'MISSING.md', - resolvedPath: p.join(tempDir.path, 'MISSING.md'), - ); - expect(result, isNull); - }); - - test('returns null when the parent directory does not exist', () { - final String? result = findSiblingSuggestion( - originalLink: 'nonexistent/X.md', - resolvedPath: p.join(tempDir.path, 'nonexistent', 'X.md'), - ); - expect(result, isNull); - }); - - test('ignores directories โ€” only files are candidates', () async { - // Create a *directory* whose name is close to the missing file's - // basename. It should not be offered as a suggestion. - await Directory(p.join(tempDir.path, 'DETAILS.md')).create(); - final String? result = findSiblingSuggestion( - originalLink: 'DEATILS.md', - resolvedPath: p.join(tempDir.path, 'DEATILS.md'), - ); - expect(result, isNull); - }); - }); -} diff --git a/tool/dart_skills_lint/test/skills_evals_test.dart b/tool/dart_skills_lint/test/skills_evals_test.dart deleted file mode 100644 index 5c02ebe1..00000000 --- a/tool/dart_skills_lint/test/skills_evals_test.dart +++ /dev/null @@ -1,152 +0,0 @@ -// 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:convert'; -import 'dart:io'; -import 'dart:isolate'; - -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -void main() { - group('Evals structure consistency', () { - // Ensures all evals.json files dynamically share the exact same JSON schema. - // Keys are not hardcoded to ensure enforcement remains schema-agnostic and flexible. - test('all evals.json files across skills share consistent structure and keys', () async { - final Uri? packageUri = await Isolate.resolvePackageUri( - Uri.parse('package:dart_skills_lint/'), - ); - final String packageRoot = packageUri!.resolve('..').toFilePath(); - - final List evalsFiles = [ - ..._findEvalsFiles(Directory(p.join(packageRoot, 'skills'))), - ..._findEvalsFiles(Directory(p.join(packageRoot, '.agents', 'skills'))), - ..._findEvalsFiles(Directory(p.join(packageRoot, 'evals'))), - ]..sort((a, b) => a.path.compareTo(b.path)); - - expect( - evalsFiles, - isNotEmpty, - reason: 'Should find at least one evals.json file in skills or .agents/skills.', - ); - - _verifyStructuralConsistency(evalsFiles, 'evals'); - }); - - // Note: We intentionally only require an evals.json file for published skills. - // Contributor skills in .agents/skills/ are not currently required to have one. - test('all published skills have an evals.json file', () async { - final Uri? packageUri = await Isolate.resolvePackageUri( - Uri.parse('package:dart_skills_lint/'), - ); - final String packageRoot = packageUri!.resolve('..').toFilePath(); - - final skillsDir = Directory(p.join(packageRoot, 'skills')); - if (!skillsDir.existsSync()) { - return; - } - - final List skillDirs = skillsDir.listSync().whereType().toList(); - - for (final skillDir in skillDirs) { - final evalsFile = File(p.join(skillDir.path, 'evals', 'evals.json')); - expect( - evalsFile.existsSync(), - isTrue, - reason: - 'Published skill "${p.basename(skillDir.path)}" is missing an evals.json file at ${evalsFile.path}', - ); - } - }); - - test('all rubric JSON files in evals/ share consistent structure and keys', () async { - final Uri? packageUri = await Isolate.resolvePackageUri( - Uri.parse('package:dart_skills_lint/'), - ); - final String packageRoot = packageUri!.resolve('..').toFilePath(); - - final rubricsDir = Directory(p.join(packageRoot, 'evals')); - if (!rubricsDir.existsSync()) { - return; - } - - final List rubricFiles = - rubricsDir - .listSync() - .whereType() - .where((File f) => f.path.endsWith('.json') && !f.path.endsWith('_evals.json')) - .toList() - ..sort((a, b) => a.path.compareTo(b.path)); - - if (rubricFiles.isEmpty) { - return; - } - - _verifyStructuralConsistency(rubricFiles, 'evals'); - }); - }); -} - -void _verifyStructuralConsistency(List files, String itemsKey) { - Set? expectedRootKeys; - String? expectedRootKeysFilePath; - Set? expectedItemKeys; - String? expectedItemFilePath; - - for (final file in files) { - final Object? decoded = jsonDecode(file.readAsStringSync()); - final Map decodedMap = switch (decoded) { - final Map map => map, - _ => fail('${file.path} must be a JSON map.'), - }; - final Set rootKeys = decodedMap.keys.toSet(); - if (expectedRootKeys == null) { - expectedRootKeys = rootKeys; - expectedRootKeysFilePath = file.path; - } else { - expect( - rootKeys, - equals(expectedRootKeys), - reason: - '${file.path} root keys do not match consistency pattern. ' - 'Expected keys to match the first processed file ($expectedRootKeysFilePath).', - ); - } - - final Object? itemsRaw = decodedMap[itemsKey]; - final List itemsList = switch (itemsRaw) { - final List list => list, - _ => fail('$itemsKey key in ${file.path} must be a List.'), - }; - for (final Object? item in itemsList) { - final Map itemMap = switch (item) { - final Map map => map, - _ => fail('Item in $itemsKey list in ${file.path} must be a JSON map.'), - }; - final Set itemKeys = itemMap.keys.toSet(); - if (expectedItemKeys == null) { - expectedItemKeys = itemKeys; - expectedItemFilePath = file.path; - } else { - expect( - itemKeys, - equals(expectedItemKeys), - reason: - 'Item in ${file.path} keys do not match consistency pattern. ' - 'Expected item keys to match the first processed file ($expectedItemFilePath).', - ); - } - } - } -} - -List _findEvalsFiles(Directory baseDir) { - if (!baseDir.existsSync()) { - return []; - } - return baseDir.listSync(recursive: true).whereType().where((File f) { - final String name = p.basename(f.path); - return name == 'evals.json' || name.endsWith('_evals.json'); - }).toList(); -} diff --git a/tool/dart_skills_lint/test/skills_ignores_storage_test.dart b/tool/dart_skills_lint/test/skills_ignores_storage_test.dart deleted file mode 100644 index 99c10f3d..00000000 --- a/tool/dart_skills_lint/test/skills_ignores_storage_test.dart +++ /dev/null @@ -1,89 +0,0 @@ -// 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/skills_ignores.dart'; -import 'package:dart_skills_lint/src/skills_ignores_storage.dart'; -import 'package:test/test.dart'; - -void main() { - late Directory tempDir; - late SkillsIgnoresStorage storage; - - setUp(() { - tempDir = Directory.systemTemp.createTempSync('storage_test.'); - storage = SkillsIgnoresStorage(); - }); - - tearDown(() async { - await tempDir.delete(recursive: true); - }); - - group('SkillsIgnoresStorage.load Integration', () { - test('inflates empty JSON into empty skills map', () async { - final file = File('${tempDir.path}/empty.json'); - await file.writeAsString('{}'); - - final SkillsIgnores ignores = await storage.load(file.path); - expect(ignores.skills.isEmpty, isTrue); - }); - - test('inflates single skill with 1 ignore', () async { - final file = File('${tempDir.path}/one_ignore.json'); - await file.writeAsString(''' -{ - "skills": { - "skill-a": [ - {"rule_id": "rule1", "file_name": "file1.md"} - ] - } -} -'''); - - final SkillsIgnores ignores = await storage.load(file.path); - expect(ignores.skills.containsKey('skill-a'), isTrue); - expect(ignores.skills['skill-a']!.length, equals(1)); - }); - - test('inflates single skill with 2 ignores', () async { - final file = File('${tempDir.path}/two_ignores.json'); - await file.writeAsString(''' -{ - "skills": { - "skill-a": [ - {"rule_id": "rule1", "file_name": "file1.md"}, - {"rule_id": "rule2", "file_name": "file1.md"} - ] - } -} -'''); - - final SkillsIgnores ignores = await storage.load(file.path); - expect(ignores.skills.containsKey('skill-a'), isTrue); - expect(ignores.skills['skill-a']!.length, equals(2)); - }); - - test('inflates three skills with varied ignores', () async { - final file = File('${tempDir.path}/three_skills.json'); - await file.writeAsString(''' -{ - "skills": { - "skill-a": [{"rule_id": "rule1", "file_name": "file1.md"}], - "skill-b": [{"rule_id": "rule1", "file_name": "file1.md"}, {"rule_id": "rule2", "file_name": "file1.md"}], - "skill-c": [{"rule_id": "rule1", "file_name": "file1.md"}, {"rule_id": "rule2", "file_name": "file1.md"}, {"rule_id": "rule3", "file_name": "file1.md"}] - } -} -'''); - - final SkillsIgnores ignores = await storage.load(file.path); - expect(ignores.skills.containsKey('skill-a'), isTrue); - expect(ignores.skills.containsKey('skill-b'), isTrue); - expect(ignores.skills.containsKey('skill-c'), isTrue); - expect(ignores.skills['skill-a']!.length, equals(1)); - expect(ignores.skills['skill-b']!.length, equals(2)); - expect(ignores.skills['skill-c']!.length, equals(3)); - }); - }); -} diff --git a/tool/dart_skills_lint/test/test_utils.dart b/tool/dart_skills_lint/test/test_utils.dart deleted file mode 100644 index afa45fd5..00000000 --- a/tool/dart_skills_lint/test/test_utils.dart +++ /dev/null @@ -1,46 +0,0 @@ -// 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:async'; -import 'dart:io'; -import 'package:path/path.dart' as p; - -String buildFrontmatter({ - String name = 'Skill-Name', - String description = 'A test skill', - String? compatibility, -}) { - final sb = StringBuffer(); - sb.writeln('---'); - sb.writeln('name: $name'); - sb.writeln('description: $description'); - if (compatibility != null) { - sb.writeln('compatibility: $compatibility'); - } - sb.writeln('---'); - return sb.toString(); -} - -/// Creates a temporary directory for testing and automatically cleans it up. -Future withTempDir(FutureOr Function(Directory tempDir) action) async { - final Directory tempDir = await Directory.systemTemp.createTemp('api_test.'); - try { - await action(tempDir); - } finally { - if (tempDir.existsSync()) { - await tempDir.delete(recursive: true); - } - } -} - -/// Helper to create a dummy skill with specific SKILL.md contents. -Future createDummySkill( - Directory parentDir, { - required String name, - required String skillContent, -}) async { - final Directory skillDir = await Directory(p.join(parentDir.path, name)).create(recursive: true); - await File(p.join(skillDir.path, 'SKILL.md')).writeAsString(skillContent); - return skillDir; -} diff --git a/tool/dart_skills_lint/test/tracked_skills_publishing_test.dart b/tool/dart_skills_lint/test/tracked_skills_publishing_test.dart deleted file mode 100644 index 9cadae9a..00000000 --- a/tool/dart_skills_lint/test/tracked_skills_publishing_test.dart +++ /dev/null @@ -1,68 +0,0 @@ -// 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/config_parser.dart'; -import 'package:dart_skills_lint/src/models/analysis_severity.dart'; -import 'package:dart_skills_lint/src/models/rule_config.dart'; -import 'package:dart_skills_lint/src/validation_session.dart'; -import 'package:test/test.dart'; - -void main() { - test('all tracked skills have prevent-skills-sh-publishing rule explicitly configured', () async { - // Explanation: - // Any skill in .agents/skills/ that is checked into version control is considered an internal skill. - // It must explicitly have the `prevent-skills-sh-publishing` rule configured in dart_skills_lint.yaml - // to prevent accidental publishing (or explicitly disabled). Un-tracked / local dev skills (which are git-ignored) - // are exempt so they can be published without friction. - - // 1. Get tracked files using git ls-files - final ProcessResult processResult = await Process.run('git', ['ls-files', '.agents/skills']); - expect(processResult.exitCode, 0, reason: 'git ls-files should succeed'); - - final output = processResult.stdout as String; - final Iterable lines = output.split('\n').where((line) => line.trim().isNotEmpty); - - final trackedSkillDirs = {}; - for (final line in lines) { - final List parts = line.split('/'); - // We look for files inside .agents/skills// - // parts[0] is .agents, parts[1] is skills - if (parts.length >= 4 && parts[0] == '.agents' && parts[1] == 'skills') { - trackedSkillDirs.add(parts[2]); - } - } - - expect(trackedSkillDirs, isNotEmpty, reason: 'Should find at least one tracked skill'); - - // 2. Parse configuration - final Configuration config = await ConfigParser.loadConfig(); - final session = ValidationSession( - config: config, - ignoreFileOverride: null, - customRules: [], - printWarnings: false, - fastFail: false, - quiet: true, - generateBaseline: false, - fix: false, - fixApply: false, - ); - - for (final skillDir in trackedSkillDirs) { - final expectedPath = '.agents/skills/$skillDir'; - final Map resolvedConfigs = session.resolveRuleConfigsForPath( - expectedPath, - ); - - expect( - resolvedConfigs['prevent-skills-sh-publishing']?.severity, - AnalysisSeverity.error, - reason: - 'The tracked skill "$skillDir" must have "prevent-skills-sh-publishing" explicitly configured in dart_skills_lint.yaml.', - ); - } - }); -} diff --git a/tool/dart_skills_lint/test/trailing_whitespace_test.dart b/tool/dart_skills_lint/test/trailing_whitespace_test.dart deleted file mode 100644 index 63a8a4e6..00000000 --- a/tool/dart_skills_lint/test/trailing_whitespace_test.dart +++ /dev/null @@ -1,152 +0,0 @@ -// 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/skill_context.dart'; -import 'package:dart_skills_lint/src/models/validation_error.dart'; -import 'package:dart_skills_lint/src/rules/trailing_whitespace_rule.dart'; -import 'package:test/test.dart'; - -import 'test_utils.dart'; - -void main() { - group('Trailing Whitespace Validation', () { - test('passes for line with no trailing whitespace', () async { - final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '${buildFrontmatter(name: 'test-skill')}Line without trailing whitespace\n', - ); - - final List errors = await rule.validate(context); - - expect(errors, isEmpty); - }); - - test('passes for line with exactly 2 trailing spaces (hard line break)', () async { - final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '${buildFrontmatter(name: 'test-skill')}Line with 2 spaces \nNext line\n', - ); - - final List errors = await rule.validate(context); - - expect(errors, isEmpty); - }); - - test('flags line with 1 trailing space as warning', () async { - final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '${buildFrontmatter(name: 'test-skill')}Line with 1 space \n', - ); - - final List errors = await rule.validate(context); - - expect(errors.any((e) => e.message.contains('has 1 trailing space(s)')), isTrue); - }); - - test('flags line with 3 trailing spaces as warning', () async { - final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '${buildFrontmatter(name: 'test-skill')}Line with 3 spaces \n', - ); - - final List errors = await rule.validate(context); - - expect(errors.any((e) => e.message.contains('has 3 trailing space(s)')), isTrue); - }); - - test('flags line with trailing tabs as warning', () async { - final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '${buildFrontmatter(name: 'test-skill')}Line with tab\t\n', - ); - - final List errors = await rule.validate(context); - - expect(errors.any((e) => e.message.contains('trailing whitespace containing tabs')), isTrue); - }); - - test('respects severity setting', () async { - final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.error); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '${buildFrontmatter(name: 'test-skill')}Line with 1 space \n', - ); - - final List errors = await rule.validate(context); - - expect(errors.length, 1); - expect(errors.first.severity, AnalysisSeverity.error); - }); - - test( - r'flags line with 1 trailing space before Windows line ending (\r\n) as warning', - () async { - final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '${buildFrontmatter(name: 'test-skill')}Line with 1 space \r\n', - ); - - final List errors = await rule.validate(context); - - expect(errors.any((e) => e.message.contains('has 1 trailing space(s)')), isTrue); - }, - ); - - test('flags line containing only whitespace (3 spaces) as warning', () async { - final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '${buildFrontmatter(name: 'test-skill')} \n', - ); - - final List errors = await rule.validate(context); - - expect(errors.any((e) => e.message.contains('has 3 trailing space(s)')), isTrue); - }); - - test('passes for line containing only 2 spaces', () async { - final rule = TrailingWhitespaceRule(severity: AnalysisSeverity.warning); - final context = SkillContext( - directory: Directory('dummy'), - rawContent: '${buildFrontmatter(name: 'test-skill')} \n', - ); - - final List errors = await rule.validate(context); - - expect(errors, isEmpty); - }); - - group('Trailing Whitespace Fix', () { - test('removes trailing whitespace', () { - final rule = TrailingWhitespaceRule(); - - expect(rule.fixLine('Line with 1 space '), 'Line with 1 space'); - expect(rule.fixLine('Line with 3 spaces '), 'Line with 3 spaces'); - expect(rule.fixLine('Line with tab\t'), 'Line with tab'); - }); - - test('keeps exactly 2 spaces', () { - final rule = TrailingWhitespaceRule(); - - expect(rule.fixLine('Line with 2 spaces '), 'Line with 2 spaces '); - }); - - test('handles Windows line endings', () { - final rule = TrailingWhitespaceRule(); - - expect(rule.fixLine('Line with 1 space \r'), 'Line with 1 space\r'); - expect(rule.fixLine('Line with 3 spaces \r'), 'Line with 3 spaces\r'); - }); - }); - }); -} diff --git a/tool/dart_skills_lint/test/workflow_consistency_test.dart b/tool/dart_skills_lint/test/workflow_consistency_test.dart deleted file mode 100644 index 0a9ceec8..00000000 --- a/tool/dart_skills_lint/test/workflow_consistency_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -// 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 'package:test/test.dart'; - -void main() { - group('CI workflow consistency', () { - test('CI workflow cognitive complexity fail-threshold does not exceed 20', () { - final File workflowFile = _getWorkflowFile(); - expect(workflowFile.existsSync(), isTrue, reason: 'CI workflow file missing'); - final String content = workflowFile.readAsStringSync(); - final regex = RegExp( - r'dart\s+run\s+cognitive_complexity\s+--fail-threshold\s+(\d+)\s+tool/dart_skills_lint/lib\s+tool/dart_skills_lint/test', - ); - final RegExpMatch? match = regex.firstMatch(content); - expect( - match, - isNotNull, - reason: 'CI workflow must run cognitive_complexity with --fail-threshold ', - ); - final int threshold = int.parse(match!.group(1)!); - expect( - threshold, - lessThanOrEqualTo(20), - reason: 'cognitive complexity fail-threshold in CI ($threshold) should not exceed 20', - ); - }); - }); -} - -File _getWorkflowFile() { - Directory dir = Directory.current; - while (dir.path != '/' && dir.path.isNotEmpty) { - final workflowFile = File( - p.join(dir.path, '.github', 'workflows', 'dart_skills_lint_workflow.yaml'), - ); - if (workflowFile.existsSync()) { - return workflowFile; - } - dir = dir.parent; - } - return File(p.normalize(p.absolute('../../.github/workflows/dart_skills_lint_workflow.yaml'))); -} diff --git a/tool/generator/pubspec.yaml b/tool/generator/pubspec.yaml index f39eb112..37fe2e38 100644 --- a/tool/generator/pubspec.yaml +++ b/tool/generator/pubspec.yaml @@ -25,8 +25,11 @@ dependencies: dev_dependencies: build_verify: ^3.1.0 coverage: ^1.15.0 - dart_skills_lint: - path: ../dart_skills_lint lints: ^6.0.0 + skills_lint: + git: + url: https://github.com/google/skills_lint.dart.git + path: packages/skills_lint + ref: e6e695e1550f81342fe5acd4dbe65040b5aa44c3 test: ^1.25.6 diff --git a/tool/generator/dart_skills_lint.yaml b/tool/generator/skills_lint.yaml similarity index 86% rename from tool/generator/dart_skills_lint.yaml rename to tool/generator/skills_lint.yaml index 32a0b91d..4460222a 100644 --- a/tool/generator/dart_skills_lint.yaml +++ b/tool/generator/skills_lint.yaml @@ -1,4 +1,4 @@ -dart_skills_lint: +skills_lint: rules: check-relative-paths: error check-absolute-paths: error diff --git a/tool/generator/test/custom_skill_rules/last_modified_rule.dart b/tool/generator/test/custom_skill_rules/last_modified_rule.dart index b3f5d066..fb4cfe95 100644 --- a/tool/generator/test/custom_skill_rules/last_modified_rule.dart +++ b/tool/generator/test/custom_skill_rules/last_modified_rule.dart @@ -1,4 +1,4 @@ -import 'package:dart_skills_lint/dart_skills_lint.dart'; +import 'package:skills_lint/skills_lint.dart'; class LastModifiedRule extends SkillRule { static const _metadataKey = 'metadata'; diff --git a/tool/generator/test/lint_skills_test.dart b/tool/generator/test/lint_skills_test.dart index 01bb34bc..aad5297b 100644 --- a/tool/generator/test/lint_skills_test.dart +++ b/tool/generator/test/lint_skills_test.dart @@ -4,9 +4,9 @@ import 'dart:io'; -import 'package:dart_skills_lint/dart_skills_lint.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; +import 'package:skills_lint/skills_lint.dart'; import 'package:test/test.dart'; import 'custom_skill_rules/last_modified_rule.dart';