skill-family-engineering-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CODE_OF_CONDUCT.md +131 -0
  2. package/CONTRIBUTING.md +69 -0
  3. package/LICENSE +201 -0
  4. package/README.md +74 -0
  5. package/SECURITY.md +34 -0
  6. package/data/licensing/registry.json +211 -0
  7. package/data/licensing/schema.json +207 -0
  8. package/docs/.nojekyll +0 -0
  9. package/docs/404.html +613 -0
  10. package/docs/architecture/index.html +904 -0
  11. package/docs/assets/images/favicon.png +0 -0
  12. package/docs/assets/javascripts/bundle.d7400e89.min.js +16 -0
  13. package/docs/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  14. package/docs/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  15. package/docs/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  16. package/docs/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  17. package/docs/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  18. package/docs/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  19. package/docs/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  20. package/docs/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  21. package/docs/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  22. package/docs/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  23. package/docs/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  24. package/docs/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  25. package/docs/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  26. package/docs/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  27. package/docs/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  28. package/docs/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  29. package/docs/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  30. package/docs/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  31. package/docs/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  32. package/docs/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  33. package/docs/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  34. package/docs/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  35. package/docs/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  36. package/docs/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  37. package/docs/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  38. package/docs/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  39. package/docs/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  40. package/docs/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  41. package/docs/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  42. package/docs/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  43. package/docs/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  44. package/docs/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  45. package/docs/assets/javascripts/lunr/tinyseg.js +206 -0
  46. package/docs/assets/javascripts/lunr/wordcut.js +6708 -0
  47. package/docs/assets/javascripts/workers/search.2c215733.min.js +42 -0
  48. package/docs/assets/stylesheets/main.ec1eaa64.min.css +1 -0
  49. package/docs/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
  50. package/docs/git-lifecycle/index.html +976 -0
  51. package/docs/help/index.html +907 -0
  52. package/docs/index.html +837 -0
  53. package/docs/integration/audit/baseline/audit-codes.json +62 -0
  54. package/docs/integration/audit/failure-evidence/index.html +872 -0
  55. package/docs/integration/audit/independence/index.html +841 -0
  56. package/docs/integration/audit/index.html +826 -0
  57. package/docs/integration/audit/mutation-taxonomy/index.html +1022 -0
  58. package/docs/integration/audit/schemas/audit-evidence.schema.json +182 -0
  59. package/docs/integration/audit/version-compatibility/index.html +845 -0
  60. package/docs/migration/index.html +1213 -0
  61. package/docs/quickstart/index.html +842 -0
  62. package/docs/search/search_index.json +1 -0
  63. package/docs/setup/index.html +941 -0
  64. package/docs/sitemap.xml +51 -0
  65. package/package.json +42 -0
  66. package/src/adopt-plan.mjs +594 -0
  67. package/src/check.mjs +415 -0
  68. package/src/cli.mjs +230 -0
  69. package/src/errors.mjs +144 -0
  70. package/src/gitprobe.mjs +230 -0
  71. package/src/identity-check.mjs +563 -0
  72. package/src/index.mjs +233 -0
  73. package/src/license-texts/Apache-2.0.txt +201 -0
  74. package/src/license-texts/MIT.txt +21 -0
  75. package/src/licensing.mjs +299 -0
  76. package/src/migration.mjs +427 -0
  77. package/src/projection.mjs +397 -0
  78. package/src/scaffold.mjs +178 -0
  79. package/src/skeleton.mjs +886 -0
  80. package/src/workspace.mjs +236 -0
package/src/check.mjs ADDED
@@ -0,0 +1,415 @@
1
+ import { CONTRACTS_VERSION, findSchemaByObject } from "skill-family-contracts";
2
+ import {
3
+ computeResourceClosure,
4
+ digestBytes,
5
+ readFileContained,
6
+ validateContractDocument,
7
+ } from "skill-family-harness-node";
8
+ import { invalidParamsError, KIT_ERROR_KINDS } from "./errors.mjs";
9
+ import { probeGitState } from "./gitprobe.mjs";
10
+ import {
11
+ KIT_TOOL_NAME,
12
+ KIT_VERSION,
13
+ MANAGED_LOCK_PATH,
14
+ PROJECT_MANIFEST_PATH,
15
+ } from "./skeleton.mjs";
16
+ import {
17
+ listTargetEntries,
18
+ loadTargetFacts,
19
+ matchAnyGlob,
20
+ normalizeRelPath,
21
+ readOptionalFile,
22
+ readOptionalJson,
23
+ resolveTargetRoot,
24
+ } from "./workspace.mjs";
25
+ import {
26
+ checkIdentityDrift,
27
+ loadIdentityRecord,
28
+ validateIdentityAgainstProfile,
29
+ } from "./identity-check.mjs";
30
+
31
+ /**
32
+ * check — diagnosis only, never auto-fix.
33
+ *
34
+ * Seven diagnosis classes, exactly as the FND-030 and FND-045 hand-offs bound them:
35
+ * contracts — discovered contract documents against registered schemas;
36
+ * drift — managed-file-lock entries vs actual bytes on disk;
37
+ * closure — harness resource closure over declared inputs/outputs;
38
+ * version — declared contracts version vs the frozen contracts version;
39
+ * docs — documentation facts (README presence, identity consistency);
40
+ * git — read-only Git pre-state (never a git write);
41
+ * identity — licensing and identity drift checks (FND-045).
42
+ *
43
+ * This module contains no write call of any kind. Mutation-looking flags
44
+ * (--fix, --apply, ...) are refused at intake by the CLI. Findings carry
45
+ * stable kinds and registered SFC codes; the exit code is the stable
46
+ * signal (0 clean, 1 findings, 2 rejected/usage/mechanism error).
47
+ */
48
+
49
+ const CHECK_CLASSES = Object.freeze(["contracts", "drift", "closure", "version", "docs", "git", "identity"]);
50
+
51
+ export { CHECK_CLASSES };
52
+
53
+ function finding(checkClass, kind, code, message, extra) {
54
+ return { class: checkClass, kind, code, message, ...(extra ?? {}) };
55
+ }
56
+
57
+ async function checkContracts(rootAbs, docs, findings) {
58
+ const targets = [
59
+ ["project-manifest", PROJECT_MANIFEST_PATH],
60
+ ["managed-file-lock", MANAGED_LOCK_PATH],
61
+ ];
62
+ let present = 0;
63
+ for (const [objectName, relPath] of targets) {
64
+ const loaded = await readOptionalJson(rootAbs, relPath);
65
+ if (loaded.reason === "missing") continue;
66
+ present += 1;
67
+ docs[relPath] = loaded.ok ? loaded.value : null;
68
+ if (!loaded.ok) {
69
+ findings.push(
70
+ finding(
71
+ "contracts",
72
+ KIT_ERROR_KINDS.CONTRACT_PARSE_FAILED,
73
+ "SFC1001",
74
+ `${relPath} is not valid JSON`,
75
+ { path: relPath },
76
+ ),
77
+ );
78
+ continue;
79
+ }
80
+ const registration = findSchemaByObject(objectName);
81
+ const outcome = validateContractDocument(loaded.value, { schemaId: registration.$id });
82
+ if (!outcome.valid) {
83
+ findings.push(
84
+ finding(
85
+ "contracts",
86
+ "schema-validation-failed",
87
+ outcome.errorCode,
88
+ `${relPath} fails the registered ${objectName} schema: ${outcome.errors
89
+ .slice(0, 3)
90
+ .map((entry) => `${entry.instancePath || "/"} ${entry.message}`)
91
+ .join("; ")}`,
92
+ { path: relPath, schemaId: registration.$id },
93
+ ),
94
+ );
95
+ }
96
+ }
97
+ if (present === 0) {
98
+ findings.push(
99
+ finding(
100
+ "contracts",
101
+ KIT_ERROR_KINDS.CONTRACTS_MISSING,
102
+ "SFC2004",
103
+ "no contract documents found (expected skill-family.project-manifest.json and/or skill-family.managed-file-lock.json)",
104
+ ),
105
+ );
106
+ }
107
+ }
108
+
109
+ /** Contained read of a target-declared path: missing => null, escape => finding. */
110
+ async function readManagedCandidate(rootAbs, rel, findings) {
111
+ try {
112
+ return await readFileContained(rootAbs, rel, { encoding: "utf8" });
113
+ } catch (cause) {
114
+ const kind = cause && cause.details ? cause.details.kind : null;
115
+ if (kind === "missing-resource") return null;
116
+ // Escaping lock entries are target-supplied data: report them, never follow them.
117
+ findings.push(
118
+ finding(
119
+ "drift",
120
+ kind ?? "uncontained-lock-path",
121
+ "SFC2004",
122
+ `lock entry path is not safely readable inside the target: ${rel} (kind: ${kind ?? "unknown"})`,
123
+ { path: rel },
124
+ ),
125
+ );
126
+ return { escaped: true };
127
+ }
128
+ }
129
+
130
+ async function checkDrift(rootAbs, findings) {
131
+ const loaded = await readOptionalJson(rootAbs, MANAGED_LOCK_PATH);
132
+ if (!loaded.ok) return null; // no lock => nothing to drift against
133
+ const lock = loaded.value;
134
+ const entries = Array.isArray(lock?.entries) ? lock.entries : [];
135
+ for (const entry of entries) {
136
+ const rel = typeof entry?.path === "string" ? normalizeRelPath(entry.path) : null;
137
+ const declaredHash = entry?.hash?.value;
138
+ if (!rel || typeof declaredHash !== "string" || !/^[0-9a-f]{64}$/.test(declaredHash)) {
139
+ continue; // malformed entries are reported by the contracts class
140
+ }
141
+ const text = await readManagedCandidate(rootAbs, rel, findings);
142
+ if (text === null) {
143
+ findings.push(
144
+ finding(
145
+ "drift",
146
+ KIT_ERROR_KINDS.MANAGED_FILE_MISSING,
147
+ "SFC2004",
148
+ `managed file declared in the lock does not exist: ${rel}`,
149
+ { path: rel },
150
+ ),
151
+ );
152
+ continue;
153
+ }
154
+ if (typeof text !== "string") continue; // escaping path already reported
155
+ const actual = digestBytes(Buffer.from(text, "utf8"));
156
+ if (actual !== declaredHash) {
157
+ findings.push(
158
+ finding(
159
+ "drift",
160
+ KIT_ERROR_KINDS.MANAGED_FILE_DRIFT,
161
+ "SFC2004",
162
+ `managed file drifted from its locked hash: ${rel} (lock=${declaredHash.slice(0, 12)}… actual=${actual.slice(0, 12)}…)`,
163
+ { path: rel },
164
+ ),
165
+ );
166
+ }
167
+ }
168
+ return lock;
169
+ }
170
+
171
+ async function checkClosure(rootAbs, lock, findings) {
172
+ if (!lock) return { digest: null, note: "no managed-file-lock present; closure check skipped" };
173
+ const resources = [
174
+ { path: MANAGED_LOCK_PATH, role: "input" },
175
+ ...(Array.isArray(lock.entries) ? lock.entries : [])
176
+ .filter((entry) => typeof entry?.path === "string")
177
+ .map((entry) => ({ path: normalizeRelPath(entry.path), role: "output" })),
178
+ ];
179
+ try {
180
+ const closure = await computeResourceClosure({ root: rootAbs, resources });
181
+ const missingOutputs = closure.resources.filter(
182
+ (resource) => resource.role === "output" && !resource.exists,
183
+ );
184
+ for (const missing of missingOutputs) {
185
+ findings.push(
186
+ finding(
187
+ "closure",
188
+ KIT_ERROR_KINDS.CLOSURE_INPUT_MISSING,
189
+ "SFC2004",
190
+ `closure output resource does not exist: ${missing.path}`,
191
+ { path: missing.path },
192
+ ),
193
+ );
194
+ }
195
+ return { digest: closure.digest, resourceCount: closure.resources.length };
196
+ } catch (cause) {
197
+ const kind =
198
+ cause && cause.details && cause.details.kind === "missing-resource"
199
+ ? KIT_ERROR_KINDS.CLOSURE_INPUT_MISSING
200
+ : "closure-failed";
201
+ findings.push(
202
+ finding(
203
+ "closure",
204
+ kind,
205
+ "SFC2004",
206
+ `resource closure could not be computed: ${cause && cause.message ? cause.message : "unknown"}`,
207
+ ),
208
+ );
209
+ return { digest: null };
210
+ }
211
+ }
212
+
213
+ async function checkVersion(docs, findings) {
214
+ const manifest = docs[PROJECT_MANIFEST_PATH];
215
+ const declared = manifest?.contracts?.version;
216
+ if (typeof declared !== "string") return { contractsVersion: null };
217
+ if (declared !== CONTRACTS_VERSION) {
218
+ findings.push(
219
+ finding(
220
+ "version",
221
+ KIT_ERROR_KINDS.CONTRACTS_VERSION_MISMATCH,
222
+ "SFC2004",
223
+ `project manifest declares contracts version ${declared}; this kit validates against ${CONTRACTS_VERSION}`,
224
+ { declared, expected: CONTRACTS_VERSION },
225
+ ),
226
+ );
227
+ }
228
+ return { contractsVersion: declared };
229
+ }
230
+
231
+ async function checkDocs(rootAbs, docs, findings) {
232
+ const readme = await readOptionalFile(rootAbs, "README.md");
233
+ if (readme === null || readme.trim().length === 0) {
234
+ findings.push(
235
+ finding(
236
+ "docs",
237
+ KIT_ERROR_KINDS.README_MISSING,
238
+ "SFC2004",
239
+ "README.md is missing or empty (documentation fact)",
240
+ { path: "README.md" },
241
+ ),
242
+ );
243
+ }
244
+ const packageJson = await readOptionalJson(rootAbs, "package.json");
245
+ const manifest = docs[PROJECT_MANIFEST_PATH];
246
+ if (packageJson.ok && manifest && typeof manifest?.project === "object") {
247
+ const packageName = packageJson.value?.name;
248
+ if (typeof packageName === "string") {
249
+ if (manifest.project.id !== packageName) {
250
+ findings.push(
251
+ finding(
252
+ "docs",
253
+ KIT_ERROR_KINDS.IDENTITY_MISMATCH,
254
+ "SFC2004",
255
+ `project manifest id "${manifest.project.id}" does not match package.json name "${packageName}"`,
256
+ { manifestId: manifest.project.id, packageName },
257
+ ),
258
+ );
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ async function checkGit(rootAbs, findings, allowGitSpawn) {
265
+ const git = await probeGitState(rootAbs, { allowSpawn: allowGitSpawn });
266
+ if (git.repository && git.headCommit === false) {
267
+ findings.push(
268
+ finding(
269
+ "git",
270
+ KIT_ERROR_KINDS.GIT_NO_COMMITS,
271
+ "SFC2004",
272
+ "the target is a git repository without any commit; history does not protect anything yet",
273
+ ),
274
+ );
275
+ }
276
+ if (git.repository && git.cleanState === false) {
277
+ findings.push(
278
+ finding(
279
+ "git",
280
+ KIT_ERROR_KINDS.GIT_DIRTY,
281
+ "SFC2004",
282
+ "the target has uncommitted changes; settle them before running write commands elsewhere",
283
+ ),
284
+ );
285
+ }
286
+ return git;
287
+ }
288
+
289
+ async function checkIdentity(rootAbs, docs, findings, profilesRoot) {
290
+ // Load identity record
291
+ const identityRecord = await loadIdentityRecord(rootAbs);
292
+
293
+ if (!identityRecord) {
294
+ findings.push(
295
+ finding(
296
+ "identity",
297
+ KIT_ERROR_KINDS.IDENTITY_RECORD_MISSING,
298
+ "SFC2004",
299
+ "identity record not found; cannot perform identity drift check",
300
+ { path: "skill-family.identity-record.json" },
301
+ ),
302
+ );
303
+ return null;
304
+ }
305
+
306
+ // Store for later use
307
+ docs["skill-family.identity-record.json"] = identityRecord;
308
+
309
+ // Validate against profile
310
+ if (profilesRoot) {
311
+ const profileValidation = await validateIdentityAgainstProfile(identityRecord, profilesRoot);
312
+ for (const f of profileValidation.findings) {
313
+ findings.push(
314
+ finding(
315
+ "identity",
316
+ f.kind,
317
+ f.code,
318
+ f.message,
319
+ { source: f.source, ...f },
320
+ ),
321
+ );
322
+ }
323
+ }
324
+
325
+ // Check identity drift
326
+ const driftReport = await checkIdentityDrift({ rootAbs, identityRecord });
327
+ for (const f of driftReport.findings) {
328
+ findings.push(
329
+ finding(
330
+ "identity",
331
+ f.kind,
332
+ f.code,
333
+ f.message,
334
+ { source: f.source, ...f },
335
+ ),
336
+ );
337
+ }
338
+
339
+ return identityRecord;
340
+ }
341
+
342
+ /**
343
+ * Runs all check classes over one target.
344
+ * Options: { root, allowGitSpawn, profilesRoot }.
345
+ * Returns the report document. Never writes anywhere; throws KitError only
346
+ * for unusable inputs (an unreadable target).
347
+ */
348
+ export async function runChecks({ root, allowGitSpawn = true, only, profilesRoot } = {}) {
349
+ if (only !== undefined && !CHECK_CLASSES.includes(only)) {
350
+ throw invalidParamsError(`--only must be one of: ${CHECK_CLASSES.join(", ")}`, { value: only });
351
+ }
352
+ const rootAbs = await resolveTargetRoot(root ?? ".");
353
+ const findings = [];
354
+ const docs = {};
355
+
356
+ const classes = only === undefined ? CHECK_CLASSES : CHECK_CLASSES.filter((name) => name === only);
357
+ let closureInfo = { digest: null, note: "skipped" };
358
+ let git = null;
359
+ let versionInfo = { contractsVersion: null };
360
+ let identityRecord = null;
361
+
362
+ if (classes.includes("contracts")) await checkContracts(rootAbs, docs, findings);
363
+ if (classes.includes("drift")) {
364
+ const lock = await checkDrift(rootAbs, findings);
365
+ if (classes.includes("closure")) closureInfo = await checkClosure(rootAbs, lock, findings);
366
+ }
367
+ if (classes.includes("version")) versionInfo = await checkVersion(docs, findings);
368
+ if (classes.includes("docs")) await checkDocs(rootAbs, docs, findings);
369
+ if (classes.includes("git")) git = await checkGit(rootAbs, findings, allowGitSpawn);
370
+ if (classes.includes("identity")) identityRecord = await checkIdentity(rootAbs, docs, findings, profilesRoot);
371
+
372
+ const byClass = {};
373
+ for (const item of findings) {
374
+ byClass[item.class] = (byClass[item.class] ?? 0) + 1;
375
+ }
376
+
377
+ // The handwritten-managed classification fact is reported as data too, so
378
+ // consumers can see which entries would be write-protected.
379
+ const entries = await listTargetEntries(rootAbs);
380
+ const managedDeclarations = [];
381
+ const facts = await loadTargetFacts(rootAbs);
382
+ for (const rel of [...facts.managedSet].sort()) {
383
+ managedDeclarations.push({
384
+ path: rel,
385
+ handwrittenAlso: matchAnyGlob(facts.handwrittenPatterns, rel),
386
+ });
387
+ }
388
+
389
+ return {
390
+ kind: "skill-family.check-report",
391
+ schemaVersion: 1,
392
+ generatedBy: { tool: KIT_TOOL_NAME, version: KIT_VERSION },
393
+ target: { root: ".", entryCount: entries.length },
394
+ ok: findings.length === 0,
395
+ classes: CHECK_CLASSES.map((name) => ({
396
+ name,
397
+ ran: classes.includes(name),
398
+ findings: byClass[name] ?? 0,
399
+ })),
400
+ findings,
401
+ data: {
402
+ closure: closureInfo,
403
+ git,
404
+ version: versionInfo,
405
+ managedDeclarations,
406
+ identity: identityRecord ? {
407
+ record: identityRecord,
408
+ licensing: identityRecord.licensing,
409
+ authors: identityRecord.authors,
410
+ } : null,
411
+ },
412
+ policy:
413
+ "check is diagnosis only: it never writes, never fixes, and never calls git write commands; findings must be resolved by a human or an authorized generator",
414
+ };
415
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import process from "node:process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { bundledProfilesRoot } from "./licensing.mjs";
6
+ import {
7
+ COMMAND_SIDE_EFFECTS,
8
+ KIT_EXIT_CODES,
9
+ REFUSED_MUTATION_FLAGS,
10
+ runCommand,
11
+ TOP_LEVEL_COMMANDS,
12
+ unknownCommandError,
13
+ invalidParamsError,
14
+ mutationModeError,
15
+ } from "./index.mjs";
16
+
17
+ /**
18
+ * skill-family-engineering-kit CLI.
19
+ *
20
+ * Exactly four top-level commands exist; there is no fifth. Exit codes are
21
+ * stable: 0 = ok/clean, 1 = findings (check), 2 = rejected/usage/mechanism
22
+ * error. Every refusal prints its registered SFC code and stable kind.
23
+ */
24
+
25
+ const HELP_TEXT = `skill-family-engineering-kit —— 构建期工程工具包(恰好四个顶层命令)
26
+
27
+ 用法: node src/cli.mjs <command> [options]
28
+
29
+ 命令(四且仅四):
30
+ scaffold 在空目录生成 Skill Family 项目骨架。
31
+ 副作用: 只向空目标目录写入骨架文件(原子写、路径收容);
32
+ 非空或冲突目标被拒绝且不被触碰。
33
+ adopt-plan 严格只读地规划存量仓采用与迁移闭环。
34
+ 副作用: 无 —— 不写任何文件(含临时文件),不运行 git 写命令,
35
+ 不改名、不触碰远端;输出精确写集、冲突、风险、旧实现退出清单、
36
+ 临时例外校验与迁移完成判定(JSON 到 stdout)。
37
+ projection 投影受管生成物。
38
+ 副作用: 只写 manifest 授权且被目标声明为受管的路径;
39
+ 未授权路径、手写文件与越界路径一律拒绝(拒绝时零写入)。
40
+ check 契约/漂移/闭包/版本/文档事实/Git 前置状态诊断。
41
+ 副作用: 无 —— 只诊断、绝不自动修复,git 仅只读探测。
42
+
43
+ 全局选项:
44
+ --root <dir> 目标工作区根目录(默认当前目录)
45
+ --help, -h 显示本帮助或单个命令的帮助
46
+
47
+ 退出码: 0 成功/无发现;1 check 有发现;2 拒绝/用法/机制错误。
48
+ 错误码复用 contracts 的 SFC* 体系(SFC2002/SFC2003/SFC2004 + 稳定 details.kind)。
49
+ 禁止项: git init/commit/push/tag、发布、删除、远端写入;不存在第五个顶层命令。
50
+ `;
51
+
52
+ function commandHelp(command) {
53
+ const sideEffect = COMMAND_SIDE_EFFECTS[command];
54
+ const details = {
55
+ scaffold: [
56
+ "选项:",
57
+ " --root <dir> 目标目录(必须为空,或其父目录存在的不存在路径)",
58
+ " --project-id <id> kebab-case 项目 id(默认取目录名)",
59
+ " --project-name <name> 人类可读项目名(默认同 id)",
60
+ " --profile <id> Profile id(默认 generic)",
61
+ " --licensing-profile <id> 许可证 Profile id(默认 registry 第一个变体)",
62
+ " --licensing-variant <id> 多变体 Profile 的必选变体 id",
63
+ " --profiles-root <dir> 许可证 Profile 根目录",
64
+ ],
65
+ "adopt-plan": [
66
+ "选项:",
67
+ " --root <dir> 目标工作区(默认当前目录)",
68
+ " --project-id <id> 计划采用的项目 id(默认取目录名)",
69
+ " --profile <id> 目标 Profile id(默认 generic)",
70
+ " --licensing-profile <id> 许可证 Profile id(默认 registry 第一个变体)",
71
+ " --licensing-variant <id> 多变体 Profile 的必选变体 id",
72
+ " --profiles-root <dir> 许可证 Profile 根目录",
73
+ " --no-git-spawn 禁用只读 git status 探测,仅用文件系统事实",
74
+ "",
75
+ " 迁移闭环(FND-070): 只读读取目标仓 skill-family.migration.json",
76
+ " (旧实现退出清单 + 临时例外);例外缺 owner/reason/deadline/",
77
+ " migrationTarget 任一字段即计为冲突;到期例外不自动续期;",
78
+ " 旧实现未全部退出时完成判定恒为 false(双轨接入不算完成)。",
79
+ ],
80
+ projection: [
81
+ "选项:",
82
+ " --root <dir> 目标工作区(默认当前目录)",
83
+ " --manifest <relpath> 投影 manifest 相对路径(默认 skill-family.projection.json)",
84
+ ],
85
+ check: [
86
+ "选项:",
87
+ " --root <dir> 目标工作区(默认当前目录)",
88
+ " --only <class> 只运行一个诊断类: contracts|drift|closure|version|docs|git|identity",
89
+ " --profiles-root <dir> 许可证 Profile 根目录",
90
+ " --no-git-spawn 禁用只读 git status 探测,仅用文件系统事实",
91
+ ],
92
+ };
93
+ return [
94
+ `${command} —— ${sideEffect.summary}`,
95
+ `副作用: ${sideEffect.sideEffect}`,
96
+ "",
97
+ ...details[command],
98
+ "",
99
+ `退出码: ${sideEffect.exitCodes}`,
100
+ ].join("\n");
101
+ }
102
+
103
+ function printError(error) {
104
+ const code = error && error.code ? error.code : "SFC2004";
105
+ const kind = error && error.details && error.details.kind ? ` (${error.details.kind})` : "";
106
+ process.stderr.write(`[kit] ${code}${kind}: ${error && error.message ? error.message : String(error)}\n`);
107
+ }
108
+
109
+ function parseOptions(argv, spec) {
110
+ const options = {};
111
+ for (let i = 0; i < argv.length; i += 1) {
112
+ const arg = argv[i];
113
+ if (REFUSED_MUTATION_FLAGS.includes(arg)) {
114
+ throw mutationModeError(arg, spec.command);
115
+ }
116
+ if (arg === "--help" || arg === "-h") {
117
+ options.help = true;
118
+ continue;
119
+ }
120
+ const flagSpec = spec.flags[arg];
121
+ if (!flagSpec) {
122
+ throw invalidParamsError(`unknown option for '${spec.command}': ${arg}`, { flag: arg });
123
+ }
124
+ if (flagSpec.value) {
125
+ const value = argv[++i];
126
+ if (value === undefined || value.startsWith("--")) {
127
+ throw invalidParamsError(`option ${arg} requires a value`, { flag: arg });
128
+ }
129
+ options[flagSpec.key] = value;
130
+ } else {
131
+ options[flagSpec.key] = true;
132
+ }
133
+ }
134
+ return options;
135
+ }
136
+
137
+ const COMMAND_SPECS = {
138
+ scaffold: {
139
+ flags: {
140
+ "--root": { key: "root", value: true },
141
+ "--project-id": { key: "projectId", value: true },
142
+ "--project-name": { key: "projectName", value: true },
143
+ "--profile": { key: "profileId", value: true },
144
+ "--licensing-profile": { key: "licensingProfile", value: true },
145
+ "--licensing-variant": { key: "licensingVariant", value: true },
146
+ "--profiles-root": { key: "profilesRoot", value: true },
147
+ },
148
+ },
149
+ "adopt-plan": {
150
+ flags: {
151
+ "--root": { key: "root", value: true },
152
+ "--project-id": { key: "projectId", value: true },
153
+ "--project-name": { key: "projectName", value: true },
154
+ "--profile": { key: "profileId", value: true },
155
+ "--licensing-profile": { key: "licensingProfile", value: true },
156
+ "--licensing-variant": { key: "licensingVariant", value: true },
157
+ "--profiles-root": { key: "profilesRoot", value: true },
158
+ "--no-git-spawn": { key: "noGitSpawn", value: false },
159
+ },
160
+ },
161
+ projection: {
162
+ flags: {
163
+ "--root": { key: "root", value: true },
164
+ "--manifest": { key: "manifest", value: true },
165
+ },
166
+ },
167
+ check: {
168
+ flags: {
169
+ "--root": { key: "root", value: true },
170
+ "--only": { key: "only", value: true },
171
+ "--profiles-root": { key: "profilesRoot", value: true },
172
+ "--no-git-spawn": { key: "noGitSpawn", value: false },
173
+ },
174
+ },
175
+ };
176
+
177
+ export async function cliMain(argv) {
178
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
179
+ process.stdout.write(HELP_TEXT);
180
+ return KIT_EXIT_CODES.ok;
181
+ }
182
+ const command = argv[0];
183
+ if (!TOP_LEVEL_COMMANDS.includes(command)) {
184
+ printError(unknownCommandError(command));
185
+ return KIT_EXIT_CODES.rejected;
186
+ }
187
+
188
+ try {
189
+ const options = parseOptions(argv.slice(1), { command, flags: COMMAND_SPECS[command].flags });
190
+ if (options.help) {
191
+ process.stdout.write(`${commandHelp(command)}\n`);
192
+ return KIT_EXIT_CODES.ok;
193
+ }
194
+ const { exitCode, output } = await runCommand(command, {
195
+ root: options.root ?? ".",
196
+ projectId: options.projectId,
197
+ projectName: options.projectName,
198
+ profileId: options.profileId,
199
+ licensingProfile: options.licensingProfile,
200
+ licensingVariant: options.licensingVariant,
201
+ profilesRoot: options.profilesRoot ?? bundledProfilesRoot(),
202
+ manifest: options.manifest,
203
+ only: options.only,
204
+ allowGitSpawn: !options.noGitSpawn,
205
+ });
206
+ if (output !== undefined) {
207
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
208
+ }
209
+ return exitCode;
210
+ } catch (error) {
211
+ printError(error);
212
+ return KIT_EXIT_CODES.rejected;
213
+ }
214
+ }
215
+
216
+ // Direct-execution detection must survive symlinked package bins (pnpm
217
+ // .bin shims): compare real paths, not the raw argv[1] URL.
218
+ function isDirectExecution() {
219
+ if (process.argv[1] === undefined) return false;
220
+ try {
221
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+ if (isDirectExecution()) {
227
+ cliMain(process.argv.slice(2)).then((code) => {
228
+ process.exit(code);
229
+ });
230
+ }