@whyshu/argent-engine 0.0.0-stage → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/MANIFEST.json +341 -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 +576 -0
  55. package/engine/scripts/billing-mode-daily.sh +13 -0
  56. package/engine/scripts/build-engine-package.mjs +126 -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 +378 -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/collect-evidence.mjs +42 -0
  81. package/engine/scripts/config-explain.mjs +278 -0
  82. package/engine/scripts/configure-provider.rb +29 -0
  83. package/engine/scripts/customer-config-migrate.mjs +179 -0
  84. package/engine/scripts/customer-preflight.py +892 -0
  85. package/engine/scripts/customer-readiness.sh +135 -0
  86. package/engine/scripts/daily-report-inbox.py +242 -0
  87. package/engine/scripts/daily-report-reconcile.mjs +312 -0
  88. package/engine/scripts/daily-report.mjs +1310 -0
  89. package/engine/scripts/daily-report.sh +47 -0
  90. package/engine/scripts/data/lip-product-dict.json +306 -0
  91. package/engine/scripts/deliver-customer.mjs +932 -0
  92. package/engine/scripts/delivery-faces.txt +31 -0
  93. package/engine/scripts/desktop-trial.sh +146 -0
  94. package/engine/scripts/dev-window.sh +261 -0
  95. package/engine/scripts/drill-clean-machine.sh +315 -0
  96. package/engine/scripts/dsh-api-client.mjs +215 -0
  97. package/engine/scripts/dsh-backup-scope.sh +83 -0
  98. package/engine/scripts/dsh-offline-pack.mjs +77 -0
  99. package/engine/scripts/dsh-overlay-pin.sh +87 -0
  100. package/engine/scripts/dsh-plane.mjs +153 -0
  101. package/engine/scripts/dsh-portforward.py +223 -0
  102. package/engine/scripts/dsh-preflight.mjs +179 -0
  103. package/engine/scripts/dsh-release-watch-daily.sh +19 -0
  104. package/engine/scripts/dsh-release-watch.mjs +458 -0
  105. package/engine/scripts/dsh-remote-check.sh +99 -0
  106. package/engine/scripts/dsh-restart.sh +179 -0
  107. package/engine/scripts/dsh-restore.sh +111 -0
  108. package/engine/scripts/dsh-scope-rewire.mjs +803 -0
  109. package/engine/scripts/dsh-snapshot.mjs +116 -0
  110. package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
  111. package/engine/scripts/dsh-upgrade.mjs +612 -0
  112. package/engine/scripts/dsh-url.sh +86 -0
  113. package/engine/scripts/dsh-web-launch.mjs +155 -0
  114. package/engine/scripts/ensure-store-windows.py +509 -0
  115. package/engine/scripts/entitlements.py +458 -0
  116. package/engine/scripts/env_probe.py +411 -0
  117. package/engine/scripts/feishu-card.mjs +252 -0
  118. package/engine/scripts/final-check.mjs +43 -0
  119. package/engine/scripts/fix-skill-frontmatter.py +209 -0
  120. package/engine/scripts/flow-auto-retry.py +454 -0
  121. package/engine/scripts/flow-requirements.json +735 -0
  122. package/engine/scripts/flow-scaffold.py +275 -0
  123. package/engine/scripts/flow-timers-plist.sh +87 -0
  124. package/engine/scripts/flow-timers.sh +184 -0
  125. package/engine/scripts/flow-trigger-wake.mjs +284 -0
  126. package/engine/scripts/flow-trigger.sh +571 -0
  127. package/engine/scripts/flow_billing.py +354 -0
  128. package/engine/scripts/flow_capability_stats.py +314 -0
  129. package/engine/scripts/flow_case.py +194 -0
  130. package/engine/scripts/flow_claim.py +497 -0
  131. package/engine/scripts/flow_config.py +1025 -0
  132. package/engine/scripts/flow_failure.py +588 -0
  133. package/engine/scripts/flow_learn.py +105 -0
  134. package/engine/scripts/flow_ledger.py +235 -0
  135. package/engine/scripts/flow_policy.py +238 -0
  136. package/engine/scripts/flow_preflight.py +339 -0
  137. package/engine/scripts/flow_registry.py +341 -0
  138. package/engine/scripts/flow_retry.py +304 -0
  139. package/engine/scripts/flow_return_to_ops.py +191 -0
  140. package/engine/scripts/flow_source.py +315 -0
  141. package/engine/scripts/flow_sources/__init__.py +53 -0
  142. package/engine/scripts/flow_sources/cases_json.py +106 -0
  143. package/engine/scripts/flow_sources/common.py +428 -0
  144. package/engine/scripts/flow_sources/feishu_base.py +273 -0
  145. package/engine/scripts/flow_stale.py +185 -0
  146. package/engine/scripts/flow_status_reconcile.py +344 -0
  147. package/engine/scripts/flow_triage.py +849 -0
  148. package/engine/scripts/flow_write_verify.py +496 -0
  149. package/engine/scripts/fulfillment.js +586 -0
  150. package/engine/scripts/gen-agent-bundles.mjs +276 -0
  151. package/engine/scripts/gen-preset-rows.mjs +259 -0
  152. package/engine/scripts/init-ml.sh +2278 -0
  153. package/engine/scripts/install-internal-role.sh +190 -0
  154. package/engine/scripts/install-license.mjs +641 -0
  155. package/engine/scripts/install-one-shot.mjs +665 -0
  156. package/engine/scripts/integration_contract.py +220 -0
  157. package/engine/scripts/integrations/wenyao/adapter.py +98 -0
  158. package/engine/scripts/integrations/xzh/adapter.py +75 -0
  159. package/engine/scripts/invoice-mapping.example.json +40 -0
  160. package/engine/scripts/invoice-table.sh +195 -0
  161. package/engine/scripts/invoice_carrier_notice.py +279 -0
  162. package/engine/scripts/invoice_fail_closeout.py +297 -0
  163. package/engine/scripts/invoice_lip_runner.py +1508 -0
  164. package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
  165. package/engine/scripts/invoice_ready_audit.py +191 -0
  166. package/engine/scripts/invoice_runner.py +675 -0
  167. package/engine/scripts/invoice_stageb.py +516 -0
  168. package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
  169. package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
  170. package/engine/scripts/lease.py +377 -0
  171. package/engine/scripts/lib/agent-install.mjs +649 -0
  172. package/engine/scripts/lib/argent-datadir.mjs +16 -0
  173. package/engine/scripts/lib/argent-datadir.sh +18 -0
  174. package/engine/scripts/lib/argent_datadir.py +285 -0
  175. package/engine/scripts/lib/argent_filelock.py +507 -0
  176. package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
  177. package/engine/scripts/lib/cdp_ports.py +197 -0
  178. package/engine/scripts/lib/customer-env.mjs +102 -0
  179. package/engine/scripts/lib/daily-report-core.mjs +517 -0
  180. package/engine/scripts/lib/dep-declaration.mjs +81 -0
  181. package/engine/scripts/lib/deps-migrate.mjs +310 -0
  182. package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
  183. package/engine/scripts/lib/dsh-scope.mjs +500 -0
  184. package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
  185. package/engine/scripts/lib/dsh-versions.mjs +96 -0
  186. package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
  187. package/engine/scripts/lib/is-direct-run.mjs +61 -0
  188. package/engine/scripts/lib/patch-compose.mjs +241 -0
  189. package/engine/scripts/lib/patch-target.mjs +35 -0
  190. package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
  191. package/engine/scripts/lib/plugin-packages.mjs +332 -0
  192. package/engine/scripts/lib/preset-compat.mjs +127 -0
  193. package/engine/scripts/lib/preset-install.mjs +575 -0
  194. package/engine/scripts/lib/preset-mechanism.mjs +56 -0
  195. package/engine/scripts/lib/preset-sync.mjs +59 -0
  196. package/engine/scripts/lib/profile-retire.mjs +97 -0
  197. package/engine/scripts/lib/python-runtime.mjs +190 -0
  198. package/engine/scripts/lib/registry.mjs +72 -0
  199. package/engine/scripts/lib/repo-root.sh +56 -0
  200. package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
  201. package/engine/scripts/lib/schedule-probe.mjs +835 -0
  202. package/engine/scripts/lib/scope-pointer.mjs +164 -0
  203. package/engine/scripts/lib/settings-panels.mjs +816 -0
  204. package/engine/scripts/lib/stdio_utf8.py +86 -0
  205. package/engine/scripts/lib/wake-auth.mjs +392 -0
  206. package/engine/scripts/lib/webdriver_ipc.py +264 -0
  207. package/engine/scripts/lib/zstd-frames.mjs +104 -0
  208. package/engine/scripts/license-e2e-drill.mjs +426 -0
  209. package/engine/scripts/license-gate.mjs +245 -0
  210. package/engine/scripts/lip_backlog.py +171 -0
  211. package/engine/scripts/lip_table.py +185 -0
  212. package/engine/scripts/local-usage.mjs +214 -0
  213. package/engine/scripts/migrate-providers.mjs +127 -0
  214. package/engine/scripts/migrate-skills.sh +65 -0
  215. package/engine/scripts/ml-oauth-setup.mjs +550 -0
  216. package/engine/scripts/ml-store-ids.json +26 -0
  217. package/engine/scripts/ml_artifact_attribution.py +333 -0
  218. package/engine/scripts/ml_capacity_probe.py +1158 -0
  219. package/engine/scripts/ml_hub_status.py +1556 -0
  220. package/engine/scripts/ml_schema_check.py +115 -0
  221. package/engine/scripts/ml_store_ids.py +139 -0
  222. package/engine/scripts/npm-publish-argent.mjs +499 -0
  223. package/engine/scripts/npm-publish-argent.sh +20 -0
  224. package/engine/scripts/orchestrator.py +13811 -0
  225. package/engine/scripts/pack-plugins.mjs +140 -0
  226. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
  227. package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
  228. package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
  229. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
  230. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
  231. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
  232. package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
  233. package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
  234. package/engine/scripts/patches/verify-hardening.mjs +167 -0
  235. package/engine/scripts/patches/verify-session-migration.mjs +149 -0
  236. package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
  237. package/engine/scripts/plan-role-rename.mjs +125 -0
  238. package/engine/scripts/plugin-bootcheck.mjs +451 -0
  239. package/engine/scripts/plugin-composition.json +15 -0
  240. package/engine/scripts/poc-mcp-host.mjs +455 -0
  241. package/engine/scripts/poll-fulfillment.sh +295 -0
  242. package/engine/scripts/poll-invoice.sh +76 -0
  243. package/engine/scripts/profile-doctor.mjs +686 -0
  244. package/engine/scripts/provider-select.sh +71 -0
  245. package/engine/scripts/provider-switch.sh +146 -0
  246. package/engine/scripts/provider-sync-target.check.mjs +64 -0
  247. package/engine/scripts/python-runtime.sh +78 -0
  248. package/engine/scripts/query_runner.py +310 -0
  249. package/engine/scripts/release-argent.sh +574 -0
  250. package/engine/scripts/repair-unresolved-tools.mjs +178 -0
  251. package/engine/scripts/retire-inventory.mjs +40 -0
  252. package/engine/scripts/run-tests.sh +257 -0
  253. package/engine/scripts/s1-audit.mjs +85 -0
  254. package/engine/scripts/self-sign-license.mjs +179 -0
  255. package/engine/scripts/session-health-scan.mjs +63 -0
  256. package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
  257. package/engine/scripts/setup.sh +22 -0
  258. package/engine/scripts/skill-install.mjs +629 -0
  259. package/engine/scripts/sku_translate.py +148 -0
  260. package/engine/scripts/sync-plugins.sh +90 -0
  261. package/engine/scripts/sync-presets.sh +27 -0
  262. package/engine/scripts/sync-scripts.sh +445 -0
  263. package/engine/scripts/telemetry.py +873 -0
  264. package/engine/scripts/ui-copy-count.mjs +220 -0
  265. package/engine/scripts/usage-anchor.sh +137 -0
  266. package/engine/scripts/usage_report.py +531 -0
  267. package/engine/scripts/validate-preset-schema.mjs +208 -0
  268. package/engine/scripts/vertical-init.mjs +323 -0
  269. package/engine/scripts/vertical-lint.mjs +482 -0
  270. package/engine/scripts/wenyao-browser.sh +74 -0
  271. package/engine/scripts/wenyao-fixture-export.py +116 -0
  272. package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
  273. package/engine/scripts/wenyao-login.sh +35 -0
  274. package/engine/scripts/wenyao-template-map.json +292 -0
  275. package/engine/scripts/wenyao_attach.py +131 -0
  276. package/engine/scripts/wenyao_download_template.py +144 -0
  277. package/engine/scripts/wenyao_fill_template.py +568 -0
  278. package/engine/scripts/wenyao_login.py +145 -0
  279. package/engine/scripts/wenyao_portal.py +633 -0
  280. package/engine/scripts/wenyao_recon.py +230 -0
  281. package/engine/scripts/wenyao_run.py +503 -0
  282. package/engine/scripts/wenyao_shadow_diff.py +160 -0
  283. package/engine/scripts/wenyao_skill_contract.py +156 -0
  284. package/engine/scripts/wenyao_template_fetch.py +175 -0
  285. package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
  286. package/engine/scripts/ziniao-auth-probe.sh +268 -0
  287. package/engine/scripts/ziniao-keepalive.sh +217 -0
  288. package/engine/scripts/ziniao-post-upgrade-check.sh +121 -0
  289. package/engine/scripts/ziniao-windows.sh +154 -0
  290. package/engine/skills/README.md +86 -0
  291. package/engine/skills/argent-dev/SKILL.md +41 -0
  292. package/engine/skills/internal.json +37 -0
  293. package/engine/skills/lktop-pm/SKILL.md +35 -0
  294. package/engine/skills/lktopapp-dev/SKILL.md +34 -0
  295. package/engine/skills/ml-config-guide/SKILL.md +50 -0
  296. package/engine/skills/ml-fulfillment-sop/SKILL.md +178 -0
  297. package/engine/skills/ml-fulfillment-sop/references/andes-components.md +69 -0
  298. package/engine/skills/ml-fulfillment-sop/references/cdp-port-mapping.md +35 -0
  299. package/engine/skills/ml-fulfillment-sop/references/feishu-base-fulfillment-schema.md +136 -0
  300. package/engine/skills/ml-fulfillment-sop/references/multi-window-concurrency.md +69 -0
  301. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +77 -0
  302. package/engine/skills/ml-fulfillment-sop/references/selectors.md +243 -0
  303. package/engine/skills/ml-fulfillment-sop/scripts/fulfillment.js +586 -0
  304. package/engine/skills/ml-fulfillment-sop/scripts/poll-and-execute.sh +271 -0
  305. package/engine/skills/ml-fulfillment-sop/templates/report.md +55 -0
  306. package/engine/skills/ml-invoice/SKILL.md +506 -0
  307. package/engine/skills/ml-invoice/references/pitfalls.md +268 -0
  308. package/engine/skills/ml-invoice/references/portal-flow.md +360 -0
  309. package/engine/skills/ml-invoice/references/template-fill-contract.md +133 -0
  310. package/engine/skills/ml-invoice-xzh/SKILL.md +133 -0
  311. package/engine/skills/ml-invoice-xzh/references/pitfalls.md +230 -0
  312. package/engine/skills/ml-store-daily-report/SKILL.md +224 -0
  313. package/engine/skills/ml-store-daily-report/scripts/README.md +25 -0
  314. package/engine/skills/ml-store-daily-report/scripts/extract.js +691 -0
  315. package/engine/skills/ml-store-daily-report/scripts/generate.js +299 -0
  316. package/engine/skills/pet-dev/SKILL.md +42 -0
  317. package/engine/skills/whyshu-pm/SKILL.md +35 -0
  318. package/engine/skills/whyshu-web-dev/SKILL.md +50 -0
  319. package/engine/version.mjs +1 -0
  320. package/engine/verticals/README.md +103 -0
  321. package/engine/verticals/_engine-words.baseline.txt +237 -0
  322. package/engine/verticals/_engine-words.json +19 -0
  323. package/engine/verticals/mercadolibre/README.md +43 -0
  324. package/engine/verticals/mercadolibre/config.json +709 -0
  325. package/engine/verticals/mercadolibre/flows.json +738 -0
  326. package/engine/verticals/mercadolibre/manifest.json +538 -0
  327. package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
  328. package/engine/verticals/mercadolibre/preflight.py +143 -0
  329. package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
  330. package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
  331. package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
  332. package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
  333. package/engine/verticals/mercadolibre/prompts/full.md +91 -0
  334. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
  335. package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
  336. package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
  337. package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
  338. package/package.json +22 -4
  339. package/requirements.txt +5 -0
  340. package/README.md +0 -3
@@ -0,0 +1,661 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-pack-closure.mjs —— 能力包「运行期闭包」体检:**按 `pack.engine` + files 清单解析
4
+ * 静态 import / 子进程调用图,报告"运行期需要但未在包内"的文件**(只读;2026-10-04 立)。
5
+ *
6
+ * ## 为什么有它(事故)
7
+ * 能力包以前只有"清单里声明的文件是否存在"这一道校验(`capability_pack.py --validate`),
8
+ * 而**清单本身对不对**没人查。实测("包内独立运行复验"):
9
+ * · `scripts/lib/argent-datadir.mjs` 与 `scripts/check-skill-classification.mjs` **漏在
10
+ * `pack.engine` 之外** —— 它们是被 `core/lib/*.mjs` import 的**运行期**件;
11
+ * · 后果:把包 build 到空目录后,`node core/cli/argent.mjs --help` **当场
12
+ * ERR_MODULE_NOT_FOUND**(不是降级、不是告警,是直接崩);
13
+ * · `capability_pack.py --validate` 全程 ✅ —— 因为缺的那个文件**根本没进清单**,
14
+ * "校验清单里的文件都在"对它天然为真。
15
+ * ⇒ 缺的是"**从入口出发把运行期真会读到的文件走一遍**"这一步。本脚本补的就是它。
16
+ *
17
+ * ## 它做什么(三步,全部只读)
18
+ * 1. **拿真实清单**:逐个能力跑 `scripts/capability_pack.py --describe <c> --json`
19
+ * (复用现有派生逻辑,**不自己算第二份清单**);包内文件集 = `integrity` 的键
20
+ * (`integrity` 已把目录展开成真实文件)+ build 时会生成的 `scripts/flow-requirements.json`。
21
+ * 2. **解析运行期引用图**(对包内每个 `.mjs/.js/.cjs/.py/.sh`,先剥注释再解析):
22
+ * · JS:静态 `import`/`export … from`/`require()`/`import()` 的**相对** specifier;
23
+ * · Python:行首 `import X` / `from X import` / `from .mod import`(含 `from . import 子模块`);
24
+ * · 子进程/读写路径:`readFileSync|execFileSync|spawnSync|subprocess.*|open|Path` 等
25
+ * I/O 调用**实参窗口内**的字面量路径(`join(...)`/`ROOT / "scripts" / "x.py"` 这类
26
+ * 全字面量的拼接可还原;`HERE`/`__dirname` → 文件所在目录,`ROOT`/`REPO` → 仓库根);
27
+ * · `.sh`:`node|python3|bash <路径>`、`source <路径>`、行内出现的仓库相对路径。
28
+ * 3. **报差集**:引用到、在仓库里**确实存在**、但**不在包内**的文件 ⇒ 缺件(并给出引用者与行号)。
29
+ * 再对缺件本身做有界 BFS(深度 ≤ 3)⇒ "补上它之后还会牵出谁"也一并列出,避免补一半。
30
+ *
31
+ * ## 覆盖范围(诚实标注)
32
+ * **覆盖**:`core/**` 与 `scripts/**` 里被包内文件**静态引用**的一切 —— 静态 ESM/CJS import、
33
+ * Python import(含相对导入)、子进程/读写调用里的字面量路径、shell 里点名的脚本。
34
+ * `core/cli/argent.mjs`、`core/mcp/*.mjs`、四个带 flow 能力共用的 `scripts/**` 引擎件都在扫面内。
35
+ * **已知盲区**(静态分析**必然**漏,别把它当"包一定完整"的证明):
36
+ * ① **动态 require / 运行时拼路径**:`import(name)`、`require(variable)`、`join(dir, argv[0])`、
37
+ * 模板串里插变量 ⇒ 解析不出目标(本脚本会把这些"像路径但没落地"的字面量计入
38
+ * 「未解析」计数并举例,**看得见**,但不是判定);
39
+ * ② **JSON/YAML 里声明的路径**:注册表/适配器契约/preset 组合里的文件引用(含清单自身的
40
+ * `pack.scripts`)—— 那是"声明面",归 `capability_pack.py --validate`;
41
+ * ③ **运行期才决定的文件**:`ARGENT_DATA_DIR` 下的数据、技能目录、宿主动态挂载的 preset;
42
+ * ④ **非代码文本**:提示词 `.md`/`.yml` 里提到的脚本名(散文提及 ≠ 调用点,计入会大量假阳性);
43
+ * ⑤ 环境/宿主提供的可执行(`dsh`、`lark-cli` 等)—— 它们**不在**仓库里,本就不该进包。
44
+ * ⇒ 结论:**本脚本绿 ≠ 包完整**;它是"静态引用面"的下界。补完缺件后仍应按
45
+ * `docs/README.md` §六.9⑤ 在**空目录**里真跑一遍独立运行(那才是上界验收)。
46
+ *
47
+ * ## 用法
48
+ * node scripts/check-pack-closure.mjs # 只报告,**恒 exit 0**(缺件只打印)
49
+ * node scripts/check-pack-closure.mjs --strict # 有缺件/豁免表有问题才 exit 1(手动跑 / 单独 CI)
50
+ * node scripts/check-pack-closure.mjs --json # 机器可读(含 exemptions / problems / 每条缺件的 via+line)
51
+ * node scripts/check-pack-closure.mjs --quiet # 只打印结论行
52
+ * node scripts/check-pack-closure.mjs --capability invoice # 只看某个能力(可重复)
53
+ * node scripts/check-pack-closure.mjs --drop <仓库相对路径> # 注入测试:从包内文件集里剔除一项
54
+ * # (模拟"漏登记",用来证明本门禁**报得出来**)
55
+ * node scripts/check-pack-closure.mjs --py <解释器> # 覆盖 python(默认 $ARGENT_PY 或 python3)
56
+ * 退出码:0 正常(有缺件也 0,除非 `--strict`);1 `--strict` 且有缺件/豁免表问题;2 用法错或**量不出来**。
57
+ *
58
+ * ## 豁免表(`NOT_PACK_RUNTIME`,默认只有一条)
59
+ * 判为"**确实不该进包**"的个例写进脚本顶部的常量表(形态照 `kernel-purity` 的 `WHITELIST`):
60
+ * 每条**必须带理由**,且**本轮必须被命中** —— 没命中就是「过期豁免」,会在报告里点名、`--strict` 下非零。
61
+ * 这是唯一的"合法例外"出口:**别**为了消一条缺件去放宽 `NON_RUNTIME` 的类别规则(那会连带吞掉真缺件)。
62
+ *
63
+ * ## 纪律
64
+ * · **只读**:源码里零写 API(回归用例【7】静态断言);跑 python 时带 `PYTHONDONTWRITEBYTECODE=1`
65
+ * 不种 `__pycache__`;不碰数据目录/台账/凭据、不联网、不改 `flow-requirements.json`;
66
+ * · **防假绿**:量不出东西(0 个能力 / 0 个包内代码文件 / python 读取失败)一律 **exit 2**,
67
+ * 绝不打印"全部闭合"——"扫了空集也算通过"正是本脚本要修的那类翻车;
68
+ * · **默认不失败**:它**不接** `scripts/run-tests.sh` 的失败判据(缺件常年可能存在,
69
+ * 接进去就是恒红陷阱);回归只覆盖 `scripts/tests/pack-closure.test.mjs` 的机制断言。
70
+ */
71
+ import { spawnSync } from 'node:child_process'
72
+ import { existsSync, readFileSync, statSync } from 'node:fs'
73
+ import { dirname, join, relative, resolve, sep } from 'node:path'
74
+ import { fileURLToPath } from 'node:url'
75
+
76
+ const HERE = dirname(fileURLToPath(import.meta.url))
77
+ const ROOT = resolve(HERE, '..')
78
+
79
+ // ── 参数 ────────────────────────────────────────────────────────────────────
80
+ const argv = process.argv.slice(2)
81
+ const has = (f) => argv.includes(f)
82
+ const valOf = (f) => {
83
+ const i = argv.indexOf(f)
84
+ return i >= 0 ? argv[i + 1] : undefined
85
+ }
86
+ const allOf = (f) => argv.reduce((acc, v, i) => (v === f && argv[i + 1] !== undefined ? [...acc, argv[i + 1]] : acc), [])
87
+ if (has('-h') || has('--help')) {
88
+ console.log(readFileSync(fileURLToPath(import.meta.url), 'utf8').split('*/')[0].replace(/^\/\*\*?/, '').replace(/^\s*\* ?/gm, '').trim())
89
+ process.exit(0)
90
+ }
91
+ const STRICT = has('--strict')
92
+ const JSON_OUT = has('--json')
93
+ const QUIET = has('--quiet')
94
+ const PY = valOf('--py') || process.env.ARGENT_PY || 'python3'
95
+ const ONLY = allOf('--capability')
96
+ const DROPS = new Set(allOf('--drop').map((d) => d.split(sep).join('/')))
97
+ const MAX_DEPTH = 3
98
+
99
+ /** 包内由 `--build` 生成(不在 `integrity` 里,但装到客户机上确实在包里)。 */
100
+ const GENERATED_IN_PACK = 'scripts/flow-requirements.json'
101
+
102
+ const CODE_RE = /\.(mjs|js|cjs|py|sh)$/
103
+ const REPO_TOP_RE = /^(scripts|core|verticals|skills|presets|plugins|deploy|docs|tools)\//
104
+
105
+ /**
106
+ * **非运行期**路径:引用了也不算缺件(客户机的能力包里**就不该有**它们)。
107
+ * 每条都要写清"为什么不进包"—— 否则这就是个吞掉真缺件的后门。
108
+ */
109
+ const NON_RUNTIME = [
110
+ { re: /^scripts\/tests\//, why: '开发期测试(客户机不装;pack.engine 明确不含测试)' },
111
+ { re: /^scripts\/patches\//, why: 'dsh 热修补丁(宿主侧资产,不由能力包分发)' },
112
+ { re: /^scripts\/capability-packs\.lock\.json$/, why: '打包版本锁(仓库侧产物)' },
113
+ { re: /^docs\//, why: '文档' },
114
+ { re: /^plugins\//, why: 'L2 宿主壳(装机由宿主提供,不在能力包分发面)' },
115
+ { re: /(^|\/)(__pycache__|node_modules)\//, why: '构建/依赖残留' },
116
+ { re: /\.(test|probe)\.(mjs|py)$/, why: '测试/探针脚本' },
117
+ { re: /\.DS_Store$/, why: 'macOS 元数据' },
118
+ ]
119
+ const nonRuntimeWhy = (rel) => (NON_RUNTIME.find((x) => x.re.test(rel)) || {}).why || null
120
+
121
+ /**
122
+ * **已判定不进能力包**的运行期引用(`{rel, reason}`)。
123
+ *
124
+ * 与 {@link NON_RUNTIME} 的区别:`NON_RUNTIME` 是"**按类别**永远不该进包"(测试/文档/插件…);
125
+ * 本表是"**按个例**判定不进包,但静态调用图确实指向它" —— 必须逐条写清"为什么它不是能力包的
126
+ * 运行期面",否则这就是个把真缺件吞掉的后门(形态照 `kernel-purity` 的 `WHITELIST` 与
127
+ * `role-gateway-exemptions.json`:**默认空**、每条带理由、条目没命中 =「过期豁免」要报出来)。
128
+ *
129
+ * ⛔ 加条目前先问:"它真的不在客户机的运行期面上吗?还是只是没登记?"—— 后者请写进 `pack.engine`。
130
+ */
131
+ const NOT_PACK_RUNTIME = [
132
+ {
133
+ rel: 'scripts/dsh-upgrade.mjs',
134
+ reason: 'L2 宿主侧 dsh 升级工具:仅被 scripts/dsh-upgrade-readiness.mjs 的**可选** --gates 路径调用;'
135
+ + '它真正的运行期闭包是 scripts/patches/**(补丁 + verify-*.mjs)与 dsh-version-lock/**,'
136
+ + '那两批**按设计不随能力包分发**(docs/README.md §六.9)⇒ 塞进 pack.engine 会连带要求把补丁目录也塞进去,'
137
+ + '那才是错的。改判方式:写进 scripts/flow-requirements.json#pack.engine 并删掉本条目。',
138
+ },
139
+ ]
140
+ const NOT_PACK_BY_REL = new Map(NOT_PACK_RUNTIME.map((x) => [x.rel, x]))
141
+ const REASON_MIN = 12
142
+
143
+ // ── 一、取真实清单(复用 capability_pack.py,不自己派生)────────────────────
144
+ function runPack(args) {
145
+ const r = spawnSync(PY, [join(ROOT, 'scripts', 'capability_pack.py'), ...args], {
146
+ encoding: 'utf8',
147
+ cwd: ROOT,
148
+ timeout: 180000,
149
+ // ⛔ 不种 __pycache__(只读门禁不许写仓库);关遥测(夹具流程的失败事件曾被推到生产告警)
150
+ env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1', ARGENT_TELEMETRY_OFF: '1' },
151
+ })
152
+ if (r.error) return { ok: false, err: `无法执行 ${PY}:${r.error.message}` }
153
+ if (r.status !== 0) return { ok: false, err: `capability_pack.py ${args.join(' ')} → exit ${r.status}:${((r.stderr || '') + (r.stdout || '')).trim().slice(0, 400)}` }
154
+ try {
155
+ return { ok: true, json: JSON.parse(r.stdout) }
156
+ } catch (e) {
157
+ return { ok: false, err: `capability_pack.py ${args.join(' ')} 的输出不是 JSON:${String(e.message).slice(0, 200)}` }
158
+ }
159
+ }
160
+
161
+ const listed = runPack(['--list', '--json'])
162
+ if (!listed.ok) {
163
+ console.error(` ❌ 读不到能力清单(**量不出来就不算通过**):${listed.err}`)
164
+ process.exit(2)
165
+ }
166
+ let caps = (Array.isArray(listed.json) ? listed.json : []).map((r) => r.capability).filter(Boolean)
167
+ caps = caps.filter((c) => ONLY.length === 0 || ONLY.includes(c))
168
+ if (caps.length === 0) {
169
+ console.error(` ❌ 没有可体检的能力(${ONLY.length ? `--capability ${ONLY.join(',')} 未匹配到任何能力` : '清单为空'})⇒ 量不出来,exit 2`)
170
+ process.exit(2)
171
+ }
172
+
173
+ // ── 二、剥注释(保留字符串内容与**行号**:注释替换成空格,换行原样)──────────
174
+ /** JS/CJS:状态机扫一遍,注释与**正则字面量**体替换成空格;字符串原样保留。 */
175
+ function maskJs(text) {
176
+ const out = [...text]
177
+ const n = text.length
178
+ const blank = (i) => { if (out[i] !== '\n') out[i] = ' ' }
179
+ let st = 'code'
180
+ let lastSig = '' // 上一个非空白字符(判 `/` 是除号还是正则)
181
+ for (let i = 0; i < n; i += 1) {
182
+ const c = text[i]
183
+ const d = i + 1 < n ? text[i + 1] : ''
184
+ if (st === 'line') { if (c === '\n') st = 'code'; else blank(i); continue }
185
+ if (st === 'block') {
186
+ if (c === '*' && d === '/') { blank(i); blank(i + 1); i += 1; st = 'code'; continue }
187
+ blank(i); continue
188
+ }
189
+ if (st === 'regex') {
190
+ if (c === '\\') { blank(i); if (i + 1 < n) blank(i + 1); i += 1; continue }
191
+ if (c === '/') { st = 'code'; lastSig = '/'; continue }
192
+ blank(i); continue
193
+ }
194
+ if (st === 'sq' || st === 'dq' || st === 'tpl') {
195
+ if (c === '\\') { i += 1; continue }
196
+ if ((st === 'sq' && c === "'") || (st === 'dq' && c === '"') || (st === 'tpl' && c === '`')) st = 'code'
197
+ lastSig = c
198
+ continue
199
+ }
200
+ // code
201
+ if (c === '/' && d === '/') { blank(i); blank(i + 1); i += 1; st = 'line'; continue }
202
+ if (c === '/' && d === '*') { blank(i); blank(i + 1); i += 1; st = 'block'; continue }
203
+ if (c === '/' && /[(,=:[!&|?{};+\-*%~^<>]|^$/.test(lastSig)) { st = 'regex'; blank(i); continue }
204
+ if (c === "'") st = 'sq'
205
+ else if (c === '"') st = 'dq'
206
+ else if (c === '`') st = 'tpl'
207
+ if (!/\s/.test(c)) lastSig = c
208
+ }
209
+ return out.join('')
210
+ }
211
+
212
+ /** Python / shell:`#` 注释与(Python 的)三引号块替换成空格;普通字符串原样保留。 */
213
+ function maskPy(text) {
214
+ const out = [...text]
215
+ const n = text.length
216
+ const blank = (i) => { if (out[i] !== '\n') out[i] = ' ' }
217
+ let st = 'code'
218
+ for (let i = 0; i < n; i += 1) {
219
+ const c = text[i]
220
+ if (st === 'line') { if (c === '\n') st = 'code'; else blank(i); continue }
221
+ if (st === 'tri1' || st === 'tri2') {
222
+ const q = st === 'tri1' ? "'''" : '"""'
223
+ if (text.startsWith(q, i)) { blank(i); blank(i + 1); blank(i + 2); i += 2; st = 'code'; continue }
224
+ if (c === '\\') { blank(i); if (i + 1 < n) blank(i + 1); i += 1; continue }
225
+ blank(i); continue
226
+ }
227
+ if (st === 'sq' || st === 'dq') {
228
+ if (c === '\\') { i += 1; continue }
229
+ if ((st === 'sq' && c === "'") || (st === 'dq' && c === '"')) st = 'code'
230
+ continue
231
+ }
232
+ if (c === '#') { blank(i); st = 'line'; continue }
233
+ if (text.startsWith("'''", i)) { blank(i); blank(i + 1); blank(i + 2); i += 2; st = 'tri1'; continue }
234
+ if (text.startsWith('"""', i)) { blank(i); blank(i + 1); blank(i + 2); i += 2; st = 'tri2'; continue }
235
+ if (c === "'") st = 'sq'
236
+ else if (c === '"') st = 'dq'
237
+ }
238
+ return out.join('')
239
+ }
240
+ const maskShell = maskPy
241
+
242
+ /** 取**字符串字面量**(含区间,用于"实参窗口内"过滤)。 */
243
+ function literalsOf(text) {
244
+ const out = []
245
+ const n = text.length
246
+ let i = 0
247
+ while (i < n) {
248
+ const c = text[i]
249
+ if (c === "'" || c === '"' || c === '`') {
250
+ const q = c
251
+ const start = i
252
+ i += 1
253
+ let buf = ''
254
+ while (i < n) {
255
+ if (text[i] === '\\') { buf += text[i + 1] || ''; i += 2; continue }
256
+ if (text[i] === q) break
257
+ if (text[i] === '\n' && q !== '`') break
258
+ buf += text[i]
259
+ i += 1
260
+ }
261
+ out.push({ value: buf, start, end: i + 1 })
262
+ i += 1
263
+ continue
264
+ }
265
+ i += 1
266
+ }
267
+ return out
268
+ }
269
+
270
+ const lineOf = (text, idx) => text.slice(0, Math.max(0, idx)).split('\n').length
271
+
272
+ // ── 三、引用解析 ────────────────────────────────────────────────────────────
273
+ const abs = (rel) => resolve(ROOT, rel)
274
+ const relOf = (p) => relative(ROOT, p).split(sep).join('/')
275
+ const isRepoFile = (p) => {
276
+ if (!p.startsWith(ROOT + sep)) return false
277
+ try { return statSync(p).isFile() } catch { return false }
278
+ }
279
+
280
+ /** JS 相对 specifier → 仓库文件(补扩展名与 index)。 */
281
+ function resolveJsSpec(fromAbs, spec) {
282
+ const base = resolve(dirname(fromAbs), spec)
283
+ const cands = [base, `${base}.mjs`, `${base}.js`, `${base}.cjs`, `${base}.json`,
284
+ join(base, 'index.mjs'), join(base, 'index.js'), join(base, 'index.cjs')]
285
+ for (const c of cands) if (isRepoFile(c)) return relOf(c)
286
+ return null
287
+ }
288
+
289
+ /** Python 模块名 → 仓库文件(在若干个"运行期 sys.path 候选根"里找;找不到 = 第三方/标准库,忽略)。 */
290
+ function resolvePyModule(fromAbs, dots, dotted) {
291
+ const parts = dotted ? dotted.split('.').filter(Boolean) : []
292
+ const roots = [dirname(fromAbs), join(ROOT, 'scripts'), join(ROOT, 'scripts', 'lib'),
293
+ join(ROOT, 'scripts', 'flow_sources'), join(ROOT, 'core'), join(ROOT, 'core', 'lib'), ROOT]
294
+ for (const r of roots) {
295
+ const p = join(r, ...parts)
296
+ if (isRepoFile(`${p}.py`)) return relOf(`${p}.py`)
297
+ if (isRepoFile(join(p, '__init__.py'))) return relOf(join(p, '__init__.py'))
298
+ }
299
+ return null
300
+ }
301
+
302
+ /** 从一段 I/O 调用实参里还原"全字面量路径"候选(还原不出/不存在 ⇒ 返回空)。 */
303
+ function pathCandidates(fromAbs, win) {
304
+ const out = []
305
+ const lits = win.lits.map((l) => l.value)
306
+ const pathish = lits.filter((s) => s && s.length < 120 && !/\s/.test(s) && !/^[a-z][a-z0-9+.-]*:\/\//i.test(s))
307
+ const base = win.hasRoot ? ROOT : dirname(fromAbs)
308
+ // 取**连续片段**而不只是整串:同一个实参窗口里常有 `'utf8'`/`'--yes'` 之类非路径字面量,
309
+ // 整串拼接会把路径拼坏 ⇒ 只试连续片段才还原得出真路径(2026-10-04 初版即此漏)。
310
+ for (let i = 0; i < pathish.length; i += 1) {
311
+ for (let j = i + 1; j <= Math.min(pathish.length, i + 6); j += 1) {
312
+ const seg = pathish.slice(i, j)
313
+ const joined = resolve(base, ...seg.map((s) => s.replace(/^\.\//, '')))
314
+ if (isRepoFile(joined)) out.push(relOf(joined))
315
+ }
316
+ }
317
+ for (const s of lits) {
318
+ const clean = s.replace(/^\.\//, '')
319
+ if (REPO_TOP_RE.test(clean)) {
320
+ const p = abs(clean)
321
+ if (isRepoFile(p)) out.push(relOf(p))
322
+ }
323
+ if (/^\.{1,2}\//.test(s)) {
324
+ const p = resolve(dirname(fromAbs), s)
325
+ if (isRepoFile(p)) out.push(relOf(p))
326
+ }
327
+ }
328
+ return out
329
+ }
330
+
331
+ /** 在掩码后的代码里取 `call(` 的实参窗口(括号配平;窗口内保留原文以便取字面量)。 */
332
+ function argWindows(code, callRe) {
333
+ const wins = []
334
+ callRe.lastIndex = 0
335
+ let m
336
+ while ((m = callRe.exec(code)) !== null) {
337
+ const open = code.indexOf('(', m.index + m[0].length - 1)
338
+ if (open < 0) continue
339
+ let depth = 0
340
+ let i = open
341
+ let st = null
342
+ for (; i < code.length && i - open < 900; i += 1) {
343
+ const c = code[i]
344
+ if (st) {
345
+ if (c === '\\') { i += 1; continue }
346
+ if (c === st) st = null
347
+ continue
348
+ }
349
+ if (c === "'" || c === '"' || c === '`') { st = c; continue }
350
+ if (c === '(') depth += 1
351
+ else if (c === ')') { depth -= 1; if (depth === 0) break }
352
+ }
353
+ const raw = code.slice(open, i + 1)
354
+ wins.push({
355
+ raw,
356
+ start: open,
357
+ lits: literalsOf(raw),
358
+ hasRoot: /\b(ROOT|REPO)\b/.test(raw),
359
+ })
360
+ callRe.lastIndex = i + 1
361
+ }
362
+ return wins
363
+ }
364
+
365
+ const JS_IO_RE = /\b(?:readFileSync|readFile|writeFileSync|existsSync|statSync|lstatSync|realpathSync|readdirSync|copyFileSync|mkdirSync|rmSync|unlinkSync|accessSync|openSync|createReadStream|createWriteStream|execFileSync|execFile|spawnSync|spawn|execSync|readFileSync)\s*\(/g
366
+ const PY_IO_RE = /\b(?:open|Path|read_text|write_text|is_file|is_dir|exists|subprocess\.\w+|check_output|check_call|Popen)\s*\(/g
367
+ const JS_IMPORT_ADJ = [
368
+ { re: /\bfrom\b/g, adjacent: true },
369
+ { re: /\brequire\s*\(/g, adjacent: true },
370
+ { re: /\bimport\s*\(/g, adjacent: true },
371
+ { re: /^[ \t]*import[ \t]+/gm, adjacent: false },
372
+ ]
373
+ const PY_IMPORT_RE = /^[ \t]*import[ \t]+([A-Za-z_][\w.]*(?:[ \t]*,[ \t]*[A-Za-z_][\w.]*)*)/gm
374
+ const PY_FROM_RE = /^[ \t]*from[ \t]+(\.*)([A-Za-z_][\w.]*)?[ \t]+import[ \t]+([^\n#]*)/gm
375
+
376
+ /** 扫一个仓库文件 → 运行期引用列表 `{rel, via, line, kind}`(`rel` 是它引用的文件)。 */
377
+ function refsOf(relPath) {
378
+ const p = abs(relPath)
379
+ const text = readFileSync(p, 'utf8')
380
+ const out = []
381
+ const excluded = []
382
+ const push = (target, line, kind) => {
383
+ if (!target || target === relPath) return
384
+ const why = nonRuntimeWhy(target)
385
+ if (why) { excluded.push({ rel: target, via: relPath, line, kind, why }); return }
386
+ out.push({ rel: target, via: relPath, line, kind })
387
+ }
388
+ const unresolved = []
389
+
390
+ if (/\.(mjs|js|cjs)$/.test(relPath)) {
391
+ const code = maskJs(text)
392
+ const lits = literalsOf(code)
393
+ for (const { re, adjacent } of JS_IMPORT_ADJ) {
394
+ re.lastIndex = 0
395
+ let m
396
+ while ((m = re.exec(code)) !== null) {
397
+ const after = m.index + m[0].length
398
+ const s = lits.find((x) => x.start >= after && (!adjacent || /^\s*$/.test(code.slice(after, x.start))))
399
+ if (!s) continue
400
+ if (!/^\.{1,2}\//.test(s.value)) continue // 裸 specifier = 宿主/第三方包,不归本门禁
401
+ const t = resolveJsSpec(p, s.value)
402
+ if (t) push(t, lineOf(text, s.start), 'import')
403
+ else unresolved.push({ line: lineOf(text, s.start), value: s.value })
404
+ }
405
+ }
406
+ for (const w of argWindows(code, JS_IO_RE)) {
407
+ const cands = pathCandidates(p, w)
408
+ if (cands.length === 0) {
409
+ for (const l of w.lits) if (/[/\\]|\.(mjs|js|cjs|py|sh|json|yml|md)$/.test(l.value) && !/^[a-z]+:\/\//i.test(l.value)) {
410
+ unresolved.push({ line: lineOf(text, w.start + l.start), value: l.value })
411
+ }
412
+ }
413
+ for (const c of cands) push(c, lineOf(text, w.start), 'io-path')
414
+ }
415
+ } else if (/\.py$/.test(relPath)) {
416
+ const code = maskPy(text)
417
+ PY_IMPORT_RE.lastIndex = 0
418
+ let m
419
+ while ((m = PY_IMPORT_RE.exec(code)) !== null) {
420
+ for (const name of m[1].split(',').map((s) => s.trim()).filter(Boolean)) {
421
+ const t = resolvePyModule(p, '', name)
422
+ if (t) push(t, lineOf(text, m.index), 'py-import')
423
+ }
424
+ }
425
+ PY_FROM_RE.lastIndex = 0
426
+ while ((m = PY_FROM_RE.exec(code)) !== null) {
427
+ const dots = m[1] || ''
428
+ const mod = m[2] || ''
429
+ const names = (m[3] || '').split(',').map((s) => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean)
430
+ let hit = null
431
+ if (dots) {
432
+ let base = dirname(p)
433
+ for (let k = 1; k < dots.length; k += 1) base = dirname(base)
434
+ const parts = mod ? mod.split('.').filter(Boolean) : []
435
+ const cand = join(base, ...parts)
436
+ if (isRepoFile(`${cand}.py`)) hit = relOf(`${cand}.py`)
437
+ else if (isRepoFile(join(cand, '__init__.py'))) hit = relOf(join(cand, '__init__.py'))
438
+ } else if (mod) {
439
+ hit = resolvePyModule(p, '', mod)
440
+ }
441
+ if (hit) push(hit, lineOf(text, m.index), 'py-import')
442
+ // `from . import a, b` ⇒ 被导入的名字本身是子模块(只在**确实是仓库文件**时才算,避免误报)
443
+ if (dots) {
444
+ let base = dirname(p)
445
+ for (let k = 1; k < dots.length; k += 1) base = dirname(base)
446
+ for (const nm of names) {
447
+ const cand = join(base, nm)
448
+ if (isRepoFile(`${cand}.py`)) push(relOf(`${cand}.py`), lineOf(text, m.index), 'py-import')
449
+ }
450
+ }
451
+ }
452
+ for (const w of argWindows(code, PY_IO_RE)) {
453
+ const cands = pathCandidates(p, w)
454
+ if (cands.length === 0) {
455
+ for (const l of w.lits) if (/[/\\]|\.(mjs|js|cjs|py|sh|json|yml|md)$/.test(l.value)) {
456
+ unresolved.push({ line: lineOf(text, w.start + l.start), value: l.value })
457
+ }
458
+ }
459
+ for (const c of cands) push(c, lineOf(text, w.start), 'io-path')
460
+ }
461
+ } else if (/\.sh$/.test(relPath)) {
462
+ // shell 的引用只在**命令位置**才算(否则 `echo "请先运行 init-ml.sh"` 这类**给人看的提示**
463
+ // 会全部变成"缺件" —— 2026-10-04 初版实测:flow-timers.sh / daily-report.sh / poll-fulfillment.sh
464
+ // 的 5 处提示语被误报)。两条取法:
465
+ // ① 命令词(node/python/bash/source/…)后的第一个非选项参数 → 裸文件名(`python3 x.py`);
466
+ // ② 任何位置的**仓库相对路径**(`"$REPO/scripts/x.py"`、`${REPO}/scripts/x.py`)—— 提示语里
467
+ // 不会写 `scripts/` 这个前缀(写 `scripts/` 的一定是真路径)。
468
+ const code = maskShell(text)
469
+ const lines = code.split('\n')
470
+ const CMD_ARG = /\b(?:node|python3?|bash|sh|source|exec|xargs)\b(?:[ \t]+--?[\w-]+)*[ \t]+["']?([^\s"';|&)]+)/g
471
+ const REPO_REL = /(?:^|["'\s=${}(/])((?:scripts|core|verticals|skills|presets|plugins|deploy|docs)\/[\w./@-]+)/g
472
+ for (let i = 0; i < lines.length; i += 1) {
473
+ // 截到本行第一个 `echo`/`printf` 命令词为止:其后是**给人看的文本**,不是调用点
474
+ const cut = lines[i].search(/(^|[\s;&|({])["']?(echo|printf)["']?[\s(]/)
475
+ const line = cut >= 0 ? lines[i].slice(0, cut) : lines[i]
476
+ const found = new Set()
477
+ for (const m of line.matchAll(CMD_ARG)) found.add(m[1])
478
+ for (const m of line.matchAll(REPO_REL)) found.add(m[1])
479
+ for (const tk of found) {
480
+ const clean = tk.replace(/^["']|["']$/g, '').replace(/^\.\//, '')
481
+ for (const c of [abs(clean), resolve(dirname(p), clean)]) {
482
+ if (isRepoFile(c)) { push(relOf(c), i + 1, 'shell'); break }
483
+ }
484
+ }
485
+ }
486
+ }
487
+ return { refs: out, unresolved, excluded }
488
+ }
489
+
490
+ // ── 四、逐个能力跑闭包 ──────────────────────────────────────────────────────
491
+ const report = []
492
+ let fatal = null
493
+ for (const cap of caps) {
494
+ const d = runPack(['--describe', cap, '--json'])
495
+ if (!d.ok) { fatal = `能力 ${cap} 的清单读不到:${d.err}`; break }
496
+ const m = d.json
497
+ const integrity = m.integrity || {}
498
+ const packFiles = new Set(Object.keys(integrity))
499
+ packFiles.add(GENERATED_IN_PACK) // build 时生成,装到客户机上确实在包里
500
+ for (const rel of DROPS) packFiles.delete(rel) // 注入测试:模拟漏登记
501
+
502
+ const codeFiles = [...packFiles].filter((f) => CODE_RE.test(f) && isRepoFile(abs(f)))
503
+ const refs = []
504
+ const unresolved = []
505
+ const excluded = []
506
+ for (const f of codeFiles) {
507
+ let r
508
+ try { r = refsOf(f) } catch (e) { unresolved.push({ via: f, line: 0, value: `<读不到:${String(e.message).slice(0, 80)}>` }); continue }
509
+ refs.push(...r.refs)
510
+ for (const u of r.unresolved) unresolved.push({ via: f, ...u })
511
+ for (const x of r.excluded) excluded.push(x)
512
+ }
513
+
514
+ // 缺件 = 引用到、仓库里有、但不在包内;再对缺件本身做有界 BFS(补一半 ⇒ 下一轮还会红)
515
+ const missing = new Map()
516
+ const judgedOut = new Map()
517
+ const hitExemptions = new Set()
518
+ const seen = new Set(packFiles)
519
+ let frontier = []
520
+ const consider = (r, depth, via) => {
521
+ const ex = NOT_PACK_BY_REL.get(r.rel)
522
+ if (ex) { // 已判定不进包:记进 judgedOut,**不**当缺件、也**不**再往下 BFS
523
+ hitExemptions.add(r.rel)
524
+ if (!judgedOut.has(r.rel)) judgedOut.set(r.rel, { rel: r.rel, depth, via, line: r.line, kind: r.kind, reason: ex.reason })
525
+ return
526
+ }
527
+ if (!missing.has(r.rel)) { missing.set(r.rel, { rel: r.rel, depth, via, line: r.line, kind: r.kind }); frontier.push(r.rel) }
528
+ }
529
+ for (const r of refs) {
530
+ if (!isRepoFile(abs(r.rel))) continue
531
+ if (packFiles.has(r.rel)) continue
532
+ consider(r, 1, r.via)
533
+ }
534
+ for (let depth = 2; depth <= MAX_DEPTH && frontier.length; depth += 1) {
535
+ const next = []
536
+ for (const f of frontier) {
537
+ if (seen.has(f)) continue
538
+ seen.add(f)
539
+ let sub
540
+ try { sub = refsOf(f) } catch { continue }
541
+ for (const r of sub.refs) {
542
+ if (!isRepoFile(abs(r.rel)) || packFiles.has(r.rel) || seen.has(r.rel)) continue
543
+ consider(r, depth, f)
544
+ }
545
+ }
546
+ frontier = next
547
+ }
548
+
549
+ report.push({
550
+ capability: cap,
551
+ version: m.version,
552
+ flows: m.flows || [],
553
+ packFiles: packFiles.size,
554
+ codeFiles: codeFiles.length,
555
+ refs: refs.length,
556
+ excluded,
557
+ declaredMissing: m.missing || [],
558
+ missing: [...missing.values()].sort((a, b) => (a.depth - b.depth) || a.rel.localeCompare(b.rel)),
559
+ judgedOut: [...judgedOut.values()].sort((a, b) => a.rel.localeCompare(b.rel)),
560
+ hitExemptions: [...hitExemptions],
561
+ unresolved,
562
+ })
563
+ }
564
+
565
+ if (fatal) {
566
+ console.error(` ❌ ${fatal}(**量不出来就不算通过**)`)
567
+ process.exit(2)
568
+ }
569
+ // 防假绿:一个能力都没量到代码文件 ⇒ "全部闭合"毫无意义(扫了空集也算通过 = 本脚本要修的那类翻车)
570
+ const totalCode = report.reduce((a, r) => a + r.codeFiles, 0)
571
+ if (totalCode === 0) {
572
+ console.error(' ❌ 一个能力包里的代码文件都没扫到(清单为空或路径全不对)⇒ 量不出来,exit 2')
573
+ process.exit(2)
574
+ }
575
+
576
+ const totalMissing = report.reduce((a, r) => a + r.missing.length, 0)
577
+ const totalUnresolved = report.reduce((a, r) => a + r.unresolved.length, 0)
578
+ const totalJudgedOut = report.reduce((a, r) => a + r.judgedOut.length, 0)
579
+
580
+ // ── 豁免表的卫生(形态照 kernel-purity WHITELIST / role-gateway-exemptions.json)──────
581
+ // · 缺理由/理由太短 ⇒ 问题("我知道它缺着"不等于"判定不进包");
582
+ // · 条目在本轮**没被命中** ⇒ 「过期豁免」也是问题(腐烂的豁免会掩盖日后新增的真缺件)。
583
+ const hitAll = new Set(report.flatMap((r) => r.hitExemptions))
584
+ const exemptionProblems = []
585
+ for (const e of NOT_PACK_RUNTIME) {
586
+ if (!e.rel) { exemptionProblems.push('豁免条目缺 rel'); continue }
587
+ if (!e.reason || String(e.reason).trim().length < REASON_MIN) {
588
+ exemptionProblems.push(`豁免 ${e.rel} 缺理由(须 ≥ ${REASON_MIN} 字:写清"为什么它不在能力包运行期面上")`)
589
+ }
590
+ if (!hitAll.has(e.rel)) exemptionProblems.push(`过期豁免:${e.rel} 本轮没有任何包引用它 ⇒ 请删掉该条目(否则它会掩盖日后新增的真缺件)`)
591
+ }
592
+
593
+ // ── 五、输出 ────────────────────────────────────────────────────────────────
594
+ if (JSON_OUT) {
595
+ console.log(JSON.stringify({
596
+ root: ROOT, python: PY, drops: [...DROPS], strict: STRICT,
597
+ exemptions: NOT_PACK_RUNTIME.map((e) => ({ rel: e.rel, hit: hitAll.has(e.rel), reason: e.reason })),
598
+ problems: exemptionProblems,
599
+ capabilities: report,
600
+ totals: { capabilities: report.length, codeFiles: totalCode, missing: totalMissing, judgedOut: totalJudgedOut, unresolved: totalUnresolved },
601
+ }, null, 2))
602
+ } else {
603
+ if (!QUIET) {
604
+ console.log('\n能力包运行期闭包体检(只读;默认只报告)')
605
+ console.log(` 判据:包内文件的 静态 import / Python import / 子进程与读写路径 ⇒ 引用到但**不在包内**的仓库文件`)
606
+ for (const r of report) {
607
+ console.log(`\n ── ${r.capability} v${r.version}(流程 ${r.flows.length}|包内 ${r.packFiles} 项|其中代码 ${r.codeFiles} 个|解析到引用 ${r.refs} 处)`)
608
+ if (r.declaredMissing.length) {
609
+ console.log(` ⚠️ 清单里声明了但仓库里不存在(capability_pack.py --validate 也管这条):${r.declaredMissing.join('、')}`)
610
+ }
611
+ if (r.missing.length === 0) {
612
+ console.log(' ✅ 静态引用面闭合:引用到的仓库文件都已在包内')
613
+ } else {
614
+ console.log(` ❌ 缺 ${r.missing.length} 个运行期文件(引用到、仓库里有、包里没有):`)
615
+ for (const x of r.missing) {
616
+ const hop = x.depth === 1 ? '' : `(第 ${x.depth} 跳:补上 ${x.via} 之后才会用到)`
617
+ console.log(` · ${x.rel} ← ${x.via}:${x.line} [${x.kind}]${hop}`)
618
+ }
619
+ console.log(' ↳ 修法:写进 `scripts/flow-requirements.json#pack.engine`(**按实际 import 关系推导**);')
620
+ console.log(' 若它是 `core/**` 运行期件,同一批还要进 `scripts/sync-scripts.sh#CORE_KEY`(两处逐项一致,')
621
+ console.log(' 由 `scripts/tests/sync-scripts-core.test.mjs` 判红),再提三个带 flow 的能力包版本 + `--stamp`。')
622
+ }
623
+ if (r.judgedOut.length) {
624
+ console.log(` ℹ️ 已判定不进包 ${r.judgedOut.length} 条:${r.judgedOut.map((x) => `${x.rel} ← ${x.via}:${x.line} [${x.kind}]`).join(';')}`)
625
+ }
626
+ if (r.excluded.length) {
627
+ const kinds = [...new Set(r.excluded.map((x) => `${x.rel.split('/')[0]}/**:${x.why}`))]
628
+ console.log(` ℹ️ 已排除 ${r.excluded.length} 处**非运行期**引用(${kinds.join(';')})`)
629
+ }
630
+ if (r.unresolved.length) {
631
+ console.log(` ℹ️ 未解析 ${r.unresolved.length} 处(动态 require / 运行时拼路径 / 非仓库路径 —— 静态法的固有盲区,不等于缺件):`)
632
+ for (const u of r.unresolved.slice(0, 5)) console.log(` · ${u.via}:${u.line} ${u.value}`)
633
+ if (r.unresolved.length > 5) console.log(` · …另 ${r.unresolved.length - 5} 处`)
634
+ }
635
+ }
636
+ if (NOT_PACK_RUNTIME.length) {
637
+ console.log('\n ── 已判定不进包(豁免表;每条必须写理由,条目没命中=过期豁免)')
638
+ for (const e of NOT_PACK_RUNTIME) {
639
+ console.log(` · ${e.rel}(本轮命中 ${hitAll.has(e.rel) ? '是' : '否'})`)
640
+ console.log(` ${e.reason}`)
641
+ }
642
+ }
643
+ if (exemptionProblems.length) {
644
+ console.log('\n ⚠️ 豁免表自身的问题(每一条都要修,否则"判定不进包"会变成吞缺件的后门):')
645
+ for (const p of exemptionProblems) console.log(` · ${p}`)
646
+ }
647
+ }
648
+ console.log('')
649
+ console.log(` =============================================`)
650
+ console.log(` 闭包体检:${report.length} 个能力包 / 扫 ${totalCode} 个代码文件 ⇒ 缺件 ${totalMissing} 个、`
651
+ + `豁免 ${NOT_PACK_RUNTIME.length} 条(命中 ${hitAll.size})、未解析 ${totalUnresolved} 处`)
652
+ if (DROPS.size) console.log(` (注入模式:已从包内文件集剔除 ${[...DROPS].join('、')})`)
653
+ const bad = totalMissing > 0 || exemptionProblems.length > 0
654
+ console.log(` ${!bad ? '✅ 静态引用面闭合(下界;上界仍按 docs/README.md §六.9⑤ 在空目录里真跑一遍)'
655
+ : (STRICT ? `❌ 有问题(--strict):缺件 ${totalMissing}、豁免表问题 ${exemptionProblems.length}`
656
+ : `⚠️ 有问题(默认只报告,exit 0;要非零请加 --strict):缺件 ${totalMissing}、豁免表问题 ${exemptionProblems.length}`)}`)
657
+ console.log(` =============================================`)
658
+ }
659
+
660
+ if (STRICT && (totalMissing > 0 || exemptionProblems.length > 0)) process.exit(1)
661
+ process.exit(0)