@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,15 @@
1
+ #!/usr/bin/env python3
2
+ """门禁用探针:打印引擎**生效的**网络类判据(P3)。
3
+
4
+ 为什么单独一个文件:`python3 -c` 传多行脚本在某些调用方式下会失败(实测门禁里报
5
+ "Command failed" 而手工跑同一段却正常),而门禁**不允许静默跳过**。改成文件调用,
6
+ 参数传递不再有歧义;输出用 `__JSON__` 前缀标记,容忍 stdout 噪声。
7
+ """
8
+ import json
9
+ import pathlib
10
+ import sys
11
+
12
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
13
+ import orchestrator as o # noqa: E402
14
+
15
+ print("__JSON__" + json.dumps(o._engine_network_rules()))
@@ -0,0 +1,568 @@
1
+ #!/usr/bin/env python3
2
+ """当日 FULL 归因报表 —— **只读、零网络、零写入**(FULL 自动路径成功率修复批,2026-10-10)。
3
+
4
+ ## 为什么需要它
5
+
6
+ 客户机 10-10 跑约 30 条 FULL,**约 1/3 失败**;同一天用 CLI 手动串行跑 6 票**全部 8/8 成功**。
7
+ "为什么自动路径比手动差"必须能**一条命令答出来**,而不是靠人翻日志 —— 否则每轮都只能凭印象
8
+ 调参(本批的窗口默认值、仓容闸门正是这么定下来的)。本脚本就是那个"答出来"的口子。
9
+
10
+ ## 数据源(**只有两个本地文件**,⛔ 不联网、不读表、不写任何东西)
11
+
12
+ | 源 | 默认路径 | 用途 |
13
+ |---|---|---|
14
+ | 统一台账 | `<数据目录>/flow-ledger.jsonl` | 当日 case 数 / 终态 / 小时×终态 / 店铺×终态 / 时间线 |
15
+ | 失败诊断 | `<数据目录>/fulfillment-logs/failures.jsonl` | 失败原因分类 / 反事实三态(**页面证据在这里**) |
16
+
17
+ 数据目录走内核**唯一判据**(`scripts/lib/argent_datadir.py`,与编排器/CLI 同一份),
18
+ ⛔ 不在这里复制三级链。
19
+
20
+ ## 输出五段(每段都带**计数 + 证据来源**,可复算)
21
+
22
+ ```
23
+ ① 当日 case 数 / 终态分布(done / failed / blocked / 其他)
24
+ ② 失败原因分类计数(仓容满 | 预约页形态异常 | 步骤8 找不到取消按钮 | 提交未跳转 hub-v2 | 其他)
25
+ ③ 按小时 × 终态、按店铺 × 终态
26
+ ④ 反事实估算:若「窗口=<给定值> ∧ 仓容可用」⇒ 本可成功 / 仍会失败 / 证据不足(**三态,⛔ 不许猜**)
27
+ ⑤ 与「手动路径」对照的时间线(同一 case 的人工 done 事件 vs 自动 failed 事件)
28
+ ```
29
+
30
+ ## 口径(**逐条对应实现,改这条 = 改判据**)
31
+
32
+ ### 终态(①③)
33
+ - 只看**当日**(`--date`,本地时区)的事件;台账 `ts` 是 epoch 秒。
34
+ - **信息性事件不算状态**(与 `flow_triage.INFO_EVENTS` **同一口径**:`phase` / `wake` /
35
+ `policy_apply` / `policy_new`)⇒ 取当日最后一条**状态性**事件:
36
+ `done` ⇒ done;`failed` ⇒ failed;`blocked` / `failed_ack` ⇒ blocked;
37
+ `planned` / `dispatched` / `retry` / `requeued` / 无 ⇒ 其他。
38
+ - 为什么不能只数事件:一张票一天可能 `failed` 3 次再 `done` —— 数事件会把"最终成功"报成失败。
39
+
40
+ ### 失败原因分类(②)
41
+ 判据是**失败文案里的标记串**(⛔ 不猜语义),逐条出自既有实现:
42
+
43
+ | 桶 | 判据(正则,命中即归桶;**顺序即优先级**) | 出处 |
44
+ |---|---|---|
45
+ | 仓容满 | `capacity_full` / 平台原文片段(`liberar espacio` 等)/ `仓容满` / `库容满` | `scripts/ml_capacity_probe.py#PLATFORM_QUOTES`(**同一份**) |
46
+ | 预约页形态异常 | `预约页形态异常` / `calendar_absent` | `orchestrator.APPT_FORM_ANOMALY` |
47
+ | 步骤8 找不到取消按钮 | `Cancelar reserva` ∧ `未出现`/`未执行`/`找不到`,或 `cancel_button_not_found` | 步骤8 取消链 |
48
+ | 提交未跳转 hub-v2 | `hub-v2` ∧ `未跳转`/`未生效`/`permancer`/`no redirig` | 步骤4 提交链 |
49
+ | 其他 | 其余全部 | —— |
50
+
51
+ ### 反事实三态(④)—— **前提 = 窗口取 `--window` ∧ 仓容可用**
52
+ 对**每一条**失败记录(`failures.jsonl` 优先;台账 `failed` 作补充),按**当时的页面证据**判:
53
+
54
+ | 结论 | 判据(**全部**条件都要满足) |
55
+ |---|---|
56
+ | `would_succeed` | 阻塞**唯一落在**两个轴上之一:① 仓容满(前提"仓容可用"直接消除该阻塞 ⇒ 前提即结论)② 顺延窗口耗尽(文案含 `已顺延到 平台今天+N 天`,且 `--window - 1 > N` ⇒ 更大窗口会继续往later 日期试) |
57
+ | `would_fail` | 证据显示阻塞**不在**这两个轴上(页面形态 / 坏货件号 / CDP/网络 / 步骤8 取消按钮 / 数据缺口…)⇒ 前提消除不了它 |
58
+ | `insufficient` | 证据缺失或不足(无文案 / 判不出属于哪条链 / 窗口类但**解析不出**当时试到第几天 ⇒ ⛔ 不许猜"更大窗口就能成") |
59
+
60
+ ⛔ **三态不许猜**:`insufficient` 是一个**正常且必须存在**的结论("证据不足" ≠ "会失败")。
61
+
62
+ ### 时间线(⑤)
63
+ 同一 case 当日既有**自动 failed** 又有**人工 done** ⇒ 打印两条事件的时刻与文本
64
+ (人工 done 的判据 = `event=done` ∧ `by` 不是引擎自己的 `orchestrator`)。
65
+
66
+ ## 用法
67
+
68
+ python3 scripts/analyze-full-day.py # 今天
69
+ python3 scripts/analyze-full-day.py --date 2026-10-10 --json
70
+ python3 scripts/analyze-full-day.py --ledger L --failures F --store 墨西哥-1店-子账号 --window 5
71
+
72
+ 退出码:0 正常(**即使当天 0 条 case 也是 0**,那是事实不是错误);2 参数/源不可读。
73
+ """
74
+
75
+ from __future__ import annotations
76
+
77
+ import argparse
78
+ import datetime as dt
79
+ import json
80
+ import pathlib
81
+ import re
82
+ import sys
83
+ from typing import Any, Optional
84
+
85
+ HERE = pathlib.Path(__file__).resolve().parent
86
+ sys.path.insert(0, str(HERE))
87
+ sys.path.insert(0, str(HERE / "lib"))
88
+ from argent_datadir import resolve_data_dir # noqa: E402 数据目录唯一判据(勿复制链)
89
+
90
+ # 仓容判据**同一份**(⛔ 不在这里再写一遍平台原文片段)
91
+ try:
92
+ from ml_capacity_probe import ACTION_TEXT as _CAPACITY_ACTION # noqa: E402
93
+ from ml_capacity_probe import CLASS_MARKER as _CAPACITY_MARKER # noqa: E402
94
+ from ml_capacity_probe import DEFER_TAG as _CAPACITY_TAG # noqa: E402
95
+ from ml_capacity_probe import PLATFORM_QUOTES as _PLATFORM_QUOTES # noqa: E402
96
+ except Exception: # noqa: BLE001
97
+ _CAPACITY_MARKER, _CAPACITY_TAG = "capacity_full", "仓容满"
98
+ _CAPACITY_ACTION = "需运营清低质库存腾库容"
99
+ _PLATFORM_QUOTES = ("liberar espacio",)
100
+
101
+ DATA_DIR = pathlib.Path(resolve_data_dir()["dir"])
102
+ DEFAULT_LEDGER = DATA_DIR / "flow-ledger.jsonl"
103
+ DEFAULT_FAILURES = DATA_DIR / "fulfillment-logs" / "failures.jsonl"
104
+
105
+ # ── 终态口径(与 flow_triage 同一份:信息性事件不算状态)──────────────────────
106
+ INFO_EVENTS = ("phase", "wake", "policy_apply", "policy_new")
107
+ STATE_BUCKETS = {
108
+ "done": "done",
109
+ "failed": "failed",
110
+ "blocked": "blocked",
111
+ "failed_ack": "blocked",
112
+ }
113
+ OTHER = "其他"
114
+
115
+ # ── 失败原因分类(顺序即优先级;判据是**标记串**,⛔ 不猜语义)────────────────
116
+ _QUOTE_RE = "|".join(re.escape(q) for q in _PLATFORM_QUOTES)
117
+ BUCKET_CAPACITY = "仓容满"
118
+ BUCKET_APPT_FORM = "预约页形态异常"
119
+ BUCKET_STEP8_CANCEL = "步骤8 找不到取消按钮"
120
+ BUCKET_HUBV2 = "提交未跳转 hub-v2"
121
+ BUCKET_OTHER = "其他"
122
+ BUCKET_ORDER = (BUCKET_CAPACITY, BUCKET_APPT_FORM, BUCKET_STEP8_CANCEL, BUCKET_HUBV2, BUCKET_OTHER)
123
+ REASON_RULES = (
124
+ (BUCKET_CAPACITY, "capacity-full", re.compile(
125
+ rf"{re.escape(_CAPACITY_MARKER)}|{_QUOTE_RE}|{re.escape(_CAPACITY_TAG)}|库容满", re.I)),
126
+ (BUCKET_APPT_FORM, "appointment-form", re.compile(r"预约页形态异常|calendar_absent", re.I)),
127
+ (BUCKET_STEP8_CANCEL, "step8-cancel-button", re.compile(
128
+ r"(?:Cancelar reserva|cancel_button_not_found|取消预约).{0,40}?"
129
+ r"(?:未出现|未执行|找不到|按钮|not found|超时)", re.I)),
130
+ (BUCKET_HUBV2, "submit-no-hubv2", re.compile(
131
+ r"hub-v2.{0,30}?(?:未跳转|未生效|未到达|no redirig)|(?:未跳转|未生效).{0,20}?hub-v2", re.I)),
132
+ )
133
+
134
+ # ── 反事实前提里"阻塞在哪条链"的判据(**与上面同一批标记**,⛔ 不另写一套)──────
135
+ WINDOW_EXHAUSTED_RE = re.compile(r"已顺延到\s*平台今天\+?(\d+)\s*天")
136
+ WINDOW_AXIS_RE = re.compile(
137
+ r"可用时段不足|时段不足|hub-v2.{0,30}?(?:未跳转|未生效)|(?:未跳转|未生效).{0,20}?hub-v2"
138
+ r"|当前无可预约时段|无可用日|时段被平台拒", re.I)
139
+ NON_AXIS_RE = re.compile(
140
+ r"预约页形态异常|calendar_absent|bad_shipment_id|取号错位|不可解析|"
141
+ r"未找到\s*Veh[íi]culo|CDP|ECONN|ETIMEDOUT|EAI_AGAIN|net::ERR|Target closed|"
142
+ r"browser has been closed|Cancelar reserva|取消预约|SKU.{0,40}在店铺|未授权|登录|"
143
+ # 代码级缺陷(我方缺陷类)同样落在前提之外:换窗口/腾库容都修不好它
144
+ r"object has no attribute|AttributeError|Traceback|TypeError|KeyError",
145
+ re.I)
146
+
147
+
148
+ # ══════════════════════════════════════════════════════════════════════════
149
+ # 读源(**只读**;每行都带 `src` 与行号,保证结论可复算)
150
+ # ══════════════════════════════════════════════════════════════════════════
151
+
152
+ def read_jsonl(path: pathlib.Path) -> list:
153
+ """逐行读 JSONL,返回 `[{... , "_src": "<path>", "_line": N}]`(坏行跳过但计数)。"""
154
+ out: list = []
155
+ try:
156
+ text = path.read_text(encoding="utf-8", errors="replace")
157
+ except OSError:
158
+ return out
159
+ for i, line in enumerate(text.splitlines(), 1):
160
+ line = line.strip()
161
+ if not line:
162
+ continue
163
+ try:
164
+ d = json.loads(line)
165
+ except json.JSONDecodeError:
166
+ continue
167
+ if not isinstance(d, dict):
168
+ continue
169
+ d["_src"], d["_line"] = str(path), i
170
+ out.append(d)
171
+ return out
172
+
173
+
174
+ def ledger_date_of(ev: dict) -> str:
175
+ """台账事件日期(epoch → **本地**日期;与 failures.jsonl 的本地时间串同口径)。"""
176
+ ts = ev.get("ts")
177
+ try:
178
+ return dt.datetime.fromtimestamp(float(ts)).date().isoformat()
179
+ except (TypeError, ValueError, OSError, OverflowError):
180
+ return ""
181
+
182
+
183
+ def failures_date_of(rec: dict) -> str:
184
+ """失败诊断日期(`YYYY-MM-DD HH:MM:SS` 前 10 位)。"""
185
+ return str(rec.get("ts") or "")[:10]
186
+
187
+
188
+ STORE_IN_ERROR_RE = re.compile(r"\[store=([^\]]*)\]")
189
+
190
+
191
+ def store_of(ev: dict) -> str:
192
+ """事件所属店铺:优先 `store` 字段,其次 `error` 里的 `[store=…]` 身份前缀。"""
193
+ s = str(ev.get("store") or "").strip()
194
+ if s:
195
+ return s
196
+ m = STORE_IN_ERROR_RE.search(str(ev.get("error") or ""))
197
+ return m.group(1).strip() if m else ""
198
+
199
+
200
+ def case_of(ev: dict) -> str:
201
+ return str(ev.get("caseId") or "")
202
+
203
+
204
+ def record_of(case_id: str) -> str:
205
+ """`full__<rec>` → `<rec>`(非 full case 原样返回)。"""
206
+ return case_id.split("__", 1)[1] if "__" in case_id else case_id
207
+
208
+
209
+ # ══════════════════════════════════════════════════════════════════════════
210
+ # 纯判据
211
+ # ══════════════════════════════════════════════════════════════════════════
212
+
213
+ def state_bucket(event: str) -> str:
214
+ """状态性事件 → 终态桶;信息性事件返回空串(**不算状态**)。"""
215
+ e = str(event or "")
216
+ if e in INFO_EVENTS:
217
+ return ""
218
+ return STATE_BUCKETS.get(e, OTHER)
219
+
220
+
221
+ def classify_reason(text: str) -> tuple:
222
+ """失败文案 → 原因桶。返回 `(bucket, rule_id)`。"""
223
+ t = str(text or "")
224
+ for bucket, rid, pat in REASON_RULES:
225
+ if pat.search(t):
226
+ return bucket, rid
227
+ return BUCKET_OTHER, "none"
228
+
229
+
230
+ def counterfactual(reason: str, window: int, *, extra_text: str = "") -> dict:
231
+ """反事实三态 —— **前提 = 窗口取 `window` ∧ 仓容可用**(判据表见模块 docstring)。
232
+
233
+ 返回 `{verdict, axis, basis, observed_window}`;`verdict ∈ {would_succeed, would_fail, insufficient}`。
234
+ ⛔ 三态不许猜:解析不出"当时试到第几天"就判 `insufficient`,**不**默认"更大窗口就能成"。
235
+ """
236
+ t = f"{reason or ''} {extra_text or ''}".strip()
237
+ if not t:
238
+ return {"verdict": "insufficient", "axis": "none",
239
+ "basis": "无失败文案(证据缺失)", "observed_window": None}
240
+
241
+ # ① 仓容轴:前提"仓容可用"**直接消除**该阻塞 ⇒ 前提即结论
242
+ if REASON_RULES[0][2].search(t):
243
+ # 但若同一段证据里另有一条**不在前提射程内**的硬阻塞 ⇒ 前提消除不了它
244
+ if NON_AXIS_RE.search(t):
245
+ m = NON_AXIS_RE.search(t)
246
+ return {"verdict": "would_fail", "axis": "capacity+other",
247
+ "basis": f"仓容是该条阻塞,但同时存在前提消除不了的硬阻塞「{m.group(0)}」",
248
+ "observed_window": None}
249
+ return {"verdict": "would_succeed", "axis": "capacity",
250
+ "basis": f"阻塞唯一落在仓容轴:命中平台原文/`{_CAPACITY_MARKER}`;"
251
+ f"前提「仓容可用」直接消除该阻塞", "observed_window": None}
252
+
253
+ # ② 顺延窗口轴:必须**同时**有"窗口耗尽"证据与"更大窗口能继续"的算术关系
254
+ if WINDOW_AXIS_RE.search(t):
255
+ if NON_AXIS_RE.search(t):
256
+ m = NON_AXIS_RE.search(t)
257
+ return {"verdict": "would_fail", "axis": "window+other",
258
+ "basis": f"窗口类失败但同时存在前提消除不了的硬阻塞「{m.group(0)}」",
259
+ "observed_window": None}
260
+ m = WINDOW_EXHAUSTED_RE.search(t)
261
+ if not m:
262
+ return {"verdict": "insufficient", "axis": "window",
263
+ "basis": "窗口类失败,但文案里**没有**「已顺延到 平台今天+N 天」⇒ "
264
+ "解析不出当时试到第几天(⛔ 不猜)", "observed_window": None}
265
+ n = int(m.group(1))
266
+ observed = n + 1
267
+ if window > observed:
268
+ return {"verdict": "would_succeed", "axis": "window",
269
+ "basis": f"当时窗口耗尽于 平台今天+{n} 天(= 窗口 {observed});"
270
+ f"给定窗口 {window} > {observed} ⇒ 会继续往更晚日期试",
271
+ "observed_window": observed}
272
+ return {"verdict": "would_fail", "axis": "window",
273
+ "basis": f"当时窗口已试到 平台今天+{n} 天(= 窗口 {observed}),"
274
+ f"给定窗口 {window} ≤ {observed} ⇒ 换窗口也够不到",
275
+ "observed_window": observed}
276
+
277
+ # ③ 非前提轴(页面形态/坏号/CDP/取消按钮…)⇒ 前提消除不了
278
+ if NON_AXIS_RE.search(t):
279
+ m = NON_AXIS_RE.search(t)
280
+ return {"verdict": "would_fail", "axis": "non-axis",
281
+ "basis": f"阻塞不在窗口/仓容两轴上(命中「{m.group(0)}」)⇒ 前提消除不了它",
282
+ "observed_window": None}
283
+
284
+ return {"verdict": "insufficient", "axis": "unknown",
285
+ "basis": "文案既不像仓容/窗口类、也没命中任何已知非前提轴 ⇒ 判不出(⛔ 不猜)",
286
+ "observed_window": None}
287
+
288
+
289
+ # ══════════════════════════════════════════════════════════════════════════
290
+ # 报表装配
291
+ # ══════════════════════════════════════════════════════════════════════════
292
+
293
+ def build_report(ledger: list, failures: list, *, date: str, store: str = "",
294
+ window: int = 5) -> dict:
295
+ """纯函数:两份已读入的记录 + 参数 → 报表(**无 IO、无时间依赖**,可纯夹具回归)。"""
296
+ def _store_ok(s: str) -> bool:
297
+ return (not store) or (s == store)
298
+
299
+ # ── 当日事件(台账)────────────────────────────────────────────────────
300
+ day_events = [e for e in ledger
301
+ if ledger_date_of(e) == date and _store_ok(store_of(e))]
302
+ by_case: dict[str, list] = {}
303
+ for e in day_events:
304
+ by_case.setdefault(case_of(e), []).append(e)
305
+ for lst in by_case.values():
306
+ lst.sort(key=lambda x: (float(x.get("ts") or 0), int(x.get("_line") or 0)))
307
+
308
+ # 店铺回填(台账事件常没有 store 字段 ⇒ 从当日失败诊断里按 record_id 补)
309
+ rec_store = {}
310
+ for r in failures:
311
+ rid = str(r.get("record_id") or "")
312
+ if rid and r.get("store"):
313
+ rec_store.setdefault(rid, str(r["store"]))
314
+
315
+ def _store_of_case(cid: str) -> str:
316
+ for e in by_case.get(cid, []):
317
+ s = store_of(e)
318
+ if s:
319
+ return s
320
+ return rec_store.get(record_of(cid), "") or "(未知)"
321
+
322
+ # ── ① 当日 case 数 / 终态分布 ──────────────────────────────────────────
323
+ terminal: dict[str, int] = {"done": 0, "failed": 0, "blocked": 0, OTHER: 0}
324
+ case_rows = []
325
+ failed_ever = 0
326
+ for cid, evs in sorted(by_case.items()):
327
+ states = [(e, state_bucket(e.get("event"))) for e in evs]
328
+ states = [(e, b) for e, b in states if b]
329
+ last_ev, last_b = (states[-1] if states else (None, OTHER))
330
+ terminal[last_b] = terminal.get(last_b, 0) + 1
331
+ ever = any(str(e.get("event") or "") == "failed" for e in evs)
332
+ failed_ever += 1 if ever else 0
333
+ case_rows.append({
334
+ "case": cid, "record_id": record_of(cid), "store": _store_of_case(cid),
335
+ "events_on_day": len(evs),
336
+ "failed_ever": ever,
337
+ "last_state_event": (last_ev or {}).get("event") or "",
338
+ "last_state_ts": int(last_ev.get("ts") or 0) if last_ev else None,
339
+ "terminal": last_b,
340
+ "src": [f"{(last_ev or {}).get('_src', '')}:{(last_ev or {}).get('_line', '')}"]
341
+ if last_ev else [],
342
+ })
343
+
344
+ # ── ② 失败原因分类计数 ────────────────────────────────────────────────
345
+ day_failures = [r for r in failures
346
+ if failures_date_of(r) == date and _store_ok(str(r.get("store") or ""))]
347
+ day_failed_events = [e for e in day_events if str(e.get("event") or "") == "failed"]
348
+ reasons: list = []
349
+ for r in day_failures:
350
+ text = str(r.get("error_message") or "")
351
+ b, rid = classify_reason(text)
352
+ reasons.append({"bucket": b, "rule": rid, "record_id": str(r.get("record_id") or ""),
353
+ "store": str(r.get("store") or ""), "ts": str(r.get("ts") or ""),
354
+ "step": r.get("failed_step"), "err_type": str(r.get("error_type") or ""),
355
+ "reason": text, "source": "failures",
356
+ "src": f"{r.get('_src')}:{r.get('_line')}"})
357
+ # 台账里 failed 但当日 failures.jsonl **已经**记过同一次失败 ⇒ 跳过(⛔ 不许双重计数):
358
+ # 同一次失败会同时进两处(`_record_failure` 写 failures.jsonl、`_ledger_record_full_failure`
359
+ # 写台账)⇒ 两条都数会把"当日失败数"翻倍,归因表当场失真。
360
+ # 去重键 =(record_id, 分钟级时刻):两处的时间戳都取自同一次失败,分钟必然相同。
361
+ _fail_keys = {(str(r.get("record_id") or ""), str(r.get("ts") or "")[:16])
362
+ for r in day_failures}
363
+ _seen = {(x["record_id"], x["reason"][:60]) for x in reasons}
364
+ deduped = 0
365
+ for e in day_failed_events:
366
+ text = str(e.get("reason") or e.get("error") or "")
367
+ rid = record_of(case_of(e))
368
+ _at = (dt.datetime.fromtimestamp(float(e.get("ts") or 0)).strftime("%Y-%m-%d %H:%M")
369
+ if e.get("ts") else "")
370
+ if (rid, _at) in _fail_keys:
371
+ deduped += 1
372
+ continue
373
+ if (rid, text[:60]) in _seen:
374
+ continue
375
+ b, rule = classify_reason(text)
376
+ reasons.append({"bucket": b, "rule": rule, "record_id": rid, "store": store_of(e),
377
+ "ts": dt.datetime.fromtimestamp(float(e.get("ts") or 0)).strftime("%Y-%m-%d %H:%M:%S")
378
+ if e.get("ts") else "",
379
+ "step": None, "err_type": str(e.get("errType") or ""),
380
+ "reason": text, "source": "ledger",
381
+ "src": f"{e.get('_src')}:{e.get('_line')}"})
382
+ reason_counts = {b: 0 for b in BUCKET_ORDER}
383
+ for x in reasons:
384
+ reason_counts[x["bucket"]] = reason_counts.get(x["bucket"], 0) + 1
385
+
386
+ # ── ③ 小时 × 终态 / 店铺 × 终态 ───────────────────────────────────────
387
+ hour_state: dict[str, dict] = {}
388
+ store_state: dict[str, dict] = {}
389
+ for row in case_rows:
390
+ ts = row.get("last_state_ts")
391
+ hour = dt.datetime.fromtimestamp(ts).strftime("%H") if ts else "??"
392
+ h = hour_state.setdefault(hour, {k: 0 for k in ("done", "failed", "blocked", OTHER)})
393
+ h[row["terminal"]] = h.get(row["terminal"], 0) + 1
394
+ s = store_state.setdefault(row["store"] or "(未知)",
395
+ {k: 0 for k in ("done", "failed", "blocked", OTHER)})
396
+ s[row["terminal"]] = s.get(row["terminal"], 0) + 1
397
+
398
+ # ── ④ 反事实三态(逐条失败)────────────────────────────────────────────
399
+ counter = []
400
+ for x in reasons:
401
+ cf = counterfactual(x["reason"], window)
402
+ counter.append({**{k: x[k] for k in ("record_id", "store", "ts", "bucket", "reason", "src")},
403
+ "window": window, **cf})
404
+ cf_counts = {"would_succeed": 0, "would_fail": 0, "insufficient": 0}
405
+ for c in counter:
406
+ cf_counts[c["verdict"]] = cf_counts.get(c["verdict"], 0) + 1
407
+
408
+ # ── ⑤ 与手动路径对照的时间线 ──────────────────────────────────────────
409
+ timelines = []
410
+ for cid, evs in sorted(by_case.items()):
411
+ fails = [e for e in evs if str(e.get("event") or "") == "failed"]
412
+ dones = [e for e in evs if str(e.get("event") or "") == "done"]
413
+ manual = [e for e in dones if str(e.get("by") or "") not in ("", "orchestrator")]
414
+ if fails and manual:
415
+ timelines.append({
416
+ "case": cid, "record_id": record_of(cid), "store": _store_of_case(cid),
417
+ "auto_failed": [{"ts": int(e.get("ts") or 0),
418
+ "at": dt.datetime.fromtimestamp(float(e.get("ts") or 0))
419
+ .strftime("%Y-%m-%d %H:%M:%S"),
420
+ "text": str(e.get("reason") or e.get("error") or "")[:200],
421
+ "src": f"{e.get('_src')}:{e.get('_line')}"} for e in fails],
422
+ "manual_done": [{"ts": int(e.get("ts") or 0),
423
+ "at": dt.datetime.fromtimestamp(float(e.get("ts") or 0))
424
+ .strftime("%Y-%m-%d %H:%M:%S"),
425
+ "by": str(e.get("by") or ""),
426
+ "text": str(e.get("detail") or e.get("reason") or "")[:200],
427
+ "src": f"{e.get('_src')}:{e.get('_line')}"} for e in manual],
428
+ })
429
+
430
+ return {
431
+ "schema": "argent-full-day-attribution/1",
432
+ "date": date,
433
+ "filters": {"store": store or None, "window": window},
434
+ "sources": {
435
+ "ledger": {"events_total": len(ledger), "events_on_day": len(day_events)},
436
+ "failures": {"records_total": len(failures), "records_on_day": len(day_failures)},
437
+ },
438
+ "cases": {"count": len(case_rows), "terminal": terminal,
439
+ # 「曾失败」与「终态=失败」是**两个不同的数**:一张票当天失败过、后来人工补完
440
+ # ⇒ 终态算 done,但它确实是自动路径的一次失败(成功率口径要看这个数)。
441
+ "failed_ever": failed_ever, "rows": case_rows},
442
+ "failures": {"count": len(reasons), "by_reason": reason_counts, "rows": reasons,
443
+ "ledger_only": len(reasons) - len(day_failures),
444
+ "deduped_ledger_events": deduped},
445
+ "matrix": {"by_hour": hour_state, "by_store": store_state},
446
+ "counterfactual": {"window": window, "counts": cf_counts, "rows": counter,
447
+ "action_if_capacity": _CAPACITY_ACTION},
448
+ "timelines": {"count": len(timelines), "rows": timelines},
449
+ }
450
+
451
+
452
+ # ══════════════════════════════════════════════════════════════════════════
453
+ # 渲染(人读)
454
+ # ══════════════════════════════════════════════════════════════════════════
455
+
456
+ def render(rep: dict) -> str:
457
+ L = []
458
+ L.append(f"═══ FULL 当日归因 {rep['date']} ═══"
459
+ + (f"(店铺={rep['filters']['store']})" if rep["filters"]["store"] else "")
460
+ + f" 反事实窗口={rep['filters']['window']}")
461
+ L.append(f"源:台账 {rep['sources']['ledger']['events_on_day']}/"
462
+ f"{rep['sources']['ledger']['events_total']} 条(当日/全部);"
463
+ f"失败诊断 {rep['sources']['failures']['records_on_day']}/"
464
+ f"{rep['sources']['failures']['records_total']} 条")
465
+ L.append("")
466
+ c = rep["cases"]
467
+ L.append(f"① 当日 case 数 = {c['count']}|终态:" +
468
+ " / ".join(f"{k} {v}" for k, v in c["terminal"].items())
469
+ + f"|当日**曾失败**的 case = {c.get('failed_ever', 0)}"
470
+ + "(终态=done 里可能含「失败后人工补完」,两个数口径不同)")
471
+ for r in c["rows"]:
472
+ L.append(f" · {r['record_id']} [{r['store']}] 当日事件 {r['events_on_day']} 条 ⇒ "
473
+ f"{r['terminal']}(末条状态事件={r['last_state_event'] or '(无)'};{r['src'][0] if r['src'] else ''})")
474
+ L.append("")
475
+ f = rep["failures"]
476
+ L.append(f"② 失败记录 {f['count']} 条|分类:" +
477
+ " | ".join(f"{k} {f['by_reason'].get(k, 0)}" for k in BUCKET_ORDER))
478
+ for x in f["rows"]:
479
+ L.append(f" · [{x['bucket']}] {x['record_id']} {x['ts']} step={x['step']} "
480
+ f"rule={x['rule']}|{x['reason'][:90]}({x['src']})")
481
+ L.append("")
482
+ L.append("③ 小时 × 终态:")
483
+ for h in sorted(rep["matrix"]["by_hour"]):
484
+ d = rep["matrix"]["by_hour"][h]
485
+ L.append(f" {h} 时:" + " ".join(f"{k}={d.get(k, 0)}" for k in ("done", "failed", "blocked", OTHER)))
486
+ L.append(" 店铺 × 终态:")
487
+ for s in sorted(rep["matrix"]["by_store"]):
488
+ d = rep["matrix"]["by_store"][s]
489
+ L.append(f" {s}:" + " ".join(f"{k}={d.get(k, 0)}" for k in ("done", "failed", "blocked", OTHER)))
490
+ L.append("")
491
+ cf = rep["counterfactual"]
492
+ L.append(f"④ 反事实(窗口={cf['window']} ∧ 仓容可用):"
493
+ f"本可成功 {cf['counts'].get('would_succeed', 0)} / "
494
+ f"仍会失败 {cf['counts'].get('would_fail', 0)} / "
495
+ f"证据不足 {cf['counts'].get('insufficient', 0)}")
496
+ for x in cf["rows"]:
497
+ L.append(f" · [{x['verdict']}] {x['record_id']}(axis={x['axis']}){x['basis']}")
498
+ L.append("")
499
+ t = rep["timelines"]
500
+ L.append(f"⑤ 与手动路径对照的时间线:{t['count']} 个 case")
501
+ for row in t["rows"]:
502
+ L.append(f" · {row['record_id']} [{row['store']}]")
503
+ for e in row["auto_failed"]:
504
+ L.append(f" 自动 failed {e['at']}|{e['text'][:80]}({e['src']})")
505
+ for e in row["manual_done"]:
506
+ L.append(f" 人工 done {e['at']}|by={e['by']}|{e['text'][:80]}({e['src']})")
507
+ L.append("")
508
+ L.append("(本报表**只读**:零网络、零写入;每条结论都带计数与证据行号,可复算)")
509
+ return "\n".join(L)
510
+
511
+
512
+ def _main(argv: Optional[list[str]] = None) -> int:
513
+ p = argparse.ArgumentParser(
514
+ prog="analyze-full-day.py",
515
+ description="当日 FULL 归因报表(只读、零网络、零写入;数据源=统一台账 + 失败诊断)")
516
+ p.add_argument("--ledger", default=str(DEFAULT_LEDGER), help=f"统一台账(默认 {DEFAULT_LEDGER})")
517
+ p.add_argument("--failures", default=str(DEFAULT_FAILURES),
518
+ help=f"失败诊断 JSONL(默认 {DEFAULT_FAILURES})")
519
+ p.add_argument("--date", default=dt.date.today().isoformat(),
520
+ help="统计日期 YYYY-MM-DD(默认今天,本地时区)")
521
+ p.add_argument("--store", default="", help="只看该店铺(精确匹配)")
522
+ p.add_argument("--window", type=int, default=None,
523
+ help="反事实前提里的顺延窗口(天);缺省 = 当前生效值"
524
+ "(`flow_config.appointment_max_day()`:env → 声明 default)")
525
+ p.add_argument("--json", action="store_true", help="机读形状(含每条的 src/行号)")
526
+ a = p.parse_args(argv)
527
+
528
+ try:
529
+ dt.date.fromisoformat(a.date)
530
+ except ValueError:
531
+ print(f"⛔ --date 必须是 YYYY-MM-DD:{a.date}", file=sys.stderr)
532
+ return 2
533
+
534
+ window = a.window
535
+ if window is None:
536
+ try:
537
+ sys.path.insert(0, str(HERE))
538
+ from flow_config import appointment_max_day as _amd # noqa: E402
539
+ window = int(_amd())
540
+ except Exception: # noqa: BLE001
541
+ window = 5
542
+ if window < 1:
543
+ print("⛔ --window 必须 ≥1", file=sys.stderr)
544
+ return 2
545
+
546
+ ledger_path, fail_path = pathlib.Path(a.ledger), pathlib.Path(a.failures)
547
+ ledger, failures = read_jsonl(ledger_path), read_jsonl(fail_path)
548
+ # ⛔ 「读不到文件」与「文件在但当天没数据」必须分得开:前者是**用法/环境问题**(exit 2),
549
+ # 后者是**事实**(0 条,exit 0)—— 混成一种会把"路径写错"报成"今天全成功"。
550
+ missing = [str(x) for x in (ledger_path, fail_path) if not x.exists()]
551
+ if len(missing) == 2:
552
+ print(f"⛔ 两个数据源都读不到(路径写错?):{', '.join(missing)}", file=sys.stderr)
553
+ return 2
554
+
555
+ rep = build_report(ledger, failures, date=a.date, store=a.store, window=window)
556
+ rep["sources"]["paths"] = {"ledger": str(ledger_path), "failures": str(fail_path),
557
+ "missing": missing}
558
+ if a.json:
559
+ print(json.dumps(rep, ensure_ascii=False, indent=2))
560
+ else:
561
+ print(render(rep))
562
+ if missing:
563
+ print(f"⚠️ 数据源缺失(按空处理):{', '.join(missing)}", file=sys.stderr)
564
+ return 0
565
+
566
+
567
+ if __name__ == "__main__":
568
+ sys.exit(_main())