@namewta/speculo 0.8.11 → 1.0.1
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/README.md +10 -6
- package/dist/src/cli.js +12 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/doctor.d.ts +10 -0
- package/dist/src/doctor.js +70 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/index.js +105 -56
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel.d.ts +98 -0
- package/dist/src/kernel.js +29 -0
- package/dist/src/kernel.js.map +1 -0
- package/dist/src/refresh.js +55 -24
- package/dist/src/refresh.js.map +1 -1
- package/dist/src/structured.d.ts +2 -2
- package/dist/src/structured.js +114 -241
- package/dist/src/structured.js.map +1 -1
- package/package.json +3 -2
- package/template/.speculo/README.md +1 -1
- package/template/.speculo/capabilities.json +14 -0
- package/template/.speculo/kernel/README.md +10 -0
- package/template/.speculo/kernel/capability-profile.schema.json +14 -0
- package/template/.speculo/kernel/checkpoint.schema.json +7 -0
- package/template/.speculo/kernel/trace-event.schema.json +8 -0
- package/template/.speculo/kernel/workflow-manifest.schema.json +13 -0
- package/template/.speculo/kernel.json +10 -0
- package/template/.speculo/refresh-contract.json +4 -1
- package/template/AGENTS.md +0 -3
- package/template/canonical/canonical-specdev-goal-plan.md +13 -7
- package/template/canonical/canonical-specdev-grill-with-docs.md +1 -1
- package/template/canonical/canonical-specdev-orchestrate-implementation.md +29 -11
- package/template/canonical/canonical-specdev-spec.md +1 -1
- package/template/canonical/canonical-specdev-tickets.md +38 -4
- package/template/commands/archive-and-consolidate.md +13 -40
- package/template/commands/status.md +3 -3
- package/template/skills/engineering-standards-builder/README.md +6 -28
- package/template/skills/engineering-standards-builder/SKILL.md +88 -163
- package/template/skills/engineering-standards-builder/examples/README.md +2 -0
- package/template/skills/engineering-standards-builder/manifest.txt +4 -0
- package/template/skills/engineering-standards-builder/references/rules/00-governance-and-precedence.md +22 -29
- package/template/skills/engineering-standards-builder/references/rules/01-project-discovery.md +26 -59
- package/template/skills/engineering-standards-builder/references/rules/02-evidence-topology-and-scope.md +28 -55
- package/template/skills/engineering-standards-builder/references/rules/03-interview-and-decisions.md +4 -3
- package/template/skills/engineering-standards-builder/references/rules/14-generation-contract.md +64 -80
- package/template/skills/engineering-standards-builder/references/rules/15-validation-contract.md +24 -47
- package/template/skills/engineering-standards-builder/references/rules/16-language-adapter-contract.md +11 -48
- package/template/skills/engineering-standards-builder/references/rules/README.md +3 -3
- package/template/skills/engineering-standards-builder/scripts/self-test.mjs +51 -5
- package/template/skills/engineering-standards-builder/scripts/validate-builder.mjs +16 -4
- package/template/skills/engineering-standards-builder/scripts/validate-generated-skill.mjs +166 -64
- package/template/skills/engineering-standards-builder/templates/README.md +12 -3
- package/template/skills/engineering-standards-builder/templates/domain-skill/SKILL.md.template +32 -0
- package/template/skills/engineering-standards-builder/templates/project-skill/SKILL.md.template +16 -11
- package/template/skills/engineering-standards-builder/templates/project-skill/generated-skill-set.json.template +7 -0
- package/template/skills/engineering-standards-builder/templates/project-skill/references/project/00-project-profile.md.template +3 -1
- package/template/skills/engineering-standards-builder/templates/project-skill/references/project/01-module-map.md.template +4 -0
- package/template/skills/engineering-standards-builder/templates/project-skill/references/project/02-decisions-and-exceptions.md.template +1 -1
- package/template/skills/engineering-standards-builder/templates/project-skill/references/project/03-skill-map.md.template +19 -0
- package/template/skills/engineering-standards-builder/templates/project-skill/references/project/04-source-and-template-map.md.template +22 -0
- package/template/skills/engineering-standards-builder/templates/project-skill/references/project/review-checklist.md.template +2 -0
- package/template/skills/git-history-squash/SKILL.md +100 -0
- package/template/skills/git-history-squash/assets/request-template.json +18 -0
- package/template/skills/git-history-squash/references/recovery-contract.md +50 -0
- package/template/skills/git-history-squash/references/rewrite-contract.md +123 -0
- package/template/skills/git-history-squash/references/submodule-contract.md +54 -0
- package/template/skills/git-history-squash/scripts/git-history-squash.mjs +1171 -0
- package/template/workflows/learning/A-archive/A-archive.md +25 -0
- package/template/workflows/learning/A-assess-and-plan/A-assess-and-plan.md +17 -23
- package/template/workflows/learning/A-assess-and-plan/background-template.md +13 -0
- package/template/workflows/learning/A-assess-and-plan/change-status-template.json +21 -14
- package/template/workflows/learning/A-assess-and-plan/course-template.md +28 -0
- package/template/workflows/learning/C-consolidate/C-consolidate.md +33 -0
- package/template/workflows/learning/H-homework/H-homework.md +31 -0
- package/template/workflows/learning/H-homework/homework-template.md +63 -0
- package/template/workflows/learning/I-init-setup/I-init-setup.md +14 -19
- package/template/workflows/learning/I-init-setup/context-index-template.md +3 -3
- package/template/workflows/learning/I-init-setup/learner-profile-template.md +12 -10
- package/template/workflows/learning/I-init-setup/review-index-template.md +1 -1
- package/template/workflows/learning/INDEX.md +7 -9
- package/template/workflows/learning/L-lesson/L-lesson.md +32 -0
- package/template/workflows/learning/L-lesson/lesson-template.md +51 -0
- package/template/workflows/learning/R-review/R-review.md +13 -18
- package/template/workflows/learning/R-review/review-template.md +13 -6
- package/template/workflows/learning/README.md +86 -81
- package/template/workflows/learning/_state/status.json +1 -1
- package/template/workflows/learning/common/rules/artifact-contract.md +11 -25
- package/template/workflows/learning/common/rules/assessment-policy.md +9 -4
- package/template/workflows/learning/common/rules/knowledge-organization.md +4 -6
- package/template/workflows/learning/common/rules/mastery-policy.md +3 -19
- package/template/workflows/learning/common/rules/path-reference-contract.md +3 -5
- package/template/workflows/learning/common/rules/teaching-policy.md +9 -9
- package/template/workflows/learning/common/schemas/change-status.schema.json +46 -18
- package/template/workflows/learning/common/schemas/status.schema.json +25 -24
- package/template/workflows/learning/common/skills/topic-synthesis/SKILL.md +17 -0
- package/template/workflows/learning/common/skills/topic-synthesis/claim-template.md +15 -0
- package/template/workflows/learning/common/tools/relocate-learning.mjs +208 -0
- package/template/workflows/learning/common/tools/validate-learning.mjs +195 -297
- package/template/workflows/learning/manifest.json +1 -0
- package/template/workflows/learning/runtime-contract.json +3 -2
- package/template/workflows/ops/manifest.json +1 -0
- package/template/workflows/person/manifest.json +1 -0
- package/template/workflows/specdev/I-implement/I-implement.md +12 -4
- package/template/workflows/specdev/I-implement/execution-preflight.md +4 -0
- package/template/workflows/specdev/README.md +1 -1
- package/template/workflows/specdev/T-tickets/T-tickets.md +16 -3
- package/template/workflows/specdev/T-tickets/ticket-readiness.md +3 -0
- package/template/workflows/specdev/T-tickets/ticket-template.md +3 -0
- package/template/workflows/specdev/T-tickets/tickets-map-template.md +15 -0
- package/template/workflows/specdev/common/rules/artifact-contract.md +1 -1
- package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +4 -2
- package/template/workflows/specdev/common/skills/subagent-delivery/references/external-web-subagent.md +2 -1
- package/template/workflows/specdev/common/skills/subagent-delivery/references/native-subagent.md +2 -1
- package/template/workflows/specdev/common/skills/subagent-delivery/references/source-package.md +4 -2
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +143 -3
- package/template/workflows/specdev/manifest.json +1 -0
- package/template/workflows/workflow-manifest.schema.json +14 -0
- package/template/workflows/learning/A-archive-and-consolidate/A-archive-and-consolidate.md +0 -38
- package/template/workflows/learning/A-archive-and-consolidate/promotion-plan-template.md +0 -23
- package/template/workflows/learning/A-assess-and-plan/learning-plan-template.md +0 -28
- package/template/workflows/learning/E-eli5/E-eli5.md +0 -37
- package/template/workflows/learning/E-eli5/lesson-template.md +0 -31
- package/template/workflows/learning/P-practice/P-practice.md +0 -34
- package/template/workflows/learning/P-practice/practice-template.md +0 -16
- package/template/workflows/learning/Q-quiz/Q-quiz.md +0 -34
- package/template/workflows/learning/Q-quiz/quiz-artifact-template.md +0 -16
- package/template/workflows/learning/common/skills/knowledge-promotion/SKILL.md +0 -46
- package/template/workflows/learning/common/skills/knowledge-promotion/domain-index-template.md +0 -6
- package/template/workflows/learning/common/skills/knowledge-promotion/domain-overview-template.md +0 -19
- package/template/workflows/learning/common/skills/knowledge-promotion/knowledge-template.md +0 -25
|
@@ -83,27 +83,43 @@ function assertFixture(inventory, expected, fixtureName) {
|
|
|
83
83
|
|
|
84
84
|
async function writeGeneratedFixture(projectRoot) {
|
|
85
85
|
await mkdir(path.join(projectRoot, 'src'), { recursive: true });
|
|
86
|
+
await mkdir(path.join(projectRoot, 'docs', 'fm'), { recursive: true });
|
|
86
87
|
await writeFile(path.join(projectRoot, 'package.json'), `${JSON.stringify({
|
|
87
88
|
name: 'generated-validation-fixture', private: true,
|
|
88
89
|
dependencies: { vue: '^3.5.0' }, devDependencies: { typescript: '^5.9.0', vite: '^7.0.0' },
|
|
89
90
|
}, null, 2)}\n`);
|
|
90
91
|
await writeFile(path.join(projectRoot, 'tsconfig.json'), '{"compilerOptions":{"strict":true},"include":["src"]}\n');
|
|
91
92
|
await writeFile(path.join(projectRoot, 'src', 'App.vue'), '<script setup lang="ts">const value = 1</script>\n<template><main>{{ value }}</main></template>\n');
|
|
93
|
+
await writeFile(path.join(projectRoot, 'docs', 'fm', 'component.vue.ftl'), '<template><main>${title}</main></template>\n');
|
|
92
94
|
|
|
93
95
|
const canonical = path.join(projectRoot, '.agents', 'skills', 'engineering-standards');
|
|
96
|
+
const domain = path.join(projectRoot, '.agents', 'skills', 'generated-vue-development');
|
|
94
97
|
await mkdir(path.join(canonical, 'references', 'project'), { recursive: true });
|
|
95
98
|
await mkdir(path.join(canonical, 'references', 'rules'), { recursive: true });
|
|
96
99
|
await mkdir(path.join(canonical, 'references', 'typescript', 'frameworks'), { recursive: true });
|
|
97
|
-
await
|
|
100
|
+
await mkdir(path.join(domain, 'references'), { recursive: true });
|
|
101
|
+
await writeFile(path.join(canonical, 'generated-skill-set.json'), `${JSON.stringify({
|
|
102
|
+
schema_version: 1,
|
|
103
|
+
generator: 'engineering-standards-builder',
|
|
104
|
+
skills: [
|
|
105
|
+
{ name: 'engineering-standards', path: '.agents/skills/engineering-standards', role: 'router' },
|
|
106
|
+
{ name: 'generated-vue-development', path: '.agents/skills/generated-vue-development', role: 'domain' },
|
|
107
|
+
],
|
|
108
|
+
}, null, 2)}\n`);
|
|
109
|
+
await writeFile(path.join(canonical, 'SKILL.md'), `---\nname: engineering-standards\ndescription: Generated fixture standards.\n---\n\n# Engineering Standards\n\nRead [profile](references/project/00-project-profile.md), [modules](references/project/01-module-map.md), [decisions](references/project/02-decisions-and-exceptions.md), [skills](references/project/03-skill-map.md), [sources](references/project/04-source-and-template-map.md), [review](references/project/review-checklist.md), [rules](references/rules/core.md), [TypeScript](references/typescript/language.md), and [Vue](references/typescript/frameworks/vue.md). Route Vue work to [generated-vue-development](../generated-vue-development/SKILL.md).\n`);
|
|
98
110
|
await writeFile(path.join(canonical, 'references', 'project', '00-project-profile.md'), '# Project Profile\n\nVue fixture.\n');
|
|
99
111
|
await writeFile(path.join(canonical, 'references', 'project', '01-module-map.md'), '# Module Map\n\n- module:. uses TypeScript and Vue.\n');
|
|
100
112
|
await writeFile(path.join(canonical, 'references', 'project', '02-decisions-and-exceptions.md'), '# Decisions and Exceptions\n\nNo exceptions.\n');
|
|
113
|
+
await writeFile(path.join(canonical, 'references', 'project', '03-skill-map.md'), '# Skill Map\n\n- generated-vue-development: Vue implementation.\n');
|
|
114
|
+
await writeFile(path.join(canonical, 'references', 'project', '04-source-and-template-map.md'), '# Source and Template Map\n\n- [Vue implementation](../../../../../src/App.vue)\n- [Component template](../../../../../docs/fm/component.vue.ftl)\n');
|
|
101
115
|
await writeFile(path.join(canonical, 'references', 'project', 'review-checklist.md'), '# Review Checklist\n\n- Verify scope.\n');
|
|
102
116
|
const rule = `# Core Rules\n\n### CORE-001 Validated boundary\nScope: repository\nLevel: MUST\nSource: repository-fact\nApplies when: external input enters the application.\nRule: Validate external input before domain use.\nRationale: Preserve trusted internal models.\nVerification: Review boundary tests.\nException: Recorded project exception only.\n`;
|
|
103
117
|
await writeFile(path.join(canonical, 'references', 'rules', 'core.md'), rule);
|
|
104
118
|
await writeFile(path.join(canonical, 'references', 'typescript', 'language.md'), rule.replace('CORE-001', 'TS-001'));
|
|
105
119
|
await writeFile(path.join(canonical, 'references', 'typescript', 'frameworks', 'vue.md'), rule.replace('CORE-001', 'VUE-001'));
|
|
106
|
-
|
|
120
|
+
await writeFile(path.join(domain, 'SKILL.md'), '---\nname: generated-vue-development\ndescription: Implement Vue features in the generated fixture.\n---\n\n# Vue Development\n\nRead `../engineering-standards/SKILL.md`, then inspect [the mature component](../../../src/App.vue) and [the project template](../../../docs/fm/component.vue.ftl).\n');
|
|
121
|
+
await writeFile(path.join(domain, 'references', 'workflow.md'), '# Workflow\n\nUse the project template and verify the component.\n');
|
|
122
|
+
return { canonical, domain };
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
async function main() {
|
|
@@ -174,9 +190,39 @@ async function main() {
|
|
|
174
190
|
|
|
175
191
|
const generatedTemp = await mkdtemp(path.join(os.tmpdir(), 'standards-builder-generated-'));
|
|
176
192
|
tempRoots.push(generatedTemp);
|
|
177
|
-
const canonical = await writeGeneratedFixture(generatedTemp);
|
|
193
|
+
const { canonical } = await writeGeneratedFixture(generatedTemp);
|
|
178
194
|
const valid = runNode(generatedValidator, ['--root', generatedTemp, '--strict']);
|
|
179
|
-
assert(valid.status === 0, `valid generated Skill was rejected: ${(valid.stderr || valid.stdout).trim()}`);
|
|
195
|
+
assert(valid.status === 0, `valid generated Skill Set was rejected: ${(valid.stderr || valid.stdout).trim()}`);
|
|
196
|
+
|
|
197
|
+
const unowned = path.join(generatedTemp, '.agents', 'skills', 'user-maintained-skill');
|
|
198
|
+
await mkdir(unowned, { recursive: true });
|
|
199
|
+
await writeFile(path.join(unowned, 'SKILL.md'), '---\nname: user-maintained-skill\ndescription: User-owned fixture skill.\n---\n\n# User Skill\n');
|
|
200
|
+
const withUnowned = runNode(generatedValidator, ['--root', generatedTemp, '--strict']);
|
|
201
|
+
assert(withUnowned.status === 0, 'validator must ignore a Skill outside the Builder ownership manifest');
|
|
202
|
+
|
|
203
|
+
const sourceMap = path.join(canonical, 'references', 'project', '04-source-and-template-map.md');
|
|
204
|
+
const sourceMapText = await readFile(sourceMap, 'utf8');
|
|
205
|
+
await writeFile(sourceMap, `${sourceMapText}\n- [Missing project source](../../../../../src/missing.ts)\n`);
|
|
206
|
+
const brokenSource = runNode(generatedValidator, ['--root', generatedTemp, '--strict']);
|
|
207
|
+
assert(brokenSource.status !== 0, 'validator must reject a missing project source link');
|
|
208
|
+
await writeFile(sourceMap, sourceMapText);
|
|
209
|
+
|
|
210
|
+
const ownershipPath = path.join(canonical, 'generated-skill-set.json');
|
|
211
|
+
const ownershipText = await readFile(ownershipPath, 'utf8');
|
|
212
|
+
const badOwnership = JSON.parse(ownershipText);
|
|
213
|
+
badOwnership.skills[1].path = '.agents/skills/../outside';
|
|
214
|
+
await writeFile(ownershipPath, `${JSON.stringify(badOwnership, null, 2)}\n`);
|
|
215
|
+
const escapingOwnership = runNode(generatedValidator, ['--root', generatedTemp, '--strict']);
|
|
216
|
+
assert(escapingOwnership.status !== 0, 'validator must reject an invalid owned Skill path');
|
|
217
|
+
await writeFile(ownershipPath, ownershipText);
|
|
218
|
+
|
|
219
|
+
const routerPath = path.join(canonical, 'SKILL.md');
|
|
220
|
+
const routerText = await readFile(routerPath, 'utf8');
|
|
221
|
+
await writeFile(routerPath, routerText.replace('[generated-vue-development](../generated-vue-development/SKILL.md)', '`generated-vue-development`'));
|
|
222
|
+
const missingRoute = runNode(generatedValidator, ['--root', generatedTemp, '--strict']);
|
|
223
|
+
assert(missingRoute.status !== 0, 'validator must reject a domain Skill that is not linked by the router');
|
|
224
|
+
await writeFile(routerPath, routerText);
|
|
225
|
+
|
|
180
226
|
await writeFile(path.join(canonical, 'references', 'typescript', 'frameworks', 'react.md'), '# React\n');
|
|
181
227
|
const wrongFramework = runNode(generatedValidator, ['--root', generatedTemp, '--strict']);
|
|
182
228
|
assert(wrongFramework.status !== 0, 'strict validator must reject an unselected React rule in a Vue-only project');
|
|
@@ -186,7 +232,7 @@ async function main() {
|
|
|
186
232
|
await writeFile(path.join(wrapper, 'SKILL.md'), '---\nname: typescript-standards\ndescription: alias\n---\n\nRoute to engineering-standards.\nSecond body line.\n');
|
|
187
233
|
const badWrapper = runNode(generatedValidator, ['--root', generatedTemp, '--strict']);
|
|
188
234
|
assert(badWrapper.status !== 0, 'validator must reject a multi-line compatibility wrapper');
|
|
189
|
-
process.stdout.write('self-test: generated Skill validator OK\n');
|
|
235
|
+
process.stdout.write('self-test: generated Skill Set validator OK\n');
|
|
190
236
|
|
|
191
237
|
const manifest = runNode(manifestScript, ['--root', args.root, '--check']);
|
|
192
238
|
assert(manifest.status === 0, `manifest check failed: ${(manifest.stderr || manifest.stdout).trim()}`);
|
|
@@ -35,7 +35,8 @@ async function collect(rootReal) {
|
|
|
35
35
|
const directories = [];
|
|
36
36
|
async function visit(directory) {
|
|
37
37
|
const relDir = toPosix(path.relative(rootReal, directory)) || '.';
|
|
38
|
-
const entries = await readdir(directory, { withFileTypes: true })
|
|
38
|
+
const entries = (await readdir(directory, { withFileTypes: true }))
|
|
39
|
+
.filter((entry) => entry.name !== '.DS_Store');
|
|
39
40
|
entries.sort((a, b) => a.name.localeCompare(b.name, 'en'));
|
|
40
41
|
directories.push({ abs: directory, rel: relDir, entries });
|
|
41
42
|
for (const entry of entries) {
|
|
@@ -89,7 +90,7 @@ function normalizeLinkTarget(sourceAbs, link) {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
async function computeManifest(rootReal, files) {
|
|
92
|
-
return `${files.map((file) => file.rel).filter((rel) => rel !== 'manifest.txt' &&
|
|
93
|
+
return `${files.map((file) => file.rel).filter((rel) => rel !== 'manifest.txt' && path.posix.basename(rel) !== '.DS_Store').sort((a, b) => a.localeCompare(b, 'en')).join('\n')}\n`;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
function failLines(errors, warnings) {
|
|
@@ -138,9 +139,11 @@ async function main() {
|
|
|
138
139
|
if (frontmatter.values.name !== 'engineering-standards-builder') errors.push('stable Skill ID must remain engineering-standards-builder for drop-in replacement');
|
|
139
140
|
if (!frontmatter.values.description) errors.push('frontmatter description is missing');
|
|
140
141
|
if (/[<>]/.test(frontmatter.values.description ?? '')) errors.push('frontmatter description must not contain angle brackets');
|
|
142
|
+
if (frontmatter.values['disable-model-invocation'] !== 'true') errors.push('Builder must remain explicitly user-invoked with disable-model-invocation: true');
|
|
143
|
+
}
|
|
144
|
+
for (const requiredConcept of ['Agent Team', 'generated-skill-set.json', '最小原则', 'Skill 边界']) {
|
|
145
|
+
if (!skillText.includes(requiredConcept)) errors.push(`SKILL.md is missing core contract: ${requiredConcept}`);
|
|
141
146
|
}
|
|
142
|
-
const completionCount = (skillText.match(/\*\*完成标准\*\*/g) ?? []).length;
|
|
143
|
-
if (completionCount < 7) errors.push(`SKILL.md must define completion criteria for all seven stages; found ${completionCount}`);
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
const localMarkdownFiles = files.filter((file) => /\.md(?:\.template)?$/.test(file.name) || file.name === 'SKILL.md.template');
|
|
@@ -218,6 +221,15 @@ async function main() {
|
|
|
218
221
|
if (!indexText.includes(file.name)) errors.push(`template is not indexed in templates/README.md: ${file.rel}`);
|
|
219
222
|
}
|
|
220
223
|
}
|
|
224
|
+
for (const requiredTemplate of [
|
|
225
|
+
'templates/project-skill/SKILL.md.template',
|
|
226
|
+
'templates/project-skill/generated-skill-set.json.template',
|
|
227
|
+
'templates/project-skill/references/project/03-skill-map.md.template',
|
|
228
|
+
'templates/project-skill/references/project/04-source-and-template-map.md.template',
|
|
229
|
+
'templates/domain-skill/SKILL.md.template',
|
|
230
|
+
]) {
|
|
231
|
+
if (!fileByRel.has(requiredTemplate)) errors.push(`required Skill Set template missing: ${requiredTemplate}`);
|
|
232
|
+
}
|
|
221
233
|
|
|
222
234
|
const fixtureExpectations = files.filter((file) => file.name === 'expected.json' && file.rel.startsWith('examples/'));
|
|
223
235
|
if (fixtureExpectations.length < 6) errors.push(`at least six fixture contracts are required; found ${fixtureExpectations.length}`);
|
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { readFile, readdir, realpath, stat } from 'node:fs/promises';
|
|
4
|
+
import { lstat, readFile, readdir, realpath, stat } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const ROUTER_REL = '.agents/skills/engineering-standards';
|
|
11
|
+
const OWNERSHIP_FILE = 'generated-skill-set.json';
|
|
11
12
|
const REQUIRED_PROJECT_REFERENCES = [
|
|
12
13
|
'references/project/00-project-profile.md',
|
|
13
14
|
'references/project/01-module-map.md',
|
|
14
15
|
'references/project/02-decisions-and-exceptions.md',
|
|
16
|
+
'references/project/03-skill-map.md',
|
|
17
|
+
'references/project/04-source-and-template-map.md',
|
|
15
18
|
'references/project/review-checklist.md',
|
|
16
19
|
];
|
|
17
20
|
const ADAPTERS = new Set(['typescript', 'java', 'go', 'rust']);
|
|
21
|
+
const ROLES = new Set(['router', 'domain']);
|
|
18
22
|
|
|
19
23
|
function usage() {
|
|
20
24
|
return `Usage: node scripts/validate-generated-skill.mjs --root <project-root> [options]\n\n` +
|
|
21
|
-
`Validate
|
|
25
|
+
`Validate the Builder-owned project Skill Set and optional compatibility wrappers.\n\n` +
|
|
22
26
|
`Options:\n` +
|
|
23
27
|
` --root <path> Project root (required)\n` +
|
|
24
28
|
` --inventory <path> Project Inventory JSON below --root; auto-discover when omitted\n` +
|
|
25
|
-
` --strict
|
|
29
|
+
` --strict Reject generated language/framework content without project signals\n` +
|
|
26
30
|
` --help Show this help\n`;
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -37,7 +41,8 @@ function parseArgs(argv) {
|
|
|
37
41
|
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`);
|
|
38
42
|
if (arg === '--root') result.root = value;
|
|
39
43
|
else result.inventory = value;
|
|
40
|
-
i += 1;
|
|
44
|
+
i += 1;
|
|
45
|
+
continue;
|
|
41
46
|
}
|
|
42
47
|
throw new Error(`unknown argument: ${arg}`);
|
|
43
48
|
}
|
|
@@ -46,6 +51,7 @@ function parseArgs(argv) {
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
function toPosix(value) { return value.split(path.sep).join('/'); }
|
|
54
|
+
|
|
49
55
|
function isInside(root, candidate) {
|
|
50
56
|
const rel = path.relative(root, candidate);
|
|
51
57
|
return rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel));
|
|
@@ -64,24 +70,43 @@ function parseFrontmatter(text) {
|
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
function markdownLinks(text) {
|
|
67
|
-
const
|
|
73
|
+
const links = [];
|
|
68
74
|
const pattern = /\[[^\]]*\]\(([^)]+)\)/g;
|
|
69
75
|
let match;
|
|
70
|
-
while ((match = pattern.exec(text)) !== null)
|
|
71
|
-
|
|
76
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
77
|
+
let target = match[1].trim();
|
|
78
|
+
if (target.startsWith('<')) {
|
|
79
|
+
const close = target.indexOf('>');
|
|
80
|
+
if (close >= 0) target = target.slice(1, close);
|
|
81
|
+
} else {
|
|
82
|
+
const titleIndex = target.search(/\s+["']/);
|
|
83
|
+
if (titleIndex >= 0) target = target.slice(0, titleIndex);
|
|
84
|
+
}
|
|
85
|
+
links.push(target);
|
|
86
|
+
}
|
|
87
|
+
return links;
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
function isExternal(link) { return /^(?:[a-z]+:|#|\/\/)/i.test(link); }
|
|
75
91
|
|
|
92
|
+
function linkTarget(sourceAbs, link) {
|
|
93
|
+
const withoutAnchor = link.split('#')[0].split('?')[0];
|
|
94
|
+
if (!withoutAnchor) return null;
|
|
95
|
+
let decoded;
|
|
96
|
+
try { decoded = decodeURIComponent(withoutAnchor); }
|
|
97
|
+
catch { decoded = withoutAnchor; }
|
|
98
|
+
return path.resolve(path.dirname(sourceAbs), decoded);
|
|
99
|
+
}
|
|
100
|
+
|
|
76
101
|
async function collectTree(rootAbs) {
|
|
77
102
|
const files = [];
|
|
78
103
|
const directories = [];
|
|
79
|
-
async function visit(
|
|
80
|
-
const entries = await readdir(
|
|
81
|
-
directories.push({ abs: dir, entries });
|
|
104
|
+
async function visit(directory) {
|
|
105
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
82
106
|
entries.sort((a, b) => a.name.localeCompare(b.name, 'en'));
|
|
107
|
+
directories.push({ abs: directory, entries });
|
|
83
108
|
for (const entry of entries) {
|
|
84
|
-
const abs = path.join(
|
|
109
|
+
const abs = path.join(directory, entry.name);
|
|
85
110
|
if (entry.isSymbolicLink()) throw new Error(`symlink is not allowed in generated Skill: ${toPosix(path.relative(rootAbs, abs))}`);
|
|
86
111
|
if (entry.isDirectory()) { await visit(abs); continue; }
|
|
87
112
|
if (entry.isFile()) files.push(abs);
|
|
@@ -108,12 +133,27 @@ async function loadInventory(args, rootReal) {
|
|
|
108
133
|
return JSON.parse(await readFile(inventoryAbs, 'utf8'));
|
|
109
134
|
}
|
|
110
135
|
const discoverScript = path.join(path.dirname(fileURLToPath(import.meta.url)), 'discover-project.mjs');
|
|
111
|
-
const result = spawnSync(process.execPath, [discoverScript, '--root', rootReal], {
|
|
136
|
+
const result = spawnSync(process.execPath, [discoverScript, '--root', rootReal], {
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
timeout: 60000,
|
|
139
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
140
|
+
});
|
|
112
141
|
if (result.status !== 0) throw new Error(`automatic project discovery failed: ${(result.stderr || result.stdout).trim()}`);
|
|
113
142
|
return JSON.parse(result.stdout);
|
|
114
143
|
}
|
|
115
144
|
|
|
116
|
-
|
|
145
|
+
function validateExactKeys(value, allowed, label, errors) {
|
|
146
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
147
|
+
errors.push(`${label} must be an object`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
for (const key of Object.keys(value)) {
|
|
151
|
+
if (!allowed.has(key)) errors.push(`${label} contains unsupported field: ${key}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function validateWrapper(projectRoot, wrapperRel, ownedPaths, errors) {
|
|
156
|
+
if (ownedPaths.has(wrapperRel)) return;
|
|
117
157
|
const wrapperRoot = path.join(projectRoot, wrapperRel);
|
|
118
158
|
if (!existsSync(wrapperRoot)) return;
|
|
119
159
|
const info = await stat(wrapperRoot);
|
|
@@ -132,7 +172,6 @@ async function validateWrapper(projectRoot, wrapperRel, errors) {
|
|
|
132
172
|
if (bodyLines.length !== 1) errors.push(`${wrapperRel}/SKILL.md body must be exactly one non-empty routing line`);
|
|
133
173
|
if (!fm.body.includes('engineering-standards')) errors.push(`${wrapperRel}/SKILL.md does not route to engineering-standards`);
|
|
134
174
|
if (fm.body.length > 400) errors.push(`${wrapperRel}/SKILL.md contains too much content for a compatibility wrapper`);
|
|
135
|
-
if (/typescript-(?:-)?standards\/SKILL\.md/.test(fm.body) && !/engineering-standards/.test(fm.body)) errors.push(`${wrapperRel}/SKILL.md may form a wrapper loop`);
|
|
136
175
|
}
|
|
137
176
|
|
|
138
177
|
async function main() {
|
|
@@ -140,7 +179,8 @@ async function main() {
|
|
|
140
179
|
try { args = parseArgs(process.argv.slice(2)); }
|
|
141
180
|
catch (error) {
|
|
142
181
|
process.stderr.write(`validate-generated-skill: ${error.message}\n\n${usage()}`);
|
|
143
|
-
process.exitCode = 2;
|
|
182
|
+
process.exitCode = 2;
|
|
183
|
+
return;
|
|
144
184
|
}
|
|
145
185
|
if (args.help) { process.stdout.write(usage()); return; }
|
|
146
186
|
|
|
@@ -149,51 +189,109 @@ async function main() {
|
|
|
149
189
|
try {
|
|
150
190
|
const rootReal = await realpath(path.resolve(args.root));
|
|
151
191
|
if (!(await stat(rootReal)).isDirectory()) throw new Error('--root is not a directory');
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
192
|
+
|
|
193
|
+
const routerRoot = path.join(rootReal, ROUTER_REL);
|
|
194
|
+
if (!existsSync(routerRoot)) throw new Error(`${ROUTER_REL} is missing`);
|
|
195
|
+
const routerReal = await realpath(routerRoot);
|
|
196
|
+
if (!isInside(rootReal, routerReal)) throw new Error(`${ROUTER_REL} resolves outside project root`);
|
|
197
|
+
|
|
198
|
+
const ownershipPath = path.join(routerReal, OWNERSHIP_FILE);
|
|
199
|
+
if (!existsSync(ownershipPath)) throw new Error(`${ROUTER_REL}/${OWNERSHIP_FILE} is missing`);
|
|
200
|
+
let ownership;
|
|
201
|
+
try { ownership = JSON.parse(await readFile(ownershipPath, 'utf8')); }
|
|
202
|
+
catch (error) { throw new Error(`${ROUTER_REL}/${OWNERSHIP_FILE} is invalid JSON: ${error.message}`); }
|
|
203
|
+
|
|
204
|
+
validateExactKeys(ownership, new Set(['schema_version', 'generator', 'skills']), OWNERSHIP_FILE, errors);
|
|
205
|
+
if (ownership.schema_version !== 1) errors.push(`${OWNERSHIP_FILE} schema_version must be 1`);
|
|
206
|
+
if (ownership.generator !== 'engineering-standards-builder') errors.push(`${OWNERSHIP_FILE} generator must be engineering-standards-builder`);
|
|
207
|
+
if (!Array.isArray(ownership.skills) || ownership.skills.length === 0) errors.push(`${OWNERSHIP_FILE} skills must be a non-empty array`);
|
|
208
|
+
|
|
209
|
+
const entries = Array.isArray(ownership.skills) ? ownership.skills : [];
|
|
210
|
+
const names = new Set();
|
|
211
|
+
const ownedPaths = new Set();
|
|
212
|
+
const routers = [];
|
|
213
|
+
for (const [index, entry] of entries.entries()) {
|
|
214
|
+
const label = `${OWNERSHIP_FILE} skills[${index}]`;
|
|
215
|
+
validateExactKeys(entry, new Set(['name', 'path', 'role']), label, errors);
|
|
216
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry?.name ?? '')) errors.push(`${label} name must be kebab-case`);
|
|
217
|
+
const expectedPath = `.agents/skills/${entry?.name}`;
|
|
218
|
+
if (entry?.path !== expectedPath) errors.push(`${label} path must equal ${expectedPath}`);
|
|
219
|
+
if (!ROLES.has(entry?.role)) errors.push(`${label} role must be router or domain`);
|
|
220
|
+
if (names.has(entry?.name)) errors.push(`${label} duplicates name ${entry?.name}`);
|
|
221
|
+
if (ownedPaths.has(entry?.path)) errors.push(`${label} duplicates path ${entry?.path}`);
|
|
222
|
+
names.add(entry?.name);
|
|
223
|
+
ownedPaths.add(entry?.path);
|
|
224
|
+
if (entry?.role === 'router') routers.push(entry);
|
|
225
|
+
}
|
|
226
|
+
if (routers.length !== 1) errors.push(`${OWNERSHIP_FILE} must contain exactly one router`);
|
|
227
|
+
else if (routers[0].name !== 'engineering-standards' || routers[0].path !== ROUTER_REL) {
|
|
228
|
+
errors.push(`${OWNERSHIP_FILE} router must be engineering-standards at ${ROUTER_REL}`);
|
|
159
229
|
}
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
230
|
+
|
|
231
|
+
const ownedRoots = [];
|
|
232
|
+
const ownedMainSkills = new Map();
|
|
233
|
+
const allOwnedFiles = [];
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
if (typeof entry?.path !== 'string' || typeof entry?.name !== 'string') continue;
|
|
236
|
+
const skillRoot = path.resolve(rootReal, entry.path);
|
|
237
|
+
if (!isInside(rootReal, skillRoot)) { errors.push(`${entry.path} escapes project root`); continue; }
|
|
238
|
+
if (!existsSync(skillRoot)) { errors.push(`owned Skill is missing: ${entry.path}`); continue; }
|
|
239
|
+
const skillReal = await realpath(skillRoot);
|
|
240
|
+
if (!isInside(rootReal, skillReal)) { errors.push(`${entry.path} resolves outside project root`); continue; }
|
|
241
|
+
const info = await stat(skillReal);
|
|
242
|
+
if (!info.isDirectory()) { errors.push(`${entry.path} must be a directory`); continue; }
|
|
243
|
+
|
|
244
|
+
const tree = await collectTree(skillReal);
|
|
245
|
+
ownedRoots.push(skillReal);
|
|
246
|
+
for (const directory of tree.directories) {
|
|
247
|
+
if (directory.entries.length === 0) errors.push(`empty generated directory: ${toPosix(path.relative(rootReal, directory.abs))}`);
|
|
248
|
+
}
|
|
249
|
+
allOwnedFiles.push(...tree.files);
|
|
250
|
+
const mainSkill = path.join(skillReal, 'SKILL.md');
|
|
251
|
+
ownedMainSkills.set(entry.name, mainSkill);
|
|
252
|
+
if (!tree.files.includes(mainSkill)) { errors.push(`${entry.path}/SKILL.md is missing`); continue; }
|
|
164
253
|
const text = await readFile(mainSkill, 'utf8');
|
|
165
254
|
const fm = parseFrontmatter(text);
|
|
166
|
-
if (!fm) errors.push(`${
|
|
255
|
+
if (!fm) errors.push(`${entry.path}/SKILL.md frontmatter is malformed`);
|
|
167
256
|
else {
|
|
168
|
-
if (fm.values.name !==
|
|
169
|
-
if (!fm.values.description) errors.push(
|
|
257
|
+
if (fm.values.name !== entry.name) errors.push(`${entry.path}/SKILL.md name must be ${entry.name}`);
|
|
258
|
+
if (!fm.values.description) errors.push(`${entry.path}/SKILL.md description is missing`);
|
|
170
259
|
}
|
|
171
|
-
if (/typescript-(?:-)?standards\/SKILL\.md/.test(text)) errors.push('canonical Skill must not route back to a compatibility alias');
|
|
172
260
|
}
|
|
173
261
|
|
|
262
|
+
const routerFiles = new Set(allOwnedFiles.filter((file) => isInside(routerReal, file)));
|
|
174
263
|
for (const required of REQUIRED_PROJECT_REFERENCES) {
|
|
175
|
-
|
|
176
|
-
if (!fileSet.has(abs)) errors.push(`required generated reference is missing: ${required}`);
|
|
264
|
+
if (!routerFiles.has(path.join(routerReal, required))) errors.push(`required router reference is missing: ${required}`);
|
|
177
265
|
}
|
|
178
266
|
|
|
179
|
-
|
|
180
|
-
|
|
267
|
+
const routerMain = path.join(routerReal, 'SKILL.md');
|
|
268
|
+
const routerLinkTargets = new Set();
|
|
269
|
+
const evidenceLinks = new Set();
|
|
270
|
+
const sourceMapPath = path.join(routerReal, 'references/project/04-source-and-template-map.md');
|
|
271
|
+
|
|
272
|
+
for (const file of allOwnedFiles.filter((item) => item.endsWith('.md'))) {
|
|
273
|
+
const rel = toPosix(path.relative(rootReal, file));
|
|
181
274
|
const text = await readFile(file, 'utf8');
|
|
182
275
|
if (/\{\{[A-Z0-9_]+\}\}|<PROJECT_NAME>|<REPOSITORY_ROOT>/.test(text)) errors.push(`${rel}: unresolved template placeholder`);
|
|
183
|
-
for (
|
|
276
|
+
for (const link of markdownLinks(text)) {
|
|
184
277
|
if (isExternal(link)) continue;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (!
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
278
|
+
const target = linkTarget(file, link);
|
|
279
|
+
if (!target) continue;
|
|
280
|
+
if (!isInside(rootReal, target)) { errors.push(`${rel}: link escapes project root: ${link}`); continue; }
|
|
281
|
+
if (!existsSync(target)) { errors.push(`${rel}: broken project link: ${link}`); continue; }
|
|
282
|
+
const directInfo = await lstat(target);
|
|
283
|
+
if (directInfo.isSymbolicLink()) { errors.push(`${rel}: link target must not be a symlink: ${link}`); continue; }
|
|
284
|
+
const targetReal = await realpath(target);
|
|
285
|
+
if (!isInside(rootReal, targetReal)) { errors.push(`${rel}: link resolves outside project root: ${link}`); continue; }
|
|
286
|
+
if (file === routerMain) routerLinkTargets.add(targetReal);
|
|
287
|
+
if (file === sourceMapPath && !ownedRoots.some((ownedRoot) => isInside(ownedRoot, targetReal))) {
|
|
288
|
+
if (!directInfo.isFile()) errors.push(`${rel}: project evidence link must target a file: ${link}`);
|
|
289
|
+
else evidenceLinks.add(targetReal);
|
|
192
290
|
}
|
|
193
|
-
if (!existsSync(target)) errors.push(`${rel}: broken local link: ${link}`);
|
|
194
291
|
}
|
|
195
292
|
|
|
196
|
-
|
|
293
|
+
const routerProjectPrefix = toPosix(path.relative(rootReal, path.join(routerReal, 'references/project')));
|
|
294
|
+
if (!rel.startsWith(`${routerProjectPrefix}/`)) {
|
|
197
295
|
for (const levelMatch of text.matchAll(/^Level:\s*(MUST|SHOULD)\s*$/gm)) {
|
|
198
296
|
const section = sectionForOffset(text, levelMatch.index);
|
|
199
297
|
for (const field of ['Scope:', 'Source:', 'Rule:', 'Verification:']) {
|
|
@@ -203,9 +301,26 @@ async function main() {
|
|
|
203
301
|
}
|
|
204
302
|
}
|
|
205
303
|
|
|
304
|
+
if (existsSync(sourceMapPath) && evidenceLinks.size === 0) {
|
|
305
|
+
errors.push('04-source-and-template-map.md must link to at least one real project file outside the generated Skill Set');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const entry of entries.filter((item) => item?.role === 'domain')) {
|
|
309
|
+
const mainSkill = ownedMainSkills.get(entry.name);
|
|
310
|
+
if (mainSkill && existsSync(mainSkill)) {
|
|
311
|
+
const mainReal = await realpath(mainSkill);
|
|
312
|
+
if (!routerLinkTargets.has(mainReal)) errors.push(`router SKILL.md does not link to domain Skill: ${entry.name}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (existsSync(routerMain)) {
|
|
317
|
+
const routerText = await readFile(routerMain, 'utf8');
|
|
318
|
+
if (/typescript-(?:-)?standards\/SKILL\.md/.test(routerText)) errors.push('router Skill must not route back to a compatibility alias');
|
|
319
|
+
}
|
|
320
|
+
|
|
206
321
|
const inventory = await loadInventory(args, rootReal);
|
|
207
322
|
const detectedAdapters = new Set(inventory?.summary?.adapters ?? []);
|
|
208
|
-
const generatedReferenceRoot = path.join(
|
|
323
|
+
const generatedReferenceRoot = path.join(routerReal, 'references');
|
|
209
324
|
const generatedAdapters = new Set();
|
|
210
325
|
if (existsSync(generatedReferenceRoot)) {
|
|
211
326
|
for (const entry of await readdir(generatedReferenceRoot, { withFileTypes: true })) {
|
|
@@ -218,12 +333,6 @@ async function main() {
|
|
|
218
333
|
if (args.strict) errors.push(message); else warnings.push(message);
|
|
219
334
|
}
|
|
220
335
|
}
|
|
221
|
-
for (const adapter of detectedAdapters) {
|
|
222
|
-
if (!generatedAdapters.has(adapter)) {
|
|
223
|
-
const message = `detected adapter ${adapter} is missing from generated references`;
|
|
224
|
-
if (args.strict) errors.push(message); else warnings.push(message);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
336
|
|
|
228
337
|
const detectedFrameworks = new Set(inventory?.summary?.frameworks ?? []);
|
|
229
338
|
const frameworkContracts = [
|
|
@@ -232,17 +341,11 @@ async function main() {
|
|
|
232
341
|
['spring-boot', 'java', 'references/java/frameworks/spring-boot.md'],
|
|
233
342
|
];
|
|
234
343
|
for (const [framework, requiredAdapter, rel] of frameworkContracts) {
|
|
235
|
-
const present =
|
|
236
|
-
|
|
237
|
-
const adapterDetected = detectedAdapters.has(requiredAdapter);
|
|
238
|
-
if (present && (!frameworkDetected || !adapterDetected)) {
|
|
344
|
+
const present = routerFiles.has(path.join(routerReal, rel));
|
|
345
|
+
if (present && (!detectedFrameworks.has(framework) || !detectedAdapters.has(requiredAdapter))) {
|
|
239
346
|
const message = `generated framework rule ${framework} has no matching ${requiredAdapter} adapter and framework signal`;
|
|
240
347
|
if (args.strict) errors.push(message); else warnings.push(message);
|
|
241
348
|
}
|
|
242
|
-
if (!present && frameworkDetected && adapterDetected) {
|
|
243
|
-
const message = `detected framework ${framework} is missing generated rule ${rel}`;
|
|
244
|
-
if (args.strict) errors.push(message); else warnings.push(message);
|
|
245
|
-
}
|
|
246
349
|
}
|
|
247
350
|
|
|
248
351
|
for (const wrapper of [
|
|
@@ -250,16 +353,15 @@ async function main() {
|
|
|
250
353
|
'.agents/skills/typescript--standards',
|
|
251
354
|
'.claude/skills/engineering-standards',
|
|
252
355
|
'.claude/skills/typescript-standards',
|
|
253
|
-
]) await validateWrapper(rootReal, wrapper, errors);
|
|
356
|
+
]) await validateWrapper(rootReal, wrapper, ownedPaths, errors);
|
|
254
357
|
|
|
255
|
-
// Detect substantial duplicate canonical copies under known agent roots.
|
|
256
358
|
for (const baseRel of ['.agents/skills', '.claude/skills']) {
|
|
257
359
|
const base = path.join(rootReal, baseRel);
|
|
258
360
|
if (!existsSync(base)) continue;
|
|
259
361
|
for (const entry of await readdir(base, { withFileTypes: true })) {
|
|
260
362
|
if (!entry.isDirectory()) continue;
|
|
261
363
|
const candidate = path.join(base, entry.name, 'SKILL.md');
|
|
262
|
-
if (!existsSync(candidate) || candidate ===
|
|
364
|
+
if (!existsSync(candidate) || candidate === routerMain) continue;
|
|
263
365
|
const text = await readFile(candidate, 'utf8');
|
|
264
366
|
const fm = parseFrontmatter(text);
|
|
265
367
|
if (fm?.values?.name === 'engineering-standards' && fm.body.trim().length > 400) {
|
|
@@ -268,7 +370,7 @@ async function main() {
|
|
|
268
370
|
}
|
|
269
371
|
}
|
|
270
372
|
|
|
271
|
-
if ((inventory?.scan?.truncated ?? false) === true) warnings.push('Project Inventory scan was truncated;
|
|
373
|
+
if ((inventory?.scan?.truncated ?? false) === true) warnings.push('Project Inventory scan was truncated; source coverage requires manual verification');
|
|
272
374
|
for (const item of inventory?.conflicts ?? []) warnings.push(`Project Inventory conflict: ${item.scope ?? 'repository'} ${item.type}`);
|
|
273
375
|
} catch (error) {
|
|
274
376
|
errors.push(error.stack ?? error.message);
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 项目 Skill Set 模板索引
|
|
2
2
|
|
|
3
|
-
模板只用于生成项目专属 Skill
|
|
3
|
+
模板只用于生成项目专属 Skill Set;占位符必须在写入目标前全部替换。模板提供最小结构,不要求生成固定数量的领域 Skill 或 references。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 根路由 Skill
|
|
6
6
|
|
|
7
7
|
- [主入口](project-skill/SKILL.md.template)
|
|
8
|
+
- [Builder 所有权清单](project-skill/generated-skill-set.json.template)
|
|
8
9
|
- [项目画像](project-skill/references/project/00-project-profile.md.template)
|
|
9
10
|
- [模块地图](project-skill/references/project/01-module-map.md.template)
|
|
10
11
|
- [决策与例外](project-skill/references/project/02-decisions-and-exceptions.md.template)
|
|
12
|
+
- [Skill 地图](project-skill/references/project/03-skill-map.md.template)
|
|
13
|
+
- [源码与模板地图](project-skill/references/project/04-source-and-template-map.md.template)
|
|
11
14
|
- [评审清单](project-skill/references/project/review-checklist.md.template)
|
|
12
15
|
|
|
16
|
+
## 可选领域 Skill
|
|
17
|
+
|
|
18
|
+
- [领域入口](domain-skill/SKILL.md.template)
|
|
19
|
+
|
|
20
|
+
只有通过 Skill 拆分门时才创建领域 Skill。领域 references 由真实证据决定,不提供固定目录模板。
|
|
21
|
+
|
|
13
22
|
## 单向兼容入口
|
|
14
23
|
|
|
15
24
|
- [Agents:typescript-standards](compatibility/agents-typescript-standards.md)
|
package/template/skills/engineering-standards-builder/templates/domain-skill/SKILL.md.template
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: {{SKILL_NAME}}
|
|
3
|
+
description: {{DESCRIPTION}}
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# {{TITLE}}
|
|
7
|
+
|
|
8
|
+
处理以下触发场景时使用本 Skill:
|
|
9
|
+
|
|
10
|
+
{{TRIGGERS}}
|
|
11
|
+
|
|
12
|
+
先读取项目根路由 `../engineering-standards/SKILL.md` 中的全局边界与决策。本 Skill 只负责 {{DOMAIN_RESPONSIBILITY}},不重新定义跨域规范。
|
|
13
|
+
|
|
14
|
+
## 适用范围
|
|
15
|
+
|
|
16
|
+
{{SCOPE_AND_EXCLUSIONS}}
|
|
17
|
+
|
|
18
|
+
## 实现流程
|
|
19
|
+
|
|
20
|
+
{{WORKFLOW}}
|
|
21
|
+
|
|
22
|
+
## 项目事实源
|
|
23
|
+
|
|
24
|
+
{{SOURCE_AND_TEMPLATE_ROUTES}}
|
|
25
|
+
|
|
26
|
+
读取真实源码、模板、消费者和测试;不要把它们复制进本 Skill。每个来源都要说明适用条件、输出位置、手工集成和验证。
|
|
27
|
+
|
|
28
|
+
## 完成检查
|
|
29
|
+
|
|
30
|
+
{{VERIFICATION}}
|
|
31
|
+
|
|
32
|
+
无法由项目证据确认的事项标记 `pending-decision`,不要用 Builder 默认补齐。
|
package/template/skills/engineering-standards-builder/templates/project-skill/SKILL.md.template
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: engineering-standards
|
|
3
|
-
description: 为 {{PROJECT_NAME}}
|
|
3
|
+
description: 为 {{PROJECT_NAME}} 的代码、目录、测试、构建、依赖、API 与交付变更提供项目专属规范和领域 Skill 路由。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# {{PROJECT_NAME}} Engineering Standards
|
|
7
7
|
|
|
8
|
-
本 Skill
|
|
8
|
+
本 Skill 是项目工程规范的权威入口。先确定变更 scope,再读取最小充分的项目证据或领域 Skill;同级领域 Skill 不得覆盖这里的决策与边界。
|
|
9
9
|
|
|
10
10
|
## 每次执行
|
|
11
11
|
|
|
12
12
|
1. 读取 [项目画像](references/project/00-project-profile.md) 和 [模块地图](references/project/01-module-map.md)。
|
|
13
|
-
2. 将当前任务映射到 repository、module、language、framework、runtime 或
|
|
14
|
-
3.
|
|
15
|
-
4.
|
|
16
|
-
5. 读取 [决策与例外](references/project/02-decisions-and-exceptions.md)
|
|
13
|
+
2. 将当前任务映射到 repository、module、language、framework、runtime、path 或 public-api scope。
|
|
14
|
+
3. 读取 [Skill 地图](references/project/03-skill-map.md),判断是否应转到领域 Skill:{{DOMAIN_SKILL_ROUTES}}。
|
|
15
|
+
4. 按任务读取 [源码与模板地图](references/project/04-source-and-template-map.md) 中的真实代码、模板、消费者和测试,不复制这些事实源。
|
|
16
|
+
5. 读取 [决策与例外](references/project/02-decisions-and-exceptions.md),区分 current、target、migration、pending-decision 和未到期 exception。
|
|
17
17
|
6. 实现前检查 public API、依赖方向、安全、错误/取消/资源和测试影响。
|
|
18
|
-
7.
|
|
18
|
+
7. 实现后运行该 scope 的真实质量门禁,并按 [评审清单](references/project/review-checklist.md) 报告。
|
|
19
|
+
|
|
20
|
+
## 项目规则路由
|
|
21
|
+
|
|
22
|
+
{{PROJECT_RULE_ROUTES}}
|
|
19
23
|
|
|
20
24
|
## 约束
|
|
21
25
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
26
|
+
- 项目代码、测试、配置、CI 和已声明模板是事实源;通用建议不能伪装成项目规范。
|
|
27
|
+
- MUST 只有在 Verification 可执行或有精确 review 证据时成立。
|
|
28
|
+
- generated/vendor/frozen scope 不手工修改。
|
|
29
|
+
- 不通过删除测试、放宽核心配置或扩大例外获得通过。
|
|
25
30
|
- 新代码遵循 Target;存量偏差按 Migration/Ratchet 处理。
|
|
26
|
-
-
|
|
31
|
+
- 事实变化时先更新项目画像、来源地图和决策,再更新规范。
|