sdd-full 4.6.2 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/bin.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/.agents/skills/flutter-add-integration-test/SKILL.md +165 -0
  4. package/skills/.agents/skills/flutter-add-widget-preview/SKILL.md +147 -0
  5. package/skills/.agents/skills/flutter-add-widget-test/SKILL.md +156 -0
  6. package/skills/.agents/skills/flutter-apply-architecture-best-practices/SKILL.md +164 -0
  7. package/skills/.agents/skills/flutter-build-responsive-layout/SKILL.md +141 -0
  8. package/skills/.agents/skills/flutter-fix-layout-issues/SKILL.md +132 -0
  9. package/skills/.agents/skills/flutter-implement-json-serialization/SKILL.md +155 -0
  10. package/skills/.agents/skills/flutter-setup-declarative-routing/SKILL.md +257 -0
  11. package/skills/.agents/skills/flutter-setup-localization/SKILL.md +212 -0
  12. package/skills/.agents/skills/flutter-use-http-package/SKILL.md +177 -0
  13. package/skills/VERSION.md +176 -62
  14. package/skills/design-planning/ai-coding-rules/SKILL.md +5 -13
  15. package/skills/design-planning/design-to-code/SKILL.md +5 -14
  16. package/skills/design-planning/enterprise-spec/SKILL.md +5 -13
  17. package/skills/design-planning/flutter-av/SKILL.md +5 -16
  18. package/skills/design-planning/flutter-map/SKILL.md +5 -14
  19. package/skills/design-planning/function-sdd/SKILL.md +5 -13
  20. package/skills/design-planning/global-overlay-stack-standard/SKILL.md +73 -0
  21. package/skills/design-planning/ui-motion-interaction-standard/SKILL.md +69 -0
  22. package/skills/design-planning/ui-sdd-specialized/SKILL.md +5 -14
  23. package/skills/development-execution/flutter-errors/SKILL.md +5 -15
  24. package/skills/flutter-skills/.github/dependabot.yaml +15 -0
  25. package/skills/flutter-skills/.github/workflows/dart_skills_lint_workflow.yaml +68 -0
  26. package/skills/flutter-skills/.github/workflows/skills_tool.yaml +51 -0
  27. package/skills/flutter-skills/CODE_OF_CONDUCT.md +3 -0
  28. package/skills/flutter-skills/CONTRIBUTING.md +36 -0
  29. package/skills/flutter-skills/LICENSE +26 -0
  30. package/skills/flutter-skills/README.md +50 -0
  31. package/skills/flutter-skills/pubspec.yaml +9 -0
  32. package/skills/flutter-skills/resources/flutter_skills.yaml +434 -0
  33. package/skills/flutter-skills/skills/flutter-add-integration-test/SKILL.md +163 -0
  34. package/skills/flutter-skills/skills/flutter-add-widget-preview/SKILL.md +145 -0
  35. package/skills/flutter-skills/skills/flutter-add-widget-test/SKILL.md +154 -0
  36. package/skills/flutter-skills/skills/flutter-apply-architecture-best-practices/SKILL.md +162 -0
  37. package/skills/flutter-skills/skills/flutter-build-responsive-layout/SKILL.md +139 -0
  38. package/skills/flutter-skills/skills/flutter-fix-layout-issues/SKILL.md +130 -0
  39. package/skills/flutter-skills/skills/flutter-implement-json-serialization/SKILL.md +153 -0
  40. package/skills/flutter-skills/skills/flutter-setup-declarative-routing/SKILL.md +255 -0
  41. package/skills/flutter-skills/skills/flutter-setup-localization/SKILL.md +210 -0
  42. package/skills/flutter-skills/skills/flutter-use-http-package/SKILL.md +175 -0
  43. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/SKILL.md +196 -0
  44. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-best-practices/SKILL.md +65 -0
  45. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-checks-migration/SKILL.md +158 -0
  46. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-cli-app-best-practices/SKILL.md +168 -0
  47. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-doc-validation/SKILL.md +87 -0
  48. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-long-lines/SKILL.md +101 -0
  49. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-matcher-best-practices/SKILL.md +136 -0
  50. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-modern-features/SKILL.md +266 -0
  51. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-package-maintenance/SKILL.md +92 -0
  52. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/SKILL.md +92 -0
  53. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/lib/src/calculator.dart +7 -0
  54. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/pubspec.yaml +8 -0
  55. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/test/calculator_test.dart +11 -0
  56. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/interpret_coverage.dart +95 -0
  57. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/pubspec.yaml +6 -0
  58. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/test/interpret_coverage_test.dart +93 -0
  59. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-fundamentals/SKILL.md +173 -0
  60. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md +27 -0
  61. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json +3 -0
  62. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/grill-me/SKILL.md +10 -0
  63. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/ignore.json +3 -0
  64. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/test-driven-development/SKILL.md +371 -0
  65. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/test-driven-development/testing-anti-patterns.md +299 -0
  66. package/skills/flutter-skills/tool/dart_skills_lint/AUTHORS +7 -0
  67. package/skills/flutter-skills/tool/dart_skills_lint/CHANGELOG.md +12 -0
  68. package/skills/flutter-skills/tool/dart_skills_lint/CONTRIBUTING.md +51 -0
  69. package/skills/flutter-skills/tool/dart_skills_lint/LICENSE +27 -0
  70. package/skills/flutter-skills/tool/dart_skills_lint/README.md +203 -0
  71. package/skills/flutter-skills/tool/dart_skills_lint/analysis_options.yaml +296 -0
  72. package/skills/flutter-skills/tool/dart_skills_lint/bench/README.md +23 -0
  73. package/skills/flutter-skills/tool/dart_skills_lint/bench/baseline_throughput.dart +230 -0
  74. package/skills/flutter-skills/tool/dart_skills_lint/bin/cli.dart +10 -0
  75. package/skills/flutter-skills/tool/dart_skills_lint/dart_skills_lint.yaml +14 -0
  76. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/PRODUCTION_READYNESS.md +48 -0
  77. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/completion_migration_plan.md +99 -0
  78. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/legacy_patterns_report.md +110 -0
  79. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/pub_vs_skill_report.md +56 -0
  80. package/skills/flutter-skills/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md +79 -0
  81. package/skills/flutter-skills/tool/dart_skills_lint/documentation/knowledge/architecture_overview.md +64 -0
  82. package/skills/flutter-skills/tool/dart_skills_lint/lib/dart_skills_lint.dart +11 -0
  83. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/config_parser.dart +156 -0
  84. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/entry_point.dart +354 -0
  85. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/fixable_rule.dart +20 -0
  86. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/analysis_severity.dart +15 -0
  87. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/check_type.dart +17 -0
  88. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/ignore_entry.dart +34 -0
  89. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/ignore_entry.g.dart +19 -0
  90. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skill_context.dart +27 -0
  91. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skill_rule.dart +27 -0
  92. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skills_ignores.dart +26 -0
  93. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skills_ignores.g.dart +24 -0
  94. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/validation_error.dart +31 -0
  95. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rule_registry.dart +79 -0
  96. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/absolute_paths_rule.dart +74 -0
  97. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/description_length_rule.dart +49 -0
  98. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/disallowed_field_rule.dart +61 -0
  99. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/name_format_rule.dart +167 -0
  100. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/relative_paths_rule.dart +72 -0
  101. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/trailing_whitespace_rule.dart +93 -0
  102. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/valid_yaml_metadata_rule.dart +74 -0
  103. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/skills_ignores_storage.dart +36 -0
  104. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/validation_session.dart +559 -0
  105. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/validator.dart +238 -0
  106. package/skills/flutter-skills/tool/dart_skills_lint/pubspec.yaml +28 -0
  107. package/skills/flutter-skills/tool/dart_skills_lint/skills/README.md +10 -0
  108. package/skills/flutter-skills/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md +195 -0
  109. package/skills/flutter-skills/tool/dart_skills_lint/skills-lock.json +75 -0
  110. package/skills/flutter-skills/tool/dart_skills_lint/test/absolute_paths_test.dart +167 -0
  111. package/skills/flutter-skills/tool/dart_skills_lint/test/cli_integration_test.dart +683 -0
  112. package/skills/flutter-skills/tool/dart_skills_lint/test/config_file_test.dart +292 -0
  113. package/skills/flutter-skills/tool/dart_skills_lint/test/custom_rule_test.dart +122 -0
  114. package/skills/flutter-skills/tool/dart_skills_lint/test/directory_structure_test.dart +163 -0
  115. package/skills/flutter-skills/tool/dart_skills_lint/test/field_constraints_test.dart +178 -0
  116. package/skills/flutter-skills/tool/dart_skills_lint/test/fixer_test.dart +172 -0
  117. package/skills/flutter-skills/tool/dart_skills_lint/test/ignore_models_test.dart +63 -0
  118. package/skills/flutter-skills/tool/dart_skills_lint/test/metadata_validation_test.dart +116 -0
  119. package/skills/flutter-skills/tool/dart_skills_lint/test/relative_path_flag_test.dart +70 -0
  120. package/skills/flutter-skills/tool/dart_skills_lint/test/relative_paths_test.dart +172 -0
  121. package/skills/flutter-skills/tool/dart_skills_lint/test/resolve_rules_test.dart +82 -0
  122. package/skills/flutter-skills/tool/dart_skills_lint/test/rule_naming_test.dart +29 -0
  123. package/skills/flutter-skills/tool/dart_skills_lint/test/skills_ignores_storage_test.dart +89 -0
  124. package/skills/flutter-skills/tool/dart_skills_lint/test/test_utils.dart +19 -0
  125. package/skills/flutter-skills/tool/dart_skills_lint/test/trailing_whitespace_test.dart +152 -0
  126. package/skills/flutter-skills/tool/generator/README.md +150 -0
  127. package/skills/flutter-skills/tool/generator/analysis_options.yaml +143 -0
  128. package/skills/flutter-skills/tool/generator/bin/skills.dart +73 -0
  129. package/skills/flutter-skills/tool/generator/lib/src/commands/base_skill_command.dart +87 -0
  130. package/skills/flutter-skills/tool/generator/lib/src/commands/base_yaml_command.dart +83 -0
  131. package/skills/flutter-skills/tool/generator/lib/src/commands/generate_skill_command.dart +92 -0
  132. package/skills/flutter-skills/tool/generator/lib/src/commands/update_readme_command.dart +150 -0
  133. package/skills/flutter-skills/tool/generator/lib/src/commands/update_skill_command.dart +97 -0
  134. package/skills/flutter-skills/tool/generator/lib/src/commands/validate_skill_command.dart +284 -0
  135. package/skills/flutter-skills/tool/generator/lib/src/models/skill_params.dart +41 -0
  136. package/skills/flutter-skills/tool/generator/lib/src/services/gemini_service.dart +310 -0
  137. package/skills/flutter-skills/tool/generator/lib/src/services/markdown_converter.dart +226 -0
  138. package/skills/flutter-skills/tool/generator/lib/src/services/prompts.dart +72 -0
  139. package/skills/flutter-skills/tool/generator/lib/src/services/resource_fetcher_service.dart +84 -0
  140. package/skills/flutter-skills/tool/generator/lib/src/services/skill_instructions.dart +30 -0
  141. package/skills/flutter-skills/tool/generator/pubspec.yaml +32 -0
  142. package/skills/flutter-skills/tool/generator/test/commands/base_skill_command_test.dart +131 -0
  143. package/skills/flutter-skills/tool/generator/test/commands/validate_skills_input_test.dart +263 -0
  144. package/skills/flutter-skills/tool/generator/test/custom_skill_rules/last_modified_rule.dart +32 -0
  145. package/skills/flutter-skills/tool/generator/test/generate_skills_retry_test.dart +105 -0
  146. package/skills/flutter-skills/tool/generator/test/generate_skills_test.dart +519 -0
  147. package/skills/flutter-skills/tool/generator/test/lint_skills_test.dart +34 -0
  148. package/skills/flutter-skills/tool/generator/test/markdown_converter_test.dart +103 -0
  149. package/skills/flutter-skills/tool/generator/test/markdown_table_test.dart +131 -0
  150. package/skills/flutter-skills/tool/generator/test/models/skill_params_test.dart +37 -0
  151. package/skills/flutter-skills/tool/generator/test/services/gemini_service_test.dart +291 -0
  152. package/skills/flutter-skills/tool/generator/test/services/markdown_converter_test.dart +156 -0
  153. package/skills/flutter-skills/tool/generator/test/services/resource_fetcher_service_test.dart +188 -0
  154. package/skills/flutter-skills/tool/generator/test/update_skills_test.dart +241 -0
  155. package/skills/flutter-skills/tool/generator/test/validate_skills_test.dart +728 -0
  156. package/skills/quality-assurance/bdd-acceptance/SKILL.md +5 -14
  157. package/skills/quality-assurance/flutter-test/SKILL.md +5 -16
  158. package/skills/rules/project_rules.md +538 -127
  159. package/skills/special-tools/env-check/SKILL.md +5 -13
  160. package/skills/special-tools/ios-full-auto-debug/SKILL.md +5 -15
  161. package/skills/writing-skills/SKILL.md +654 -0
  162. package/skills/writing-skills/anthropic-best-practices.md +1149 -0
  163. package/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  164. package/skills/writing-skills/graphviz-conventions.dot +172 -0
  165. package/skills/writing-skills/persuasion-principles.md +187 -0
  166. package/skills/writing-skills/render-graphs.js +168 -0
  167. package/skills/writing-skills/testing-skills-with-subagents.md +384 -0
  168. package/skills/checklist.md +0 -154
  169. package/skills/rules/user_rules.md +0 -263
  170. package/skills//345/256/214/346/225/264/345/274/200/345/217/221/346/265/201/347/250/213/346/211/213/345/206/214.md +0 -454
  171. package/skills//346/212/200/350/203/275/344/275/223/347/263/273/345/256/214/345/226/204/345/273/272/350/256/256.md +0 -308
  172. package/skills//346/212/200/350/203/275/344/275/277/347/224/250/346/214/207/345/215/227.md +0 -309
  173. package/skills//346/212/200/350/203/275/345/206/263/347/255/226/346/240/221.md +0 -338
@@ -0,0 +1,559 @@
1
+ // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2
+ // for details. All rights reserved. Use of this source code is governed by a
3
+ // BSD-style license that can be found in the LICENSE file.
4
+
5
+ import 'dart:convert';
6
+ import 'dart:io';
7
+
8
+ import 'package:logging/logging.dart';
9
+ import 'package:meta/meta.dart';
10
+ import 'package:path/path.dart' as p;
11
+
12
+ import 'config_parser.dart';
13
+ import 'fixable_rule.dart';
14
+ import 'models/analysis_severity.dart';
15
+ import 'models/ignore_entry.dart';
16
+ import 'models/skill_context.dart';
17
+ import 'models/skill_rule.dart';
18
+ import 'models/skills_ignores.dart';
19
+ import 'models/validation_error.dart';
20
+ import 'skills_ignores_storage.dart';
21
+ import 'validator.dart';
22
+
23
+ final _log = Logger('dart_skills_lint');
24
+
25
+ /// Default filename for the per-run ignore baseline file.
26
+ ///
27
+ /// Referenced both by production code (the `--generate-baseline` help text in
28
+ /// the CLI) and by tests, so this is intentionally not `@visibleForTesting`.
29
+ const defaultIgnoreFileName = 'dart_skills_lint_ignore.json';
30
+
31
+ @visibleForTesting
32
+ const skillIsValidMsg = ' Skill is valid.';
33
+ @visibleForTesting
34
+ const skillIsInvalidMsg = ' Skill is invalid:';
35
+ @visibleForTesting
36
+ const warningsMsg = 'Warnings:';
37
+
38
+ @visibleForTesting
39
+ const evaluatingDirMsg = 'Evaluating directory:';
40
+
41
+ @visibleForTesting
42
+ const directoryErrorMsg = 'Directory error:';
43
+
44
+ /// Per-invocation state and orchestration for skill validation.
45
+ ///
46
+ /// One session is constructed per CLI invocation (or embedded call). The
47
+ /// caller invokes [processIndividualSkill] for each `--skill` path and
48
+ /// [processSkillRoot] for each `--skills-directory` path, then optionally
49
+ /// [reportNoSkillsValidated] to emit the "no skills found" diagnostics.
50
+ /// Failure state is exposed via [anyFailed] and [anySkillsValidated].
51
+ class ValidationSession {
52
+ ValidationSession({
53
+ required this.config,
54
+ required this.resolvedRules,
55
+ required this.ignoreFileOverride,
56
+ required this.customRules,
57
+ required this.printWarnings,
58
+ required this.fastFail,
59
+ required this.quiet,
60
+ required this.generateBaseline,
61
+ required this.fix,
62
+ required this.fixApply,
63
+ }) : _normalizedDirectoryConfigs = [
64
+ for (final dc in config.directoryConfigs)
65
+ (normalizedPath: p.normalize(dc.path), config: dc),
66
+ ];
67
+
68
+ final Configuration config;
69
+ final Map<String, AnalysisSeverity> resolvedRules;
70
+ final String? ignoreFileOverride;
71
+ final List<SkillRule> customRules;
72
+ final bool printWarnings;
73
+ final bool fastFail;
74
+ final bool quiet;
75
+ final bool generateBaseline;
76
+ final bool fix;
77
+ final bool fixApply;
78
+
79
+ /// [config.directoryConfigs] with each `path` pre-normalized once.
80
+ ///
81
+ /// `config` is static for the lifetime of a session, so we pay the
82
+ /// `p.normalize` cost up front instead of once per skill in
83
+ /// [_resolveRulesForPath] and [_resolveIgnoreFile].
84
+ final List<({String normalizedPath, DirectoryConfig config})> _normalizedDirectoryConfigs;
85
+
86
+ bool _anyFailed = false;
87
+ bool _anySkillsValidated = false;
88
+
89
+ bool get anyFailed => _anyFailed;
90
+ bool get anySkillsValidated => _anySkillsValidated;
91
+
92
+ /// Validates a single skill directory passed via `--skill` / `-s`.
93
+ ///
94
+ /// Returns `true` if the caller should continue iterating, `false` to
95
+ /// stop. Only a real validation failure under [fastFail] returns `false`;
96
+ /// a missing directory contributes to [anyFailed] but still allows the
97
+ /// caller to continue.
98
+ Future<bool> processIndividualSkill(String skillPath) async {
99
+ final String normalizedSkillPath = p.normalize(_expandPath(skillPath));
100
+ if (!quiet) {
101
+ _log.info('$evaluatingDirMsg $normalizedSkillPath');
102
+ }
103
+ final skillDir = Directory(normalizedSkillPath);
104
+
105
+ if (!skillDir.existsSync()) {
106
+ _log.severe('Specified skill directory does not exist: $normalizedSkillPath');
107
+ _anyFailed = true;
108
+ return true;
109
+ }
110
+
111
+ final Map<String, AnalysisSeverity> localRules = _resolveRulesForPath(normalizedSkillPath);
112
+ final String? localIgnoreFile = _resolveIgnoreFile(normalizedSkillPath);
113
+ final validator = Validator(ruleOverrides: localRules, customRules: customRules);
114
+
115
+ final ({SkillsIgnores ignores, String ignorePath}) loaded = await _loadIgnores(
116
+ localIgnoreFile,
117
+ skillDir.parent,
118
+ );
119
+ final SkillsIgnores ignores = loaded.ignores;
120
+ final String skillName = p.basename(skillDir.path);
121
+ final List<IgnoreEntry> skillIgnores = ignores.skills[skillName] ?? [];
122
+
123
+ _anySkillsValidated = true;
124
+ final ValidationResult finalResult = await _runValidationWorkflow(
125
+ skillDir: skillDir,
126
+ validator: validator,
127
+ ignores: ignores,
128
+ );
129
+
130
+ if (generateBaseline) {
131
+ await _saveBaseline(loaded.ignorePath, ignores);
132
+ } else {
133
+ final String fullPath = p.absolute(skillDir.path);
134
+ for (final ignore in skillIgnores) {
135
+ if (!ignore.used) {
136
+ _log.info(
137
+ "Stale ignore entry found for rule '${ignore.ruleId}' in skill "
138
+ "'$skillName' at '$fullPath'. Consider removing it.",
139
+ );
140
+ }
141
+ }
142
+ }
143
+
144
+ if (!finalResult.isValid) {
145
+ _anyFailed = true;
146
+ if (fastFail) {
147
+ return false;
148
+ }
149
+ }
150
+ return true;
151
+ }
152
+
153
+ /// Validates every skill directory under a root passed via
154
+ /// `--skills-directory` / `-d`.
155
+ ///
156
+ /// Returns `true` if the caller should continue iterating, `false` to
157
+ /// stop. Missing-root and listing-failure errors contribute to [anyFailed]
158
+ /// but allow the caller to continue. After a successful iteration, returns
159
+ /// `false` if [fastFail] is set and any failure has accumulated across the
160
+ /// run so far.
161
+ Future<bool> processSkillRoot(String rootPath) async {
162
+ final String normalizedRootPath = p.normalize(_expandPath(rootPath));
163
+ if (!quiet) {
164
+ _log.info('$evaluatingDirMsg $normalizedRootPath');
165
+ }
166
+ final rootDir = Directory(normalizedRootPath);
167
+
168
+ if (!rootDir.existsSync()) {
169
+ _log.severe('Specified root directory does not exist: $normalizedRootPath');
170
+ _anyFailed = true;
171
+ return true;
172
+ }
173
+
174
+ final Map<String, AnalysisSeverity> localRules = _resolveRulesForPath(normalizedRootPath);
175
+ final String? localIgnoreFile = _resolveIgnoreFile(normalizedRootPath);
176
+ final validator = Validator(ruleOverrides: localRules, customRules: customRules);
177
+
178
+ List<FileSystemEntity> entities;
179
+ try {
180
+ entities = await rootDir.list().toList();
181
+ } catch (_) {
182
+ _log.severe(' $directoryErrorMsg');
183
+ _log.severe(' - Failed to list children of: $normalizedRootPath');
184
+ _anyFailed = true;
185
+ return true;
186
+ }
187
+ entities.sort((a, b) => a.path.compareTo(b.path));
188
+
189
+ final ({SkillsIgnores ignores, String ignorePath}) loaded = await _loadIgnores(
190
+ localIgnoreFile,
191
+ rootDir,
192
+ );
193
+ final SkillsIgnores ignores = loaded.ignores;
194
+
195
+ for (final entity in entities) {
196
+ if (entity is! Directory) {
197
+ continue;
198
+ }
199
+ if (p.basename(entity.path).startsWith('.')) {
200
+ continue;
201
+ }
202
+
203
+ _anySkillsValidated = true;
204
+ final ValidationResult finalResult = await _runValidationWorkflow(
205
+ skillDir: entity,
206
+ validator: validator,
207
+ ignores: ignores,
208
+ );
209
+
210
+ if (!finalResult.isValid) {
211
+ _anyFailed = true;
212
+ if (fastFail) {
213
+ break;
214
+ }
215
+ }
216
+ }
217
+
218
+ if (generateBaseline) {
219
+ await _saveBaseline(loaded.ignorePath, ignores);
220
+ } else {
221
+ for (final MapEntry<String, List<IgnoreEntry>> entry in ignores.skills.entries) {
222
+ final String skillName = entry.key;
223
+ for (final IgnoreEntry ignore in entry.value) {
224
+ if (!ignore.used) {
225
+ final String fullPath = p.absolute(p.join(rootDir.path, skillName));
226
+ _log.info(
227
+ "Stale ignore entry found for rule '${ignore.ruleId}' in skill "
228
+ "'$skillName' at '$fullPath'. Consider removing it.",
229
+ );
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ return !(_anyFailed && fastFail);
236
+ }
237
+
238
+ /// If no skills were validated across the whole run, emit appropriate
239
+ /// diagnostics and mark the session as failed.
240
+ void reportNoSkillsValidated(List<String> rootPaths) {
241
+ if (_anySkillsValidated) {
242
+ return;
243
+ }
244
+
245
+ var foundSingleSkillPassedToD = false;
246
+ for (final rootPath in rootPaths) {
247
+ final String expandedRootPath = _expandPath(rootPath);
248
+ final skillMdFile = File(p.join(expandedRootPath, SkillContext.skillFileName));
249
+ if (skillMdFile.existsSync()) {
250
+ _log.severe(
251
+ 'Directory "$expandedRootPath" appears to be an individual skill. '
252
+ 'Use --skill / -s instead of -d / --skills-directory.',
253
+ );
254
+ foundSingleSkillPassedToD = true;
255
+ }
256
+ }
257
+ if (!foundSingleSkillPassedToD) {
258
+ _log.severe('No skills found to validate in the specified directories.');
259
+ }
260
+ _anyFailed = true;
261
+ }
262
+
263
+ Map<String, AnalysisSeverity> _resolveRulesForPath(String normalizedPath) {
264
+ final localRules = Map<String, AnalysisSeverity>.from(resolvedRules);
265
+ for (final ({String normalizedPath, DirectoryConfig config}) entry
266
+ in _normalizedDirectoryConfigs) {
267
+ if (normalizedPath.startsWith(entry.normalizedPath)) {
268
+ localRules.addAll(entry.config.rules);
269
+ break;
270
+ }
271
+ }
272
+ return localRules;
273
+ }
274
+
275
+ String? _resolveIgnoreFile(String normalizedPath) {
276
+ if (ignoreFileOverride != null) {
277
+ return ignoreFileOverride;
278
+ }
279
+ for (final ({String normalizedPath, DirectoryConfig config}) entry
280
+ in _normalizedDirectoryConfigs) {
281
+ if (normalizedPath.startsWith(entry.normalizedPath)) {
282
+ return entry.config.ignoreFile;
283
+ }
284
+ }
285
+ return null;
286
+ }
287
+
288
+ /// Loads the ignore JSON for a root, returning both the parsed
289
+ /// [SkillsIgnores] and the resolved on-disk path it came from (or where it
290
+ /// would be written).
291
+ ///
292
+ /// Returning the [SkillsIgnores] object (not just `.skills`) lets callers
293
+ /// mutate it in memory across all skills in a root and then save it once,
294
+ /// instead of doing a load+save round-trip per skill.
295
+ Future<({SkillsIgnores ignores, String ignorePath})> _loadIgnores(
296
+ String? localIgnoreFile,
297
+ Directory rootDir,
298
+ ) async {
299
+ final String ignorePath = localIgnoreFile != null
300
+ ? p.normalize(_expandPath(localIgnoreFile))
301
+ : p.join(rootDir.path, defaultIgnoreFileName);
302
+
303
+ final file = File(ignorePath);
304
+
305
+ if (file.existsSync()) {
306
+ final storage = SkillsIgnoresStorage();
307
+ final SkillsIgnores ignores = await storage.load(ignorePath);
308
+ return (ignores: ignores, ignorePath: ignorePath);
309
+ }
310
+
311
+ // If a custom ignore file was specified but not found, create an empty one
312
+ // so the user can start adding ignores to it.
313
+ if (localIgnoreFile != null) {
314
+ _log.warning('File not found generating-baseline');
315
+ try {
316
+ await file.writeAsString(jsonEncode({SkillsIgnores.skillsKey: <String, dynamic>{}}));
317
+ } catch (_) {
318
+ // Ignore write errors, we will just return empty ignores.
319
+ }
320
+ }
321
+
322
+ return (ignores: SkillsIgnores(skills: {}), ignorePath: ignorePath);
323
+ }
324
+
325
+ void _applyIgnores(ValidationResult result, List<IgnoreEntry> ignores) {
326
+ // Pre-normalize ignore filenames once so the inner loop below is a
327
+ // straight string comparison instead of repeated path normalization.
328
+ final List<({IgnoreEntry entry, String normalizedFileName})> preNormalizedIgnores = [
329
+ for (final ignore in ignores)
330
+ (entry: ignore, normalizedFileName: p.normalize(ignore.fileName)),
331
+ ];
332
+
333
+ for (final ValidationError error in result.validationErrors) {
334
+ if (error.isIgnored) {
335
+ continue;
336
+ }
337
+ final String normalizedErrorFile = p.normalize(error.file);
338
+ for (final pair in preNormalizedIgnores) {
339
+ final IgnoreEntry ignore = pair.entry;
340
+ if (ignore.ruleId == error.ruleId && pair.normalizedFileName == normalizedErrorFile) {
341
+ error.isIgnored = true;
342
+ ignore.used = true;
343
+ break;
344
+ }
345
+ }
346
+ }
347
+ }
348
+
349
+ /// Validates [skillDir], applies fixes if requested, and (when
350
+ /// [generateBaseline] is set) updates [ignores] in memory with any new
351
+ /// baseline entries for this skill. The caller is responsible for
352
+ /// persisting [ignores] to disk once after all skills are processed —
353
+ /// see [_saveBaseline].
354
+ Future<ValidationResult> _runValidationWorkflow({
355
+ required Directory skillDir,
356
+ required Validator validator,
357
+ required SkillsIgnores ignores,
358
+ }) async {
359
+ final String skillName = p.basename(skillDir.path);
360
+ final List<IgnoreEntry> skillIgnores = ignores.skills[skillName] ?? [];
361
+
362
+ final ValidationResult result = await _validateSingleSkill(
363
+ skillDir: skillDir,
364
+ validator: validator,
365
+ skillIgnores: skillIgnores,
366
+ );
367
+
368
+ final ValidationResult finalResult = await _applyFixesIfNeeded(
369
+ skillDir: skillDir,
370
+ result: result,
371
+ validator: validator,
372
+ skillIgnores: skillIgnores,
373
+ );
374
+
375
+ if (generateBaseline) {
376
+ _updateBaselineForSkill(ignores, finalResult, skillName);
377
+ }
378
+
379
+ return finalResult;
380
+ }
381
+
382
+ Future<ValidationResult> _validateSingleSkill({
383
+ required Directory skillDir,
384
+ required Validator validator,
385
+ required List<IgnoreEntry> skillIgnores,
386
+ }) async {
387
+ final String skillName = p.basename(skillDir.path);
388
+ if (!quiet) {
389
+ _log.info('--- Validating skill: $skillName ---');
390
+ }
391
+ final ValidationResult result = await validator.validate(skillDir);
392
+ _applyIgnores(result, skillIgnores);
393
+ _printValidationResult(result);
394
+ return result;
395
+ }
396
+
397
+ Future<ValidationResult> _applyFixesIfNeeded({
398
+ required Directory skillDir,
399
+ required ValidationResult result,
400
+ required Validator validator,
401
+ required List<IgnoreEntry> skillIgnores,
402
+ }) async {
403
+ if (!fix && !fixApply) {
404
+ return result;
405
+ }
406
+
407
+ final SkillContext? context = result.context;
408
+ if (context == null) {
409
+ return result;
410
+ }
411
+
412
+ final String skillName = p.basename(skillDir.path);
413
+ final skillMdFile = File(p.join(skillDir.path, SkillContext.skillFileName));
414
+ if (!skillMdFile.existsSync()) {
415
+ return result;
416
+ }
417
+
418
+ String currentContent = context.rawContent;
419
+ final originalContent = currentContent;
420
+ var modified = false;
421
+
422
+ for (final SkillRule rule in validator.rules) {
423
+ if (rule is FixableRule) {
424
+ final bool hasErrors = result.validationErrors.any(
425
+ (e) => e.ruleId == rule.name && !e.isIgnored,
426
+ );
427
+ if (hasErrors) {
428
+ try {
429
+ final String newContent = await rule.fix(
430
+ SkillContext.skillFileName,
431
+ currentContent,
432
+ context.directory,
433
+ );
434
+ if (newContent != currentContent) {
435
+ currentContent = newContent;
436
+ modified = true;
437
+ }
438
+ } catch (e) {
439
+ _log.severe(" Failed to apply fix for rule '${rule.name}': $e");
440
+ }
441
+ }
442
+ }
443
+ }
444
+
445
+ if (modified) {
446
+ if (fixApply) {
447
+ await skillMdFile.writeAsString(currentContent);
448
+ if (!quiet) {
449
+ _log.info(' Applied fixes for $skillName');
450
+ }
451
+ final ValidationResult newResult = await validator.validate(skillDir);
452
+ _applyIgnores(newResult, skillIgnores);
453
+ return newResult;
454
+ } else if (fix) {
455
+ if (!quiet) {
456
+ _log.info(' [Dry Run] Proposed changes for $skillName (SKILL.md):');
457
+ _printDiff(originalContent, currentContent);
458
+ }
459
+ }
460
+ }
461
+
462
+ return result;
463
+ }
464
+
465
+ /// Prints a simple line-by-line diff between [original] and [modified].
466
+ ///
467
+ /// **Limitation**: This naive diff algorithm does not handle line additions
468
+ /// or removals well, as it compares lines at the same index. It is
469
+ /// sufficient for current fixers that only modify existing lines, but
470
+ /// should be replaced with a more robust diffing solution (e.g.,
471
+ /// `package:diff`) if future fixers add or remove lines.
472
+ void _printDiff(String original, String modified) {
473
+ final List<String> origLines = original.split('\n');
474
+ final List<String> modLines = modified.split('\n');
475
+ final int maxLines = origLines.length > modLines.length ? origLines.length : modLines.length;
476
+ for (var i = 0; i < maxLines; i++) {
477
+ final String orig = i < origLines.length ? origLines[i] : '';
478
+ final String mod = i < modLines.length ? modLines[i] : '';
479
+ if (orig != mod) {
480
+ if (orig.isNotEmpty) {
481
+ _log.info('- Line ${i + 1}: $orig');
482
+ }
483
+ if (mod.isNotEmpty) {
484
+ _log.info('+ Line ${i + 1}: $mod');
485
+ }
486
+ }
487
+ }
488
+ }
489
+
490
+ /// Mutates [ignores] in place to add baseline entries for any non-ignored
491
+ /// errors in [result] under the [skillName] key. Pure in-memory operation
492
+ /// — pair with [_saveBaseline] to persist changes.
493
+ void _updateBaselineForSkill(SkillsIgnores ignores, ValidationResult result, String skillName) {
494
+ final List<IgnoreEntry> currentSkillIgnores = ignores.skills[skillName] ?? [];
495
+ final currentSkillSeen = <String>{};
496
+ for (final ignore in currentSkillIgnores) {
497
+ currentSkillSeen.add('${ignore.ruleId}:${ignore.fileName}');
498
+ }
499
+
500
+ for (final ValidationError error in result.validationErrors) {
501
+ if (!error.isIgnored) {
502
+ final key = '${error.ruleId}:${error.file}';
503
+ if (currentSkillSeen.contains(key)) {
504
+ continue;
505
+ }
506
+ currentSkillSeen.add(key);
507
+
508
+ currentSkillIgnores.add(IgnoreEntry(ruleId: error.ruleId, fileName: error.file));
509
+ }
510
+ }
511
+
512
+ if (currentSkillIgnores.isNotEmpty) {
513
+ ignores.skills[skillName] = currentSkillIgnores;
514
+ } else {
515
+ ignores.skills.remove(skillName);
516
+ }
517
+ }
518
+
519
+ /// Writes [ignores] to [ignorePath]. Write failures are logged at warning
520
+ /// level and otherwise swallowed so a single I/O error does not abort the
521
+ /// rest of the run.
522
+ Future<void> _saveBaseline(String ignorePath, SkillsIgnores ignores) async {
523
+ try {
524
+ await SkillsIgnoresStorage().save(ignorePath, ignores);
525
+ } catch (e) {
526
+ _log.warning('Failed to generate baseline file at $ignorePath: $e');
527
+ }
528
+ }
529
+
530
+ void _printValidationResult(ValidationResult result) {
531
+ if (result.isValid) {
532
+ if (!quiet) {
533
+ _log.info(' $skillIsValidMsg');
534
+ }
535
+ } else {
536
+ _log.severe(' $skillIsInvalidMsg');
537
+ for (final String error in result.errors) {
538
+ _log.severe(' - $error');
539
+ }
540
+ }
541
+
542
+ if (printWarnings && result.warnings.isNotEmpty) {
543
+ _log.warning(' $warningsMsg');
544
+ for (final String warning in result.warnings) {
545
+ _log.warning(' - $warning');
546
+ }
547
+ }
548
+ }
549
+
550
+ String _expandPath(String path) {
551
+ if (path.startsWith('~/')) {
552
+ final String? homeDir = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
553
+ if (homeDir != null) {
554
+ return p.join(homeDir, path.substring(2));
555
+ }
556
+ }
557
+ return path;
558
+ }
559
+ }