@pigcloud/skills 1.0.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 (161) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +201 -0
  3. package/README.en.md +84 -0
  4. package/README.md +79 -0
  5. package/bin/cli.js +663 -0
  6. package/bin/postinstall.js +60 -0
  7. package/bin/rules-loader.js +484 -0
  8. package/bin/runtime-bootstrap.js +121 -0
  9. package/index.js +7 -0
  10. package/install.cmd +5 -0
  11. package/install.ps1 +74 -0
  12. package/install.sh +168 -0
  13. package/package.json +70 -0
  14. package/pig-cloud-skills-commands/.codex-plugin/plugin.json +35 -0
  15. package/pig-cloud-skills-commands/README.md +24 -0
  16. package/pig-cloud-skills-commands/commands/analyze.md +21 -0
  17. package/pig-cloud-skills-commands/commands/build.md +21 -0
  18. package/pig-cloud-skills-commands/commands/design.md +21 -0
  19. package/pig-cloud-skills-commands/commands/distill.md +21 -0
  20. package/pig-cloud-skills-commands/commands/doc.md +21 -0
  21. package/pig-cloud-skills-commands/commands/infra.md +21 -0
  22. package/pig-cloud-skills-commands/commands/init.md +20 -0
  23. package/pig-cloud-skills-commands/commands/kb.md +20 -0
  24. package/pig-cloud-skills-commands/commands/perf.md +20 -0
  25. package/pig-cloud-skills-commands/commands/prd.md +21 -0
  26. package/pig-cloud-skills-commands/commands/review.md +21 -0
  27. package/pig-cloud-skills-commands/commands/security.md +21 -0
  28. package/pig-cloud-skills-commands/commands/test.md +21 -0
  29. package/pig-cloud-skills-commands/commands/workflow.md +20 -0
  30. package/rules/bundles.json +358 -0
  31. package/rules/coding/analysis.md +27 -0
  32. package/rules/coding/backend/cache-invalidation.md +30 -0
  33. package/rules/coding/backend/cache-keying.md +30 -0
  34. package/rules/coding/backend/cache.md +37 -0
  35. package/rules/coding/backend/database.md +32 -0
  36. package/rules/coding/backend/feign.md +30 -0
  37. package/rules/coding/backend/index.md +42 -0
  38. package/rules/coding/backend/query.md +32 -0
  39. package/rules/coding/backend/remote.md +33 -0
  40. package/rules/coding/backend/transaction-boundary.md +30 -0
  41. package/rules/coding/backend/transaction-rollback.md +30 -0
  42. package/rules/coding/backend/transaction.md +38 -0
  43. package/rules/coding/boundary.md +25 -0
  44. package/rules/coding/implementation.md +30 -0
  45. package/rules/coding/index.md +38 -0
  46. package/rules/coding/scaffold.md +28 -0
  47. package/rules/coding/testing.md +29 -0
  48. package/rules/coding/validation.md +29 -0
  49. package/rules/core/code-quality.md +30 -0
  50. package/rules/core/evidence.md +26 -0
  51. package/rules/core/index.md +41 -0
  52. package/rules/core/interface.md +26 -0
  53. package/rules/core/iteration.md +26 -0
  54. package/rules/core/layer-boundary.md +25 -0
  55. package/rules/core/logging.md +26 -0
  56. package/rules/core/security.md +26 -0
  57. package/rules/core/task-boundary.md +27 -0
  58. package/rules/docs/api.md +34 -0
  59. package/rules/docs/capture-summary.md +29 -0
  60. package/rules/docs/capture.md +34 -0
  61. package/rules/docs/contract.md +30 -0
  62. package/rules/docs/decision-log.md +32 -0
  63. package/rules/docs/examples.md +28 -0
  64. package/rules/docs/index.md +49 -0
  65. package/rules/docs/reference.md +32 -0
  66. package/rules/index.md +46 -0
  67. package/rules/overlays/index.md +28 -0
  68. package/rules/overlays/pig-cloud/controller.md +33 -0
  69. package/rules/overlays/pig-cloud/dto-vo.md +33 -0
  70. package/rules/overlays/pig-cloud/entity.md +32 -0
  71. package/rules/overlays/pig-cloud/exception.md +32 -0
  72. package/rules/overlays/pig-cloud/layering.md +31 -0
  73. package/rules/overlays/pig-cloud/mapper.md +32 -0
  74. package/rules/overlays/pig-cloud/query-style.md +32 -0
  75. package/rules/overlays/pig-cloud/rest-response.md +33 -0
  76. package/rules/overlays/pig-cloud/service.md +33 -0
  77. package/rules/overlays/pig-cloud/transactions.md +32 -0
  78. package/rules/overlays/pig-cloud/validation.md +33 -0
  79. package/rules/overlays/pig-cloud.md +45 -0
  80. package/rules/product/acceptance.md +25 -0
  81. package/rules/product/briefing.md +27 -0
  82. package/rules/product/index.md +36 -0
  83. package/rules/product/intake.md +27 -0
  84. package/rules/product/modeling.md +25 -0
  85. package/rules/product/project-context.md +29 -0
  86. package/rules/review/code.md +35 -0
  87. package/rules/review/evidence.md +31 -0
  88. package/rules/review/index.md +50 -0
  89. package/rules/review/java.md +42 -0
  90. package/rules/review/performance.md +38 -0
  91. package/rules/review/rubric.md +28 -0
  92. package/rules/review/security.md +38 -0
  93. package/rules/review/ts.md +33 -0
  94. package/rules/review/vue.md +33 -0
  95. package/rules/skill-profile-map.json +58 -0
  96. package/rules/skill-profile-map.md +28 -0
  97. package/rules/workflow/handoff.md +25 -0
  98. package/rules/workflow/index.md +37 -0
  99. package/rules/workflow/refinement.md +29 -0
  100. package/rules/workflow/router.md +25 -0
  101. package/rules/workflow/selection.md +25 -0
  102. package/rules/workflow/stop.md +25 -0
  103. package/scripts/ci-validator.sh +114 -0
  104. package/scripts/run-golden-replays.js +312 -0
  105. package/scripts/validate-rules.js +125 -0
  106. package/scripts/validate-skill-replay-signals.js +75 -0
  107. package/scripts/validate-skill-shapes.js +141 -0
  108. package/scripts/validate-skill-stop-rules.js +139 -0
  109. package/scripts/validate-skills.cmd +3 -0
  110. package/scripts/validate-skills.ps1 +42 -0
  111. package/scripts/validate-skills.sh +36 -0
  112. package/skills/api-docs/SKILL.md +76 -0
  113. package/skills/code-review/SKILL.md +135 -0
  114. package/skills/code-review/references/findings-template.md +51 -0
  115. package/skills/code-review/references/performance-checklist.md +213 -0
  116. package/skills/code-review/references/rubric.md +232 -0
  117. package/skills/code-review/references/rules.md +32 -0
  118. package/skills/code-review/references/security-checklist.md +178 -0
  119. package/skills/code-review/references/stack-notes.md +25 -0
  120. package/skills/code-review/references/template-review.md +214 -0
  121. package/skills/code-review/scripts/lint-code-review.mjs +431 -0
  122. package/skills/domain-modeling/SKILL.md +80 -0
  123. package/skills/domain-modeling/references/README.md +134 -0
  124. package/skills/domain-modeling/references/distillation-checklist.md +152 -0
  125. package/skills/domain-modeling/references/test-cases-template.md +128 -0
  126. package/skills/environment-deploy/SKILL.md +81 -0
  127. package/skills/feature-build/SKILL.md +122 -0
  128. package/skills/feature-build/references/coding-checklist.md +97 -0
  129. package/skills/feature-build/references/comment-specification.md +102 -0
  130. package/skills/knowledge-capture/SKILL.md +84 -0
  131. package/skills/performance-check/SKILL.md +117 -0
  132. package/skills/product-intake/SKILL.md +98 -0
  133. package/skills/project-bootstrap/SKILL.md +80 -0
  134. package/skills/references/agent-personas.md +34 -0
  135. package/skills/references/anti-rationalization.md +144 -0
  136. package/skills/references/engineering-delivery-method.md +63 -0
  137. package/skills/references/engineering-delivery-template.md +80 -0
  138. package/skills/references/flow-test-cases.md +62 -0
  139. package/skills/references/full-chain-replay-scenarios.md +79 -0
  140. package/skills/references/golden-prompt-suite.js +385 -0
  141. package/skills/references/golden-prompt-suite.md +33 -0
  142. package/skills/references/hooks.md +67 -0
  143. package/skills/references/negative-replay-scenarios.md +49 -0
  144. package/skills/references/project-requirement-alignment.md +41 -0
  145. package/skills/references/prompt-replay-checklist.md +128 -0
  146. package/skills/references/requirements-separation-map.md +71 -0
  147. package/skills/references/rule-loading-map.md +108 -0
  148. package/skills/references/skill-authoring-standard.md +73 -0
  149. package/skills/references/skill-boundary-template.md +38 -0
  150. package/skills/references/skill-enhanced-template.md +53 -0
  151. package/skills/references/skill-reference-matrix.md +53 -0
  152. package/skills/references/slash-commands.md +34 -0
  153. package/skills/security-review/SKILL.md +117 -0
  154. package/skills/spec-refinement/SKILL.md +143 -0
  155. package/skills/spec-refinement/references/ears-syntax.md +127 -0
  156. package/skills/spec-refinement/references/requirement-checklist.md +139 -0
  157. package/skills/spec-refinement/references/spec-workbook.md +75 -0
  158. package/skills/technical-design/SKILL.md +105 -0
  159. package/skills/technical-design/references/solid-checklist.md +199 -0
  160. package/skills/test-design/SKILL.md +91 -0
  161. package/skills/workflow-router/SKILL.md +86 -0
@@ -0,0 +1,25 @@
1
+ # Workflow Selection Rules
2
+
3
+ ## 默认做法
4
+
5
+ - Decide the next skill based on the current artifact, not the final desired outcome.
6
+ - Prefer the earliest stage that can safely absorb the input.
7
+ - If the stage is unclear, choose routing over execution.
8
+
9
+ ## 禁区
10
+
11
+ - Do not skip to implementation when the input is still vague.
12
+ - Do not choose a deeper stage just because it is more convenient.
13
+
14
+ ## 检查
15
+
16
+ - Is the next stage the smallest safe step?
17
+ - Is the reason for selection visible?
18
+
19
+ ## 回放信号
20
+
21
+ - Trigger when the request is asking "what should happen next" or "where should this go".
22
+
23
+ ## References
24
+
25
+ - `workflow/index.md`
@@ -0,0 +1,25 @@
1
+ # Workflow Stop Rules
2
+
3
+ ## 默认做法
4
+
5
+ - Stop as soon as the current stage has produced its expected artifact.
6
+ - Preserve a clean handoff note for the next owner.
7
+ - Do not carry next-stage work into the current stage.
8
+
9
+ ## 禁区
10
+
11
+ - Do not keep refining past the stage boundary.
12
+ - Do not smuggle implementation work into a routing or handoff task.
13
+
14
+ ## 检查
15
+
16
+ - Is the artifact complete for this stage?
17
+ - Is the next stage ready to take over?
18
+
19
+ ## 回放信号
20
+
21
+ - Trigger when a stage boundary, handoff, or stop condition needs to be enforced.
22
+
23
+ ## References
24
+
25
+ - `workflow/index.md`
@@ -0,0 +1,114 @@
1
+ #!/bin/bash
2
+ # ci-validator.sh - Skills CI 验证脚本
3
+ # 用于验证技能文件的完整性和规范性
4
+
5
+ set -e
6
+
7
+ echo "=== Skills CI Validator ==="
8
+
9
+ # 1. 检查技能文件完整性
10
+ echo "[1/5] Checking skill files..."
11
+ SKILLS_DIR="${SKILLS_DIR:-.trae/skills}"
12
+ ERROR_COUNT=0
13
+
14
+ for skill in $(find "$SKILLS_DIR" -name "SKILL.md" -type f); do
15
+ skill_name=$(basename $(dirname "$skill"))
16
+
17
+ # 检查 frontmatter
18
+ if ! grep -q "^name:" "$skill"; then
19
+ echo "ERROR: Missing 'name' in $skill"
20
+ ERROR_COUNT=$((ERROR_COUNT + 1))
21
+ fi
22
+ if ! grep -q "^description:" "$skill"; then
23
+ echo "ERROR: Missing 'description' in $skill"
24
+ ERROR_COUNT=$((ERROR_COUNT + 1))
25
+ fi
26
+ if ! grep -q "^lifecycle_stage:" "$skill"; then
27
+ echo "WARN: Missing 'lifecycle_stage' in $skill"
28
+ fi
29
+ if ! grep -q "^version:" "$skill"; then
30
+ echo "WARN: Missing 'version' in $skill"
31
+ fi
32
+
33
+ # 检查必要章节
34
+ for section in "When to Run" "Workflow"; do
35
+ if ! grep -q "## $section" "$skill"; then
36
+ echo "ERROR: Missing '$section' section in $skill"
37
+ ERROR_COUNT=$((ERROR_COUNT + 1))
38
+ fi
39
+ done
40
+
41
+ # 检查 Lifecycle Position(新增)
42
+ if ! grep -q "## Lifecycle Position" "$skill"; then
43
+ echo "WARN: Missing 'Lifecycle Position' in $skill (recommended)"
44
+ fi
45
+
46
+ echo "✓ $skill_name valid"
47
+ done
48
+
49
+ if [ "$ERROR_COUNT" -gt 0 ]; then
50
+ echo "ERROR: $ERROR_COUNT errors found in skill files"
51
+ exit 1
52
+ fi
53
+ echo "✓ Skill files valid"
54
+
55
+ # 2. 检查生命周期映射完整性
56
+ echo "[2/5] Checking lifecycle mapping..."
57
+ LIFECYCLE_STAGES="define plan build verify review ship meta"
58
+ for stage in $LIFECYCLE_STAGES; do
59
+ count=$(grep -l "lifecycle_stage: \"$stage\"" $(find "$SKILLS_DIR" -name "SKILL.md" -type f) 2>/dev/null | wc -l || echo "0")
60
+ if [ "$count" -eq 0 ] && [ "$stage" != "meta" ]; then
61
+ echo "WARN: No skill mapped to '$stage' stage"
62
+ else
63
+ echo "✓ $stage stage has $count skills"
64
+ fi
65
+ done
66
+
67
+ # 3. 检查技能间依赖一致性
68
+ echo "[3/5] Checking skill dependencies..."
69
+ for skill in $(find "$SKILLS_DIR" -name "SKILL.md" -type f); do
70
+ deps=$(grep "^dependencies:" "$skill" | sed 's/dependencies: //' | tr ',' ' ' 2>/dev/null || echo "")
71
+ for dep in $deps; do
72
+ dep_name=$(echo "$dep" | tr -d '[]"')
73
+ if [ -n "$dep_name" ] && [ ! -d "$SKILLS_DIR/$dep_name" ]; then
74
+ echo "ERROR: Dependency '$dep_name' not found for $(basename $(dirname "$skill"))"
75
+ ERROR_COUNT=$((ERROR_COUNT + 1))
76
+ fi
77
+ done
78
+ done
79
+
80
+ if [ "$ERROR_COUNT" -gt 0 ]; then
81
+ echo "ERROR: Dependency errors found"
82
+ exit 1
83
+ fi
84
+ echo "✓ Dependencies valid"
85
+
86
+ # 4. 检查 Reference Checklists
87
+ echo "[4/5] Checking reference checklists..."
88
+ REQUIRED_CHECKLISTS="coding-checklist.md solid-checklist.md security-checklist.md performance-checklist.md anti-rationalization.md"
89
+ for checklist in $REQUIRED_CHECKLISTS; do
90
+ found=$(find "$SKILLS_DIR" -name "$checklist" -type f | wc -l)
91
+ if [ "$found" -eq 0 ]; then
92
+ echo "WARN: Missing reference checklist '$checklist'"
93
+ else
94
+ echo "✓ Found $checklist"
95
+ fi
96
+ done
97
+
98
+ # 5. 检查 Agent Personas 和 Slash Commands
99
+ echo "[5/5] Checking agent personas and slash commands..."
100
+ if [ ! -f "$SKILLS_DIR/../references/agent-personas.md" ]; then
101
+ echo "WARN: Missing agent-personas.md"
102
+ else
103
+ echo "✓ Found agent-personas.md"
104
+ fi
105
+
106
+ if [ ! -f "$SKILLS_DIR/../references/slash-commands.md" ]; then
107
+ echo "WARN: Missing slash-commands.md"
108
+ else
109
+ echo "✓ Found slash-commands.md"
110
+ fi
111
+
112
+ echo "=== CI Validator Complete ==="
113
+ echo "All checks passed!"
114
+ exit 0
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const https = require('https');
5
+ const path = require('path');
6
+
7
+ const suite = require('../skills/references/golden-prompt-suite.js');
8
+
9
+ const repoRoot = path.join(__dirname, '..');
10
+ const skillsRoot = path.join(repoRoot, 'skills');
11
+
12
+ function parseArgs(argv) {
13
+ const options = {
14
+ provider: process.env.PIG_SKILLS_REPLAY_PROVIDER || 'openai',
15
+ model: process.env.OPENAI_MODEL || 'gpt-4.1-mini',
16
+ limit: 0,
17
+ out: null,
18
+ };
19
+
20
+ for (let index = 0; index < argv.length; index += 1) {
21
+ const token = argv[index];
22
+ if (token === '--provider') {
23
+ options.provider = argv[++index];
24
+ continue;
25
+ }
26
+ if (token.startsWith('--provider=')) {
27
+ options.provider = token.slice('--provider='.length);
28
+ continue;
29
+ }
30
+ if (token === '--model') {
31
+ options.model = argv[++index];
32
+ continue;
33
+ }
34
+ if (token.startsWith('--model=')) {
35
+ options.model = token.slice('--model='.length);
36
+ continue;
37
+ }
38
+ if (token === '--limit') {
39
+ options.limit = Number(argv[++index]) || 0;
40
+ continue;
41
+ }
42
+ if (token.startsWith('--limit=')) {
43
+ options.limit = Number(token.slice('--limit='.length)) || 0;
44
+ continue;
45
+ }
46
+ if (token === '--out') {
47
+ options.out = argv[++index];
48
+ continue;
49
+ }
50
+ if (token.startsWith('--out=')) {
51
+ options.out = token.slice('--out='.length);
52
+ continue;
53
+ }
54
+ }
55
+
56
+ return options;
57
+ }
58
+
59
+ function readSkillCatalog() {
60
+ return fs.readdirSync(skillsRoot, { withFileTypes: true })
61
+ .filter((entry) => entry.isDirectory() && entry.name !== 'references')
62
+ .map((entry) => {
63
+ const skillFile = path.join(skillsRoot, entry.name, 'SKILL.md');
64
+ const content = fs.readFileSync(skillFile, 'utf8');
65
+ const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
66
+ const frontmatter = frontmatterMatch ? frontmatterMatch[1] : '';
67
+ const name = (frontmatter.match(/^name:\s*(.+)$/m) || [])[1]?.trim() || entry.name;
68
+ const description = (frontmatter.match(/^description:\s*(.+)$/m) || [])[1]?.trim() || '';
69
+ const lifecycleStage = (frontmatter.match(/^lifecycle_stage:\s*(.+)$/m) || [])[1]?.trim() || '';
70
+ const outputSection = (content.match(/## Output\s+([\s\S]*?)(?:\n## |\n---|$)/) || [])[1] || '';
71
+ const gatesSection = (content.match(/## Stop Rules\s+([\s\S]*?)(?:\n## |\n---|$)/) || [])[1] || '';
72
+
73
+ return {
74
+ name,
75
+ description,
76
+ lifecycleStage,
77
+ outputs: outputSection
78
+ .split(/\r?\n/)
79
+ .map((line) => line.trim().replace(/^-+\s*/, ''))
80
+ .filter(Boolean)
81
+ .map((line) => line.replace(/^[*-]\s*/, '')),
82
+ stopRules: gatesSection
83
+ .split(/\r?\n/)
84
+ .map((line) => line.trim().replace(/^-+\s*/, ''))
85
+ .filter(Boolean),
86
+ };
87
+ });
88
+ }
89
+
90
+ function buildSystemPrompt(catalog) {
91
+ const lines = [
92
+ 'You are evaluating the pig-skills workflow replay suite.',
93
+ 'Choose the first skill that should handle the prompt.',
94
+ 'Return ONLY valid JSON with these keys:',
95
+ '{"firstSkill":"...","artifact":"...","stopRule":"...","handoff":"...","notes":"..."}',
96
+ 'Use exact canonical skill names.',
97
+ 'Artifact should be short and reflect the expected work product.',
98
+ 'Do not explain your reasoning outside the JSON object.',
99
+ '',
100
+ 'Available skills:',
101
+ ...catalog.map((skill) => `- ${skill.name} | ${skill.description} | outputs: ${skill.outputs.join('; ')} | stop: ${skill.stopRules.join(' | ')}`),
102
+ ];
103
+ return lines.join('\n');
104
+ }
105
+
106
+ function normalizeText(value) {
107
+ return String(value || '')
108
+ .toLowerCase()
109
+ .replace(/\s+/g, ' ')
110
+ .replace(/[^\p{L}\p{N}\s-]+/gu, '')
111
+ .trim();
112
+ }
113
+
114
+ function extractJson(text) {
115
+ const content = String(text || '').trim();
116
+ const start = content.indexOf('{');
117
+ const end = content.lastIndexOf('}');
118
+ if (start < 0 || end < 0 || end <= start) {
119
+ throw new Error('Model output did not contain JSON.');
120
+ }
121
+ return JSON.parse(content.slice(start, end + 1));
122
+ }
123
+
124
+ function matchesOutput(actual, expectedItems) {
125
+ const normalized = normalizeText(actual);
126
+ const items = expectedItems.map((item) => normalizeText(item));
127
+ const hits = items.filter((item) => normalized.includes(item));
128
+ return {
129
+ score: hits.length === items.length ? 2 : hits.length > 0 ? 1 : 0,
130
+ hits,
131
+ missing: items.filter((item) => !normalized.includes(item)),
132
+ };
133
+ }
134
+
135
+ function matchesText(actual, expected) {
136
+ if (expected === null) {
137
+ return {
138
+ score: actual === null || actual === undefined || String(actual).trim() === '' ? 2 : 0,
139
+ actual,
140
+ };
141
+ }
142
+ const normalizedActual = normalizeText(actual);
143
+ const normalizedExpected = normalizeText(expected);
144
+ return {
145
+ score: normalizedActual === normalizedExpected
146
+ ? 2
147
+ : normalizedActual.includes(normalizedExpected)
148
+ ? 1
149
+ : 0,
150
+ actual,
151
+ };
152
+ }
153
+
154
+ function buildMockResponse(testCase) {
155
+ return {
156
+ firstSkill: testCase.expectedFirstSkill,
157
+ artifact: testCase.expectedOutput.join(', '),
158
+ stopRule: testCase.expectedStop,
159
+ handoff: testCase.expectedHandoff,
160
+ notes: 'mock replay',
161
+ };
162
+ }
163
+
164
+ function postJson(url, payload, apiKey) {
165
+ return new Promise((resolve, reject) => {
166
+ const body = JSON.stringify(payload);
167
+ const request = https.request(url, {
168
+ method: 'POST',
169
+ headers: {
170
+ 'Authorization': `Bearer ${apiKey}`,
171
+ 'Content-Type': 'application/json',
172
+ 'Content-Length': Buffer.byteLength(body),
173
+ },
174
+ }, (response) => {
175
+ const chunks = [];
176
+ response.on('data', (chunk) => chunks.push(chunk));
177
+ response.on('end', () => {
178
+ const raw = Buffer.concat(chunks).toString('utf8');
179
+ if (response.statusCode && response.statusCode >= 400) {
180
+ return reject(new Error(`OpenAI API error ${response.statusCode}: ${raw}`));
181
+ }
182
+ try {
183
+ resolve(JSON.parse(raw));
184
+ } catch (error) {
185
+ reject(new Error(`Failed to parse API response: ${error.message}\n${raw}`));
186
+ }
187
+ });
188
+ });
189
+
190
+ request.on('error', reject);
191
+ request.write(body);
192
+ request.end();
193
+ });
194
+ }
195
+
196
+ async function runOpenAI(model, systemPrompt, userPrompt) {
197
+ const apiKey = process.env.OPENAI_API_KEY;
198
+ if (!apiKey) {
199
+ throw new Error('OPENAI_API_KEY is required for provider=openai.');
200
+ }
201
+
202
+ const response = await postJson('https://api.openai.com/v1/chat/completions', {
203
+ model,
204
+ temperature: 0,
205
+ messages: [
206
+ { role: 'system', content: systemPrompt },
207
+ { role: 'user', content: userPrompt },
208
+ ],
209
+ }, apiKey);
210
+
211
+ const content = response?.choices?.[0]?.message?.content;
212
+ if (!content) {
213
+ throw new Error('OpenAI response did not include message content.');
214
+ }
215
+ return extractJson(content);
216
+ }
217
+
218
+ async function evaluateCase(testCase, context) {
219
+ const userPrompt = [
220
+ `Prompt: ${testCase.prompt}`,
221
+ 'Return the first skill, the artifact shape, the stop boundary, and the next handoff.',
222
+ 'Keep the response concise and only use the canonical skill names from the catalog.',
223
+ ].join('\n');
224
+
225
+ const modelOutput = context.provider === 'mock'
226
+ ? buildMockResponse(testCase)
227
+ : await runOpenAI(context.model, context.systemPrompt, userPrompt);
228
+
229
+ const firstSkillResult = matchesText(modelOutput.firstSkill, testCase.expectedFirstSkill);
230
+ const artifactResult = matchesOutput(modelOutput.artifact, testCase.expectedOutput);
231
+ const stopResult = matchesText(modelOutput.stopRule, testCase.expectedStop);
232
+ const handoffResult = matchesText(modelOutput.handoff, testCase.expectedHandoff);
233
+
234
+ const score = firstSkillResult.score + artifactResult.score + stopResult.score + handoffResult.score;
235
+ const passed = firstSkillResult.score === 2 && artifactResult.score === 2 && stopResult.score === 2 && handoffResult.score === 2;
236
+
237
+ return {
238
+ id: testCase.id,
239
+ prompt: testCase.prompt,
240
+ expected: {
241
+ firstSkill: testCase.expectedFirstSkill,
242
+ output: testCase.expectedOutput,
243
+ stopRule: testCase.expectedStop,
244
+ handoff: testCase.expectedHandoff,
245
+ },
246
+ actual: modelOutput,
247
+ scores: {
248
+ firstSkill: firstSkillResult.score,
249
+ artifact: artifactResult.score,
250
+ stopRule: stopResult.score,
251
+ handoff: handoffResult.score,
252
+ total: score,
253
+ },
254
+ passed,
255
+ };
256
+ }
257
+
258
+ async function main() {
259
+ const options = parseArgs(process.argv.slice(2));
260
+ const catalog = readSkillCatalog();
261
+ const systemPrompt = buildSystemPrompt(catalog);
262
+ const cases = options.limit > 0 ? suite.cases.slice(0, options.limit) : suite.cases;
263
+
264
+ const context = {
265
+ provider: options.provider,
266
+ model: options.model,
267
+ systemPrompt,
268
+ };
269
+
270
+ console.log(`# Golden replay run`);
271
+ console.log(`provider: ${context.provider}`);
272
+ console.log(`model: ${context.model}`);
273
+ console.log(`cases: ${cases.length}`);
274
+
275
+ const results = [];
276
+ for (const testCase of cases) {
277
+ const result = await evaluateCase(testCase, context);
278
+ results.push(result);
279
+ const status = result.passed ? 'PASS' : 'FAIL';
280
+ console.log(`${status} ${result.id} (${result.scores.total}/8)`);
281
+ if (!result.passed) {
282
+ console.log(JSON.stringify(result, null, 2));
283
+ }
284
+ }
285
+
286
+ const passedCount = results.filter((result) => result.passed).length;
287
+ const report = {
288
+ provider: context.provider,
289
+ model: context.model,
290
+ totalCases: results.length,
291
+ passedCases: passedCount,
292
+ failedCases: results.length - passedCount,
293
+ results,
294
+ };
295
+
296
+ if (options.out) {
297
+ const outputPath = path.resolve(options.out);
298
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
299
+ fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
300
+ }
301
+
302
+ if (passedCount !== results.length) {
303
+ process.exitCode = 1;
304
+ } else {
305
+ console.log('Golden replay complete: all cases passed.');
306
+ }
307
+ }
308
+
309
+ main().catch((error) => {
310
+ console.error(error.message);
311
+ process.exitCode = 1;
312
+ });
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const repoRoot = path.join(__dirname, '..');
7
+ const rulesRoot = path.join(repoRoot, 'rules');
8
+ const bundlesPath = path.join(rulesRoot, 'bundles.json');
9
+ const skillProfileMapPath = path.join(rulesRoot, 'skill-profile-map.json');
10
+ const skillsRoot = path.join(repoRoot, 'skills');
11
+ const expectedBundles = ['core', 'workflow', 'product', 'coding', 'docs', 'review', 'overlays'];
12
+ const requiredSections = [
13
+ '## 默认做法',
14
+ '## 禁区',
15
+ '## 检查',
16
+ '## 回放信号',
17
+ '## References',
18
+ ];
19
+
20
+ function readJson(filePath) {
21
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
22
+ }
23
+
24
+ function readText(filePath) {
25
+ return fs.readFileSync(filePath, 'utf8');
26
+ }
27
+
28
+ function assert(condition, message) {
29
+ if (!condition) {
30
+ throw new Error(message);
31
+ }
32
+ }
33
+
34
+ function readSkillDirectories() {
35
+ return fs.readdirSync(skillsRoot, { withFileTypes: true })
36
+ .filter((entry) => entry.isDirectory() && entry.name !== 'references')
37
+ .map((entry) => entry.name)
38
+ .sort();
39
+ }
40
+
41
+ function readSkillProfile(skillName) {
42
+ const skillFile = path.join(skillsRoot, skillName, 'SKILL.md');
43
+ const content = readText(skillFile);
44
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
45
+ if (!match) return null;
46
+ const profileMatch = match[1].match(/^rule_profile:\s*(.+)$/m);
47
+ return profileMatch ? profileMatch[1].trim() : null;
48
+ }
49
+
50
+ function verifyBundleFiles(bundleName, bundleConfig) {
51
+ assert(bundleConfig && typeof bundleConfig === 'object', `${bundleName}: missing bundle config`);
52
+ assert(typeof bundleConfig.entry === 'string' && bundleConfig.entry.length > 0, `${bundleName}: missing entry`);
53
+ assert(Array.isArray(bundleConfig.files) && bundleConfig.files.length > 0, `${bundleName}: missing files`);
54
+
55
+ const files = new Set(bundleConfig.files);
56
+ files.add(bundleConfig.entry);
57
+
58
+ for (const relativePath of files) {
59
+ assert(relativePath.startsWith(`${bundleName}/`) || relativePath === `${bundleName}/index.md`, `${bundleName}: file escapes bundle namespace: ${relativePath}`);
60
+ const filePath = path.join(rulesRoot, relativePath);
61
+ assert(fs.existsSync(filePath), `${bundleName}: missing file ${relativePath}`);
62
+ const content = readText(filePath);
63
+ for (const section of requiredSections) {
64
+ assert(content.includes(section), `${relativePath}: missing section ${section}`);
65
+ }
66
+ }
67
+
68
+ if (bundleConfig.profiles) {
69
+ for (const [profileName, profileConfig] of Object.entries(bundleConfig.profiles)) {
70
+ assert(typeof profileConfig.entry === 'string' && profileConfig.entry.length > 0, `${bundleName}/${profileName}: missing entry`);
71
+ assert(Array.isArray(profileConfig.files) && profileConfig.files.length > 0, `${bundleName}/${profileName}: missing files`);
72
+ const profileFiles = new Set(profileConfig.files);
73
+ profileFiles.add(profileConfig.entry);
74
+ for (const relativePath of profileFiles) {
75
+ assert(relativePath.startsWith(`${bundleName}/`) || relativePath === `${bundleName}/index.md`, `${bundleName}/${profileName}: file escapes bundle namespace: ${relativePath}`);
76
+ const filePath = path.join(rulesRoot, relativePath);
77
+ assert(fs.existsSync(filePath), `${bundleName}/${profileName}: missing file ${relativePath}`);
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ function main() {
84
+ const bundles = readJson(bundlesPath);
85
+ const skillProfileMap = readJson(skillProfileMapPath);
86
+ const skillDirs = readSkillDirectories();
87
+
88
+ for (const bundleName of expectedBundles) {
89
+ assert(Object.prototype.hasOwnProperty.call(bundles, bundleName), `bundles.json missing ${bundleName} bundle`);
90
+ }
91
+
92
+ for (const [bundleName, bundleConfig] of Object.entries(bundles)) {
93
+ verifyBundleFiles(bundleName, bundleConfig);
94
+ }
95
+
96
+ for (const skillName of skillDirs) {
97
+ const declaredProfile = readSkillProfile(skillName);
98
+ assert(declaredProfile, `${skillName}: missing rule_profile`);
99
+ const mapped = skillProfileMap[skillName];
100
+ assert(mapped, `${skillName}: missing skill-profile mapping`);
101
+ assert(mapped.profile === declaredProfile, `${skillName}: profile mismatch (${mapped.profile} != ${declaredProfile})`);
102
+ assert(expectedBundles.includes(mapped.bundle), `${skillName}: unknown bundle ${mapped.bundle}`);
103
+ assert(bundles[mapped.bundle].profiles && bundles[mapped.bundle].profiles[mapped.profile], `${skillName}: unknown profile ${mapped.bundle}/${mapped.profile}`);
104
+ }
105
+
106
+ const indexContent = readText(path.join(rulesRoot, 'index.md'));
107
+ for (const bundleName of expectedBundles) {
108
+ assert(indexContent.includes(bundleName), `rules/index.md should mention ${bundleName}`);
109
+ }
110
+ assert(indexContent.includes('skill-profile-map.json'), 'rules/index.md should mention skill-profile-map.json');
111
+
112
+ const loaderContent = readText(path.join(repoRoot, 'bin', 'rules-loader.js'));
113
+ assert(loaderContent.includes("const CORE_BUNDLE_NAME = 'core'"), 'rules-loader.js should define core bundle');
114
+ assert(loaderContent.includes("const CORE_PROFILE_NAME = 'core'"), 'rules-loader.js should define core profile');
115
+ assert(loaderContent.includes("hasCodingSignals"), 'rules-loader.js should define coding detection');
116
+
117
+ console.log(`=== Rules validation complete for ${skillDirs.length} skills and ${Object.keys(bundles).length} bundles ===`);
118
+ }
119
+
120
+ try {
121
+ main();
122
+ } catch (error) {
123
+ console.error(error.message);
124
+ process.exitCode = 1;
125
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const repoRoot = path.join(__dirname, '..');
7
+ const skillsRoot = path.join(repoRoot, 'skills');
8
+
9
+ function readSkillDirectories() {
10
+ return fs.readdirSync(skillsRoot, { withFileTypes: true })
11
+ .filter((entry) => entry.isDirectory() && entry.name !== 'references')
12
+ .map((entry) => entry.name)
13
+ .sort();
14
+ }
15
+
16
+ function readSkillFile(skillName) {
17
+ const skillFile = path.join(skillsRoot, skillName, 'SKILL.md');
18
+ return fs.readFileSync(skillFile, 'utf8');
19
+ }
20
+
21
+ function validateSkill(skillName) {
22
+ const content = readSkillFile(skillName);
23
+ const lines = content.split(/\r?\n/);
24
+ const sectionLines = [];
25
+ let inReplaySection = false;
26
+
27
+ for (const rawLine of lines) {
28
+ const line = rawLine.trim();
29
+ if (!inReplaySection) {
30
+ if (line === '## Replay Signals') {
31
+ inReplaySection = true;
32
+ }
33
+ continue;
34
+ }
35
+
36
+ if (line.startsWith('## ')) {
37
+ break;
38
+ }
39
+
40
+ if (line.length > 0) {
41
+ sectionLines.push(line.replace(/^[-*]\s*/, ''));
42
+ }
43
+ }
44
+
45
+ if (sectionLines.length === 0) {
46
+ throw new Error(`${skillName}: missing Replay Signals section`);
47
+ }
48
+
49
+ const requiredPrefixes = [
50
+ 'Input signal:',
51
+ 'Output to verify:',
52
+ 'Stop signal:',
53
+ 'Handoff signal:',
54
+ ];
55
+
56
+ const missing = requiredPrefixes.filter((prefix) => !sectionLines.some((line) => line.startsWith(prefix)));
57
+ if (missing.length > 0) {
58
+ throw new Error(`${skillName}: Replay Signals missing lines: ${missing.join(' | ')}`);
59
+ }
60
+ }
61
+
62
+ function main() {
63
+ for (const skillName of readSkillDirectories()) {
64
+ validateSkill(skillName);
65
+ }
66
+
67
+ console.log('=== Replay signal validation complete ===');
68
+ }
69
+
70
+ try {
71
+ main();
72
+ } catch (error) {
73
+ console.error(error.message);
74
+ process.exitCode = 1;
75
+ }