dsh-engineering-ui 0.0.0-stage → 1.5.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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +772 -3
  3. package/engineering/plugins/dsh-engineering-ui/cordis.patch.yml +5 -0
  4. package/engineering/plugins/dsh-engineering-ui/icon.svg +14 -0
  5. package/engineering/plugins/dsh-engineering-ui/lib/client.js +8259 -0
  6. package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
  7. package/engineering/plugins/dsh-engineering-ui/lib/index.js +5633 -0
  8. package/engineering/plugins/dsh-engineering-ui/lib/job-wait-guard.js +236 -0
  9. package/engineering/plugins/dsh-engineering-ui/lib/mem-guard.js +331 -0
  10. package/engineering/plugins/dsh-engineering-ui/lib/webots.js +114 -0
  11. package/engineering/plugins/dsh-engineering-ui/locale/en.json +6 -0
  12. package/engineering/plugins/dsh-engineering-ui/locale/zh.json +6 -0
  13. package/engineering/plugins/dsh-engineering-ui/preset-engineering.patch.yml +601 -0
  14. package/engineering/skills/assembly-orchestration/SKILL.md +651 -0
  15. package/engineering/skills/cad-workflow/SKILL.md +132 -0
  16. package/engineering/skills/mode-selection/SKILL.md +660 -0
  17. package/engineering/skills/physics-in-loop/SKILL.md +238 -0
  18. package/engineering/skills/physics-in-loop/references/feadocs.md +142 -0
  19. package/engineering/skills/solidworks-bridge/SKILL.md +445 -0
  20. package/engineering/skills/sw-design/SKILL.md +112 -0
  21. package/engineering/skills/sw-design/references/autocad-workflow.md +81 -0
  22. package/engineering/skills/sw-design/references/gbt-drafting.md +71 -0
  23. package/engineering/skills/sw-to-cad/SKILL.md +121 -0
  24. package/engineering/skills/webots-simulation/SKILL.md +58 -0
  25. package/engineering/tools/LICENSE.Auto-SolidWorks +21 -0
  26. package/engineering/tools/_plugin_paths.py +93 -0
  27. package/engineering/tools/_root.py +330 -0
  28. package/engineering/tools/_store.py +446 -0
  29. package/engineering/tools/ac_bridge.py +459 -0
  30. package/engineering/tools/ac_rules.json +21 -0
  31. package/engineering/tools/ac_validate.py +699 -0
  32. package/engineering/tools/ask_user.py +887 -0
  33. package/engineering/tools/build_arm1.py +46 -0
  34. package/engineering/tools/build_assembly.py +94 -0
  35. package/engineering/tools/build_base.py +57 -0
  36. package/engineering/tools/build_housing.py +70 -0
  37. package/engineering/tools/build_structural.py +43 -0
  38. package/engineering/tools/build_transmission.py +69 -0
  39. package/engineering/tools/cadx.lsp +128 -0
  40. package/engineering/tools/choice_contract.py +61 -0
  41. package/engineering/tools/conn_state.py +1671 -0
  42. package/engineering/tools/defense_gate.py +3374 -0
  43. package/engineering/tools/drawing_input.py +314 -0
  44. package/engineering/tools/env_check.py +467 -0
  45. package/engineering/tools/examples/build_axis.py +142 -0
  46. package/engineering/tools/examples/build_cup.py +56 -0
  47. package/engineering/tools/examples/build_plate.py +58 -0
  48. package/engineering/tools/examples/build_sphere.py +30 -0
  49. package/engineering/tools/examples/build_transition_duct.py +381 -0
  50. package/engineering/tools/gate_integrity.py +401 -0
  51. package/engineering/tools/mode2_flow.py +3589 -0
  52. package/engineering/tools/mode_checker.py +77 -0
  53. package/engineering/tools/mode_gate.py +8013 -0
  54. package/engineering/tools/part_registry.py +4266 -0
  55. package/engineering/tools/physics/__init__.py +50 -0
  56. package/engineering/tools/physics/_compat.py +12 -0
  57. package/engineering/tools/physics/design_state.py +142 -0
  58. package/engineering/tools/physics/domain_validator.py +751 -0
  59. package/engineering/tools/physics/fatigue.py +489 -0
  60. package/engineering/tools/physics/fea_solver.py +1163 -0
  61. package/engineering/tools/physics/feapy_solver.py +314 -0
  62. package/engineering/tools/physics/gb_materials.json +1228 -0
  63. package/engineering/tools/physics/geometry_gate.py +337 -0
  64. package/engineering/tools/physics/load_case.py +951 -0
  65. package/engineering/tools/physics/material_db.py +874 -0
  66. package/engineering/tools/physics/mesh_adapter.py +321 -0
  67. package/engineering/tools/physics/refine_rules.py +272 -0
  68. package/engineering/tools/physics/rules/gb_base_rules.json +377 -0
  69. package/engineering/tools/physics/rules/gb_drafting_rules.json +57 -0
  70. package/engineering/tools/physics/rules/gb_electrical_rules.json +25 -0
  71. package/engineering/tools/physics/rules/gb_fasteners_rules.json +73 -0
  72. package/engineering/tools/physics/rules/gb_housing_rules.json +41 -0
  73. package/engineering/tools/physics/rules/gb_manufacturing_rules.json +25 -0
  74. package/engineering/tools/physics/rules/gb_material_verify_rules.json +72 -0
  75. package/engineering/tools/physics/rules/gb_materials_rules.json +377 -0
  76. package/engineering/tools/physics/rules/gb_safety_rules.json +89 -0
  77. package/engineering/tools/physics/rules/gb_springs_rules.json +41 -0
  78. package/engineering/tools/physics/rules/gb_standards_registry.json +1646 -0
  79. package/engineering/tools/physics/rules/gb_structural_rules.json +201 -0
  80. package/engineering/tools/physics/rules/gb_surface_rules.json +25 -0
  81. package/engineering/tools/physics/rules/gb_transmission_rules.json +233 -0
  82. package/engineering/tools/physics/rules/housing_rules.json +48 -0
  83. package/engineering/tools/physics/rules/manifest.json +54 -0
  84. package/engineering/tools/physics/rules/mold_rules.json +48 -0
  85. package/engineering/tools/physics/rules/structural_rules.json +56 -0
  86. package/engineering/tools/physics/rules/transmission_rules.json +48 -0
  87. package/engineering/tools/physics/simulation_report.py +829 -0
  88. package/engineering/tools/physics_bridge.py +2685 -0
  89. package/engineering/tools/profile-manifest.ps1 +72 -0
  90. package/engineering/tools/run_regression.py +109 -0
  91. package/engineering/tools/settings.py +419 -0
  92. package/engineering/tools/solidworks-modeling.md +519 -0
  93. package/engineering/tools/sw_bridge.py +8215 -0
  94. package/engineering/tools/sw_guard.py +1066 -0
  95. package/engineering/tools/sw_surface.py +87 -0
  96. package/engineering/tools/swapi.py +14766 -0
  97. package/engineering/tools/verify_sync.py +218 -0
  98. package/engineering/tools/visual_check.py +2589 -0
  99. package/engineering/tools/webots_env.py +121 -0
  100. package/engineering/tools/webots_mcp/__init__.py +3 -0
  101. package/engineering/tools/webots_mcp/__main__.py +3 -0
  102. package/engineering/tools/webots_mcp/assembly.py +391 -0
  103. package/engineering/tools/webots_mcp/bootstrap.py +55 -0
  104. package/engineering/tools/webots_mcp/controller_worker.py +124 -0
  105. package/engineering/tools/webots_mcp/evidence.py +288 -0
  106. package/engineering/tools/webots_mcp/fake_worker.py +69 -0
  107. package/engineering/tools/webots_mcp/fault_resources.py +121 -0
  108. package/engineering/tools/webots_mcp/host.py +45 -0
  109. package/engineering/tools/webots_mcp/ledger.py +73 -0
  110. package/engineering/tools/webots_mcp/owned_job.py +70 -0
  111. package/engineering/tools/webots_mcp/physical_gate.py +187 -0
  112. package/engineering/tools/webots_mcp/physics.py +152 -0
  113. package/engineering/tools/webots_mcp/pipeline.py +108 -0
  114. package/engineering/tools/webots_mcp/recipes.py +129 -0
  115. package/engineering/tools/webots_mcp/repair.py +132 -0
  116. package/engineering/tools/webots_mcp/repair_trial.py +375 -0
  117. package/engineering/tools/webots_mcp/runtime.py +311 -0
  118. package/engineering/tools/webots_mcp/server.py +217 -0
  119. package/engineering/tools/webots_mcp/sw_export.py +509 -0
  120. package/engineering/tools/webots_mcp/transport.py +156 -0
  121. package/engineering/tools/workflow_gate.py +6134 -0
  122. package/package.json +64 -4
@@ -0,0 +1,401 @@
1
+ # -*- coding: utf-8 -*-
2
+ r"""gate_integrity.py —— 门禁核心脚本的完整性自检(【A-20】修复)
3
+
4
+ ═══ 为什么需要这个模块 ═══════════════════════════════════════════════════
5
+ 2026-11-01 任务(20261101_002006_024C510A)实测:
6
+ 逐份子代理日志里 "has been updated successfully" 去重后共 **33 次**,
7
+ 跨 **7 个代理**,改的全是【门禁自己】:
8
+ fea_solver.py ×21、swapi.py ×5、domain_validator.py ×2、
9
+ physics_bridge.py ×2、mode_gate.py ×1、sw_bridge.py ×2
10
+ 其中一次修完之后,check-room 从
11
+ ok=false, blockers=["【防线③领域】…4 条 CRITICAL"]
12
+ 变成 ok=true, blockers=[] —— 而磁盘上的零件没有任何变化。
13
+
14
+ 这是三道防线最根本的架构漏洞:**裁判可以被被告改写**。
15
+ 判定逻辑(defense_gate / physics / domain)+ 判定对象(parts/ 下的零件)
16
+ 都在同一个可写目录里,AI 有写权限 → "改源码让判定变绿"永远比
17
+ "改设计让校核通过"便宜。
18
+
19
+ ═══ 本模块的定位(诚实说明能力边界)═══════════════════════════════════
20
+ 它不是、也不可能阻止有写权限的进程去改文件 —— 那是操作系统权限的事。
21
+ 它能做的是【让改动无法静默】:
22
+ · 首次运行时把门禁核心文件的 sha256 记进基线文件;
23
+ · 之后每次调用都能回答"这些文件相对基线有没有变";
24
+ · 门禁关键路径(room-end / confirm-assembly / 收尾)调用 assert_clean(),
25
+ 一旦发现被改就【留痕 + 降级】而不是静默照常放行。
26
+
27
+ 用法:
28
+ python gate_integrity.py baseline # 建立/更新基线(人工确认后)
29
+ python gate_integrity.py check [--json] # 检查是否被改动(CI/门禁可调)
30
+ python gate_integrity.py explain # 说明基线与威胁模型
31
+
32
+ 作为库:
33
+ from gate_integrity import check_integrity, assert_clean
34
+ r = check_integrity() # {ok, changed[], missing[], baseline_at}
35
+ assert_clean("room-end") # 被改则写留痕并返回 False
36
+
37
+ ═══ 设计取舍 ═══════════════════════════════════════════════════════════
38
+ · 基线缺失时 **不判失败**(首次部署 / 换机都不该直接红),
39
+ 只返回 baseline_missing=True 并在门禁返回体里如实标注;
40
+ · 基线文件本身放在 .defense/ 下,与凭据隔离区同处;
41
+ · 只覆盖【判定逻辑】文件,不覆盖业务脚本(避免正常迭代天天报红)。
42
+ """
43
+ from __future__ import annotations
44
+
45
+ import hashlib
46
+ import io
47
+ import json
48
+ import os
49
+ import sys
50
+ import time
51
+
52
+ try:
53
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
54
+ except Exception:
55
+ pass
56
+
57
+ _HERE = os.path.dirname(os.path.abspath(__file__))
58
+
59
+ # ── 被保护的文件:判定逻辑本身 ────────────────────────────────────────────
60
+ # 注意:刻意【不包含】sw_bridge.py / ask_user.py 这类业务入口 ——
61
+ # 它们经常需要按现场调整,纳入保护会导致天天报"被改",
62
+ # 反而让人习惯性忽略告警(告警疲劳)。真正的判定本体才在这里。
63
+ PROTECTED = (
64
+ "defense_gate.py", # 三道防线的判定本体
65
+ os.path.join("physics", "fea_solver.py"), # 强度/疲劳判据
66
+ os.path.join("physics", "domain_validator.py"), # 领域规则引擎
67
+ os.path.join("physics", "load_case.py"), # 载荷口径(唯一事实源)
68
+ os.path.join("physics", "fatigue.py"), # 疲劳判据
69
+ os.path.join("physics", "geometry_gate.py"), # 几何门禁
70
+ os.path.join("physics", "material_db.py"), # 材料库(属性来源)
71
+ )
72
+
73
+ BASELINE_NAME = "gate_baseline.json"
74
+
75
+
76
+ def _state_dir():
77
+ """状态目录(与 _store.state_dir 同口径,但这里不 import 以免循环依赖)。"""
78
+ try:
79
+ env = (os.environ.get("DSH_STATE_DIR") or "").strip()
80
+ if env:
81
+ return env
82
+ except Exception:
83
+ pass
84
+ try:
85
+ home = os.environ.get("USERPROFILE") or os.environ.get("HOME") or ""
86
+ if home:
87
+ cand = os.path.join(home, ".dsh", ".agent-presets", "engineering", "tools")
88
+ if os.path.isdir(cand):
89
+ return cand
90
+ except Exception:
91
+ pass
92
+ return _HERE
93
+
94
+
95
+ def baseline_path():
96
+ return os.path.join(_state_dir(), ".defense", BASELINE_NAME)
97
+
98
+
99
+ def _sha256(path):
100
+ try:
101
+ h = hashlib.sha256()
102
+ with open(path, "rb") as f:
103
+ for chunk in iter(lambda: f.read(1 << 20), b""):
104
+ h.update(chunk)
105
+ return h.hexdigest()
106
+ except Exception:
107
+ return None
108
+
109
+
110
+ def _file_mtimes(root=None):
111
+ """取全部受保护文件的 sha256 + 大小。"""
112
+ root = root or _HERE
113
+ out = {}
114
+ for rel in PROTECTED:
115
+ p = os.path.join(root, rel)
116
+ if not os.path.isfile(p):
117
+ continue
118
+ key = rel.replace(os.sep, "/")
119
+ try:
120
+ out[key] = {
121
+ "sha256": _sha256(p),
122
+ "size": int(os.path.getsize(p)),
123
+ "mtime": round(os.path.getmtime(p), 3),
124
+ }
125
+ except Exception:
126
+ continue
127
+ return out
128
+
129
+
130
+ def write_baseline(reason="manual", root=None):
131
+ """建立/更新基线。只有【人工确认过】才该调用。"""
132
+ p = baseline_path()
133
+ data = {
134
+ "schema": "dsh-engineering/gate-baseline@1",
135
+ "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
136
+ "created_ts": time.time(),
137
+ "reason": str(reason),
138
+ "root": os.path.abspath(root or _HERE),
139
+ "files": _file_mtimes(root),
140
+ }
141
+ try:
142
+ os.makedirs(os.path.dirname(p), exist_ok=True)
143
+ tmp = p + ".tmp"
144
+ with io.open(tmp, "w", encoding="utf-8") as f:
145
+ json.dump(data, f, ensure_ascii=False, indent=2)
146
+ os.replace(tmp, p)
147
+ return {"ok": True, "path": p, "count": len(data["files"])}
148
+ except Exception as e:
149
+ return {"ok": False, "error": repr(e), "path": p}
150
+
151
+
152
+ def load_baseline():
153
+ p = baseline_path()
154
+ if not os.path.isfile(p):
155
+ return None
156
+ try:
157
+ with io.open(p, "r", encoding="utf-8-sig") as f:
158
+ d = json.load(f)
159
+ return d if isinstance(d, dict) else None
160
+ except Exception:
161
+ return None
162
+
163
+
164
+ def check_integrity(root=None):
165
+ """检查受保护文件相对基线是否被改动。
166
+
167
+ Returns:
168
+ {ok, baseline_missing, baseline_at, changed[], missing[], added[],
169
+ checked_count, note}
170
+ ok=True 表示"没有发现相对基线的改动"(基线缺失时也为 True,
171
+ 但 baseline_missing=True,调用方应如实标注"未校验")。
172
+ """
173
+ out = {"ok": True, "baseline_missing": False, "baseline_at": None,
174
+ "changed": [], "missing": [], "added": [],
175
+ "checked_count": 0, "note": ""}
176
+ bl = load_baseline()
177
+ cur = _file_mtimes(root)
178
+ out["checked_count"] = len(cur)
179
+ if not bl or not isinstance(bl.get("files"), dict):
180
+ out["baseline_missing"] = True
181
+ out["note"] = (
182
+ "尚无门禁完整性基线 —— 本次【未校验】判定逻辑是否被改动。"
183
+ "人工确认当前代码可信后执行一次:"
184
+ "python gate_integrity.py baseline")
185
+ return out
186
+ out["baseline_at"] = bl.get("created_at")
187
+ base = bl["files"]
188
+ for rel, info in base.items():
189
+ c = cur.get(rel)
190
+ if c is None:
191
+ out["missing"].append(rel)
192
+ continue
193
+ if str(c.get("sha256") or "") != str(info.get("sha256") or ""):
194
+ out["changed"].append({
195
+ "file": rel,
196
+ "sha_now": (c.get("sha256") or "")[:16],
197
+ "sha_baseline": (info.get("sha256") or "")[:16],
198
+ "size_now": c.get("size"),
199
+ "size_baseline": info.get("size"),
200
+ })
201
+ for rel in cur:
202
+ if rel not in base:
203
+ out["added"].append(rel)
204
+ _bad = bool(out["changed"] or out["missing"])
205
+ out["ok"] = not _bad
206
+ if _bad:
207
+ out["note"] = (
208
+ "【A-20】门禁判定逻辑相对基线发生了改动:%d 个文件被改、%d 个缺失。"
209
+ "三道防线的判定本体被改过,其结论的可信度存疑 —— "
210
+ "请先核对 diff 是否经人工审阅;确认无误后重建基线:"
211
+ "python gate_integrity.py baseline"
212
+ % (len(out["changed"]), len(out["missing"])))
213
+ return out
214
+
215
+
216
+ def assert_clean(where="", room=""):
217
+ """门禁关键路径调用点:返回 (clean: bool, evidence: dict)。
218
+
219
+ 被改动时不抛异常、不阻断(避免把正常迭代锁死),但会:
220
+ ① 把事件写进 .defense/gate_integrity_log.jsonl(只追加,可审计);
221
+ ② 返回 clean=False,供调用方在响应体里如实标注。
222
+ """
223
+ r = check_integrity()
224
+ if r.get("ok"):
225
+ return True, r
226
+ try:
227
+ _log = os.path.join(_state_dir(), ".defense", "gate_integrity_log.jsonl")
228
+ os.makedirs(os.path.dirname(_log), exist_ok=True)
229
+ rec = {
230
+ "at": time.strftime("%Y-%m-%d %H:%M:%S"),
231
+ "where": str(where),
232
+ "room": str(room),
233
+ "changed": [c.get("file") for c in (r.get("changed") or [])],
234
+ "missing": r.get("missing") or [],
235
+ "baseline_at": r.get("baseline_at"),
236
+ }
237
+ with io.open(_log, "a", encoding="utf-8") as f:
238
+ f.write(json.dumps(rec, ensure_ascii=False) + "\n")
239
+ r["logged_to"] = _log
240
+ try:
241
+ if os.path.getsize(_log) > 2 * 1024 * 1024:
242
+ with io.open(_log, "r", encoding="utf-8") as f:
243
+ lines = f.readlines()[-2000:]
244
+ with io.open(_log, "w", encoding="utf-8") as f:
245
+ f.writelines(lines)
246
+ except Exception:
247
+ pass
248
+ except Exception:
249
+ pass
250
+ return False, r
251
+
252
+
253
+ def protect_gate_files(scope="all", dry_run=False):
254
+ """【A-20 修复】把判定本体置为【只读】—— 让"裁判被被告改写"变贵。
255
+
256
+ ── 原缺陷(本次审计再次确认)──────────────────────────────────────────
257
+ 本模块此前只能"事后发现被改"(留痕),改文件本身仍然是一次 chmod/覆盖
258
+ 就完成的事。同目录同账户,API 能力上没有任何阻力 —— 实测一次任务里
259
+ 7 个代理改了 33 次判定源码,其中一次直接把 CRITICAL 改成 PASS。
260
+
261
+ ── 现在做的(是"提高成本",不是"绝对防止",如实说明)──────────────
262
+ · 对 PROTECTED 里的文件清掉写位(Windows 只读属性 / POSIX 去 w);
263
+ · 改完立刻复核:os.access(path, W_OK) 必须为 False 才算成功;
264
+ · 失败项如实列出(被占用/权限不足/文件系统不支持),绝不假装成功。
265
+
266
+ 这不是安全边界:有权限的进程依然能 chmod 回来。它的价值在于 ——
267
+ "改判定逻辑"从此变成一个【明显、可留痕、需要两步】的动作,
268
+ 而不是 AI 顺手 edit 一下就能把红灯改绿。
269
+
270
+ 解除保护(人工审阅完改动、重建基线后):protect_gate_files(scope="none")
271
+ """
272
+ out = {"ok": True, "protected": [], "failed": [], "already": [],
273
+ "scope": str(scope), "dry_run": bool(dry_run)}
274
+ if str(scope) in ("none", "off", "0", "false"):
275
+ for rel in PROTECTED:
276
+ p = os.path.join(_HERE, rel)
277
+ if not os.path.isfile(p):
278
+ continue
279
+ if dry_run:
280
+ out["protected"].append(rel)
281
+ continue
282
+ try:
283
+ mode = os.stat(p).st_mode
284
+ os.chmod(p, mode | 0o200)
285
+ out["protected"].append(rel)
286
+ except Exception as e:
287
+ out["failed"].append({"file": rel, "error": repr(e)})
288
+ out["ok"] = not out["failed"]
289
+ out["note"] = "已解除只读保护(恢复可写)"
290
+ return out
291
+ for rel in PROTECTED:
292
+ p = os.path.join(_HERE, rel)
293
+ if not os.path.isfile(p):
294
+ continue
295
+ try:
296
+ if not os.access(p, os.W_OK):
297
+ out["already"].append(rel)
298
+ continue
299
+ if dry_run:
300
+ out["protected"].append(rel)
301
+ continue
302
+ mode = os.stat(p).st_mode
303
+ os.chmod(p, mode & ~0o222)
304
+ if os.access(p, os.W_OK):
305
+ out["failed"].append({"file": rel,
306
+ "error": "chmod 后仍可写(文件系统/属性不生效)"})
307
+ else:
308
+ out["protected"].append(rel)
309
+ except Exception as e:
310
+ out["failed"].append({"file": rel, "error": repr(e)})
311
+ out["ok"] = not out["failed"]
312
+ if out["failed"]:
313
+ out["note"] = ("部分文件无法置为只读(多半被进程占用或权限不足)——"
314
+ "这部分仍是可改的,改动只能靠基线比对发现。")
315
+ return out
316
+
317
+
318
+ def register_objection(room, reason, evidence=None, by=""):
319
+ """【A-20 补】合法的"异议通道":对门禁结论不服时,走登记而不是改源码。
320
+
321
+ 问题:审计发现 AI 遇到门禁挡住时会去改判定源码(因为那是唯一"出路")。
322
+ 现在给一条正规通道 —— 把"我认为这条结论不成立,理由与证据如下"
323
+ 记进 .defense/objections.jsonl,并【明确告诉调用方】:
324
+ · 登记异议不会改变本次判定(门禁结论仍然生效,防线不因此放宽);
325
+ · 它会成为人工复核的输入,由人决定是改设计、改规则,还是改代码。
326
+ 这样"有不同意见"与"私自改裁判"就分开了。
327
+ """
328
+ rec = {
329
+ "at": time.strftime("%Y-%m-%d %H:%M:%S"),
330
+ "ts": time.time(),
331
+ "room": str(room or ""),
332
+ "reason": str(reason or "").strip()[:2000],
333
+ "evidence": evidence if isinstance(evidence, (list, dict)) else None,
334
+ "by": str(by or ""),
335
+ }
336
+ if not rec["reason"]:
337
+ return {"ok": False, "error": "异议必须写理由(reason 不能为空)"}
338
+ try:
339
+ _d = os.path.join(_state_dir(), ".defense")
340
+ os.makedirs(_d, exist_ok=True)
341
+ _p = os.path.join(_d, "objections.jsonl")
342
+ with io.open(_p, "a", encoding="utf-8") as f:
343
+ f.write(json.dumps(rec, ensure_ascii=False) + "\n")
344
+ return {"ok": True, "path": _p,
345
+ "note": ("异议已登记。注意:本次门禁结论【不因此改变】,"
346
+ "登记只用于人工复核;不要为此去改判定源码(那会被基线比对抓到)。")}
347
+ except Exception as e:
348
+ return {"ok": False, "error": repr(e)}
349
+
350
+
351
+ def main(argv=None):
352
+ argv = list(sys.argv[1:] if argv is None else argv)
353
+ cmd = argv[0] if argv else "check"
354
+ if cmd == "baseline":
355
+ reason = argv[1] if len(argv) > 1 else "manual"
356
+ res = write_baseline(reason=reason)
357
+ print(json.dumps(res, ensure_ascii=False, indent=2))
358
+ return 0 if res.get("ok") else 1
359
+ if cmd == "explain":
360
+ print(__doc__)
361
+ return 0
362
+ if cmd == "protect":
363
+ # 【A-20】protect / unprotect —— 判定本体的 OS 写保护
364
+ scope = "all"
365
+ _args = [a for a in argv[1:] if not a.startswith("-")]
366
+ if _args:
367
+ scope = _args[0]
368
+ if "--off" in argv or "unprotect" in argv:
369
+ scope = "none"
370
+ res = protect_gate_files(scope=scope, dry_run=("--dry-run" in argv))
371
+ print(json.dumps(res, ensure_ascii=False, indent=2))
372
+ return 0 if res.get("ok") else 1
373
+ if cmd == "objection":
374
+ # 【A-20】objection 登记:<room> <reason>
375
+ _pos = [a for a in argv[1:] if not a.startswith("-")]
376
+ _room = _pos[0] if _pos else ""
377
+ _reason = _pos[1] if len(_pos) > 1 else ""
378
+ res = register_objection(_room, _reason, by="cli")
379
+ print(json.dumps(res, ensure_ascii=False, indent=2))
380
+ return 0 if res.get("ok") else 1
381
+ r = check_integrity()
382
+ if "--json" in argv:
383
+ print(json.dumps(r, ensure_ascii=False, indent=2))
384
+ else:
385
+ print("受检文件数 : %d" % r["checked_count"])
386
+ print("基线建立于 : %s" % (r.get("baseline_at") or "(尚未建立)"))
387
+ print("结论 : %s" % ("未被改动" if r["ok"] else "【发现改动】"))
388
+ for c in r.get("changed") or []:
389
+ print(" 改动 %-46s %s -> %s"
390
+ % (c["file"], c["sha_baseline"], c["sha_now"]))
391
+ for m in r.get("missing") or []:
392
+ print(" 缺失 %s" % m)
393
+ for a in r.get("added") or []:
394
+ print(" 新增 %s(基线里没有)" % a)
395
+ if r.get("note"):
396
+ print("说明 : %s" % r["note"])
397
+ return 0 if r["ok"] else 1
398
+
399
+
400
+ if __name__ == "__main__":
401
+ sys.exit(main())