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,153 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { copyPath } = require("./copy-path");
7
+ const {
8
+ normalizeRelativePath,
9
+ targetPath,
10
+ pathKind,
11
+ } = require("./path-utils");
12
+
13
+ class FileTransaction {
14
+ constructor(targetDir, deps = {}) {
15
+ this.targetDir = path.resolve(targetDir);
16
+ this.fs = deps.fs || fs;
17
+ this.path = deps.path || path;
18
+ this.copyPath = deps.copyPath || copyPath;
19
+ this.pathKind = deps.pathKind || pathKind;
20
+ this.backupRoot = deps.backupRoot || this.fs.mkdtempSync(
21
+ this.path.join((deps.os || os).tmpdir(), "create-yss-spec-backup-"),
22
+ );
23
+ this.backups = [];
24
+ this.mutated = [];
25
+ this.createdDirectories = [];
26
+ }
27
+
28
+ relative(absolutePath) {
29
+ return normalizeRelativePath(this.path.relative(this.targetDir, absolutePath));
30
+ }
31
+
32
+ backupPath(relativePath) {
33
+ return this.path.join(this.backupRoot, ...normalizeRelativePath(relativePath).split("/"));
34
+ }
35
+
36
+ prepare(relativePaths) {
37
+ const candidates = [...new Set(relativePaths)]
38
+ .map((relativePath) => normalizeRelativePath(relativePath))
39
+ .filter(
40
+ (relativePath) =>
41
+ relativePath &&
42
+ this.pathKind(targetPath(this.targetDir, relativePath)) !== "missing",
43
+ )
44
+ .sort((left, right) => left.split("/").length - right.split("/").length);
45
+
46
+ for (const relativePath of candidates) {
47
+ if (
48
+ this.backups.some(
49
+ (backup) =>
50
+ relativePath === backup.relativePath ||
51
+ relativePath.startsWith(`${backup.relativePath}/`),
52
+ )
53
+ ) {
54
+ continue;
55
+ }
56
+
57
+ const source = targetPath(this.targetDir, relativePath);
58
+ const destination = this.backupPath(relativePath);
59
+ this.copyPath(source, destination);
60
+ this.backups.push({ relativePath, destination });
61
+ }
62
+ }
63
+
64
+ ensureParent(absolutePath) {
65
+ const missing = [];
66
+ let current = this.path.dirname(absolutePath);
67
+
68
+ while (current !== this.targetDir && !this.fs.existsSync(current)) {
69
+ missing.push(current);
70
+ current = this.path.dirname(current);
71
+ }
72
+
73
+ if (current !== this.targetDir && this.pathKind(current) !== "directory") {
74
+ throw new Error(`目标父路径不是目录:${this.relative(current)}`);
75
+ }
76
+
77
+ for (const directory of missing.reverse()) {
78
+ this.fs.mkdirSync(directory);
79
+ this.createdDirectories.push(this.relative(directory));
80
+ }
81
+ }
82
+
83
+ mark(absolutePath) {
84
+ this.mutated.push(this.relative(absolutePath));
85
+ }
86
+
87
+ writeFile(absolutePath, content, options = "utf8") {
88
+ this.ensureParent(absolutePath);
89
+ this.fs.writeFileSync(absolutePath, content, options);
90
+ this.mark(absolutePath);
91
+ }
92
+
93
+ copyFile(sourcePath, destinationPath) {
94
+ this.ensureParent(destinationPath);
95
+ this.copyPath(sourcePath, destinationPath);
96
+ this.mark(destinationPath);
97
+ }
98
+
99
+ remove(absolutePath) {
100
+ if (!this.fs.existsSync(absolutePath)) return;
101
+ this.fs.rmSync(absolutePath, { recursive: true, force: true });
102
+ this.mark(absolutePath);
103
+ }
104
+
105
+ move(sourcePath, destinationPath) {
106
+ this.ensureParent(destinationPath);
107
+ this.copyPath(sourcePath, destinationPath);
108
+ this.mark(destinationPath);
109
+ this.remove(sourcePath);
110
+ }
111
+
112
+ rollback() {
113
+ for (const relativePath of [...new Set(this.mutated)].sort(
114
+ (left, right) => right.split("/").length - left.split("/").length,
115
+ )) {
116
+ this.fs.rmSync(targetPath(this.targetDir, relativePath), {
117
+ recursive: true,
118
+ force: true,
119
+ });
120
+ }
121
+
122
+ for (const relativePath of [...new Set(this.createdDirectories)].sort(
123
+ (left, right) => right.split("/").length - left.split("/").length,
124
+ )) {
125
+ const directory = targetPath(this.targetDir, relativePath);
126
+ if (this.fs.existsSync(directory) && this.fs.readdirSync(directory).length === 0) {
127
+ this.fs.rmdirSync(directory);
128
+ }
129
+ }
130
+
131
+ for (const backup of [...this.backups].sort(
132
+ (left, right) =>
133
+ left.relativePath.split("/").length - right.relativePath.split("/").length,
134
+ )) {
135
+ this.copyPath(
136
+ backup.destination,
137
+ targetPath(this.targetDir, backup.relativePath),
138
+ );
139
+ }
140
+ }
141
+
142
+ finish() {
143
+ if (this.backups.length === 0) {
144
+ this.fs.rmSync(this.backupRoot, { recursive: true, force: true });
145
+ return null;
146
+ }
147
+ return this.backupRoot;
148
+ }
149
+ }
150
+
151
+ module.exports = {
152
+ FileTransaction,
153
+ };
@@ -0,0 +1,115 @@
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 GITLINK_MODE = "160000";
8
+ const GITLINK_PATH_CACHE = new Map();
9
+
10
+ function posixRelative(from, to) {
11
+ return path.relative(from, to).split(path.sep).join("/");
12
+ }
13
+
14
+ function parseGitmodules(content) {
15
+ const modules = [];
16
+ if (typeof content !== "string" || content.length === 0) return modules;
17
+ let current = null;
18
+ for (const rawLine of content.split(/\r?\n/)) {
19
+ const line = rawLine.trim();
20
+ const name = line.match(/^\[submodule "(.+)"\]$/);
21
+ if (name) {
22
+ current = { name: name[1], path: "", url: "" };
23
+ modules.push(current);
24
+ continue;
25
+ }
26
+ if (!current) continue;
27
+ const pathMatch = line.match(/^path\s*=\s*(.+)$/);
28
+ if (pathMatch) {
29
+ current.path = pathMatch[1].trim().replace(/\\/g, "/").replace(/\/+$/, "");
30
+ }
31
+ const urlMatch = line.match(/^url\s*=\s*(.+)$/);
32
+ if (urlMatch) current.url = urlMatch[1].trim();
33
+ }
34
+ return modules;
35
+ }
36
+
37
+ function readGitmodules(repoRoot, deps = {}) {
38
+ const file = path.join(repoRoot, ".gitmodules");
39
+ const pathKind =
40
+ deps.pathKind ||
41
+ ((value) => {
42
+ try {
43
+ const stat = fs.lstatSync(value);
44
+ if (stat.isFile()) return "file";
45
+ if (stat.isDirectory()) return "directory";
46
+ return "other";
47
+ } catch (error) {
48
+ if (error.code === "ENOENT") return "missing";
49
+ throw error;
50
+ }
51
+ });
52
+ if (!fs.existsSync(file) || pathKind(file) !== "file") return [];
53
+ return parseGitmodules(fs.readFileSync(file, "utf8"));
54
+ }
55
+
56
+ function gitlinkPaths(repoRoot, deps = {}) {
57
+ const resolvedRoot = path.resolve(repoRoot);
58
+ const useCache = !deps.disableCache && !deps.spawn;
59
+ if (useCache && GITLINK_PATH_CACHE.has(resolvedRoot)) {
60
+ return GITLINK_PATH_CACHE.get(resolvedRoot);
61
+ }
62
+
63
+ const spawn = deps.spawn || spawnSync;
64
+ const result = spawn(
65
+ "git",
66
+ ["-C", resolvedRoot, "ls-files", "--stage", "-z"],
67
+ {
68
+ encoding: "utf8",
69
+ timeout: 5000,
70
+ },
71
+ );
72
+ const paths = new Set();
73
+ if (result.status === 0) {
74
+ for (const record of (result.stdout || "").split("\0")) {
75
+ const match = record.match(/^(160000)\s+[0-9a-f]+\s+\d+\s+(.+)$/i);
76
+ if (match) paths.add(match[2].replaceAll("\\", "/"));
77
+ }
78
+ }
79
+
80
+ if (useCache) GITLINK_PATH_CACHE.set(resolvedRoot, paths);
81
+ return paths;
82
+ }
83
+
84
+ function clearGitlinkPathCache(repoRoot = null) {
85
+ if (repoRoot === null) {
86
+ GITLINK_PATH_CACHE.clear();
87
+ return;
88
+ }
89
+ GITLINK_PATH_CACHE.delete(path.resolve(repoRoot));
90
+ }
91
+
92
+ function isGitSubmoduleMount(repoRoot, targetPathValue, deps = {}) {
93
+ const relative = posixRelative(repoRoot, path.resolve(targetPathValue));
94
+ if (
95
+ !relative ||
96
+ relative === "." ||
97
+ relative.startsWith("..") ||
98
+ path.isAbsolute(relative)
99
+ ) {
100
+ return false;
101
+ }
102
+ if (readGitmodules(repoRoot, deps).some((item) => item.path === relative)) return true;
103
+ return gitlinkPaths(repoRoot, deps).has(relative);
104
+ }
105
+
106
+ module.exports = {
107
+ GITLINK_MODE,
108
+ GITLINK_PATH_CACHE,
109
+ parseGitmodules,
110
+ readGitmodules,
111
+ gitlinkPaths,
112
+ clearGitlinkPathCache,
113
+ isGitSubmoduleMount,
114
+ posixRelative,
115
+ };
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { spawnSync } = require("node:child_process");
6
+ const { isGitSubmoduleMount, posixRelative } = require("./submodule");
7
+
8
+ function collectGitRoots(start) {
9
+ const roots = [];
10
+ if (!start) return roots;
11
+ let current = path.resolve(start);
12
+ while (true) {
13
+ const gitPath = path.join(current, ".git");
14
+ let validGitEntry = false;
15
+ try {
16
+ const gitStat = fs.lstatSync(gitPath);
17
+ validGitEntry = gitStat.isDirectory() ? fs.readdirSync(gitPath).length > 0 : gitStat.isFile();
18
+ } catch (error) {
19
+ if (error.code !== "ENOENT") throw error;
20
+ }
21
+ if (validGitEntry || fs.existsSync(path.join(current, ".gitmodules"))) roots.push(current);
22
+ const parent = path.dirname(current);
23
+ if (parent === current) break;
24
+ current = parent;
25
+ }
26
+ return roots;
27
+ }
28
+
29
+ function gitShowSuperproject(cwd, spawn = spawnSync) {
30
+ const result = spawn("git", ["-C", cwd, "rev-parse", "--show-superproject-working-tree"], { encoding: "utf8", timeout: 5000 });
31
+ return result.status === 0 ? (result.stdout || "").trim() : "";
32
+ }
33
+
34
+ function gitAbbrevRef(cwd, spawn = spawnSync) {
35
+ const result = spawn("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf8", timeout: 5000 });
36
+ return result.status === 0 ? (result.stdout || "").trim() : null;
37
+ }
38
+
39
+ function isEmptyDir(dir) {
40
+ if (!fs.existsSync(dir)) return true;
41
+ try {
42
+ return fs.readdirSync(dir).filter((name) => name !== "." && name !== "..").length === 0;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ function inspectCheckoutState(repoRoot, targetPathValue, deps = {}) {
49
+ const resolved = path.resolve(targetPathValue);
50
+ const relative = posixRelative(path.resolve(repoRoot), resolved);
51
+ const isMount = isGitSubmoduleMount(repoRoot, resolved, deps) || Boolean(fs.existsSync(path.join(resolved, ".git")) && gitShowSuperproject(resolved, deps.spawn || spawnSync));
52
+ const hasGit = fs.existsSync(path.join(resolved, ".git"));
53
+ if (isMount && !hasGit) return isEmptyDir(resolved) ? "empty-gitlink" : "uninitialized";
54
+ if (isMount && hasGit) return gitAbbrevRef(resolved, deps.spawn || spawnSync) === "HEAD" ? "detached-head" : "attached-branch";
55
+ if (!relative || relative.startsWith("..")) return null;
56
+ return null;
57
+ }
58
+
59
+ function gitDirtyWarning(targetDir, spawn = spawnSync) {
60
+ const probe = spawn("git", ["rev-parse", "--is-inside-work-tree"], { cwd: targetDir, encoding: "utf8", timeout: 5000 });
61
+ if (probe.status !== 0 || (probe.stdout || "").trim() !== "true") return null;
62
+ const status = spawn("git", ["status", "--porcelain"], { cwd: targetDir, encoding: "utf8", timeout: 5000 });
63
+ if (status.status === 0 && (status.stdout || "").trim()) {
64
+ return "警告:目标 Git worktree 存在未提交改动;CLI 不会自动 stash 或提交,请在结果后检查 git diff / git status";
65
+ }
66
+ return null;
67
+ }
68
+
69
+ module.exports = { collectGitRoots, gitShowSuperproject, gitAbbrevRef, isEmptyDir, inspectCheckoutState, gitDirtyWarning };
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+
3
+ const {
4
+ classifyAttachOperations,
5
+ createAttachPlan,
6
+ } = require("./attach-planner");
7
+ const {
8
+ ownershipAwareUnmanagedReason,
9
+ decoratePlanWithOwnership,
10
+ } = require("./ownership-runtime");
11
+ const {
12
+ applyLifecycleToOperations,
13
+ decoratePlanWithLifecycle,
14
+ } = require("./lifecycle-runtime");
15
+
16
+ function buildAttachPlanFromRuntime({
17
+ targetDir,
18
+ desiredOperations,
19
+ migration,
20
+ warning = null,
21
+ getUnmanagedReason,
22
+ getPathKind,
23
+ getFileHash,
24
+ }) {
25
+ const lifecycleOperations = applyLifecycleToOperations(desiredOperations);
26
+ const classified = classifyAttachOperations({
27
+ desiredOperations: lifecycleOperations,
28
+ getUnmanagedReason: (operation) =>
29
+ ownershipAwareUnmanagedReason(operation, getUnmanagedReason),
30
+ getPathKind,
31
+ getFileHash,
32
+ });
33
+
34
+ const basePlan = createAttachPlan({
35
+ targetDir,
36
+ classified,
37
+ migration,
38
+ warnings: warning ? [warning] : [],
39
+ });
40
+ const ownershipPlan = decoratePlanWithOwnership(basePlan, lifecycleOperations);
41
+ const plan = decoratePlanWithLifecycle(ownershipPlan, lifecycleOperations);
42
+
43
+ return {
44
+ classified,
45
+ plan,
46
+ };
47
+ }
48
+
49
+ module.exports = {
50
+ buildAttachPlanFromRuntime,
51
+ };
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+
3
+ function classifyAttachOperations({
4
+ desiredOperations = [],
5
+ getUnmanagedReason = () => null,
6
+ getPathKind,
7
+ getFileHash,
8
+ }) {
9
+ if (typeof getPathKind !== "function") {
10
+ throw new TypeError("classifyAttachOperations requires getPathKind");
11
+ }
12
+ if (typeof getFileHash !== "function") {
13
+ throw new TypeError("classifyAttachOperations requires getFileHash");
14
+ }
15
+
16
+ const missing = [];
17
+ const matched = [];
18
+ const identity = [];
19
+ const conflicts = [];
20
+ const forceableConflicts = [];
21
+ const unsafe = [];
22
+
23
+ for (const operation of desiredOperations) {
24
+ const unmanagedReason = getUnmanagedReason(operation);
25
+ if (unmanagedReason) {
26
+ unsafe.push({ ...operation, reason: unmanagedReason });
27
+ continue;
28
+ }
29
+
30
+ const kind = getPathKind(operation);
31
+ if (kind === "missing") {
32
+ missing.push(operation);
33
+ continue;
34
+ }
35
+
36
+ if (kind !== "file") {
37
+ unsafe.push({
38
+ ...operation,
39
+ reason: `目标路径类型为 ${kind},无法安全判断是否可以替换`,
40
+ });
41
+ continue;
42
+ }
43
+
44
+ const currentHash = getFileHash(operation);
45
+ if (currentHash === operation.desiredHash) {
46
+ matched.push(operation);
47
+ } else if (operation.identityConversion) {
48
+ identity.push(operation);
49
+ } else {
50
+ const conflict = {
51
+ ...operation,
52
+ reason:
53
+ operation.mergeStrategy === "manual"
54
+ ? "目标可定制文件已存在且内容不一致;mergeStrategy=manual,必须人工处理"
55
+ : "目标受管文件已存在且内容不一致",
56
+ };
57
+ conflicts.push(conflict);
58
+ if (operation.mergeStrategy !== "manual") {
59
+ forceableConflicts.push(conflict);
60
+ }
61
+ }
62
+ }
63
+
64
+ return {
65
+ missing,
66
+ matched,
67
+ identity,
68
+ conflicts,
69
+ forceableConflicts,
70
+ unsafe,
71
+ desiredOperations,
72
+ };
73
+ }
74
+
75
+ function createAttachPlan({
76
+ targetDir,
77
+ classified,
78
+ migration = null,
79
+ warnings = [],
80
+ }) {
81
+ if (!classified || typeof classified !== "object") {
82
+ throw new TypeError("createAttachPlan requires classified attach operations");
83
+ }
84
+
85
+ const missing = classified.missing || [];
86
+ const matched = classified.matched || [];
87
+ const identity = classified.identity || [];
88
+ const classifiedConflicts = classified.conflicts || [];
89
+ const forceableConflicts = classified.forceableConflicts || classifiedConflicts;
90
+ const classifiedUnsafe = classified.unsafe || [];
91
+ const migrationConflicts = migration?.conflicts || [];
92
+ const migrationUnsafe = migration?.unsafe || [];
93
+
94
+ const conflicts = [
95
+ ...classifiedConflicts.map((operation) => ({
96
+ path: operation.relativePath,
97
+ reason: operation.reason,
98
+ forceable: forceableConflicts.includes(operation),
99
+ source: "managed-file",
100
+ })),
101
+ ...migrationConflicts.map((item) => ({
102
+ path: item.from || item.path || item.to,
103
+ from: item.from,
104
+ to: item.to,
105
+ reason: item.reason,
106
+ forceable: false,
107
+ source: "migration",
108
+ })),
109
+ ];
110
+ const unsafe = [
111
+ ...classifiedUnsafe.map((operation) => ({
112
+ path: operation.relativePath,
113
+ reason: operation.reason,
114
+ source: "managed-file",
115
+ })),
116
+ ...migrationUnsafe.map((item) => ({
117
+ path: item.path,
118
+ reason: item.reason,
119
+ source: "migration",
120
+ })),
121
+ ];
122
+
123
+ return {
124
+ operation: "attach",
125
+ targetDir,
126
+ template: null,
127
+ changes: [
128
+ ...missing.map((operation) => ({
129
+ action: "add",
130
+ path: operation.relativePath,
131
+ })),
132
+ ...identity.map((operation) => ({
133
+ action: "identity-convert",
134
+ path: operation.relativePath,
135
+ })),
136
+ ],
137
+ conflicts,
138
+ unsafe,
139
+ migration: migration || {
140
+ operations: [],
141
+ legacy: [],
142
+ conflicts: [],
143
+ unsafe: [],
144
+ },
145
+ warnings,
146
+ blocked: unsafe.length > 0 || migrationConflicts.length > 0,
147
+ stats: {
148
+ missing: missing.length,
149
+ matched: matched.length,
150
+ identity: identity.length,
151
+ conflicts: conflicts.length,
152
+ unsafe: unsafe.length,
153
+ },
154
+ };
155
+ }
156
+
157
+ module.exports = {
158
+ classifyAttachOperations,
159
+ createAttachPlan,
160
+ };