create-yss-spec 3.1.0 → 3.1.3

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 (139) hide show
  1. package/README.md +117 -91
  2. package/bin/create-yss-spec.js +11 -3
  3. package/package.json +9 -2
  4. package/src/api/_sync-service.js +191 -0
  5. package/src/api/index.js +22 -0
  6. package/src/api/project-diff.js +9 -0
  7. package/src/api/project-doctor.js +10 -0
  8. package/src/api/template-apply.js +10 -0
  9. package/src/api/template-plan.js +9 -0
  10. package/src/cli/args.js +60 -0
  11. package/src/cli/error-output.js +52 -0
  12. package/src/cli/flags.js +9 -0
  13. package/src/cli/help.js +95 -0
  14. package/src/cli/index.js +82 -0
  15. package/src/cli/plan-output.js +61 -0
  16. package/src/cli/prompts.js +91 -0
  17. package/src/cli/router.js +41 -0
  18. package/src/cli.js +5 -2253
  19. package/src/commands/attach.js +280 -0
  20. package/src/commands/diff.js +23 -0
  21. package/src/commands/doctor.js +417 -0
  22. package/src/commands/init.js +193 -0
  23. package/src/commands/sync.js +146 -0
  24. package/src/commands/update.js +14 -0
  25. package/src/contracts/apply-result-v1.json +45 -0
  26. package/src/contracts/customization-policy-v1.json +28 -0
  27. package/src/contracts/doctor-report-v1.json +29 -0
  28. package/src/contracts/error-envelope-v1.json +24 -0
  29. package/src/contracts/generator-policy-v1.json +24 -0
  30. package/src/contracts/ownership-policy-v1.json +40 -0
  31. package/src/contracts/plan-schema-v1.json +115 -0
  32. package/src/family-identity.js +365 -0
  33. package/src/family-runtime.js +23 -0
  34. package/src/filesystem/apply-plan.js +44 -0
  35. package/src/filesystem/copy-path.js +28 -0
  36. package/src/filesystem/path-utils.js +67 -0
  37. package/src/filesystem/transaction-runner.js +41 -0
  38. package/src/filesystem/transaction.js +153 -0
  39. package/src/git/submodule.js +115 -0
  40. package/src/git/worktree.js +69 -0
  41. package/src/template/attach-planner-runtime.js +51 -0
  42. package/src/template/attach-planner.js +160 -0
  43. package/src/template/instance-runtime.js +463 -0
  44. package/src/template/lifecycle-metadata.js +99 -0
  45. package/src/template/lifecycle-policy.js +129 -0
  46. package/src/template/lifecycle-runtime.js +90 -0
  47. package/src/template/migration-planner.js +95 -0
  48. package/src/template/migration-runtime.js +247 -0
  49. package/src/template/ownership-metadata.js +79 -0
  50. package/src/template/ownership-policy.js +149 -0
  51. package/src/template/ownership-runtime.js +109 -0
  52. package/src/template/plan-schema.js +36 -0
  53. package/src/template/sync-planner-runtime.js +57 -0
  54. package/src/template/sync-planner.js +206 -0
  55. package/src/template/verification-runtime.js +154 -0
  56. package/src/validation/identity.js +41 -0
  57. package/src/validation/metadata.js +105 -0
  58. package/src/validation/security.js +67 -0
  59. package/src/validation/snapshot.js +47 -0
  60. package/template/.agents/skills/.strategic-design-skills-manifest.json +1 -1
  61. package/template/.agents/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
  62. package/template/.agents/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  63. package/template/.agents/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
  64. package/template/.agents/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
  65. package/template/.agents/skills/yss-product-lifecycle/SKILL.md +2 -0
  66. package/template/.agents/skills/yss-ui/SKILL.md +2 -0
  67. package/template/.claude/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
  68. package/template/.claude/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  69. package/template/.claude/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
  70. package/template/.claude/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
  71. package/template/.claude/skills/yss-product-lifecycle/SKILL.md +2 -0
  72. package/template/.claude/skills/yss-ui/SKILL.md +2 -0
  73. package/template/.codex/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
  74. package/template/.codex/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  75. package/template/.codex/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
  76. package/template/.codex/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
  77. package/template/.codex/skills/yss-product-lifecycle/SKILL.md +2 -0
  78. package/template/.codex/skills/yss-ui/SKILL.md +2 -0
  79. package/template/.cursor/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
  80. package/template/.cursor/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  81. package/template/.cursor/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
  82. package/template/.cursor/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
  83. package/template/.cursor/skills/yss-product-lifecycle/SKILL.md +2 -0
  84. package/template/.cursor/skills/yss-ui/SKILL.md +2 -0
  85. package/template/.pi/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
  86. package/template/.pi/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  87. package/template/.pi/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
  88. package/template/.pi/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
  89. package/template/.pi/skills/yss-product-lifecycle/SKILL.md +2 -0
  90. package/template/.pi/skills/yss-ui/SKILL.md +2 -0
  91. package/template/.qoder/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
  92. package/template/.qoder/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  93. package/template/.qoder/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
  94. package/template/.qoder/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
  95. package/template/.qoder/skills/yss-product-lifecycle/SKILL.md +2 -0
  96. package/template/.qoder/skills/yss-ui/SKILL.md +2 -0
  97. package/template/.trae/skills/yss-frontend-scaffold-generator/SKILL.md +2 -0
  98. package/template/.trae/skills/yss-implementation-contract-compiler/SKILL.md +2 -0
  99. package/template/.trae/skills/yss-implementation-contract-compiler/references/compiler-contract.yaml +6 -0
  100. package/template/.trae/skills/yss-implementation-contract-compiler/references/slice-implementation-contract.md +2 -0
  101. package/template/.trae/skills/yss-product-lifecycle/SKILL.md +2 -0
  102. package/template/.trae/skills/yss-ui/SKILL.md +2 -0
  103. package/template/CONTEXT.md +2 -0
  104. package/template/README.md +4 -0
  105. package/template/docs/agents/digital-human-roles.yaml +1 -0
  106. package/template/docs/process/frontend-backend-delivery.md +72 -0
  107. package/template/docs/process/implementation-repo-integration.md +2 -0
  108. package/template/docs/process/lifecycle-artifact-map.md +1 -0
  109. package/template/docs/process/lifecycle-registry-baseline.json +2 -1
  110. package/template/docs/process/lifecycle-registry.yaml +5 -0
  111. package/template/docs/process/schemas/backend-delivery-verification.schema.json +113 -0
  112. package/template/docs/process/schemas/backend-delivery.schema.json +301 -0
  113. package/template/docs/process/schemas/digital-human-task-package.schema.json +8 -0
  114. package/template/docs/process/schemas/frontend-delivery-acceptance.schema.json +200 -0
  115. package/template/docs/process/schemas/frontend-implementation-evidence.schema.json +3 -0
  116. package/template/docs/process/template-verification-profiles.yaml +6 -0
  117. package/template/docs/user-guide//346/210/230/346/234/257/350/256/276/350/256/241/345/255/220/351/241/271/347/233/256/347/224/250/346/210/267/346/211/213/345/206/214.md +20 -206
  118. package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214.md +63 -963
  119. package/template/docs/user-guide//347/224/250/346/210/267/346/211/213/345/206/214/347/264/242/345/274/225.md +11 -14
  120. package/template/docs/user-guide//350/256/276/345/244/207/345/200/237/347/224/250/350/264/257/347/251/277/346/241/210/344/276/213.md +127 -0
  121. package/template/scripts/backend-delivery +14 -0
  122. package/template/scripts/fixtures/backend-delivery/revision-server.mjs +11 -0
  123. package/template/scripts/instantiate-harness +69 -0
  124. package/template/scripts/lib/backend-delivery.mjs +164 -0
  125. package/template/scripts/lib/frontend-delivery-boundary.mjs +49 -0
  126. package/template/scripts/lib/frontend-delivery.mjs +73 -0
  127. package/template/scripts/lib/harness-execution-scope.mjs +52 -0
  128. package/template/scripts/lib/implementation-contract-compiler.mjs +12 -2
  129. package/template/scripts/lib/lifecycle-transition.mjs +7 -0
  130. package/template/scripts/lib/strategic-handoff-consumption.mjs +14 -7
  131. package/template/scripts/lib/strategic-handoff.mjs +1 -1
  132. package/template/scripts/lib/task-package.mjs +7 -0
  133. package/template/scripts/sync-strategic-handoff-tools +3 -1
  134. package/template/scripts/verify-frontend-delivery +9 -0
  135. package/template/scripts/verify-frontend-delivery-scenarios +131 -0
  136. package/template/scripts/verify-frontend-implementation-evidence +2 -0
  137. package/template/skills-lock.json +11 -11
  138. package/template.manifest.json +43 -1
  139. package/template.snapshot.json +5 -5
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ const { parseArgs } = require("../cli/args");
7
+ const { pathKind, targetPath, normalizeRelativePath } = require("../filesystem/path-utils");
8
+ const { applyManagedOperation, applyMigrationOperations } = require("../filesystem/apply-plan");
9
+ const { runInTransaction } = require("../filesystem/transaction-runner");
10
+ const { assertBundledFamily, assertTargetFamily } = require("../family-runtime");
11
+ const { gitDirtyWarning } = require("../git/worktree");
12
+ const { unmanagedPathReason, assertTargetWorkingTreeWritable } = require("../validation/security");
13
+ const { buildAttachPlanFromRuntime } = require("../template/attach-planner-runtime");
14
+ const { buildLegacyMigrationPlan } = require("../template/migration-runtime");
15
+ const {
16
+ PACKAGE_ROOT,
17
+ BUNDLED_TEMPLATE_ROOT,
18
+ TEMPLATE_METADATA_FILENAME,
19
+ fileHash,
20
+ readTemplateSnapshot,
21
+ readTargetIdentity,
22
+ buildAttachDesiredOperations,
23
+ buildMetadata,
24
+ writeTemplateMetadata,
25
+ } = require("../template/instance-runtime");
26
+ const { verifyGeneratedAttach } = require("../template/verification-runtime");
27
+
28
+ function assertRequiredOptions(options) {
29
+ if (!options.projectName) {
30
+ throw new Error("attach 需要 --project-name,项目名称不能为空");
31
+ }
32
+ if (!options.businessDomain) {
33
+ throw new Error("attach 需要 --business-domain,业务领域不能为空");
34
+ }
35
+ if (!options.targetDir) {
36
+ throw new Error("attach 需要 --target-dir,目标目录不能为空");
37
+ }
38
+ }
39
+
40
+ function normalizeTargetDir(targetDir) {
41
+ return path.resolve(process.cwd(), targetDir);
42
+ }
43
+
44
+ function isInsideTemplateRoot(targetDir) {
45
+ const relativePath = path.relative(BUNDLED_TEMPLATE_ROOT, targetDir);
46
+ return (
47
+ relativePath === "" ||
48
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
49
+ );
50
+ }
51
+
52
+ function inspectExistingTargetDir(targetDir, { force = false } = {}) {
53
+ if (isInsideTemplateRoot(targetDir)) {
54
+ throw new Error("目标目录不能位于模板源仓库内部");
55
+ }
56
+ if (!fs.existsSync(targetDir) || pathKind(targetDir) !== "directory") {
57
+ throw new Error("attach 目标目录必须是已经存在的项目目录");
58
+ }
59
+ assertTargetWorkingTreeWritable(targetDir, {
60
+ force,
61
+ packageRoot: PACKAGE_ROOT,
62
+ });
63
+ }
64
+
65
+ function attachVariables(options) {
66
+ return {
67
+ projectName: options.projectName,
68
+ businessDomain: options.businessDomain,
69
+ teamSize: options.teamSize || "待补充",
70
+ issueTracker: options.issueTracker || "github",
71
+ includeExampleDocs:
72
+ options.includeExampleDocs === undefined
73
+ ? true
74
+ : Boolean(options.includeExampleDocs),
75
+ };
76
+ }
77
+
78
+ function shellQuote(value) {
79
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
80
+ }
81
+
82
+ function printLimitedOperations(operations, formatter, limit = 40) {
83
+ const prioritized = [...operations].sort((left, right) => {
84
+ const leftPath = typeof left === "string" ? left : left.relativePath;
85
+ const rightPath = typeof right === "string" ? right : right.relativePath;
86
+ const leftIsRoot = !leftPath.includes("/");
87
+ const rightIsRoot = !rightPath.includes("/");
88
+ if (leftIsRoot !== rightIsRoot) return leftIsRoot ? -1 : 1;
89
+ return leftPath.localeCompare(rightPath);
90
+ });
91
+
92
+ for (const operation of prioritized.slice(0, limit)) {
93
+ console.log(formatter(operation));
94
+ }
95
+ if (operations.length > limit) {
96
+ console.log(
97
+ `... 其余 ${operations.length - limit} 项省略,可用 manifest / git diff 查看完整清单`,
98
+ );
99
+ }
100
+ }
101
+
102
+ function printMigrationPlan(migrationPlan) {
103
+ for (const item of migrationPlan.legacy || []) {
104
+ const destination = item.to ? ` -> ${item.to}` : "";
105
+ console.log(`legacy: ${item.action} ${item.from}${destination}`);
106
+ }
107
+ for (const item of migrationPlan.unsafe || []) {
108
+ console.log(`unsafe: ${item.path} (${item.reason})`);
109
+ }
110
+ for (const item of migrationPlan.conflicts || []) {
111
+ console.log(`conflict: ${item.from} -> ${item.to} (${item.reason})`);
112
+ }
113
+ }
114
+
115
+ function printAttachDryRun(plan, targetDir) {
116
+ console.log("attach dry-run 预览");
117
+ console.log(`目标目录:${targetDir}`);
118
+ printLimitedOperations(
119
+ plan.missing,
120
+ (operation) => `add: ${operation.relativePath}`,
121
+ );
122
+ printLimitedOperations(
123
+ plan.identity,
124
+ (operation) => `identity: ${operation.relativePath}(规范化为 project-instance)`,
125
+ );
126
+ printLimitedOperations(
127
+ plan.conflicts,
128
+ (operation) => `conflict: ${operation.relativePath} (${operation.reason})`,
129
+ );
130
+ printLimitedOperations(
131
+ plan.unsafe,
132
+ (operation) => `unsafe: ${operation.relativePath} (${operation.reason})`,
133
+ );
134
+ if (plan.matched.length > 0) {
135
+ console.log(`matched: ${plan.matched.length} 项内容一致,已纳入 managed baseline`);
136
+ }
137
+ printMigrationPlan(plan.migration);
138
+ console.log(
139
+ `统计:新增 ${plan.missing.length},一致 ${plan.matched.length},身份转换 ${plan.identity.length},冲突 ${plan.conflicts.length},unsafe ${plan.unsafe.length}`,
140
+ );
141
+ if (plan.conflicts.length > 0) {
142
+ console.log("提示:--force 只覆盖 forceable 冲突;mergeStrategy=manual 的冲突必须人工处理");
143
+ }
144
+ }
145
+
146
+ function affectedPathsForManagedOperations(operations) {
147
+ return operations.map((operation) => normalizeRelativePath(operation.relativePath));
148
+ }
149
+
150
+ function affectedPathsForMigration(migrationPlan) {
151
+ return (migrationPlan.operations || []).flatMap((operation) =>
152
+ operation.kind === "move" ? [operation.from, operation.to] : [operation.path],
153
+ );
154
+ }
155
+
156
+ function runAttach(argv = []) {
157
+ const options = parseArgs(argv);
158
+ assertRequiredOptions(options);
159
+
160
+ if (options.dryRun && options.apply) {
161
+ throw new Error("attach 的 --dry-run 与 --apply 互斥");
162
+ }
163
+ if (!options.dryRun && !options.apply) {
164
+ throw new Error("attach 必须显式传入 --dry-run 或 --apply");
165
+ }
166
+
167
+ const snapshot = readTemplateSnapshot();
168
+ assertBundledFamily(snapshot);
169
+ const targetDir = normalizeTargetDir(options.targetDir);
170
+ assertTargetFamily(targetDir);
171
+ inspectExistingTargetDir(targetDir, { force: Boolean(options.force) });
172
+
173
+ if (pathKind(targetPath(targetDir, TEMPLATE_METADATA_FILENAME)) !== "missing") {
174
+ throw new Error("当前项目已有模板元数据,请使用 sync,不要重复 attach");
175
+ }
176
+
177
+ const variables = attachVariables(options);
178
+ const identity = readTargetIdentity(targetDir);
179
+ const desiredOperations = buildAttachDesiredOperations(
180
+ targetDir,
181
+ variables,
182
+ identity,
183
+ );
184
+ const migrationPlan = buildLegacyMigrationPlan(targetDir, desiredOperations, {
185
+ checkFlatTickets: true,
186
+ });
187
+ const warning = gitDirtyWarning(targetDir);
188
+
189
+ const { classified: plan } = buildAttachPlanFromRuntime({
190
+ targetDir,
191
+ desiredOperations,
192
+ migration: migrationPlan,
193
+ warning,
194
+ getUnmanagedReason: (operation) =>
195
+ unmanagedPathReason(targetDir, operation.relativePath, {
196
+ packageRoot: PACKAGE_ROOT,
197
+ }),
198
+ getPathKind: (operation) => pathKind(operation.targetPath),
199
+ getFileHash: (operation) => fileHash(operation.targetPath),
200
+ });
201
+ plan.migration = migrationPlan;
202
+
203
+ if (warning) console.log(warning);
204
+
205
+ if (options.dryRun) {
206
+ printAttachDryRun(plan, targetDir);
207
+ return;
208
+ }
209
+
210
+ if (migrationPlan.unsafe.length > 0) {
211
+ printMigrationPlan(migrationPlan);
212
+ throw new Error(
213
+ "attach 被 unsafe 迁移项阻断;--force 不能绕过,请先人工整理 Ticket 归属",
214
+ );
215
+ }
216
+ if (plan.unsafe.length > 0) {
217
+ printAttachDryRun(plan, targetDir);
218
+ throw new Error(
219
+ "attach 被 unsafe 受管路径阻断;--force 不能绕过,请先人工整理目标路径",
220
+ );
221
+ }
222
+ if (migrationPlan.conflicts.length > 0) {
223
+ printMigrationPlan(migrationPlan);
224
+ throw new Error("attach 被旧路径迁移冲突阻断,请先处理目标冲突");
225
+ }
226
+ if (plan.conflicts.length > 0 && !options.force) {
227
+ printAttachDryRun(plan, targetDir);
228
+ throw new Error(
229
+ "attach 检测到受管文件冲突;请先 dry-run,再使用 --apply --force",
230
+ );
231
+ }
232
+
233
+ const managedToApply = [
234
+ ...plan.missing,
235
+ ...plan.identity,
236
+ ...(options.force ? plan.forceableConflicts : []),
237
+ ];
238
+
239
+ const { backupPath } = runInTransaction({
240
+ targetDir,
241
+ operation: "attach",
242
+ affectedPaths: [
243
+ ...affectedPathsForManagedOperations(managedToApply),
244
+ ...affectedPathsForMigration(migrationPlan),
245
+ TEMPLATE_METADATA_FILENAME,
246
+ ],
247
+ execute: (transaction) => {
248
+ for (const operation of managedToApply) {
249
+ applyManagedOperation(operation, transaction);
250
+ }
251
+ applyMigrationOperations(migrationPlan, transaction);
252
+ verifyGeneratedAttach(targetDir);
253
+ writeTemplateMetadata(
254
+ targetDir,
255
+ buildMetadata(variables, desiredOperations),
256
+ transaction,
257
+ );
258
+ },
259
+ });
260
+
261
+ console.log("接管完成");
262
+ console.log(`目标目录:${targetDir}`);
263
+ console.log(`新增研发管理资产:${plan.missing.length + plan.identity.length}`);
264
+ if (options.force && plan.forceableConflicts.length > 0) {
265
+ console.log(`force 覆盖冲突:${plan.forceableConflicts.length}`);
266
+ }
267
+ const manualConflicts = plan.conflicts.length - plan.forceableConflicts.length;
268
+ if (manualConflicts > 0) {
269
+ console.log(`需人工处理冲突:${manualConflicts}`);
270
+ }
271
+ if (backupPath) {
272
+ console.log(`备份目录:${backupPath}`);
273
+ console.log(`清理命令:rm -rf ${shellQuote(backupPath)}`);
274
+ }
275
+ console.log("下一步建议:运行 git diff 或 git status 检查接管结果");
276
+ }
277
+
278
+ module.exports = {
279
+ runAttach,
280
+ };
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ const { parseArgs } = require("../cli/args");
4
+ const { renderPlanText } = require("../cli/plan-output");
5
+ const { serializePlan } = require("../template/plan-schema");
6
+ const { projectDiff } = require("../api/project-diff");
7
+
8
+ function runDiff(argv = []) {
9
+ const options = parseArgs(argv);
10
+ const plan = projectDiff({
11
+ targetDir: options.targetDir || ".",
12
+ force: Boolean(options.force),
13
+ });
14
+
15
+ if (options.json) {
16
+ process.stdout.write(serializePlan(plan));
17
+ } else {
18
+ process.stdout.write(renderPlanText(plan));
19
+ }
20
+ return plan;
21
+ }
22
+
23
+ module.exports = { runDiff };
@@ -0,0 +1,417 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { spawnSync } = require("node:child_process");
6
+
7
+ const { parseArgs } = require("../cli/args");
8
+ const { pathKind, targetPath } = require("../filesystem/path-utils");
9
+ const { gitDirtyWarning } = require("../git/worktree");
10
+ const { gitlinkWriteViolation } = require("../validation/security");
11
+ const {
12
+ PACKAGE_ROOT,
13
+ PACKAGE_MANIFEST,
14
+ fileHash,
15
+ readTemplateSnapshot,
16
+ readTargetIdentity,
17
+ loadTemplateMetadata,
18
+ } = require("../template/instance-runtime");
19
+ const {
20
+ ownershipPolicyDrift,
21
+ } = require("../template/ownership-metadata");
22
+ const {
23
+ lifecyclePolicyDrift,
24
+ } = require("../template/lifecycle-metadata");
25
+
26
+ const DOCTOR_SCHEMA_VERSION = 1;
27
+ const VERIFIERS = [
28
+ {
29
+ name: "verifier-sync-skills",
30
+ path: "scripts/sync-skills",
31
+ args: ["--check"],
32
+ },
33
+ {
34
+ name: "verifier-skill-lock",
35
+ path: "scripts/update-skill-lock",
36
+ args: ["--check"],
37
+ },
38
+ {
39
+ name: "verifier-template",
40
+ path: "scripts/verify-template",
41
+ args: [],
42
+ requiresGit: true,
43
+ },
44
+ ];
45
+
46
+ function normalizeTargetDir(value) {
47
+ return path.resolve(process.cwd(), value || ".");
48
+ }
49
+
50
+ function addCheck(report, name, status, detail, data = undefined) {
51
+ const check = { name, status, detail };
52
+ if (data !== undefined) check.data = data;
53
+ report.checks.push(check);
54
+ if (status === "error") report.ok = false;
55
+ return check;
56
+ }
57
+
58
+ function isGitWorktree(targetDir) {
59
+ const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
60
+ cwd: targetDir,
61
+ encoding: "utf8",
62
+ timeout: 5000,
63
+ });
64
+ return result.status === 0 && (result.stdout || "").trim() === "true";
65
+ }
66
+
67
+ function checkManagedBaseline(report, targetDir, metadata) {
68
+ const managedFiles = metadata.managedFiles || {};
69
+ const stats = {
70
+ total: 0,
71
+ matched: 0,
72
+ modified: 0,
73
+ missing: 0,
74
+ invalid: 0,
75
+ };
76
+
77
+ for (const [relativePath, record] of Object.entries(managedFiles)) {
78
+ stats.total += 1;
79
+ if (
80
+ !record ||
81
+ typeof record !== "object" ||
82
+ Array.isArray(record) ||
83
+ !/^[0-9a-f]{64}$/.test(record.contentHash || "")
84
+ ) {
85
+ stats.invalid += 1;
86
+ continue;
87
+ }
88
+
89
+ let absolutePath;
90
+ try {
91
+ absolutePath = targetPath(targetDir, relativePath);
92
+ } catch {
93
+ stats.invalid += 1;
94
+ continue;
95
+ }
96
+
97
+ const kind = pathKind(absolutePath);
98
+ if (kind === "missing") {
99
+ stats.missing += 1;
100
+ continue;
101
+ }
102
+ if (kind !== "file") {
103
+ stats.invalid += 1;
104
+ continue;
105
+ }
106
+
107
+ if (fileHash(absolutePath) === record.contentHash) {
108
+ stats.matched += 1;
109
+ } else {
110
+ stats.modified += 1;
111
+ }
112
+ }
113
+
114
+ if (stats.invalid > 0) {
115
+ addCheck(
116
+ report,
117
+ "managed-baseline",
118
+ "error",
119
+ `managed baseline 含 ${stats.invalid} 个非法记录或不安全路径`,
120
+ stats,
121
+ );
122
+ } else if (stats.missing > 0 || stats.modified > 0) {
123
+ addCheck(
124
+ report,
125
+ "managed-baseline",
126
+ "warning",
127
+ `受管基线存在漂移:本地修改 ${stats.modified},缺失 ${stats.missing}`,
128
+ stats,
129
+ );
130
+ } else {
131
+ addCheck(
132
+ report,
133
+ "managed-baseline",
134
+ "ok",
135
+ `受管基线一致:${stats.matched}/${stats.total}`,
136
+ stats,
137
+ );
138
+ }
139
+ }
140
+
141
+ function checkOwnershipPolicy(report, metadata) {
142
+ const drift = ownershipPolicyDrift(metadata);
143
+ if (drift.state === "missing") {
144
+ addCheck(
145
+ report,
146
+ "ownership-policy",
147
+ "warning",
148
+ "实例 metadata 尚未记录 ownership policy baseline;下一次 attach/sync 会自动补齐",
149
+ drift,
150
+ );
151
+ return;
152
+ }
153
+ if (drift.state === "drift") {
154
+ addCheck(
155
+ report,
156
+ "ownership-policy",
157
+ "warning",
158
+ `ownership policy baseline 与当前 CLI 不一致:v${drift.actualVersion} -> v${drift.expectedVersion}`,
159
+ drift,
160
+ );
161
+ return;
162
+ }
163
+ addCheck(
164
+ report,
165
+ "ownership-policy",
166
+ "ok",
167
+ `ownership policy baseline 一致:v${drift.expectedVersion}`,
168
+ drift,
169
+ );
170
+ }
171
+
172
+ function addLifecycleDriftCheck(report, name, label, drift) {
173
+ if (drift.state === "missing") {
174
+ addCheck(
175
+ report,
176
+ name,
177
+ "warning",
178
+ `实例 metadata 尚未记录 ${label} baseline;下一次 attach/sync 会自动补齐`,
179
+ drift,
180
+ );
181
+ return;
182
+ }
183
+ if (drift.state === "drift") {
184
+ addCheck(
185
+ report,
186
+ name,
187
+ "warning",
188
+ `${label} baseline 与当前 CLI 不一致:v${drift.actual.version} -> v${drift.expected.version}`,
189
+ drift,
190
+ );
191
+ return;
192
+ }
193
+ addCheck(
194
+ report,
195
+ name,
196
+ "ok",
197
+ `${label} baseline 一致:v${drift.expected.version}`,
198
+ drift,
199
+ );
200
+ }
201
+
202
+ function checkLifecyclePolicies(report, metadata) {
203
+ const drift = lifecyclePolicyDrift(metadata);
204
+ addLifecycleDriftCheck(
205
+ report,
206
+ "customization-policy",
207
+ "customization policy",
208
+ drift.customization,
209
+ );
210
+ addLifecycleDriftCheck(
211
+ report,
212
+ "generator-policy",
213
+ "generator policy",
214
+ drift.generator,
215
+ );
216
+ }
217
+
218
+ function runVerifierCheck(report, targetDir, verifier, gitWorktree) {
219
+ const commandPath = targetPath(targetDir, verifier.path);
220
+ if (pathKind(commandPath) !== "file") {
221
+ addCheck(
222
+ report,
223
+ verifier.name,
224
+ "error",
225
+ `缺少 verifier:${verifier.path}`,
226
+ );
227
+ return;
228
+ }
229
+
230
+ if (verifier.requiresGit && !gitWorktree) {
231
+ addCheck(
232
+ report,
233
+ verifier.name,
234
+ "warning",
235
+ `未执行 ${verifier.path}:目标目录不是 Git worktree;doctor 不会临时 git init`,
236
+ );
237
+ return;
238
+ }
239
+
240
+ const result = spawnSync(commandPath, verifier.args || [], {
241
+ cwd: targetDir,
242
+ encoding: "utf8",
243
+ timeout: 30000,
244
+ });
245
+ const output = [result.stdout, result.stderr]
246
+ .filter(Boolean)
247
+ .join("")
248
+ .trim();
249
+
250
+ if (result.error || result.status !== 0) {
251
+ addCheck(
252
+ report,
253
+ verifier.name,
254
+ "error",
255
+ `${verifier.path} 校验失败${output ? `:${output}` : ""}`,
256
+ { exitCode: result.status },
257
+ );
258
+ return;
259
+ }
260
+
261
+ addCheck(
262
+ report,
263
+ verifier.name,
264
+ "ok",
265
+ `${verifier.path} 校验通过`,
266
+ { exitCode: result.status },
267
+ );
268
+ }
269
+
270
+ function buildDoctorReport(targetDir) {
271
+ const report = {
272
+ schemaVersion: DOCTOR_SCHEMA_VERSION,
273
+ operation: "doctor",
274
+ targetDir,
275
+ cliVersion: PACKAGE_MANIFEST.version,
276
+ ok: true,
277
+ checks: [],
278
+ };
279
+
280
+ let snapshot = null;
281
+ try {
282
+ snapshot = readTemplateSnapshot();
283
+ addCheck(
284
+ report,
285
+ "template-snapshot",
286
+ "ok",
287
+ `模板快照有效:${snapshot.templateCommit}`,
288
+ { templateCommit: snapshot.templateCommit },
289
+ );
290
+ } catch (error) {
291
+ addCheck(report, "template-snapshot", "error", error.message);
292
+ }
293
+
294
+ if (!fs.existsSync(targetDir) || pathKind(targetDir) !== "directory") {
295
+ addCheck(report, "target-directory", "error", "目标目录不存在或不是目录");
296
+ return report;
297
+ }
298
+ addCheck(report, "target-directory", "ok", "目标目录可读取");
299
+
300
+ const violation = gitlinkWriteViolation(targetDir, {
301
+ packageRoot: PACKAGE_ROOT,
302
+ });
303
+ if (violation) {
304
+ addCheck(report, "git-safety", "error", violation);
305
+ } else {
306
+ addCheck(report, "git-safety", "ok", "未检测到 gitlink / detached HEAD 写入风险");
307
+ }
308
+
309
+ let metadata = null;
310
+ try {
311
+ ({ metadata } = loadTemplateMetadata(targetDir));
312
+ addCheck(
313
+ report,
314
+ "template-metadata",
315
+ "ok",
316
+ `模板元数据有效:${metadata.templateVersion || metadata.cliVersion || "unknown"}`,
317
+ {
318
+ metadataSchemaVersion: metadata.metadataSchemaVersion,
319
+ templateVersion: metadata.templateVersion || metadata.cliVersion || null,
320
+ templateCommit: metadata.templateCommit || null,
321
+ },
322
+ );
323
+
324
+ if (snapshot && metadata.templateCommit !== snapshot.templateCommit) {
325
+ addCheck(
326
+ report,
327
+ "template-drift",
328
+ "warning",
329
+ `实例模板 commit 与当前 CLI 快照不同:${metadata.templateCommit} -> ${snapshot.templateCommit}`,
330
+ {
331
+ from: metadata.templateCommit,
332
+ to: snapshot.templateCommit,
333
+ },
334
+ );
335
+ } else if (snapshot) {
336
+ addCheck(report, "template-drift", "ok", "实例模板 commit 与当前 CLI 快照一致");
337
+ }
338
+
339
+ checkOwnershipPolicy(report, metadata);
340
+ checkLifecyclePolicies(report, metadata);
341
+ checkManagedBaseline(report, targetDir, metadata);
342
+ } catch (error) {
343
+ addCheck(report, "template-metadata", "error", error.message);
344
+ }
345
+
346
+ try {
347
+ const identity = readTargetIdentity(targetDir);
348
+ if (identity.state !== "valid") {
349
+ addCheck(report, "project-identity", "error", "缺少 yss-project.yaml");
350
+ } else if (identity.fields.repository_mode !== "project-instance") {
351
+ addCheck(
352
+ report,
353
+ "project-identity",
354
+ "error",
355
+ `repository_mode 不是 project-instance:${identity.fields.repository_mode}`,
356
+ );
357
+ } else {
358
+ addCheck(report, "project-identity", "ok", "yss-project.yaml 为 project-instance");
359
+ }
360
+ } catch (error) {
361
+ addCheck(report, "project-identity", "error", error.message);
362
+ }
363
+
364
+ const gitWorktree = isGitWorktree(targetDir);
365
+ const dirtyWarning = gitDirtyWarning(targetDir);
366
+ if (dirtyWarning) {
367
+ addCheck(report, "git-worktree", "warning", dirtyWarning);
368
+ } else if (gitWorktree) {
369
+ addCheck(report, "git-worktree", "ok", "Git worktree 无需警告");
370
+ } else {
371
+ addCheck(report, "git-worktree", "warning", "目标目录不是 Git worktree");
372
+ }
373
+
374
+ for (const verifier of VERIFIERS) {
375
+ runVerifierCheck(report, targetDir, verifier, gitWorktree);
376
+ }
377
+
378
+ return report;
379
+ }
380
+
381
+ function renderDoctorText(report) {
382
+ const lines = [
383
+ "create-yss-spec doctor",
384
+ `目标目录:${report.targetDir}`,
385
+ `结果:${report.ok ? "PASS" : "FAIL"}`,
386
+ ];
387
+ for (const check of report.checks) {
388
+ lines.push(`[${check.status.toUpperCase()}] ${check.name}: ${check.detail}`);
389
+ }
390
+ return `${lines.join("\n")}\n`;
391
+ }
392
+
393
+ function runDoctor(argv = []) {
394
+ const options = parseArgs(argv);
395
+ const targetDir = normalizeTargetDir(options.targetDir);
396
+ const report = buildDoctorReport(targetDir);
397
+
398
+ if (options.json) {
399
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
400
+ } else {
401
+ process.stdout.write(renderDoctorText(report));
402
+ }
403
+
404
+ return report;
405
+ }
406
+
407
+ module.exports = {
408
+ DOCTOR_SCHEMA_VERSION,
409
+ VERIFIERS,
410
+ checkManagedBaseline,
411
+ checkOwnershipPolicy,
412
+ checkLifecyclePolicies,
413
+ runVerifierCheck,
414
+ buildDoctorReport,
415
+ renderDoctorText,
416
+ runDoctor,
417
+ };