com.wallstop-studios.dxmessaging 2.1.5 → 2.1.7
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.
- package/.artifacts/SourceGenerators.Tests/obj/Debug/net9.0/WallstopStudios.DxMessaging.SourceGenerators.Tests.AssemblyInfo.cs +1 -1
- package/.cspell.json +4 -1
- package/.github/workflows/actionlint.yml +11 -1
- package/.github/workflows/csharpier-autofix.yml +34 -3
- package/.github/workflows/dotnet-tests.yml +13 -0
- package/.github/workflows/format-on-demand.yml +38 -44
- package/.github/workflows/json-format-check.yml +24 -0
- package/.github/workflows/lint-doc-links.yml +13 -0
- package/.github/workflows/markdown-json.yml +21 -4
- package/.github/workflows/markdown-link-text-check.yml +10 -0
- package/.github/workflows/markdown-link-validity.yml +10 -0
- package/.github/workflows/markdownlint.yml +7 -5
- package/.github/workflows/prettier-autofix.yml +67 -11
- package/.github/workflows/release-drafter.yml +2 -2
- package/.github/workflows/sync-wiki.yml +3 -3
- package/.github/workflows/yaml-format-lint.yml +26 -0
- package/.llm/context.md +113 -3
- package/.llm/skills/documentation/changelog-management.md +38 -0
- package/.llm/skills/documentation/documentation-style-guide.md +18 -0
- package/.llm/skills/documentation/documentation-update-workflow.md +2 -0
- package/.llm/skills/documentation/documentation-updates.md +2 -0
- package/.llm/skills/documentation/markdown-compatibility.md +476 -0
- package/.llm/skills/documentation/mermaid-theming.md +326 -0
- package/.llm/skills/documentation/mkdocs-navigation.md +290 -0
- package/.llm/skills/github-actions/git-renormalize-patterns.md +231 -0
- package/.llm/skills/github-actions/workflow-consistency.md +346 -0
- package/.llm/skills/index.md +53 -27
- package/.llm/skills/scripting/javascript-code-quality.md +417 -0
- package/.llm/skills/scripting/regex-documentation.md +461 -0
- package/.llm/skills/scripting/shell-best-practices.md +55 -4
- package/.llm/skills/scripting/validation-patterns.md +418 -0
- package/.llm/skills/specification.md +4 -1
- package/.llm/skills/testing/test-code-quality.md +243 -0
- package/.llm/skills/testing/test-production-code.md +348 -0
- package/CHANGELOG.md +24 -0
- package/README.md +113 -24
- package/Tests/Runtime/Benchmarks/WallstopStudios.DxMessaging.Tests.Runtime.Benchmarks.asmdef +1 -6
- package/Tests/Runtime/Integrations/Reflex/WallstopStudios.DxMessaging.Tests.Runtime.Reflex.asmdef +1 -1
- package/Tests/Runtime/Integrations/VContainer/WallstopStudios.DxMessaging.Tests.Runtime.VContainer.asmdef +1 -1
- package/Tests/Runtime/Integrations/Zenject/WallstopStudios.DxMessaging.Tests.Runtime.Zenject.asmdef +1 -1
- package/coverage/clover.xml +216 -3
- package/coverage/clover.xml.meta +7 -7
- package/coverage/coverage-final.json +2 -1
- package/coverage/coverage-final.json.meta +7 -7
- package/coverage/lcov-report/base.css.meta +1 -1
- package/coverage/lcov-report/block-navigation.js.meta +1 -1
- package/coverage/lcov-report/favicon.png.meta +1 -1
- package/coverage/lcov-report/index.html +25 -10
- package/coverage/lcov-report/index.html.meta +7 -7
- package/coverage/lcov-report/prettify.css.meta +1 -1
- package/coverage/lcov-report/prettify.js.meta +1 -1
- package/coverage/lcov-report/sort-arrow-sprite.png.meta +1 -1
- package/coverage/lcov-report/sorter.js.meta +1 -1
- package/coverage/lcov-report/transform-docs-to-wiki.js.html +1 -1
- package/coverage/lcov-report/transform-docs-to-wiki.js.html.meta +7 -7
- package/coverage/lcov-report/vendor.meta +1 -1
- package/coverage/lcov-report.meta +8 -8
- package/coverage/lcov.info +365 -0
- package/coverage/lcov.info.meta +7 -7
- package/docs/architecture/design-and-architecture.md +0 -1
- package/docs/concepts/index.md +37 -0
- package/docs/concepts/index.md.meta +7 -0
- package/docs/concepts/interceptors-and-ordering.md +0 -2
- package/docs/concepts/mental-model.md +390 -0
- package/docs/concepts/mental-model.md.meta +7 -0
- package/docs/concepts/message-types.md +0 -1
- package/docs/getting-started/getting-started.md +1 -0
- package/docs/getting-started/index.md +6 -5
- package/docs/getting-started/overview.md +1 -0
- package/docs/getting-started/quick-start.md +2 -1
- package/docs/getting-started/visual-guide.md +4 -10
- package/docs/hooks.py +10 -1
- package/docs/images/DxMessaging-banner.svg +1 -1
- package/docs/index.md +7 -7
- package/docs/javascripts/mermaid-config.js +44 -4
- package/docs/reference/helpers.md +130 -154
- package/docs/reference/quick-reference.md +5 -1
- package/docs/reference/reference.md +124 -130
- package/mkdocs.yml +2 -0
- package/package.json +7 -2
- package/scripts/__tests__/generate-skills-index.test.js +397 -0
- package/scripts/__tests__/generate-skills-index.test.js.meta +7 -0
- package/scripts/__tests__/mermaid-config.test.js +467 -0
- package/scripts/__tests__/mermaid-config.test.js.meta +7 -0
- package/scripts/__tests__/validate-skills-optional-fields.test.js +1474 -0
- package/scripts/__tests__/validate-skills-optional-fields.test.js.meta +7 -0
- package/scripts/__tests__/validate-skills-required-fields.test.js +188 -0
- package/scripts/__tests__/validate-skills-required-fields.test.js.meta +7 -0
- package/scripts/__tests__/validate-skills-tags.test.js +353 -0
- package/scripts/__tests__/validate-skills-tags.test.js.meta +7 -0
- package/scripts/__tests__/validate-workflows.test.js +188 -0
- package/scripts/__tests__/validate-workflows.test.js.meta +7 -0
- package/scripts/generate-skills-index.js +88 -3
- package/scripts/validate-skills.js +230 -30
- package/scripts/validate-workflows.js +272 -0
- package/scripts/validate-workflows.js.meta +7 -0
- package/scripts/wiki/generate-wiki-sidebar.js.meta +1 -8
- package/scripts/wiki/transform-docs-to-wiki.js.meta +1 -1
- package/site/404.html +1 -1
- package/site/advanced/emit-shorthands/index.html +2 -2
- package/site/advanced/message-bus-providers/index.html +2 -2
- package/site/advanced/registration-builders/index.html +2 -2
- package/site/advanced/runtime-configuration/index.html +2 -2
- package/site/advanced/string-messages/index.html +2 -2
- package/site/advanced.meta +1 -1
- package/site/architecture/comparisons/index.html +2 -2
- package/site/architecture/design-and-architecture/index.html +2 -2
- package/site/architecture/performance/index.html +1 -1
- package/site/architecture.meta +1 -1
- package/site/concepts/index.html +1 -0
- package/site/concepts/index.html.meta +7 -0
- package/site/concepts/interceptors-and-ordering/index.html +4 -4
- package/site/concepts/listening-patterns/index.html +2 -2
- package/site/concepts/mental-model/index.html +146 -0
- package/site/concepts/mental-model/index.html.meta +7 -0
- package/site/concepts/mental-model.meta +8 -0
- package/site/concepts/message-types/index.html +2 -2
- package/site/concepts/targeting-and-context/index.html +2 -2
- package/site/concepts.meta +1 -1
- package/site/examples/end-to-end/index.html +2 -2
- package/site/examples/end-to-end-scene-transitions/index.html +2 -2
- package/site/examples.meta +1 -1
- package/site/getting-started/getting-started/index.html +3 -3
- package/site/getting-started/index.html +4 -4
- package/site/getting-started/install/index.html +3 -3
- package/site/getting-started/overview/index.html +2 -2
- package/site/getting-started/quick-start/index.html +2 -2
- package/site/getting-started/visual-guide/index.html +11 -11
- package/site/getting-started.meta +1 -1
- package/site/guides/advanced/index.html +2 -2
- package/site/guides/diagnostics/index.html +2 -2
- package/site/guides/migration-guide/index.html +2 -2
- package/site/guides/patterns/index.html +2 -2
- package/site/guides/testing/index.html +2 -2
- package/site/guides/unity-integration/index.html +2 -2
- package/site/guides.meta +1 -1
- package/site/hooks.py.meta +1 -1
- package/site/images/DxMessaging-banner.svg +119 -0
- package/site/images/DxMessaging-banner.svg.meta +7 -0
- package/site/images.meta +8 -0
- package/site/index.html +2 -2
- package/site/integrations/index.html +2 -2
- package/site/integrations/reflex/index.html +2 -2
- package/site/integrations/vcontainer/index.html +2 -2
- package/site/integrations/zenject/index.html +2 -2
- package/site/integrations.meta +1 -1
- package/site/javascripts/csharp-highlight.js.meta +7 -7
- package/site/javascripts/mermaid-config.js +4 -1
- package/site/javascripts/mermaid-config.js.meta +1 -1
- package/site/javascripts.meta +1 -1
- package/site/reference/compatibility/index.html +1 -1
- package/site/reference/faq/index.html +1 -1
- package/site/reference/glossary/index.html +2 -2
- package/site/reference/helpers/index.html +15 -15
- package/site/reference/quick-reference/index.html +3 -3
- package/site/reference/reference/index.html +37 -37
- package/site/reference/troubleshooting/index.html +1 -1
- package/site/reference.meta +1 -1
- package/site/search/search_index.json +1 -1
- package/site/sitemap.xml +46 -38
- package/site/sitemap.xml.gz +0 -0
- package/site/stylesheets/extra.css.meta +1 -1
- package/site/stylesheets.meta +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for validate-workflows.js logic.
|
|
3
|
+
*
|
|
4
|
+
* These tests validate the core detection logic for problematic
|
|
5
|
+
* git add --renormalize patterns in GitHub Actions workflows.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
isForbiddenRenormalizePattern,
|
|
12
|
+
hasExistenceCheck,
|
|
13
|
+
} = require('../validate-workflows.js');
|
|
14
|
+
|
|
15
|
+
describe("isForbiddenRenormalizePattern", () => {
|
|
16
|
+
describe("should detect FORBIDDEN patterns", () => {
|
|
17
|
+
test("single line with multiple distinct extensions", () => {
|
|
18
|
+
const line =
|
|
19
|
+
"git add --renormalize -- '*.md' '**/*.md' '*.json' '**/*.json'";
|
|
20
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("single line with three extensions", () => {
|
|
24
|
+
const line =
|
|
25
|
+
"git add --renormalize -- '*.md' '*.json' '*.yml'";
|
|
26
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("double-quoted patterns", () => {
|
|
30
|
+
const line =
|
|
31
|
+
'git add --renormalize -- "*.md" "**/*.md" "*.json" "**/*.json"';
|
|
32
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("mixed quote styles", () => {
|
|
36
|
+
const line =
|
|
37
|
+
"git add --renormalize -- '*.cs' \"*.md\"";
|
|
38
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("indented in workflow file", () => {
|
|
42
|
+
const line =
|
|
43
|
+
" git add --renormalize -- '*.md' '*.json' '*.yml'";
|
|
44
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("should ALLOW safe patterns", () => {
|
|
49
|
+
test("single extension with recursive pattern", () => {
|
|
50
|
+
const line = "git add --renormalize -- '*.md' '**/*.md'";
|
|
51
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("variable-based pattern in loop", () => {
|
|
55
|
+
const line = 'git add --renormalize -- "*.$ext" "**/*.$ext"';
|
|
56
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("variable-based pattern with braces", () => {
|
|
60
|
+
const line = 'git add --renormalize -- "*.${ext}" "**/*.${ext}"';
|
|
61
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("single specific file", () => {
|
|
65
|
+
const line =
|
|
66
|
+
"git add --renormalize -- '.config/dotnet-tools.json'";
|
|
67
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("line without git add", () => {
|
|
71
|
+
const line = "echo 'renormalize'";
|
|
72
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("line without renormalize", () => {
|
|
76
|
+
const line = "git add -- '*.md' '*.json'";
|
|
77
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("add_options: --renormalize (YAML key)", () => {
|
|
81
|
+
const line = "add_options: --renormalize";
|
|
82
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("comment describing renormalize", () => {
|
|
86
|
+
const line =
|
|
87
|
+
"# Use --renormalize to ensure line endings";
|
|
88
|
+
expect(isForbiddenRenormalizePattern(line)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("hasExistenceCheck", () => {
|
|
94
|
+
describe("should detect proper guards", () => {
|
|
95
|
+
test("if statement with git ls-files and grep", () => {
|
|
96
|
+
const lines = [
|
|
97
|
+
"for ext in cs md json; do",
|
|
98
|
+
' if git ls-files "*.$ext" "**/*.$ext" | grep -q .; then',
|
|
99
|
+
' git add --renormalize -- "*.$ext" "**/*.$ext"',
|
|
100
|
+
" fi",
|
|
101
|
+
"done",
|
|
102
|
+
];
|
|
103
|
+
expect(hasExistenceCheck(lines, 2)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("for loop with existence check", () => {
|
|
107
|
+
const lines = [
|
|
108
|
+
"for ext in cs md json asmdef yml yaml; do",
|
|
109
|
+
' if git ls-files "*.$ext" "**/*.$ext" | grep -q .; then',
|
|
110
|
+
' git add --renormalize -- "*.$ext" "**/*.$ext"',
|
|
111
|
+
" fi",
|
|
112
|
+
"done",
|
|
113
|
+
];
|
|
114
|
+
expect(hasExistenceCheck(lines, 2)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("should detect missing guards", () => {
|
|
119
|
+
test("direct command without check", () => {
|
|
120
|
+
const lines = [
|
|
121
|
+
"- name: Renormalize line endings",
|
|
122
|
+
" run: |",
|
|
123
|
+
" git add --renormalize -- '*.md' '**/*.md'",
|
|
124
|
+
];
|
|
125
|
+
expect(hasExistenceCheck(lines, 2)).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("guard too far away", () => {
|
|
129
|
+
const lines = [
|
|
130
|
+
'if git ls-files "*.md" | grep -q .; then',
|
|
131
|
+
" echo 'files exist'",
|
|
132
|
+
"fi",
|
|
133
|
+
"",
|
|
134
|
+
"",
|
|
135
|
+
"",
|
|
136
|
+
"",
|
|
137
|
+
"git add --renormalize -- '*.md' '**/*.md'",
|
|
138
|
+
];
|
|
139
|
+
// Index 7, lookback 5 won't reach index 0
|
|
140
|
+
expect(hasExistenceCheck(lines, 7)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("Real workflow patterns", () => {
|
|
146
|
+
describe("should correctly handle actual workflow content", () => {
|
|
147
|
+
test("correct per-extension loop pattern", () => {
|
|
148
|
+
const workflowContent = `
|
|
149
|
+
- name: Renormalize line endings
|
|
150
|
+
shell: bash
|
|
151
|
+
run: |
|
|
152
|
+
# Renormalize each extension separately to avoid "pathspec did not match" failures
|
|
153
|
+
for ext in cs md json asmdef yml yaml; do
|
|
154
|
+
if git ls-files "*.$ext" "**/*.$ext" | grep -q .; then
|
|
155
|
+
git add --renormalize -- "*.$ext" "**/*.$ext"
|
|
156
|
+
fi
|
|
157
|
+
done
|
|
158
|
+
`;
|
|
159
|
+
const lines = workflowContent.split("\n");
|
|
160
|
+
const renormalizeLine = lines.findIndex((l) =>
|
|
161
|
+
l.includes("git add --renormalize")
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Should NOT be forbidden (uses variable)
|
|
165
|
+
expect(isForbiddenRenormalizePattern(lines[renormalizeLine])).toBe(
|
|
166
|
+
false
|
|
167
|
+
);
|
|
168
|
+
// Should have existence check
|
|
169
|
+
expect(hasExistenceCheck(lines, renormalizeLine)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("problematic single-line pattern", () => {
|
|
173
|
+
const workflowContent = `
|
|
174
|
+
- name: Renormalize line endings
|
|
175
|
+
run: git add --renormalize -- '*.md' '*.markdown' '*.json'
|
|
176
|
+
`;
|
|
177
|
+
const lines = workflowContent.split("\n");
|
|
178
|
+
const renormalizeLine = lines.findIndex((l) =>
|
|
179
|
+
l.includes("git add --renormalize")
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Should BE forbidden (multiple extensions on single line)
|
|
183
|
+
expect(isForbiddenRenormalizePattern(lines[renormalizeLine])).toBe(
|
|
184
|
+
true
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -33,6 +33,79 @@ const EXCLUDED_DIRS = ["templates"];
|
|
|
33
33
|
const REPO_ROOT = path.join(__dirname, "..");
|
|
34
34
|
const PRETTIER_VERSION = "3.8.1";
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Brand name capitalization mapping.
|
|
38
|
+
* Maps lowercase words to their properly capitalized forms.
|
|
39
|
+
*/
|
|
40
|
+
const BRAND_NAMES = {
|
|
41
|
+
github: "GitHub",
|
|
42
|
+
javascript: "JavaScript",
|
|
43
|
+
typescript: "TypeScript",
|
|
44
|
+
nodejs: "Node.js",
|
|
45
|
+
csharp: "C#",
|
|
46
|
+
dotnet: ".NET",
|
|
47
|
+
nuget: "NuGet",
|
|
48
|
+
npm: "npm",
|
|
49
|
+
api: "API",
|
|
50
|
+
apis: "APIs",
|
|
51
|
+
cli: "CLI",
|
|
52
|
+
json: "JSON",
|
|
53
|
+
yaml: "YAML",
|
|
54
|
+
xml: "XML",
|
|
55
|
+
html: "HTML",
|
|
56
|
+
css: "CSS",
|
|
57
|
+
sql: "SQL",
|
|
58
|
+
url: "URL",
|
|
59
|
+
urls: "URLs",
|
|
60
|
+
uri: "URI",
|
|
61
|
+
uris: "URIs",
|
|
62
|
+
http: "HTTP",
|
|
63
|
+
https: "HTTPS",
|
|
64
|
+
rest: "REST",
|
|
65
|
+
graphql: "GraphQL",
|
|
66
|
+
oauth: "OAuth",
|
|
67
|
+
jwt: "JWT",
|
|
68
|
+
sdk: "SDK",
|
|
69
|
+
ide: "IDE",
|
|
70
|
+
vscode: "VS Code",
|
|
71
|
+
visualstudio: "Visual Studio",
|
|
72
|
+
macos: "macOS",
|
|
73
|
+
ios: "iOS",
|
|
74
|
+
webgl: "WebGL",
|
|
75
|
+
opengl: "OpenGL",
|
|
76
|
+
directx: "DirectX",
|
|
77
|
+
llm: "LLM",
|
|
78
|
+
ai: "AI",
|
|
79
|
+
ml: "ML",
|
|
80
|
+
ci: "CI",
|
|
81
|
+
cd: "CD",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Apply proper brand name capitalization to a word.
|
|
86
|
+
* Returns the properly capitalized form if it's a known brand,
|
|
87
|
+
* otherwise returns the word with standard title case.
|
|
88
|
+
*/
|
|
89
|
+
function applyBrandCapitalization(word) {
|
|
90
|
+
const lowerWord = word.toLowerCase();
|
|
91
|
+
if (BRAND_NAMES[lowerWord]) {
|
|
92
|
+
return BRAND_NAMES[lowerWord];
|
|
93
|
+
}
|
|
94
|
+
// Standard title case: capitalize first letter
|
|
95
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Convert a category slug (e.g., "github-actions") to a properly
|
|
100
|
+
* capitalized title (e.g., "GitHub Actions").
|
|
101
|
+
*/
|
|
102
|
+
function categoryToTitle(category) {
|
|
103
|
+
return category
|
|
104
|
+
.split("-")
|
|
105
|
+
.map((word) => applyBrandCapitalization(word))
|
|
106
|
+
.join(" ");
|
|
107
|
+
}
|
|
108
|
+
|
|
36
109
|
function normalizeToCrlf(text) {
|
|
37
110
|
let normalized = text.replace(/\r\n/g, "\n");
|
|
38
111
|
normalized = normalized.replace(/\r/g, "\n");
|
|
@@ -369,7 +442,7 @@ function generateIndex(skills) {
|
|
|
369
442
|
`;
|
|
370
443
|
|
|
371
444
|
for (const cat of sortedCategories) {
|
|
372
|
-
const catTitle =
|
|
445
|
+
const catTitle = categoryToTitle(cat);
|
|
373
446
|
content += `- [${catTitle}](#${cat}) (${byCategory[cat].length})\n`;
|
|
374
447
|
}
|
|
375
448
|
|
|
@@ -380,7 +453,7 @@ function generateIndex(skills) {
|
|
|
380
453
|
|
|
381
454
|
// Category sections
|
|
382
455
|
for (const cat of sortedCategories) {
|
|
383
|
-
const catTitle =
|
|
456
|
+
const catTitle = categoryToTitle(cat);
|
|
384
457
|
content += `## ${catTitle}\n\n`;
|
|
385
458
|
|
|
386
459
|
content += `| Skill | Lines | Complexity | Status | Performance | Tags |\n`;
|
|
@@ -509,4 +582,16 @@ function main() {
|
|
|
509
582
|
return 0;
|
|
510
583
|
}
|
|
511
584
|
|
|
512
|
-
|
|
585
|
+
// Export for testing when required as a module
|
|
586
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
587
|
+
module.exports = {
|
|
588
|
+
applyBrandCapitalization,
|
|
589
|
+
categoryToTitle,
|
|
590
|
+
BRAND_NAMES,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Only run main when executed directly (not when required as a module)
|
|
595
|
+
if (require.main === module) {
|
|
596
|
+
process.exit(main());
|
|
597
|
+
}
|
|
@@ -35,6 +35,7 @@ const EXCLUDED_DIRS = ['templates'];
|
|
|
35
35
|
// Files excluded from "short file" informational messages
|
|
36
36
|
const SHORT_FILE_EXCLUDES = ['context.md'];
|
|
37
37
|
|
|
38
|
+
// Required fields that must be present in all skill files
|
|
38
39
|
const REQUIRED_FIELDS = ['title', 'id', 'category', 'version', 'created', 'updated', 'status'];
|
|
39
40
|
|
|
40
41
|
// File size limits (in lines)
|
|
@@ -54,6 +55,7 @@ const VALID_CATEGORIES = [
|
|
|
54
55
|
'code-generation',
|
|
55
56
|
'documentation',
|
|
56
57
|
'scripting',
|
|
58
|
+
'github-actions',
|
|
57
59
|
];
|
|
58
60
|
|
|
59
61
|
const VALID_COMPLEXITY_LEVELS = ['basic', 'intermediate', 'advanced', 'expert'];
|
|
@@ -73,6 +75,127 @@ class ValidationError {
|
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Validates a single required field of a frontmatter object.
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} frontmatter - The parsed frontmatter object
|
|
82
|
+
* @param {string} field - The field name to validate
|
|
83
|
+
* @param {string} relativePath - The relative path for error reporting
|
|
84
|
+
* @returns {ValidationError[]} Array of validation errors
|
|
85
|
+
*/
|
|
86
|
+
function validateRequiredField(frontmatter, field, relativePath) {
|
|
87
|
+
const errors = [];
|
|
88
|
+
|
|
89
|
+
if (frontmatter[field] === undefined || frontmatter[field] === null) {
|
|
90
|
+
errors.push(new ValidationError(relativePath, field, `Required field '${field}' is missing`));
|
|
91
|
+
} else if (frontmatter[field] === '') {
|
|
92
|
+
errors.push(new ValidationError(relativePath, field, `Required field '${field}' is empty`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return errors;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validates the tags field of a frontmatter object.
|
|
100
|
+
*
|
|
101
|
+
* @param {Object} frontmatter - The parsed frontmatter object
|
|
102
|
+
* @param {string} relativePath - The relative path for error reporting
|
|
103
|
+
* @returns {ValidationError[]} Array of validation warnings
|
|
104
|
+
*/
|
|
105
|
+
function validateTags(frontmatter, relativePath) {
|
|
106
|
+
const warnings = [];
|
|
107
|
+
|
|
108
|
+
if (frontmatter.tags === undefined || frontmatter.tags === null) {
|
|
109
|
+
warnings.push(
|
|
110
|
+
new ValidationError(
|
|
111
|
+
relativePath,
|
|
112
|
+
'tags',
|
|
113
|
+
`Missing 'tags' array - will show empty Tags column in skills index`
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
} else if (!Array.isArray(frontmatter.tags)) {
|
|
117
|
+
warnings.push(
|
|
118
|
+
new ValidationError(
|
|
119
|
+
relativePath,
|
|
120
|
+
'tags',
|
|
121
|
+
`'tags' must be an array, got ${typeof frontmatter.tags} - will show empty Tags column in skills index`
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
} else if (frontmatter.tags.length === 0) {
|
|
125
|
+
warnings.push(
|
|
126
|
+
new ValidationError(
|
|
127
|
+
relativePath,
|
|
128
|
+
'tags',
|
|
129
|
+
`Empty 'tags' array - will show empty Tags column in skills index`
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return warnings;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Validates the complexity.level field of a frontmatter object.
|
|
139
|
+
*
|
|
140
|
+
* @param {Object} frontmatter - The parsed frontmatter object
|
|
141
|
+
* @param {string} relativePath - The relative path for error reporting
|
|
142
|
+
* @returns {ValidationError[]} Array of validation warnings
|
|
143
|
+
*/
|
|
144
|
+
function validateComplexityLevel(frontmatter, relativePath) {
|
|
145
|
+
const warnings = [];
|
|
146
|
+
|
|
147
|
+
if (frontmatter.complexity == null || frontmatter.complexity.level == null) {
|
|
148
|
+
warnings.push(
|
|
149
|
+
new ValidationError(
|
|
150
|
+
relativePath,
|
|
151
|
+
'complexity.level',
|
|
152
|
+
`Missing 'complexity.level' - will show '?' in Complexity column of skills index`
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
} else if (frontmatter.complexity.level === '') {
|
|
156
|
+
warnings.push(
|
|
157
|
+
new ValidationError(
|
|
158
|
+
relativePath,
|
|
159
|
+
'complexity.level',
|
|
160
|
+
`Empty 'complexity.level' - will show '?' in Complexity column of skills index`
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return warnings;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Validates the impact.performance.rating field of a frontmatter object.
|
|
170
|
+
*
|
|
171
|
+
* @param {Object} frontmatter - The parsed frontmatter object
|
|
172
|
+
* @param {string} relativePath - The relative path for error reporting
|
|
173
|
+
* @returns {ValidationError[]} Array of validation warnings
|
|
174
|
+
*/
|
|
175
|
+
function validatePerformanceRating(frontmatter, relativePath) {
|
|
176
|
+
const warnings = [];
|
|
177
|
+
|
|
178
|
+
if (frontmatter.impact == null || frontmatter.impact.performance == null || frontmatter.impact.performance.rating == null) {
|
|
179
|
+
warnings.push(
|
|
180
|
+
new ValidationError(
|
|
181
|
+
relativePath,
|
|
182
|
+
'impact.performance.rating',
|
|
183
|
+
`Missing 'impact.performance.rating' - will show '?' in Performance column of skills index`
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
} else if (frontmatter.impact.performance.rating === '') {
|
|
187
|
+
warnings.push(
|
|
188
|
+
new ValidationError(
|
|
189
|
+
relativePath,
|
|
190
|
+
'impact.performance.rating',
|
|
191
|
+
`Empty 'impact.performance.rating' - will show '?' in Performance column of skills index`
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return warnings;
|
|
197
|
+
}
|
|
198
|
+
|
|
76
199
|
/**
|
|
77
200
|
* Parse YAML frontmatter from markdown file content.
|
|
78
201
|
* Uses a stack-based approach to handle arbitrary nesting depth.
|
|
@@ -216,6 +339,33 @@ function findSkillFiles(dir) {
|
|
|
216
339
|
return skills;
|
|
217
340
|
}
|
|
218
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Validates that impact is present and is an object (not a primitive or array).
|
|
344
|
+
*
|
|
345
|
+
* @param {Object} frontmatter - The parsed frontmatter object
|
|
346
|
+
* @returns {boolean} True if impact is a valid object to iterate over
|
|
347
|
+
*/
|
|
348
|
+
function isValidImpactObject(frontmatter) {
|
|
349
|
+
return frontmatter.impact != null && typeof frontmatter.impact === 'object' && !Array.isArray(frontmatter.impact);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Validates all required fields of a frontmatter object.
|
|
354
|
+
*
|
|
355
|
+
* @param {Object} frontmatter - The parsed frontmatter object
|
|
356
|
+
* @param {string} relativePath - The relative path for error reporting
|
|
357
|
+
* @returns {ValidationError[]} Array of validation errors
|
|
358
|
+
*/
|
|
359
|
+
function validateRequiredFields(frontmatter, relativePath) {
|
|
360
|
+
const errors = [];
|
|
361
|
+
|
|
362
|
+
for (const field of REQUIRED_FIELDS) {
|
|
363
|
+
errors.push(...validateRequiredField(frontmatter, field, relativePath));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return errors;
|
|
367
|
+
}
|
|
368
|
+
|
|
219
369
|
/**
|
|
220
370
|
* Validate a single skill file.
|
|
221
371
|
* Returns validation results with errors, warnings, and line count.
|
|
@@ -269,14 +419,10 @@ function validateSkill(skillFile) {
|
|
|
269
419
|
}
|
|
270
420
|
|
|
271
421
|
// Check required fields
|
|
272
|
-
|
|
273
|
-
if (!frontmatter[field]) {
|
|
274
|
-
errors.push(new ValidationError(skillFile.relativePath, field, `Required field '${field}' is missing`));
|
|
275
|
-
}
|
|
276
|
-
}
|
|
422
|
+
errors.push(...validateRequiredFields(frontmatter, skillFile.relativePath));
|
|
277
423
|
|
|
278
424
|
// Validate id matches filename
|
|
279
|
-
if (frontmatter.id && frontmatter.id !== skillFile.expectedId) {
|
|
425
|
+
if (frontmatter.id != null && frontmatter.id !== '' && frontmatter.id !== skillFile.expectedId) {
|
|
280
426
|
errors.push(
|
|
281
427
|
new ValidationError(
|
|
282
428
|
skillFile.relativePath,
|
|
@@ -287,7 +433,7 @@ function validateSkill(skillFile) {
|
|
|
287
433
|
}
|
|
288
434
|
|
|
289
435
|
// Validate category matches folder
|
|
290
|
-
if (frontmatter.category && frontmatter.category !== skillFile.category) {
|
|
436
|
+
if (frontmatter.category != null && frontmatter.category !== '' && frontmatter.category !== skillFile.category) {
|
|
291
437
|
errors.push(
|
|
292
438
|
new ValidationError(
|
|
293
439
|
skillFile.relativePath,
|
|
@@ -298,7 +444,7 @@ function validateSkill(skillFile) {
|
|
|
298
444
|
}
|
|
299
445
|
|
|
300
446
|
// Validate category is known
|
|
301
|
-
if (frontmatter.category && !VALID_CATEGORIES.includes(frontmatter.category)) {
|
|
447
|
+
if (frontmatter.category != null && frontmatter.category !== '' && !VALID_CATEGORIES.includes(frontmatter.category)) {
|
|
302
448
|
warnings.push(
|
|
303
449
|
new ValidationError(
|
|
304
450
|
skillFile.relativePath,
|
|
@@ -309,7 +455,7 @@ function validateSkill(skillFile) {
|
|
|
309
455
|
}
|
|
310
456
|
|
|
311
457
|
// Validate status
|
|
312
|
-
if (frontmatter.status && !VALID_STATUSES.includes(frontmatter.status)) {
|
|
458
|
+
if (frontmatter.status != null && frontmatter.status !== '' && !VALID_STATUSES.includes(frontmatter.status)) {
|
|
313
459
|
errors.push(
|
|
314
460
|
new ValidationError(
|
|
315
461
|
skillFile.relativePath,
|
|
@@ -321,8 +467,9 @@ function validateSkill(skillFile) {
|
|
|
321
467
|
|
|
322
468
|
// Validate complexity level
|
|
323
469
|
if (
|
|
324
|
-
frontmatter.complexity &&
|
|
325
|
-
frontmatter.complexity.level &&
|
|
470
|
+
frontmatter.complexity != null &&
|
|
471
|
+
frontmatter.complexity.level != null &&
|
|
472
|
+
frontmatter.complexity.level !== '' &&
|
|
326
473
|
!VALID_COMPLEXITY_LEVELS.includes(frontmatter.complexity.level)
|
|
327
474
|
) {
|
|
328
475
|
warnings.push(
|
|
@@ -335,7 +482,7 @@ function validateSkill(skillFile) {
|
|
|
335
482
|
}
|
|
336
483
|
|
|
337
484
|
// Validate impact ratings
|
|
338
|
-
if (frontmatter
|
|
485
|
+
if (isValidImpactObject(frontmatter)) {
|
|
339
486
|
// Warn about unknown impact types
|
|
340
487
|
for (const impactType of Object.keys(frontmatter.impact)) {
|
|
341
488
|
if (!VALID_IMPACT_TYPES.includes(impactType)) {
|
|
@@ -351,8 +498,9 @@ function validateSkill(skillFile) {
|
|
|
351
498
|
// Validate ratings for known impact types
|
|
352
499
|
for (const impactType of VALID_IMPACT_TYPES) {
|
|
353
500
|
if (
|
|
354
|
-
frontmatter.impact[impactType] &&
|
|
355
|
-
frontmatter.impact[impactType].rating &&
|
|
501
|
+
frontmatter.impact[impactType] != null &&
|
|
502
|
+
frontmatter.impact[impactType].rating != null &&
|
|
503
|
+
frontmatter.impact[impactType].rating !== '' &&
|
|
356
504
|
!VALID_IMPACT_RATINGS.includes(frontmatter.impact[impactType].rating)
|
|
357
505
|
) {
|
|
358
506
|
warnings.push(
|
|
@@ -367,29 +515,43 @@ function validateSkill(skillFile) {
|
|
|
367
515
|
}
|
|
368
516
|
|
|
369
517
|
// Validate version format (semver-like)
|
|
370
|
-
if (frontmatter.version &&
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
'version',
|
|
375
|
-
`Version '${frontmatter.version}' should be in semver format (e.g., 1.0.0)`
|
|
376
|
-
)
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Validate date format
|
|
381
|
-
for (const dateField of ['created', 'updated']) {
|
|
382
|
-
if (frontmatter[dateField] && !frontmatter[dateField].match(/^\d{4}-\d{2}-\d{2}$/)) {
|
|
518
|
+
if (frontmatter.version != null && frontmatter.version !== '') {
|
|
519
|
+
// Coerce to string to handle numeric or other non-string types
|
|
520
|
+
const versionStr = String(frontmatter.version);
|
|
521
|
+
if (!versionStr.match(/^\d+\.\d+\.\d+$/)) {
|
|
383
522
|
warnings.push(
|
|
384
523
|
new ValidationError(
|
|
385
524
|
skillFile.relativePath,
|
|
386
|
-
|
|
387
|
-
`
|
|
525
|
+
'version',
|
|
526
|
+
`Version '${versionStr}' should be in semver format (e.g., 1.0.0)`
|
|
388
527
|
)
|
|
389
528
|
);
|
|
390
529
|
}
|
|
391
530
|
}
|
|
392
531
|
|
|
532
|
+
// Warn about missing optional fields that affect skills index display
|
|
533
|
+
// These are not required but cause '?' placeholders in the generated index
|
|
534
|
+
warnings.push(...validateComplexityLevel(frontmatter, skillFile.relativePath));
|
|
535
|
+
warnings.push(...validatePerformanceRating(frontmatter, skillFile.relativePath));
|
|
536
|
+
warnings.push(...validateTags(frontmatter, skillFile.relativePath));
|
|
537
|
+
|
|
538
|
+
// Validate date format
|
|
539
|
+
for (const dateField of ['created', 'updated']) {
|
|
540
|
+
if (frontmatter[dateField] != null && frontmatter[dateField] !== '') {
|
|
541
|
+
// Coerce to string to handle numeric or other non-string types
|
|
542
|
+
const dateStr = String(frontmatter[dateField]);
|
|
543
|
+
if (!dateStr.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
|
544
|
+
warnings.push(
|
|
545
|
+
new ValidationError(
|
|
546
|
+
skillFile.relativePath,
|
|
547
|
+
dateField,
|
|
548
|
+
`Date '${dateStr}' should be in ISO format (YYYY-MM-DD)`
|
|
549
|
+
)
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
393
555
|
// Check for required sections in body
|
|
394
556
|
const requiredSections = ['## Overview', '## Solution'];
|
|
395
557
|
for (const section of requiredSections) {
|
|
@@ -554,4 +716,42 @@ function main() {
|
|
|
554
716
|
return 0;
|
|
555
717
|
}
|
|
556
718
|
|
|
557
|
-
|
|
719
|
+
/**
|
|
720
|
+
* @module validate-skills
|
|
721
|
+
* @description Validates skill files in .llm/skills/ for frontmatter correctness, naming conventions,
|
|
722
|
+
* size limits, and cross-references. Used by pre-commit hooks and CI pipelines.
|
|
723
|
+
*
|
|
724
|
+
* @exports {Function} validateSkill - Validates a single skill file, returning errors and warnings
|
|
725
|
+
* @exports {Function} parseFrontmatter - Extracts YAML frontmatter from markdown content
|
|
726
|
+
* @exports {Function} validateRequiredField - Validates a single required frontmatter field
|
|
727
|
+
* @exports {Function} validateRequiredFields - Validates all required frontmatter fields
|
|
728
|
+
* @exports {Function} validateTags - Validates the tags array in frontmatter
|
|
729
|
+
* @exports {Function} validateComplexityLevel - Validates the complexity.level field
|
|
730
|
+
* @exports {Function} validatePerformanceRating - Validates presence of impact.performance.rating field
|
|
731
|
+
* @exports {Function} isValidImpactObject - Checks if impact field is a non-null object (excludes arrays)
|
|
732
|
+
* @exports {Class} ValidationError - Error class with file, field, and message properties
|
|
733
|
+
* @exports {Array<string>} REQUIRED_FIELDS - List of required frontmatter field names
|
|
734
|
+
* @exports {Array<string>} VALID_COMPLEXITY_LEVELS - Valid values: 'basic', 'intermediate', 'advanced', 'expert'
|
|
735
|
+
* @exports {Array<string>} VALID_IMPACT_RATINGS - Valid rating values: 'none', 'low', 'medium', 'high', 'critical'
|
|
736
|
+
*/
|
|
737
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
738
|
+
module.exports = {
|
|
739
|
+
validateSkill,
|
|
740
|
+
parseFrontmatter,
|
|
741
|
+
ValidationError,
|
|
742
|
+
REQUIRED_FIELDS,
|
|
743
|
+
VALID_COMPLEXITY_LEVELS,
|
|
744
|
+
VALID_IMPACT_RATINGS,
|
|
745
|
+
validateRequiredField,
|
|
746
|
+
validateRequiredFields,
|
|
747
|
+
validateTags,
|
|
748
|
+
validateComplexityLevel,
|
|
749
|
+
validatePerformanceRating,
|
|
750
|
+
isValidImpactObject,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Only run main when executed directly (not when required as a module)
|
|
755
|
+
if (require.main === module) {
|
|
756
|
+
process.exit(main());
|
|
757
|
+
}
|