@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.
Files changed (128) hide show
  1. package/README.md +10 -6
  2. package/dist/src/cli.js +12 -1
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/doctor.d.ts +10 -0
  5. package/dist/src/doctor.js +70 -0
  6. package/dist/src/doctor.js.map +1 -0
  7. package/dist/src/index.js +105 -56
  8. package/dist/src/index.js.map +1 -1
  9. package/dist/src/kernel.d.ts +98 -0
  10. package/dist/src/kernel.js +29 -0
  11. package/dist/src/kernel.js.map +1 -0
  12. package/dist/src/refresh.js +55 -24
  13. package/dist/src/refresh.js.map +1 -1
  14. package/dist/src/structured.d.ts +2 -2
  15. package/dist/src/structured.js +114 -241
  16. package/dist/src/structured.js.map +1 -1
  17. package/package.json +3 -2
  18. package/template/.speculo/README.md +1 -1
  19. package/template/.speculo/capabilities.json +14 -0
  20. package/template/.speculo/kernel/README.md +10 -0
  21. package/template/.speculo/kernel/capability-profile.schema.json +14 -0
  22. package/template/.speculo/kernel/checkpoint.schema.json +7 -0
  23. package/template/.speculo/kernel/trace-event.schema.json +8 -0
  24. package/template/.speculo/kernel/workflow-manifest.schema.json +13 -0
  25. package/template/.speculo/kernel.json +10 -0
  26. package/template/.speculo/refresh-contract.json +4 -1
  27. package/template/AGENTS.md +0 -3
  28. package/template/canonical/canonical-specdev-goal-plan.md +13 -7
  29. package/template/canonical/canonical-specdev-grill-with-docs.md +1 -1
  30. package/template/canonical/canonical-specdev-orchestrate-implementation.md +29 -11
  31. package/template/canonical/canonical-specdev-spec.md +1 -1
  32. package/template/canonical/canonical-specdev-tickets.md +38 -4
  33. package/template/commands/archive-and-consolidate.md +13 -40
  34. package/template/commands/status.md +3 -3
  35. package/template/skills/engineering-standards-builder/README.md +6 -28
  36. package/template/skills/engineering-standards-builder/SKILL.md +88 -163
  37. package/template/skills/engineering-standards-builder/examples/README.md +2 -0
  38. package/template/skills/engineering-standards-builder/manifest.txt +4 -0
  39. package/template/skills/engineering-standards-builder/references/rules/00-governance-and-precedence.md +22 -29
  40. package/template/skills/engineering-standards-builder/references/rules/01-project-discovery.md +26 -59
  41. package/template/skills/engineering-standards-builder/references/rules/02-evidence-topology-and-scope.md +28 -55
  42. package/template/skills/engineering-standards-builder/references/rules/03-interview-and-decisions.md +4 -3
  43. package/template/skills/engineering-standards-builder/references/rules/14-generation-contract.md +64 -80
  44. package/template/skills/engineering-standards-builder/references/rules/15-validation-contract.md +24 -47
  45. package/template/skills/engineering-standards-builder/references/rules/16-language-adapter-contract.md +11 -48
  46. package/template/skills/engineering-standards-builder/references/rules/README.md +3 -3
  47. package/template/skills/engineering-standards-builder/scripts/self-test.mjs +51 -5
  48. package/template/skills/engineering-standards-builder/scripts/validate-builder.mjs +16 -4
  49. package/template/skills/engineering-standards-builder/scripts/validate-generated-skill.mjs +166 -64
  50. package/template/skills/engineering-standards-builder/templates/README.md +12 -3
  51. package/template/skills/engineering-standards-builder/templates/domain-skill/SKILL.md.template +32 -0
  52. package/template/skills/engineering-standards-builder/templates/project-skill/SKILL.md.template +16 -11
  53. package/template/skills/engineering-standards-builder/templates/project-skill/generated-skill-set.json.template +7 -0
  54. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/00-project-profile.md.template +3 -1
  55. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/01-module-map.md.template +4 -0
  56. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/02-decisions-and-exceptions.md.template +1 -1
  57. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/03-skill-map.md.template +19 -0
  58. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/04-source-and-template-map.md.template +22 -0
  59. package/template/skills/engineering-standards-builder/templates/project-skill/references/project/review-checklist.md.template +2 -0
  60. package/template/skills/git-history-squash/SKILL.md +100 -0
  61. package/template/skills/git-history-squash/assets/request-template.json +18 -0
  62. package/template/skills/git-history-squash/references/recovery-contract.md +50 -0
  63. package/template/skills/git-history-squash/references/rewrite-contract.md +123 -0
  64. package/template/skills/git-history-squash/references/submodule-contract.md +54 -0
  65. package/template/skills/git-history-squash/scripts/git-history-squash.mjs +1171 -0
  66. package/template/workflows/learning/A-archive/A-archive.md +25 -0
  67. package/template/workflows/learning/A-assess-and-plan/A-assess-and-plan.md +17 -23
  68. package/template/workflows/learning/A-assess-and-plan/background-template.md +13 -0
  69. package/template/workflows/learning/A-assess-and-plan/change-status-template.json +21 -14
  70. package/template/workflows/learning/A-assess-and-plan/course-template.md +28 -0
  71. package/template/workflows/learning/C-consolidate/C-consolidate.md +33 -0
  72. package/template/workflows/learning/H-homework/H-homework.md +31 -0
  73. package/template/workflows/learning/H-homework/homework-template.md +63 -0
  74. package/template/workflows/learning/I-init-setup/I-init-setup.md +14 -19
  75. package/template/workflows/learning/I-init-setup/context-index-template.md +3 -3
  76. package/template/workflows/learning/I-init-setup/learner-profile-template.md +12 -10
  77. package/template/workflows/learning/I-init-setup/review-index-template.md +1 -1
  78. package/template/workflows/learning/INDEX.md +7 -9
  79. package/template/workflows/learning/L-lesson/L-lesson.md +32 -0
  80. package/template/workflows/learning/L-lesson/lesson-template.md +51 -0
  81. package/template/workflows/learning/R-review/R-review.md +13 -18
  82. package/template/workflows/learning/R-review/review-template.md +13 -6
  83. package/template/workflows/learning/README.md +86 -81
  84. package/template/workflows/learning/_state/status.json +1 -1
  85. package/template/workflows/learning/common/rules/artifact-contract.md +11 -25
  86. package/template/workflows/learning/common/rules/assessment-policy.md +9 -4
  87. package/template/workflows/learning/common/rules/knowledge-organization.md +4 -6
  88. package/template/workflows/learning/common/rules/mastery-policy.md +3 -19
  89. package/template/workflows/learning/common/rules/path-reference-contract.md +3 -5
  90. package/template/workflows/learning/common/rules/teaching-policy.md +9 -9
  91. package/template/workflows/learning/common/schemas/change-status.schema.json +46 -18
  92. package/template/workflows/learning/common/schemas/status.schema.json +25 -24
  93. package/template/workflows/learning/common/skills/topic-synthesis/SKILL.md +17 -0
  94. package/template/workflows/learning/common/skills/topic-synthesis/claim-template.md +15 -0
  95. package/template/workflows/learning/common/tools/relocate-learning.mjs +208 -0
  96. package/template/workflows/learning/common/tools/validate-learning.mjs +195 -297
  97. package/template/workflows/learning/manifest.json +1 -0
  98. package/template/workflows/learning/runtime-contract.json +3 -2
  99. package/template/workflows/ops/manifest.json +1 -0
  100. package/template/workflows/person/manifest.json +1 -0
  101. package/template/workflows/specdev/I-implement/I-implement.md +12 -4
  102. package/template/workflows/specdev/I-implement/execution-preflight.md +4 -0
  103. package/template/workflows/specdev/README.md +1 -1
  104. package/template/workflows/specdev/T-tickets/T-tickets.md +16 -3
  105. package/template/workflows/specdev/T-tickets/ticket-readiness.md +3 -0
  106. package/template/workflows/specdev/T-tickets/ticket-template.md +3 -0
  107. package/template/workflows/specdev/T-tickets/tickets-map-template.md +15 -0
  108. package/template/workflows/specdev/common/rules/artifact-contract.md +1 -1
  109. package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +4 -2
  110. package/template/workflows/specdev/common/skills/subagent-delivery/references/external-web-subagent.md +2 -1
  111. package/template/workflows/specdev/common/skills/subagent-delivery/references/native-subagent.md +2 -1
  112. package/template/workflows/specdev/common/skills/subagent-delivery/references/source-package.md +4 -2
  113. package/template/workflows/specdev/common/tools/validate-specdev.mjs +143 -3
  114. package/template/workflows/specdev/manifest.json +1 -0
  115. package/template/workflows/workflow-manifest.schema.json +14 -0
  116. package/template/workflows/learning/A-archive-and-consolidate/A-archive-and-consolidate.md +0 -38
  117. package/template/workflows/learning/A-archive-and-consolidate/promotion-plan-template.md +0 -23
  118. package/template/workflows/learning/A-assess-and-plan/learning-plan-template.md +0 -28
  119. package/template/workflows/learning/E-eli5/E-eli5.md +0 -37
  120. package/template/workflows/learning/E-eli5/lesson-template.md +0 -31
  121. package/template/workflows/learning/P-practice/P-practice.md +0 -34
  122. package/template/workflows/learning/P-practice/practice-template.md +0 -16
  123. package/template/workflows/learning/Q-quiz/Q-quiz.md +0 -34
  124. package/template/workflows/learning/Q-quiz/quiz-artifact-template.md +0 -16
  125. package/template/workflows/learning/common/skills/knowledge-promotion/SKILL.md +0 -46
  126. package/template/workflows/learning/common/skills/knowledge-promotion/domain-index-template.md +0 -6
  127. package/template/workflows/learning/common/skills/knowledge-promotion/domain-overview-template.md +0 -19
  128. 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 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), [review](references/project/review-checklist.md), [rules](references/rules/core.md), [TypeScript](references/typescript/language.md), and [Vue](references/typescript/frameworks/vue.md).\n`);
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
- return canonical;
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' && !rel.endsWith('/.DS_Store')).sort((a, b) => a.localeCompare(b, 'en')).join('\n')}\n`;
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 CANONICAL_REL = '.agents/skills/engineering-standards';
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 .agents/skills/engineering-standards and optional compatibility wrappers.\n\n` +
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 Require every detected adapter/framework and reject unselected ones\n` +
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; continue;
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 result = [];
73
+ const links = [];
68
74
  const pattern = /\[[^\]]*\]\(([^)]+)\)/g;
69
75
  let match;
70
- while ((match = pattern.exec(text)) !== null) result.push(match[1].trim());
71
- return result;
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(dir) {
80
- const entries = await readdir(dir, { withFileTypes: true });
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(dir, entry.name);
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], { encoding: 'utf8', timeout: 60000, maxBuffer: 64 * 1024 * 1024 });
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
- async function validateWrapper(projectRoot, wrapperRel, errors) {
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; return;
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
- const canonicalRoot = path.join(rootReal, CANONICAL_REL);
153
- if (!existsSync(canonicalRoot)) throw new Error(`${CANONICAL_REL} is missing`);
154
- const canonicalReal = await realpath(canonicalRoot);
155
- if (!isInside(rootReal, canonicalReal)) throw new Error(`${CANONICAL_REL} resolves outside project root`);
156
- const { files, directories } = await collectTree(canonicalReal);
157
- for (const directory of directories) {
158
- if (directory.entries.length === 0) errors.push(`empty generated directory: ${toPosix(path.relative(rootReal, directory.abs))}`);
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
- const fileSet = new Set(files);
161
- const mainSkill = path.join(canonicalReal, 'SKILL.md');
162
- if (!fileSet.has(mainSkill)) errors.push(`${CANONICAL_REL}/SKILL.md is missing`);
163
- else {
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(`${CANONICAL_REL}/SKILL.md frontmatter is malformed`);
255
+ if (!fm) errors.push(`${entry.path}/SKILL.md frontmatter is malformed`);
167
256
  else {
168
- if (fm.values.name !== 'engineering-standards') errors.push(`canonical frontmatter name must be engineering-standards, found ${fm.values.name ?? '<missing>'}`);
169
- if (!fm.values.description) errors.push('canonical frontmatter description is missing');
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
- const abs = path.join(canonicalReal, required);
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
- for (const file of files.filter((item) => item.endsWith('.md'))) {
180
- const rel = toPosix(path.relative(canonicalReal, file));
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 (let link of markdownLinks(text)) {
276
+ for (const link of markdownLinks(text)) {
184
277
  if (isExternal(link)) continue;
185
- if (link.startsWith('<') && link.endsWith('>')) link = link.slice(1, -1);
186
- const targetPart = link.split('#')[0].split('?')[0];
187
- if (!targetPart) continue;
188
- const target = path.resolve(path.dirname(file), decodeURIComponent(targetPart));
189
- if (!isInside(canonicalReal, target)) {
190
- errors.push(`${rel}: link escapes canonical Skill: ${link}`);
191
- continue;
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
- if (!rel.startsWith('references/project/')) {
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(canonicalReal, 'references');
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 = fileSet.has(path.join(canonicalReal, rel));
236
- const frameworkDetected = detectedFrameworks.has(framework);
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 === mainSkill) continue;
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; adapter selection requires manual verification');
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
- ## Canonical Skill
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)
@@ -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 默认补齐。
@@ -1,26 +1,31 @@
1
1
  ---
2
2
  name: engineering-standards
3
- description: 为 {{PROJECT_NAME}} 的实现、重构、评审与交付提供按模块路由的工程规范。处理代码、目录、测试、构建、依赖、API CI 变化前使用。
3
+ description: 为 {{PROJECT_NAME}} 的代码、目录、测试、构建、依赖、API 与交付变更提供项目专属规范和领域 Skill 路由。
4
4
  ---
5
5
 
6
6
  # {{PROJECT_NAME}} Engineering Standards
7
7
 
8
- 本 Skill 只描述本项目已确认的规范。先确定变更 scope,再加载最小充分 references;不要把一个模块的语言或框架规则扩散到其他模块。
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 或 path scope。
14
- 3. 读取与变更相关的通用规则:{{COMMON_RULE_LINKS}}。
15
- 4. 按模块地图只读取适用语言/框架规则:{{ADAPTER_ROUTE_LINKS}}。
16
- 5. 读取 [决策与例外](references/project/02-decisions-and-exceptions.md),确认 current、target、migration 与未到期 exception。
13
+ 2. 将当前任务映射到 repository、module、language、framework、runtime、pathpublic-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. 实现后运行项目画像中该 scope 的真实质量门禁,并按 [评审清单](references/project/review-checklist.md) 报告结果。
18
+ 7. 实现后运行该 scope 的真实质量门禁,并按 [评审清单](references/project/review-checklist.md) 报告。
19
+
20
+ ## 项目规则路由
21
+
22
+ {{PROJECT_RULE_ROUTES}}
19
23
 
20
24
  ## 约束
21
25
 
22
- - MUST 规则只有在 Verification 可执行或有精确 review 证据时成立。
23
- - 不适用规则不加载;generated/vendor/frozen scope 不手工修改。
24
- - 不通过删除测试、关闭核心规则、放宽编译配置或扩大例外获得通过。
26
+ - 项目代码、测试、配置、CI 和已声明模板是事实源;通用建议不能伪装成项目规范。
27
+ - MUST 只有在 Verification 可执行或有精确 review 证据时成立。
28
+ - generated/vendor/frozen scope 不手工修改。
29
+ - 不通过删除测试、放宽核心配置或扩大例外获得通过。
25
30
  - 新代码遵循 Target;存量偏差按 Migration/Ratchet 处理。
26
- - 项目事实变化时更新 Project Profile/Module Map,再更新规则,不静默猜测。
31
+ - 事实变化时先更新项目画像、来源地图和决策,再更新规范。