cortico 0.1.0

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 (311) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/package.json +43 -0
  4. package/src/boot.ts +46 -0
  5. package/src/bot.ts +1342 -0
  6. package/src/config-file.ts +41 -0
  7. package/src/core/README.md +92 -0
  8. package/src/core/billing.ts +26 -0
  9. package/src/core/blobs.ts +106 -0
  10. package/src/core/bus.ts +311 -0
  11. package/src/core/config-schema.ts +201 -0
  12. package/src/core/config.ts +262 -0
  13. package/src/core/core.ts +699 -0
  14. package/src/core/cost.ts +244 -0
  15. package/src/core/event-store.ts +304 -0
  16. package/src/core/fork.ts +113 -0
  17. package/src/core/generation.ts +118 -0
  18. package/src/core/instance-lock.ts +215 -0
  19. package/src/core/ipc-logger.ts +105 -0
  20. package/src/core/language.ts +62 -0
  21. package/src/core/log-context.ts +38 -0
  22. package/src/core/loop.ts +1858 -0
  23. package/src/core/markers.ts +48 -0
  24. package/src/core/prefix.ts +121 -0
  25. package/src/core/run.ts +95 -0
  26. package/src/core/secrets.ts +18 -0
  27. package/src/core/session.ts +56 -0
  28. package/src/core/sessions.ts +261 -0
  29. package/src/core/state.ts +104 -0
  30. package/src/core/template.ts +72 -0
  31. package/src/core/timers.ts +129 -0
  32. package/src/core/tool-log.ts +135 -0
  33. package/src/core/transcript.ts +92 -0
  34. package/src/core/truncate.ts +121 -0
  35. package/src/core/types.ts +1140 -0
  36. package/src/core/usage-log.ts +79 -0
  37. package/src/core/util.ts +386 -0
  38. package/src/deploy-listing.ts +160 -0
  39. package/src/deploy.ts +162 -0
  40. package/src/extensions/README.md +72 -0
  41. package/src/extensions/dry-mount.ts +390 -0
  42. package/src/extensions/manifest.ts +153 -0
  43. package/src/extensions/runtime.ts +42 -0
  44. package/src/extensions.ts +519 -0
  45. package/src/launcher.ts +285 -0
  46. package/src/paths.ts +155 -0
  47. package/src/protocol/open-responses/LICENSE +201 -0
  48. package/src/protocol/open-responses/README.md +14 -0
  49. package/src/protocol/open-responses/context-helpers.ts +27 -0
  50. package/src/protocol/open-responses/context-log.ts +59 -0
  51. package/src/protocol/open-responses/context.ts +89 -0
  52. package/src/protocol/open-responses/generated.ts +115 -0
  53. package/src/protocol/open-responses/index.ts +33 -0
  54. package/src/protocol/open-responses/openapi.json +4230 -0
  55. package/src/protocol/open-responses/stream.ts +146 -0
  56. package/src/protocol/open-responses/tokens.ts +31 -0
  57. package/src/providers/README.md +86 -0
  58. package/src/providers/base.ts +111 -0
  59. package/src/providers/configuration.ts +97 -0
  60. package/src/providers/console/config.ts +21 -0
  61. package/src/providers/console/settings.ts +443 -0
  62. package/src/providers/console/strings.ts +74 -0
  63. package/src/providers/console/types.ts +11 -0
  64. package/src/providers/llamacpp/archive.ts +148 -0
  65. package/src/providers/llamacpp/catalog.ts +247 -0
  66. package/src/providers/llamacpp/config.ts +36 -0
  67. package/src/providers/llamacpp/console/client.ts +6 -0
  68. package/src/providers/llamacpp/console/models-panel.ts +120 -0
  69. package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
  70. package/src/providers/llamacpp/console/server.ts +153 -0
  71. package/src/providers/llamacpp/index.ts +95 -0
  72. package/src/providers/llamacpp/native.ts +51 -0
  73. package/src/providers/llamacpp/options.ts +117 -0
  74. package/src/providers/llamacpp/runtime-store.ts +139 -0
  75. package/src/providers/llamacpp/runtime.ts +214 -0
  76. package/src/providers/llamacpp/server.ts +286 -0
  77. package/src/providers/llamacpp/strings.ts +230 -0
  78. package/src/providers/openai-responses-compat/config.ts +20 -0
  79. package/src/providers/openai-responses-compat/index.ts +89 -0
  80. package/src/providers/openai-responses-compat/native.ts +127 -0
  81. package/src/providers/openai-responses-compat/strings.ts +19 -0
  82. package/src/providers/pricebook.ts +75 -0
  83. package/src/providers/registry.ts +137 -0
  84. package/src/providers/strings.ts +71 -0
  85. package/src/providers/transport/chat.ts +156 -0
  86. package/src/providers/transport/errors.ts +48 -0
  87. package/src/providers/transport/history.ts +68 -0
  88. package/src/providers/transport/native-input.ts +83 -0
  89. package/src/providers/transport/native-types.ts +13 -0
  90. package/src/providers/transport/response-assembly.ts +191 -0
  91. package/src/providers/transport/response-http.ts +196 -0
  92. package/src/providers/transport/response-meters.ts +27 -0
  93. package/src/providers/transport/responses-input.ts +56 -0
  94. package/src/web/README.md +84 -0
  95. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
  96. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
  97. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
  98. package/src/web/client/console-pages/builtins.ts +10 -0
  99. package/src/web/client/console-pages/context.ts +135 -0
  100. package/src/web/client/console-pages/host.ts +633 -0
  101. package/src/web/client/console-pages/loader.ts +132 -0
  102. package/src/web/client/console-pages/strings.ts +90 -0
  103. package/src/web/client/console-pages/tools/strings.ts +49 -0
  104. package/src/web/client/console-pages/tools/view.ts +215 -0
  105. package/src/web/client/core/api.ts +200 -0
  106. package/src/web/client/core/language.ts +56 -0
  107. package/src/web/client/core/lifecycle.ts +132 -0
  108. package/src/web/client/core/router.ts +199 -0
  109. package/src/web/client/core/stream.ts +157 -0
  110. package/src/web/client/core/websocket.ts +82 -0
  111. package/src/web/client/features/appearance/index.ts +333 -0
  112. package/src/web/client/features/appearance/strings.ts +87 -0
  113. package/src/web/client/features/config/index.ts +11 -0
  114. package/src/web/client/features/config/strings.ts +41 -0
  115. package/src/web/client/features/config/view.ts +428 -0
  116. package/src/web/client/features/core/events.ts +197 -0
  117. package/src/web/client/features/core/index.ts +254 -0
  118. package/src/web/client/features/core/run.ts +56 -0
  119. package/src/web/client/features/core/runlog.ts +214 -0
  120. package/src/web/client/features/core/sessions.ts +53 -0
  121. package/src/web/client/features/core/strings.ts +157 -0
  122. package/src/web/client/features/extensions/index.ts +422 -0
  123. package/src/web/client/features/extensions/strings.ts +164 -0
  124. package/src/web/client/features/feature.ts +93 -0
  125. package/src/web/client/features/live/context.ts +280 -0
  126. package/src/web/client/features/live/fork.ts +156 -0
  127. package/src/web/client/features/live/index.ts +392 -0
  128. package/src/web/client/features/live/onboarding.ts +113 -0
  129. package/src/web/client/features/live/protocol.ts +122 -0
  130. package/src/web/client/features/live/sessions.ts +71 -0
  131. package/src/web/client/features/live/status.ts +68 -0
  132. package/src/web/client/features/live/strings.ts +252 -0
  133. package/src/web/client/features/live/timeline.ts +590 -0
  134. package/src/web/client/features/prompts/editor.ts +292 -0
  135. package/src/web/client/features/prompts/index.ts +346 -0
  136. package/src/web/client/features/prompts/strings.ts +136 -0
  137. package/src/web/client/features/prompts/view.ts +256 -0
  138. package/src/web/client/features/providers/index.ts +100 -0
  139. package/src/web/client/features/providers/strings.ts +23 -0
  140. package/src/web/client/features/settings/general.ts +28 -0
  141. package/src/web/client/features/settings/index.ts +82 -0
  142. package/src/web/client/features/settings/strings.ts +33 -0
  143. package/src/web/client/features/storage/index.ts +1 -0
  144. package/src/web/client/features/storage/strings.ts +45 -0
  145. package/src/web/client/features/storage/view.ts +154 -0
  146. package/src/web/client/features/usage/chart.ts +486 -0
  147. package/src/web/client/features/usage/index.ts +407 -0
  148. package/src/web/client/features/usage/labels.ts +46 -0
  149. package/src/web/client/features/usage/range.ts +36 -0
  150. package/src/web/client/features/usage/state.ts +37 -0
  151. package/src/web/client/features/usage/strings.ts +191 -0
  152. package/src/web/client/features/usage/tooltip.ts +86 -0
  153. package/src/web/client/features/usage/types.ts +96 -0
  154. package/src/web/client/features/worlds/index.ts +424 -0
  155. package/src/web/client/features/worlds/strings.ts +121 -0
  156. package/src/web/client/main.ts +273 -0
  157. package/src/web/client/shell/avatar.ts +233 -0
  158. package/src/web/client/shell/index.ts +549 -0
  159. package/src/web/client/shell/strings.ts +89 -0
  160. package/src/web/client/theme/handoff.ts +43 -0
  161. package/src/web/client/theme/palette.ts +109 -0
  162. package/src/web/client/theme/registry.ts +130 -0
  163. package/src/web/client/theme/storage.ts +72 -0
  164. package/src/web/client/theme/strings.ts +123 -0
  165. package/src/web/client/theme/studio.ts +366 -0
  166. package/src/web/client/ui/actions.ts +109 -0
  167. package/src/web/client/ui/data.ts +185 -0
  168. package/src/web/client/ui/dom.ts +23 -0
  169. package/src/web/client/ui/fields.ts +191 -0
  170. package/src/web/client/ui/format.ts +75 -0
  171. package/src/web/client/ui/icons.ts +183 -0
  172. package/src/web/client/ui/images.ts +91 -0
  173. package/src/web/client/ui/index.ts +89 -0
  174. package/src/web/client/ui/lamp.ts +125 -0
  175. package/src/web/client/ui/log.ts +98 -0
  176. package/src/web/client/ui/overlay.ts +184 -0
  177. package/src/web/client/ui/page.ts +8 -0
  178. package/src/web/client/ui/prompt-input.tsx +311 -0
  179. package/src/web/client/ui/sheet.ts +131 -0
  180. package/src/web/client/ui/strings.ts +71 -0
  181. package/src/web/console-pages.ts +367 -0
  182. package/src/web/files.ts +107 -0
  183. package/src/web/path-picker.ts +296 -0
  184. package/src/web/public/index.html +17 -0
  185. package/src/web/public/styles.css +1715 -0
  186. package/src/web/server.ts +1883 -0
  187. package/src/web/shared/client-panel.ts +603 -0
  188. package/src/web/shared/console-protocol.ts +560 -0
  189. package/src/web/shared/css.d.ts +1 -0
  190. package/src/web/shared/path-picker.ts +23 -0
  191. package/src/web/shared/theme.ts +256 -0
  192. package/src/web/theme-store.ts +37 -0
  193. package/src/world.ts +449 -0
  194. package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
  195. package/src/worlds/bilibili/README.md +176 -0
  196. package/src/worlds/bilibili/audience-admission.ts +995 -0
  197. package/src/worlds/bilibili/client.ts +528 -0
  198. package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
  199. package/src/worlds/bilibili/config.ts +185 -0
  200. package/src/worlds/bilibili/console/client.ts +202 -0
  201. package/src/worlds/bilibili/definition.ts +35 -0
  202. package/src/worlds/bilibili/gift-frame.ts +131 -0
  203. package/src/worlds/bilibili/normalize.ts +452 -0
  204. package/src/worlds/bilibili/overlay/announcement.ts +76 -0
  205. package/src/worlds/bilibili/overlay/assets.ts +82 -0
  206. package/src/worlds/bilibili/overlay/model.ts +494 -0
  207. package/src/worlds/bilibili/overlay/project.ts +202 -0
  208. package/src/worlds/bilibili/overlay/server.ts +286 -0
  209. package/src/worlds/bilibili/overlay/types.ts +217 -0
  210. package/src/worlds/bilibili/overlay/web/app.js +690 -0
  211. package/src/worlds/bilibili/overlay/web/editor.css +297 -0
  212. package/src/worlds/bilibili/overlay/web/editor.html +102 -0
  213. package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
  214. package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
  215. package/src/worlds/bilibili/overlay/web/styles.css +258 -0
  216. package/src/worlds/bilibili/protobuf.ts +123 -0
  217. package/src/worlds/bilibili/wire.ts +94 -0
  218. package/src/worlds/bilibili/world.ts +1389 -0
  219. package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
  220. package/src/worlds/console-fixture/console/client.ts +72 -0
  221. package/src/worlds/console-fixture/world.ts +73 -0
  222. package/src/worlds/index.ts +15 -0
  223. package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
  224. package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
  225. package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
  226. package/src/worlds/minecraft/README.md +917 -0
  227. package/src/worlds/minecraft/blueprint-plan.ts +915 -0
  228. package/src/worlds/minecraft/blueprint-registry.ts +566 -0
  229. package/src/worlds/minecraft/blueprint-repair.ts +236 -0
  230. package/src/worlds/minecraft/blueprint-resource.ts +272 -0
  231. package/src/worlds/minecraft/blueprint.ts +492 -0
  232. package/src/worlds/minecraft/body-lease.ts +340 -0
  233. package/src/worlds/minecraft/bridge.ts +749 -0
  234. package/src/worlds/minecraft/check.ts +651 -0
  235. package/src/worlds/minecraft/chests.ts +292 -0
  236. package/src/worlds/minecraft/client-launch.ts +272 -0
  237. package/src/worlds/minecraft/client-options.ts +85 -0
  238. package/src/worlds/minecraft/client-skins.ts +164 -0
  239. package/src/worlds/minecraft/client-window.ps1 +91 -0
  240. package/src/worlds/minecraft/client.ts +423 -0
  241. package/src/worlds/minecraft/combat-context.ts +109 -0
  242. package/src/worlds/minecraft/combat.ts +1552 -0
  243. package/src/worlds/minecraft/config.ts +499 -0
  244. package/src/worlds/minecraft/console/access.ts +218 -0
  245. package/src/worlds/minecraft/console/client.ts +244 -0
  246. package/src/worlds/minecraft/console/log.ts +110 -0
  247. package/src/worlds/minecraft/console/mount.ts +420 -0
  248. package/src/worlds/minecraft/console/skin.ts +257 -0
  249. package/src/worlds/minecraft/console/style.css +154 -0
  250. package/src/worlds/minecraft/console/world.ts +449 -0
  251. package/src/worlds/minecraft/deaths.ts +135 -0
  252. package/src/worlds/minecraft/definition.ts +18 -0
  253. package/src/worlds/minecraft/engine-child.ts +274 -0
  254. package/src/worlds/minecraft/engine-ipc.ts +98 -0
  255. package/src/worlds/minecraft/entity-facts.ts +132 -0
  256. package/src/worlds/minecraft/escape.ts +407 -0
  257. package/src/worlds/minecraft/executor.ts +14494 -0
  258. package/src/worlds/minecraft/explored.ts +156 -0
  259. package/src/worlds/minecraft/geometry.ts +205 -0
  260. package/src/worlds/minecraft/goal-plan.ts +647 -0
  261. package/src/worlds/minecraft/item-break.ts +126 -0
  262. package/src/worlds/minecraft/item-facts.ts +146 -0
  263. package/src/worlds/minecraft/item-pick.ts +67 -0
  264. package/src/worlds/minecraft/level-dat.ts +176 -0
  265. package/src/worlds/minecraft/log.ts +132 -0
  266. package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
  267. package/src/worlds/minecraft/names.ts +377 -0
  268. package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
  269. package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
  270. package/src/worlds/minecraft/piglin.ts +49 -0
  271. package/src/worlds/minecraft/policy.ts +547 -0
  272. package/src/worlds/minecraft/precheck.ts +655 -0
  273. package/src/worlds/minecraft/proxy.ts +445 -0
  274. package/src/worlds/minecraft/ranged.ts +635 -0
  275. package/src/worlds/minecraft/readouts.ts +102 -0
  276. package/src/worlds/minecraft/round.ts +26 -0
  277. package/src/worlds/minecraft/search-observation.ts +92 -0
  278. package/src/worlds/minecraft/server-config.ts +467 -0
  279. package/src/worlds/minecraft/server.ts +658 -0
  280. package/src/worlds/minecraft/show.ts +84 -0
  281. package/src/worlds/minecraft/skills.ts +1763 -0
  282. package/src/worlds/minecraft/terrain.ts +1735 -0
  283. package/src/worlds/minecraft/window.ts +96 -0
  284. package/src/worlds/minecraft/works.ts +179 -0
  285. package/src/worlds/minecraft/world.ts +6438 -0
  286. package/src/worlds/qq/ENV_PROMPT.md +5 -0
  287. package/src/worlds/qq/config.ts +108 -0
  288. package/src/worlds/qq/console/client.ts +126 -0
  289. package/src/worlds/qq/console/events.ts +155 -0
  290. package/src/worlds/qq/console/gate.ts +232 -0
  291. package/src/worlds/qq/console/roster.ts +203 -0
  292. package/src/worlds/qq/console/style.css +65 -0
  293. package/src/worlds/qq/conversation.ts +28 -0
  294. package/src/worlds/qq/definition.ts +62 -0
  295. package/src/worlds/qq/driver.ts +375 -0
  296. package/src/worlds/qq/history-tools.ts +209 -0
  297. package/src/worlds/qq/normalize.ts +323 -0
  298. package/src/worlds/qq/qface-map.ts +282 -0
  299. package/src/worlds/qq/vision-prompt.ts +27 -0
  300. package/src/worlds/qq/vision.ts +642 -0
  301. package/src/worlds/qq/vlm.ts +142 -0
  302. package/src/worlds/qq/world.ts +1503 -0
  303. package/src/worlds/terminal/ENV_PROMPT.md +11 -0
  304. package/src/worlds/terminal/config.ts +9 -0
  305. package/src/worlds/terminal/definition.ts +14 -0
  306. package/src/worlds/terminal/world.ts +700 -0
  307. package/src/worlds/websearch/ENV_PROMPT.md +1 -0
  308. package/src/worlds/websearch/brave-client.ts +261 -0
  309. package/src/worlds/websearch/config.ts +75 -0
  310. package/src/worlds/websearch/definition.ts +20 -0
  311. package/src/worlds/websearch/world.ts +171 -0
@@ -0,0 +1,651 @@
1
+ /**
2
+ * 通过 CheckWorld 读取当前状态并核验断言。
3
+ * 方块按 ID 比较;未加载单独报告;sealed 将水和岩浆视为通路。
4
+ */
5
+
6
+ import { blueprintStepStateMatches, diffBlueprint, type BlueprintPlan } from './blueprint-plan.ts';
7
+ import { blockIdOf, isAirState, type NormalizedBlueprint, type PositionXYZ } from './blueprint.ts';
8
+ import type { ItemEnchant } from './item-facts.ts';
9
+ import { roman, zhDimension, zhEnchant, zhName } from './names.ts';
10
+
11
+ export const CHECK_MAX_ASSERTS = 16;
12
+ export const CHECK_BOX_CELL_CAP = 4096;
13
+ export const CHECK_SEALED_VOLUME_CAP = 32 * 32 * 32;
14
+ const CHECK_SAMPLE_CAP = 6;
15
+ const CHECK_LEAK_SAMPLE_CAP = 3;
16
+
17
+ const AIR_IDS = new Set(['air', 'cave_air', 'void_air']);
18
+
19
+ const LIQUID_IDS = new Set(['water', 'lava', 'bubble_column']);
20
+
21
+ function isLiquidId(id: string): boolean {
22
+ return LIQUID_IDS.has(id);
23
+ }
24
+
25
+ // ── 世界取用面 ────────────────────────────────────────────────────────────────
26
+
27
+ /** 一格现状。`state` 带属性(蓝图对账要它),`solid` 是碰撞箱占满整格 */
28
+ export interface CheckCell {
29
+ state: string;
30
+ solid: boolean;
31
+ }
32
+
33
+ /** 一份蓝图在这个世界的施工面;没装载给 null,装载了没开工给 anchor: null */
34
+ export interface CheckSite {
35
+ key: string;
36
+ name: string | null;
37
+ blueprint: NormalizedBlueprint;
38
+ plan: BlueprintPlan;
39
+ anchor: PositionXYZ | null;
40
+ }
41
+
42
+ export interface CheckMarkProbe {
43
+ name: string;
44
+ kind: string;
45
+ dimension: string;
46
+ pos: PositionXYZ;
47
+ verdict: 'ok' | 'mismatch' | 'unloaded' | 'other-dimension' | 'unchecked';
48
+ found: string | null;
49
+ }
50
+
51
+ export interface CheckWorld {
52
+ /** 世界坐标一格;区块没加载、读不到 → null */
53
+ cell(x: number, y: number, z: number): CheckCell | null;
54
+ /** 背包现有:物品名 → 个数 */
55
+ inventory(): ReadonlyMap<string, number>;
56
+ /** 每个同名物品栈的附魔;未提供读取接口时,enchant 断言报告不可读取。 */
57
+ enchantsOf?: (item: string) => ItemEnchant[][];
58
+ /** 方块名的注册表校验;无此接口时跳过名字校验。 */
59
+ knowsBlock?: (id: string) => boolean;
60
+ site(key: string): CheckSite | null;
61
+ mark(name: string): CheckMarkProbe | null;
62
+ }
63
+
64
+ // ── 断言 ──────────────────────────────────────────────────────────────────────
65
+
66
+ type CheckCompare = { op: '=' | '>=' | '<='; n: number };
67
+
68
+ type CheckAssert =
69
+ | { kind: 'at'; at: PositionXYZ; is: string }
70
+ | { kind: 'count'; box: CheckBox; want: Array<{ id: string } & CheckCompare> }
71
+ | { kind: 'all'; box: CheckBox; is: string }
72
+ | { kind: 'air'; box: CheckBox }
73
+ | { kind: 'sealed'; box: CheckBox; from: PositionXYZ | null }
74
+ | { kind: 'inv'; want: Array<{ item: string; enchant?: string } & CheckCompare> }
75
+ | { kind: 'blueprint'; key: string }
76
+ | { kind: 'mark'; name: string };
77
+
78
+ interface CheckBox {
79
+ min: PositionXYZ;
80
+ max: PositionXYZ;
81
+ }
82
+
83
+ export type CheckParsed = { ok: true; assert: CheckAssert } | { ok: false; error: string };
84
+
85
+ function posText(p: readonly [number, number, number]): string {
86
+ return `(${p[0]}, ${p[1]}, ${p[2]})`;
87
+ }
88
+
89
+ function boxText(box: CheckBox): string {
90
+ return `${posText(box.min)}–${posText(box.max)}`;
91
+ }
92
+
93
+ function boxCells(box: CheckBox): number {
94
+ return (box.max[0] - box.min[0] + 1) * (box.max[1] - box.min[1] + 1) * (box.max[2] - box.min[2] + 1);
95
+ }
96
+
97
+ function idOf(state: string): string {
98
+ return blockIdOf(state).replace(/^minecraft:/, '');
99
+ }
100
+
101
+ function parseBlockId(value: unknown, path: string): string | { error: string } {
102
+ if (typeof value !== 'string' || value.trim() === '') return { error: `${path} 要一个方块名` };
103
+ return value.trim().toLowerCase().replace(/^minecraft:/, '');
104
+ }
105
+
106
+ function parsePos(value: unknown, path: string): PositionXYZ | { error: string } {
107
+ if (!Array.isArray(value) || value.length !== 3) return { error: `${path} 要 [x, y, z] 三个整数` };
108
+ const out = value.map((v) => (typeof v === 'number' && Number.isFinite(v) ? Math.floor(v) : null));
109
+ if (out.some((v) => v === null)) return { error: `${path} 要 [x, y, z] 三个整数` };
110
+ return out as PositionXYZ;
111
+ }
112
+
113
+ function parseBox(value: unknown): CheckBox | { error: string } {
114
+ if (!Array.isArray(value) || value.length !== 2) {
115
+ return { error: 'box 要 [[x1,y1,z1],[x2,y2,z2]] 两个角' };
116
+ }
117
+ const a = parsePos(value[0], 'box 的第一个角');
118
+ if ('error' in a) return a;
119
+ const b = parsePos(value[1], 'box 的第二个角');
120
+ if ('error' in b) return b;
121
+ return {
122
+ min: [Math.min(a[0], b[0]), Math.min(a[1], b[1]), Math.min(a[2], b[2])],
123
+ max: [Math.max(a[0], b[0]), Math.max(a[1], b[1]), Math.max(a[2], b[2])],
124
+ };
125
+ }
126
+
127
+ function parseCompare(value: unknown, path: string): CheckCompare | { error: string } {
128
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0) return { op: '=', n: value };
129
+ if (typeof value === 'string') {
130
+ const m = /^\s*(>=|<=|=)?\s*(\d+)\s*$/.exec(value);
131
+ if (m) return { op: (m[1] === '=' || m[1] === undefined ? '=' : m[1]) as CheckCompare['op'], n: Number(m[2]) };
132
+ }
133
+ return { error: `${path} 要一个非负整数,或者 ">=8" / "<=2" 这样的界` };
134
+ }
135
+
136
+ function compareText(c: CheckCompare): string {
137
+ return c.op === '=' ? `${c.n}` : `${c.op === '>=' ? '≥' : '≤'}${c.n}`;
138
+ }
139
+
140
+ function compareHolds(c: CheckCompare, actual: number): boolean {
141
+ return c.op === '=' ? actual === c.n : c.op === '>=' ? actual >= c.n : actual <= c.n;
142
+ }
143
+
144
+ function parseWantMap(
145
+ raw: unknown,
146
+ field: string,
147
+ ): Array<{ key: string } & CheckCompare> | { error: string } {
148
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
149
+ return { error: `${field} 要一个 {"名字": 个数} 对象` };
150
+ }
151
+ const entries = Object.entries(raw as Record<string, unknown>);
152
+ if (entries.length === 0) return { error: `${field} 是空的,没有东西可对` };
153
+ const out: Array<{ key: string } & CheckCompare> = [];
154
+ for (const [name, value] of entries) {
155
+ const key = name.trim().toLowerCase().replace(/^minecraft:/, '');
156
+ if (key === '') return { error: `${field} 里有一个空名字` };
157
+ const cmp = parseCompare(value, `${field}["${name}"]`);
158
+ if ('error' in cmp) return cmp;
159
+ out.push({ key, ...cmp });
160
+ }
161
+ return out;
162
+ }
163
+
164
+ /** inv 接受数量或附魔条件;附魔条件未指定 count 时要求至少一件。 */
165
+ function parseInvWant(
166
+ raw: unknown,
167
+ ): Array<{ item: string; enchant?: string } & CheckCompare> | { error: string } {
168
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
169
+ return { error: 'inv 要一个 {"名字": 个数} 对象' };
170
+ }
171
+ const out: Array<{ item: string; enchant?: string } & CheckCompare> = [];
172
+ const entries = Object.entries(raw as Record<string, unknown>);
173
+ if (entries.length === 0) return { error: 'inv 是空的,没有东西可对' };
174
+ for (const [name, value] of entries) {
175
+ const item = name.trim().toLowerCase().replace(/^minecraft:/, '');
176
+ if (item === '') return { error: 'inv 里有一个空名字' };
177
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
178
+ const body = value as Record<string, unknown>;
179
+ const enchant = typeof body.enchant === 'string' ? body.enchant.trim().toLowerCase() : '';
180
+ if (enchant === '') return { error: `inv["${name}"] 那个对象要一个 enchant 附魔名` };
181
+ const cmp = body.count === undefined
182
+ ? { op: '>=' as const, n: 1 }
183
+ : parseCompare(body.count, `inv["${name}"].count`);
184
+ if ('error' in cmp) return cmp;
185
+ out.push({ item, enchant: enchant.replace(/^minecraft:/, ''), ...cmp });
186
+ continue;
187
+ }
188
+ const cmp = parseCompare(value, `inv["${name}"]`);
189
+ if ('error' in cmp) return cmp;
190
+ out.push({ item, ...cmp });
191
+ }
192
+ return out;
193
+ }
194
+
195
+ export function parseAssert(raw: unknown, knowsBlock?: (id: string) => boolean): CheckParsed {
196
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
197
+ return { ok: false, error: '一条断言要一个对象' };
198
+ }
199
+ const body = raw as Record<string, unknown>;
200
+ const unknownName = (id: string): string | null =>
201
+ knowsBlock && !knowsBlock(id) && !AIR_IDS.has(id) ? `这一版里没有叫「${id}」的方块` : null;
202
+
203
+ if (body.at !== undefined) {
204
+ const at = parsePos(body.at, 'at');
205
+ if ('error' in at) return { ok: false, error: at.error };
206
+ const is = parseBlockId(body.is, 'is');
207
+ if (typeof is === 'object') return { ok: false, error: is.error };
208
+ const bad = unknownName(is);
209
+ if (bad) return { ok: false, error: bad };
210
+ return { ok: true, assert: { kind: 'at', at, is } };
211
+ }
212
+
213
+ if (body.inv !== undefined) {
214
+ const want = parseInvWant(body.inv);
215
+ if ('error' in want) return { ok: false, error: want.error };
216
+ return { ok: true, assert: { kind: 'inv', want } };
217
+ }
218
+
219
+ if (body.blueprint !== undefined) {
220
+ if (typeof body.blueprint !== 'string' || body.blueprint.trim() === '') {
221
+ return { ok: false, error: 'blueprint 要一个蓝图键' };
222
+ }
223
+ return { ok: true, assert: { kind: 'blueprint', key: body.blueprint.trim() } };
224
+ }
225
+
226
+ if (body.mark !== undefined) {
227
+ if (typeof body.mark !== 'string' || body.mark.trim() === '') {
228
+ return { ok: false, error: 'mark 要一个路标名' };
229
+ }
230
+ return { ok: true, assert: { kind: 'mark', name: body.mark.trim() } };
231
+ }
232
+
233
+ if (body.box === undefined) {
234
+ return { ok: false, error: '认不出这条断言:要 at / box / inv / blueprint / mark 之一' };
235
+ }
236
+ const box = parseBox(body.box);
237
+ if ('error' in box) return { ok: false, error: box.error };
238
+
239
+ if (body.sealed !== undefined) {
240
+ if (body.sealed !== true) return { ok: false, error: 'sealed 只收 true' };
241
+ if (boxCells(box) > CHECK_SEALED_VOLUME_CAP) {
242
+ return {
243
+ ok: false,
244
+ error: `这个盒子 ${boxCells(box)} 格,超过一次能洪泛的 ${CHECK_SEALED_VOLUME_CAP} 格,分块再对`,
245
+ };
246
+ }
247
+ let from: PositionXYZ | null = null;
248
+ if (body.from !== undefined) {
249
+ const parsed = parsePos(body.from, 'from');
250
+ if ('error' in parsed) return { ok: false, error: parsed.error };
251
+ const inside = parsed.every((v, i) => v >= box.min[i] && v <= box.max[i]);
252
+ if (!inside) return { ok: false, error: `from ${posText(parsed)} 不在 ${boxText(box)} 里面` };
253
+ from = parsed;
254
+ }
255
+ return { ok: true, assert: { kind: 'sealed', box, from } };
256
+ }
257
+
258
+ if (boxCells(box) > CHECK_BOX_CELL_CAP) {
259
+ return {
260
+ ok: false,
261
+ error: `这个区域 ${boxCells(box)} 格,超过一次能对的 ${CHECK_BOX_CELL_CAP} 格,分块再对`,
262
+ };
263
+ }
264
+
265
+ if (body.count !== undefined) {
266
+ const want = parseWantMap(body.count, 'count');
267
+ if ('error' in want) return { ok: false, error: want.error };
268
+ for (const { key } of want) {
269
+ const bad = unknownName(key);
270
+ if (bad) return { ok: false, error: bad };
271
+ }
272
+ return { ok: true, assert: { kind: 'count', box, want: want.map(({ key, ...c }) => ({ id: key, ...c })) } };
273
+ }
274
+ if (body.all !== undefined) {
275
+ const is = parseBlockId(body.all, 'all');
276
+ if (typeof is === 'object') return { ok: false, error: is.error };
277
+ const bad = unknownName(is);
278
+ if (bad) return { ok: false, error: bad };
279
+ return { ok: true, assert: { kind: 'all', box, is } };
280
+ }
281
+ if (body.air !== undefined) {
282
+ if (body.air !== true) return { ok: false, error: 'air 只收 true' };
283
+ return { ok: true, assert: { kind: 'air', box } };
284
+ }
285
+ return { ok: false, error: '给了 box 但没说要对什么:count / all / air / sealed 选一个' };
286
+ }
287
+
288
+ /** checks 格式错误拒绝整单;单条格式错误不影响其余断言。 */
289
+ export function parseChecks(
290
+ args: Record<string, unknown>,
291
+ knowsBlock?: (id: string) => boolean,
292
+ ): CheckParsed[] | { error: string } {
293
+ const raw = args.checks;
294
+ if (!Array.isArray(raw)) return { error: 'checks 要一个断言数组' };
295
+ if (raw.length === 0) return { error: 'checks 是空的,没有东西可对' };
296
+ if (raw.length > CHECK_MAX_ASSERTS) {
297
+ return { error: `一次最多 ${CHECK_MAX_ASSERTS} 条断言,这次给了 ${raw.length} 条;分几次对` };
298
+ }
299
+ return raw.map((item) => parseAssert(item, knowsBlock));
300
+ }
301
+
302
+ // ── 求值 ──────────────────────────────────────────────────────────────────────
303
+
304
+ /** unknown 表示读数不可用,不计入符合或不符。 */
305
+ export type CheckVerdict = 'ok' | 'bad' | 'unknown' | 'error';
306
+
307
+ export interface CheckResult {
308
+ verdict: CheckVerdict;
309
+ text: string;
310
+ }
311
+
312
+ function forEachBoxCell(box: CheckBox, fn: (x: number, y: number, z: number) => void): void {
313
+ for (let y = box.min[1]; y <= box.max[1]; y++) {
314
+ for (let z = box.min[2]; z <= box.max[2]; z++) {
315
+ for (let x = box.min[0]; x <= box.max[0]; x++) fn(x, y, z);
316
+ }
317
+ }
318
+ }
319
+
320
+ function unloadedTail(n: number): string {
321
+ return n > 0 ? `;另有 ${n} 格没加载,没对全` : '';
322
+ }
323
+
324
+ function evalAt(a: Extract<CheckAssert, { kind: 'at' }>, world: CheckWorld): CheckResult {
325
+ const cell = world.cell(a.at[0], a.at[1], a.at[2]);
326
+ if (!cell) return { verdict: 'unknown', text: `${posText(a.at)} 区块没加载,走近再对` };
327
+ const actual = idOf(cell.state);
328
+ const hit = AIR_IDS.has(a.is) ? AIR_IDS.has(actual) : actual === a.is;
329
+ if (hit) return { verdict: 'ok', text: `${posText(a.at)} 是${zhName(a.is)}` };
330
+ return { verdict: 'bad', text: `${posText(a.at)} 该是${zhName(a.is)},现在是${zhName(actual)}` };
331
+ }
332
+
333
+ function evalCount(a: Extract<CheckAssert, { kind: 'count' }>, world: CheckWorld): CheckResult {
334
+ const tally = new Map<string, number>();
335
+ let unloaded = 0;
336
+ forEachBoxCell(a.box, (x, y, z) => {
337
+ const cell = world.cell(x, y, z);
338
+ if (!cell) { unloaded++; return; }
339
+ const id = idOf(cell.state);
340
+ tally.set(id, (tally.get(id) ?? 0) + 1);
341
+ });
342
+ const bad: string[] = [];
343
+ const okBits: string[] = [];
344
+ for (const want of a.want) {
345
+ const actual = AIR_IDS.has(want.id)
346
+ ? [...tally].filter(([id]) => AIR_IDS.has(id)).reduce((n, [, c]) => n + c, 0)
347
+ : tally.get(want.id) ?? 0;
348
+ const line = `${zhName(want.id)} ${actual}(要 ${compareText(want)})`;
349
+ if (compareHolds(want, actual)) okBits.push(line);
350
+ else bad.push(line);
351
+ }
352
+ const where = boxText(a.box);
353
+ if (bad.length > 0) {
354
+ return { verdict: 'bad', text: `${where} 里 ${bad.join('、')}${unloadedTail(unloaded)}` };
355
+ }
356
+ if (unloaded > 0) {
357
+ return { verdict: 'unknown', text: `${where} 里 ${okBits.join('、')},但有 ${unloaded} 格没加载,数不准` };
358
+ }
359
+ return { verdict: 'ok', text: `${where} 里 ${okBits.join('、')}` };
360
+ }
361
+
362
+ function evalUniform(
363
+ a: Extract<CheckAssert, { kind: 'all' | 'air' }>,
364
+ world: CheckWorld,
365
+ ): CheckResult {
366
+ const wantAir = a.kind === 'air';
367
+ const wantId = a.kind === 'air' ? 'air' : a.is;
368
+ const offenders: string[] = [];
369
+ let unloaded = 0;
370
+ let bad = 0;
371
+ let total = 0;
372
+ forEachBoxCell(a.box, (x, y, z) => {
373
+ total++;
374
+ const cell = world.cell(x, y, z);
375
+ if (!cell) { unloaded++; return; }
376
+ const id = idOf(cell.state);
377
+ if (wantAir ? AIR_IDS.has(id) : id === wantId) return;
378
+ bad++;
379
+ if (offenders.length < CHECK_SAMPLE_CAP) offenders.push(`${posText([x, y, z])} 是${zhName(id)}`);
380
+ });
381
+ const what = wantAir ? '空的' : zhName(wantId);
382
+ if (bad > 0) {
383
+ const more = bad > offenders.length ? `,还有 ${bad - offenders.length} 格` : '';
384
+ return {
385
+ verdict: 'bad',
386
+ text: `${boxText(a.box)} ${total} 格里 ${bad} 格不是${what}:${offenders.join('、')}${more}`
387
+ + unloadedTail(unloaded),
388
+ };
389
+ }
390
+ if (unloaded > 0) {
391
+ return {
392
+ verdict: 'unknown',
393
+ text: `${boxText(a.box)} 读到的 ${total - unloaded} 格都是${what},另有 ${unloaded} 格没加载,没对全`,
394
+ };
395
+ }
396
+ return { verdict: 'ok', text: `${boxText(a.box)} ${total} 格都是${what}` };
397
+ }
398
+
399
+ /**
400
+ * 从盒内起点搜索可通行格;与盒外相连时报告盒内漏口。
401
+ * 液体视为通路,未加载的格单独计数。
402
+ */
403
+ function evalSealed(a: Extract<CheckAssert, { kind: 'sealed' }>, world: CheckWorld): CheckResult {
404
+ const { box } = a;
405
+ const inside = (x: number, y: number, z: number): boolean =>
406
+ x >= box.min[0] && x <= box.max[0] && y >= box.min[1] && y <= box.max[1]
407
+ && z >= box.min[2] && z <= box.max[2];
408
+ const passable = (x: number, y: number, z: number): boolean | null => {
409
+ const cell = world.cell(x, y, z);
410
+ if (!cell) return null;
411
+ return !cell.solid;
412
+ };
413
+
414
+ const start = a.from ?? findStart(box, world);
415
+ if (!start) {
416
+ return { verdict: 'error', text: `${boxText(a.box)} 里没有可站的空格(整个盒子是实心的,或者区块没加载)` };
417
+ }
418
+ if (passable(start[0], start[1], start[2]) === null) {
419
+ return { verdict: 'unknown', text: `${posText(start)} 区块没加载,洪泛起不了步` };
420
+ }
421
+ if (passable(start[0], start[1], start[2]) === false) {
422
+ return { verdict: 'error', text: `${posText(start)} 是实心的,从这里洪泛不起来;换一个 from` };
423
+ }
424
+
425
+ const seen = new Set<string>([start.join(',')]);
426
+ const queue: PositionXYZ[] = [start];
427
+ const leaks: PositionXYZ[] = [];
428
+ let leakCount = 0;
429
+ let liquidLeak = false;
430
+ const unloadedCells = new Set<string>();
431
+ const steps: ReadonlyArray<PositionXYZ> = [
432
+ [1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1],
433
+ ];
434
+ while (queue.length > 0) {
435
+ const [x, y, z] = queue.shift()!;
436
+ let leaked = false;
437
+ for (const [dx, dy, dz] of steps) {
438
+ const nx = x + dx;
439
+ const ny = y + dy;
440
+ const nz = z + dz;
441
+ const cell = world.cell(nx, ny, nz);
442
+ if (!cell) { unloadedCells.add(`${nx},${ny},${nz}`); continue; }
443
+ if (cell.solid) continue;
444
+ if (!inside(nx, ny, nz)) {
445
+ leaked = true;
446
+ if (isLiquidId(idOf(cell.state))) liquidLeak = true;
447
+ continue;
448
+ }
449
+ const key = `${nx},${ny},${nz}`;
450
+ if (seen.has(key)) continue;
451
+ seen.add(key);
452
+ queue.push([nx, ny, nz]);
453
+ }
454
+ if (leaked) {
455
+ leakCount++;
456
+ const self = world.cell(x, y, z);
457
+ if (self && isLiquidId(idOf(self.state))) liquidLeak = true;
458
+ if (leaks.length < CHECK_LEAK_SAMPLE_CAP) leaks.push([x, y, z]);
459
+ }
460
+ }
461
+
462
+ if (leakCount > 0) {
463
+ const nearest = leaks
464
+ .map((p) => ({ p, d: Math.hypot(p[0] - start[0], p[1] - start[1], p[2] - start[2]) }))
465
+ .sort((l, r) => l.d - r.d);
466
+ const rest = nearest.slice(1).map((e) => posText(e.p));
467
+ return {
468
+ verdict: 'bad',
469
+ text: `${boxText(box)} 不封闭:${leakCount} 个漏口,最近的在 ${posText(nearest[0].p)}`
470
+ + `${rest.length > 0 ? `,还有 ${rest.join('、')}` : ''}`
471
+ + `${liquidLeak ? '(液体按通路算,水会灌进来)' : ''}`
472
+ + unloadedTail(unloadedCells.size),
473
+ };
474
+ }
475
+ if (unloadedCells.size > 0) {
476
+ return {
477
+ verdict: 'unknown',
478
+ text: `${boxText(box)} 目前没找到漏口,但周边 ${unloadedCells.size} 格没加载,没对全`,
479
+ };
480
+ }
481
+ return { verdict: 'ok', text: `${boxText(box)} 封闭(从 ${posText(start)} 洪泛 ${seen.size} 格,没走出去)` };
482
+ }
483
+
484
+ function findStart(box: CheckBox, world: CheckWorld): PositionXYZ | null {
485
+ const center: PositionXYZ = [
486
+ Math.floor((box.min[0] + box.max[0]) / 2),
487
+ Math.floor((box.min[1] + box.max[1]) / 2),
488
+ Math.floor((box.min[2] + box.max[2]) / 2),
489
+ ];
490
+ const centerCell = world.cell(center[0], center[1], center[2]);
491
+ if (centerCell && !centerCell.solid) return center;
492
+ let bestPos: PositionXYZ | null = null;
493
+ let bestDistance = Infinity;
494
+ forEachBoxCell(box, (x, y, z) => {
495
+ const cell = world.cell(x, y, z);
496
+ if (!cell || cell.solid) return;
497
+ const d = Math.hypot(x - center[0], y - center[1], z - center[2]);
498
+ if (d < bestDistance) {
499
+ bestDistance = d;
500
+ bestPos = [x, y, z];
501
+ }
502
+ });
503
+ return bestPos;
504
+ }
505
+
506
+ /** 背包断言按精确物品 id 计数,与 at/count 的核对口径一致。类别断言须逐项列出。 */
507
+ function evalInv(a: Extract<CheckAssert, { kind: 'inv' }>, world: CheckWorld): CheckResult {
508
+ const inv = world.inventory();
509
+ const bad: string[] = [];
510
+ const okBits: string[] = [];
511
+ for (const want of a.want) {
512
+ if (want.enchant !== undefined) {
513
+ if (!world.enchantsOf) {
514
+ bad.push(`${zhName(want.item)} 的附魔读不到(这个部署没接附魔读数)`);
515
+ continue;
516
+ }
517
+ const stacks = world.enchantsOf(want.item);
518
+ const hits = stacks.filter((e) => e.some((one) => one.name === want.enchant));
519
+ const seen = stacks.length === 0
520
+ ? '包里没有这样东西'
521
+ : stacks.map((e) => (e.length === 0 ? '没附魔' : e.map((o) => `${zhEnchant(o.name)}${roman(o.level)}`).join('·')))
522
+ .join(' / ');
523
+ const line = `${zhName(want.item)} 带${zhEnchant(want.enchant)}的 ${hits.length} 件`
524
+ + `(要 ${compareText(want)};包里那几件:${seen})`;
525
+ if (compareHolds(want, hits.length)) okBits.push(line);
526
+ else bad.push(line);
527
+ continue;
528
+ }
529
+ const actual = inv.get(want.item) ?? 0;
530
+ const line = `${zhName(want.item)} ${actual}(要 ${compareText(want)})`;
531
+ if (compareHolds(want, actual)) okBits.push(line);
532
+ else bad.push(line);
533
+ }
534
+ if (bad.length > 0) return { verdict: 'bad', text: `包里现有:${bad.join('、')}` };
535
+ return { verdict: 'ok', text: `包里现有:${okBits.join('、')}` };
536
+ }
537
+
538
+ function missingSamples(site: CheckSite, anchor: PositionXYZ, world: CheckWorld, cap: number): string[] {
539
+ const out: string[] = [];
540
+ for (const step of site.plan.steps) {
541
+ for (let y = step.from[1]; y <= step.to[1] && out.length < cap; y++) {
542
+ for (let z = step.from[2]; z <= step.to[2] && out.length < cap; z++) {
543
+ for (let x = step.from[0]; x <= step.to[0] && out.length < cap; x++) {
544
+ const pos: PositionXYZ = [anchor[0] + x, anchor[1] + y, anchor[2] + z];
545
+ const cell = world.cell(pos[0], pos[1], pos[2]);
546
+ if (!cell) continue;
547
+ if (blueprintStepStateMatches(step, cell.state)) continue;
548
+ out.push(`${zhName(blockIdOf(step.state))} 在 ${posText(pos)}`);
549
+ }
550
+ }
551
+ }
552
+ if (out.length >= cap) break;
553
+ }
554
+ return out;
555
+ }
556
+
557
+ /** 蓝图核验报告符合、缺失和冲突格;未装载或未绑定锚点时不计算。 */
558
+ export function blueprintCheckText(site: CheckSite | null, key: string, world: CheckWorld): CheckResult {
559
+ if (!site) return { verdict: 'error', text: `蓝图「${key}」没装载,对不了` };
560
+ if (!site.anchor) {
561
+ return { verdict: 'error', text: `蓝图「${site.key}」在这个世界还没绑锚点(没开工过),没有工地可对` };
562
+ }
563
+ const anchor = site.anchor;
564
+ const diff = diffBlueprint(
565
+ site.blueprint,
566
+ site.plan,
567
+ anchor,
568
+ (x, y, z) => world.cell(x, y, z)?.state ?? null,
569
+ { checkAir: true, sampleLimit: CHECK_SAMPLE_CAP },
570
+ );
571
+ const conflicts = diff.conflictCounts['wrong-block'] + diff.conflictCounts['should-be-air'];
572
+ const head = `蓝图「${site.key}」对上 ${diff.matched}/${diff.total} 格`;
573
+ if (diff.missing === 0 && conflicts === 0 && diff.unknown === 0) {
574
+ return { verdict: 'ok', text: `${head},整张图都到位了` };
575
+ }
576
+ const bits: string[] = [];
577
+ if (diff.missing > 0) {
578
+ const samples = missingSamples(site, anchor, world, CHECK_SAMPLE_CAP);
579
+ bits.push(`缺 ${diff.missing} 格${samples.length > 0 ? `(${samples.join('、')}${diff.missing > samples.length ? ' 等' : ''})` : ''}`);
580
+ }
581
+ if (conflicts > 0) bits.push(`冲突 ${conflicts} 格`);
582
+ if (diff.unknown > 0) bits.push(`${diff.unknown} 格没加载,没对全`);
583
+ return {
584
+ verdict: diff.missing > 0 || conflicts > 0 ? 'bad' : 'unknown',
585
+ text: `${head};${bits.join(';')}`,
586
+ };
587
+ }
588
+
589
+ function evalMark(a: Extract<CheckAssert, { kind: 'mark' }>, world: CheckWorld): CheckResult {
590
+ const probe = world.mark(a.name);
591
+ if (!probe) return { verdict: 'error', text: `路标表里没有「${a.name}」` };
592
+ const where = `[${zhDimension(probe.dimension)}] ${posText(probe.pos)}`;
593
+ switch (probe.verdict) {
594
+ case 'ok':
595
+ return { verdict: 'ok', text: `「${probe.name}」${where} 还是${probe.kind}` };
596
+ case 'mismatch':
597
+ return {
598
+ verdict: 'bad',
599
+ text: `「${probe.name}」登记的是${probe.kind},${where} 现在是${probe.found}`,
600
+ };
601
+ case 'unloaded':
602
+ return { verdict: 'unknown', text: `「${probe.name}」${where} 区块没加载,走近再对` };
603
+ case 'other-dimension':
604
+ return { verdict: 'unknown', text: `「${probe.name}」在别的维度 ${where},当前维度没法核` };
605
+ case 'unchecked':
606
+ return {
607
+ verdict: 'error',
608
+ text: `「${probe.name}」是${probe.kind},世界里没有一格叫这个,核不了`,
609
+ };
610
+ }
611
+ }
612
+
613
+ export function evalAssert(assert: CheckAssert, world: CheckWorld): CheckResult {
614
+ switch (assert.kind) {
615
+ case 'at': return evalAt(assert, world);
616
+ case 'count': return evalCount(assert, world);
617
+ case 'all': case 'air': return evalUniform(assert, world);
618
+ case 'sealed': return evalSealed(assert, world);
619
+ case 'inv': return evalInv(assert, world);
620
+ case 'blueprint': return blueprintCheckText(world.site(assert.key), assert.key, world);
621
+ case 'mark': return evalMark(assert, world);
622
+ }
623
+ }
624
+
625
+ /** 符合项合并计数,其余逐条报告。 */
626
+ export function renderChecks(parsed: readonly CheckParsed[], world: CheckWorld): string {
627
+ const lines: Array<{ n: number; result: CheckResult }> = parsed.map((item, i) => ({
628
+ n: i + 1,
629
+ result: item.ok ? evalAssert(item.assert, world) : { verdict: 'error' as const, text: item.error },
630
+ }));
631
+ const by = (v: CheckVerdict): typeof lines => lines.filter((l) => l.result.verdict === v);
632
+ const ok = by('ok');
633
+ const bad = by('bad');
634
+ const unknown = by('unknown');
635
+ const errored = by('error');
636
+
637
+ const head = [
638
+ `对账 ${lines.length} 条`,
639
+ [
640
+ `${ok.length} 条符合`,
641
+ bad.length > 0 ? `${bad.length} 条不符` : '',
642
+ unknown.length > 0 ? `${unknown.length} 条没对上(区块没加载)` : '',
643
+ errored.length > 0 ? `${errored.length} 条没受理` : '',
644
+ ].filter(Boolean).join('、'),
645
+ ].join(':');
646
+
647
+ const body = [...bad, ...unknown, ...errored]
648
+ .sort((l, r) => l.n - r.n)
649
+ .map((l) => `#${l.n} ${l.result.text}`);
650
+ return body.length === 0 ? `${head}。` : `${head}。\n${body.join('\n')}`;
651
+ }