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,109 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ const {
7
+ DEFAULT_OWNERSHIP_POLICY,
8
+ validateOwnershipPolicy,
9
+ resolveOwnership,
10
+ ownershipWriteViolation,
11
+ isTemplateManagedOwnership,
12
+ } = require("./ownership-policy");
13
+
14
+ const PACKAGE_ROOT = path.resolve(__dirname, "../..");
15
+ const MANIFEST_PATH = path.join(PACKAGE_ROOT, "template.manifest.json");
16
+
17
+ function loadOwnershipPolicy() {
18
+ const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
19
+ return validateOwnershipPolicy(
20
+ manifest.ownershipPolicy || DEFAULT_OWNERSHIP_POLICY,
21
+ );
22
+ }
23
+
24
+ const OWNERSHIP_POLICY = loadOwnershipPolicy();
25
+
26
+ function resolveTemplateOwnership(relativePath) {
27
+ return resolveOwnership(relativePath, OWNERSHIP_POLICY);
28
+ }
29
+
30
+ function applyOwnershipToOperations(operations = []) {
31
+ return operations.map((operation) => ({
32
+ ...operation,
33
+ ownership:
34
+ operation.ownership || resolveTemplateOwnership(operation.relativePath),
35
+ }));
36
+ }
37
+
38
+ function ownershipAwareUnmanagedReason(operation, fallback = null) {
39
+ const violation = ownershipWriteViolation(operation);
40
+ if (violation) return violation;
41
+ return typeof fallback === "function" ? fallback(operation) : null;
42
+ }
43
+
44
+ function assertWritableTemplateOperations(operations = []) {
45
+ for (const operation of applyOwnershipToOperations(operations)) {
46
+ const violation = ownershipWriteViolation(operation);
47
+ if (violation) throw new Error(violation);
48
+ }
49
+ }
50
+
51
+ function decoratePlanWithOwnership(plan, desiredOperations = []) {
52
+ const operations = applyOwnershipToOperations(desiredOperations);
53
+ const ownershipByPath = new Map(
54
+ operations.map((operation) => [operation.relativePath, operation.ownership]),
55
+ );
56
+ const ownershipFor = (item) =>
57
+ ownershipByPath.get(item.path) || resolveTemplateOwnership(item.path);
58
+
59
+ return {
60
+ ...plan,
61
+ changes: (plan.changes || []).map((item) => ({
62
+ ...item,
63
+ ownership: item.ownership || ownershipFor(item),
64
+ })),
65
+ conflicts: (plan.conflicts || []).map((item) => ({
66
+ ...item,
67
+ ownership: item.ownership || ownershipFor(item),
68
+ })),
69
+ unsafe: (plan.unsafe || []).map((item) => ({
70
+ ...item,
71
+ ownership: item.ownership || ownershipFor(item),
72
+ })),
73
+ };
74
+ }
75
+
76
+ function summarizeOwnership(operations = []) {
77
+ const summary = Object.fromEntries(
78
+ [
79
+ "managed",
80
+ "managed-customizable",
81
+ "generated",
82
+ "user-owned",
83
+ "protected",
84
+ ].map((name) => [name, 0]),
85
+ );
86
+ for (const operation of applyOwnershipToOperations(operations)) {
87
+ summary[operation.ownership] += 1;
88
+ }
89
+ return summary;
90
+ }
91
+
92
+ function templateManagedOperations(operations = []) {
93
+ return applyOwnershipToOperations(operations).filter((operation) =>
94
+ isTemplateManagedOwnership(operation.ownership),
95
+ );
96
+ }
97
+
98
+ module.exports = {
99
+ PACKAGE_ROOT,
100
+ OWNERSHIP_POLICY,
101
+ loadOwnershipPolicy,
102
+ resolveTemplateOwnership,
103
+ applyOwnershipToOperations,
104
+ ownershipAwareUnmanagedReason,
105
+ assertWritableTemplateOperations,
106
+ decoratePlanWithOwnership,
107
+ summarizeOwnership,
108
+ templateManagedOperations,
109
+ };
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ const PLAN_SCHEMA_VERSION = 1;
4
+
5
+ function normalizePlan(plan) {
6
+ if (!plan || typeof plan !== "object" || Array.isArray(plan)) {
7
+ throw new TypeError("plan must be an object");
8
+ }
9
+ if (!plan.operation) {
10
+ throw new TypeError("plan.operation is required");
11
+ }
12
+
13
+ return {
14
+ schemaVersion: PLAN_SCHEMA_VERSION,
15
+ operation: plan.operation,
16
+ targetDir: plan.targetDir || null,
17
+ template: plan.template || null,
18
+ changes: Array.isArray(plan.changes) ? plan.changes : [],
19
+ conflicts: Array.isArray(plan.conflicts) ? plan.conflicts : [],
20
+ unsafe: Array.isArray(plan.unsafe) ? plan.unsafe : [],
21
+ warnings: Array.isArray(plan.warnings) ? plan.warnings : [],
22
+ blocked: Boolean(plan.blocked),
23
+ stats: plan.stats && typeof plan.stats === "object" ? plan.stats : {},
24
+ migration: plan.migration || null,
25
+ };
26
+ }
27
+
28
+ function serializePlan(plan, space = 2) {
29
+ return `${JSON.stringify(normalizePlan(plan), null, space)}\n`;
30
+ }
31
+
32
+ module.exports = {
33
+ PLAN_SCHEMA_VERSION,
34
+ normalizePlan,
35
+ serializePlan,
36
+ };
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+
3
+ const {
4
+ classifySyncOperations,
5
+ createSyncPlan,
6
+ } = require("./sync-planner");
7
+ const {
8
+ ownershipAwareUnmanagedReason,
9
+ decoratePlanWithOwnership,
10
+ } = require("./ownership-runtime");
11
+ const {
12
+ applyLifecycleToOperations,
13
+ decoratePlanWithLifecycle,
14
+ } = require("./lifecycle-runtime");
15
+
16
+ function buildSyncPlanFromRuntime({
17
+ targetDir,
18
+ metadata,
19
+ desiredOperations,
20
+ migration,
21
+ warning = null,
22
+ toVersion = "unknown",
23
+ getUnmanagedReason,
24
+ getPathKind,
25
+ getFileHash,
26
+ }) {
27
+ const managedFiles = metadata?.managedFiles || {};
28
+ const lifecycleOperations = applyLifecycleToOperations(desiredOperations);
29
+ const classified = classifySyncOperations({
30
+ managedFiles,
31
+ desiredOperations: lifecycleOperations,
32
+ getUnmanagedReason: (operation) =>
33
+ ownershipAwareUnmanagedReason(operation, getUnmanagedReason),
34
+ getPathKind,
35
+ getFileHash,
36
+ });
37
+
38
+ const basePlan = createSyncPlan({
39
+ targetDir,
40
+ fromVersion: metadata?.templateVersion || metadata?.cliVersion || "unknown",
41
+ toVersion,
42
+ classified,
43
+ migration,
44
+ warnings: warning ? [warning] : [],
45
+ });
46
+ const ownershipPlan = decoratePlanWithOwnership(basePlan, lifecycleOperations);
47
+ const plan = decoratePlanWithLifecycle(ownershipPlan, lifecycleOperations);
48
+
49
+ return {
50
+ classified,
51
+ plan,
52
+ };
53
+ }
54
+
55
+ module.exports = {
56
+ buildSyncPlanFromRuntime,
57
+ };
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+
3
+ const { normalizePlan } = require("./plan-schema");
4
+
5
+ function classifySyncOperations({
6
+ managedFiles = {},
7
+ desiredOperations = [],
8
+ getUnmanagedReason = () => null,
9
+ getPathKind,
10
+ getFileHash,
11
+ }) {
12
+ if (typeof getPathKind !== "function") {
13
+ throw new TypeError("classifySyncOperations requires getPathKind");
14
+ }
15
+ if (typeof getFileHash !== "function") {
16
+ throw new TypeError("classifySyncOperations requires getFileHash");
17
+ }
18
+
19
+ const updated = [];
20
+ const added = [];
21
+ const unchanged = [];
22
+ const skipped = [];
23
+ const conflicts = [];
24
+ const forceableConflicts = [];
25
+ const unsafe = [];
26
+
27
+ for (const operation of desiredOperations) {
28
+ const unmanagedReason = getUnmanagedReason(operation);
29
+ if (unmanagedReason) {
30
+ unsafe.push({ ...operation, reason: unmanagedReason });
31
+ continue;
32
+ }
33
+
34
+ const existingRecord = managedFiles[operation.relativePath];
35
+ const existingKind = getPathKind(operation);
36
+
37
+ if (existingKind === "missing") {
38
+ added.push(operation);
39
+ continue;
40
+ }
41
+
42
+ if (existingKind !== "file") {
43
+ unsafe.push({
44
+ ...operation,
45
+ reason: `目标路径类型为 ${existingKind},无法安全判断是否可以替换`,
46
+ });
47
+ continue;
48
+ }
49
+
50
+ const currentHash = getFileHash(operation);
51
+
52
+ if (operation.identityConversion && currentHash === operation.identitySourceHash) {
53
+ updated.push(operation);
54
+ continue;
55
+ }
56
+
57
+ if (!existingRecord) {
58
+ if (currentHash === operation.desiredHash) {
59
+ unchanged.push(operation);
60
+ } else {
61
+ const conflict = {
62
+ ...operation,
63
+ reason: "文件已存在,但不在受管模板文件基线中",
64
+ };
65
+ conflicts.push(conflict);
66
+ skipped.push(conflict);
67
+ }
68
+ continue;
69
+ }
70
+
71
+ if (currentHash === operation.desiredHash) {
72
+ unchanged.push(operation);
73
+ continue;
74
+ }
75
+
76
+ if (currentHash !== existingRecord.contentHash) {
77
+ const conflict = {
78
+ ...operation,
79
+ reason:
80
+ operation.mergeStrategy === "manual"
81
+ ? "检测到本地已修改的可定制文件;mergeStrategy=manual,必须人工处理"
82
+ : "检测到本地已修改的受管文件",
83
+ };
84
+ conflicts.push(conflict);
85
+ if (operation.mergeStrategy !== "manual") {
86
+ forceableConflicts.push(conflict);
87
+ }
88
+ skipped.push(conflict);
89
+ continue;
90
+ }
91
+
92
+ updated.push(operation);
93
+ }
94
+
95
+ const desiredPathSet = new Set(desiredOperations.map((operation) => operation.relativePath));
96
+ const removed = Object.keys(managedFiles).filter(
97
+ (relativePath) => !desiredPathSet.has(relativePath),
98
+ );
99
+
100
+ return {
101
+ updated,
102
+ added,
103
+ unchanged,
104
+ skipped,
105
+ conflicts,
106
+ forceableConflicts,
107
+ unsafe,
108
+ removed,
109
+ desiredOperations,
110
+ };
111
+ }
112
+
113
+ function createSyncPlan({
114
+ targetDir,
115
+ fromVersion = "unknown",
116
+ toVersion = "unknown",
117
+ classified,
118
+ migration = null,
119
+ warnings = [],
120
+ }) {
121
+ if (!classified || typeof classified !== "object") {
122
+ throw new TypeError("createSyncPlan requires classified sync operations");
123
+ }
124
+
125
+ const migrationConflicts = migration?.conflicts || [];
126
+ const migrationUnsafe = migration?.unsafe || [];
127
+ const blocked =
128
+ classified.unsafe.length > 0 ||
129
+ migrationConflicts.length > 0 ||
130
+ migrationUnsafe.length > 0;
131
+
132
+ const changes = [
133
+ ...classified.updated.map((operation) => ({
134
+ action: "update",
135
+ path: operation.relativePath,
136
+ })),
137
+ ...classified.added.map((operation) => ({
138
+ action: "add",
139
+ path: operation.relativePath,
140
+ })),
141
+ ...classified.removed.map((relativePath) => ({
142
+ action: "remove-report",
143
+ path: relativePath,
144
+ })),
145
+ ];
146
+
147
+ const classifiedConflicts = classified.conflicts.map((operation) => ({
148
+ path: operation.relativePath,
149
+ reason: operation.reason,
150
+ forceable: classified.forceableConflicts.includes(operation),
151
+ source: "managed-file",
152
+ }));
153
+ const normalizedMigrationConflicts = migrationConflicts.map((item) => ({
154
+ path: item.from || item.path || item.to,
155
+ from: item.from,
156
+ to: item.to,
157
+ reason: item.reason,
158
+ forceable: false,
159
+ source: "migration",
160
+ }));
161
+
162
+ return normalizePlan({
163
+ operation: "sync",
164
+ targetDir,
165
+ template: {
166
+ from: fromVersion,
167
+ to: toVersion,
168
+ },
169
+ changes,
170
+ conflicts: [...classifiedConflicts, ...normalizedMigrationConflicts],
171
+ unsafe: [
172
+ ...classified.unsafe.map((operation) => ({
173
+ path: operation.relativePath,
174
+ reason: operation.reason,
175
+ source: "managed-file",
176
+ })),
177
+ ...migrationUnsafe.map((item) => ({
178
+ path: item.path,
179
+ reason: item.reason,
180
+ source: "migration",
181
+ })),
182
+ ],
183
+ migration: migration || {
184
+ operations: [],
185
+ legacy: [],
186
+ conflicts: [],
187
+ unsafe: [],
188
+ },
189
+ warnings,
190
+ blocked,
191
+ stats: {
192
+ updated: classified.updated.length,
193
+ added: classified.added.length,
194
+ unchanged: classified.unchanged.length,
195
+ skipped: classified.skipped.length,
196
+ conflicts: classified.conflicts.length + migrationConflicts.length,
197
+ unsafe: classified.unsafe.length + migrationUnsafe.length,
198
+ removed: classified.removed.length,
199
+ },
200
+ });
201
+ }
202
+
203
+ module.exports = {
204
+ classifySyncOperations,
205
+ createSyncPlan,
206
+ };
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { spawnSync } = require("node:child_process");
7
+
8
+ const { targetPath, pathKind } = require("../filesystem/path-utils");
9
+ const {
10
+ TEMPLATE_MANIFEST,
11
+ readTargetIdentity,
12
+ } = require("./instance-runtime");
13
+
14
+ const INSTANCE_FORBIDDEN_PATHS = [
15
+ ".template-source",
16
+ ".github",
17
+ ".cursor/environment.json",
18
+ "wiki",
19
+ "docs/reviews",
20
+ ];
21
+
22
+ function verificationEnvironment(environment = process.env) {
23
+ const sanitized = { ...environment };
24
+ for (const key of Object.keys(sanitized)) {
25
+ if (key === "NODE_TEST_CONTEXT" || key.startsWith("NODE_TEST_")) {
26
+ delete sanitized[key];
27
+ }
28
+ }
29
+ return sanitized;
30
+ }
31
+
32
+ function initializeGitRepository(targetDir) {
33
+ const result = spawnSync("git", ["init"], {
34
+ cwd: targetDir,
35
+ encoding: "utf8",
36
+ });
37
+ if (result.status !== 0) {
38
+ throw new Error(result.stderr.trim() || "git init 执行失败");
39
+ }
40
+ }
41
+
42
+ function runTemplateVerification(targetDir, scriptPath, args = ["--check"]) {
43
+ const commandPath = targetPath(targetDir, scriptPath);
44
+ const result = spawnSync(commandPath, args, {
45
+ cwd: targetDir,
46
+ encoding: "utf8",
47
+ env: verificationEnvironment(),
48
+ });
49
+ const output = [result.stdout, result.stderr].filter(Boolean).join("");
50
+ if (output) {
51
+ process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
52
+ }
53
+ if (result.status !== 0) {
54
+ const detail = result.error?.message || output.trim();
55
+ throw new Error(
56
+ `生成项目校验失败:${scriptPath}${detail ? `\n${detail}` : ""}`,
57
+ );
58
+ }
59
+ }
60
+
61
+ function runTemplateVerificationWithGit(targetDir, scriptPath, args = []) {
62
+ const gitPath = targetPath(targetDir, ".git");
63
+ const probe = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
64
+ cwd: targetDir,
65
+ encoding: "utf8",
66
+ });
67
+ if (probe.status === 0 && probe.stdout.trim() === "true") {
68
+ runTemplateVerification(targetDir, scriptPath, args);
69
+ return;
70
+ }
71
+
72
+ let backupRoot;
73
+ let backupGitPath;
74
+ try {
75
+ try {
76
+ fs.lstatSync(gitPath);
77
+ backupRoot = fs.mkdtempSync(
78
+ path.join(os.tmpdir(), "create-yss-spec-git-backup-"),
79
+ );
80
+ backupGitPath = path.join(backupRoot, ".git");
81
+ fs.renameSync(gitPath, backupGitPath);
82
+ } catch (error) {
83
+ if (error.code !== "ENOENT") throw error;
84
+ }
85
+
86
+ initializeGitRepository(targetDir);
87
+ runTemplateVerification(targetDir, scriptPath, args);
88
+ } finally {
89
+ fs.rmSync(gitPath, { recursive: true, force: true });
90
+ if (backupGitPath) fs.renameSync(backupGitPath, gitPath);
91
+ if (backupRoot) fs.rmSync(backupRoot, { recursive: true, force: true });
92
+ }
93
+ }
94
+
95
+ function verifyGeneratedInstance(targetDir, { checkForbiddenPaths = false } = {}) {
96
+ if (checkForbiddenPaths) {
97
+ const forbiddenPaths = [
98
+ ...INSTANCE_FORBIDDEN_PATHS,
99
+ ...(TEMPLATE_MANIFEST.initExcludeRootEntries || []),
100
+ ...(TEMPLATE_MANIFEST.initExcludeRootFiles || []),
101
+ ...(TEMPLATE_MANIFEST.initExcludePaths || []),
102
+ ];
103
+ for (const relativePath of [...new Set(forbiddenPaths)]) {
104
+ if (pathKind(targetPath(targetDir, relativePath)) !== "missing") {
105
+ throw new Error(`初始化结果包含禁止分发的模板源资产:${relativePath}`);
106
+ }
107
+ }
108
+ }
109
+
110
+ const identity = readTargetIdentity(targetDir);
111
+ if (
112
+ identity.state !== "valid" ||
113
+ identity.fields.repository_mode !== "project-instance"
114
+ ) {
115
+ throw new Error("初始化结果的 yss-project.yaml 必须是 project-instance");
116
+ }
117
+
118
+ const agentsContent = fs.readFileSync(targetPath(targetDir, "AGENTS.md"), "utf8");
119
+ const readmeContent = fs.readFileSync(targetPath(targetDir, "README.md"), "utf8");
120
+ if (agentsContent.includes("[填写]") || readmeContent.includes("[填写]")) {
121
+ throw new Error("初始化结果仍包含模板占位信息");
122
+ }
123
+ }
124
+
125
+ function verifyGeneratedInit(targetDir) {
126
+ verifyGeneratedInstance(targetDir, { checkForbiddenPaths: true });
127
+ }
128
+
129
+ function verifyGeneratedAttach(targetDir) {
130
+ runTemplateVerification(targetDir, "scripts/sync-skills");
131
+ runTemplateVerification(targetDir, "scripts/update-skill-lock");
132
+ // Attach validates the generated project instance, not the template source
133
+ // release candidate. The release profile intentionally runs source-governance
134
+ // checks (including external tooling such as ripgrep) that are inappropriate
135
+ // as a runtime dependency for end-user projects. Use the fast profile with a
136
+ // known routed instance path so required files, repository identity and
137
+ // read-only Git invariants are still checked without escalating to release.
138
+ runTemplateVerificationWithGit(targetDir, "scripts/run-template-verification", [
139
+ "--profile",
140
+ "fast",
141
+ "--changed-file",
142
+ "README.md",
143
+ ]);
144
+ }
145
+
146
+ module.exports = {
147
+ verificationEnvironment,
148
+ initializeGitRepository,
149
+ runTemplateVerification,
150
+ runTemplateVerificationWithGit,
151
+ verifyGeneratedInstance,
152
+ verifyGeneratedInit,
153
+ verifyGeneratedAttach,
154
+ };
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+
3
+ function parseRepositoryIdentity(content) {
4
+ const fields = {};
5
+ for (const [index, line] of String(content).split(/\r?\n/).entries()) {
6
+ const trimmed = line.trim();
7
+ if (!trimmed || trimmed.startsWith("#")) continue;
8
+ const match = line.match(/^([a-z_][a-z0-9_-]*):\s*([^\s#]+)\s*$/);
9
+ if (!match) throw new Error(`yss-project.yaml 第 ${index + 1} 行格式非法`);
10
+ const [, key, value] = match;
11
+ if (fields[key] !== undefined) throw new Error(`yss-project.yaml 不允许重复字段:${key}`);
12
+ fields[key] = value;
13
+ }
14
+
15
+ const keys = Object.keys(fields).sort();
16
+ if (keys.join(",") !== "repository_mode,schema_version") {
17
+ throw new Error("yss-project.yaml 只能包含 schema_version 和 repository_mode");
18
+ }
19
+ if (fields.schema_version !== "1") {
20
+ throw new Error("yss-project.yaml 的 schema_version 必须为 1");
21
+ }
22
+ if (!["template-source", "project-instance"].includes(fields.repository_mode)) {
23
+ throw new Error("yss-project.yaml 的 repository_mode 必须是 template-source 或 project-instance");
24
+ }
25
+ return fields;
26
+ }
27
+
28
+ function convertTemplateSourceToInstance(content) {
29
+ const identity = parseRepositoryIdentity(content);
30
+ if (identity.repository_mode !== "template-source") {
31
+ throw new Error("模板 yss-project.yaml 必须声明 repository_mode: template-source");
32
+ }
33
+ const rendered = String(content).replace(/^repository_mode:\s*template-source$/m, "repository_mode: project-instance");
34
+ const converted = parseRepositoryIdentity(rendered);
35
+ if (converted.repository_mode !== "project-instance") {
36
+ throw new Error("生成项目 yss-project.yaml 未转换为 project-instance");
37
+ }
38
+ return rendered;
39
+ }
40
+
41
+ module.exports = { parseRepositoryIdentity, convertTemplateSourceToInstance };