@whyshu/argent-engine 0.0.0-stage → 0.6.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 (305) hide show
  1. package/MANIFEST.json +307 -0
  2. package/engine/VERSION +1 -0
  3. package/engine/agents/catalog.json +454 -0
  4. package/engine/configs/_aliases.proposed.json +651 -0
  5. package/engine/configs/_migration-plan.proposed.json +899 -0
  6. package/engine/configs/account.json +272 -0
  7. package/engine/configs/advanced.json +205 -0
  8. package/engine/configs/host.json +149 -0
  9. package/engine/configs/hr.json +27 -0
  10. package/engine/configs/notify.json +264 -0
  11. package/engine/core/cli/argent.mjs +277 -0
  12. package/engine/core/contracts/argent.contracts.json +1065 -0
  13. package/engine/core/contracts/verbs.schema.json +2539 -0
  14. package/engine/core/lib/argent-datadir.mjs +223 -0
  15. package/engine/core/lib/audit.mjs +187 -0
  16. package/engine/core/lib/claim.mjs +258 -0
  17. package/engine/core/lib/config-catalog.mjs +1172 -0
  18. package/engine/core/lib/doctor-customer.mjs +577 -0
  19. package/engine/core/lib/flow-list.mjs +138 -0
  20. package/engine/core/lib/flow-registry.mjs +48 -0
  21. package/engine/core/lib/init.mjs +247 -0
  22. package/engine/core/lib/ledger.mjs +124 -0
  23. package/engine/core/lib/license-activate.mjs +813 -0
  24. package/engine/core/lib/license-device.mjs +314 -0
  25. package/engine/core/lib/license-file.mjs +349 -0
  26. package/engine/core/lib/license-guard.mjs +123 -0
  27. package/engine/core/lib/license-ops.mjs +1645 -0
  28. package/engine/core/lib/license-verdict.mjs +133 -0
  29. package/engine/core/lib/license.mjs +577 -0
  30. package/engine/core/lib/notify/dedup.mjs +101 -0
  31. package/engine/core/lib/notify/feishu.mjs +38 -0
  32. package/engine/core/lib/notify/index.mjs +95 -0
  33. package/engine/core/lib/notify/verb.mjs +327 -0
  34. package/engine/core/lib/notify/webhook.mjs +135 -0
  35. package/engine/core/lib/notify/wecom.mjs +36 -0
  36. package/engine/core/lib/python.mjs +145 -0
  37. package/engine/core/lib/sop.mjs +116 -0
  38. package/engine/core/lib/versions.mjs +205 -0
  39. package/engine/core/mcp/client.mjs +67 -0
  40. package/engine/core/mcp/http.mjs +138 -0
  41. package/engine/core/mcp/server.mjs +702 -0
  42. package/engine/scripts/_engine_rules_probe.py +15 -0
  43. package/engine/scripts/analyze-full-day.py +568 -0
  44. package/engine/scripts/archive/feishu-bridge.js +246 -0
  45. package/engine/scripts/argent-agents.mjs +527 -0
  46. package/engine/scripts/argent-capabilities.mjs +776 -0
  47. package/engine/scripts/argent-catalog.mjs +186 -0
  48. package/engine/scripts/argent-datadir.py +62 -0
  49. package/engine/scripts/argent-doctor.mjs +624 -0
  50. package/engine/scripts/argent-env-write.mjs +302 -0
  51. package/engine/scripts/argent-schedule.mjs +620 -0
  52. package/engine/scripts/argent-setup-check.mjs +369 -0
  53. package/engine/scripts/argent-upgrade.mjs +1046 -0
  54. package/engine/scripts/base-bootstrap.sh +575 -0
  55. package/engine/scripts/billing-mode-daily.sh +12 -0
  56. package/engine/scripts/build-engine-package.mjs +123 -0
  57. package/engine/scripts/capability-packs.lock.json +484 -0
  58. package/engine/scripts/capability_pack.py +1242 -0
  59. package/engine/scripts/check-1.0-criteria.mjs +355 -0
  60. package/engine/scripts/check-agent-catalog.mjs +141 -0
  61. package/engine/scripts/check-billing-mode.sh +377 -0
  62. package/engine/scripts/check-child-sessions.mjs +83 -0
  63. package/engine/scripts/check-config-catalog.mjs +677 -0
  64. package/engine/scripts/check-docs-drift.mjs +522 -0
  65. package/engine/scripts/check-dsh-web-expose.sh +98 -0
  66. package/engine/scripts/check-judgment-drift.mjs +403 -0
  67. package/engine/scripts/check-license.mjs +199 -0
  68. package/engine/scripts/check-model-auth.py +443 -0
  69. package/engine/scripts/check-pack-closure.mjs +661 -0
  70. package/engine/scripts/check-portable-symbols.mjs +116 -0
  71. package/engine/scripts/check-preset-landing.mjs +124 -0
  72. package/engine/scripts/check-production-home.mjs +602 -0
  73. package/engine/scripts/check-role-mapping.mjs +461 -0
  74. package/engine/scripts/check-sample-redaction.mjs +320 -0
  75. package/engine/scripts/check-skill-classification.mjs +188 -0
  76. package/engine/scripts/check-sop-consistency.mjs +431 -0
  77. package/engine/scripts/check-test-env-safety.mjs +627 -0
  78. package/engine/scripts/check-version-consistency.mjs +452 -0
  79. package/engine/scripts/ci-run.sh +558 -0
  80. package/engine/scripts/config-explain.mjs +278 -0
  81. package/engine/scripts/configure-provider.rb +29 -0
  82. package/engine/scripts/customer-config-migrate.mjs +179 -0
  83. package/engine/scripts/customer-preflight.py +892 -0
  84. package/engine/scripts/customer-readiness.sh +134 -0
  85. package/engine/scripts/daily-report-inbox.py +242 -0
  86. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  87. package/engine/scripts/daily-report.mjs +1310 -0
  88. package/engine/scripts/daily-report.sh +46 -0
  89. package/engine/scripts/data/lip-product-dict.json +306 -0
  90. package/engine/scripts/deliver-customer.mjs +932 -0
  91. package/engine/scripts/delivery-faces.txt +31 -0
  92. package/engine/scripts/desktop-trial.sh +146 -0
  93. package/engine/scripts/dev-window.sh +260 -0
  94. package/engine/scripts/drill-clean-machine.sh +315 -0
  95. package/engine/scripts/dsh-api-client.mjs +215 -0
  96. package/engine/scripts/dsh-backup-scope.sh +83 -0
  97. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  98. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  99. package/engine/scripts/dsh-plane.mjs +153 -0
  100. package/engine/scripts/dsh-portforward.py +223 -0
  101. package/engine/scripts/dsh-preflight.mjs +179 -0
  102. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  103. package/engine/scripts/dsh-release-watch.mjs +458 -0
  104. package/engine/scripts/dsh-remote-check.sh +99 -0
  105. package/engine/scripts/dsh-restart.sh +179 -0
  106. package/engine/scripts/dsh-restore.sh +111 -0
  107. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  108. package/engine/scripts/dsh-snapshot.mjs +116 -0
  109. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  110. package/engine/scripts/dsh-upgrade.mjs +612 -0
  111. package/engine/scripts/dsh-url.sh +86 -0
  112. package/engine/scripts/dsh-web-launch.mjs +155 -0
  113. package/engine/scripts/ensure-store-windows.py +509 -0
  114. package/engine/scripts/entitlements.py +458 -0
  115. package/engine/scripts/env_probe.py +411 -0
  116. package/engine/scripts/feishu-card.mjs +252 -0
  117. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  118. package/engine/scripts/flow-auto-retry.py +454 -0
  119. package/engine/scripts/flow-requirements.json +720 -0
  120. package/engine/scripts/flow-scaffold.py +275 -0
  121. package/engine/scripts/flow-timers-plist.sh +86 -0
  122. package/engine/scripts/flow-timers.sh +182 -0
  123. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  124. package/engine/scripts/flow-trigger.sh +571 -0
  125. package/engine/scripts/flow_billing.py +354 -0
  126. package/engine/scripts/flow_capability_stats.py +314 -0
  127. package/engine/scripts/flow_case.py +194 -0
  128. package/engine/scripts/flow_claim.py +497 -0
  129. package/engine/scripts/flow_config.py +1024 -0
  130. package/engine/scripts/flow_failure.py +588 -0
  131. package/engine/scripts/flow_learn.py +105 -0
  132. package/engine/scripts/flow_ledger.py +235 -0
  133. package/engine/scripts/flow_policy.py +238 -0
  134. package/engine/scripts/flow_preflight.py +339 -0
  135. package/engine/scripts/flow_registry.py +341 -0
  136. package/engine/scripts/flow_retry.py +304 -0
  137. package/engine/scripts/flow_return_to_ops.py +191 -0
  138. package/engine/scripts/flow_source.py +315 -0
  139. package/engine/scripts/flow_sources/__init__.py +53 -0
  140. package/engine/scripts/flow_sources/cases_json.py +106 -0
  141. package/engine/scripts/flow_sources/common.py +428 -0
  142. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  143. package/engine/scripts/flow_stale.py +185 -0
  144. package/engine/scripts/flow_status_reconcile.py +344 -0
  145. package/engine/scripts/flow_triage.py +849 -0
  146. package/engine/scripts/flow_write_verify.py +496 -0
  147. package/engine/scripts/fulfillment.js +586 -0
  148. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  149. package/engine/scripts/gen-preset-rows.mjs +259 -0
  150. package/engine/scripts/init-ml.sh +2275 -0
  151. package/engine/scripts/install-internal-role.sh +190 -0
  152. package/engine/scripts/install-license.mjs +641 -0
  153. package/engine/scripts/install-one-shot.mjs +665 -0
  154. package/engine/scripts/integration_contract.py +220 -0
  155. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  156. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  157. package/engine/scripts/invoice-mapping.example.json +40 -0
  158. package/engine/scripts/invoice-table.sh +194 -0
  159. package/engine/scripts/invoice_carrier_notice.py +279 -0
  160. package/engine/scripts/invoice_fail_closeout.py +297 -0
  161. package/engine/scripts/invoice_lip_runner.py +1508 -0
  162. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  163. package/engine/scripts/invoice_ready_audit.py +191 -0
  164. package/engine/scripts/invoice_runner.py +675 -0
  165. package/engine/scripts/invoice_stageb.py +516 -0
  166. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  167. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  168. package/engine/scripts/lease.py +377 -0
  169. package/engine/scripts/lib/agent-install.mjs +649 -0
  170. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  171. package/engine/scripts/lib/argent_datadir.py +262 -0
  172. package/engine/scripts/lib/argent_filelock.py +507 -0
  173. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  174. package/engine/scripts/lib/cdp_ports.py +197 -0
  175. package/engine/scripts/lib/customer-env.mjs +102 -0
  176. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  177. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  178. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  179. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  180. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  181. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  182. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  183. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  184. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  185. package/engine/scripts/lib/patch-compose.mjs +241 -0
  186. package/engine/scripts/lib/patch-target.mjs +35 -0
  187. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  188. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  189. package/engine/scripts/lib/preset-compat.mjs +127 -0
  190. package/engine/scripts/lib/preset-install.mjs +575 -0
  191. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  192. package/engine/scripts/lib/preset-sync.mjs +59 -0
  193. package/engine/scripts/lib/profile-retire.mjs +97 -0
  194. package/engine/scripts/lib/python-runtime.mjs +190 -0
  195. package/engine/scripts/lib/registry.mjs +72 -0
  196. package/engine/scripts/lib/repo-root.sh +56 -0
  197. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  198. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  199. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  200. package/engine/scripts/lib/settings-panels.mjs +816 -0
  201. package/engine/scripts/lib/stdio_utf8.py +86 -0
  202. package/engine/scripts/lib/wake-auth.mjs +392 -0
  203. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  204. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  205. package/engine/scripts/license-e2e-drill.mjs +426 -0
  206. package/engine/scripts/license-gate.mjs +245 -0
  207. package/engine/scripts/lip_backlog.py +171 -0
  208. package/engine/scripts/lip_table.py +185 -0
  209. package/engine/scripts/local-usage.mjs +214 -0
  210. package/engine/scripts/migrate-providers.mjs +127 -0
  211. package/engine/scripts/migrate-skills.sh +65 -0
  212. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  213. package/engine/scripts/ml-store-ids.json +26 -0
  214. package/engine/scripts/ml_artifact_attribution.py +333 -0
  215. package/engine/scripts/ml_capacity_probe.py +1158 -0
  216. package/engine/scripts/ml_hub_status.py +1556 -0
  217. package/engine/scripts/ml_schema_check.py +115 -0
  218. package/engine/scripts/ml_store_ids.py +139 -0
  219. package/engine/scripts/npm-publish-argent.mjs +499 -0
  220. package/engine/scripts/npm-publish-argent.sh +20 -0
  221. package/engine/scripts/orchestrator.py +13811 -0
  222. package/engine/scripts/pack-plugins.mjs +140 -0
  223. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  224. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  225. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  226. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  227. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  228. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  229. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  230. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  231. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  232. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  233. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  234. package/engine/scripts/plan-role-rename.mjs +125 -0
  235. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  236. package/engine/scripts/plugin-composition.json +15 -0
  237. package/engine/scripts/poc-mcp-host.mjs +455 -0
  238. package/engine/scripts/poll-fulfillment.sh +293 -0
  239. package/engine/scripts/poll-invoice.sh +74 -0
  240. package/engine/scripts/profile-doctor.mjs +686 -0
  241. package/engine/scripts/provider-select.sh +70 -0
  242. package/engine/scripts/provider-switch.sh +146 -0
  243. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  244. package/engine/scripts/python-runtime.sh +78 -0
  245. package/engine/scripts/query_runner.py +310 -0
  246. package/engine/scripts/release-argent.sh +574 -0
  247. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  248. package/engine/scripts/run-tests.sh +256 -0
  249. package/engine/scripts/self-sign-license.mjs +179 -0
  250. package/engine/scripts/session-health-scan.mjs +63 -0
  251. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  252. package/engine/scripts/setup.sh +22 -0
  253. package/engine/scripts/skill-install.mjs +629 -0
  254. package/engine/scripts/sku_translate.py +148 -0
  255. package/engine/scripts/sync-plugins.sh +90 -0
  256. package/engine/scripts/sync-presets.sh +27 -0
  257. package/engine/scripts/sync-scripts.sh +445 -0
  258. package/engine/scripts/telemetry.py +873 -0
  259. package/engine/scripts/ui-copy-count.mjs +220 -0
  260. package/engine/scripts/usage-anchor.sh +136 -0
  261. package/engine/scripts/usage_report.py +531 -0
  262. package/engine/scripts/validate-preset-schema.mjs +208 -0
  263. package/engine/scripts/vertical-init.mjs +323 -0
  264. package/engine/scripts/vertical-lint.mjs +482 -0
  265. package/engine/scripts/wenyao-browser.sh +73 -0
  266. package/engine/scripts/wenyao-fixture-export.py +116 -0
  267. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  268. package/engine/scripts/wenyao-login.sh +34 -0
  269. package/engine/scripts/wenyao-template-map.json +292 -0
  270. package/engine/scripts/wenyao_attach.py +131 -0
  271. package/engine/scripts/wenyao_download_template.py +144 -0
  272. package/engine/scripts/wenyao_fill_template.py +568 -0
  273. package/engine/scripts/wenyao_login.py +145 -0
  274. package/engine/scripts/wenyao_portal.py +633 -0
  275. package/engine/scripts/wenyao_recon.py +230 -0
  276. package/engine/scripts/wenyao_run.py +503 -0
  277. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  278. package/engine/scripts/wenyao_skill_contract.py +156 -0
  279. package/engine/scripts/wenyao_template_fetch.py +175 -0
  280. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  281. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  282. package/engine/scripts/ziniao-keepalive.sh +216 -0
  283. package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
  284. package/engine/scripts/ziniao-windows.sh +153 -0
  285. package/engine/version.mjs +1 -0
  286. package/engine/verticals/README.md +103 -0
  287. package/engine/verticals/_engine-words.baseline.txt +237 -0
  288. package/engine/verticals/_engine-words.json +19 -0
  289. package/engine/verticals/mercadolibre/README.md +43 -0
  290. package/engine/verticals/mercadolibre/config.json +709 -0
  291. package/engine/verticals/mercadolibre/flows.json +706 -0
  292. package/engine/verticals/mercadolibre/manifest.json +538 -0
  293. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  294. package/engine/verticals/mercadolibre/preflight.py +143 -0
  295. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  296. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  297. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  298. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  299. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  300. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  301. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  302. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  303. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  304. package/package.json +21 -4
  305. package/README.md +0 -3
@@ -0,0 +1,522 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-docs-drift.mjs —— 文档漂移**只报告**检查(2026-10-03,Argent 文档一致性)
4
+ *
5
+ * 为什么有它:《Argent 产品化策划案(第三方可安装)》§4.3 点出过"把过期当现行"的两处口径
6
+ * (`docs/Argent-0.3.0-验收清单.md` 的"本机跑生产"、`docs/客户机部署与上线.md` 的客户机
7
+ * dsh 旧路径/旧版本),以及一类"版本号 / README 数字漂移"。人工复读文档不可持续 ⇒ 固化成
8
+ * 一条**只报告**的检查:只打印「文件:行 + 命中内容 + 为什么可疑」,**绝不修改任何文件**。
9
+ *
10
+ * 三条规则
11
+ * R1 旧 dsh 版本号当现行
12
+ * 出现 `0.1.<n>[-rc.<m>]` 且 ≠ **现役 dsh 版本**(判据 = `dsh-version-lock/` 里晋级过的
13
+ * 最高快照,可用 `--dsh-version` 覆盖;与 `~/.dsh/AGENTS.md`「版本现状」一致)⇒ 可疑。
14
+ * · 先过一道**真伪闸**:该命中必须"确实在说 dsh"——属于 `dsh-version-lock/` 的版本族,
15
+ * 或所在行含 `dsh` / `宿主` / `harness`。否则(如能力包版本 `daily-report-lite 0.1.46`)
16
+ * **不报**,避免把包版本误当宿主版本。
17
+ * **降级为信息**的情形(逐行判据,同一行出现即降级):
18
+ * · 历史字样:历史 / 升级记录 / 回滚 / 归档 / 当时 / 曾 / 旧版 / 遗留 / 退役 / 此前 …
19
+ * · 版本推移:写成 `0.1.1→0.1.5` 这种**过渡对**(不是"现役/目标"声明)
20
+ * · 行为沿革:`0.1.5 起根路径带 token 鉴权` 这种"自某版起的行为",不是版本声明
21
+ * · 快照/备份产物引用:`dsh-version-lock/0.1.5-rc.1.json`、`backup/dsh-0.1.5-…`
22
+ * · 位于 `docs/archive/`,或**文件名带日期**(`…-2026-09-29.md` = 阶段性记录),
23
+ * 或所在小节的标题带日期/历史/快照字样(整节降级)
24
+ * **反向规则**:命中附近 ±24 字符若有"这是现役"的声明词(当前/现役/现行/锁定…),
25
+ * 则**压过**同行的历史字样 —— 表里写「当前 = 旧版本」正是要抓的漂移。
26
+ * 代码块(```)里的 `# 注释` 不当标题;每行判据独立,不因上下文串味。
27
+ * R2 README 数字漂移
28
+ * `README.md` / `docs/README.md` 里「N 个 preset / N 个角色 / N 个技能 / N 条业务线」
29
+ * (含 `presets/ 下 N 个` 形态)与**实际计数**不符 ⇒ 可疑。
30
+ * 实际计数来源:`presets/<角色>/agent.cordis.yml`(或 `plugins/argent-presets/presets/<角色>.patch.yml`)、
31
+ * `skills/<技能>/SKILL.md`、`verticals/<业务线>/manifest.json`。
32
+ * R3 已废弃路径当现行
33
+ * `~/node/bin`、`$HOME/node/lib`、`/Users/<user>/node/`、裸 `~/node` 等**旧 node 前缀**
34
+ * (现役 = `~/.local/node/bin`),且不在历史语境 ⇒ 可疑。
35
+ * R4 宿主矩阵结构(`docs/Argent-宿主适配契约.md` §3 能力×宿主矩阵)
36
+ * **只判表格还成不成形,不判内容对错**(内容真伪是 review 的活,不是本检查的):
37
+ * · **表头列数 == 每一行的格数**(错位即报:2026-10-04 的在途改动把 ③ 行写成 7 格、
38
+ * 把 Android 的长文塞进 `MCP 云端` 格,而当时**四条门禁全绿** —— 它们一条都不读这张表的结构);
39
+ * · **无空标签**(整格为空 = `||` / `| |`,含行首"接口名"格与表头宿主名格);
40
+ * · **状态符号合法**(每格以 `✅` 已实现 / `🟡` 部分 / `⬜` 待接入 / `—` 不适用 开头,
41
+ * 允许前面带 `**` 粗体标记;图例见该文档 §3 首行)。
42
+ * ⇒ 报的是"第几行几格",改法只有一种:**让表格回到 N 列**(⛔ 不许靠删符号糊过去)。
43
+ *
44
+ * 用法
45
+ * node scripts/check-docs-drift.mjs # 只报告,**恒 exit 0**
46
+ * node scripts/check-docs-drift.mjs --strict # 有可疑条目才 exit 1(给人手动跑 / 单独 CI 用)
47
+ * node scripts/check-docs-drift.mjs --json # 机器可读
48
+ * node scripts/check-docs-drift.mjs --root <dir> # 换根目录(自测夹具用;仍然只读)
49
+ * node scripts/check-docs-drift.mjs --dsh-version <v># 覆盖现役 dsh 版本判据
50
+ * node scripts/check-docs-drift.mjs --strict --matrix-only
51
+ * # **只**跑 R4(CI 的 `host-matrix` 条用:
52
+ * # 失败时点名"结构错位"而不是"口径漂移" ——
53
+ * # 两类缺陷的修法完全不同,故各占一条判据)
54
+ *
55
+ * 纪律
56
+ * · **只读**:不改文件、不写台账/数据目录、不读凭据、不联网;扫描对象是 `docs/**\/*.md`
57
+ * (R1/R3)、两处 README(R2)与**一份**矩阵文档(R4,路径写死 = 契约 §3 的唯一落点)。
58
+ * · **默认不失败**:它**不接** `scripts/run-tests.sh` 的失败判据(文档漂移常年存在,
59
+ * 接进去就是恒红陷阱)。回归只覆盖 `scripts/tests/docs-drift.test.mjs` 的**纯夹具**。
60
+ * · **R4 例外说明**:R4 与 R1–R3 同档(默认报告、`--strict` 才红),但它是**结构性**判据 ——
61
+ * 接进 CI 的是 `--matrix-only` 那一条(见 `scripts/ci-run.sh#CRITERIA`)。
62
+ */
63
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
64
+ import { dirname, join, relative, resolve, sep } from 'node:path'
65
+ import { fileURLToPath } from 'node:url'
66
+
67
+ const HERE = dirname(fileURLToPath(import.meta.url))
68
+ const SELF_ROOT = resolve(HERE, '..')
69
+
70
+ // ── 参数 ────────────────────────────────────────────────────────────────────
71
+ const argv = process.argv.slice(2)
72
+ const has = (f) => argv.includes(f)
73
+ const val = (f) => {
74
+ const i = argv.indexOf(f)
75
+ return i >= 0 ? argv[i + 1] : undefined
76
+ }
77
+ if (has('-h') || has('--help')) {
78
+ console.log(readFileSync(fileURLToPath(import.meta.url), 'utf8').split('*/')[0].replace(/^\/\*\*?/, '').trim())
79
+ process.exit(0)
80
+ }
81
+ const STRICT = has('--strict')
82
+ const JSON_OUT = has('--json')
83
+ const MATRIX_ONLY = has('--matrix-only') // 只跑 R4(CI 的 host-matrix 条;见文件头「用法」)
84
+ const ROOT = resolve(val('--root') || SELF_ROOT)
85
+ const OVERRIDE_VERSION = val('--dsh-version')
86
+
87
+ // ── 判据(现役 dsh 版本 / 实际计数)─────────────────────────────────────────
88
+ /** semver-ish 比较:只处理 `0.1.x-rc.y`(本仓库实际形态)。 */
89
+ function cmpVersion(a, b) {
90
+ const p = (s) => {
91
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/.exec(s)
92
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3]), m[4] === undefined ? Number.MAX_SAFE_INTEGER : Number(m[4])] : null
93
+ }
94
+ const x = p(a)
95
+ const y = p(b)
96
+ if (!x || !y) return null
97
+ for (let i = 0; i < 4; i += 1) {
98
+ if (x[i] !== y[i]) return x[i] - y[i]
99
+ }
100
+ return 0
101
+ }
102
+
103
+ /** 现役 dsh 版本 = `dsh-version-lock/` 顶层晋级快照里最高的一版(排除 candidate/)。 */
104
+ function resolveActiveDshVersion(root) {
105
+ if (OVERRIDE_VERSION) return { version: OVERRIDE_VERSION, source: '--dsh-version 覆盖', all: [OVERRIDE_VERSION] }
106
+ const dir = join(root, 'dsh-version-lock')
107
+ if (!existsSync(dir)) return { version: null, source: '未找到 dsh-version-lock/(R1 跳过)', all: [] }
108
+ const versions = readdirSync(dir)
109
+ .filter((f) => f.endsWith('.json'))
110
+ .map((f) => f.replace(/\.json$/, ''))
111
+ .filter((v) => cmpVersion(v, v) !== null)
112
+ if (!versions.length) return { version: null, source: 'dsh-version-lock/ 无可用快照(R1 跳过)', all: [] }
113
+ versions.sort(cmpVersion)
114
+ const top = versions[versions.length - 1]
115
+ return { version: top, source: `dsh-version-lock/${top}.json`, all: versions }
116
+ }
117
+
118
+ function countPresets(root) {
119
+ const dir = join(root, 'presets')
120
+ if (existsSync(dir)) {
121
+ const n = readdirSync(dir, { withFileTypes: true })
122
+ .filter((e) => e.isDirectory() && existsSync(join(dir, e.name, 'agent.cordis.yml'))).length
123
+ if (n > 0) return { n, source: 'presets/*/agent.cordis.yml' }
124
+ }
125
+ const pdir = join(root, 'plugins', 'argent-presets', 'presets')
126
+ if (existsSync(pdir)) {
127
+ const n = readdirSync(pdir).filter((f) => f.endsWith('.patch.yml')).length
128
+ if (n > 0) return { n, source: 'plugins/argent-presets/presets/*.patch.yml' }
129
+ }
130
+ return { n: 0, source: '(未找到 preset 目录)' }
131
+ }
132
+
133
+ function countSkills(root) {
134
+ const dir = join(root, 'skills')
135
+ if (!existsSync(dir)) return { n: 0, source: '(未找到 skills/)' }
136
+ const n = readdirSync(dir, { withFileTypes: true })
137
+ .filter((e) => e.isDirectory() && existsSync(join(dir, e.name, 'SKILL.md'))).length
138
+ return { n, source: 'skills/*/SKILL.md' }
139
+ }
140
+
141
+ function countVerticals(root) {
142
+ const dir = join(root, 'verticals')
143
+ if (!existsSync(dir)) return { n: 0, source: '(未找到 verticals/)' }
144
+ const n = readdirSync(dir, { withFileTypes: true })
145
+ .filter((e) => e.isDirectory() && existsSync(join(dir, e.name, 'manifest.json'))).length
146
+ return { n, source: 'verticals/*/manifest.json' }
147
+ }
148
+
149
+ // ── 语境判据(降级用)──────────────────────────────────────────────────────
150
+ const HISTORY_RE = /历史|升级记录|升级日志|回滚|归档|当时|当年|曾经|曾|此前|旧版|旧前缀|过时|退役|遗留|撤销|已删除|删除|弃用|废弃|事故|教训|踩坑|复盘|deprecated|legacy/i
151
+ /** 明确的"这是现役"声明词:同行即使有历史字样,也不能拿它解释掉。 */
152
+ const CURRENCY_RE = /当前|现役|现行|目前|现在|正在用|锁定|正在跑/g
153
+ const ARTIFACT_RE = /快照|snapshot|backup|备份|离线包|\.tgz|\.tar\.gz/i
154
+ const WORD_CH = /[\w./@+-]/
155
+ const DATED_RE = /20\d\d-\d\d-\d\d/
156
+ const DSH_MENTION_RE = /dsh|宿主|harness/gi
157
+
158
+ /** 命中是否属于"路径/文件名的一部分"(快照产物引用,而不是版本声明)。 */
159
+ function isArtifactRef(line, idx, token) {
160
+ let s = idx
161
+ let e = idx + token.length
162
+ while (s > 0 && WORD_CH.test(line[s - 1])) s -= 1
163
+ while (e < line.length && WORD_CH.test(line[e])) e += 1
164
+ const word = line.slice(s, e)
165
+ return word.includes('/') || /\.(json|tgz|tar\.gz|zip|txt)$/.test(word)
166
+ }
167
+
168
+ /** 命中处附近(默认 ±radius 字符)有没有在谈 dsh / 宿主 / harness。 */
169
+ function nearDsh(line, idx, token, radius = 12) {
170
+ const lo = Math.max(0, idx - radius)
171
+ const hi = idx + token.length + radius
172
+ for (const m of line.matchAll(DSH_MENTION_RE)) if (m.index >= lo && m.index <= hi) return true
173
+ return false
174
+ }
175
+
176
+ /** 能力包 / 业务能力版本也用 `0.1.x` 形状(`daily-report-lite 0.1.46`、`hr 0.1.37`)——不是宿主版本。 */
177
+ const PACK_NAME_RE = /(?:daily-report-lite|lite|hr|invoice|full|daily-report|pack|capability|能力包)\s*[:=]?\s*$/i
178
+ function looksLikePackVersion(line, idx) {
179
+ return PACK_NAME_RE.test(line.slice(Math.max(0, idx - 32), idx))
180
+ }
181
+
182
+ /** 一行里的降级理由;返回 null = 不得降级(可疑)。 */
183
+ function infoReason(line, idx, token) {
184
+ if (HISTORY_RE.test(line)) return '行内含历史字样(历史/升级记录/回滚/归档/当时/旧版/事故教训…)'
185
+ const before = line.slice(Math.max(0, idx - 16), idx)
186
+ const rawAfter = line.slice(idx + token.length, idx + token.length + 16)
187
+ const after = rawAfter.replace(/^[\s`'"(())\]]*/, '') // 去掉反引号/引号/括号等包裹
188
+ if (/→|->/.test(before) || /^(?:→|->)/.test(after)) return '版本推移记录(`A→B`),不是"现役/目标"声明'
189
+ if (/(?:>=|<=|>|<|~|\^)\s*$/.test(before)) return '版本约束/范围表达式(如 `dsh >=0.1.5 <0.2`),不是"现役版本"声明'
190
+ if (/^(?:\+|起|CLI|禁止|不支持|支持|以上|以下|之后|之前|及以后|开始|兼容)/.test(after)) {
191
+ return '版本作用域行为陈述(`0.1.5+` / `0.1.5 起` / `0.1.5 禁止…`),不是版本声明'
192
+ }
193
+ if (/(?:自|从)\s*$/.test(before)) return '行为沿革("自某版起…"),不是版本声明'
194
+ if (isArtifactRef(line, idx, token) || ARTIFACT_RE.test(line)) return '快照 / 备份产物引用(不是现行版本口径)'
195
+ return null
196
+ }
197
+
198
+ /** 命中附近(±24 字符)有没有"这是现役"的声明词。 */
199
+ function currencyClaim(line, idx, token, radius = 24) {
200
+ for (const m of line.matchAll(CURRENCY_RE)) {
201
+ const dist = Math.min(
202
+ Math.abs(m.index - (idx + token.length)),
203
+ Math.abs(idx - (m.index + m[0].length)),
204
+ )
205
+ if (dist <= radius) return true
206
+ }
207
+ return false
208
+ }
209
+
210
+ /** 小节语境:所在标题栈里有带日期 / 历史字样 / 快照字样的标题 ⇒ 整节降级。 */
211
+ function sectionHeadingReason(stack) {
212
+ for (const h of stack) {
213
+ const title = h.text.trim()
214
+ if (DATED_RE.test(title)) return `所在小节标题带日期(\`${clip(title, 40)}\` = 阶段性记录)`
215
+ if (HISTORY_RE.test(title)) return `所在小节是历史/复盘小节(\`${clip(title, 40)}\`)`
216
+ if (ARTIFACT_RE.test(title)) return `所在小节讲快照/备份产物(\`${clip(title, 40)}\`)`
217
+ }
218
+ return null
219
+ }
220
+
221
+ // ── 规则 ───────────────────────────────────────────────────────────────────
222
+ const VERSION_RE = /(?<![\d.])0\.1\.(\d+)(?:-rc\.(\d+))?(?!\d)/g
223
+ const DATED_DOC_RE = /20\d\d-\d\d-\d\d/
224
+
225
+ /**
226
+ * 这个 `0.1.x` 命中是否**真的在说 dsh 版本**?
227
+ * 不设这道闸就会把能力包版本(`daily-report-lite 0.1.46`、`hr 0.1.0`)误报成 dsh 漂移。
228
+ * 判据:① 带 `-rc.N` 后缀 —— dsh 的版本命名(能力包不用 rc 后缀);
229
+ * ② 裸版本号则要求**紧邻**(±12 字符)dsh / 宿主 / harness("dsh 0.1.5"、"宿主 0.1.5");
230
+ * ③ 命中前紧挨着能力包名(`lite 0.1.1`、`hr 0.1.0`)⇒ 一律不算 dsh 版本。
231
+ */
232
+ function isDshVersionToken(token, line, idx) {
233
+ if (looksLikePackVersion(line, idx)) return false
234
+ if (/-rc\.\d+$/.test(token)) return true
235
+ return nearDsh(line, idx, token)
236
+ }
237
+ const DEPRECATED_PATHS = [
238
+ { re: /(?:\$HOME|~|\/Users\/[\w.-]+)\/node\/(?:bin|lib)\b/g, label: '旧 node 前缀路径(`~/node/bin` / `~/node/lib`)' },
239
+ { re: /\/Users\/[\w.-]+\/node\//g, label: '旧 node 前缀绝对路径(`/Users/<user>/node/`)' },
240
+ { re: /(?:\$HOME|~)\/node(?![\w/.-])/g, label: '旧 node 前缀(裸 `~/node`)' },
241
+ ]
242
+
243
+ function clip(s, max = 160) {
244
+ const t = s.replace(/\s+/g, ' ').trim()
245
+ return t.length <= max ? t : `${t.slice(0, max - 1)}…`
246
+ }
247
+
248
+ function walkMd(dir, out = []) {
249
+ if (!existsSync(dir)) return out
250
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
251
+ if (e.name.startsWith('.')) continue
252
+ const p = join(dir, e.name)
253
+ if (e.isDirectory()) walkMd(p, out)
254
+ else if (e.isFile() && e.name.endsWith('.md')) out.push(p)
255
+ }
256
+ return out
257
+ }
258
+
259
+ const findings = []
260
+ const push = (rule, level, file, line, text, token, why) =>
261
+ findings.push({ rule, level, file, line, token, text: clip(text), why })
262
+
263
+ function scanVersionsAndPaths(file, rel) {
264
+ const text = readFileSync(file, 'utf8')
265
+ const lines = text.split('\n')
266
+ const inArchive = rel.split(sep).includes('archive')
267
+ const dated = DATED_DOC_RE.test(rel.split(sep).pop() || '')
268
+ const fileReason = inArchive ? 'docs/archive/ 内(按定义是历史文档)'
269
+ : dated ? '文件名带日期(阶段性记录,不是现行口径文档)' : null
270
+ const headings = [] // 标题栈:{ level, text }
271
+ let inFence = false
272
+ lines.forEach((line, i) => {
273
+ const lineNo = i + 1
274
+ if (/^\s*(```|~~~)/.test(line)) { // 代码块里的 `# 注释` 不是标题
275
+ inFence = !inFence
276
+ return
277
+ }
278
+ const h = inFence ? null : /^(#{1,6})\s+(.*)$/.exec(line)
279
+ if (h) {
280
+ const level = h[1].length
281
+ while (headings.length && headings[headings.length - 1].level >= level) headings.pop()
282
+ headings.push({ level, text: h[2] })
283
+ return
284
+ }
285
+ const sectionReason = fileReason || sectionHeadingReason(headings)
286
+ // R1
287
+ if (ACTIVE.version) {
288
+ for (const m of line.matchAll(VERSION_RE)) {
289
+ const token = m[0]
290
+ const idx = m.index
291
+ if (!isDshVersionToken(token, line, idx)) continue
292
+ const base = `0.1.${m[1]}`
293
+ const isActive = token === ACTIVE.version || (m[2] === undefined && ACTIVE.version.startsWith(`${base}-`))
294
+ if (isActive) continue
295
+ const lineReason = infoReason(line, idx, token)
296
+ const currency = currencyClaim(line, idx, token)
297
+ // 现役声明词(当前/现役/锁定…)**压过**同行的历史字样:表里写"当前 = 旧版本"就是漂移。
298
+ const reason = fileReason || sectionReason || (lineReason && !currency ? lineReason : null)
299
+ if (reason) push('R1', 'info', rel, lineNo, line, token, reason)
300
+ else push('R1', 'suspect', rel, lineNo, line, token, currency && lineReason
301
+ ? `现役 dsh 是 ${ACTIVE.version}(判据 ${ACTIVE.source}),此行含现役声明词(当前/现役/锁定)并把 ${token} 当现行版本`
302
+ : `现役 dsh 是 ${ACTIVE.version}(判据 ${ACTIVE.source}),此行把 ${token} 当现行/目标版本,且行内无历史语境`)
303
+ }
304
+ }
305
+ // R3
306
+ const taken = []
307
+ for (const { re, label } of DEPRECATED_PATHS) {
308
+ for (const m of line.matchAll(re)) {
309
+ const s = m.index
310
+ const e = s + m[0].length
311
+ if (taken.some(([a, b]) => s < b && e > a)) continue
312
+ taken.push([s, e])
313
+ const reason = sectionReason || (HISTORY_RE.test(line) ? '行内含历史字样(归档/删除/旧前缀…)' : null)
314
+ if (reason) push('R3', 'info', rel, lineNo, line, m[0], reason)
315
+ else push('R3', 'suspect', rel, lineNo, line, m[0],
316
+ `${label} 已废弃(现役 = \`~/.local/node/bin\`;旧前缀 \`~/node\` 已于 2026-10-03 归档删除),此行把它当现行路径`)
317
+ }
318
+ }
319
+ })
320
+ }
321
+
322
+ const NUMBER_PATTERNS = [
323
+ { re: /(\d+)\s*个\s*[Pp]resets?/g, what: 'preset 数', pick: (c) => c.presets },
324
+ { re: /presets\/`?\s*下\s*(\d+)\s*个/g, what: 'preset 数', pick: (c) => c.presets },
325
+ { re: /(\d+)\s*个角色/g, what: '角色数(一角色一 preset)', pick: (c) => c.presets },
326
+ { re: /(\d+)\s*个技能/g, what: '技能数', pick: (c) => c.skills },
327
+ { re: /(\d+)\s*[条个]业务线/g, what: '业务线数', pick: (c) => c.verticals },
328
+ ]
329
+
330
+ function scanReadmeNumbers(file, rel, counts) {
331
+ const lines = readFileSync(file, 'utf8').split('\n')
332
+ lines.forEach((line, i) => {
333
+ for (const { re, what, pick } of NUMBER_PATTERNS) {
334
+ for (const m of line.matchAll(re)) {
335
+ const claimed = Number(m[1])
336
+ const { n, source } = pick(counts)
337
+ if (claimed === n) continue
338
+ const why = `README 写「${what} = ${claimed}」,实际 ${n}(计数来源:${source})`
339
+ if (HISTORY_RE.test(line)) push('R2', 'info', rel, i + 1, line, m[0], `${why};但行内含历史字样 ⇒ 降级`)
340
+ else push('R2', 'suspect', rel, i + 1, line, m[0], why)
341
+ }
342
+ }
343
+ })
344
+ }
345
+
346
+ // ── R4:宿主矩阵结构 ────────────────────────────────────────────────────────
347
+ /** 矩阵的唯一落点(契约 §3)。⛔ 不扫别的文档 —— 矩阵**只此一处**(§4 SOP:新宿主只加一列)。 */
348
+ const HOST_MATRIX_REL = 'docs/Argent-宿主适配契约.md'
349
+ const MATRIX_HEADER_PREFIX = '| 接口' // 表头首格 = `接口 \ 宿主`
350
+ const STATUS_SYMBOLS = ['✅', '🟡', '⬜', '—']
351
+
352
+ /** Markdown 表格行 ⇒ 单元格数组(去首尾空段;不是表格行 ⇒ null)。 */
353
+ function cellsOf(line) {
354
+ const s = line.trim()
355
+ if (!s.startsWith('|')) return null
356
+ const body = s.endsWith('|') ? s.slice(0, -1) : s
357
+ return body.slice(1).split('|').map((c) => c.trim())
358
+ }
359
+
360
+ /** `|---|---|` 形态的分隔行。 */
361
+ const isSeparatorCells = (cells) => cells.length > 0 && cells.every((c) => /^:?-{2,}:?$/.test(c))
362
+
363
+ /** 状态符号判据:允许前置 `**` / `*` / 反引号 / 空白,第一个**实义字符**必须是四个符号之一。
364
+ * ⚠️ 必须按**码点**取首字符:`✅`/`🟡`/`⬜` 在 UTF-16 里是代理对(2 个 code unit),
365
+ * 用 `[\s\S]` 会只取到半个代理 ⇒ 恒判非法(本规则首版就踩了这个,见 2026-10-06 记录)。 */
366
+ function statusSymbolOf(cell) {
367
+ return Array.from(cell.replace(/^[\s*`>]+/, ''))[0] || ''
368
+ }
369
+
370
+ /**
371
+ * R4:`docs/Argent-宿主适配契约.md` §3 能力×宿主矩阵的**结构**判据。
372
+ * 三条:① 每行列数 == 表头列数(含分隔行)② 无空标签 ③ 状态符号合法。
373
+ * 返回该文件的统计(给 --json 与汇总行用);文件不存在 ⇒ null(夹具仓库不必带这份文档)。
374
+ */
375
+ function scanHostMatrix(root, rel) {
376
+ const file = join(root, rel)
377
+ if (!existsSync(file)) return null
378
+ const lines = readFileSync(file, 'utf8').split('\n')
379
+ const headIdx = lines.findIndex((l) => l.trim().startsWith(MATRIX_HEADER_PREFIX))
380
+ if (headIdx < 0) {
381
+ push('R4', 'suspect', rel, 1, '', '', `找不到 §3 矩阵表头(首格应为 \`${MATRIX_HEADER_PREFIX} \\ 宿主\`)—— 表格被改名/删除后本检查就瞎了`)
382
+ return { found: false, headerCols: 0, dataRows: 0, defects: 1 }
383
+ }
384
+ const header = cellsOf(lines[headIdx])
385
+ const cols = header.length
386
+ let dataRows = 0
387
+ let defects = 0
388
+ const bad = (lineNo, text, why, token = '') => { defects += 1; push('R4', 'suspect', rel, lineNo, text, token, why) }
389
+
390
+ // 表头自己的空标签(宿主名不得留空)
391
+ header.forEach((c, j) => {
392
+ if (!c) bad(headIdx + 1, lines[headIdx], `表头第 ${j + 1} 格是空标签(宿主名/接口名不得留空)`)
393
+ })
394
+
395
+ for (let i = headIdx + 1; i < lines.length; i += 1) {
396
+ const raw = lines[i]
397
+ if (!raw.trim().startsWith('|')) break // 表格结束
398
+ const cells = cellsOf(raw)
399
+ const lineNo = i + 1
400
+ const sep = isSeparatorCells(cells)
401
+ if (cells.length !== cols) {
402
+ const delta = cells.length - cols
403
+ bad(lineNo, raw, `表格错位:本行 ${cells.length} 格,表头 ${cols} 格(${delta > 0 ? `多 ${delta}` : `少 ${-delta}`} 格)`
404
+ + ` ⇒ 该行以下所有格的内容都串位了(修法:让本行回到 ${cols} 格,⛔ 不要靠删状态符号糊过去)`)
405
+ continue
406
+ }
407
+ if (sep) continue
408
+ const isEmptyLabelRow = cells.every((c) => c === '')
409
+ if (isEmptyLabelRow) { bad(lineNo, raw, '整行所有格都为空(空标签行)'); continue }
410
+ dataRows += 1
411
+ cells.forEach((c, j) => {
412
+ if (!c) { bad(lineNo, raw, `第 ${j + 1} 格是空标签(${j === 0 ? '行首"接口名"格' : '宿主格'}不得留空)`); return }
413
+ if (j === 0) return // 第 1 格 = 接口名,不是状态格
414
+ const sym = statusSymbolOf(c)
415
+ if (!STATUS_SYMBOLS.includes(sym)) {
416
+ bad(lineNo, raw, `第 ${j + 1} 格的状态符号非法:以 ${JSON.stringify(sym)} 开头,`
417
+ + `合法符号只有 ${STATUS_SYMBOLS.join(' / ')}(图例见该文档 §3)`, sym)
418
+ }
419
+ })
420
+ }
421
+ if (dataRows === 0) {
422
+ bad(headIdx + 1, lines[headIdx], '表头下面没有数据行(矩阵被清空?)')
423
+ }
424
+ return { found: true, headerCols: cols, dataRows, defects }
425
+ }
426
+
427
+
428
+ // ── 执行 ───────────────────────────────────────────────────────────────────
429
+ const ACTIVE = resolveActiveDshVersion(ROOT)
430
+ const counts = { presets: countPresets(ROOT), skills: countSkills(ROOT), verticals: countVerticals(ROOT) }
431
+
432
+ const docsDir = join(ROOT, 'docs')
433
+ const docsFiles = MATRIX_ONLY ? [] : walkMd(docsDir)
434
+ .map((p) => relative(ROOT, p))
435
+ .sort()
436
+ for (const rel of docsFiles) scanVersionsAndPaths(join(ROOT, rel), rel)
437
+
438
+ const readmes = MATRIX_ONLY ? [] : ['README.md', join('docs', 'README.md')].filter((r) => existsSync(join(ROOT, r)))
439
+ for (const rel of readmes) scanReadmeNumbers(join(ROOT, rel), rel, counts)
440
+
441
+ // R4:宿主矩阵结构(**不随 --matrix-only 开关,永远跑** —— 它是结构性判据,任何模式下都该看得见)
442
+ const matrix = scanHostMatrix(ROOT, HOST_MATRIX_REL)
443
+
444
+ const suspects = findings.filter((f) => f.level === 'suspect')
445
+ const infos = findings.filter((f) => f.level === 'info')
446
+ const byRule = (lvl, rule) => findings.filter((f) => f.level === lvl && f.rule === rule).length
447
+ const RULE_TITLE = {
448
+ R1: 'R1 旧 dsh 版本号当现行', R2: 'R2 README 数字漂移',
449
+ R3: 'R3 已废弃路径当现行', R4: 'R4 宿主矩阵结构(列数/空标签/状态符号)',
450
+ }
451
+ const INFO_PRINT_PER_RULE = 12 // 每条规则的信息项逐条打印上限(避免刷屏;全量用 --json)
452
+
453
+ /** 矩阵结构汇总行(两个 CI 判据共用;`host-matrix` 的 expect 命中它)。 */
454
+ function matrixSummary() {
455
+ if (!matrix) return `矩阵结构:跳过(${HOST_MATRIX_REL} 不存在)`
456
+ if (!matrix.found) return '矩阵结构:表头缺失(见上)'
457
+ return `矩阵结构:${matrix.defects} 处(表头 ${matrix.headerCols} 列 | 数据行 ${matrix.dataRows})`
458
+ }
459
+
460
+ if (JSON_OUT) {
461
+ console.log(JSON.stringify({
462
+ root: ROOT,
463
+ activeDshVersion: ACTIVE.version,
464
+ activeDshVersionSource: ACTIVE.source,
465
+ docsScanned: docsFiles.length,
466
+ matrixOnly: MATRIX_ONLY,
467
+ counts: Object.fromEntries(Object.entries(counts).map(([k, v]) => [k, v.n])),
468
+ matrix,
469
+ suspects: suspects.length,
470
+ infos: infos.length,
471
+ findings,
472
+ }, null, 2))
473
+ } else {
474
+ console.log('check-docs-drift —— 文档漂移**只报告**检查(不改任何文件)')
475
+ console.log(` 仓库根:${ROOT}`)
476
+ if (MATRIX_ONLY) {
477
+ console.log(' 模式:--matrix-only(**只**跑 R4 宿主矩阵结构;R1/R2/R3 本轮未跑)')
478
+ } else {
479
+ console.log(` 扫描:docs/**/*.md ${docsFiles.length} 个(archive/ 按历史语境降级)|README 数字:${readmes.join(' · ') || '(无)'}`)
480
+ }
481
+ console.log(` 现役 dsh 版本(R1 判据):${ACTIVE.version || '未知'} ← ${ACTIVE.source}`)
482
+ console.log(` 实际计数(R2 判据):preset ${counts.presets.n} · 技能 ${counts.skills.n} · 业务线 ${counts.verticals.n}`)
483
+ console.log(` ${matrixSummary()} ← 文档:${HOST_MATRIX_REL}`)
484
+ console.log('')
485
+ console.log(`⚠️ 可疑 ${suspects.length} 条`)
486
+ for (const rule of ['R1', 'R2', 'R3', 'R4']) {
487
+ const list = suspects.filter((f) => f.rule === rule)
488
+ if (!list.length) continue
489
+ console.log(` ── ${RULE_TITLE[rule]}(${list.length} 条)`)
490
+ for (const f of list) {
491
+ console.log(` ${f.file}:${f.line}`)
492
+ console.log(` 命中:${f.token}`)
493
+ console.log(` 原文:${f.text}`)
494
+ console.log(` 为什么可疑:${f.why}`)
495
+ }
496
+ }
497
+ console.log('')
498
+ console.log(`ℹ️ 信息(历史/沿革语境,不计入可疑)${infos.length} 条`)
499
+ let infoSkipped = 0
500
+ for (const rule of ['R1', 'R2', 'R3', 'R4']) {
501
+ const list = infos.filter((f) => f.rule === rule)
502
+ if (!list.length) continue
503
+ console.log(` ── ${RULE_TITLE[rule]}(${list.length} 条)`)
504
+ list.forEach((f, i) => {
505
+ if (i >= INFO_PRINT_PER_RULE) { infoSkipped += 1; return }
506
+ console.log(` ${f.file}:${f.line} 「${f.token}」 — ${f.why}`)
507
+ })
508
+ }
509
+ if (infoSkipped) console.log(` …另有 ${infoSkipped} 条信息未逐条打印(需要全量看 --json)`)
510
+ console.log('')
511
+ console.log(` =============================================`)
512
+ console.log(` ${matrixSummary()}`)
513
+ console.log(` 可疑 ${suspects.length} 条(R1=${byRule('suspect', 'R1')} / R2=${byRule('suspect', 'R2')} / R3=${byRule('suspect', 'R3')} / R4=${byRule('suspect', 'R4')})`
514
+ + `|信息 ${infos.length} 条(R1=${byRule('info', 'R1')} / R2=${byRule('info', 'R2')} / R3=${byRule('info', 'R3')} / R4=${byRule('info', 'R4')})`)
515
+ console.log(` 如何修:现行口径写**现役值**(dsh ${ACTIVE.version || '<现役版本>'}、路径 \`~/.local/node/bin\`、README 数字按实际计数),`
516
+ + `历史内容**不要删**、只补「历史/升级记录」字样即可让本检查降级为信息;`
517
+ + `矩阵结构(R4)另有修法:让出问题的行**回到表头的列数**(内容一格都不许丢)。`)
518
+ console.log(` 本检查只报告:默认 exit 0;要当门禁用 --strict(有可疑条才非零);只查矩阵用 --strict --matrix-only。`)
519
+ console.log(` =============================================`)
520
+ }
521
+
522
+ process.exit(STRICT && suspects.length > 0 ? 1 : 0)
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ # check-dsh-web-expose.sh — 检查 dsh web 是否已对局域网/组网暴露(方案 C:--host 0.0.0.0)
3
+ #
4
+ # 背景:dsh web 默认只绑 127.0.0.1;`/api` 还有一道**浏览器信任围栏**
5
+ # (`dsh-client-connection` 的 isTrustedApiRequest):非回环 Host 且不在 trustedHosts 里 → **403**。
6
+ # 而 trustedHosts 仅在 `--host 0.0.0.0` 时自动填入本机所有非回环 IPv4(`resolveLanTrust`)。
7
+ #
8
+ # 所以暴露成功要同时满足:① 监听在 0.0.0.0(或指定网卡)② 目标 Host 在信任表里 ③ token 对。
9
+ #
10
+ # ⑩b(2026-10-08):取 token 的日志落点**不再写死 `/tmp/dsh-web.log`**,也不在取不到时打印
11
+ # 占位提示(那是**半静默**:看的人会以为"只是没取到",实际是链路断了)。候选顺序的唯一真源是
12
+ # `scripts/lib/wake-auth.mjs#logPaths()`(`node …/wake-auth.mjs --log-paths`):
13
+ # DSH_WEB_LOG → plist 的 StandardOutPath → $DSH_HOME/dsh-web.log → ~/Library/Logs/dsh-web.log
14
+ # → /tmp/dsh-web.log(历史兜底,排最后);逐项试、**命中即停**。
15
+ # 全部落空 ⇒ 第 3 段**响亮失败**:非零退出 + 点名每个落点与分类 + 修法。
16
+ #
17
+ # 用法:bash scripts/check-dsh-web-expose.sh [port]
18
+ # 退出码:0 三段都拿到证据;1 取不到 token(第 3 段失败,点名列过的落点与修法)
19
+ set -uo pipefail
20
+ PORT="${1:-3080}"
21
+
22
+ SELF="${BASH_SOURCE[0]}"
23
+ . "$(dirname "${SELF}")/lib/repo-root.sh"
24
+ REPO="$(argent_repo_root "${SELF}")"
25
+ WAKE_AUTH="${REPO}/scripts/lib/wake-auth.mjs"
26
+
27
+ # ── 日志落点链(唯一真源):逐项试、命中即停 ─────────────────────────────────────
28
+ CANDS="$(node "${WAKE_AUTH}" --log-paths 2>&1)"
29
+ NODE_RC=$?
30
+ LOG=""
31
+ TRIED=""
32
+ if [ "${NODE_RC}" -ne 0 ] || [ -z "${CANDS}" ]; then
33
+ TRIED=" ❌ (候选表都取不到:node ${WAKE_AUTH} --log-paths exit=${NODE_RC})"$'\n'
34
+ else
35
+ while IFS= read -r p; do
36
+ [ -n "${p}" ] || continue
37
+ if [ ! -f "${p}" ]; then
38
+ TRIED="${TRIED} ❌ ${p}:文件不存在(日志被系统清理或被运维挪走)"$'\n'
39
+ continue
40
+ fi
41
+ if grep -q "dsh web: http" "${p}" 2>/dev/null; then LOG="${p}"; break; fi
42
+ TRIED="${TRIED} ❌ ${p}:有文件,但没有「dsh web: http」那一行(多半是别的进程/旧日志)"$'\n'
43
+ done <<< "${CANDS}"
44
+ fi
45
+
46
+ echo "=== 1. 监听绑定 ==="
47
+ lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN 2>/dev/null | sed -n '1,4p'
48
+ bind=$(lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN 2>/dev/null | awk 'NR==2{print $9}')
49
+ echo " → ${bind:-(未监听)}"
50
+ case "${bind}" in
51
+ *0.0.0.0*) echo " ✅ 已绑所有网卡:局域网 + 组网都能连" ;;
52
+ *127.0.0.1*) echo " ❌ 仍只绑回环 —— LaunchAgent 的 --host 没生效。" ;;
53
+ "") echo " ⚠️ 没在监听:服务没起来,先跑 bash ${REPO}/scripts/dsh-restart.sh(它会打印日志落点)" ;;
54
+ *) echo " ℹ️ 绑在指定网卡上:${bind}" ;;
55
+ esac
56
+
57
+ echo
58
+ echo "=== 2. Host 信任围栏(/api)==="
59
+ ips=$(node -e "const os=require('os');console.log(Object.values(os.networkInterfaces()).flat().filter(i=>i&&i.family==='IPv4'&&!i.internal).map(i=>i.address).join(' '))" 2>/dev/null)
60
+ for h in 127.0.0.1 ${ips:-}; do
61
+ code=$(curl -s -o /dev/null -w '%{http_code}' -m 5 -H "Host: ${h}:${PORT}" "http://127.0.0.1:${PORT}/api" 2>/dev/null || echo 000)
62
+ case "${code}" in
63
+ 403) verdict="❌ 被围栏拒绝(该 Host 不在信任表)" ;;
64
+ 000) verdict="⚠️ 连不上" ;;
65
+ *) verdict="✅ 过了围栏(${code} = 缺 token,属正常)" ;;
66
+ esac
67
+ printf " Host=%-20s /api → %s %s\n" "${h}:${PORT}" "${code}" "${verdict}"
68
+ done
69
+
70
+ echo
71
+ echo "=== 3. 手机该打开的地址 ==="
72
+ if [ -z "${LOG}" ]; then
73
+ {
74
+ echo "❌ 取不到 dsh web 的启动 token:候选日志**全部不可用**"
75
+ printf '%s' "${TRIED}"
76
+ echo " 修法(按代价从低到高):"
77
+ echo " · 日志若被挪走,本链会自动跟随 plist 的声明:plutil -p ~/Library/LaunchAgents/com.whyshu.dsh-web.plist | grep -i path"
78
+ echo " · 重启并打印落点:bash ${REPO}/scripts/dsh-restart.sh"
79
+ echo " · 或显式指定落点(冒号分隔可给多个):export DSH_WEB_LOG=<日志路径> / export ARGENT_WAKE_LOG_PATHS=<路径1>:<路径2>"
80
+ echo " · 候选表由 scripts/lib/wake-auth.mjs#logPaths() 给出:node ${WAKE_AUTH} --log-paths"
81
+ echo " · ⛔ 这里**不再**打印占位提示:取不到 token 时「手机该打开的地址」是**不可知**的,不是让人自己填"
82
+ } >&2
83
+ exit 1
84
+ fi
85
+ tok=$(grep -o 'token=[A-Za-z0-9_-]*' "${LOG}" 2>/dev/null | tail -1 | cut -d= -f2)
86
+ if [ -z "${tok}" ]; then
87
+ {
88
+ echo "❌ 命中日志 ${LOG},但里面没有可用的 token(取到了文件、解析不出 = 另一种故障)"
89
+ echo " 修法:确认它是**当前进程**的启动日志:grep -c 'dsh web: http' '${LOG}';必要时 bash ${REPO}/scripts/dsh-restart.sh"
90
+ } >&2
91
+ exit 1
92
+ fi
93
+ for h in ${ips:-}; do
94
+ echo " http://${h}:${PORT}/?token=${tok}"
95
+ done
96
+ echo
97
+ echo "说明:token 每次启动都会变;首次访问带 token,之后浏览器有 30 天 cookie。"
98
+ echo " 本次 token 取自:${LOG}(候选链见 node ${WAKE_AUTH} --log-paths)"