skill-family-engineering-kit 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/CHANGELOG.zh-CN.md +22 -0
- package/README.md +15 -12
- package/README.zh-CN.md +15 -12
- package/candidate/profile-bundle.mjs +1058 -176
- package/candidate/projection-bundle-cli.mjs +57 -15
- package/candidate/skill-naming-cli.mjs +59 -0
- package/candidate/skill-naming-policy.json +46 -0
- package/candidate/skill-naming.mjs +280 -0
- package/docs/agents/capability-catalog.en.json +13 -10
- package/docs/agents/capability-catalog.json +3 -2
- package/docs/agents/capability-catalog.zh-CN.json +13 -10
- package/docs/en/migration/index.html +4 -3
- package/docs/en/recipes/adopt-existing-repository/index.html +2 -1
- package/docs/en/recipes/domain-schema-validation/index.html +3 -1
- package/docs/en/recipes/index.html +2 -1
- package/docs/en/reference/api/index.html +24 -0
- package/docs/en/reference/compatibility/index.html +51 -5
- package/docs/migration/index.html +4 -3
- package/docs/public/status/index.html +3 -3
- package/docs/recipes/adopt-existing-repository/index.html +2 -1
- package/docs/recipes/domain-schema-validation/index.html +3 -1
- package/docs/recipes/index.html +2 -1
- package/docs/reference/api/contracts/index.html +26 -0
- package/docs/reference/api/engineering-kit/index.html +26 -0
- package/docs/reference/api/harness/index.html +27 -1
- package/docs/reference/api/index.html +25 -1
- package/docs/reference/compatibility/index.html +51 -5
- package/docs/search/search_index.json +1 -1
- package/package.json +5 -4
- package/release-notes/0.3.0.yaml +25 -0
|
@@ -1,22 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { buildQuickstartProfileProjection } from "./profile-bundle.mjs";
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Build-time CLI for the Quickstart Profile v2 offline bundle. It only emits
|
|
7
|
+
* the projection manifest on stdout; diagnostics go to stderr and every
|
|
8
|
+
* argument or build failure exits 2. Writing stays with runProjection.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const USAGE = [
|
|
12
|
+
"usage: projection-bundle-cli.mjs",
|
|
13
|
+
" [--target-prefix <relative-path>]",
|
|
14
|
+
" --consumer-schema-root <absolute-build-time-root>",
|
|
15
|
+
" --consumer-schema <relative-path> (repeatable)",
|
|
16
|
+
" --source-repository <identity>",
|
|
17
|
+
" --source-base-commit <identity>",
|
|
18
|
+
].join("\n");
|
|
19
|
+
|
|
20
|
+
function fail(message) {
|
|
21
|
+
process.stderr.write(`${message}\n${USAGE}\n`);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
|
|
4
25
|
const args = process.argv.slice(2);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
26
|
+
const options = { consumerSchemaPaths: [] };
|
|
27
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
28
|
+
const flag = args[index];
|
|
29
|
+
const next = args[index + 1];
|
|
30
|
+
switch (flag) {
|
|
31
|
+
case "--target-prefix":
|
|
32
|
+
case "--consumer-schema-root":
|
|
33
|
+
case "--consumer-schema":
|
|
34
|
+
case "--source-repository":
|
|
35
|
+
case "--source-base-commit":
|
|
36
|
+
if (next === undefined) fail(`missing value for ${flag}`);
|
|
37
|
+
index += 1;
|
|
38
|
+
if (flag === "--consumer-schema") options.consumerSchemaPaths.push(next);
|
|
39
|
+
else if (flag === "--target-prefix") options.targetPrefix = next;
|
|
40
|
+
else if (flag === "--consumer-schema-root") options.consumerSchemaRoot = next;
|
|
41
|
+
else if (flag === "--source-repository") options.sourceRepository = next;
|
|
42
|
+
else options.sourceBaseCommit = next;
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
fail(`unknown argument: ${flag}`);
|
|
12
46
|
}
|
|
13
47
|
}
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
48
|
+
if (!options.consumerSchemaRoot) fail("--consumer-schema-root is required");
|
|
49
|
+
if (!path.isAbsolute(options.consumerSchemaRoot)) {
|
|
50
|
+
fail("--consumer-schema-root must be an absolute directory");
|
|
51
|
+
}
|
|
52
|
+
if (options.consumerSchemaPaths.length === 0) {
|
|
53
|
+
fail("at least one --consumer-schema is required");
|
|
54
|
+
}
|
|
55
|
+
if (!options.sourceRepository) fail("--source-repository is required");
|
|
56
|
+
if (!options.sourceBaseCommit) fail("--source-base-commit is required");
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const { manifest } = await buildQuickstartProfileProjection(options);
|
|
60
|
+
process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
61
|
+
} catch (cause) {
|
|
62
|
+
process.stderr.write(`${cause?.message ?? String(cause)}\n`);
|
|
63
|
+
process.exit(2);
|
|
22
64
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { checkPluginSkillNaming } from "./skill-naming.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build-time CLI for the candidate plugin skill naming check. It only emits
|
|
6
|
+
* the report on stdout; diagnostics go to stderr. Exit codes: 0 = every
|
|
7
|
+
* skill passes, 1 = at least one rule FAIL, 2 = usage or mechanism error.
|
|
8
|
+
* The CLI never writes anywhere.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const USAGE = [
|
|
12
|
+
"usage: skill-naming-cli.mjs",
|
|
13
|
+
" --skills-root <directory> (required; each immediate child with SKILL.md is one published skill)",
|
|
14
|
+
" --plugin-slug <slug> (required; the published plugin slug, required as description signal)",
|
|
15
|
+
" --name-prefix <prefix> (optional approved name prefix; defaults to the plugin slug)",
|
|
16
|
+
" --domain-signal <word> (repeatable; extra domain words accepted as description signals)",
|
|
17
|
+
" --policy <path> (optional; defaults to the bundled candidate policy)",
|
|
18
|
+
].join("\n");
|
|
19
|
+
|
|
20
|
+
function fail(message) {
|
|
21
|
+
process.stderr.write(`${message}\n${USAGE}\n`);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const options = { domainSignals: [] };
|
|
27
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
28
|
+
const flag = args[index];
|
|
29
|
+
const next = args[index + 1];
|
|
30
|
+
switch (flag) {
|
|
31
|
+
case "--skills-root":
|
|
32
|
+
case "--plugin-slug":
|
|
33
|
+
case "--name-prefix":
|
|
34
|
+
case "--domain-signal":
|
|
35
|
+
case "--policy":
|
|
36
|
+
if (next === undefined) fail(`missing value for ${flag}`);
|
|
37
|
+
index += 1;
|
|
38
|
+
if (flag === "--skills-root") options.skillsRoot = next;
|
|
39
|
+
else if (flag === "--plugin-slug") options.pluginSlug = next;
|
|
40
|
+
else if (flag === "--name-prefix") options.namePrefix = next;
|
|
41
|
+
else if (flag === "--domain-signal") options.domainSignals.push(next);
|
|
42
|
+
else options.policyPath = next;
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
fail(`unknown argument: ${flag}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!options.skillsRoot) fail("--skills-root is required");
|
|
49
|
+
if (!options.pluginSlug) fail("--plugin-slug is required");
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const report = await checkPluginSkillNaming(options);
|
|
53
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
54
|
+
process.exit(report.ok ? 0 : 1);
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
const kind = cause && cause.details && cause.details.kind ? ` (${cause.details.kind})` : "";
|
|
57
|
+
process.stderr.write(`${cause?.code ?? "SFC2004"}${kind}: ${cause?.message ?? String(cause)}\n`);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"kind": "skill-family.skill-naming-policy",
|
|
4
|
+
"policyVersion": "1.0.0",
|
|
5
|
+
"status": "candidate",
|
|
6
|
+
"summary": "Plugin skill naming and description policy: every published skill name carries the plugin prefix, every frontmatter description carries a domain signal, and routing entry skills declare plugin-internal routing scope.",
|
|
7
|
+
"pluginSlugPattern": "^[a-z][a-z0-9-]*$",
|
|
8
|
+
"skillName": {
|
|
9
|
+
"ruleId": "SNR-001",
|
|
10
|
+
"summary": "Every published skill name must be <plugin-slug>-<suffix>; bare generic names are forbidden.",
|
|
11
|
+
"suffixPattern": "[a-z0-9][a-z0-9-]*",
|
|
12
|
+
"reservedBareNames": ["help", "setup", "quickstart", "where-am-i"],
|
|
13
|
+
"additionalApprovedPatterns": [],
|
|
14
|
+
"note": "Equivalent namespace forms are only valid when their regular expression is explicitly approved and registered in additionalApprovedPatterns; a reserved bare name stays forbidden even then."
|
|
15
|
+
},
|
|
16
|
+
"description": {
|
|
17
|
+
"ruleId": "SNR-002",
|
|
18
|
+
"summary": "The frontmatter description must contain a domain signal (the plugin slug or a declared domain word) and must not consist of a bare global trigger phrase.",
|
|
19
|
+
"forbiddenGlobalTriggers": [
|
|
20
|
+
"what can this plugin do",
|
|
21
|
+
"what can you do",
|
|
22
|
+
"how to start",
|
|
23
|
+
"how do i start",
|
|
24
|
+
"is my environment ready",
|
|
25
|
+
"你能做什么",
|
|
26
|
+
"怎么开始",
|
|
27
|
+
"我的环境准备好了吗"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"routingEntry": {
|
|
31
|
+
"ruleId": "SNR-003",
|
|
32
|
+
"summary": "A routing entry skill must name its plugin slug in the description (plugin-internal routing scope) and must not imply global arbitration.",
|
|
33
|
+
"suffixes": ["help", "quickstart", "where-am-i"],
|
|
34
|
+
"forbiddenGlobalScopePhrases": [
|
|
35
|
+
"across all plugins",
|
|
36
|
+
"all installed plugins",
|
|
37
|
+
"global routing",
|
|
38
|
+
"global arbitration",
|
|
39
|
+
"routes every plugin",
|
|
40
|
+
"全局路由",
|
|
41
|
+
"全局仲裁",
|
|
42
|
+
"所有插件",
|
|
43
|
+
"跨插件仲裁"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Candidate plugin skill naming checker (unstable).
|
|
7
|
+
*
|
|
8
|
+
* Mechanically evaluates the cross-repository skill naming and description
|
|
9
|
+
* policy (candidate/skill-naming-policy.json) over one plugin skills root:
|
|
10
|
+
* every immediate subdirectory that carries a SKILL.md is one published
|
|
11
|
+
* skill, and its frontmatter `name` / `description` are checked against the
|
|
12
|
+
* three frozen rules:
|
|
13
|
+
*
|
|
14
|
+
* SNR-001 name prefix — name must be `<plugin-slug>-<suffix>`
|
|
15
|
+
* (or match an explicitly approved extra
|
|
16
|
+
* pattern); reserved bare names are always
|
|
17
|
+
* forbidden;
|
|
18
|
+
* SNR-002 description signal — description must contain the plugin slug
|
|
19
|
+
* or a caller-declared domain word, and must
|
|
20
|
+
* not consist of a bare global trigger
|
|
21
|
+
* phrase;
|
|
22
|
+
* SNR-003 routing scope — routing entry skills (policy suffixes)
|
|
23
|
+
* must name the plugin slug in the
|
|
24
|
+
* description and must not imply global
|
|
25
|
+
* arbitration.
|
|
26
|
+
*
|
|
27
|
+
* The module only reads; it never writes, never consults the network, the
|
|
28
|
+
* clock, or Git. Every rule outcome is reported per skill as PASS or FAIL;
|
|
29
|
+
* the report's `ok` is the AND of all rule outcomes.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export const SKILL_NAMING_POLICY_PATH = fileURLToPath(
|
|
33
|
+
new URL("./skill-naming-policy.json", import.meta.url),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export const SKILL_NAMING_RULE_IDS = Object.freeze(["SNR-001", "SNR-002", "SNR-003"]);
|
|
37
|
+
|
|
38
|
+
function namingError(kind, message, details) {
|
|
39
|
+
const error = new Error(message);
|
|
40
|
+
error.code = "SFC2004";
|
|
41
|
+
error.details = { ...(details ?? {}), kind };
|
|
42
|
+
return error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function escapeRegExp(value) {
|
|
46
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function unquote(value) {
|
|
50
|
+
const trimmed = value.trim();
|
|
51
|
+
if (
|
|
52
|
+
trimmed.length >= 2 &&
|
|
53
|
+
((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
54
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'")))
|
|
55
|
+
) {
|
|
56
|
+
return trimmed.slice(1, -1);
|
|
57
|
+
}
|
|
58
|
+
return trimmed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Line-based frontmatter extraction (no YAML dependency): the document must
|
|
63
|
+
* open with a `---` line and close with another `---` line; `name` and
|
|
64
|
+
* `description` are top-level keys, optionally quoted, optionally continued
|
|
65
|
+
* on indented lines (plain multi-line or `|`/`>` block scalars).
|
|
66
|
+
*/
|
|
67
|
+
export function parseSkillFrontmatter(text) {
|
|
68
|
+
if (typeof text !== "string" || !text.startsWith("---\n")) {
|
|
69
|
+
throw namingError("frontmatter-missing", "SKILL.md does not start with a frontmatter fence");
|
|
70
|
+
}
|
|
71
|
+
const lines = text.split("\n");
|
|
72
|
+
const end = lines.indexOf("---", 1);
|
|
73
|
+
if (end === -1) {
|
|
74
|
+
throw namingError("frontmatter-unclosed", "SKILL.md frontmatter fence is not closed");
|
|
75
|
+
}
|
|
76
|
+
const fields = {};
|
|
77
|
+
for (let index = 1; index < end; index += 1) {
|
|
78
|
+
const match = /^([A-Za-z][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(lines[index]);
|
|
79
|
+
if (!match) continue;
|
|
80
|
+
const [, key, rawValue] = match;
|
|
81
|
+
let value = rawValue;
|
|
82
|
+
if (value === "" || value === "|" || value === ">" || value === "|-" || value === ">-") {
|
|
83
|
+
const collected = [];
|
|
84
|
+
for (let follow = index + 1; follow < end; follow += 1) {
|
|
85
|
+
const line = lines[follow];
|
|
86
|
+
if (/^[ \t]+\S/.test(line)) collected.push(line.trim());
|
|
87
|
+
else if (line.trim() === "") collected.push("");
|
|
88
|
+
else break;
|
|
89
|
+
}
|
|
90
|
+
value = collected.join(" ").trim();
|
|
91
|
+
index += collected.length;
|
|
92
|
+
}
|
|
93
|
+
fields[key] = unquote(value);
|
|
94
|
+
}
|
|
95
|
+
return { name: fields.name, description: fields.description };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Loads and shape-validates a naming policy document. */
|
|
99
|
+
export async function loadSkillNamingPolicy(policyPath) {
|
|
100
|
+
const resolved = policyPath ?? SKILL_NAMING_POLICY_PATH;
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(await readFile(resolved, "utf8"));
|
|
104
|
+
} catch (cause) {
|
|
105
|
+
throw namingError(
|
|
106
|
+
cause && cause.code === "ENOENT" ? "policy-missing" : "policy-parse-failed",
|
|
107
|
+
`skill naming policy is not readable JSON: ${resolved}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (!parsed || parsed.kind !== "skill-family.skill-naming-policy" || parsed.schemaVersion !== 1) {
|
|
111
|
+
throw namingError("policy-invalid", "skill naming policy must carry schemaVersion 1 and kind skill-family.skill-naming-policy");
|
|
112
|
+
}
|
|
113
|
+
for (const section of ["skillName", "description", "routingEntry"]) {
|
|
114
|
+
if (!parsed[section] || typeof parsed[section].ruleId !== "string") {
|
|
115
|
+
throw namingError("policy-invalid", `skill naming policy lacks a usable ${section} section`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return parsed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizePhrase(value) {
|
|
122
|
+
return String(value ?? "")
|
|
123
|
+
.toLocaleLowerCase("en-US")
|
|
124
|
+
.replace(/[\s\p{P}\p{S}]+/gu, " ")
|
|
125
|
+
.trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ruleOutcome(ruleId, ok, message) {
|
|
129
|
+
return { ruleId, status: ok ? "PASS" : "FAIL", ...(ok ? {} : { message }) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function checkNameRule(name, namePrefix, policy) {
|
|
133
|
+
const { suffixPattern, reservedBareNames, additionalApprovedPatterns } = policy.skillName;
|
|
134
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
135
|
+
return ruleOutcome("SNR-001", false, "frontmatter lacks a usable name");
|
|
136
|
+
}
|
|
137
|
+
if (reservedBareNames.includes(name)) {
|
|
138
|
+
return ruleOutcome("SNR-001", false, `reserved bare generic name is forbidden: ${name}`);
|
|
139
|
+
}
|
|
140
|
+
const prefixed = new RegExp(`^${escapeRegExp(namePrefix)}-(?:${suffixPattern})$`).test(name);
|
|
141
|
+
const approved = (Array.isArray(additionalApprovedPatterns) ? additionalApprovedPatterns : []).some(
|
|
142
|
+
(pattern) => new RegExp(pattern).test(name),
|
|
143
|
+
);
|
|
144
|
+
return prefixed || approved
|
|
145
|
+
? ruleOutcome("SNR-001", true)
|
|
146
|
+
: ruleOutcome("SNR-001", false, `name must be ${namePrefix}-<suffix> or an approved namespace form: ${name}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function checkDescriptionRule(description, slug, domainSignals, policy) {
|
|
150
|
+
if (typeof description !== "string" || description.trim().length === 0) {
|
|
151
|
+
return ruleOutcome("SNR-002", false, "frontmatter lacks a usable description");
|
|
152
|
+
}
|
|
153
|
+
const lowered = description.toLocaleLowerCase("en-US");
|
|
154
|
+
const signals = [slug, ...domainSignals].filter((signal) => typeof signal === "string" && signal.length > 0);
|
|
155
|
+
if (!signals.some((signal) => lowered.includes(signal.toLocaleLowerCase("en-US")))) {
|
|
156
|
+
return ruleOutcome(
|
|
157
|
+
"SNR-002",
|
|
158
|
+
false,
|
|
159
|
+
"description carries no domain signal (plugin slug or a declared domain word)",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const normalized = normalizePhrase(description);
|
|
163
|
+
const bareTrigger = (policy.description.forbiddenGlobalTriggers ?? []).find(
|
|
164
|
+
(trigger) => normalizePhrase(trigger) === normalized,
|
|
165
|
+
);
|
|
166
|
+
if (bareTrigger !== undefined) {
|
|
167
|
+
return ruleOutcome("SNR-002", false, `description is a bare global trigger phrase: ${bareTrigger}`);
|
|
168
|
+
}
|
|
169
|
+
return ruleOutcome("SNR-002", true);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function checkRoutingRule(name, description, slug, namePrefix, policy) {
|
|
173
|
+
const suffixes = policy.routingEntry.suffixes ?? [];
|
|
174
|
+
const prefix = `${namePrefix}-`;
|
|
175
|
+
const suffix = typeof name === "string" && name.startsWith(prefix) ? name.slice(prefix.length) : null;
|
|
176
|
+
if (suffix === null || !suffixes.includes(suffix)) {
|
|
177
|
+
return ruleOutcome("SNR-003", true); // not a routing entry skill: vacuous pass
|
|
178
|
+
}
|
|
179
|
+
const lowered = typeof description === "string" ? description.toLocaleLowerCase("en-US") : "";
|
|
180
|
+
if (!lowered.includes(slug.toLocaleLowerCase("en-US"))) {
|
|
181
|
+
return ruleOutcome(
|
|
182
|
+
"SNR-003",
|
|
183
|
+
false,
|
|
184
|
+
"routing entry skill must name the plugin slug to declare plugin-internal routing scope",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const scopePhrase = (policy.routingEntry.forbiddenGlobalScopePhrases ?? []).find((phrase) =>
|
|
188
|
+
lowered.includes(phrase.toLocaleLowerCase("en-US")),
|
|
189
|
+
);
|
|
190
|
+
if (scopePhrase !== undefined) {
|
|
191
|
+
return ruleOutcome("SNR-003", false, `routing entry skill implies global arbitration: ${scopePhrase}`);
|
|
192
|
+
}
|
|
193
|
+
return ruleOutcome("SNR-003", true);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Checks every published skill under one skills root.
|
|
198
|
+
* Options: { skillsRoot, pluginSlug, namePrefix?, domainSignals?, policyPath? }.
|
|
199
|
+
* namePrefix is the approved name prefix used by SNR-001/SNR-003 and defaults
|
|
200
|
+
* to pluginSlug; the reference implementation shows the two can differ (an
|
|
201
|
+
* approved short prefix), while SNR-002/SNR-003 still require the full plugin
|
|
202
|
+
* slug as the description signal.
|
|
203
|
+
* Returns the report document; throws a coded error only for unusable
|
|
204
|
+
* inputs (missing root, invalid slug, unreadable policy).
|
|
205
|
+
*/
|
|
206
|
+
export async function checkPluginSkillNaming({ skillsRoot, pluginSlug, namePrefix, domainSignals = [], policyPath } = {}) {
|
|
207
|
+
const policy = await loadSkillNamingPolicy(policyPath);
|
|
208
|
+
if (typeof pluginSlug !== "string" || !new RegExp(policy.pluginSlugPattern).test(pluginSlug)) {
|
|
209
|
+
throw namingError("invalid-plugin-slug", "pluginSlug must satisfy the policy plugin slug pattern", {
|
|
210
|
+
pattern: policy.pluginSlugPattern,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const effectivePrefix = namePrefix ?? pluginSlug;
|
|
214
|
+
if (typeof effectivePrefix !== "string" || !new RegExp(policy.pluginSlugPattern).test(effectivePrefix)) {
|
|
215
|
+
throw namingError("invalid-name-prefix", "namePrefix must satisfy the policy plugin slug pattern", {
|
|
216
|
+
pattern: policy.pluginSlugPattern,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (typeof skillsRoot !== "string" || skillsRoot.length === 0) {
|
|
220
|
+
throw namingError("invalid-skills-root", "skillsRoot is required");
|
|
221
|
+
}
|
|
222
|
+
const rootAbs = await realpath(path.resolve(skillsRoot)).catch(() => null);
|
|
223
|
+
if (rootAbs === null || !(await stat(rootAbs)).isDirectory()) {
|
|
224
|
+
throw namingError("invalid-skills-root", `skills root is not an existing directory: ${skillsRoot}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const directories = (await readdir(rootAbs, { withFileTypes: true }))
|
|
228
|
+
.filter((entry) => entry.isDirectory())
|
|
229
|
+
.map((entry) => entry.name)
|
|
230
|
+
.sort((left, right) => left.localeCompare(right));
|
|
231
|
+
|
|
232
|
+
const skills = [];
|
|
233
|
+
for (const directory of directories) {
|
|
234
|
+
const relPath = `${directory}/SKILL.md`;
|
|
235
|
+
let text = null;
|
|
236
|
+
try {
|
|
237
|
+
const candidate = path.join(rootAbs, directory, "SKILL.md");
|
|
238
|
+
if ((await stat(candidate)).isFile()) text = await readFile(candidate, "utf8");
|
|
239
|
+
} catch {
|
|
240
|
+
text = null;
|
|
241
|
+
}
|
|
242
|
+
if (text === null) continue; // not a published skill directory
|
|
243
|
+
let frontmatter = { name: undefined, description: undefined };
|
|
244
|
+
let parseFailure = null;
|
|
245
|
+
try {
|
|
246
|
+
frontmatter = parseSkillFrontmatter(text);
|
|
247
|
+
} catch (cause) {
|
|
248
|
+
parseFailure = cause && cause.message ? cause.message : String(cause);
|
|
249
|
+
}
|
|
250
|
+
const rules =
|
|
251
|
+
parseFailure !== null
|
|
252
|
+
? SKILL_NAMING_RULE_IDS.map((ruleId) => ruleOutcome(ruleId, false, parseFailure))
|
|
253
|
+
: [
|
|
254
|
+
checkNameRule(frontmatter.name, effectivePrefix, policy),
|
|
255
|
+
checkDescriptionRule(frontmatter.description, pluginSlug, domainSignals, policy),
|
|
256
|
+
checkRoutingRule(frontmatter.name, frontmatter.description, pluginSlug, effectivePrefix, policy),
|
|
257
|
+
];
|
|
258
|
+
skills.push({
|
|
259
|
+
directory,
|
|
260
|
+
path: relPath,
|
|
261
|
+
name: frontmatter.name ?? null,
|
|
262
|
+
rules,
|
|
263
|
+
ok: rules.every((rule) => rule.status === "PASS"),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
kind: "skill-family.skill-naming-report",
|
|
269
|
+
schemaVersion: 1,
|
|
270
|
+
policyVersion: policy.policyVersion,
|
|
271
|
+
pluginSlug,
|
|
272
|
+
namePrefix: effectivePrefix,
|
|
273
|
+
skillsRoot: rootAbs,
|
|
274
|
+
skillCount: skills.length,
|
|
275
|
+
ok: skills.every((skill) => skill.ok),
|
|
276
|
+
skills,
|
|
277
|
+
policy:
|
|
278
|
+
"skill naming check is diagnosis only: it never writes and never renames; violations must be resolved by the owning plugin",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
@@ -505,7 +505,7 @@
|
|
|
505
505
|
"Need to freely author a report from open business outputs (forbidden)"
|
|
506
506
|
],
|
|
507
507
|
"prerequisites": [
|
|
508
|
-
"REPORT_RENDERER_NAME / VERSION come from their respective packages (currently 0.
|
|
508
|
+
"REPORT_RENDERER_NAME / VERSION come from their respective packages (currently 0.3.0)",
|
|
509
509
|
"SUPPORTED_REPORT_LOCALES = [zh-CN, en-US]"
|
|
510
510
|
],
|
|
511
511
|
"inputs": [
|
|
@@ -1430,7 +1430,7 @@
|
|
|
1430
1430
|
},
|
|
1431
1431
|
{
|
|
1432
1432
|
"id": "foundation.contracts.quickstart-profile-candidate",
|
|
1433
|
-
"intent": "Expose the
|
|
1433
|
+
"intent": "Expose the Quickstart Profile v2 protocol and real-$id Resource, Task, and Result schema collection through skill-family-contracts/candidate/quickstart-profile for exact-version integration trials.",
|
|
1434
1434
|
"useWhen": [
|
|
1435
1435
|
"Need to inspect or validate the candidate Resource, Task, or Result shape before it is proposed for the frozen registry",
|
|
1436
1436
|
"The consumer can pin the exact skill-family-contracts package version and isolate candidate imports from its stable API"
|
|
@@ -1458,6 +1458,7 @@
|
|
|
1458
1458
|
"An invalid candidate document returns valid:false with Ajv findings"
|
|
1459
1459
|
],
|
|
1460
1460
|
"invariants": [
|
|
1461
|
+
"The sole business-neutral operation is execute-method; method identifiers and domain schemas remain consumer-owned",
|
|
1461
1462
|
"Candidate schemas remain absent from src/registry.json and do not expand the 18 stable object classes",
|
|
1462
1463
|
"Stability is candidate: a later minor release may change or remove this subpath"
|
|
1463
1464
|
],
|
|
@@ -1502,7 +1503,7 @@
|
|
|
1502
1503
|
"verifyQuickstartExchange converts a failure into {valid:false, code, message, details}"
|
|
1503
1504
|
],
|
|
1504
1505
|
"invariants": [
|
|
1505
|
-
"The Result must bind the exact Task digest,
|
|
1506
|
+
"The Result must bind the exact Task digest, real Resource bytes, globally unique Resource ids, operation identity, correlation fields, and the complete evidence-binding set",
|
|
1506
1507
|
"Stability is candidate: a later minor release may change or remove this subpath"
|
|
1507
1508
|
],
|
|
1508
1509
|
"ownedByCaller": [
|
|
@@ -1516,35 +1517,37 @@
|
|
|
1516
1517
|
},
|
|
1517
1518
|
{
|
|
1518
1519
|
"id": "foundation.kit.quickstart-profile-candidate",
|
|
1519
|
-
"intent": "Build a deterministic
|
|
1520
|
+
"intent": "Build a deterministic Quickstart Profile v2 offline Bundle with standalone validators selected by schema $id through skill-family-engineering-kit/candidate/quickstart-profile.",
|
|
1520
1521
|
"useWhen": [
|
|
1521
1522
|
"Need to trial the candidate Quickstart schemas and runner in a target using the stable projection authorization boundary",
|
|
1522
|
-
"Need
|
|
1523
|
+
"Need complete source, consumer-schema, payload, tool-version, and license provenance for the generated Bundle"
|
|
1523
1524
|
],
|
|
1524
1525
|
"doNotUseWhen": [
|
|
1525
1526
|
"Need a stable Quickstart API or a fifth top-level Kit command",
|
|
1526
1527
|
"Need to bypass target manifest authorization or handwritten-file protection"
|
|
1527
1528
|
],
|
|
1528
1529
|
"prerequisites": [
|
|
1529
|
-
"Install exact matching versions of all three Foundation packages",
|
|
1530
|
+
"Install exact matching 0.3.0 versions of all three Foundation packages",
|
|
1530
1531
|
"The target prefix is a contained relative POSIX path"
|
|
1531
1532
|
],
|
|
1532
1533
|
"inputs": [
|
|
1533
|
-
"
|
|
1534
|
+
"Contained targetPrefix, consumerSchemaRoot, and an explicit relative consumer-schema path set",
|
|
1535
|
+
"Frozen sourceRepository and sourceBaseCommit identity",
|
|
1534
1536
|
"Exact installed Contracts, Harness, and Kit package bytes"
|
|
1535
1537
|
],
|
|
1536
1538
|
"outputs": [
|
|
1537
|
-
"A projection manifest and provenance record with
|
|
1539
|
+
"A projection manifest and provenance record with Foundation sources, consumer schemas, payload digests, tool versions, and licenses"
|
|
1538
1540
|
],
|
|
1539
1541
|
"sideEffects": [
|
|
1540
|
-
"Builds the manifest in memory and reads installed package files",
|
|
1542
|
+
"Builds the manifest in memory and reads installed package plus explicit consumer-schema files",
|
|
1541
1543
|
"Performs no writes; callers pass the manifest to stable runProjection for authorized writes"
|
|
1542
1544
|
],
|
|
1543
1545
|
"failureSemantics": [
|
|
1544
1546
|
"An invalid targetPrefix throws TypeError",
|
|
1545
|
-
"
|
|
1547
|
+
"Duplicate schema ids, bad references, mixed dialects, unsupported formats, path escapes, or missing source identity reject the build; runProjection reports its own stable projection failures"
|
|
1546
1548
|
],
|
|
1547
1549
|
"invariants": [
|
|
1550
|
+
"The Bundle contains no node_modules or runtime Ajv and runs without installed Foundation packages or network access",
|
|
1548
1551
|
"The helper adds no top-level Kit command and never bypasses runProjection",
|
|
1549
1552
|
"Stability is candidate: a later minor release may change or remove this subpath"
|
|
1550
1553
|
],
|
|
@@ -875,10 +875,11 @@
|
|
|
875
875
|
"since": "0.2.1",
|
|
876
876
|
"stability": "candidate",
|
|
877
877
|
"entrypoints": [
|
|
878
|
-
"skill-family-contracts/candidate/quickstart-profile: QUICKSTART_PROTOCOL, quickstartProfileSchemas, validateQuickstartProfileDocument (packages/skill-family-contracts/candidate/quickstart-profile/index.mjs)"
|
|
878
|
+
"skill-family-contracts/candidate/quickstart-profile: QUICKSTART_PROTOCOL, QUICKSTART_PROFILE_VERSION, loadQuickstartProtocol, quickstartProfileSchemas, listQuickstartProfileSchemas, validateQuickstartProfileDocument (packages/skill-family-contracts/candidate/quickstart-profile/index.mjs)"
|
|
879
879
|
],
|
|
880
880
|
"sourceRefs": [
|
|
881
881
|
"packages/skill-family-contracts/candidate/quickstart-profile/index.mjs",
|
|
882
|
+
"packages/skill-family-contracts/candidate/quickstart-profile/protocol.json",
|
|
882
883
|
"packages/skill-family-contracts/candidate/quickstart-profile/resource.schema.json",
|
|
883
884
|
"packages/skill-family-contracts/candidate/quickstart-profile/task.schema.json",
|
|
884
885
|
"packages/skill-family-contracts/candidate/quickstart-profile/result.schema.json"
|
|
@@ -897,7 +898,7 @@
|
|
|
897
898
|
"since": "0.2.1",
|
|
898
899
|
"stability": "candidate",
|
|
899
900
|
"entrypoints": [
|
|
900
|
-
"skill-family-harness-node/candidate/quickstart-profile: createObservationResource, verifyObservationResource, createQuickstartTask, wrapQuickstartResult, assertQuickstartExchange, verifyQuickstartExchange (packages/skill-family-harness-node/candidate/quickstart-profile.mjs)"
|
|
901
|
+
"skill-family-harness-node/candidate/quickstart-profile: createObservationResource, verifyResourceBytes, verifyObservationResource, createQuickstartTask, wrapQuickstartResult, assertQuickstartExchange, verifyQuickstartExchange, canonicalJson, computeResourceClosure, digestBytes, digestDocument (packages/skill-family-harness-node/candidate/quickstart-profile.mjs)"
|
|
901
902
|
],
|
|
902
903
|
"sourceRefs": [
|
|
903
904
|
"packages/skill-family-harness-node/candidate/quickstart-profile.mjs"
|
|
@@ -505,7 +505,7 @@
|
|
|
505
505
|
"需要从开放业务 outputs 自由编报告(禁止)"
|
|
506
506
|
],
|
|
507
507
|
"prerequisites": [
|
|
508
|
-
"REPORT_RENDERER_NAME / VERSION 取自各自 package(当前 0.
|
|
508
|
+
"REPORT_RENDERER_NAME / VERSION 取自各自 package(当前 0.3.0)",
|
|
509
509
|
"SUPPORTED_REPORT_LOCALES = [zh-CN, en-US]"
|
|
510
510
|
],
|
|
511
511
|
"inputs": [
|
|
@@ -1430,7 +1430,7 @@
|
|
|
1430
1430
|
},
|
|
1431
1431
|
{
|
|
1432
1432
|
"id": "foundation.contracts.quickstart-profile-candidate",
|
|
1433
|
-
"intent": "通过 skill-family-contracts/candidate/quickstart-profile
|
|
1433
|
+
"intent": "通过 skill-family-contracts/candidate/quickstart-profile 公开 Quickstart Profile v2 协议与按真实 $id 索引的 Resource、Task、Result Schema 集合,供锁定精确版本的接入试验使用",
|
|
1434
1434
|
"useWhen": [
|
|
1435
1435
|
"需要在提议进入冻结登记表前检查或校验候选 Resource、Task、Result 形状",
|
|
1436
1436
|
"调用方能够锁定 skill-family-contracts 精确版本,并把 candidate 导入隔离在自身稳定 API 之外"
|
|
@@ -1458,6 +1458,7 @@
|
|
|
1458
1458
|
"候选文档无效时返回 valid:false 与 Ajv 发现"
|
|
1459
1459
|
],
|
|
1460
1460
|
"invariants": [
|
|
1461
|
+
"唯一业务中立操作为 execute-method;method 标识与领域 Schema 继续归消费者所有",
|
|
1461
1462
|
"候选 Schema 不进入 src/registry.json,也不扩张 18 类稳定对象",
|
|
1462
1463
|
"稳定性为 candidate:后续小版本可以修改或移除该子路径"
|
|
1463
1464
|
],
|
|
@@ -1502,7 +1503,7 @@
|
|
|
1502
1503
|
"verifyQuickstartExchange 把失败转成 {valid:false, code, message, details}"
|
|
1503
1504
|
],
|
|
1504
1505
|
"invariants": [
|
|
1505
|
-
"Result 必须绑定精确 Task
|
|
1506
|
+
"Result 必须绑定精确 Task 摘要、真实 Resource 字节、全局唯一 Resource id、operation 身份、correlation 字段与完整 evidence binding 集合",
|
|
1506
1507
|
"稳定性为 candidate:后续小版本可以修改或移除该子路径"
|
|
1507
1508
|
],
|
|
1508
1509
|
"ownedByCaller": [
|
|
@@ -1516,35 +1517,37 @@
|
|
|
1516
1517
|
},
|
|
1517
1518
|
{
|
|
1518
1519
|
"id": "foundation.kit.quickstart-profile-candidate",
|
|
1519
|
-
"intent": "通过 skill-family-engineering-kit/candidate/quickstart-profile
|
|
1520
|
+
"intent": "通过 skill-family-engineering-kit/candidate/quickstart-profile 构建确定性的 Quickstart Profile v2 离线 Bundle,并按 Schema $id 选择 standalone validator",
|
|
1520
1521
|
"useWhen": [
|
|
1521
1522
|
"需要通过稳定投影授权边界,在目标中试用候选 Quickstart Schema 与 runner",
|
|
1522
|
-
"
|
|
1523
|
+
"需要取得完整的来源、消费者 Schema、payload、工具版本与许可证 provenance"
|
|
1523
1524
|
],
|
|
1524
1525
|
"doNotUseWhen": [
|
|
1525
1526
|
"需要稳定 Quickstart API 或第五个 Kit 顶层命令",
|
|
1526
1527
|
"需要绕过目标 manifest 授权或手写文件保护"
|
|
1527
1528
|
],
|
|
1528
1529
|
"prerequisites": [
|
|
1529
|
-
"安装精确匹配的三个 Foundation 包版本",
|
|
1530
|
+
"安装精确匹配的三个 Foundation 0.3.0 包版本",
|
|
1530
1531
|
"targetPrefix 是受收容的 POSIX 相对路径"
|
|
1531
1532
|
],
|
|
1532
1533
|
"inputs": [
|
|
1533
|
-
"
|
|
1534
|
+
"受收容 targetPrefix、consumerSchemaRoot 与显式消费者 Schema 相对路径集合",
|
|
1535
|
+
"冻结的 sourceRepository 与 sourceBaseCommit 身份",
|
|
1534
1536
|
"已安装 Contracts、Harness 与 Kit 的精确包字节"
|
|
1535
1537
|
],
|
|
1536
1538
|
"outputs": [
|
|
1537
|
-
"投影 manifest 与 provenance 记录,包含
|
|
1539
|
+
"投影 manifest 与 provenance 记录,包含 Foundation 来源、消费者 Schema、payload 摘要、工具版本与许可证"
|
|
1538
1540
|
],
|
|
1539
1541
|
"sideEffects": [
|
|
1540
|
-
"在内存中构建 manifest
|
|
1542
|
+
"在内存中构建 manifest,并读取已安装包文件与显式消费者 Schema 文件",
|
|
1541
1543
|
"不执行写入;调用方把 manifest 交给稳定 runProjection 后才发生受权写入"
|
|
1542
1544
|
],
|
|
1543
1545
|
"failureSemantics": [
|
|
1544
1546
|
"targetPrefix 无效时抛出 TypeError",
|
|
1545
|
-
"
|
|
1547
|
+
"重复 Schema id、坏引用、混合方言、不支持的 format、路径越界或来源身份缺失时拒绝构建;runProjection 另行报告稳定投影失败"
|
|
1546
1548
|
],
|
|
1547
1549
|
"invariants": [
|
|
1550
|
+
"Bundle 不含 node_modules 或运行时 Ajv,不依赖已安装 Foundation 包或网络",
|
|
1548
1551
|
"辅助函数不增加 Kit 顶层命令,也不绕过 runProjection",
|
|
1549
1552
|
"稳定性为 candidate:后续小版本可以修改或移除该子路径"
|
|
1550
1553
|
],
|