@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,1669 @@
1
+ // 构建产物:由 scripts/build.mjs 从 src/ 生成,请勿手改。
2
+
3
+
4
+ // src/cli/check-building.ts
5
+ import path2 from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ // src/core/errors.ts
9
+ var CoreError = class extends Error {
10
+ code;
11
+ httpStatus;
12
+ // 字段在构造函数体内赋值(不用 TS 参数属性):core 全部保持"可擦除语法",
13
+ // 纯 Node 剥类型模式即可加载,scripts/smoke.mjs 不依赖 tsx。
14
+ constructor(code, message, httpStatus = 500) {
15
+ super(message);
16
+ this.code = code;
17
+ this.httpStatus = httpStatus;
18
+ this.name = "CoreError";
19
+ }
20
+ };
21
+
22
+ // src/core/git.ts
23
+ import { execFile } from "node:child_process";
24
+ import { promisify } from "node:util";
25
+ import { readFile, stat } from "node:fs/promises";
26
+ import { realpathSync } from "node:fs";
27
+ import path from "node:path";
28
+ var execFileAsync = promisify(execFile);
29
+ var GIT_TIMEOUT_MS = 1e4;
30
+ var MAX_FILE_BYTES = 2 * 1024 * 1024;
31
+ var EXEC_MAX_BUFFER = MAX_FILE_BYTES * 2;
32
+ async function runGit(repoRoot, args) {
33
+ let caught;
34
+ try {
35
+ const { stdout } = await execFileAsync("git", ["-C", repoRoot, ...args], {
36
+ timeout: GIT_TIMEOUT_MS,
37
+ maxBuffer: EXEC_MAX_BUFFER,
38
+ encoding: "utf8",
39
+ windowsHide: true
40
+ });
41
+ return stdout;
42
+ } catch (error) {
43
+ caught = error;
44
+ }
45
+ const err = caught;
46
+ if (err.code === "ENOENT") {
47
+ 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);
48
+ }
49
+ if (err.killed || err.signal === "SIGTERM") {
50
+ throw new CoreError("git-timeout", `git ${args[0]} \u8D85\u8FC7 ${GIT_TIMEOUT_MS / 1e3} \u79D2\u672A\u8FD4\u56DE\uFF0C\u5DF2\u4E2D\u6B62`, 504);
51
+ }
52
+ const rawText = String(err.stderr || err.message || "");
53
+ if (/not a git repository/i.test(rawText)) {
54
+ throw new CoreError(
55
+ "not-a-git-repo",
56
+ `\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`,
57
+ 422
58
+ );
59
+ }
60
+ const firstLine = rawText.split("\n")[0].trim();
61
+ throw new CoreError("git-failed", `git ${args[0]} \u5931\u8D25\uFF1A${firstLine || "\u672A\u77E5\u9519\u8BEF"}`);
62
+ }
63
+ function isHex40(value) {
64
+ return typeof value === "string" && /^[0-9a-f]{40}$/.test(value);
65
+ }
66
+ async function assertRepoUsable(repoRoot) {
67
+ let info;
68
+ try {
69
+ info = await stat(repoRoot);
70
+ } catch (error) {
71
+ const code = error?.code;
72
+ if (code === "ENOENT" || code === "ENOTDIR") {
73
+ throw new CoreError("repo-unavailable", `\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u79FB\u52A8\uFF1A${repoRoot}`, 404);
74
+ }
75
+ throw new CoreError("repo-unavailable", `\u76EE\u5F55\u65E0\u6CD5\u8BFB\u53D6\uFF08${code ?? String(error)}\uFF09\uFF1A${repoRoot}`, 403);
76
+ }
77
+ if (!info.isDirectory()) {
78
+ throw new CoreError("repo-unavailable", `\u8DEF\u5F84\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55\uFF1A${repoRoot}`, 404);
79
+ }
80
+ }
81
+ function sameRealPath(a, b) {
82
+ const norm = (p) => {
83
+ let resolved;
84
+ try {
85
+ resolved = realpathSync(p);
86
+ } catch {
87
+ resolved = p;
88
+ }
89
+ const unified = path.resolve(resolved);
90
+ return process.platform === "win32" ? unified.toLowerCase() : unified;
91
+ };
92
+ return norm(a) === norm(b);
93
+ }
94
+ async function assertRepoTopLevel(repoRoot) {
95
+ const output = (await runGit(repoRoot, ["rev-parse", "--show-toplevel"])).trim();
96
+ if (!output) {
97
+ throw new CoreError("not-a-git-repo", `\u65E0\u6CD5\u786E\u5B9A ${repoRoot} \u7684 Git \u4ED3\u5E93\u9876\u5C42`, 422);
98
+ }
99
+ if (!sameRealPath(output, repoRoot)) {
100
+ throw new CoreError(
101
+ "repo-not-top-level",
102
+ `\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`,
103
+ 422
104
+ );
105
+ }
106
+ }
107
+ async function gitShowFileOptional(repoRoot, commit, relPath) {
108
+ if (!isHex40(commit)) {
109
+ throw new CoreError("bad-request", `\u5185\u90E8\u9519\u8BEF\uFF1AgitShowFileOptional \u53EA\u63A5\u53D7 40 \u4F4D\u63D0\u4EA4\u53F7\uFF0C\u6536\u5230 ${commit}`, 400);
110
+ }
111
+ const spec = `${commit}:${relPath}`;
112
+ const entry = (await runGit(repoRoot, ["ls-tree", commit, "--", relPath])).trim();
113
+ if (entry === "") return null;
114
+ const objectType = entry.split(" ")[0].split(" ")[1];
115
+ if (objectType !== "blob") return null;
116
+ const sizeText = await runGit(repoRoot, ["cat-file", "-s", spec]);
117
+ const size = Number(sizeText.trim());
118
+ if (!Number.isFinite(size) || size < 0) {
119
+ throw new CoreError("git-failed", `\u65E0\u6CD5\u786E\u5B9A ${relPath} \u5728\u63D0\u4EA4 ${commit.slice(0, 8)} \u4E0A\u7684\u5927\u5C0F`);
120
+ }
121
+ if (size > MAX_FILE_BYTES) {
122
+ throw new CoreError(
123
+ "file-too-large",
124
+ `\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`,
125
+ 413
126
+ );
127
+ }
128
+ return runGit(repoRoot, ["show", spec]);
129
+ }
130
+ function statConfirmedAbsent(error) {
131
+ const code = error?.code;
132
+ return code === "ENOENT" || code === "ENOTDIR";
133
+ }
134
+ function fsFailed(relPath, error) {
135
+ const code = error?.code;
136
+ return new CoreError("fs-failed", `\u8BBF\u95EE ${relPath} \u5931\u8D25\uFF08${code ?? String(error)}\uFF09`, 500);
137
+ }
138
+ async function worktreeFileExists(repoRoot, relPath) {
139
+ const normalized = relPath.split("/").filter((part) => part !== "" && part !== ".");
140
+ if (normalized.some((part) => part === "..") || path.isAbsolute(relPath)) {
141
+ throw new CoreError("bad-request", `\u975E\u6CD5\u8DEF\u5F84\uFF1A${relPath}`, 400);
142
+ }
143
+ const full = path.resolve(repoRoot, ...normalized);
144
+ try {
145
+ return (await stat(full)).isFile();
146
+ } catch (error) {
147
+ if (statConfirmedAbsent(error)) return false;
148
+ throw fsFailed(relPath, error);
149
+ }
150
+ }
151
+ async function readWorktreeFileOptional(repoRoot, relPath) {
152
+ const normalized = relPath.split("/").filter((part) => part !== "" && part !== ".");
153
+ if (normalized.some((part) => part === "..") || path.isAbsolute(relPath)) {
154
+ throw new CoreError("bad-request", `\u975E\u6CD5\u8DEF\u5F84\uFF1A${relPath}`, 400);
155
+ }
156
+ const full = path.resolve(repoRoot, ...normalized);
157
+ const root = path.resolve(repoRoot);
158
+ if (full !== root && !full.startsWith(root + path.sep)) {
159
+ throw new CoreError("bad-request", `\u8DEF\u5F84\u8D8A\u51FA\u4ED3\u5E93\u6839\uFF1A${relPath}`, 400);
160
+ }
161
+ let size;
162
+ try {
163
+ size = (await stat(full)).size;
164
+ } catch (error) {
165
+ if (statConfirmedAbsent(error)) return null;
166
+ throw fsFailed(relPath, error);
167
+ }
168
+ if (size > MAX_FILE_BYTES) {
169
+ throw new CoreError(
170
+ "file-too-large",
171
+ `\u6587\u4EF6 ${relPath} \u4E3A ${size} \u5B57\u8282\uFF0C\u8D85\u8FC7\u8BFB\u53D6\u4E0A\u9650 ${MAX_FILE_BYTES} \u5B57\u8282`,
172
+ 413
173
+ );
174
+ }
175
+ return readFile(full, "utf8");
176
+ }
177
+
178
+ // src/core/types.ts
179
+ var CONVENTION_ROOT = "docs/specdev";
180
+ var SCHEMA = {
181
+ project: "specdev/project/1",
182
+ business: "specdev/business/1",
183
+ chart: "specdev/chart/1",
184
+ evidence: "specdev/evidence/1",
185
+ snapshot: "specdev/snapshot/1"
186
+ };
187
+ var BUILDING_BLUEPRINT_REL = `${CONVENTION_ROOT}/building.json`;
188
+ var BUILDING_FORMAT = "specdev-building";
189
+ var BUILDING_SCHEMA = "specdev/building/1";
190
+
191
+ // src/core/snapshots.ts
192
+ var STAGES = ["design", "implemented"];
193
+ function parseTagName(tag) {
194
+ const parts = tag.split("/");
195
+ if (parts.length < 3 || parts[0] !== "specdev" || !parts[1] || parts.some((p) => p === "")) return null;
196
+ return { chart: parts[1], version: parts.slice(2).join("/") };
197
+ }
198
+ async function listSpecdevTags(repoRoot) {
199
+ const format = "%(refname:short)%09%(objecttype)%09%(*objectname)%09%(*objecttype)%09%(creatordate:iso8601-strict)%09%(contents)%00";
200
+ const out = await runGit(repoRoot, ["for-each-ref", `refs/tags/${"specdev"}/`, `--format=${format}`]);
201
+ const records = [];
202
+ for (const raw of out.split("\0")) {
203
+ const chunk = raw.replace(/^[\r\n]+/, "").replace(/[\r\n]+$/, "");
204
+ if (!chunk) continue;
205
+ const head = chunk.indexOf(" ");
206
+ if (head < 0) continue;
207
+ const rest = chunk.slice(head + 1);
208
+ const fields = rest.split(" ");
209
+ records.push({
210
+ tag: chunk.slice(0, head),
211
+ objecttype: fields[0],
212
+ derefObject: fields[1],
213
+ derefType: fields[2],
214
+ createdDate: fields[3],
215
+ message: fields.slice(4).join(" ")
216
+ });
217
+ }
218
+ return records;
219
+ }
220
+ function validateRecord(record) {
221
+ const parsed = parseTagName(record.tag);
222
+ if (!parsed) return { invalid: "\u6807\u7B7E\u540D\u4E0D\u7B26\u5408 specdev/<\u56FE\u7F16\u53F7>/<\u7248\u672C> \u7ED3\u6784" };
223
+ if (record.objecttype !== "tag") return { invalid: "\u4E0D\u662F\u9644\u6CE8\u6807\u7B7E\uFF08\u8F7B\u91CF\u6807\u7B7E\u4E0D\u767B\u8BB0\u5FEB\u7167\uFF09" };
224
+ if (!/^[0-9a-f]{40}$/.test(record.derefObject)) return { invalid: "\u5265\u58F3\u540E\u6CA1\u6709\u6307\u5411\u6709\u6548\u5BF9\u8C61" };
225
+ if (record.derefType !== "commit") {
226
+ 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` };
227
+ }
228
+ let meta;
229
+ try {
230
+ meta = JSON.parse(record.message);
231
+ } catch {
232
+ return { invalid: "\u6807\u7B7E message \u4E0D\u662F\u5408\u6CD5 JSON" };
233
+ }
234
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return { invalid: "\u6807\u7B7E message \u4E0D\u662F JSON \u5BF9\u8C61" };
235
+ const m = meta;
236
+ if (m.schema !== SCHEMA.snapshot) return { invalid: `message.schema \u4E0D\u662F ${SCHEMA.snapshot}` };
237
+ if (typeof m.chart !== "string" || !m.chart) return { invalid: "message.chart \u7F3A\u5931\u6216\u4E0D\u662F\u5B57\u7B26\u4E32" };
238
+ if (typeof m.name !== "string" || !m.name.trim()) return { invalid: "message.name \u7F3A\u5931\u6216\u4E3A\u7A7A" };
239
+ if (!STAGES.includes(m.stage)) return { invalid: `message.stage \u5FC5\u987B\u662F ${STAGES.join(" / ")}` };
240
+ if (typeof m.dir !== "string" || !m.dir.startsWith(CONVENTION_ROOT + "/")) {
241
+ return { invalid: `message.dir \u5FC5\u987B\u4EE5 ${CONVENTION_ROOT}/ \u5F00\u5934` };
242
+ }
243
+ 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` };
244
+ if (m.savedAt !== void 0 && typeof m.savedAt !== "string") return { invalid: "message.savedAt \u4E0D\u662F\u5B57\u7B26\u4E32" };
245
+ if (m.note !== void 0 && typeof m.note !== "string") return { invalid: "message.note \u4E0D\u662F\u5B57\u7B26\u4E32" };
246
+ return {
247
+ meta: {
248
+ schema: SCHEMA.snapshot,
249
+ chart: m.chart,
250
+ name: m.name,
251
+ stage: m.stage,
252
+ note: m.note,
253
+ dir: m.dir,
254
+ savedAt: m.savedAt
255
+ },
256
+ commit: record.derefObject
257
+ };
258
+ }
259
+ function snapshotsForChart(records, chartId) {
260
+ const snapshots = [];
261
+ let invalidCount = 0;
262
+ const invalidSamples = [];
263
+ for (const record of records) {
264
+ const parsed = parseTagName(record.tag);
265
+ if (!parsed || parsed.chart !== chartId) continue;
266
+ const result = validateRecord(record);
267
+ if ("invalid" in result) {
268
+ invalidCount += 1;
269
+ if (invalidSamples.length < 5) invalidSamples.push(record.tag);
270
+ continue;
271
+ }
272
+ snapshots.push({
273
+ tag: record.tag,
274
+ version: parsed.version,
275
+ chart: chartId,
276
+ commit: result.commit,
277
+ createdDate: record.createdDate,
278
+ meta: result.meta
279
+ });
280
+ }
281
+ snapshots.sort((a, b) => {
282
+ const keyA = a.meta.savedAt ?? a.createdDate;
283
+ const keyB = b.meta.savedAt ?? b.createdDate;
284
+ if (keyA !== keyB) return keyA < keyB ? 1 : -1;
285
+ return a.tag < b.tag ? 1 : -1;
286
+ });
287
+ return { snapshots, invalidCount, invalidSamples };
288
+ }
289
+
290
+ // src/core/inventory.ts
291
+ import { readdir } from "node:fs/promises";
292
+
293
+ // src/core/descriptor.ts
294
+ async function readDescriptor(repoRoot, relPath) {
295
+ const text = await readWorktreeFileOptional(repoRoot, relPath);
296
+ if (text === null) return { data: null, error: `\u7F3A\u5C11\u8BF4\u660E\u6587\u4EF6 ${relPath}` };
297
+ try {
298
+ return { data: JSON.parse(text) };
299
+ } catch (error) {
300
+ return { data: null, error: `${relPath} \u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${error.message}` };
301
+ }
302
+ }
303
+ function isPlainObject(value) {
304
+ return typeof value === "object" && value !== null && !Array.isArray(value);
305
+ }
306
+ function requireStringFields(record, fields) {
307
+ for (const field of fields) {
308
+ const value = record[field];
309
+ if (typeof value !== "string" || !value.trim()) return `${field} \u7F3A\u5931\u6216\u4E0D\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`;
310
+ }
311
+ return null;
312
+ }
313
+
314
+ // src/core/chart-files.ts
315
+ var CHART_FILE_NAMES = ["workflow.json", "details.md", "evidence.json"];
316
+
317
+ // src/core/fingerprint.ts
318
+ function normalizeEol(text) {
319
+ return text.replace(/\r\n/g, "\n");
320
+ }
321
+
322
+ // src/core/chart.ts
323
+ function normalizeOrNull(text) {
324
+ return text === null ? null : normalizeEol(text);
325
+ }
326
+ async function compareCurrentWithLatest(repoRoot, chartDir, latest) {
327
+ for (const name of CHART_FILE_NAMES) {
328
+ const worktree = await readWorktreeFileOptional(repoRoot, `${chartDir}/${name}`);
329
+ const snapshot = await gitShowFileOptional(repoRoot, latest.commit, `${latest.meta.dir}/${name}`);
330
+ if (normalizeOrNull(worktree) !== normalizeOrNull(snapshot)) return "changed";
331
+ }
332
+ return "identical";
333
+ }
334
+
335
+ // src/core/inventory.ts
336
+ async function readProjectInfo(repoRoot) {
337
+ const relPath = `${CONVENTION_ROOT}/project.json`;
338
+ const text = await readWorktreeFileOptional(repoRoot, relPath);
339
+ if (text === null) {
340
+ throw new CoreError(
341
+ "no-convention-root",
342
+ `\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`,
343
+ 404
344
+ );
345
+ }
346
+ let data;
347
+ try {
348
+ data = JSON.parse(text);
349
+ } catch (error) {
350
+ throw new CoreError("bad-inventory", `${relPath} \u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${error.message}`, 500);
351
+ }
352
+ if (!isPlainObject(data) || data.schema !== SCHEMA.project) {
353
+ throw new CoreError("bad-inventory", `${relPath} \u7684 schema \u5FC5\u987B\u662F ${SCHEMA.project}`, 500);
354
+ }
355
+ const fieldError = requireStringFields(data, ["name"]);
356
+ if (fieldError) throw new CoreError("bad-inventory", `${relPath}\uFF1A${fieldError}`, 500);
357
+ return data;
358
+ }
359
+ async function readInventory(repoRoot) {
360
+ const project = await readProjectInfo(repoRoot);
361
+ const tagRecords = await listSpecdevTags(repoRoot);
362
+ const businessEntries = await readdir(`${repoRoot}/${CONVENTION_ROOT}`, { withFileTypes: true });
363
+ const businesses = [];
364
+ for (const entry of businessEntries) {
365
+ if (!entry.isDirectory()) continue;
366
+ const businessId = entry.name;
367
+ const businessDir = `${CONVENTION_ROOT}/${businessId}`;
368
+ const summary = { id: businessId, name: businessId, docs: [], charts: [] };
369
+ const { data, error } = await readDescriptor(repoRoot, `${businessDir}/business.json`);
370
+ if (!data) {
371
+ summary.descriptorError = error ?? "business.json \u7F3A\u5931";
372
+ businesses.push(summary);
373
+ continue;
374
+ }
375
+ if (!isPlainObject(data) || data.schema !== SCHEMA.business) {
376
+ summary.descriptorError = `${businessDir}/business.json \u7684 schema \u5FC5\u987B\u662F ${SCHEMA.business}`;
377
+ businesses.push(summary);
378
+ continue;
379
+ }
380
+ const fieldError = requireStringFields(data, ["id", "name"]);
381
+ if (fieldError) {
382
+ summary.descriptorError = `${businessDir}/business.json\uFF1A${fieldError}`;
383
+ businesses.push(summary);
384
+ continue;
385
+ }
386
+ if (data.id !== businessId) {
387
+ summary.descriptorError = `business.json \u7684 id\uFF08${data.id}\uFF09\u4E0E\u76EE\u5F55\u540D\uFF08${businessId}\uFF09\u4E0D\u4E00\u81F4`;
388
+ businesses.push(summary);
389
+ continue;
390
+ }
391
+ summary.name = data.name;
392
+ summary.intro = typeof data.intro === "string" ? data.intro : void 0;
393
+ if (Array.isArray(data.docs)) {
394
+ summary.docs = data.docs.filter((item) => typeof item === "string" && item !== "");
395
+ }
396
+ summary.charts = await readChartSummaries(repoRoot, businessDir, tagRecords);
397
+ businesses.push(summary);
398
+ }
399
+ businesses.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
400
+ const ownersByChartId = /* @__PURE__ */ new Map();
401
+ for (const business of businesses) {
402
+ for (const chart of business.charts) {
403
+ ownersByChartId.set(chart.id, [...ownersByChartId.get(chart.id) ?? [], business.id]);
404
+ }
405
+ }
406
+ for (const business of businesses) {
407
+ for (const chart of business.charts) {
408
+ const owners = ownersByChartId.get(chart.id) ?? [];
409
+ if (owners.length > 1) {
410
+ 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`;
411
+ }
412
+ }
413
+ }
414
+ return { project, businesses };
415
+ }
416
+ async function readChartSummaries(repoRoot, businessDir, tagRecords) {
417
+ const chartEntries = await readdir(`${repoRoot}/${businessDir}`, { withFileTypes: true });
418
+ const charts = [];
419
+ for (const entry of chartEntries) {
420
+ if (!entry.isDirectory()) continue;
421
+ const chartId = entry.name;
422
+ const chartDir = `${businessDir}/${chartId}`;
423
+ const snapshots = snapshotsForChart(tagRecords, chartId);
424
+ const summary = {
425
+ id: chartId,
426
+ name: chartId,
427
+ hasWorkflow: await worktreeFileExists(repoRoot, `${chartDir}/workflow.json`),
428
+ snapshotCount: snapshots.snapshots.length,
429
+ invalidTagCount: snapshots.invalidCount,
430
+ currentStatus: "no-snapshot"
431
+ };
432
+ const { data, error } = await readDescriptor(repoRoot, `${chartDir}/chart.json`);
433
+ if (!data) {
434
+ summary.descriptorError = error ?? "chart.json \u7F3A\u5931";
435
+ charts.push(summary);
436
+ continue;
437
+ }
438
+ if (!isPlainObject(data) || data.schema !== SCHEMA.chart) {
439
+ summary.descriptorError = `${chartDir}/chart.json \u7684 schema \u5FC5\u987B\u662F ${SCHEMA.chart}`;
440
+ charts.push(summary);
441
+ continue;
442
+ }
443
+ const fieldError = requireStringFields(data, ["id", "name"]);
444
+ if (fieldError) {
445
+ summary.descriptorError = `${chartDir}/chart.json\uFF1A${fieldError}`;
446
+ charts.push(summary);
447
+ continue;
448
+ }
449
+ if (data.id !== chartId) {
450
+ summary.descriptorError = `chart.json \u7684 id\uFF08${data.id}\uFF09\u4E0E\u76EE\u5F55\u540D\uFF08${chartId}\uFF09\u4E0D\u4E00\u81F4`;
451
+ charts.push(summary);
452
+ continue;
453
+ }
454
+ summary.name = data.name;
455
+ summary.summary = typeof data.summary === "string" ? data.summary : void 0;
456
+ const latest = snapshots.snapshots[0];
457
+ if (latest) {
458
+ try {
459
+ summary.currentStatus = await compareCurrentWithLatest(repoRoot, chartDir, latest);
460
+ } catch (error2) {
461
+ summary.currentStatus = "compare-failed";
462
+ summary.compareError = error2 instanceof Error ? error2.message : String(error2);
463
+ }
464
+ summary.latestSnapshot = {
465
+ tag: latest.tag,
466
+ name: latest.meta.name,
467
+ stage: latest.meta.stage,
468
+ savedAt: latest.meta.savedAt,
469
+ createdDate: latest.createdDate
470
+ };
471
+ }
472
+ charts.push(summary);
473
+ }
474
+ charts.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
475
+ return charts;
476
+ }
477
+
478
+ // src/core/room.ts
479
+ var ROOM_REQUIRED_FIELDS = ["schemaVersion", "roomId", "businessId", "templateRef", "instances"];
480
+ var ROOM_ALLOWED_FIELDS = [...ROOM_REQUIRED_FIELDS, "bindings"];
481
+
482
+ // src/core/house.ts
483
+ var PHASES = [
484
+ "design",
485
+ "foundation",
486
+ "structure",
487
+ "enclosure",
488
+ "services",
489
+ "interior",
490
+ "accepted"
491
+ ];
492
+ var PROTOTYPES = {
493
+ foyer: "#708c79",
494
+ studio: "#4e8586",
495
+ archive: "#9a8963",
496
+ review: "#ab7966",
497
+ observatory: "#728ba9"
498
+ };
499
+ var EPS = 1e-6;
500
+ var SIGN_CLEARANCE = 0.07;
501
+ var round = (n) => Math.round(n * 1e6) / 1e6;
502
+ var eq = (a, b) => Math.abs(a - b) < EPS;
503
+ var positiveOverlap = (a, b, c, d) => Math.min(b, d) - Math.max(a, c) > EPS;
504
+ var overlap = (a, b) => positiveOverlap(a.x0, a.x1, b.x0, b.x1) && positiveOverlap(a.z0, a.z1, b.z0, b.z1);
505
+ var inside = (b, p, margin = 0) => p.x >= b.x0 + margin - EPS && p.x <= b.x1 - margin + EPS && p.z >= b.z0 + margin - EPS && p.z <= b.z1 - margin + EPS;
506
+ var sorted = (xs) => [...xs].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
507
+ var sides = ["north", "south", "east", "west"];
508
+ var boxRect = (r) => ({
509
+ x0: round(r.x - r.width / 2),
510
+ x1: round(r.x + r.width / 2),
511
+ z0: round(r.z - r.depth / 2),
512
+ z1: round(r.z + r.depth / 2)
513
+ });
514
+ var boundsOf = (bs) => ({
515
+ x0: Math.min(...bs.map((b) => b.x0)),
516
+ x1: Math.max(...bs.map((b) => b.x1)),
517
+ z0: Math.min(...bs.map((b) => b.z0)),
518
+ z1: Math.max(...bs.map((b) => b.z1))
519
+ });
520
+ var DEFAULT_STATUS = {
521
+ design: "detailed",
522
+ approval: "pending",
523
+ implementation: "unknown",
524
+ tests: "pending",
525
+ acceptance: "pending",
526
+ evidence: {}
527
+ };
528
+ var S = { type: "string" };
529
+ var N = { type: "number" };
530
+ var B = { type: "boolean" };
531
+ var enumeration = (values) => ({ type: "string", enum: values });
532
+ var object = (props, required = Object.keys(props)) => ({
533
+ type: "object",
534
+ props,
535
+ required
536
+ });
537
+ var array = (item) => ({ type: "array", item });
538
+ var idRule = { type: "string", pattern: /^[a-z][a-z0-9-]*$/ };
539
+ var businessIdRule = { type: "string", pattern: /^[A-Za-z0-9][A-Za-z0-9._-]*$/ };
540
+ var statusRule = object({
541
+ design: enumeration(["concept", "detailed"]),
542
+ approval: enumeration(["pending", "approved"]),
543
+ implementation: enumeration(["absent", "unknown", "present"]),
544
+ tests: enumeration(["pending", "passed"]),
545
+ acceptance: enumeration(["pending", "passed"]),
546
+ evidence: object({ implementation: S, tests: S, acceptance: S }, [])
547
+ });
548
+ var endpointRule = object({ spaceId: idRule, x: N, z: N });
549
+ var blueprintRule = object(
550
+ {
551
+ format: enumeration([BUILDING_FORMAT]),
552
+ schema: enumeration([BUILDING_SCHEMA]),
553
+ units: enumeration(["m"]),
554
+ style: enumeration(["warm-modern", "chinese-timber"]),
555
+ defaults: object({ wallThickness: N, slabThickness: N, doorHeight: N }, []),
556
+ floors: array(object({ id: idRule, name: S, elevation: N, height: N })),
557
+ spaces: array(
558
+ object(
559
+ {
560
+ id: idRule,
561
+ name: S,
562
+ kind: enumeration(["room", "corridor", "terrace", "yard"]),
563
+ floorId: idRule,
564
+ rect: object({ x: N, z: N, width: N, depth: N }),
565
+ businessId: businessIdRule,
566
+ directory: B,
567
+ prototype: enumeration(Object.keys(PROTOTYPES)),
568
+ height: N,
569
+ accent: { type: "string", pattern: /^#[a-fA-F0-9]{6}$/ },
570
+ wallType: enumeration(["plaster", "brick", "timber"]),
571
+ status: statusRule
572
+ },
573
+ ["id", "kind", "floorId", "rect"]
574
+ )
575
+ ),
576
+ openings: array(
577
+ object(
578
+ {
579
+ id: idRule,
580
+ kind: enumeration(["door", "window"]),
581
+ spaceId: idRule,
582
+ side: enumeration(sides),
583
+ offset: N,
584
+ width: N,
585
+ height: N,
586
+ sill: N,
587
+ to: idRule
588
+ },
589
+ ["id", "kind", "spaceId", "side", "offset", "width"]
590
+ )
591
+ ),
592
+ stairs: array(object({ id: idRule, from: endpointRule, to: endpointRule, width: N, steps: N })),
593
+ roofs: array(
594
+ object({ id: idRule, spaceIds: array(idRule), type: enumeration(["flat", "gable"]), rise: N, overhang: N }, [
595
+ "id",
596
+ "spaceIds",
597
+ "type"
598
+ ])
599
+ ),
600
+ construction: object({ phase: enumeration(PHASES), status: statusRule }),
601
+ entry: endpointRule
602
+ },
603
+ ["format", "schema", "units", "style", "floors", "spaces", "entry"]
604
+ );
605
+ function validateShape(value, rule, path3, report) {
606
+ const type = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
607
+ if (type !== rule.type || type === "number" && !Number.isFinite(value)) {
608
+ report("INPUT_TYPE", path3, `\u9700\u8981 ${rule.type} \u7C7B\u578B${rule.type === "number" ? "\u7684\u6709\u9650\u6570\u503C" : ""}\u3002`);
609
+ return;
610
+ }
611
+ if (rule.type === "string") {
612
+ const text = value;
613
+ if (rule.enum && !rule.enum.includes(text)) report("INPUT_ENUM", path3, `\u5141\u8BB8\u503C\uFF1A${rule.enum.join("\u3001")}\u3002`);
614
+ if (rule.pattern && !rule.pattern.test(text)) report("INPUT_PATTERN", path3, "\u683C\u5F0F\u4E0D\u5408\u6CD5\u3002");
615
+ if (!text.trim()) report("INPUT_EMPTY", path3, "\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A\u3002");
616
+ return;
617
+ }
618
+ if (rule.type === "object") {
619
+ const record = value;
620
+ for (const key of Object.keys(record)) {
621
+ if (!Object.hasOwn(rule.props, key)) {
622
+ report("INPUT_UNKNOWN_FIELD", `${path3}/${key}`, "\u672A\u5B9A\u4E49\u7684\u5B57\u6BB5\uFF0C\u8BF7\u68C0\u67E5\u62FC\u5199\u6216\u67E5\u9605\u5951\u7EA6\u3002");
623
+ } else {
624
+ validateShape(record[key], rule.props[key], `${path3}/${key}`, report);
625
+ }
626
+ }
627
+ for (const key of rule.required) if (!Object.hasOwn(record, key)) report("INPUT_REQUIRED", `${path3}/${key}`, "\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5\u3002");
628
+ return;
629
+ }
630
+ if (rule.type === "array") value.forEach((item, i) => validateShape(item, rule.item, `${path3}/${i}`, report));
631
+ }
632
+ function sideOf(space, side) {
633
+ const b = space.bounds;
634
+ const alongX = side === "north" || side === "south";
635
+ return {
636
+ axis: alongX ? "x" : "z",
637
+ at: side === "north" ? b.z0 : side === "south" ? b.z1 : side === "west" ? b.x0 : b.x1,
638
+ lo: alongX ? b.x0 : b.z0,
639
+ hi: alongX ? b.x1 : b.z1,
640
+ center: alongX ? space.rect.x : space.rect.z,
641
+ normal: { x: side === "west" ? 1 : side === "east" ? -1 : 0, y: 0, z: side === "north" ? 1 : side === "south" ? -1 : 0 }
642
+ };
643
+ }
644
+ function sharedEdge(a, b) {
645
+ const x = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
646
+ const z = Math.min(a.z1, b.z1) - Math.max(a.z0, b.z0);
647
+ if (x > EPS && (eq(a.z1, b.z0) || eq(a.z0, b.z1))) {
648
+ return { axis: "x", at: eq(a.z1, b.z0) ? a.z1 : a.z0, lo: Math.max(a.x0, b.x0), hi: Math.min(a.x1, b.x1) };
649
+ }
650
+ if (z > EPS && (eq(a.x1, b.x0) || eq(a.x0, b.x1))) {
651
+ return { axis: "z", at: eq(a.x1, b.x0) ? a.x1 : a.x0, lo: Math.max(a.z0, b.z0), hi: Math.min(a.z1, b.z1) };
652
+ }
653
+ return null;
654
+ }
655
+ function inWallRange(edge, lo, hi) {
656
+ return lo >= edge.lo - EPS && hi <= edge.hi + EPS;
657
+ }
658
+ function planeBox(axis, at, lo, hi, y0, y1, thickness) {
659
+ return {
660
+ position: { x: axis === "x" ? (lo + hi) / 2 : at, y: (y0 + y1) / 2, z: axis === "z" ? (lo + hi) / 2 : at },
661
+ size: { x: axis === "x" ? hi - lo : thickness, y: y1 - y0, z: axis === "z" ? hi - lo : thickness }
662
+ };
663
+ }
664
+ function generateBuilding(input, sources) {
665
+ const diagnostics = [];
666
+ const report = (code, path3, message, ids = [], evidence = {}, severity = "error") => {
667
+ diagnostics.push({ code, severity, message, subject: { path: path3, ids: [...ids] }, evidence });
668
+ };
669
+ const fail = () => ({ ok: false, model: null, diagnostics });
670
+ const errors = () => diagnostics.some((d) => d.severity === "error");
671
+ const rewriteIndexPaths = (source) => {
672
+ if (typeof source !== "object" || source === null) return;
673
+ const record = source;
674
+ const idAt = (key, index) => {
675
+ const list = record[key];
676
+ if (!Array.isArray(list)) return null;
677
+ const item = list[index];
678
+ if (typeof item !== "object" || item === null) return null;
679
+ const id = item.id;
680
+ return typeof id === "string" ? id : null;
681
+ };
682
+ for (const d of diagnostics) {
683
+ d.subject.path = d.subject.path.replace(
684
+ /^\/(floors|spaces|openings|stairs|roofs)\/(\d+)(?=\/|$)/,
685
+ (full, key, index) => {
686
+ const id = idAt(key, Number(index));
687
+ return id !== null ? `/${key}/${id}` : full;
688
+ }
689
+ );
690
+ }
691
+ };
692
+ validateShape(input, blueprintRule, "", report);
693
+ if (errors()) {
694
+ rewriteIndexPaths(input);
695
+ return fail();
696
+ }
697
+ const data = JSON.parse(JSON.stringify(input));
698
+ const defaults = { wallThickness: 0.18, slabThickness: 0.18, doorHeight: 2.5, ...data.defaults ?? {} };
699
+ const construction = data.construction ?? { phase: "design", status: structuredClone(DEFAULT_STATUS) };
700
+ for (const [key, value] of Object.entries(defaults)) {
701
+ if (!(value > 0 && value <= (key === "doorHeight" ? 8 : 0.6))) {
702
+ report("DEFAULT_RANGE", `/defaults/${key}`, "\u539A\u5EA6\u5E94\u5728 (0, 0.6] \u7C73\uFF0C\u95E8\u9AD8\u5E94\u5728 (0, 8] \u7C73\u3002");
703
+ }
704
+ }
705
+ if (!data.floors.length || !data.spaces.length) report("EMPTY_BUILDING", "/spaces", "\u81F3\u5C11\u9700\u8981\u4E00\u5C42\u3001\u4E00\u4E2A\u7A7A\u95F4\u3002");
706
+ if (data.spaces.length > 240 || data.floors.length > 24 || (data.openings ?? []).length > 2e3) {
707
+ report("INPUT_LIMIT", "/spaces", "\u672C\u7248\u652F\u6301\u6700\u591A 240 \u7A7A\u95F4\u300124 \u5C42\u30012000 \u4E2A\u5F00\u53E3\u3002");
708
+ }
709
+ if ((data.stairs ?? []).length > 96 || (data.roofs ?? []).length > 240) {
710
+ report("INPUT_LIMIT", "", "\u672C\u7248\u6700\u591A\u652F\u6301 96 \u8DD1\u697C\u68AF\u548C 240 \u4E2A\u5C4B\u9876\u3002");
711
+ }
712
+ if (errors()) return fail();
713
+ const paths = /* @__PURE__ */ new Map();
714
+ const identities = /* @__PURE__ */ new Map();
715
+ const register = (item, container) => {
716
+ const path3 = `${container}/${item.id}`;
717
+ if (identities.has(item.id)) {
718
+ report("ID_DUPLICATE", `${path3}/id`, "id \u5728\u84DD\u56FE\u5185\u91CD\u590D\u3002", [item.id], { duplicateOf: identities.get(item.id) });
719
+ } else {
720
+ identities.set(item.id, path3);
721
+ paths.set(item.id, path3);
722
+ }
723
+ };
724
+ for (const key of ["floors", "spaces", "openings", "stairs", "roofs"]) {
725
+ for (const item of data[key] ?? []) register(item, `/${key}`);
726
+ }
727
+ const pathOf = (id) => paths.get(id) ?? "";
728
+ const checkStatus = (status, path3, ids = []) => {
729
+ for (const key of ["implementation", "tests", "acceptance"]) {
730
+ if (status[key] === (key === "implementation" ? "present" : "passed") && !(status.evidence[key] ?? "").trim()) {
731
+ report("STATE_EVIDENCE_REQUIRED", `${path3}/evidence/${key}`, "\u6B64\u58F0\u660E\u5FC5\u987B\u6709\u5BF9\u5E94\u7684\u8BC1\u636E\u51FA\u5904\u3002", ids);
732
+ }
733
+ }
734
+ if ((status.tests === "passed" || status.acceptance === "passed") && status.implementation !== "present") {
735
+ report("STATE_CONTRADICTION", path3, "\u672A\u58F0\u660E\u6709\u5B9E\u73B0\uFF0C\u4E0D\u80FD\u58F0\u79F0\u5B9E\u73B0\u6D4B\u8BD5\u6216\u4F5C\u8005\u9A8C\u6536\u5DF2\u901A\u8FC7\u3002", ids);
736
+ }
737
+ if (status.approval === "approved" && status.design !== "detailed") {
738
+ report("STATE_CONTRADICTION", path3, "\u672C\u7248\u53EA\u6709\u8BE6\u7EC6\u8BBE\u8BA1\u624D\u80FD\u767B\u8BB0\u6279\u51C6\u3002", ids);
739
+ }
740
+ };
741
+ checkStatus(construction.status, "/construction/status");
742
+ const floors = sorted(data.floors).map((f) => ({ ...f, y: f.elevation }));
743
+ const floorMap = new Map(floors.map((f) => [f.id, f]));
744
+ for (const f of floors) {
745
+ if (f.height < 2.6 || f.height > 12 || Math.abs(f.y) > 500) {
746
+ report("FLOOR_RANGE", pathOf(f.id), "\u697C\u5C42\u51C0\u9AD8\u5E94\u5728 2.6\u201412 \u7C73\u4E4B\u95F4\uFF0C\u6807\u9AD8\u7EDD\u5BF9\u503C\u4E0D\u8D85\u8FC7 500 \u7C73\u3002", [f.id]);
747
+ }
748
+ }
749
+ const businessById = new Map(sources.businesses.map((b) => [b.id, b]));
750
+ const spaces = sorted(data.spaces).map((s) => {
751
+ const path3 = pathOf(s.id);
752
+ const f = floorMap.get(s.floorId);
753
+ if (!f) report("FLOOR_REF", `${path3}/floorId`, "\u697C\u5C42\u4E0D\u5B58\u5728\u3002", [s.id, s.floorId]);
754
+ if (s.rect.width < 1.6 || s.rect.depth < 1.6 || s.rect.width > 200 || s.rect.depth > 200 || Math.abs(s.rect.x) > 1e3 || Math.abs(s.rect.z) > 1e3) {
755
+ report("SPACE_DIMENSION", `${path3}/rect`, "\u7A7A\u95F4\u8FB9\u957F\u5E94\u4E3A 1.6\u2014200 \u7C73\uFF0C\u5750\u6807\u7EDD\u5BF9\u503C\u4E0D\u8D85\u8FC7 1000 \u7C73\u3002", [s.id]);
756
+ }
757
+ if (s.kind === "room" && !s.businessId) report("BUSINESS_REQUIRED", `${path3}/businessId`, "\u4E1A\u52A1\u623F\u9700\u8981\u7A33\u5B9A\u7684\u4E1A\u52A1 id\u3002", [s.id]);
758
+ const height = s.height ?? f?.height ?? 3.4;
759
+ if (height < 2.6 || height > 12) report("SPACE_HEIGHT", `${path3}/height`, "\u7A7A\u95F4\u5899\u9AD8\u5E94\u5728 2.6\u201412 \u7C73\u4E4B\u95F4\u3002", [s.id]);
760
+ const status = s.status ?? structuredClone(construction.status);
761
+ if (s.status) checkStatus(status, `${path3}/status`, [s.id]);
762
+ if (construction.phase === "accepted" && s.kind === "room" && !(status.approval === "approved" && status.implementation === "present" && status.tests === "passed" && status.acceptance === "passed")) {
763
+ report("ACCEPTANCE_INCOMPLETE", `${path3}/status`, "\u9A8C\u6536\u5165\u4F4F\u9700\u8981\u8BBE\u8BA1\u6279\u51C6\u3001\u5B9E\u73B0\u3001\u6D4B\u8BD5\u548C\u4F5C\u8005\u9A8C\u6536\u8BB0\u5F55\u9F50\u5907\u3002", [s.id]);
764
+ }
765
+ const source = s.businessId !== void 0 ? businessById.get(s.businessId) : void 0;
766
+ const name = s.kind === "room" ? source?.name ?? s.name ?? s.businessId ?? s.id : s.name ?? s.id;
767
+ return {
768
+ ...s,
769
+ name,
770
+ intro: source?.intro,
771
+ y: f?.y ?? 0,
772
+ height,
773
+ bounds: boxRect(s.rect),
774
+ prototype: s.prototype ?? "studio",
775
+ wallType: s.wallType ?? "plaster",
776
+ accent: s.accent ?? PROTOTYPES[s.prototype ?? "studio"],
777
+ status
778
+ };
779
+ });
780
+ if (errors()) return fail();
781
+ const spaceMap = new Map(spaces.map((s) => [s.id, s]));
782
+ const rooms = spaces.filter((s) => s.kind === "room");
783
+ for (let i = 0; i < spaces.length; i++) {
784
+ for (let j = i + 1; j < spaces.length; j++) {
785
+ const a = spaces[i];
786
+ const b = spaces[j];
787
+ if (a.floorId === b.floorId && overlap(a.bounds, b.bounds)) {
788
+ report("SPACE_OVERLAP", `${pathOf(b.id)}/rect`, "\u540C\u5C42\u7A7A\u95F4\u5185\u90E8\u76F8\u4EA4\uFF1B\u5171\u8FB9\u53EF\u7528\uFF0C\u91CD\u53E0\u4E0D\u53EF\u7528\u3002", [a.id, b.id], {
789
+ first: a.bounds,
790
+ second: b.bounds
791
+ });
792
+ }
793
+ if (a.floorId !== b.floorId && overlap(a.bounds, b.bounds) && positiveOverlap(a.y, a.y + a.height + defaults.slabThickness, b.y, b.y + b.height + defaults.slabThickness)) {
794
+ report("FLOOR_CLEARANCE", pathOf(b.id), "\u4E0A\u4E0B\u7A7A\u95F4\u51C0\u7A7A\u6216\u697C\u677F\u76F8\u4EA4\uFF0C\u8BF7\u8C03\u6574\u6807\u9AD8\u6216\u5899\u9AD8\u3002", [a.id, b.id]);
795
+ }
796
+ }
797
+ }
798
+ const entrySpace = spaceMap.get(data.entry.spaceId);
799
+ if (!entrySpace || !inside(entrySpace.bounds, data.entry, 0.35)) {
800
+ report("ENTRY_POSITION", "/entry", "\u5165\u53E3\u8D77\u70B9\u9700\u4F4D\u4E8E\u5DF2\u767B\u8BB0\u7A7A\u95F4\u5185\uFF0C\u5E76\u4E0E\u8FB9\u7F18\u4FDD\u7559 0.35 \u7C73\u3002", [data.entry.spaceId]);
801
+ }
802
+ const entry = { ...data.entry, y: entrySpace?.y ?? 0 };
803
+ const lines = /* @__PURE__ */ new Map();
804
+ for (const room of rooms) {
805
+ for (const side of sides) {
806
+ const edge = sideOf(room, side);
807
+ const key = `${room.floorId}|${edge.axis}|${edge.at}`;
808
+ if (!lines.has(key)) lines.set(key, []);
809
+ lines.get(key).push({ ...edge, room });
810
+ }
811
+ }
812
+ const walls = [];
813
+ for (const [key, entries] of [...lines].sort(([a], [b]) => a.localeCompare(b, "en"))) {
814
+ const cuts = [...new Set(entries.flatMap((e) => [e.lo, e.hi]))].sort((a, b) => a - b);
815
+ for (let i = 0; i < cuts.length - 1; i++) {
816
+ const lo = cuts[i];
817
+ const hi = cuts[i + 1];
818
+ const owners = entries.filter((e) => e.lo < hi - EPS && e.hi > lo + EPS);
819
+ if (!owners.length) continue;
820
+ const room = owners[0].room;
821
+ if (owners.some((e) => !eq(e.room.height, room.height) || e.room.wallType !== room.wallType)) {
822
+ report("SHARED_WALL_CONFLICT", pathOf(room.id), "\u5171\u5899\u623F\u95F4\u5FC5\u987B\u4F7F\u7528\u76F8\u540C\u5899\u9AD8\u4E0E\u5899\u6750\u7C7B\u578B\u3002", owners.map((e) => e.room.id));
823
+ }
824
+ walls.push({
825
+ id: `wall:${key}:${lo}:${hi}`,
826
+ axis: owners[0].axis,
827
+ at: owners[0].at,
828
+ lo,
829
+ hi,
830
+ y: room.y,
831
+ height: room.height,
832
+ floorId: room.floorId,
833
+ wallType: room.wallType,
834
+ spaceIds: owners.map((e) => e.room.id).sort(),
835
+ openings: [],
836
+ segments: []
837
+ });
838
+ }
839
+ }
840
+ const openings = [];
841
+ for (const o of sorted(data.openings ?? [])) {
842
+ const room = spaceMap.get(o.spaceId);
843
+ const path3 = pathOf(o.id);
844
+ if (!room || room.kind !== "room") {
845
+ report("OPENING_ROOM", `${path3}/spaceId`, "\u5F00\u53E3\u5FC5\u987B\u5F15\u7528\u4E1A\u52A1\u623F\u3002", [o.id, o.spaceId]);
846
+ continue;
847
+ }
848
+ const edge = sideOf(room, o.side);
849
+ const center = edge.center + o.offset;
850
+ const lo = center - o.width / 2;
851
+ const hi = center + o.width / 2;
852
+ const height = o.height ?? (o.kind === "door" ? defaults.doorHeight : 1.2);
853
+ const sill = o.sill ?? (o.kind === "door" ? 0 : 1);
854
+ if (o.width < (o.kind === "door" ? 0.95 : 0.2) || height < 0.2 || o.kind === "door" && height < 2.2 || sill < 0 || sill + height > room.height - 0.12 || !inWallRange({ lo: edge.lo + 0.2, hi: edge.hi - 0.2 }, lo, hi)) {
855
+ report("OPENING_SIZE", path3, "\u5F00\u53E3\u5C3A\u5BF8\u6216\u4F4D\u7F6E\u8D8A\u754C\uFF0C\u95E8\u9700\u81F3\u5C11\u5BBD 0.95\u3001\u9AD8 2.2 \u7C73\uFF0C\u5899\u89D2\u81F3\u5C11\u7559 0.2 \u7C73\u3002", [o.id]);
856
+ }
857
+ if (o.kind === "door") {
858
+ const target = o.to !== void 0 ? spaceMap.get(o.to) : void 0;
859
+ const shared = target ? sharedEdge(room.bounds, target.bounds) : null;
860
+ if (sill !== 0 || !target || target.floorId !== room.floorId || !shared || shared.axis !== edge.axis || !eq(shared.at, edge.at) || !inWallRange(shared, lo, hi)) {
861
+ report("DOOR_CONNECTION", path3, "\u95E8\u5FC5\u987B\u843D\u5728\u6765\u6E90\u623F\u95F4\u4E0E to \u7A7A\u95F4\u540C\u5C42\u7684\u5171\u8FB9\u4E0A\uFF0C\u4E14\u6CA1\u6709\u95E8\u69DB\u3002", [o.id, room.id, o.to ?? ""].filter(Boolean));
862
+ }
863
+ } else if (o.to !== void 0) {
864
+ report("WINDOW_CONNECTION", `${path3}/to`, "\u7A97\u4E0D\u80FD\u58F0\u660E\u4E3A\u53EF\u901A\u884C\u8FDE\u63A5\u3002", [o.id]);
865
+ }
866
+ const wall = walls.find((w) => w.floorId === room.floorId && w.axis === edge.axis && eq(w.at, edge.at) && inWallRange(w, lo, hi));
867
+ if (!wall) {
868
+ report("OPENING_PARTITION", path3, "\u5F00\u53E3\u8DE8\u8D8A\u4E86\u5171\u5899\u5206\u533A\u7AEF\u70B9\uFF0C\u8BF7\u8C03\u6574\u4F4D\u7F6E\u3002", [o.id]);
869
+ continue;
870
+ }
871
+ const value = {
872
+ ...o,
873
+ axis: edge.axis,
874
+ at: edge.at,
875
+ lo,
876
+ hi,
877
+ center,
878
+ height,
879
+ sill,
880
+ y: room.y,
881
+ floorId: room.floorId,
882
+ wallId: wall.id,
883
+ position: { x: edge.axis === "x" ? center : edge.at, y: room.y, z: edge.axis === "z" ? center : edge.at },
884
+ normal: edge.normal
885
+ };
886
+ wall.openings.push(value);
887
+ openings.push(value);
888
+ }
889
+ for (const wall of walls) {
890
+ for (let i = 0; i < wall.openings.length; i++) {
891
+ for (let j = i + 1; j < wall.openings.length; j++) {
892
+ const a = wall.openings[i];
893
+ const b = wall.openings[j];
894
+ if (positiveOverlap(a.lo, a.hi, b.lo, b.hi) && positiveOverlap(a.sill, a.sill + a.height, b.sill, b.sill + b.height)) {
895
+ report("OPENING_OVERLAP", pathOf(b.id), "\u540C\u4E00\u5899\u9762\u5F00\u53E3\u91CD\u53E0\uFF1B\u5171\u5899\u7684\u95E8\u53EA\u767B\u8BB0\u4E00\u6B21\u3002", [a.id, b.id]);
896
+ }
897
+ }
898
+ }
899
+ const cuts = [.../* @__PURE__ */ new Set([wall.lo, wall.hi, ...wall.openings.flatMap((o) => [o.lo, o.hi])])].sort((a, b) => a - b);
900
+ for (let i = 0; i < cuts.length - 1; i++) {
901
+ const lo = cuts[i];
902
+ const hi = cuts[i + 1];
903
+ const active = wall.openings.filter((o) => positiveOverlap(lo, hi, o.lo, o.hi)).sort((a, b) => a.sill - b.sill);
904
+ let bottom = 0;
905
+ for (const o of [...active, { sill: wall.height, height: 0 }]) {
906
+ if (o.sill > bottom + EPS) {
907
+ wall.segments.push({
908
+ id: `${wall.id}:piece:${wall.segments.length}`,
909
+ ...planeBox(wall.axis, wall.at, lo, hi, wall.y + bottom, wall.y + o.sill, defaults.wallThickness)
910
+ });
911
+ }
912
+ bottom = Math.max(bottom, o.sill + o.height);
913
+ }
914
+ }
915
+ }
916
+ const stairs = [];
917
+ for (const s of sorted(data.stairs ?? [])) {
918
+ const a = spaceMap.get(s.from.spaceId);
919
+ const b = spaceMap.get(s.to.spaceId);
920
+ const path3 = pathOf(s.id);
921
+ if (!a || !b || a.floorId === b.floorId || b.y <= a.y) {
922
+ report("STAIR_FLOORS", path3, "\u697C\u68AF\u9700\u4ECE\u4F4E\u5C42\u7A7A\u95F4\u8FDE\u5230\u9AD8\u5C42\u7A7A\u95F4\u3002", [s.id]);
923
+ continue;
924
+ }
925
+ if (a.kind === "room" || b.kind === "room") {
926
+ report("STAIR_ENDPOINT_KIND", path3, "\u697C\u68AF\u7AEF\u70B9\u9700\u8FDE\u63A5\u8D70\u5ECA\u3001\u5E73\u53F0\u6216\u5EAD\u9662\uFF1B\u4E0D\u80FD\u7A7F\u8FC7\u672A\u5F00\u6D1E\u7684\u623F\u95F4\u8FB9\u754C\u3002", [s.id, a.id, b.id]);
927
+ continue;
928
+ }
929
+ const dx = s.to.x - s.from.x;
930
+ const dz = s.to.z - s.from.z;
931
+ const length = Math.hypot(dx, dz);
932
+ const rise = b.y - a.y;
933
+ if (!eq(dx, 0) && !eq(dz, 0) || length < rise || length < EPS || s.width < 1.2 || s.width > 8 || !Number.isInteger(s.steps) || s.steps < 2 || s.steps > 120) {
934
+ report("STAIR_GEOMETRY", path3, "\u672C\u7248\u652F\u6301\u5BBD 1.2\u20148 \u7C73\u3001\u5761\u5EA6\u4E0D\u8D85\u8FC7 45\xB0\u30012\u2014120 \u7EA7\u7684\u8F74\u5411\u76F4\u8DD1\u68AF\u3002", [s.id]);
935
+ continue;
936
+ }
937
+ const ux = dx / length;
938
+ const uz = dz / length;
939
+ const rect = {
940
+ x0: Math.min(s.from.x, s.to.x) - Math.abs(uz) * s.width / 2,
941
+ x1: Math.max(s.from.x, s.to.x) + Math.abs(uz) * s.width / 2,
942
+ z0: Math.min(s.from.z, s.to.z) - Math.abs(ux) * s.width / 2,
943
+ z1: Math.max(s.from.z, s.to.z) + Math.abs(ux) * s.width / 2
944
+ };
945
+ let landed = true;
946
+ for (const [space, p, sign] of [
947
+ [a, s.from, -1],
948
+ [b, s.to, 1]
949
+ ]) {
950
+ for (const lateral of [-s.width / 2, s.width / 2]) {
951
+ for (const longitudinal of [0, 0.6]) {
952
+ const q = { x: p.x + ux * sign * longitudinal - uz * lateral, z: p.z + uz * sign * longitudinal + ux * lateral };
953
+ if (!inside(space.bounds, q)) landed = false;
954
+ }
955
+ }
956
+ if (inside(space.bounds, { x: p.x - ux * sign * 0.1, z: p.z - uz * sign * 0.1 }, 0.01)) landed = false;
957
+ }
958
+ if (!landed) {
959
+ report("STAIR_LANDING", path3, "\u697C\u68AF\u7AEF\u70B9\u5FC5\u987B\u63A5\u5728\u5E73\u53F0\u8FB9\u754C\uFF0C\u5E73\u53F0\u8981\u5BB9\u7EB3\u68AF\u5BBD\u5E76\u5411\u5916\u5EF6\u4F38\u81F3\u5C11 0.6 \u7C73\u3002", [s.id, a.id, b.id]);
960
+ }
961
+ for (const space of spaces) {
962
+ if (overlap(rect, space.bounds) && space.y + space.height > a.y + EPS && space.y - defaults.slabThickness < b.y + 2.2) {
963
+ report("STAIR_SPACE_INTERSECTION", path3, "\u697C\u68AF\u6295\u5F71\u4E0E\u672C\u7248\u8981\u6C42\u7559\u7A7A\u7684\u68AF\u4E95\u76F8\u4EA4\uFF1B\u8BF7\u5206\u5272\u697C\u677F\u7A7A\u95F4\u907F\u5F00\u68AF\u4E95\u3002", [s.id, space.id]);
964
+ }
965
+ }
966
+ const value = {
967
+ ...s,
968
+ from: { ...s.from, y: a.y },
969
+ to: { ...s.to, y: b.y },
970
+ bounds: rect,
971
+ length,
972
+ rise,
973
+ ux,
974
+ uz
975
+ };
976
+ for (const other of stairs) {
977
+ if (overlap(rect, other.bounds)) {
978
+ const intersection = {
979
+ x0: Math.max(rect.x0, other.bounds.x0),
980
+ x1: Math.min(rect.x1, other.bounds.x1),
981
+ z0: Math.max(rect.z0, other.bounds.z0),
982
+ z1: Math.min(rect.z1, other.bounds.z1)
983
+ };
984
+ const yAt = (stair, x, z) => stair.from.y + ((x - stair.from.x) * stair.ux + (z - stair.from.z) * stair.uz) / stair.length * stair.rise;
985
+ const gaps = [intersection.x0, intersection.x1].flatMap(
986
+ (x) => [intersection.z0, intersection.z1].map((z) => yAt(value, x, z) - yAt(other, x, z))
987
+ );
988
+ if (Math.min(...gaps) < 2.2 && Math.max(...gaps) > -2.2) {
989
+ report("STAIR_INTERSECTION", path3, "\u697C\u68AF\u4E4B\u95F4\u4E0D\u8DB3 2.2 \u7C73\u5934\u90E8\u51C0\u7A7A\u3002", [s.id, other.id]);
990
+ }
991
+ }
992
+ }
993
+ stairs.push(value);
994
+ if (rise / s.steps > 0.22 || length / s.steps < 0.24) {
995
+ report("STAIR_COMFORT", path3, "\u8E0F\u6B65\u504F\u9661\u6216\u504F\u7A84\uFF0C\u5EFA\u8BAE\u8C03\u6574\u6B65\u6570\u548C\u6C34\u5E73\u957F\u5EA6\u4EE5\u6539\u5584\u89C2\u611F\u3002", [s.id], {
996
+ risePerStep: rise / s.steps,
997
+ runPerStep: length / s.steps
998
+ }, "warning");
999
+ }
1000
+ }
1001
+ const navigation = {
1002
+ nodes: spaces.map((s) => ({ id: s.id, x: s.rect.x, y: s.y, z: s.rect.z })),
1003
+ edges: []
1004
+ };
1005
+ const connect = (a, b, kind, via) => {
1006
+ navigation.edges.push({ from: a, to: b, kind, via });
1007
+ };
1008
+ for (let i = 0; i < spaces.length; i++) {
1009
+ for (let j = i + 1; j < spaces.length; j++) {
1010
+ const a = spaces[i];
1011
+ const b = spaces[j];
1012
+ const edge = sharedEdge(a.bounds, b.bounds);
1013
+ if (a.kind !== "room" && b.kind !== "room" && a.floorId === b.floorId && edge && edge.hi - edge.lo >= 0.9) {
1014
+ connect(a.id, b.id, "open", null);
1015
+ }
1016
+ }
1017
+ }
1018
+ for (const door of openings) {
1019
+ if (door.kind === "door" && door.to !== void 0 && spaceMap.has(door.to)) connect(door.spaceId, door.to, "door", door.id);
1020
+ }
1021
+ for (const stair of stairs) connect(stair.from.spaceId, stair.to.spaceId, "stair", stair.id);
1022
+ const seen = new Set(entrySpace ? [entrySpace.id] : []);
1023
+ const queue = [...seen];
1024
+ for (let i = 0; i < queue.length; i++) {
1025
+ for (const edge of navigation.edges) {
1026
+ const neighbor = edge.from === queue[i] ? edge.to : edge.to === queue[i] ? edge.from : null;
1027
+ if (neighbor && !seen.has(neighbor)) {
1028
+ seen.add(neighbor);
1029
+ queue.push(neighbor);
1030
+ }
1031
+ }
1032
+ }
1033
+ for (const space of spaces) {
1034
+ if (!seen.has(space.id)) report("SPACE_UNREACHABLE", pathOf(space.id), "\u6309\u95E8\u548C\u697C\u68AF\u8FDE\u63A5\u4ECE\u5165\u53E3\u65E0\u6CD5\u5230\u8FBE\u6B64\u7A7A\u95F4\u3002", [space.id]);
1035
+ }
1036
+ const roofs = [];
1037
+ for (const r of sorted(data.roofs ?? [])) {
1038
+ const path3 = pathOf(r.id);
1039
+ const selected = r.spaceIds.map((id) => spaceMap.get(id));
1040
+ if (!selected.length || selected.some((s) => !s) || new Set(r.spaceIds).size !== r.spaceIds.length) {
1041
+ report("ROOF_REFS", `${path3}/spaceIds`, "\u5C4B\u9876\u9700\u5F15\u7528\u4E0D\u91CD\u590D\u7684\u73B0\u6709\u7A7A\u95F4\u3002", [r.id]);
1042
+ continue;
1043
+ }
1044
+ const picked = selected;
1045
+ const bounds2 = boundsOf(picked.map((s) => s.bounds));
1046
+ const first = picked[0];
1047
+ const base = first.y + first.height;
1048
+ const area = picked.reduce((sum, s) => sum + s.rect.width * s.rect.depth, 0);
1049
+ const enclosingArea = (bounds2.x1 - bounds2.x0) * (bounds2.z1 - bounds2.z0);
1050
+ if (picked.some((s) => s.floorId !== first.floorId || !eq(s.y + s.height, base)) || Math.abs(area - enclosingArea) > EPS) {
1051
+ report("ROOF_RECTANGLE", path3, "\u672C\u7248\u5C4B\u9876\u8986\u76D6\u5FC5\u987B\u662F\u540C\u5C42\u3001\u540C\u5899\u9AD8\u4E14\u65E0\u7A7A\u6D1E\u7684\u5B8C\u6574\u77E9\u5F62\u3002", [r.id]);
1052
+ }
1053
+ const overhang = r.overhang ?? 0.35;
1054
+ const rise = r.type === "gable" ? r.rise ?? 1.8 : 0;
1055
+ if (overhang < 0 || overhang > 2 || r.type === "gable" && (rise <= 0 || rise > 8) || r.type === "flat" && r.rise !== void 0 && r.rise !== 0) {
1056
+ report("ROOF_SIZE", path3, "\u6311\u6A90\u8303\u56F4\u4E3A 0\u20142 \u7C73\uFF0C\u5761\u9876\u5347\u9AD8\u4E3A (0, 8] \u7C73\uFF0C\u5E73\u9876\u4E0D\u8BBE\u7F6E rise\u3002", [r.id]);
1057
+ }
1058
+ const expanded = {
1059
+ x0: bounds2.x0 - overhang,
1060
+ x1: bounds2.x1 + overhang,
1061
+ z0: bounds2.z0 - overhang,
1062
+ z1: bounds2.z1 + overhang
1063
+ };
1064
+ for (const s of spaces) {
1065
+ if (s.y > first.y + EPS && overlap(expanded, s.bounds) && s.y - defaults.slabThickness < base + rise + 0.2) {
1066
+ report("ROOF_UPPER_FLOOR", path3, "\u5C4B\u9876\u7A7F\u5165\u4E0A\u65B9\u697C\u5C42\uFF0C\u8BF7\u6539\u4E3A\u9876\u5C42\u5C4B\u9876\u6216\u8C03\u6574\u8303\u56F4\u3002", [r.id, s.id]);
1067
+ }
1068
+ }
1069
+ for (const other of roofs) {
1070
+ if (overlap(expanded, other.bounds) && positiveOverlap(base - 0.1, base + rise + 0.2, other.base - 0.1, other.base + other.rise + 0.2)) {
1071
+ report("ROOF_INTERSECTION", path3, "\u5C4B\u9876\u5305\u7EDC\u76F8\u4EA4\uFF1B\u672C\u7248\u4E0D\u81EA\u52A8\u5904\u7406\u4EA4\u63A5\u5C4B\u810A\u3002", [r.id, other.id]);
1072
+ }
1073
+ }
1074
+ roofs.push({ ...r, rise, overhang, bounds: expanded, base, floorId: first.floorId });
1075
+ }
1076
+ for (const room of rooms) {
1077
+ if (!roofs.some(
1078
+ (r) => inside(r.bounds, { x: room.bounds.x0, z: room.bounds.z0 }) && inside(r.bounds, { x: room.bounds.x1, z: room.bounds.z1 }) && r.base >= room.y + room.height - EPS
1079
+ )) {
1080
+ report("ROOF_MISSING", pathOf(room.id), "\u6B64\u623F\u95F4\u4E0A\u65B9\u672A\u767B\u8BB0\u5B8C\u6574\u8986\u76D6\u7684\u5C4B\u9876\uFF0C\u4ECD\u53EF\u751F\u6210\u8BBE\u8BA1\u6846\u67B6\u3002", [room.id], {}, "warning");
1081
+ }
1082
+ }
1083
+ if (errors()) return fail();
1084
+ const floorJoints = [];
1085
+ const elevations = [...new Set(floors.map((f) => f.y))].sort((a, b) => a - b);
1086
+ for (const lower of rooms) {
1087
+ const nextY = elevations.find((y) => y > lower.y + EPS);
1088
+ if (nextY === void 0) continue;
1089
+ const y0 = round(lower.y + lower.height);
1090
+ const y1 = round(nextY - defaults.slabThickness);
1091
+ if (y1 - y0 <= EPS) continue;
1092
+ for (const upper of spaces) {
1093
+ if (!eq(upper.y, nextY) || !overlap(lower.bounds, upper.bounds)) continue;
1094
+ floorJoints.push({
1095
+ id: `floor-joint:${lower.id}:${upper.id}`,
1096
+ lowerSpaceId: lower.id,
1097
+ upperSpaceId: upper.id,
1098
+ lowerFloorId: lower.floorId,
1099
+ upperFloorId: upper.floorId,
1100
+ y0,
1101
+ y1,
1102
+ upperY: nextY,
1103
+ bounds: {
1104
+ x0: Math.max(lower.bounds.x0, upper.bounds.x0),
1105
+ x1: Math.min(lower.bounds.x1, upper.bounds.x1),
1106
+ z0: Math.max(lower.bounds.z0, upper.bounds.z0),
1107
+ z1: Math.min(lower.bounds.z1, upper.bounds.z1)
1108
+ }
1109
+ });
1110
+ }
1111
+ }
1112
+ const signs = [
1113
+ {
1114
+ id: "project-sign",
1115
+ title: sources.project.name,
1116
+ subtitle: sources.project.description || "\u9879\u76EE\u5165\u53E3",
1117
+ position: { x: entry.x, y: entry.y + 2.6, z: entry.z },
1118
+ normal: { x: 0, y: 0, z: 1 }
1119
+ }
1120
+ ];
1121
+ for (const room of rooms) {
1122
+ const door = openings.find((o) => o.kind === "door" && (o.spaceId === room.id || o.to === room.id));
1123
+ if (!door) continue;
1124
+ const normal = door.spaceId === room.id ? { x: -door.normal.x, y: 0, z: -door.normal.z } : door.normal;
1125
+ const signGap = defaults.wallThickness / 2 + SIGN_CLEARANCE;
1126
+ signs.push({
1127
+ id: `sign:${room.id}`,
1128
+ spaceId: room.id,
1129
+ floorId: room.floorId,
1130
+ title: room.name,
1131
+ subtitle: room.intro ?? room.businessId,
1132
+ position: { x: door.position.x + normal.x * signGap, y: room.y + door.height + 0.27, z: door.position.z + normal.z * signGap },
1133
+ normal
1134
+ });
1135
+ }
1136
+ const bounds = {
1137
+ ...boundsOf([...spaces.map((s) => s.bounds), ...roofs.map((r) => r.bounds), ...stairs.map((s) => s.bounds)]),
1138
+ y0: Math.min(...spaces.map((s) => s.y)) - defaults.slabThickness,
1139
+ y1: Math.max(...spaces.map((s) => s.y + s.height), ...roofs.map((r) => r.base + r.rise + 0.2))
1140
+ };
1141
+ const model = {
1142
+ format: "specdev-building-model",
1143
+ generator: BUILDING_SCHEMA,
1144
+ project: { name: sources.project.name, description: sources.project.description },
1145
+ style: data.style,
1146
+ defaults,
1147
+ construction,
1148
+ floors,
1149
+ spaces,
1150
+ rooms,
1151
+ walls,
1152
+ doors: openings.filter((o) => o.kind === "door"),
1153
+ openings,
1154
+ stairs,
1155
+ roofs,
1156
+ floorJoints,
1157
+ signs,
1158
+ entry,
1159
+ bounds,
1160
+ navigation,
1161
+ diagnostics
1162
+ };
1163
+ return { ok: true, model, diagnostics };
1164
+ }
1165
+
1166
+ // src/core/building.ts
1167
+ async function readBuildingBlueprint(repoRoot) {
1168
+ const text = await readWorktreeFileOptional(repoRoot, BUILDING_BLUEPRINT_REL);
1169
+ if (text === null) {
1170
+ throw new CoreError(
1171
+ "no-building-blueprint",
1172
+ `\u8BE5\u9879\u76EE\u8FD8\u6CA1\u6709\u5EFA\u7B51\u84DD\u56FE\uFF1A\u672A\u627E\u5230 ${BUILDING_BLUEPRINT_REL}\u3002\u4E0D\u4F1A\u81EA\u52A8\u521B\u5EFA\uFF1B\u84DD\u56FE\u63CF\u8FF0\u7A7A\u95F4\u5B89\u6392\uFF0C\u4E1A\u52A1\u4E0E\u8D44\u6599\u5185\u5BB9\u4EE5\u9879\u76EE\u6E05\u5355\u4E3A\u51C6\u3002`,
1173
+ 404
1174
+ );
1175
+ }
1176
+ let data;
1177
+ try {
1178
+ data = JSON.parse(text);
1179
+ } catch (error) {
1180
+ throw new CoreError("bad-blueprint", `${BUILDING_BLUEPRINT_REL} \u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${error.message}`, 500);
1181
+ }
1182
+ if (typeof data !== "object" || data === null || Array.isArray(data) || data.format !== BUILDING_FORMAT || data.schema !== BUILDING_SCHEMA) {
1183
+ throw new CoreError(
1184
+ "bad-blueprint",
1185
+ `${BUILDING_BLUEPRINT_REL} \u7684\u683C\u5F0F\u5934\u5FC5\u987B\u662F format=${BUILDING_FORMAT}\u3001schema=${BUILDING_SCHEMA}`,
1186
+ 500
1187
+ );
1188
+ }
1189
+ return data;
1190
+ }
1191
+ function extractSpaces(blueprint, problems) {
1192
+ const rawSpaces = blueprint.spaces;
1193
+ if (!Array.isArray(rawSpaces)) {
1194
+ problems.push({
1195
+ code: "BLUEPRINT_SHAPE",
1196
+ severity: "error",
1197
+ message: `${BUILDING_BLUEPRINT_REL} \u7F3A\u5C11 spaces \u6570\u7EC4\uFF08\u7A7A\u95F4\u5B89\u6392\u65E0\u6CD5\u6838\u5BF9\u7ED1\u5B9A\uFF09\u3002`,
1198
+ subject: { path: "/spaces", ids: [] }
1199
+ });
1200
+ return [];
1201
+ }
1202
+ const spaces = [];
1203
+ const seen = /* @__PURE__ */ new Map();
1204
+ rawSpaces.forEach((raw, index) => {
1205
+ if (typeof raw !== "object" || raw === null || typeof raw.id !== "string" || raw.id === "") {
1206
+ problems.push({
1207
+ code: "BLUEPRINT_SHAPE",
1208
+ severity: "error",
1209
+ message: `spaces/${index} \u4E0D\u662F\u5E26\u975E\u7A7A id \u7684\u5BF9\u8C61\uFF0C\u65E0\u6CD5\u53C2\u4E0E\u7ED1\u5B9A\u6838\u5BF9\u3002`,
1210
+ subject: { path: `/spaces/${index}`, ids: [] }
1211
+ });
1212
+ return;
1213
+ }
1214
+ const space = raw;
1215
+ const first = seen.get(space.id);
1216
+ if (first !== void 0) {
1217
+ problems.push({
1218
+ code: "BLUEPRINT_SHAPE",
1219
+ severity: "error",
1220
+ message: `\u7A7A\u95F4 id ${space.id} \u5728\u84DD\u56FE\u5185\u91CD\u590D\u3002`,
1221
+ subject: { path: `/spaces/${index}/id`, ids: [space.id] },
1222
+ evidence: { firstAt: `/spaces/${first}/id` }
1223
+ });
1224
+ return;
1225
+ }
1226
+ seen.set(space.id, index);
1227
+ spaces.push(space);
1228
+ });
1229
+ return spaces;
1230
+ }
1231
+ function checkBusinessBinding(inventory, blueprint) {
1232
+ const problems = [];
1233
+ const spaces = extractSpaces(blueprint, problems);
1234
+ for (const space of spaces) {
1235
+ if (space.directory !== void 0 && typeof space.directory !== "boolean") {
1236
+ problems.push({
1237
+ code: "BLUEPRINT_SHAPE",
1238
+ severity: "error",
1239
+ message: `\u7A7A\u95F4 ${space.id} \u7684 directory \u5FC5\u987B\u662F\u5E03\u5C14\u503C\uFF08\u7F3A\u7701\u89C6\u4E3A\u5F00\u542F\uFF09\uFF1B\u6536\u5230 ${JSON.stringify(space.directory)}\u3002`,
1240
+ subject: { path: `/spaces/${space.id}/directory`, ids: [space.id] },
1241
+ evidence: { got: JSON.stringify(space.directory) }
1242
+ });
1243
+ }
1244
+ }
1245
+ const businessById = new Map(inventory.businesses.map((b) => [b.id, b]));
1246
+ const rooms = [];
1247
+ const boundSpaces = /* @__PURE__ */ new Map();
1248
+ for (const space of spaces) {
1249
+ const kind = typeof space.kind === "string" ? space.kind : "";
1250
+ const hasBusiness = typeof space.businessId === "string" && space.businessId !== "";
1251
+ if (kind !== "room") {
1252
+ if (hasBusiness) {
1253
+ problems.push({
1254
+ code: "BUSINESS_NOT_ROOM",
1255
+ severity: "error",
1256
+ message: `\u7A7A\u95F4 ${space.id} \u4E0D\u662F room\uFF0C\u4E0D\u80FD\u643A\u5E26 businessId\uFF08\u7B2C\u4E00\u671F\u4E1A\u52A1\u8EAB\u4EFD\u53EA\u843D\u5728\u623F\u95F4\u4E0A\uFF09\u3002`,
1257
+ subject: { path: `/spaces/${space.id}/businessId`, ids: [space.id, space.businessId] }
1258
+ });
1259
+ }
1260
+ continue;
1261
+ }
1262
+ if (!hasBusiness) {
1263
+ problems.push({
1264
+ code: "BUSINESS_REQUIRED",
1265
+ severity: "error",
1266
+ message: `\u623F\u95F4 ${space.id} \u7F3A\u5C11 businessId\uFF1B\u7B2C\u4E00\u671F\u4E00\u4E1A\u52A1\u4E00\u623F\u95F4\uFF0C\u6BCF\u95F4\u4E1A\u52A1\u623F\u90FD\u8981\u7ED1\u5B9A\u6E05\u5355\u91CC\u7684\u4E1A\u52A1\u3002`,
1267
+ subject: { path: `/spaces/${space.id}/businessId`, ids: [space.id] }
1268
+ });
1269
+ continue;
1270
+ }
1271
+ const businessId = space.businessId;
1272
+ const business = businessById.get(businessId);
1273
+ if (!business) {
1274
+ problems.push({
1275
+ code: "BUSINESS_UNKNOWN",
1276
+ severity: "error",
1277
+ message: `\u623F\u95F4 ${space.id} \u7ED1\u5B9A\u7684\u4E1A\u52A1 ${businessId} \u4E0D\u5728\u9879\u76EE\u6E05\u5355\uFF08docs/specdev/ \u7684\u4E1A\u52A1\u76EE\u5F55\uFF09\u91CC\u3002`,
1278
+ subject: { path: `/spaces/${space.id}/businessId`, ids: [businessId, space.id] }
1279
+ });
1280
+ continue;
1281
+ }
1282
+ const firstSpace = boundSpaces.get(businessId);
1283
+ if (firstSpace !== void 0) {
1284
+ problems.push({
1285
+ code: "BUSINESS_DUPLICATE",
1286
+ severity: "error",
1287
+ message: `\u4E1A\u52A1 ${businessId} \u88AB\u623F\u95F4 ${firstSpace} \u4E0E ${space.id} \u91CD\u590D\u7ED1\u5B9A\uFF1B\u7B2C\u4E00\u671F\u4E00\u4E1A\u52A1\u4E00\u623F\u95F4\u3002`,
1288
+ subject: { path: `/spaces/${space.id}/businessId`, ids: [businessId, firstSpace, space.id] },
1289
+ evidence: { firstAt: `/spaces/${firstSpace}/businessId` }
1290
+ });
1291
+ continue;
1292
+ }
1293
+ boundSpaces.set(businessId, space.id);
1294
+ rooms.push({
1295
+ spaceId: space.id,
1296
+ spaceName: space.name,
1297
+ floorId: space.floorId,
1298
+ businessId,
1299
+ businessName: business.name,
1300
+ directory: space.directory !== false
1301
+ });
1302
+ }
1303
+ for (const business of inventory.businesses) {
1304
+ if (!boundSpaces.has(business.id)) {
1305
+ problems.push({
1306
+ code: "BUSINESS_MISSING",
1307
+ severity: "error",
1308
+ message: `\u6E05\u5355\u4E1A\u52A1 ${business.id}\uFF08${business.name}\uFF09\u5728\u84DD\u56FE\u91CC\u6CA1\u6709\u5BF9\u5E94\u623F\u95F4\uFF1B\u6362\u5E03\u5C40\u4E0D\u80FD\u5220\u51CF\u9879\u76EE\u4E1A\u52A1\u3002`,
1309
+ subject: { path: "/spaces", ids: [business.id] },
1310
+ evidence: { businessName: business.name }
1311
+ });
1312
+ }
1313
+ }
1314
+ return { problems, rooms };
1315
+ }
1316
+ function materialKey(entry) {
1317
+ return entry.kind === "document" ? `doc:${entry.businessId}:${entry.path ?? ""}` : `chart:${entry.businessId}:${entry.chartId ?? ""}`;
1318
+ }
1319
+ async function buildReadingCatalog(repoRoot, inventory, rooms) {
1320
+ const problems = [];
1321
+ const businesses = [];
1322
+ const roomByBusiness = new Map(rooms.map((room) => [room.businessId, room]));
1323
+ const reportUnreadable = (businessId, evidence, reason) => {
1324
+ problems.push({
1325
+ code: "MATERIAL_UNREADABLE",
1326
+ severity: "error",
1327
+ message: `\u4E1A\u52A1 ${businessId} \u7684\u8D44\u6599\u65E0\u6CD5\u9605\u8BFB\uFF1A${reason}`,
1328
+ subject: { path: "", ids: [businessId] },
1329
+ evidence: { ...evidence, reason }
1330
+ });
1331
+ };
1332
+ for (const business of inventory.businesses) {
1333
+ const room = roomByBusiness.get(business.id);
1334
+ if (!room) continue;
1335
+ const entries = [];
1336
+ if (business.descriptorError) {
1337
+ reportUnreadable(
1338
+ business.id,
1339
+ { businessId: business.id },
1340
+ `\u4E1A\u52A1\u8BF4\u660E\u6587\u4EF6\u6709\u95EE\u9898\uFF08${business.descriptorError}\uFF09\uFF0C\u8D44\u6599\u6E05\u5355\u4E0D\u53EF\u4FE1\uFF1B\u4E0D\u80FD\u5F53\u4F5C\u6CA1\u6709\u8D44\u6599\u7EE7\u7EED\u751F\u6210`
1341
+ );
1342
+ }
1343
+ if (room.directory) {
1344
+ for (const docPath of business.docs) {
1345
+ let reason = null;
1346
+ try {
1347
+ const text = await readWorktreeFileOptional(repoRoot, docPath);
1348
+ if (text === null) reason = `\u767B\u8BB0\u7684\u6587\u6863\u4E0D\u5B58\u5728\uFF08\u5DE5\u4F5C\u533A\uFF09\uFF1A${docPath}`;
1349
+ } catch (error) {
1350
+ reason = error instanceof CoreError ? error.message : `\u8BFB\u53D6\u6587\u6863 ${docPath} \u5931\u8D25\uFF1A${String(error)}`;
1351
+ }
1352
+ if (reason) reportUnreadable(business.id, { kind: "document", businessId: business.id, path: docPath }, reason);
1353
+ entries.push({
1354
+ kind: "document",
1355
+ businessId: business.id,
1356
+ path: docPath,
1357
+ title: docPath,
1358
+ href: `/specdev-workbench/api/doc?business=${encodeURIComponent(business.id)}&path=${encodeURIComponent(docPath)}`
1359
+ });
1360
+ }
1361
+ for (const chart of business.charts) {
1362
+ const workflowRel = `${CONVENTION_ROOT}/${business.id}/${chart.id}/workflow.json`;
1363
+ let reason = chart.descriptorError ? `\u56FE\u8BF4\u660E\u6587\u4EF6\u6709\u95EE\u9898\uFF08${chart.descriptorError}\uFF09` : chart.idConflict ? `\u56FE\u7F16\u53F7\u51B2\u7A81\uFF1A${chart.idConflict}` : null;
1364
+ if (!reason) {
1365
+ try {
1366
+ const text = await readWorktreeFileOptional(repoRoot, workflowRel);
1367
+ if (text === null) reason = "\u56FE\u76EE\u5F55\u7F3A\u5C11 workflow.json\uFF0C\u65E0\u6CD5\u9605\u8BFB";
1368
+ } catch (error) {
1369
+ reason = error instanceof CoreError ? error.message : `\u8BFB\u53D6 ${workflowRel} \u5931\u8D25\uFF1A${String(error)}`;
1370
+ }
1371
+ }
1372
+ if (reason) reportUnreadable(business.id, { kind: "workflow", businessId: business.id, chartId: chart.id }, reason);
1373
+ entries.push({
1374
+ kind: "workflow",
1375
+ businessId: business.id,
1376
+ chartId: chart.id,
1377
+ title: chart.descriptorError ? chart.id : chart.name,
1378
+ href: `/specdev-workbench/read/${encodeURIComponent(business.id)}/${encodeURIComponent(chart.id)}`
1379
+ });
1380
+ }
1381
+ }
1382
+ businesses.push({ businessId: business.id, businessName: business.name, spaceId: room.spaceId, entries });
1383
+ }
1384
+ return { problems, catalog: { mode: "room-directory", businesses } };
1385
+ }
1386
+ function checkCatalogCoverage(inventory, catalog, boundBusinessIds = new Set(catalog.businesses.map((b) => b.businessId))) {
1387
+ const problems = [];
1388
+ const covered = /* @__PURE__ */ new Set();
1389
+ for (const business of catalog.businesses) {
1390
+ for (const entry of business.entries) covered.add(materialKey(entry));
1391
+ }
1392
+ for (const business of inventory.businesses) {
1393
+ if (!boundBusinessIds.has(business.id)) continue;
1394
+ for (const docPath of business.docs) {
1395
+ const key = `doc:${business.id}:${docPath}`;
1396
+ if (!covered.has(key)) {
1397
+ problems.push({
1398
+ code: "MATERIAL_NO_ENTRY",
1399
+ severity: "error",
1400
+ message: `\u4E1A\u52A1 ${business.id} \u7684\u6587\u6863 ${docPath} \u6CA1\u6709\u4EFB\u4F55\u9605\u8BFB\u5165\u53E3\uFF1B\u6BCF\u4E2A\u4E1A\u52A1\u7684\u6587\u6863\u4E0E\u6D41\u7A0B\u56FE\u90FD\u5FC5\u987B\u53EF\u4EE5\u4ECE\u623F\u5185\u8D44\u6599\u76EE\u5F55\u8BBF\u95EE\u3002`,
1401
+ subject: { path: "/spaces", ids: [business.id] },
1402
+ evidence: { kind: "document", businessId: business.id, path: docPath }
1403
+ });
1404
+ }
1405
+ }
1406
+ for (const chart of business.charts) {
1407
+ const key = `chart:${business.id}:${chart.id}`;
1408
+ if (!covered.has(key)) {
1409
+ problems.push({
1410
+ code: "MATERIAL_NO_ENTRY",
1411
+ severity: "error",
1412
+ message: `\u4E1A\u52A1 ${business.id} \u7684\u56FE ${chart.id} \u6CA1\u6709\u4EFB\u4F55\u9605\u8BFB\u5165\u53E3\uFF1B\u6BCF\u4E2A\u4E1A\u52A1\u7684\u6587\u6863\u4E0E\u6D41\u7A0B\u56FE\u90FD\u5FC5\u987B\u53EF\u4EE5\u4ECE\u623F\u5185\u8D44\u6599\u76EE\u5F55\u8BBF\u95EE\u3002`,
1413
+ subject: { path: "/spaces", ids: [business.id] },
1414
+ evidence: { kind: "workflow", businessId: business.id, chartId: chart.id }
1415
+ });
1416
+ }
1417
+ }
1418
+ }
1419
+ return problems;
1420
+ }
1421
+ async function readBuildingBinding(repoRoot) {
1422
+ const inventory = await readInventory(repoRoot);
1423
+ const blueprint = await readBuildingBlueprint(repoRoot);
1424
+ const binding = checkBusinessBinding(inventory, blueprint);
1425
+ const built = await buildReadingCatalog(repoRoot, inventory, binding.rooms);
1426
+ const coverage = checkCatalogCoverage(inventory, built.catalog);
1427
+ const problems = [...binding.problems, ...built.problems, ...coverage];
1428
+ let model = null;
1429
+ if (problems.every((p) => p.severity !== "error")) {
1430
+ const sources = {
1431
+ project: { name: inventory.project.name, description: inventory.project.description },
1432
+ businesses: inventory.businesses.map((b) => ({ id: b.id, name: b.name, intro: b.intro }))
1433
+ };
1434
+ const generation = generateBuilding(blueprint, sources);
1435
+ problems.push(
1436
+ ...generation.diagnostics.map(
1437
+ (d) => ({
1438
+ code: d.code,
1439
+ severity: d.severity,
1440
+ message: d.message,
1441
+ subject: d.subject,
1442
+ evidence: d.evidence ?? {}
1443
+ })
1444
+ )
1445
+ );
1446
+ if (generation.ok) model = generation.model;
1447
+ }
1448
+ return {
1449
+ ok: problems.every((p) => p.severity !== "error"),
1450
+ blueprintPath: BUILDING_BLUEPRINT_REL,
1451
+ rooms: binding.rooms,
1452
+ catalog: built.catalog,
1453
+ problems,
1454
+ model
1455
+ };
1456
+ }
1457
+
1458
+ // src/core/diagnosis.ts
1459
+ var FIX_TARGET_LABELS = {
1460
+ blueprint: "\u6539\u84DD\u56FE",
1461
+ inventory: "\u6539\u6E05\u5355\u6216\u8D44\u6599",
1462
+ evidence: "\u6309\u8BC1\u636E\u5224"
1463
+ };
1464
+ var FIX_TARGETS = {
1465
+ // 输入形状(拼写、取值、必填)——蓝图自己写错了
1466
+ INPUT_TYPE: "blueprint",
1467
+ INPUT_ENUM: "blueprint",
1468
+ INPUT_PATTERN: "blueprint",
1469
+ INPUT_EMPTY: "blueprint",
1470
+ INPUT_UNKNOWN_FIELD: "blueprint",
1471
+ INPUT_REQUIRED: "blueprint",
1472
+ DEFAULT_RANGE: "blueprint",
1473
+ EMPTY_BUILDING: "blueprint",
1474
+ INPUT_LIMIT: "blueprint",
1475
+ ID_DUPLICATE: "blueprint",
1476
+ // 状态声明——不许凭空声称已实现/已验收,改法在蓝图(去掉声明或补出处)
1477
+ STATE_EVIDENCE_REQUIRED: "blueprint",
1478
+ STATE_CONTRADICTION: "blueprint",
1479
+ ACCEPTANCE_INCOMPLETE: "blueprint",
1480
+ // 楼层、空间、尺寸、排布
1481
+ FLOOR_REF: "blueprint",
1482
+ FLOOR_RANGE: "blueprint",
1483
+ SPACE_DIMENSION: "blueprint",
1484
+ SPACE_HEIGHT: "blueprint",
1485
+ SPACE_OVERLAP: "blueprint",
1486
+ FLOOR_CLEARANCE: "blueprint",
1487
+ SPACE_UNREACHABLE: "blueprint",
1488
+ ENTRY_POSITION: "blueprint",
1489
+ SHARED_WALL_CONFLICT: "blueprint",
1490
+ // 开口(门、窗)与楼梯
1491
+ OPENING_ROOM: "blueprint",
1492
+ OPENING_SIZE: "blueprint",
1493
+ DOOR_CONNECTION: "blueprint",
1494
+ WINDOW_CONNECTION: "blueprint",
1495
+ OPENING_PARTITION: "blueprint",
1496
+ OPENING_OVERLAP: "blueprint",
1497
+ STAIR_FLOORS: "blueprint",
1498
+ STAIR_ENDPOINT_KIND: "blueprint",
1499
+ STAIR_GEOMETRY: "blueprint",
1500
+ STAIR_LANDING: "blueprint",
1501
+ STAIR_SPACE_INTERSECTION: "blueprint",
1502
+ STAIR_INTERSECTION: "blueprint",
1503
+ STAIR_COMFORT: "blueprint",
1504
+ // 屋顶
1505
+ ROOF_REFS: "blueprint",
1506
+ ROOF_RECTANGLE: "blueprint",
1507
+ ROOF_SIZE: "blueprint",
1508
+ ROOF_UPPER_FLOOR: "blueprint",
1509
+ ROOF_INTERSECTION: "blueprint",
1510
+ ROOF_MISSING: "blueprint",
1511
+ // 绑定层:业务与蓝图之间的对账
1512
+ BLUEPRINT_SHAPE: "blueprint",
1513
+ BUSINESS_REQUIRED: "blueprint",
1514
+ BUSINESS_NOT_ROOM: "blueprint",
1515
+ BUSINESS_DUPLICATE: "blueprint",
1516
+ // 清单里有没有这个业务、业务该不该留,是作者的取舍,按证据判
1517
+ BUSINESS_MISSING: "evidence",
1518
+ BUSINESS_UNKNOWN: "evidence",
1519
+ // 有绑定但没入口:装配被 directory:false 关掉,或(未来)装配漂移——改蓝图
1520
+ MATERIAL_NO_ENTRY: "blueprint",
1521
+ // 资料本身读不了(文档不在/读不开、workflow.json 缺失或读不开、业务/图说明文件坏、图编号冲突)
1522
+ MATERIAL_UNREADABLE: "inventory"
1523
+ };
1524
+ var LOCATE_HINTS = {
1525
+ MATERIAL_NO_ENTRY: "\u7528 evidence.businessId \u627E\u5230\u5BF9\u5E94\u623F\u95F4\uFF0C\u68C0\u67E5\u5B83\u7684 directory \u662F\u5426\u88AB\u5173\u6389\uFF08\u5173\u6389\u5C31\u6CA1\u6709\u8D44\u6599\u5165\u53E3\uFF09\u3002",
1526
+ BUSINESS_MISSING: 'subject.ids \u91CC\u662F"\u6CA1\u6709\u623F\u95F4\u7684\u4E1A\u52A1 id"\uFF1A\u9ED8\u8BA4\u5728\u84DD\u56FE\u91CC\u7ED9\u5B83\u52A0\u4E00\u95F4\uFF1B\u8FD9\u4E2A\u4E1A\u52A1\u8BE5\u4E0D\u8BE5\u7559\u5728\u6E05\u5355\u91CC\u7531\u4F5C\u8005\u51B3\u5B9A\u3002',
1527
+ BUSINESS_UNKNOWN: "\u5BF9\u7167 subject.ids\uFF08\u4E1A\u52A1 id \u4E0E\u7A7A\u95F4 id\uFF09\u4E0E\u9879\u76EE\u6E05\u5355\uFF1A\u591A\u6570\u662F\u84DD\u56FE\u628A\u4E1A\u52A1 id \u5199\u9519\u4E86\uFF0C\u5148\u6539\u84DD\u56FE\u6307\u5411\u6E05\u5355\u91CC\u7684\u771F\u5B9E id\u3002",
1528
+ MATERIAL_UNREADABLE: "\u6309 evidence \u91CC\u7684 path\uFF0FchartId\uFF0Freason \u6838\u5BF9\u8D44\u6599\u6587\u4EF6\u672C\u8EAB\uFF1A\u8FD9\u7C7B\u6539\u84DD\u56FE\u89E3\u51B3\u4E0D\u4E86\u3002"
1529
+ };
1530
+
1531
+ // src/cli/check-building.ts
1532
+ var EXIT_OK = 0;
1533
+ var EXIT_PROBLEMS = 1;
1534
+ var EXIT_UNREADABLE = 2;
1535
+ var USAGE = "\u7528\u6CD5\uFF1Anode dist/check-building.js <\u9879\u76EE\u4ED3\u5E93\u6839> [--json]";
1536
+ var FAILURE_HINTS = {
1537
+ "no-convention-root": "\u8FD9\u91CC\u8FD8\u6CA1\u6709\u4E1A\u52A1\u6E05\u5355\uFF08docs/specdev/project.json\uFF09\uFF1A\u5148\u505A\u4E1A\u52A1\u6E05\u5355\uFF0C\u518D\u5EFA\u623F\u3002",
1538
+ "no-building-blueprint": "\u84DD\u56FE\u8FD8\u4E0D\u5B58\u5728\u2014\u2014docs/specdev/building.json \u6B63\u662F\u5EFA\u623F\u4EFB\u52A1\u8981\u5199\u7684\u6587\u4EF6\u3002",
1539
+ "bad-blueprint": "\u84DD\u56FE\u4E0D\u662F\u5408\u6CD5 JSON \u6216\u683C\u5F0F\u5934\u4E0D\u5BF9\uFF1A\u6309 SKILL \u7684\u5951\u7EA6\u8868\u91CD\u5199\u683C\u5F0F\u5934\u540E\u518D\u8DD1\u4E00\u6B21\u3002",
1540
+ "bad-inventory": "\u9879\u76EE\u6E05\u5355\uFF08project.json\uFF09\u574F\u4E86\uFF1A\u5148\u4FEE\u6E05\u5355\uFF0C\u518D\u5EFA\u623F\u3002",
1541
+ "repo-not-top-level": "\u8BF7\u5728 Git \u4ED3\u5E93\u9876\u5C42\u8FD0\u884C\u8FD9\u4E2A\u547D\u4EE4\uFF08\u5DE5\u4F5C\u533A\u91CC\u4F20\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\uFF09\u3002",
1542
+ "not-a-git-repo": "\u8FD9\u4E2A\u76EE\u5F55\u4E0D\u662F Git \u4ED3\u5E93\uFF1A\u5EFA\u7B51\u8BCA\u65AD\u8981\u8BFB\u6E05\u5355\u4E0E\u5FEB\u7167\u6807\u7B7E\uFF0C\u9700\u8981 Git \u4ED3\u5E93\u3002",
1543
+ "repo-unavailable": "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u8BFB\u4E0D\u4E86\uFF1A\u6838\u5BF9\u4F20\u5165\u7684\u4ED3\u5E93\u6839\u8DEF\u5F84\u3002",
1544
+ "git-unavailable": "\u627E\u4E0D\u5230 git \u53EF\u6267\u884C\u6587\u4EF6\uFF1A\u786E\u8BA4 git \u5DF2\u5B89\u88C5\u5E76\u5728 PATH \u4E2D\u3002"
1545
+ };
1546
+ async function runBuildingCheck(input) {
1547
+ const repo = { mode: "manual", title: "\u547D\u4EE4\u884C", path: path2.resolve(input), workspaceId: "" };
1548
+ try {
1549
+ await assertRepoUsable(repo.path);
1550
+ await assertRepoTopLevel(repo.path);
1551
+ return { kind: "checked", binding: await readBuildingBinding(repo.path), repo };
1552
+ } catch (error) {
1553
+ if (error instanceof CoreError) return { kind: "failed", code: error.code, error: error.message, repo };
1554
+ return {
1555
+ kind: "failed",
1556
+ code: "internal",
1557
+ error: error instanceof Error ? error.message : String(error),
1558
+ repo
1559
+ };
1560
+ }
1561
+ }
1562
+ function sorted2(problems) {
1563
+ return [...problems].sort((a, b) => (a.severity === "error" ? 0 : 1) - (b.severity === "error" ? 0 : 1));
1564
+ }
1565
+ function problemLines(problems) {
1566
+ const lines = [];
1567
+ for (const problem of sorted2(problems)) {
1568
+ const target = FIX_TARGETS[problem.code];
1569
+ lines.push(`[${problem.severity}] ${problem.code} ${problem.subject.path || "(\u6839)"}`);
1570
+ lines.push(` ${problem.message}`);
1571
+ if (problem.subject.ids.length) lines.push(` \u6D89\u53CA\uFF1A${problem.subject.ids.join("\u3001")}`);
1572
+ const evidence = Object.entries(problem.evidence ?? {});
1573
+ if (evidence.length) lines.push(` \u8BC1\u636E\uFF1A${JSON.stringify(Object.fromEntries(evidence))}`);
1574
+ lines.push(` \u4FEE\u590D\u65B9\u5411\uFF1A${target ? FIX_TARGET_LABELS[target] : "\u672A\u77E5\u95EE\u9898\u7801\uFF1A\u5148\u67E5\u8BCA\u65AD\u76EE\u5F55\uFF0C\u522B\u76F4\u63A5\u6539 JSON"}`);
1575
+ const locate = LOCATE_HINTS[problem.code];
1576
+ if (locate) lines.push(` \u5B9A\u4F4D\uFF1A${locate}`);
1577
+ }
1578
+ return lines;
1579
+ }
1580
+ function formatReport(outcome) {
1581
+ if (outcome.kind === "failed") {
1582
+ const lines2 = [
1583
+ `\u5EFA\u7B51\u8BCA\u65AD\uFF1A\u8BFB\u4E0D\u4E86\uFF08${outcome.code}\uFF09`,
1584
+ outcome.error,
1585
+ `\u4ED3\u5E93\uFF1A${outcome.repo?.path ?? "(\u672A\u89E3\u6790)"}`
1586
+ ];
1587
+ const hint = FAILURE_HINTS[outcome.code];
1588
+ if (hint) lines2.push(`\u63D0\u793A\uFF1A${hint}`);
1589
+ return lines2.join("\n");
1590
+ }
1591
+ const { binding, repo } = outcome;
1592
+ const errors = binding.problems.filter((p) => p.severity === "error");
1593
+ const warnings = binding.problems.filter((p) => p.severity === "warning");
1594
+ const entryCount = binding.catalog.businesses.reduce((sum, b) => sum + b.entries.length, 0);
1595
+ const lines = [`\u5EFA\u7B51\u8BCA\u65AD\uFF1A${repo.path}`];
1596
+ if (binding.model) lines.push(`\u9879\u76EE\uFF1A${binding.model.project.name}`);
1597
+ lines.push(
1598
+ `\u84DD\u56FE\uFF1A${binding.blueprintPath}`,
1599
+ `\u6E05\u5355\u4E1A\u52A1 ${binding.catalog.businesses.length} \u4E2A\uFF1B\u623F\u95F4\u7ED1\u5B9A ${binding.rooms.length} \u95F4\uFF1B\u8D44\u6599\u5165\u53E3 ${entryCount} \u6761`
1600
+ );
1601
+ if (binding.rooms.length) {
1602
+ const entriesByBusiness = new Map(binding.catalog.businesses.map((b) => [b.businessId, b.entries.length]));
1603
+ lines.push(`\u623F\u95F4\uFF08${binding.rooms.length}\uFF09`);
1604
+ for (const room of binding.rooms) {
1605
+ const directory = room.directory ? "\u76EE\u5F55\u5F00" : "\u76EE\u5F55\u5173";
1606
+ const count2 = entriesByBusiness.get(room.businessId) ?? 0;
1607
+ lines.push(` ${room.spaceId} \u2190 ${room.businessId} ${room.businessName}\uFF08${room.floorId}\uFF0C${directory}\uFF0C\u8D44\u6599 ${count2} \u6761\uFF09`);
1608
+ }
1609
+ }
1610
+ lines.push(`\u9519\u8BEF ${errors.length} \u6761\uFF0C\u8B66\u544A ${warnings.length} \u6761`);
1611
+ if (binding.problems.length) lines.push(...problemLines(binding.problems));
1612
+ if (!errors.length) {
1613
+ if (warnings.length) lines.push("\u63D0\u793A\uFF1Awarning \u4E0D\u963B\u585E\u751F\u6210\uFF1B\u4FDD\u7559\u5B83\u4EEC\u65F6\u8981\u5728\u4EA4\u4ED8\u8BF4\u660E\u91CC\u5199\u660E\u7406\u7531\u3002");
1614
+ lines.push("\u7ED3\u8BBA\uFF1A\u901A\u8FC7\uFF08\u6A21\u578B\u53EF\u751F\u6210\uFF09\u3002\u80FD\u5426\u8D70\u901A\u3001\u95E8\u724C\u70B9\u4E0D\u70B9\u5F97\u5F00\uFF0C\u4ECD\u7531\u4F5C\u8005\u5728\u9875\u9762\u9A8C\u6536\u3002");
1615
+ return lines.join("\n");
1616
+ }
1617
+ const count = (target) => errors.filter((p) => FIX_TARGETS[p.code] === target).length;
1618
+ lines.push('\u7ED3\u8BBA\uFF1A\u672A\u901A\u8FC7\uFF08\u6709 error\uFF0C\u6A21\u578B\u4E0D\u4F1A\u751F\u6210\uFF09\u3002\u6309\u6BCF\u6761\u7684"\u4FEE\u590D\u65B9\u5411"\u6539\u5B8C\u518D\u8DD1\u4E00\u6B21\u3002');
1619
+ if (count("blueprint")) lines.push(` \u6539\u84DD\u56FE ${count("blueprint")} \u6761\uFF1A\u6309 subject.path \u56DE ${binding.blueprintPath} \u6539\u3002`);
1620
+ if (count("inventory")) lines.push(` \u6539\u6E05\u5355\u6216\u8D44\u6599 ${count("inventory")} \u6761\uFF1A\u6539\u84DD\u56FE\u6539\u4E0D\u597D\u2014\u2014\u62A5\u544A\u4F5C\u8005\uFF0C\u6216\u5148\u4FEE\u6E05\u5355/\u8D44\u6599\u3002`);
1621
+ if (count("evidence")) lines.push(` \u6309\u8BC1\u636E\u5224 ${count("evidence")} \u6761\uFF1A\u5BF9\u7167 subject.ids \u4E0E\u9879\u76EE\u6E05\u5355\u6838\u5BF9\uFF1B\u9ED8\u8BA4\u5148\u6838\u67E5\u84DD\u56FE\uFF0C\u6E05\u5355\u7684\u53D6\u820D\u4EA4\u4F5C\u8005\u51B3\u5B9A\u3002`);
1622
+ return lines.join("\n");
1623
+ }
1624
+ function formatJson(outcome) {
1625
+ if (outcome.kind === "failed") {
1626
+ return JSON.stringify({ code: outcome.code, error: outcome.error, repo: outcome.repo }, null, 2);
1627
+ }
1628
+ return JSON.stringify({ ...outcome.binding, repo: outcome.repo }, null, 2);
1629
+ }
1630
+ function exitCodeOf(outcome) {
1631
+ if (outcome.kind === "failed") return EXIT_UNREADABLE;
1632
+ return outcome.binding.ok ? EXIT_OK : EXIT_PROBLEMS;
1633
+ }
1634
+ async function main(argv, io) {
1635
+ const jsonCount = argv.filter((arg) => arg === "--json").length;
1636
+ const json = jsonCount > 0;
1637
+ const paths = argv.filter((arg) => arg !== "--json");
1638
+ const unknown = paths.filter((arg) => arg.startsWith("-"));
1639
+ if (unknown.length || paths.length !== 1 || jsonCount > 1) {
1640
+ const message = `${unknown.length ? `\u672A\u77E5\u53C2\u6570\uFF1A${unknown.join("\u3001")}\u3002` : ""}${jsonCount > 1 ? "--json \u53EA\u80FD\u7ED9\u4E00\u6B21\u3002" : ""}${paths.length !== 1 ? "\u9700\u8981\u4E00\u4E2A\u9879\u76EE\u4ED3\u5E93\u6839\u53C2\u6570\u3002" : ""}`;
1641
+ io.err(`${message}
1642
+ ${USAGE}
1643
+ `);
1644
+ if (json) io.out(`${JSON.stringify({ code: "bad-request", error: message, repo: null }, null, 2)}
1645
+ `);
1646
+ return EXIT_UNREADABLE;
1647
+ }
1648
+ const outcome = await runBuildingCheck(paths[0]);
1649
+ io.out(json ? `${formatJson(outcome)}
1650
+ ` : `${formatReport(outcome)}
1651
+ `);
1652
+ return exitCodeOf(outcome);
1653
+ }
1654
+ if (process.argv[1] !== void 0 && pathToFileURL(process.argv[1]).href === import.meta.url) {
1655
+ process.exitCode = await main(process.argv.slice(2), {
1656
+ out: (text) => process.stdout.write(text),
1657
+ err: (text) => process.stderr.write(text)
1658
+ });
1659
+ }
1660
+ export {
1661
+ EXIT_OK,
1662
+ EXIT_PROBLEMS,
1663
+ EXIT_UNREADABLE,
1664
+ exitCodeOf,
1665
+ formatJson,
1666
+ formatReport,
1667
+ main,
1668
+ runBuildingCheck
1669
+ };