@pioneer_zmc/dsh-workbench 0.2.1

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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/README.zh.md +100 -0
  4. package/client.js +122 -0
  5. package/cordis.patch.yml +8 -0
  6. package/dist/check-building.js +1669 -0
  7. package/dist/check-room.js +980 -0
  8. package/dist/index.js +2873 -0
  9. package/package.json +60 -0
  10. package/skills/specdev-building/SKILL.md +85 -0
  11. package/skills/specdev-building/assets/layouts/court.json +53 -0
  12. package/skills/specdev-building/assets/layouts/row.json +48 -0
  13. package/skills/specdev-building/assets/layouts/single.json +27 -0
  14. package/skills/specdev-building/assets/layouts/tower-9.json +71 -0
  15. package/skills/specdev-building/assets/layouts/tower.json +59 -0
  16. package/skills/specdev-building/references/blueprint-contract.md +126 -0
  17. package/skills/specdev-building/references/diagnostics.md +101 -0
  18. package/skills/specdev-building/references/layouts.md +112 -0
  19. package/skills/specdev-business/SKILL.md +91 -0
  20. package/skills/specdev-business/assets/plan-template.md +66 -0
  21. package/skills/specdev-business/assets/spec-template.md +111 -0
  22. package/skills/specdev-business/assets/tasks-template.md +36 -0
  23. package/skills/specdev-business/references/change.md +117 -0
  24. package/skills/specdev-business/references/evidence-review.md +125 -0
  25. package/skills/specdev-business/references/implementation.md +67 -0
  26. package/skills/specdev-business/references/project-contract.md +211 -0
  27. package/skills/specdev-business/references/source-map.md +110 -0
  28. package/skills/specdev-business/references/specification.md +93 -0
  29. package/skills/specdev-business/references/workflow.md +126 -0
  30. package/skills/specdev-room/SKILL.md +77 -0
  31. package/skills/specdev-room/assets/intro-room.json +14 -0
  32. package/skills/specdev-room/references/diagnostics.md +21 -0
  33. package/skills/specdev-room/references/room-contract.md +59 -0
  34. package/vendor/archify-renderer/VERSION.md +52 -0
  35. package/vendor/archify-renderer/archify/LICENSE +22 -0
  36. package/vendor/archify-renderer/archify/THIRD_PARTY_NOTICES.md +56 -0
  37. package/vendor/archify-renderer/archify/assets/template.html +14787 -0
  38. package/vendor/archify-renderer/archify/bin/archify.mjs +1990 -0
  39. package/vendor/archify-renderer/archify/renderers/shared/brand-marks.mjs +563 -0
  40. package/vendor/archify-renderer/archify/renderers/shared/cli.mjs +218 -0
  41. package/vendor/archify-renderer/archify/renderers/shared/desktop-readability.mjs +26 -0
  42. package/vendor/archify-renderer/archify/renderers/shared/diagnostics.mjs +127 -0
  43. package/vendor/archify-renderer/archify/renderers/shared/engineering-profiles.mjs +157 -0
  44. package/vendor/archify-renderer/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  45. package/vendor/archify-renderer/archify/renderers/shared/generated-validators.mjs +13 -0
  46. package/vendor/archify-renderer/archify/renderers/shared/geometry.mjs +1423 -0
  47. package/vendor/archify-renderer/archify/renderers/shared/i18n.mjs +594 -0
  48. package/vendor/archify-renderer/archify/renderers/shared/legend.mjs +217 -0
  49. package/vendor/archify-renderer/archify/renderers/shared/output-path.mjs +321 -0
  50. package/vendor/archify-renderer/archify/renderers/shared/repository-evidence.mjs +235 -0
  51. package/vendor/archify-renderer/archify/renderers/shared/text-fit.mjs +49 -0
  52. package/vendor/archify-renderer/archify/renderers/shared/utils.mjs +232 -0
  53. package/vendor/archify-renderer/archify/renderers/shared/validator.mjs +86 -0
  54. package/vendor/archify-renderer/archify/renderers/workflow/render-workflow.mjs +35 -0
  55. package/vendor/archify-renderer/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  56. package/vendor/archify-renderer/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  57. package/vendor/archify-renderer/archify/schemas/common.schema.json +115 -0
  58. package/vendor/archify-renderer/archify/schemas/workflow.schema.json +428 -0
  59. package/vendor/archify-renderer/archify/scripts/check-render-output.mjs +835 -0
  60. package/vendor/archify-renderer/shims/process.mjs +18 -0
  61. package/vendor/archify-renderer/stubs/child_process.mjs +2 -0
  62. package/vendor/archify-renderer/stubs/crypto.mjs +3 -0
  63. package/vendor/archify-renderer/stubs/dns-promises.mjs +2 -0
  64. package/vendor/archify-renderer/stubs/fs.mjs +3 -0
  65. package/vendor/archify-renderer/stubs/http.mjs +3 -0
  66. package/vendor/archify-renderer/stubs/https.mjs +3 -0
  67. package/vendor/archify-renderer/stubs/net.mjs +3 -0
  68. package/vendor/archify-renderer/stubs/path.mjs +3 -0
  69. package/vendor/archify-renderer/stubs/url.mjs +3 -0
  70. package/web/assets/building-kit.js +1157 -0
  71. package/web/assets/building-look.js +185 -0
  72. package/web/assets/building-plan.js +495 -0
  73. package/web/assets/building-scene.js +529 -0
  74. package/web/assets/building-walk.js +248 -0
  75. package/web/assets/building.js +859 -0
  76. package/web/assets/business.js +124 -0
  77. package/web/assets/chart-link.js +165 -0
  78. package/web/assets/common.js +302 -0
  79. package/web/assets/details.js +197 -0
  80. package/web/assets/evidence.js +30 -0
  81. package/web/assets/furniture/SOURCES.md +24 -0
  82. package/web/assets/furniture/assets/ceramic-display-stand-01/asset.json +25 -0
  83. package/web/assets/furniture/assets/ceramic-display-stand-01/model.js +116 -0
  84. package/web/assets/furniture/assets/ceramic-display-stand-01/preview.webp +0 -0
  85. package/web/assets/furniture/assets/ceramic-floor-lamp-01/asset.json +61 -0
  86. package/web/assets/furniture/assets/ceramic-floor-lamp-01/model.js +56 -0
  87. package/web/assets/furniture/assets/ceramic-floor-lamp-01/preview.webp +0 -0
  88. package/web/assets/furniture/assets/ceramic-low-cabinet-01/asset.json +81 -0
  89. package/web/assets/furniture/assets/ceramic-low-cabinet-01/model.js +67 -0
  90. package/web/assets/furniture/assets/ceramic-low-cabinet-01/preview.webp +0 -0
  91. package/web/assets/furniture/assets/ceramic-vase-01/asset.json +21 -0
  92. package/web/assets/furniture/assets/ceramic-vase-01/model.js +119 -0
  93. package/web/assets/furniture/assets/ceramic-vase-01/preview.webp +0 -0
  94. package/web/assets/furniture/assets/fairy-bed-01/asset.json +61 -0
  95. package/web/assets/furniture/assets/fairy-bed-01/model.js +63 -0
  96. package/web/assets/furniture/assets/fairy-bed-01/preview.webp +0 -0
  97. package/web/assets/furniture/assets/fairy-book-stack-01/asset.json +21 -0
  98. package/web/assets/furniture/assets/fairy-book-stack-01/model.js +71 -0
  99. package/web/assets/furniture/assets/fairy-book-stack-01/preview.webp +0 -0
  100. package/web/assets/furniture/assets/fairy-bookshelf-01/asset.json +32 -0
  101. package/web/assets/furniture/assets/fairy-bookshelf-01/model.js +168 -0
  102. package/web/assets/furniture/assets/fairy-bookshelf-01/preview.webp +0 -0
  103. package/web/assets/furniture/assets/fairy-desk-01/asset.json +25 -0
  104. package/web/assets/furniture/assets/fairy-desk-01/model.js +132 -0
  105. package/web/assets/furniture/assets/fairy-desk-01/preview.webp +0 -0
  106. package/web/assets/furniture/assets/fairy-notice-board-01/asset.json +23 -0
  107. package/web/assets/furniture/assets/fairy-notice-board-01/model.js +125 -0
  108. package/web/assets/furniture/assets/fairy-notice-board-01/preview.webp +0 -0
  109. package/web/assets/furniture/assets/fairy-potted-plant-01/asset.json +21 -0
  110. package/web/assets/furniture/assets/fairy-potted-plant-01/model.js +128 -0
  111. package/web/assets/furniture/assets/fairy-potted-plant-01/preview.webp +0 -0
  112. package/web/assets/furniture/assets/fairy-reading-chair-01/asset.json +61 -0
  113. package/web/assets/furniture/assets/fairy-reading-chair-01/model.js +66 -0
  114. package/web/assets/furniture/assets/fairy-reading-chair-01/preview.webp +0 -0
  115. package/web/assets/furniture/assets/fairy-table-lamp-01/asset.json +21 -0
  116. package/web/assets/furniture/assets/fairy-table-lamp-01/model.js +182 -0
  117. package/web/assets/furniture/assets/fairy-table-lamp-01/preview.webp +0 -0
  118. package/web/assets/furniture/catalog.js +137 -0
  119. package/web/assets/furniture/index.json +110 -0
  120. package/web/assets/furniture/registry.js +72 -0
  121. package/web/assets/furniture-page.css +68 -0
  122. package/web/assets/furniture-page.js +302 -0
  123. package/web/assets/home.js +65 -0
  124. package/web/assets/node-detail.js +148 -0
  125. package/web/assets/panel-link.js +47 -0
  126. package/web/assets/read.js +746 -0
  127. package/web/assets/room-scene.js +386 -0
  128. package/web/assets/room.css +287 -0
  129. package/web/assets/room.js +437 -0
  130. package/web/assets/rooms/bindings.js +449 -0
  131. package/web/assets/rooms/ceramic-reflection.js +193 -0
  132. package/web/assets/rooms/ceramic-room.js +302 -0
  133. package/web/assets/rooms/door.js +35 -0
  134. package/web/assets/rooms/fairy-room.js +492 -0
  135. package/web/assets/rooms/furniture-assembly.js +113 -0
  136. package/web/assets/rooms/navigation.js +262 -0
  137. package/web/assets/rooms/placeholder-catalog.json +71 -0
  138. package/web/assets/rooms/placeholder-furniture.js +76 -0
  139. package/web/assets/rooms/placement.js +588 -0
  140. package/web/assets/rooms/reader.js +441 -0
  141. package/web/assets/rooms/reading-gate.js +157 -0
  142. package/web/assets/rooms/room-lighting.js +50 -0
  143. package/web/assets/rooms/templates.json +50 -0
  144. package/web/assets/rooms/walk-frame.js +64 -0
  145. package/web/assets/rooms/walk-session.js +138 -0
  146. package/web/assets/save-result.js +89 -0
  147. package/web/assets/screenshots/business.png +0 -0
  148. package/web/assets/screenshots/node-detail.png +0 -0
  149. package/web/assets/screenshots/project-home.png +0 -0
  150. package/web/assets/screenshots/workflow.png +0 -0
  151. package/web/assets/showcase.js +232 -0
  152. package/web/assets/style.css +836 -0
  153. package/web/assets/template.js +7 -0
  154. package/web/assets/theme.js +29 -0
  155. package/web/assets/three/OrbitControls.js +1972 -0
  156. package/web/assets/three/VERSION.md +12 -0
  157. package/web/assets/three/three.core.js +60586 -0
  158. package/web/assets/three/three.module.js +19719 -0
  159. package/web/assets/workspaces.css +21 -0
  160. package/web/assets/workspaces.js +81 -0
  161. package/web/building.html +131 -0
  162. package/web/business.html +43 -0
  163. package/web/furniture.html +47 -0
  164. package/web/index.html +57 -0
  165. package/web/read.html +107 -0
  166. package/web/room.html +146 -0
  167. package/web/showcase.html +50 -0
  168. package/web/workspaces.html +32 -0
@@ -0,0 +1,980 @@
1
+ // 构建产物:由 scripts/build.mjs 从 src/ 生成,请勿手改。
2
+
3
+
4
+ // src/cli/check-room.ts
5
+ import path2 from "node:path";
6
+ import { readFile as readFile2, realpath } from "node:fs/promises";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ // src/core/errors.ts
10
+ var CoreError = class extends Error {
11
+ code;
12
+ httpStatus;
13
+ // 字段在构造函数体内赋值(不用 TS 参数属性):core 全部保持"可擦除语法",
14
+ // 纯 Node 剥类型模式即可加载,scripts/smoke.mjs 不依赖 tsx。
15
+ constructor(code, message, httpStatus = 500) {
16
+ super(message);
17
+ this.code = code;
18
+ this.httpStatus = httpStatus;
19
+ this.name = "CoreError";
20
+ }
21
+ };
22
+
23
+ // src/core/git.ts
24
+ import { execFile } from "node:child_process";
25
+ import { promisify } from "node:util";
26
+ import { readFile, stat } from "node:fs/promises";
27
+ import { realpathSync } from "node:fs";
28
+ import path from "node:path";
29
+ var execFileAsync = promisify(execFile);
30
+ var GIT_TIMEOUT_MS = 1e4;
31
+ var MAX_FILE_BYTES = 2 * 1024 * 1024;
32
+ var EXEC_MAX_BUFFER = MAX_FILE_BYTES * 2;
33
+ async function runGit(repoRoot, args) {
34
+ let caught;
35
+ try {
36
+ const { stdout } = await execFileAsync("git", ["-C", repoRoot, ...args], {
37
+ timeout: GIT_TIMEOUT_MS,
38
+ maxBuffer: EXEC_MAX_BUFFER,
39
+ encoding: "utf8",
40
+ windowsHide: true
41
+ });
42
+ return stdout;
43
+ } catch (error) {
44
+ caught = error;
45
+ }
46
+ const err = caught;
47
+ if (err.code === "ENOENT") {
48
+ throw new CoreError("git-unavailable", "\u65E0\u6CD5\u542F\u52A8 git\uFF1A\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6\uFF08\u8BF7\u786E\u8BA4\u5DF2\u5B89\u88C5 git \u4E14\u5728 PATH \u4E2D\uFF09", 500);
49
+ }
50
+ if (err.killed || err.signal === "SIGTERM") {
51
+ throw new CoreError("git-timeout", `git ${args[0]} \u8D85\u8FC7 ${GIT_TIMEOUT_MS / 1e3} \u79D2\u672A\u8FD4\u56DE\uFF0C\u5DF2\u4E2D\u6B62`, 504);
52
+ }
53
+ const rawText = String(err.stderr || err.message || "");
54
+ if (/not a git repository/i.test(rawText)) {
55
+ throw new CoreError(
56
+ "not-a-git-repo",
57
+ `\u8FD9\u4E2A\u76EE\u5F55\u4E0D\u662F\u53EF\u7528\u7684 Git \u4ED3\u5E93\uFF1A${repoRoot}\uFF08\u5386\u53F2\u7248\u672C\u4E0E\u5FEB\u7167\u4FE1\u606F\u5B58\u653E\u5728 Git \u91CC\uFF0C\u9700\u8981\u662F Git \u4ED3\u5E93\u624D\u80FD\u7BA1\u7406\uFF09`,
58
+ 422
59
+ );
60
+ }
61
+ const firstLine = rawText.split("\n")[0].trim();
62
+ throw new CoreError("git-failed", `git ${args[0]} \u5931\u8D25\uFF1A${firstLine || "\u672A\u77E5\u9519\u8BEF"}`);
63
+ }
64
+ function isHex40(value) {
65
+ return typeof value === "string" && /^[0-9a-f]{40}$/.test(value);
66
+ }
67
+ async function assertRepoUsable(repoRoot) {
68
+ let info;
69
+ try {
70
+ info = await stat(repoRoot);
71
+ } catch (error) {
72
+ const code = error?.code;
73
+ if (code === "ENOENT" || code === "ENOTDIR") {
74
+ throw new CoreError("repo-unavailable", `\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u79FB\u52A8\uFF1A${repoRoot}`, 404);
75
+ }
76
+ throw new CoreError("repo-unavailable", `\u76EE\u5F55\u65E0\u6CD5\u8BFB\u53D6\uFF08${code ?? String(error)}\uFF09\uFF1A${repoRoot}`, 403);
77
+ }
78
+ if (!info.isDirectory()) {
79
+ throw new CoreError("repo-unavailable", `\u8DEF\u5F84\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55\uFF1A${repoRoot}`, 404);
80
+ }
81
+ }
82
+ function sameRealPath(a, b) {
83
+ const norm = (p) => {
84
+ let resolved;
85
+ try {
86
+ resolved = realpathSync(p);
87
+ } catch {
88
+ resolved = p;
89
+ }
90
+ const unified = path.resolve(resolved);
91
+ return process.platform === "win32" ? unified.toLowerCase() : unified;
92
+ };
93
+ return norm(a) === norm(b);
94
+ }
95
+ async function assertRepoTopLevel(repoRoot) {
96
+ const output = (await runGit(repoRoot, ["rev-parse", "--show-toplevel"])).trim();
97
+ if (!output) {
98
+ throw new CoreError("not-a-git-repo", `\u65E0\u6CD5\u786E\u5B9A ${repoRoot} \u7684 Git \u4ED3\u5E93\u9876\u5C42`, 422);
99
+ }
100
+ if (!sameRealPath(output, repoRoot)) {
101
+ throw new CoreError(
102
+ "repo-not-top-level",
103
+ `\u8FD9\u4E2A\u76EE\u5F55\u4E0D\u662F Git \u4ED3\u5E93\u7684\u9876\u5C42\uFF1A${repoRoot} \u5C5E\u4E8E\u5916\u5C42\u4ED3\u5E93 ${output}\u3002\u5386\u53F2\u7248\u672C\u4F1A\u4E0E\u5916\u5C42\u4ED3\u5E93\u6DF7\u5728\u4E00\u8D77\uFF0C\u7BA1\u7406\u9875\u4E0D\u4F1A\u81EA\u52A8\u6539\u7ED1\uFF1B\u8BF7\u628A\u4ED3\u5E93\u9876\u5C42\uFF08${output}\uFF09\u767B\u8BB0\u4E3A\u5DE5\u4F5C\u533A\u3002`,
104
+ 422
105
+ );
106
+ }
107
+ }
108
+ async function gitShowFileOptional(repoRoot, commit, relPath) {
109
+ if (!isHex40(commit)) {
110
+ throw new CoreError("bad-request", `\u5185\u90E8\u9519\u8BEF\uFF1AgitShowFileOptional \u53EA\u63A5\u53D7 40 \u4F4D\u63D0\u4EA4\u53F7\uFF0C\u6536\u5230 ${commit}`, 400);
111
+ }
112
+ const spec = `${commit}:${relPath}`;
113
+ const entry = (await runGit(repoRoot, ["ls-tree", commit, "--", relPath])).trim();
114
+ if (entry === "") return null;
115
+ const objectType = entry.split(" ")[0].split(" ")[1];
116
+ if (objectType !== "blob") return null;
117
+ const sizeText = await runGit(repoRoot, ["cat-file", "-s", spec]);
118
+ const size = Number(sizeText.trim());
119
+ if (!Number.isFinite(size) || size < 0) {
120
+ throw new CoreError("git-failed", `\u65E0\u6CD5\u786E\u5B9A ${relPath} \u5728\u63D0\u4EA4 ${commit.slice(0, 8)} \u4E0A\u7684\u5927\u5C0F`);
121
+ }
122
+ if (size > MAX_FILE_BYTES) {
123
+ throw new CoreError(
124
+ "file-too-large",
125
+ `\u6587\u4EF6 ${relPath} \u5728\u63D0\u4EA4 ${commit.slice(0, 8)} \u4E0A\u4E3A ${size} \u5B57\u8282\uFF0C\u8D85\u8FC7\u8BFB\u53D6\u4E0A\u9650 ${MAX_FILE_BYTES} \u5B57\u8282`,
126
+ 413
127
+ );
128
+ }
129
+ return runGit(repoRoot, ["show", spec]);
130
+ }
131
+ function statConfirmedAbsent(error) {
132
+ const code = error?.code;
133
+ return code === "ENOENT" || code === "ENOTDIR";
134
+ }
135
+ function fsFailed(relPath, error) {
136
+ const code = error?.code;
137
+ return new CoreError("fs-failed", `\u8BBF\u95EE ${relPath} \u5931\u8D25\uFF08${code ?? String(error)}\uFF09`, 500);
138
+ }
139
+ async function worktreeDirExists(repoRoot, relPath) {
140
+ const normalized = relPath.split("/").filter((part) => part !== "" && part !== ".");
141
+ if (normalized.some((part) => part === "..") || path.isAbsolute(relPath)) {
142
+ throw new CoreError("bad-request", `\u975E\u6CD5\u8DEF\u5F84\uFF1A${relPath}`, 400);
143
+ }
144
+ const full = path.resolve(repoRoot, ...normalized);
145
+ try {
146
+ return (await stat(full)).isDirectory();
147
+ } catch (error) {
148
+ if (statConfirmedAbsent(error)) return false;
149
+ throw fsFailed(relPath, error);
150
+ }
151
+ }
152
+ async function worktreeFileExists(repoRoot, relPath) {
153
+ const normalized = relPath.split("/").filter((part) => part !== "" && part !== ".");
154
+ if (normalized.some((part) => part === "..") || path.isAbsolute(relPath)) {
155
+ throw new CoreError("bad-request", `\u975E\u6CD5\u8DEF\u5F84\uFF1A${relPath}`, 400);
156
+ }
157
+ const full = path.resolve(repoRoot, ...normalized);
158
+ try {
159
+ return (await stat(full)).isFile();
160
+ } catch (error) {
161
+ if (statConfirmedAbsent(error)) return false;
162
+ throw fsFailed(relPath, error);
163
+ }
164
+ }
165
+ async function readWorktreeFileOptional(repoRoot, relPath) {
166
+ const normalized = relPath.split("/").filter((part) => part !== "" && part !== ".");
167
+ if (normalized.some((part) => part === "..") || path.isAbsolute(relPath)) {
168
+ throw new CoreError("bad-request", `\u975E\u6CD5\u8DEF\u5F84\uFF1A${relPath}`, 400);
169
+ }
170
+ const full = path.resolve(repoRoot, ...normalized);
171
+ const root = path.resolve(repoRoot);
172
+ if (full !== root && !full.startsWith(root + path.sep)) {
173
+ throw new CoreError("bad-request", `\u8DEF\u5F84\u8D8A\u51FA\u4ED3\u5E93\u6839\uFF1A${relPath}`, 400);
174
+ }
175
+ let size;
176
+ try {
177
+ size = (await stat(full)).size;
178
+ } catch (error) {
179
+ if (statConfirmedAbsent(error)) return null;
180
+ throw fsFailed(relPath, error);
181
+ }
182
+ if (size > MAX_FILE_BYTES) {
183
+ throw new CoreError(
184
+ "file-too-large",
185
+ `\u6587\u4EF6 ${relPath} \u4E3A ${size} \u5B57\u8282\uFF0C\u8D85\u8FC7\u8BFB\u53D6\u4E0A\u9650 ${MAX_FILE_BYTES} \u5B57\u8282`,
186
+ 413
187
+ );
188
+ }
189
+ return readFile(full, "utf8");
190
+ }
191
+
192
+ // src/core/inventory.ts
193
+ import { readdir as readdir2 } from "node:fs/promises";
194
+
195
+ // src/core/types.ts
196
+ var CONVENTION_ROOT = "docs/specdev";
197
+ var SCHEMA = {
198
+ project: "specdev/project/1",
199
+ business: "specdev/business/1",
200
+ chart: "specdev/chart/1",
201
+ evidence: "specdev/evidence/1",
202
+ snapshot: "specdev/snapshot/1"
203
+ };
204
+ var BUILDING_BLUEPRINT_REL = `${CONVENTION_ROOT}/building.json`;
205
+
206
+ // src/core/snapshots.ts
207
+ var STAGES = ["design", "implemented"];
208
+ function parseTagName(tag) {
209
+ const parts = tag.split("/");
210
+ if (parts.length < 3 || parts[0] !== "specdev" || !parts[1] || parts.some((p) => p === "")) return null;
211
+ return { chart: parts[1], version: parts.slice(2).join("/") };
212
+ }
213
+ async function listSpecdevTags(repoRoot) {
214
+ const format = "%(refname:short)%09%(objecttype)%09%(*objectname)%09%(*objecttype)%09%(creatordate:iso8601-strict)%09%(contents)%00";
215
+ const out = await runGit(repoRoot, ["for-each-ref", `refs/tags/${"specdev"}/`, `--format=${format}`]);
216
+ const records = [];
217
+ for (const raw of out.split("\0")) {
218
+ const chunk = raw.replace(/^[\r\n]+/, "").replace(/[\r\n]+$/, "");
219
+ if (!chunk) continue;
220
+ const head = chunk.indexOf(" ");
221
+ if (head < 0) continue;
222
+ const rest = chunk.slice(head + 1);
223
+ const fields = rest.split(" ");
224
+ records.push({
225
+ tag: chunk.slice(0, head),
226
+ objecttype: fields[0],
227
+ derefObject: fields[1],
228
+ derefType: fields[2],
229
+ createdDate: fields[3],
230
+ message: fields.slice(4).join(" ")
231
+ });
232
+ }
233
+ return records;
234
+ }
235
+ function validateRecord(record) {
236
+ const parsed = parseTagName(record.tag);
237
+ if (!parsed) return { invalid: "\u6807\u7B7E\u540D\u4E0D\u7B26\u5408 specdev/<\u56FE\u7F16\u53F7>/<\u7248\u672C> \u7ED3\u6784" };
238
+ if (record.objecttype !== "tag") return { invalid: "\u4E0D\u662F\u9644\u6CE8\u6807\u7B7E\uFF08\u8F7B\u91CF\u6807\u7B7E\u4E0D\u767B\u8BB0\u5FEB\u7167\uFF09" };
239
+ if (!/^[0-9a-f]{40}$/.test(record.derefObject)) return { invalid: "\u5265\u58F3\u540E\u6CA1\u6709\u6307\u5411\u6709\u6548\u5BF9\u8C61" };
240
+ if (record.derefType !== "commit") {
241
+ return { invalid: `\u6807\u7B7E\u5265\u58F3\u540E\u6307\u5411\u7684\u4E0D\u662F\u63D0\u4EA4\uFF08${record.derefType || "\u65E0\u5BF9\u8C61"}\uFF09\uFF0C\u5FEB\u7167\u5FC5\u987B\u843D\u5728\u5177\u4F53\u63D0\u4EA4\u4E0A` };
242
+ }
243
+ let meta;
244
+ try {
245
+ meta = JSON.parse(record.message);
246
+ } catch {
247
+ return { invalid: "\u6807\u7B7E message \u4E0D\u662F\u5408\u6CD5 JSON" };
248
+ }
249
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return { invalid: "\u6807\u7B7E message \u4E0D\u662F JSON \u5BF9\u8C61" };
250
+ const m = meta;
251
+ if (m.schema !== SCHEMA.snapshot) return { invalid: `message.schema \u4E0D\u662F ${SCHEMA.snapshot}` };
252
+ if (typeof m.chart !== "string" || !m.chart) return { invalid: "message.chart \u7F3A\u5931\u6216\u4E0D\u662F\u5B57\u7B26\u4E32" };
253
+ if (typeof m.name !== "string" || !m.name.trim()) return { invalid: "message.name \u7F3A\u5931\u6216\u4E3A\u7A7A" };
254
+ if (!STAGES.includes(m.stage)) return { invalid: `message.stage \u5FC5\u987B\u662F ${STAGES.join(" / ")}` };
255
+ if (typeof m.dir !== "string" || !m.dir.startsWith(CONVENTION_ROOT + "/")) {
256
+ return { invalid: `message.dir \u5FC5\u987B\u4EE5 ${CONVENTION_ROOT}/ \u5F00\u5934` };
257
+ }
258
+ if (m.chart !== parsed.chart) return { invalid: `message.chart\uFF08${m.chart}\uFF09\u4E0E\u6807\u7B7E\u540D\u91CC\u7684\u56FE\u7F16\u53F7\uFF08${parsed.chart}\uFF09\u4E0D\u4E00\u81F4` };
259
+ if (m.savedAt !== void 0 && typeof m.savedAt !== "string") return { invalid: "message.savedAt \u4E0D\u662F\u5B57\u7B26\u4E32" };
260
+ if (m.note !== void 0 && typeof m.note !== "string") return { invalid: "message.note \u4E0D\u662F\u5B57\u7B26\u4E32" };
261
+ return {
262
+ meta: {
263
+ schema: SCHEMA.snapshot,
264
+ chart: m.chart,
265
+ name: m.name,
266
+ stage: m.stage,
267
+ note: m.note,
268
+ dir: m.dir,
269
+ savedAt: m.savedAt
270
+ },
271
+ commit: record.derefObject
272
+ };
273
+ }
274
+ function snapshotsForChart(records, chartId) {
275
+ const snapshots = [];
276
+ let invalidCount = 0;
277
+ const invalidSamples = [];
278
+ for (const record of records) {
279
+ const parsed = parseTagName(record.tag);
280
+ if (!parsed || parsed.chart !== chartId) continue;
281
+ const result = validateRecord(record);
282
+ if ("invalid" in result) {
283
+ invalidCount += 1;
284
+ if (invalidSamples.length < 5) invalidSamples.push(record.tag);
285
+ continue;
286
+ }
287
+ snapshots.push({
288
+ tag: record.tag,
289
+ version: parsed.version,
290
+ chart: chartId,
291
+ commit: result.commit,
292
+ createdDate: record.createdDate,
293
+ meta: result.meta
294
+ });
295
+ }
296
+ snapshots.sort((a, b) => {
297
+ const keyA = a.meta.savedAt ?? a.createdDate;
298
+ const keyB = b.meta.savedAt ?? b.createdDate;
299
+ if (keyA !== keyB) return keyA < keyB ? 1 : -1;
300
+ return a.tag < b.tag ? 1 : -1;
301
+ });
302
+ return { snapshots, invalidCount, invalidSamples };
303
+ }
304
+ function findSnapshot(records, tag) {
305
+ const record = records.find((item) => item.tag === tag);
306
+ if (!record) {
307
+ throw new CoreError("not-found", `\u6807\u7B7E ${tag} \u4E0D\u5B58\u5728`, 404);
308
+ }
309
+ const result = validateRecord(record);
310
+ if ("invalid" in result) {
311
+ throw new CoreError("not-found", `\u6807\u7B7E ${tag} \u4E0D\u7B26\u5408\u5FEB\u7167\u7EA6\u5B9A\uFF08${result.invalid}\uFF09\uFF0C\u5DF2\u88AB\u5FFD\u7565`, 404);
312
+ }
313
+ const parsed = parseTagName(tag);
314
+ return {
315
+ tag,
316
+ version: parsed.version,
317
+ chart: parsed.chart,
318
+ commit: result.commit,
319
+ createdDate: record.createdDate,
320
+ meta: result.meta
321
+ };
322
+ }
323
+
324
+ // src/core/chart.ts
325
+ import { readdir } from "node:fs/promises";
326
+
327
+ // src/core/descriptor.ts
328
+ async function readDescriptor(repoRoot, relPath) {
329
+ const text = await readWorktreeFileOptional(repoRoot, relPath);
330
+ if (text === null) return { data: null, error: `\u7F3A\u5C11\u8BF4\u660E\u6587\u4EF6 ${relPath}` };
331
+ try {
332
+ return { data: JSON.parse(text) };
333
+ } catch (error) {
334
+ return { data: null, error: `${relPath} \u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${error.message}` };
335
+ }
336
+ }
337
+ function isPlainObject(value) {
338
+ return typeof value === "object" && value !== null && !Array.isArray(value);
339
+ }
340
+ function requireStringFields(record, fields) {
341
+ for (const field of fields) {
342
+ const value = record[field];
343
+ if (typeof value !== "string" || !value.trim()) return `${field} \u7F3A\u5931\u6216\u4E0D\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`;
344
+ }
345
+ return null;
346
+ }
347
+
348
+ // src/core/chart-files.ts
349
+ var CHART_FILE_NAMES = ["workflow.json", "details.md", "evidence.json"];
350
+ var FILE_KEY = {
351
+ "workflow.json": "workflow",
352
+ "details.md": "details",
353
+ "evidence.json": "evidence"
354
+ };
355
+
356
+ // src/core/fingerprint.ts
357
+ import { createHash } from "node:crypto";
358
+ function normalizeEol(text) {
359
+ return text.replace(/\r\n/g, "\n");
360
+ }
361
+ function fingerprintOf(files) {
362
+ const json = JSON.stringify(
363
+ CHART_FILE_NAMES.map((name) => {
364
+ const text = files[FILE_KEY[name]];
365
+ return text === null ? null : normalizeEol(text);
366
+ })
367
+ );
368
+ return createHash("sha256").update(json, "utf8").digest("hex");
369
+ }
370
+
371
+ // src/core/chart.ts
372
+ async function chartIdOwners(repoRoot, chartId) {
373
+ const entries = await readdir(`${repoRoot}/${CONVENTION_ROOT}`, { withFileTypes: true });
374
+ const owners = [];
375
+ for (const entry of entries) {
376
+ if (!entry.isDirectory()) continue;
377
+ if (await worktreeDirExists(repoRoot, `${CONVENTION_ROOT}/${entry.name}/${chartId}`)) owners.push(entry.name);
378
+ }
379
+ return owners;
380
+ }
381
+ async function locateChartDir(repoRoot, businessId, chartId) {
382
+ const businessDir = `${CONVENTION_ROOT}/${businessId}`;
383
+ const chartDir = `${businessDir}/${chartId}`;
384
+ if (!await worktreeDirExists(repoRoot, businessDir)) {
385
+ throw new CoreError("not-found", `\u4E1A\u52A1 ${businessId} \u4E0D\u5B58\u5728\uFF08\u7EA6\u5B9A\u76EE\u5F55 ${businessDir} \u4E0D\u5B58\u5728\uFF09`, 404);
386
+ }
387
+ if (!await worktreeDirExists(repoRoot, chartDir)) {
388
+ throw new CoreError("not-found", `\u56FE ${chartId} \u4E0D\u5B58\u5728\uFF08\u7EA6\u5B9A\u76EE\u5F55 ${chartDir} \u4E0D\u5B58\u5728\uFF09`, 404);
389
+ }
390
+ const owners = await chartIdOwners(repoRoot, chartId);
391
+ if (owners.length > 1) {
392
+ throw new CoreError(
393
+ "duplicate-chart-id",
394
+ `\u56FE\u7F16\u53F7 ${chartId} \u540C\u65F6\u5B58\u5728\u4E8E\u4E1A\u52A1\uFF08${owners.join("\u3001")}\uFF09\u4E0B\uFF1A\u5386\u53F2\u5FEB\u7167\u6309\u56FE\u7F16\u53F7\u5F52\u7EC4\uFF0C\u65E0\u6CD5\u533A\u5206\u5F52\u5C5E\u3002\u8BF7\u628A\u5176\u4E2D\u4E00\u4E2A\u56FE\u76EE\u5F55\u6539\u6210\u4E0D\u540C\u7684\u7F16\u53F7\u3002`,
395
+ 409
396
+ );
397
+ }
398
+ return chartDir;
399
+ }
400
+ function normalizeOrNull(text) {
401
+ return text === null ? null : normalizeEol(text);
402
+ }
403
+ async function compareCurrentWithLatest(repoRoot, chartDir, latest) {
404
+ for (const name of CHART_FILE_NAMES) {
405
+ const worktree = await readWorktreeFileOptional(repoRoot, `${chartDir}/${name}`);
406
+ const snapshot = await gitShowFileOptional(repoRoot, latest.commit, `${latest.meta.dir}/${name}`);
407
+ if (normalizeOrNull(worktree) !== normalizeOrNull(snapshot)) return "changed";
408
+ }
409
+ return "identical";
410
+ }
411
+ var FILE_KEYS = { "workflow.json": "workflow", "details.md": "details", "evidence.json": "evidence" };
412
+ async function readWorktreeFiles(repoRoot, chartDir) {
413
+ const files = { workflow: null, details: null, evidence: null };
414
+ const errors = {};
415
+ for (const name of CHART_FILE_NAMES) {
416
+ const key = FILE_KEYS[name];
417
+ try {
418
+ files[key] = await readWorktreeFileOptional(repoRoot, `${chartDir}/${name}`);
419
+ } catch (error) {
420
+ errors[key] = error instanceof Error ? error.message : String(error);
421
+ }
422
+ }
423
+ return { files, errors };
424
+ }
425
+ async function readSnapshotFiles(repoRoot, snapshot) {
426
+ const dir = snapshot.meta.dir;
427
+ const files = { workflow: null, details: null, evidence: null };
428
+ const errors = {};
429
+ for (const name of CHART_FILE_NAMES) {
430
+ const key = FILE_KEYS[name];
431
+ try {
432
+ files[key] = await gitShowFileOptional(repoRoot, snapshot.commit, `${dir}/${name}`);
433
+ } catch (error) {
434
+ errors[key] = error instanceof Error ? error.message : String(error);
435
+ }
436
+ }
437
+ return { files, errors };
438
+ }
439
+ async function readChartPage(repoRoot, businessId, chartId, v) {
440
+ const businessDir = `${CONVENTION_ROOT}/${businessId}`;
441
+ const chartDir = await locateChartDir(repoRoot, businessId, chartId);
442
+ const business = await readDescriptor(repoRoot, `${businessDir}/business.json`);
443
+ const chart = await readDescriptor(repoRoot, `${chartDir}/chart.json`);
444
+ const chartName = chart.data && isPlainObject(chart.data) && typeof chart.data.name === "string" ? chart.data.name : chartId;
445
+ const chartSummaryOk = !!(chart.data && isPlainObject(chart.data) && chart.data.schema === SCHEMA.chart && !requireStringFields(chart.data, ["id", "name"]) && chart.data.id === chartId);
446
+ const tagRecords = await listSpecdevTags(repoRoot);
447
+ const snapshots = snapshotsForChart(tagRecords, chartId);
448
+ const latest = snapshots.snapshots[0];
449
+ let currentStatus;
450
+ let compareError;
451
+ if (!latest) {
452
+ currentStatus = "no-snapshot";
453
+ } else {
454
+ try {
455
+ currentStatus = await compareCurrentWithLatest(repoRoot, chartDir, latest);
456
+ } catch (error) {
457
+ currentStatus = "compare-failed";
458
+ compareError = error instanceof Error ? error.message : String(error);
459
+ }
460
+ }
461
+ let version;
462
+ let currentFingerprint = null;
463
+ if (v === "current") {
464
+ const { files, errors } = await readWorktreeFiles(repoRoot, chartDir);
465
+ currentFingerprint = errors.workflow || errors.details || errors.evidence ? null : fingerprintOf(files);
466
+ version = {
467
+ kind: "current",
468
+ files,
469
+ workflowError: errors.workflow ?? (files.workflow === null ? `\u5F53\u524D\u5DE5\u4F5C\u533A\u7F3A\u5C11 workflow.json\uFF08${chartDir}/workflow.json\uFF09\uFF0C\u8BE5\u56FE\u4E0D\u53EF\u8BFB` : void 0),
470
+ workflowErrorKind: errors.workflow ? "unreadable" : files.workflow === null ? "missing" : void 0,
471
+ detailsError: errors.details,
472
+ evidenceError: errors.evidence
473
+ };
474
+ } else {
475
+ const parsed = parseTagName(v);
476
+ if (!parsed || parsed.chart !== chartId) {
477
+ throw new CoreError("bad-request", `\u7248\u672C\u53C2\u6570\u5FC5\u987B\u662F current \u6216\u672C\u56FE\uFF08${chartId}\uFF09\u7684\u6807\u7B7E\u540D\uFF0C\u6536\u5230\uFF1A${v}`, 400);
478
+ }
479
+ const snapshot = findSnapshot(tagRecords, v);
480
+ const { files, errors } = await readSnapshotFiles(repoRoot, snapshot);
481
+ version = {
482
+ kind: "snapshot",
483
+ tag: snapshot.tag,
484
+ commit: snapshot.commit,
485
+ label: snapshot.meta.name,
486
+ stage: snapshot.meta.stage,
487
+ note: snapshot.meta.note,
488
+ savedAt: snapshot.meta.savedAt,
489
+ files,
490
+ workflowError: errors.workflow ?? (files.workflow === null ? `\u5FEB\u7167 ${snapshot.tag} \u91CC\u7F3A\u5C11 workflow.json\uFF08\u767B\u8BB0\u76EE\u5F55 ${snapshot.meta.dir}\uFF09\uFF0C\u8BE5\u7248\u672C\u4E0D\u53EF\u8BFB` : void 0),
491
+ workflowErrorKind: errors.workflow ? "unreadable" : files.workflow === null ? "missing" : void 0,
492
+ detailsError: errors.details,
493
+ evidenceError: errors.evidence
494
+ };
495
+ }
496
+ return {
497
+ business: {
498
+ id: businessId,
499
+ name: business.data && isPlainObject(business.data) && typeof business.data.name === "string" ? business.data.name : businessId
500
+ },
501
+ chart: { id: chartId, name: chartName, summary: chartSummaryOk && typeof chart.data?.summary === "string" ? chart.data.summary : void 0 },
502
+ snapshots,
503
+ currentStatus,
504
+ compareError,
505
+ version,
506
+ currentFingerprint
507
+ };
508
+ }
509
+
510
+ // src/core/inventory.ts
511
+ async function readProjectInfo(repoRoot) {
512
+ const relPath = `${CONVENTION_ROOT}/project.json`;
513
+ const text = await readWorktreeFileOptional(repoRoot, relPath);
514
+ if (text === null) {
515
+ throw new CoreError(
516
+ "no-convention-root",
517
+ `\u8FD9\u91CC\u8FD8\u6CA1\u6709\u6D41\u7A0B\u56FE\u8D44\u6599\uFF1A\u672A\u627E\u5230 ${relPath}\u3002\u4E0D\u4F1A\u81EA\u52A8\u521B\u5EFA\u6216\u6539\u52A8\u8FD9\u4E2A\u76EE\u5F55\u91CC\u7684\u4EFB\u4F55\u6587\u4EF6\u3002`,
518
+ 404
519
+ );
520
+ }
521
+ let data;
522
+ try {
523
+ data = JSON.parse(text);
524
+ } catch (error) {
525
+ throw new CoreError("bad-inventory", `${relPath} \u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${error.message}`, 500);
526
+ }
527
+ if (!isPlainObject(data) || data.schema !== SCHEMA.project) {
528
+ throw new CoreError("bad-inventory", `${relPath} \u7684 schema \u5FC5\u987B\u662F ${SCHEMA.project}`, 500);
529
+ }
530
+ const fieldError = requireStringFields(data, ["name"]);
531
+ if (fieldError) throw new CoreError("bad-inventory", `${relPath}\uFF1A${fieldError}`, 500);
532
+ return data;
533
+ }
534
+ async function readInventory(repoRoot) {
535
+ const project = await readProjectInfo(repoRoot);
536
+ const tagRecords = await listSpecdevTags(repoRoot);
537
+ const businessEntries = await readdir2(`${repoRoot}/${CONVENTION_ROOT}`, { withFileTypes: true });
538
+ const businesses = [];
539
+ for (const entry of businessEntries) {
540
+ if (!entry.isDirectory()) continue;
541
+ const businessId = entry.name;
542
+ const businessDir = `${CONVENTION_ROOT}/${businessId}`;
543
+ const summary = { id: businessId, name: businessId, docs: [], charts: [] };
544
+ const { data, error } = await readDescriptor(repoRoot, `${businessDir}/business.json`);
545
+ if (!data) {
546
+ summary.descriptorError = error ?? "business.json \u7F3A\u5931";
547
+ businesses.push(summary);
548
+ continue;
549
+ }
550
+ if (!isPlainObject(data) || data.schema !== SCHEMA.business) {
551
+ summary.descriptorError = `${businessDir}/business.json \u7684 schema \u5FC5\u987B\u662F ${SCHEMA.business}`;
552
+ businesses.push(summary);
553
+ continue;
554
+ }
555
+ const fieldError = requireStringFields(data, ["id", "name"]);
556
+ if (fieldError) {
557
+ summary.descriptorError = `${businessDir}/business.json\uFF1A${fieldError}`;
558
+ businesses.push(summary);
559
+ continue;
560
+ }
561
+ if (data.id !== businessId) {
562
+ summary.descriptorError = `business.json \u7684 id\uFF08${data.id}\uFF09\u4E0E\u76EE\u5F55\u540D\uFF08${businessId}\uFF09\u4E0D\u4E00\u81F4`;
563
+ businesses.push(summary);
564
+ continue;
565
+ }
566
+ summary.name = data.name;
567
+ summary.intro = typeof data.intro === "string" ? data.intro : void 0;
568
+ if (Array.isArray(data.docs)) {
569
+ summary.docs = data.docs.filter((item) => typeof item === "string" && item !== "");
570
+ }
571
+ summary.charts = await readChartSummaries(repoRoot, businessDir, tagRecords);
572
+ businesses.push(summary);
573
+ }
574
+ businesses.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
575
+ const ownersByChartId = /* @__PURE__ */ new Map();
576
+ for (const business of businesses) {
577
+ for (const chart of business.charts) {
578
+ ownersByChartId.set(chart.id, [...ownersByChartId.get(chart.id) ?? [], business.id]);
579
+ }
580
+ }
581
+ for (const business of businesses) {
582
+ for (const chart of business.charts) {
583
+ const owners = ownersByChartId.get(chart.id) ?? [];
584
+ if (owners.length > 1) {
585
+ chart.idConflict = `\u56FE\u7F16\u53F7 ${chart.id} \u5728\u4E1A\u52A1\uFF08${owners.join("\u3001")}\uFF09\u4E2D\u91CD\u590D\uFF1A\u5386\u53F2\u5FEB\u7167\u6309\u56FE\u7F16\u53F7\u5F52\u7EC4\uFF0C\u65E0\u6CD5\u533A\u5206\u5F52\u5C5E\uFF0C\u8BE5\u56FE\u7684\u9605\u8BFB\u5DF2\u505C\u7528\uFF08\u76F4\u8FBE\u9605\u8BFB\u9875\u4F1A\u660E\u786E\u62A5\u9519\uFF09`;
586
+ }
587
+ }
588
+ }
589
+ return { project, businesses };
590
+ }
591
+ async function readChartSummaries(repoRoot, businessDir, tagRecords) {
592
+ const chartEntries = await readdir2(`${repoRoot}/${businessDir}`, { withFileTypes: true });
593
+ const charts = [];
594
+ for (const entry of chartEntries) {
595
+ if (!entry.isDirectory()) continue;
596
+ const chartId = entry.name;
597
+ const chartDir = `${businessDir}/${chartId}`;
598
+ const snapshots = snapshotsForChart(tagRecords, chartId);
599
+ const summary = {
600
+ id: chartId,
601
+ name: chartId,
602
+ hasWorkflow: await worktreeFileExists(repoRoot, `${chartDir}/workflow.json`),
603
+ snapshotCount: snapshots.snapshots.length,
604
+ invalidTagCount: snapshots.invalidCount,
605
+ currentStatus: "no-snapshot"
606
+ };
607
+ const { data, error } = await readDescriptor(repoRoot, `${chartDir}/chart.json`);
608
+ if (!data) {
609
+ summary.descriptorError = error ?? "chart.json \u7F3A\u5931";
610
+ charts.push(summary);
611
+ continue;
612
+ }
613
+ if (!isPlainObject(data) || data.schema !== SCHEMA.chart) {
614
+ summary.descriptorError = `${chartDir}/chart.json \u7684 schema \u5FC5\u987B\u662F ${SCHEMA.chart}`;
615
+ charts.push(summary);
616
+ continue;
617
+ }
618
+ const fieldError = requireStringFields(data, ["id", "name"]);
619
+ if (fieldError) {
620
+ summary.descriptorError = `${chartDir}/chart.json\uFF1A${fieldError}`;
621
+ charts.push(summary);
622
+ continue;
623
+ }
624
+ if (data.id !== chartId) {
625
+ summary.descriptorError = `chart.json \u7684 id\uFF08${data.id}\uFF09\u4E0E\u76EE\u5F55\u540D\uFF08${chartId}\uFF09\u4E0D\u4E00\u81F4`;
626
+ charts.push(summary);
627
+ continue;
628
+ }
629
+ summary.name = data.name;
630
+ summary.summary = typeof data.summary === "string" ? data.summary : void 0;
631
+ const latest = snapshots.snapshots[0];
632
+ if (latest) {
633
+ try {
634
+ summary.currentStatus = await compareCurrentWithLatest(repoRoot, chartDir, latest);
635
+ } catch (error2) {
636
+ summary.currentStatus = "compare-failed";
637
+ summary.compareError = error2 instanceof Error ? error2.message : String(error2);
638
+ }
639
+ summary.latestSnapshot = {
640
+ tag: latest.tag,
641
+ name: latest.meta.name,
642
+ stage: latest.meta.stage,
643
+ savedAt: latest.meta.savedAt,
644
+ createdDate: latest.createdDate
645
+ };
646
+ }
647
+ charts.push(summary);
648
+ }
649
+ charts.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
650
+ return charts;
651
+ }
652
+
653
+ // src/core/room.ts
654
+ var ROOM_REQUIRED_FIELDS = ["schemaVersion", "roomId", "businessId", "templateRef", "instances"];
655
+ var ROOM_ALLOWED_FIELDS = [...ROOM_REQUIRED_FIELDS, "bindings"];
656
+ var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
657
+ function roomLayoutRel(businessId) {
658
+ return `${CONVENTION_ROOT}/${businessId}/room.json`;
659
+ }
660
+ async function readRoomLayout(repoRoot, businessId) {
661
+ if (!ID_PATTERN.test(businessId)) {
662
+ throw new CoreError("bad-request", `\u53C2\u6570 business \u4E0D\u5408\u6CD5\uFF1A${businessId}`, 400);
663
+ }
664
+ const inventory = await readInventory(repoRoot);
665
+ const business = inventory.businesses.find((item) => item.id === businessId);
666
+ if (!business) {
667
+ throw new CoreError("not-found", `\u4E1A\u52A1 ${businessId} \u4E0D\u5B58\u5728\uFF08\u9879\u76EE\u6E05\u5355\u91CC\u6CA1\u6709\u8FD9\u4E2A\u4E1A\u52A1\uFF09`, 404);
668
+ }
669
+ if (business.descriptorError) {
670
+ throw new CoreError(
671
+ "bad-business",
672
+ `\u4E1A\u52A1 ${businessId} \u7684\u8BF4\u660E\u6587\u4EF6\u6709\u95EE\u9898\uFF0C\u4E0D\u80FD\u5F53\u4F5C\u53EF\u7528\u4E1A\u52A1\uFF1A${business.descriptorError}`,
673
+ 422
674
+ );
675
+ }
676
+ const relPath = roomLayoutRel(businessId);
677
+ const text = await readWorktreeFileOptional(repoRoot, relPath);
678
+ if (text === null) {
679
+ throw new CoreError(
680
+ "no-room-layout",
681
+ `\u4E1A\u52A1 ${businessId} \u8FD8\u6CA1\u6709\u623F\u95F4\u914D\u7F6E\uFF1A\u672A\u627E\u5230 ${relPath}\u3002\u4E0D\u4F1A\u81EA\u52A8\u751F\u6210\uFF0C\u4E5F\u4E0D\u56DE\u9000\u5230\u6837\u4F8B\u3002`,
682
+ 404
683
+ );
684
+ }
685
+ let layout;
686
+ try {
687
+ layout = JSON.parse(text);
688
+ } catch (error) {
689
+ throw new CoreError("bad-room-layout", `${relPath} \u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${error.message}`, 422);
690
+ }
691
+ if (!isPlainObject(layout)) {
692
+ throw new CoreError(
693
+ "bad-room-layout",
694
+ `${relPath} \u9876\u5C42\u5FC5\u987B\u662F JSON \u5BF9\u8C61\uFF0C\u5B9E\u9645\u662F ${Array.isArray(layout) ? "\u6570\u7EC4" : typeof layout}`,
695
+ 422
696
+ );
697
+ }
698
+ const unknownFields = Object.keys(layout).filter((key) => !ROOM_ALLOWED_FIELDS.includes(key));
699
+ if (unknownFields.length > 0) {
700
+ throw new CoreError(
701
+ "bad-room-layout",
702
+ `${relPath} \u53EA\u5141\u8BB8\u5B57\u6BB5 ${ROOM_ALLOWED_FIELDS.join("\u3001")}\uFF0C\u591A\u51FA\uFF1A${unknownFields.join("\u3001")}`,
703
+ 422
704
+ );
705
+ }
706
+ const missingFields = ROOM_REQUIRED_FIELDS.filter((field) => !(field in layout));
707
+ if (missingFields.length > 0) {
708
+ throw new CoreError("bad-room-layout", `${relPath} \u7F3A\u5FC5\u586B\u5B57\u6BB5\uFF1A${missingFields.join("\u3001")}`, 422);
709
+ }
710
+ if (typeof layout.schemaVersion !== "number" || layout.schemaVersion !== 1) {
711
+ throw new CoreError(
712
+ "bad-room-layout",
713
+ `${relPath} \u7684 schemaVersion \u5FC5\u987B\u662F\u6570\u5B57 1\uFF08\u5B9E\u9645\u662F ${JSON.stringify(layout.schemaVersion)}\uFF09`,
714
+ 422
715
+ );
716
+ }
717
+ if (typeof layout.roomId !== "string" || !ID_PATTERN.test(layout.roomId)) {
718
+ throw new CoreError(
719
+ "bad-room-layout",
720
+ `${relPath} \u7684 roomId \u5FC5\u987B\u662F\u7B26\u5408 ID \u89C4\u5219\uFF08\u5B57\u6BCD\u6570\u5B57\u5F00\u5934\uFF0C\u53EF\u542B . _ -\uFF09\u7684\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u5B9E\u9645\u662F ${JSON.stringify(layout.roomId)}\uFF09`,
721
+ 422
722
+ );
723
+ }
724
+ if (typeof layout.businessId !== "string" || !ID_PATTERN.test(layout.businessId)) {
725
+ throw new CoreError(
726
+ "bad-room-layout",
727
+ `${relPath} \u7684 businessId \u5FC5\u987B\u662F\u7B26\u5408 ID \u89C4\u5219\u7684\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u5B9E\u9645\u662F ${JSON.stringify(layout.businessId)}\uFF09`,
728
+ 422
729
+ );
730
+ }
731
+ if (typeof layout.templateRef !== "string" || !layout.templateRef.trim()) {
732
+ throw new CoreError(
733
+ "bad-room-layout",
734
+ `${relPath} \u7684 templateRef \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u5B9E\u9645\u662F ${JSON.stringify(layout.templateRef)}\uFF09`,
735
+ 422
736
+ );
737
+ }
738
+ if (!Array.isArray(layout.instances)) {
739
+ throw new CoreError(
740
+ "bad-room-layout",
741
+ `${relPath} \u7684 instances \u5FC5\u987B\u662F\u6570\u7EC4\uFF08\u7A7A\u6570\u7EC4\uFF1D\u7A7A\u623F\u95F4\uFF1B\u5B9E\u9645\u662F ${typeof layout.instances}\uFF09`,
742
+ 422
743
+ );
744
+ }
745
+ if ("bindings" in layout && !Array.isArray(layout.bindings)) {
746
+ throw new CoreError(
747
+ "bad-room-layout",
748
+ `${relPath} \u7684 bindings \u5FC5\u987B\u662F\u6570\u7EC4\uFF08\u7701\u7565\u6216\u7A7A\u6570\u7EC4\uFF1D\u6CA1\u6709\u5BB6\u5177\u8D44\u6599\u7ED1\u5B9A\uFF1B\u5B9E\u9645\u662F ${layout.bindings === null ? "null" : typeof layout.bindings}\uFF09`,
749
+ 422
750
+ );
751
+ }
752
+ if (layout.businessId !== businessId) {
753
+ throw new CoreError(
754
+ "room-business-mismatch",
755
+ `${relPath} \u7684 businessId\uFF08${layout.businessId}\uFF09\u4E0E\u8BF7\u6C42\u7684\u4E1A\u52A1\uFF08${businessId}\uFF09\u4E0D\u4E00\u81F4\uFF1A\u623F\u95F4\u914D\u7F6E\u53EA\u5F52\u5C5E\u5B83\u767B\u8BB0\u7684\u4E1A\u52A1\u3002`,
756
+ 422
757
+ );
758
+ }
759
+ return {
760
+ // docs 来自该业务清单登记(business.json 的 docs),charts 是本业务图目录的投影
761
+ // (E9c,既有 inventory 扫描结果:目录内 chart.json/workflow.json 等按既有规则扫描,
762
+ // 不新开扫描器,也不在 business.json 里加 charts 登记数组);页面拿一次房间响应即可
763
+ // 校验文档与图的资料绑定——不从 room.json 接受 docs/charts,也不混入其他业务(E3a/E9c)。
764
+ business: {
765
+ id: business.id,
766
+ name: business.name,
767
+ intro: business.intro,
768
+ docs: business.docs,
769
+ charts: business.charts.map((chart) => ({
770
+ id: chart.id,
771
+ name: chart.name,
772
+ ...chart.summary === void 0 ? {} : { summary: chart.summary },
773
+ hasWorkflow: chart.hasWorkflow,
774
+ ...chart.descriptorError === void 0 ? {} : { descriptorError: chart.descriptorError },
775
+ ...chart.idConflict === void 0 ? {} : { idConflict: chart.idConflict }
776
+ }))
777
+ },
778
+ layout,
779
+ source: { path: relPath }
780
+ };
781
+ }
782
+
783
+ // src/cli/check-room.ts
784
+ var USAGE = "\u7528\u6CD5\uFF1Anode <\u63D2\u4EF6\u5305\u6839>/dist/check-room.js <\u9879\u76EE\u4ED3\u5E93\u6839> <\u4E1A\u52A1ID> [--json]";
785
+ var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
786
+ var PACKAGE_ROOT = fileURLToPath(new URL("../", import.meta.url));
787
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
788
+ var reasonOf = (error) => error instanceof Error ? error.message : String(error);
789
+ function initialOutcome() {
790
+ return { schemaVersion: 1, kind: "failed", ok: false, repoRoot: null, businessId: null, sourcePath: null, diagnostics: [] };
791
+ }
792
+ function withProblems(outcome, kind, diagnostics) {
793
+ return { ...outcome, kind, ok: false, diagnostics };
794
+ }
795
+ function atStage(diagnostics, stage) {
796
+ return diagnostics.map((diagnostic) => ({ ...diagnostic, stage }));
797
+ }
798
+ async function runRoomCheck(input, businessId) {
799
+ let outcome = initialOutcome();
800
+ let stage = "input";
801
+ try {
802
+ if (!input || !ID_PATTERN2.test(businessId)) {
803
+ throw new CoreError("bad-request", "\u9700\u8981\u975E\u7A7A\u9879\u76EE\u4ED3\u5E93\u6839\u548C\u5408\u6CD5\u4E1A\u52A1 ID\uFF08\u5B57\u6BCD\u6570\u5B57\u5F00\u5934\uFF0C\u53EF\u542B . _ -\uFF09", 400);
804
+ }
805
+ outcome = { ...outcome, repoRoot: path2.resolve(input), businessId, sourcePath: roomLayoutRel(businessId) };
806
+ const repoRoot = outcome.repoRoot;
807
+ stage = "repo";
808
+ await assertRepoUsable(repoRoot);
809
+ await assertRepoTopLevel(repoRoot);
810
+ stage = "read";
811
+ const { business, layout } = await readRoomLayout(repoRoot, businessId);
812
+ stage = "resources";
813
+ const packageReal = await realpath(PACKAGE_ROOT);
814
+ const webRoot = path2.join(packageReal, "web");
815
+ const resourcePath = async (relative) => {
816
+ try {
817
+ const absolute = await realpath(path2.join(webRoot, relative));
818
+ const fromWeb = path2.relative(webRoot, absolute);
819
+ if (fromWeb === ".." || fromWeb.startsWith(".." + path2.sep) || path2.isAbsolute(fromWeb)) {
820
+ throw new Error("\u8D44\u6E90\u8DEF\u5F84\u8D8A\u51FA\u8FD0\u884C\u5305\u7684 web \u76EE\u5F55");
821
+ }
822
+ return absolute;
823
+ } catch (error) {
824
+ throw new CoreError("ROOM_RESOURCE_READ", `\u65E0\u6CD5\u8BFB\u53D6\u8FD0\u884C\u5305\u8D44\u6E90 web/${relative}\uFF1A${reasonOf(error)}`, 500);
825
+ }
826
+ };
827
+ const readJson = async (relative) => {
828
+ try {
829
+ return JSON.parse(await readFile2(await resourcePath(relative), "utf8"));
830
+ } catch (error) {
831
+ if (error instanceof CoreError) throw error;
832
+ throw new CoreError("ROOM_RESOURCE_READ", `\u65E0\u6CD5\u89E3\u6790\u8FD0\u884C\u5305\u8D44\u6E90 web/${relative}\uFF1A${reasonOf(error)}`, 500);
833
+ }
834
+ };
835
+ const importResource = async (relative) => {
836
+ try {
837
+ const url = pathToFileURL(await resourcePath(relative)).href;
838
+ return await import(url);
839
+ } catch (error) {
840
+ if (error instanceof CoreError) throw error;
841
+ throw new CoreError("ROOM_RESOURCE_READ", `\u65E0\u6CD5\u5BFC\u5165\u8FD0\u884C\u5305\u6A21\u5757 web/${relative}\uFF1A${reasonOf(error)}`, 500);
842
+ }
843
+ };
844
+ const templates = await readJson("assets/rooms/templates.json");
845
+ if (!isObject(templates) || templates.schemaVersion !== 1 || !Array.isArray(templates.templates)) {
846
+ return withProblems(outcome, "failed", [{ stage: "template", code: "TEMPLATE_INPUT", message: "\u8FD0\u884C\u5305 templates.json \u5FC5\u987B\u662F schemaVersion=1 \u4E14 templates \u4E3A\u6570\u7EC4\u7684\u5BF9\u8C61", fieldPath: "templates", sourcePath: "web/assets/rooms/templates.json" }]);
847
+ }
848
+ const matches = templates.templates.filter((item) => isObject(item) && item.templateId === layout.templateRef);
849
+ if (matches.length === 0) {
850
+ return withProblems(outcome, "checked", [{ stage: "template", code: "TEMPLATE_UNKNOWN", message: `\u623F\u95F4\u6A21\u677F\u672A\u767B\u8BB0\uFF1A${layout.templateRef}`, fieldPath: "layout.templateRef", sourcePath: outcome.sourcePath }]);
851
+ }
852
+ if (matches.length !== 1) {
853
+ return withProblems(outcome, "failed", [{ stage: "template", code: "TEMPLATE_INPUT", message: `\u8FD0\u884C\u5305\u6A21\u677F ${layout.templateRef} \u91CD\u590D\u767B\u8BB0\uFF0C\u5FC5\u987B\u6070\u597D\u4E00\u9879`, fieldPath: "templates", sourcePath: "web/assets/rooms/templates.json" }]);
854
+ }
855
+ const placeholder = await readJson("assets/rooms/placeholder-catalog.json");
856
+ if (!isObject(placeholder) || !Array.isArray(placeholder.assets)) {
857
+ return withProblems(outcome, "failed", [{ stage: "resources", code: "CATALOG_INPUT", message: "\u8FD0\u884C\u5305 placeholder-catalog.json \u5FC5\u987B\u662F assets \u4E3A\u6570\u7EC4\u7684\u975E null\u3001\u975E\u6570\u7EC4\u5BF9\u8C61", fieldPath: "catalog", sourcePath: "web/assets/rooms/placeholder-catalog.json" }]);
858
+ }
859
+ const placement = await importResource("assets/rooms/placement.js");
860
+ const bindings = await importResource("assets/rooms/bindings.js");
861
+ const library = await importResource("assets/furniture/catalog.js");
862
+ if (typeof placement.validatePlacement !== "function" || typeof bindings.resolveRoomBindings !== "function" || typeof library.loadFurnitureCatalog !== "function") {
863
+ throw new CoreError("ROOM_RESOURCE_READ", "\u8FD0\u884C\u5305\u6821\u9A8C\u6A21\u5757\u7F3A\u5C11\u6240\u9700\u5BFC\u51FA\u51FD\u6570", 500);
864
+ }
865
+ const loaded = await library.loadFurnitureCatalog({ fetchJson: async (url) => {
866
+ const prefix = "/specdev-workbench/assets/furniture/";
867
+ if (url === prefix + "index.json") return readJson("assets/furniture/index.json");
868
+ const match = /^\/specdev-workbench\/assets\/furniture\/assets\/([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\/asset\.json$/.exec(url);
869
+ if (!match) throw new Error(`\u5BB6\u5177\u8D44\u6E90\u5730\u5740\u4E0D\u5728\u56FA\u5B9A\u5305\u5185\u767D\u540D\u5355\uFF1A${url}`);
870
+ return readJson(`assets/furniture/assets/${match[1]}/asset.json`);
871
+ } });
872
+ if (!loaded.ok) return withProblems(outcome, "failed", atStage(loaded.diagnostics, "resources"));
873
+ if (!loaded.catalog) throw new CoreError("ROOM_RESOURCE_READ", "\u5BB6\u5177\u76EE\u5F55\u8BFB\u53D6\u6210\u529F\u5374\u672A\u8FD4\u56DE\u76EE\u5F55", 500);
874
+ const catalog = { schemaVersion: 1, assets: [...placeholder.assets, ...loaded.catalog.assets] };
875
+ stage = "placement";
876
+ const placed = placement.validatePlacement(matches[0], catalog, layout);
877
+ if (!placed.ok) return withProblems(outcome, "checked", atStage(placed.diagnostics, stage));
878
+ stage = "bindings";
879
+ const resolved = bindings.resolveRoomBindings({ business, layout, catalog });
880
+ if (!resolved.ok) return withProblems(outcome, "checked", atStage(resolved.diagnostics, stage));
881
+ stage = "documents";
882
+ const diagnostics = [];
883
+ const seenDocs = /* @__PURE__ */ new Set();
884
+ const seenCharts = /* @__PURE__ */ new Set();
885
+ for (const entry of resolved.directory) {
886
+ if (entry.businessId !== businessId) continue;
887
+ if (entry.kind === "workflow") {
888
+ if (seenCharts.has(entry.chartId)) continue;
889
+ seenCharts.add(entry.chartId);
890
+ if (entry.unavailableReason) {
891
+ diagnostics.push({ stage: "charts", code: "CHART_UNAVAILABLE", targetId: entry.chartId, message: `\u4E1A\u52A1 ${businessId} \u767B\u8BB0\u7684\u6D41\u7A0B\u56FE ${entry.chartId}\uFF08${entry.title}\uFF09\u5F53\u524D\u4E0D\u53EF\u7528\uFF1A${entry.unavailableReason}` });
892
+ continue;
893
+ }
894
+ let page;
895
+ try {
896
+ page = await readChartPage(repoRoot, businessId, entry.chartId, "current");
897
+ } catch (error) {
898
+ const causeCode = error instanceof CoreError ? error.code : error?.code;
899
+ diagnostics.push({ stage: "charts", code: "CHART_UNREADABLE", targetId: entry.chartId, message: `\u6D41\u7A0B\u56FE ${entry.chartId} \u8BFB\u53D6\u5931\u8D25\uFF1A${reasonOf(error)}`, ...causeCode ? { causeCode } : {} });
900
+ continue;
901
+ }
902
+ if (page.version.workflowError || page.version.files.workflow == null) {
903
+ diagnostics.push({ stage: "charts", code: "CHART_UNREADABLE", targetId: entry.chartId, message: `\u6D41\u7A0B\u56FE ${entry.chartId} \u7684 workflow.json \u7F3A\u5931\u6216\u8BFB\u4E0D\u5F00\uFF1A${page.version.workflowError ?? "readChartPage \u672A\u8FD4\u56DE workflow \u5185\u5BB9"}` });
904
+ continue;
905
+ }
906
+ let workflow;
907
+ try {
908
+ workflow = JSON.parse(page.version.files.workflow);
909
+ } catch (error) {
910
+ diagnostics.push({ stage: "charts", code: "CHART_JSON_INVALID", targetId: entry.chartId, message: `\u6D41\u7A0B\u56FE ${entry.chartId} \u7684 workflow.json \u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${reasonOf(error)}` });
911
+ continue;
912
+ }
913
+ if (!isObject(workflow)) {
914
+ diagnostics.push({ stage: "charts", code: "CHART_JSON_INVALID", targetId: entry.chartId, message: `\u6D41\u7A0B\u56FE ${entry.chartId} \u7684 workflow.json \u9876\u5C42\u5FC5\u987B\u662F JSON \u5BF9\u8C61\uFF08\u5B9E\u9645\u662F ${Array.isArray(workflow) ? "\u6570\u7EC4" : typeof workflow}\uFF09` });
915
+ continue;
916
+ }
917
+ continue;
918
+ }
919
+ if (seenDocs.has(entry.path)) continue;
920
+ seenDocs.add(entry.path);
921
+ try {
922
+ if (await readWorktreeFileOptional(repoRoot, entry.path) === null) {
923
+ diagnostics.push({ stage, code: "DOCUMENT_UNREADABLE", sourcePath: entry.path, message: `business.json \u767B\u8BB0\u7684\u8D44\u6599\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u6587\u4EF6\uFF1A${entry.path}` });
924
+ }
925
+ } catch (error) {
926
+ const causeCode = error instanceof CoreError ? error.code : error?.code;
927
+ diagnostics.push({ stage, code: "DOCUMENT_UNREADABLE", sourcePath: entry.path, message: `business.json \u767B\u8BB0\u7684\u8D44\u6599\u65E0\u6CD5\u8BFB\u53D6\uFF1A${entry.path}\uFF08${reasonOf(error)}\uFF09`, ...causeCode ? { causeCode } : {} });
928
+ }
929
+ }
930
+ return { ...outcome, kind: "checked", ok: diagnostics.length === 0, diagnostics };
931
+ } catch (error) {
932
+ return withProblems(outcome, "failed", [{ stage, code: error instanceof CoreError ? error.code : "internal", message: reasonOf(error) }]);
933
+ }
934
+ }
935
+ function exitCodeOf(outcome) {
936
+ return outcome.kind === "failed" ? 2 : outcome.ok ? 0 : 1;
937
+ }
938
+ function formatReport(outcome) {
939
+ const lines = [`\u623F\u95F4\u6821\u9A8C\uFF1A${outcome.businessId ?? "\uFF08\u4E1A\u52A1\u672A\u89E3\u6790\uFF09"}`, `\u4ED3\u5E93\uFF1A${outcome.repoRoot ?? "\uFF08\u672A\u89E3\u6790\uFF09"}`, `\u914D\u7F6E\uFF1A${outcome.sourcePath ?? "\uFF08\u672A\u89E3\u6790\uFF09"}`];
940
+ lines.push(outcome.ok ? "\u623F\u95F4\u914D\u7F6E\u4E0E\u767B\u8BB0\u8D44\u6599\u68C0\u67E5\u901A\u8FC7" : outcome.kind === "failed" ? "\u672A\u901A\u8FC7\uFF1A\u65E0\u6CD5\u5B8C\u6210\u6B63\u5E38\u68C0\u67E5" : "\u672A\u901A\u8FC7\uFF1A\u914D\u7F6E\u6216\u767B\u8BB0\u8D44\u6599\u6709\u95EE\u9898");
941
+ for (const diagnostic of outcome.diagnostics) {
942
+ lines.push(`[${diagnostic.stage}/${diagnostic.code}] ${diagnostic.message}`);
943
+ if (diagnostic.fieldPath) lines.push(` \u5B57\u6BB5\uFF1A${diagnostic.fieldPath}`);
944
+ if (diagnostic.instanceIds?.length) lines.push(` \u5B9E\u4F8B\uFF1A${diagnostic.instanceIds.join("\u3001")}`);
945
+ if (diagnostic.targetId) lines.push(` \u76EE\u6807\uFF1A${diagnostic.targetId}`);
946
+ if (diagnostic.sourcePath) lines.push(` \u6587\u4EF6\uFF1A${diagnostic.sourcePath}`);
947
+ if (diagnostic.causeCode) lines.push(` \u5E95\u5C42\u539F\u56E0\u7801\uFF1A${diagnostic.causeCode}`);
948
+ if (diagnostic.stage === "documents" || diagnostic.stage === "bindings") lines.push(" \u6838\u5BF9\u5F53\u524D\u4E1A\u52A1 business.json \u7684\u8D44\u6599\u767B\u8BB0\u3001\u8D44\u6599\u6587\u4EF6\u53CA room.json \u7ED1\u5B9A\u3002");
949
+ if (diagnostic.stage === "charts") lines.push(" \u6838\u5BF9\u5F53\u524D\u4E1A\u52A1\u56FE\u76EE\u5F55\u7684 chart.json\u3001workflow.json \u53CA room.json \u7ED1\u5B9A\uFF1B\u56FE\u7684\u65B0\u5EFA\u3001\u4FEE\u6539\u4E0E\u8282\u70B9\u8BE6\u60C5\u5F52 specdev-business\uFF0C\u4E0D\u5728\u672C\u68C0\u67E5\u5185\u3002");
950
+ if (diagnostic.stage === "resources" || diagnostic.code === "TEMPLATE_INPUT") lines.push(" \u6838\u5BF9\u63D2\u4EF6\u8FD0\u884C\u5305\u8D44\u6E90\uFF1B\u4E0D\u8981\u7528\u6539\u7528\u6237 room.json \u63A9\u76D6\u5305\u8D44\u6E90\u95EE\u9898\u3002");
951
+ }
952
+ return lines.join("\n");
953
+ }
954
+ async function main(argv, io) {
955
+ const jsonCount = argv.filter((arg) => arg === "--json").length;
956
+ const inputs = argv.filter((arg) => arg !== "--json");
957
+ const unknown = inputs.filter((arg) => arg.startsWith("-"));
958
+ let outcome;
959
+ if (jsonCount > 1 || inputs.length !== 2 || unknown.length > 0 || inputs.some((arg) => arg === "")) {
960
+ const message = `\u9700\u8981\u4E24\u4E2A\u4F4D\u7F6E\u53C2\u6570\uFF08\u9879\u76EE\u4ED3\u5E93\u6839\u3001\u4E1A\u52A1 ID\uFF09\u548C\u81F3\u591A\u4E00\u6B21 --json\u3002${unknown.length ? `\u672A\u77E5\u53C2\u6570\uFF1A${unknown.join("\u3001")}` : ""}`;
961
+ outcome = withProblems(initialOutcome(), "failed", [{ stage: "input", code: "bad-request", message }]);
962
+ io.err(`${message}
963
+ ${USAGE}
964
+ `);
965
+ } else {
966
+ outcome = await runRoomCheck(inputs[0], inputs[1]);
967
+ }
968
+ io.out(`${jsonCount > 0 ? JSON.stringify(outcome, null, 2) : formatReport(outcome)}
969
+ `);
970
+ return exitCodeOf(outcome);
971
+ }
972
+ if (process.argv[1] !== void 0 && pathToFileURL(process.argv[1]).href === import.meta.url) {
973
+ process.exitCode = await main(process.argv.slice(2), { out: (text) => process.stdout.write(text), err: (text) => process.stderr.write(text) });
974
+ }
975
+ export {
976
+ exitCodeOf,
977
+ formatReport,
978
+ main,
979
+ runRoomCheck
980
+ };