@namewta/speculo 0.8.11 → 0.8.13
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 +2 -1
- package/package.json +1 -1
- 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/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/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
|
@@ -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
|
+
- 事实变化时先更新项目画像、来源地图和决策,再更新规范。
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
{{EVIDENCE_SUMMARY}}
|
|
13
13
|
|
|
14
|
+
证据使用项目根相对路径。扫描结果只作基线,重要结论必须由源码、配置、CI、测试、消费者或已声明模板复核。
|
|
15
|
+
|
|
14
16
|
## 工具链与质量门禁
|
|
15
17
|
|
|
16
18
|
{{QUALITY_GATES}}
|
|
17
19
|
|
|
18
|
-
每个命令必须包含 scope、working directory、来源和当前状态(active/planned
|
|
20
|
+
每个命令必须包含 scope、working directory、来源和当前状态(active/planned)。`planned` 不能作为已通过门禁。
|
|
19
21
|
|
|
20
22
|
## 排除与冻结区域
|
|
21
23
|
|
|
@@ -18,6 +18,8 @@ quality gates
|
|
|
18
18
|
evidence/confidence
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
不要因根目录 manifest 或单个扩展名把语言/框架规则扩散到其他模块。
|
|
22
|
+
|
|
21
23
|
## 依赖方向
|
|
22
24
|
|
|
23
25
|
{{DEPENDENCY_DIRECTIONS}}
|
|
@@ -25,3 +27,5 @@ evidence/confidence
|
|
|
25
27
|
## 路由
|
|
26
28
|
|
|
27
29
|
{{MODULE_RULE_ROUTES}}
|
|
30
|
+
|
|
31
|
+
领域 Skill 的独立触发和 owner 统一记录在 [Skill 地图](03-skill-map.md)。
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
Decision ID:
|
|
11
11
|
Scope:
|
|
12
12
|
Decision:
|
|
13
|
-
Source: user-decision | repository-fact | official-guidance | builder-baseline
|
|
13
|
+
Source: user-decision | repository-fact | project-template | official-guidance | builder-baseline
|
|
14
14
|
Rationale:
|
|
15
15
|
Migration:
|
|
16
16
|
Verification:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Skill 地图
|
|
2
|
+
|
|
3
|
+
`engineering-standards` 是规范权威。只有当前任务命中下表中的独立触发条件时,才读取对应领域 Skill。
|
|
4
|
+
|
|
5
|
+
{{SKILL_MAP}}
|
|
6
|
+
|
|
7
|
+
每个领域 Skill 记录:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
Name
|
|
11
|
+
Role
|
|
12
|
+
Triggers
|
|
13
|
+
Scope
|
|
14
|
+
Canonical evidence
|
|
15
|
+
Exclusions
|
|
16
|
+
Relationship to engineering-standards
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
清单外的项目 Skill 不归 Engineering Standards Builder 所有,不得因 refresh 被修改或删除。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 源码与模板地图
|
|
2
|
+
|
|
3
|
+
这里导航到项目中的唯一事实源,不复制源码、FM 模板或脚手架正文。每个记录必须含至少一个可解析的项目内 Markdown 链接。
|
|
4
|
+
|
|
5
|
+
{{SOURCE_AND_TEMPLATE_RECORDS}}
|
|
6
|
+
|
|
7
|
+
推荐格式:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
## <Capability>
|
|
11
|
+
- Read when:
|
|
12
|
+
- Canonical source/template: project-relative Markdown link
|
|
13
|
+
- Role:
|
|
14
|
+
- Applies when:
|
|
15
|
+
- Produces:
|
|
16
|
+
- Manual integration:
|
|
17
|
+
- Consumers/tests:
|
|
18
|
+
- Verification:
|
|
19
|
+
- Legacy/counterexamples:
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
模板只负责骨架时必须明确列出生成后仍需完成的业务逻辑、注册、import、配置和测试。
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
只检查与当前 scope 和风险相关的条目。
|
|
4
4
|
|
|
5
5
|
- [ ] 变更映射到模块地图中的明确 scope。
|
|
6
|
+
- [ ] 只读取了 Skill 地图路由的最小充分领域 Skill 和项目来源。
|
|
6
7
|
- [ ] 依赖只通过允许的 public contract,未产生循环或深导入。
|
|
7
8
|
- [ ] 外部输入、权限、秘密和敏感数据边界已处理。
|
|
8
9
|
- [ ] 错误、超时、取消、资源和并发生命周期完整。
|
|
9
10
|
- [ ] public API、schema、事件、CLI 或持久化兼容影响已说明。
|
|
10
11
|
- [ ] 测试层级匹配风险,失败与清理路径被覆盖。
|
|
11
12
|
- [ ] generated/vendor/frozen 文件未被无意修改。
|
|
13
|
+
- [ ] 引用的源码/FM/脚手架仍是项目真实路径,未在 Skill 中复制第二份事实源。
|
|
12
14
|
- [ ] 实际运行的 format/lint/compile/test/build 命令及退出码已记录。
|
|
13
15
|
- [ ] 当前状态、目标状态、Ratchet 与例外没有混写。
|
|
14
16
|
- [ ] 文档、迁移、回滚、可观测性和发布影响按需更新。
|