@team-agent/installer 0.2.11 → 0.3.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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1204 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1207 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +557 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1084 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +526 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1101 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +237 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +272 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +410 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +489 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +2109 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +985 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +710 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +187 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +468 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +743 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +329 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +553 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +578 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +659 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +765 -0
  172. package/crates/team-agent/src/tmux_backend.rs +810 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +118 -112
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,800 @@
1
+ //! step 2 · model — `simple_yaml` 方言移植(真相源 `src/team_agent/simple_yaml.py`)。
2
+ //!
3
+ //! Team Agent **不**用标准 YAML:它有一份零依赖、字节级特判的子集 `loads`/`dumps`,
4
+ //! spec / role-doc front-matter / team_state 全靠它解析与回写。**禁用 serde_yaml**——
5
+ //! 标准 YAML 在空白 / 引号 / `|` block scalar / `[...]`(`ast.literal_eval`)/ int 解析 /
6
+ //! dict-vs-list-上下文 dump 的诸多怪癖上都会漂移 → spec 解析不一致 → 下游全错(§7)。
7
+ //! 这里移植的是**那份方言本身**,golden 字节由 Python 真相源双跑锁死(§4.2)。
8
+ //!
9
+ //! 与 Python `Any` 语义对齐的 [`Value`]:
10
+ //! - `dict` 保持插入顺序 → `Map(Vec<(String, Value)>)`(非 `BTreeMap`,否则 JSON 路径乱序)。
11
+ //! - int 用 `i64`(真相源 Python 任意精度;实际语料只有 version/priority/timeout 等小整数)。
12
+ //! - float 仅经 JSON 顶层路径出现(`{`/`[` 开头 → `json.loads`),`dumps` 时按 Python
13
+ //! `json.dumps(str(f))` 走带引号字符串分支。
14
+ //!
15
+ //! §10:纯层无 panic —— `loads` 校验失败返 [`ModelError`];`dumps` 不会失败。
16
+
17
+ use serde::de::{self, Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
18
+
19
+ use crate::model::errors::ModelError;
20
+
21
+ /// 与 Python `simple_yaml` 的 `Any` 一一对应的动态值。
22
+ ///
23
+ /// `Map` 用有序 `Vec<(String, Value)>` 复刻 Python dict 的插入顺序(`dumps` 按此顺序输出,
24
+ /// 字节对拍依赖它)。
25
+ #[derive(Debug, Clone, PartialEq)]
26
+ pub enum Value {
27
+ Null,
28
+ Bool(bool),
29
+ Int(i64),
30
+ /// 仅经 JSON 顶层路径产生(`loads("{...}")`)。`dumps` 走 `json.dumps(str(f))` 分支。
31
+ Float(f64),
32
+ Str(String),
33
+ List(Vec<Value>),
34
+ /// 有序键值对(复刻 Python dict 插入顺序)。
35
+ Map(Vec<(String, Value)>),
36
+ }
37
+
38
+ impl Value {
39
+ fn is_empty_list(&self) -> bool {
40
+ matches!(self, Value::List(v) if v.is_empty())
41
+ }
42
+ fn is_empty_map(&self) -> bool {
43
+ matches!(self, Value::Map(m) if m.is_empty())
44
+ }
45
+ fn is_collection(&self) -> bool {
46
+ matches!(self, Value::List(_) | Value::Map(_))
47
+ }
48
+
49
+ // --- 公开访问器(spec/compiler/routing/state 遍历 spec Value 用;对应 Python dict/list 取值)---
50
+
51
+ /// `Str` → `&str`,否则 `None`。
52
+ pub fn as_str(&self) -> Option<&str> {
53
+ if let Value::Str(s) = self {
54
+ Some(s)
55
+ } else {
56
+ None
57
+ }
58
+ }
59
+ /// `Map` 的有序键值对切片,否则 `None`。
60
+ pub fn as_map(&self) -> Option<&[(String, Value)]> {
61
+ if let Value::Map(m) = self {
62
+ Some(m)
63
+ } else {
64
+ None
65
+ }
66
+ }
67
+ /// `List` 切片,否则 `None`。
68
+ pub fn as_list(&self) -> Option<&[Value]> {
69
+ if let Value::List(l) = self {
70
+ Some(l)
71
+ } else {
72
+ None
73
+ }
74
+ }
75
+ /// 是否 `Map`(对应 Python `isinstance(x, dict)`)。
76
+ pub fn is_map(&self) -> bool {
77
+ matches!(self, Value::Map(_))
78
+ }
79
+ /// `Int` → `i64`,否则 `None`。
80
+ pub fn as_i64(&self) -> Option<i64> {
81
+ if let Value::Int(i) = self {
82
+ Some(*i)
83
+ } else {
84
+ None
85
+ }
86
+ }
87
+ /// dict get:首个匹配 key 的值(`insert_ordered` 已去重 → 首即唯一)。非 Map → `None`。
88
+ pub fn get(&self, key: &str) -> Option<&Value> {
89
+ self.as_map()?.iter().find(|(k, _)| k == key).map(|(_, v)| v)
90
+ }
91
+ /// Python 真值语义:None/Null/false/0/""/空集 → false。
92
+ pub fn is_truthy(&self) -> bool {
93
+ match self {
94
+ Value::Null => false,
95
+ Value::Bool(b) => *b,
96
+ Value::Int(i) => *i != 0,
97
+ Value::Float(f) => *f != 0.0,
98
+ Value::Str(s) => !s.is_empty(),
99
+ Value::List(l) => !l.is_empty(),
100
+ Value::Map(m) => !m.is_empty(),
101
+ }
102
+ }
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // loads
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /// 移植 `simple_yaml.loads`。
110
+ ///
111
+ /// `text.lstrip()` 以 `{`/`[` 开头 → 整段走 `json.loads`(JSON 子集,保持键序)。
112
+ /// 否则按 Python 的缩进块方言解析。无法解析返 [`ModelError::Validation`]。
113
+ pub fn loads(text: &str) -> Result<Value, ModelError> {
114
+ let stripped = text.trim_start();
115
+ if stripped.starts_with('{') || stripped.starts_with('[') {
116
+ return loads_json(text);
117
+ }
118
+ let lines: Vec<&str> = split_lines(text);
119
+ let p = Parser { lines: &lines };
120
+ let (value, mut index) = p.parse_block(0, 0)?;
121
+ while index < lines.len() && !content(lines[index]) {
122
+ index += 1;
123
+ }
124
+ if index != lines.len() {
125
+ return Err(ModelError::Validation(format!(
126
+ "unexpected content at line {}: {}",
127
+ index + 1,
128
+ lines[index]
129
+ )));
130
+ }
131
+ Ok(value)
132
+ }
133
+
134
+ /// Python `str.splitlines()`:无尾随空行(末尾 `\n` 不产生空元素),`\r\n`/`\r` 也算行界。
135
+ /// 真相源语料是 `\n` 结尾的 UTF-8 文本;这里按 `\n` 切并剥掉每行尾随 `\r`,与 Python 对齐。
136
+ fn split_lines(text: &str) -> Vec<&str> {
137
+ if text.is_empty() {
138
+ return Vec::new();
139
+ }
140
+ let mut out: Vec<&str> = Vec::new();
141
+ for part in text.split('\n') {
142
+ out.push(part.strip_suffix('\r').unwrap_or(part));
143
+ }
144
+ // split('\n') 在尾随 '\n' 时产生一个末尾空串 —— Python splitlines 不会,去掉它。
145
+ if text.ends_with('\n') {
146
+ out.pop();
147
+ }
148
+ out
149
+ }
150
+
151
+ fn loads_json(text: &str) -> Result<Value, ModelError> {
152
+ let mut de = serde_json::Deserializer::from_str(text);
153
+ let value =
154
+ Value::deserialize(&mut de).map_err(|e| ModelError::Validation(format!("json: {e}")))?;
155
+ de.end()
156
+ .map_err(|e| ModelError::Validation(format!("json: {e}")))?;
157
+ Ok(value)
158
+ }
159
+
160
+ struct Parser<'a> {
161
+ lines: &'a [&'a str],
162
+ }
163
+
164
+ impl<'a> Parser<'a> {
165
+ fn parse_block(&self, mut index: usize, indent: usize) -> Result<(Value, usize), ModelError> {
166
+ index = skip_blank(self.lines, index);
167
+ if index >= self.lines.len() {
168
+ return Ok((Value::Null, index));
169
+ }
170
+ let current_indent = line_indent(self.lines[index]);
171
+ if current_indent < indent {
172
+ return Ok((Value::Null, index));
173
+ }
174
+ if self.lines[index].trim().starts_with("- ") {
175
+ self.parse_list(index, current_indent)
176
+ } else {
177
+ self.parse_dict(index, current_indent)
178
+ }
179
+ }
180
+
181
+ fn parse_dict(&self, mut index: usize, indent: usize) -> Result<(Value, usize), ModelError> {
182
+ let mut obj: Vec<(String, Value)> = Vec::new();
183
+ while index < self.lines.len() {
184
+ if !content(self.lines[index]) {
185
+ index += 1;
186
+ continue;
187
+ }
188
+ let line_ind = line_indent(self.lines[index]);
189
+ if line_ind < indent {
190
+ break;
191
+ }
192
+ if line_ind > indent {
193
+ return Err(ModelError::Validation(format!(
194
+ "unexpected indentation at line {}: {}",
195
+ index + 1,
196
+ self.lines[index]
197
+ )));
198
+ }
199
+ let stripped = self.lines[index].trim();
200
+ if stripped.starts_with("- ") {
201
+ break;
202
+ }
203
+ let (key, raw) = split_key_value(stripped, index)?;
204
+ let value = if raw == "|" {
205
+ let (v, ni) = self.parse_block_scalar(index + 1, indent + 2);
206
+ index = ni;
207
+ v
208
+ } else if raw.is_empty() {
209
+ let (v, ni) = self.parse_block(index + 1, indent + 2)?;
210
+ index = ni;
211
+ v
212
+ } else {
213
+ index += 1;
214
+ parse_scalar(&raw)
215
+ };
216
+ insert_ordered(&mut obj, key, value);
217
+ }
218
+ Ok((Value::Map(obj), index))
219
+ }
220
+
221
+ fn parse_list(&self, mut index: usize, indent: usize) -> Result<(Value, usize), ModelError> {
222
+ let mut items: Vec<Value> = Vec::new();
223
+ while index < self.lines.len() {
224
+ if !content(self.lines[index]) {
225
+ index += 1;
226
+ continue;
227
+ }
228
+ let line_ind = line_indent(self.lines[index]);
229
+ if line_ind < indent {
230
+ break;
231
+ }
232
+ if line_ind != indent {
233
+ return Err(ModelError::Validation(format!(
234
+ "unexpected list indentation at line {}: {}",
235
+ index + 1,
236
+ self.lines[index]
237
+ )));
238
+ }
239
+ let stripped = self.lines[index].trim();
240
+ if !stripped.starts_with("- ") {
241
+ break;
242
+ }
243
+ let item_text = stripped[2..].trim();
244
+ if item_text.is_empty() {
245
+ let (value, ni) = self.parse_block(index + 1, indent + 2)?;
246
+ items.push(value);
247
+ index = ni;
248
+ continue;
249
+ }
250
+ if looks_like_key_value(item_text) {
251
+ let (key, raw) = split_key_value(item_text, index)?;
252
+ let mut item: Vec<(String, Value)> = Vec::new();
253
+ let (value, mut next_index) = if raw == "|" {
254
+ self.parse_block_scalar(index + 1, indent + 2)
255
+ } else if raw.is_empty() {
256
+ self.parse_block(index + 1, indent + 2)?
257
+ } else {
258
+ (parse_scalar(&raw), index + 1)
259
+ };
260
+ insert_ordered(&mut item, key, value);
261
+ if next_index < self.lines.len()
262
+ && line_indent(self.lines[next_index]) == indent + 2
263
+ {
264
+ let (extra, ni) = self.parse_dict(next_index, indent + 2)?;
265
+ if let Value::Map(pairs) = extra {
266
+ for (k, v) in pairs {
267
+ insert_ordered(&mut item, k, v);
268
+ }
269
+ }
270
+ next_index = ni;
271
+ }
272
+ items.push(Value::Map(item));
273
+ index = next_index;
274
+ } else {
275
+ items.push(parse_scalar(item_text));
276
+ index += 1;
277
+ }
278
+ }
279
+ Ok((Value::List(items), index))
280
+ }
281
+
282
+ /// `|` block scalar:取 `indent` 之后的内容,空行记为 `""`,末尾 `rstrip()` + `"\n"`。
283
+ fn parse_block_scalar(&self, mut index: usize, indent: usize) -> (Value, usize) {
284
+ let mut block: Vec<String> = Vec::new();
285
+ while index < self.lines.len() {
286
+ if self.lines[index].trim().is_empty() {
287
+ block.push(String::new());
288
+ index += 1;
289
+ continue;
290
+ }
291
+ let line_ind = line_indent(self.lines[index]);
292
+ if line_ind < indent {
293
+ break;
294
+ }
295
+ block.push(slice_from_byte(self.lines[index], indent));
296
+ index += 1;
297
+ }
298
+ let joined = block.join("\n");
299
+ (Value::Str(format!("{}\n", py_rstrip(&joined))), index)
300
+ }
301
+ }
302
+
303
+ /// Python dict 赋值语义:键已存在则覆盖**值**但保留原插入位置。
304
+ fn insert_ordered(map: &mut Vec<(String, Value)>, key: String, value: Value) {
305
+ if let Some(slot) = map.iter_mut().find(|(k, _)| *k == key) {
306
+ slot.1 = value;
307
+ } else {
308
+ map.push((key, value));
309
+ }
310
+ }
311
+
312
+ /// 移植 `_parse_scalar`。
313
+ fn parse_scalar(raw: &str) -> Value {
314
+ match raw {
315
+ "null" | "Null" | "NULL" | "~" => return Value::Null,
316
+ "true" | "True" | "TRUE" => return Value::Bool(true),
317
+ "false" | "False" | "FALSE" => return Value::Bool(false),
318
+ _ => {}
319
+ }
320
+ if let Some(n) = py_int(raw) {
321
+ return Value::Int(n);
322
+ }
323
+ if raw.starts_with('[') && raw.ends_with(']') {
324
+ // ast.literal_eval(raw):成功 → list,失败(SyntaxError/ValueError)→ 原样 str。
325
+ return match literal_eval_list(raw) {
326
+ Some(list) => list,
327
+ None => Value::Str(raw.to_string()),
328
+ };
329
+ }
330
+ if raw == "{}" {
331
+ return Value::Map(Vec::new());
332
+ }
333
+ if (raw.starts_with('"') && raw.ends_with('"')) || (raw.starts_with('\'') && raw.ends_with('\''))
334
+ {
335
+ if raw.len() < 2 {
336
+ // 单个引号字符:不是合法的成对引号,落到末尾 return raw。
337
+ return Value::Str(raw.to_string());
338
+ }
339
+ return match literal_eval_quoted(raw) {
340
+ Some(s) => Value::Str(s),
341
+ // ast.literal_eval 失败 → Python 退化为 raw[1:-1](剥首尾各一字节)。
342
+ None => Value::Str(strip_one_each_end(raw)),
343
+ };
344
+ }
345
+ Value::Str(raw.to_string())
346
+ }
347
+
348
+ /// 移植 Python `int(raw)`:十进制,允许前导 `+`/`-`、内部单下划线(不可前/尾/连续),
349
+ /// 拒绝空、浮点、十六进制、`e` 记数等。失败返 `None`(→ 落到字符串)。
350
+ /// 任意精度退化为 `i64`:语料只有小整数,超出 `i64` 时返 `None`(回退字符串)以免 panic。
351
+ fn py_int(raw: &str) -> Option<i64> {
352
+ let bytes = raw.as_bytes();
353
+ if bytes.is_empty() {
354
+ return None;
355
+ }
356
+ let mut i = 0usize;
357
+ let neg = match bytes[0] {
358
+ b'+' => {
359
+ i = 1;
360
+ false
361
+ }
362
+ b'-' => {
363
+ i = 1;
364
+ true
365
+ }
366
+ _ => false,
367
+ };
368
+ let digits = &raw[i..];
369
+ if digits.is_empty() {
370
+ return None;
371
+ }
372
+ // 下划线规则:不能开头/结尾,不能连续,两侧必须是数字。
373
+ let db = digits.as_bytes();
374
+ let mut prev_underscore = false;
375
+ for (idx, &c) in db.iter().enumerate() {
376
+ if c == b'_' {
377
+ if idx == 0 || idx == db.len() - 1 || prev_underscore {
378
+ return None;
379
+ }
380
+ prev_underscore = true;
381
+ } else if c.is_ascii_digit() {
382
+ prev_underscore = false;
383
+ } else {
384
+ return None;
385
+ }
386
+ }
387
+ let cleaned: String = digits.chars().filter(|&c| c != '_').collect();
388
+ let magnitude: i64 = cleaned.parse().ok()?;
389
+ Some(if neg { -magnitude } else { magnitude })
390
+ }
391
+
392
+ /// `ast.literal_eval` 子集,仅覆盖 `[...]` 中由标量(int / 引号字符串 / bool / None)
393
+ /// 组成的列表 —— 这是真相源唯一会命中此路径的形态(如 `tools: ['a', 'b']`)。
394
+ /// 任何无法以此子集解析的输入返 `None`(→ 调用方回退为原始字符串,与 Python 一致)。
395
+ fn literal_eval_list(raw: &str) -> Option<Value> {
396
+ let inner = &raw[1..raw.len() - 1];
397
+ let inner_trim = inner.trim();
398
+ if inner_trim.is_empty() {
399
+ return Some(Value::List(Vec::new()));
400
+ }
401
+ let mut items: Vec<Value> = Vec::new();
402
+ for part in split_top_level_commas(inner_trim)? {
403
+ items.push(literal_eval_atom(part.trim())?);
404
+ }
405
+ Some(Value::List(items))
406
+ }
407
+
408
+ /// 按顶层逗号切分(不进入引号 / 不处理嵌套,够覆盖语料中的扁平列表)。
409
+ /// 末尾允许一个尾随逗号(Python 列表字面量允许)。
410
+ fn split_top_level_commas(s: &str) -> Option<Vec<&str>> {
411
+ let bytes = s.as_bytes();
412
+ let mut parts: Vec<&str> = Vec::new();
413
+ let mut start = 0usize;
414
+ let mut quote: Option<u8> = None;
415
+ let mut i = 0usize;
416
+ while i < bytes.len() {
417
+ let c = bytes[i];
418
+ match quote {
419
+ Some(q) => {
420
+ if c == q {
421
+ quote = None;
422
+ }
423
+ }
424
+ None => {
425
+ if c == b'\'' || c == b'"' {
426
+ quote = Some(c);
427
+ } else if c == b'[' || c == b']' || c == b'{' || c == b'}' {
428
+ // 嵌套结构超出本子集 → 让 ast 在 Python 里也可能成功但这里保守失败。
429
+ return None;
430
+ } else if c == b',' {
431
+ parts.push(&s[start..i]);
432
+ start = i + 1;
433
+ }
434
+ }
435
+ }
436
+ i += 1;
437
+ }
438
+ if quote.is_some() {
439
+ return None;
440
+ }
441
+ let tail = s[start..].trim();
442
+ if !tail.is_empty() {
443
+ parts.push(&s[start..]);
444
+ }
445
+ Some(parts)
446
+ }
447
+
448
+ fn literal_eval_atom(tok: &str) -> Option<Value> {
449
+ if tok.is_empty() {
450
+ return None;
451
+ }
452
+ match tok {
453
+ "None" => return Some(Value::Null),
454
+ "True" => return Some(Value::Bool(true)),
455
+ "False" => return Some(Value::Bool(false)),
456
+ _ => {}
457
+ }
458
+ let bytes = tok.as_bytes();
459
+ let first = bytes[0];
460
+ if (first == b'"' || first == b'\'') && tok.len() >= 2 && bytes[bytes.len() - 1] == first {
461
+ return literal_eval_quoted(tok).map(Value::Str);
462
+ }
463
+ // Python int literal(literal_eval 允许下划线/正负号)。
464
+ if let Some(n) = py_int(tok) {
465
+ return Some(Value::Int(n));
466
+ }
467
+ None
468
+ }
469
+
470
+ /// `ast.literal_eval` 一个引号字符串字面量。支持 `\n \t \" \' \\` 等常见转义;
471
+ /// 解析失败返 `None`。
472
+ fn literal_eval_quoted(raw: &str) -> Option<String> {
473
+ let bytes = raw.as_bytes();
474
+ if raw.len() < 2 {
475
+ return None;
476
+ }
477
+ let q = bytes[0];
478
+ if (q != b'"' && q != b'\'') || bytes[raw.len() - 1] != q {
479
+ return None;
480
+ }
481
+ let inner = &raw[1..raw.len() - 1];
482
+ let mut out = String::with_capacity(inner.len());
483
+ let mut chars = inner.chars().peekable();
484
+ while let Some(c) = chars.next() {
485
+ if c == '\\' {
486
+ match chars.next() {
487
+ Some('n') => out.push('\n'),
488
+ Some('t') => out.push('\t'),
489
+ Some('r') => out.push('\r'),
490
+ Some('\\') => out.push('\\'),
491
+ Some('\'') => out.push('\''),
492
+ Some('"') => out.push('"'),
493
+ Some('0') => out.push('\0'),
494
+ Some(other) => {
495
+ out.push('\\');
496
+ out.push(other);
497
+ }
498
+ None => return None,
499
+ }
500
+ } else if c == q as char {
501
+ // 未转义的同种引号出现在内部 → 非法字面量。
502
+ return None;
503
+ } else {
504
+ out.push(c);
505
+ }
506
+ }
507
+ Some(out)
508
+ }
509
+
510
+ /// `raw[1:-1]`:按字节剥掉首尾各一字节(Python 切片语义)。语料中引号为 ASCII。
511
+ fn strip_one_each_end(raw: &str) -> String {
512
+ let mut chars = raw.chars();
513
+ chars.next();
514
+ chars.next_back();
515
+ chars.as_str().to_string()
516
+ }
517
+
518
+ // ---------------------------------------------------------------------------
519
+ // dumps
520
+ // ---------------------------------------------------------------------------
521
+
522
+ /// 移植 `simple_yaml.dumps`:`"\n".join(_dump(value, indent)) + "\n"`。
523
+ pub fn dumps(value: &Value) -> String {
524
+ let lines = dump(value, 0);
525
+ let mut out = lines.join("\n");
526
+ out.push('\n');
527
+ out
528
+ }
529
+
530
+ fn dump(value: &Value, indent: usize) -> Vec<String> {
531
+ let pad = " ".repeat(indent);
532
+ match value {
533
+ Value::Map(pairs) => {
534
+ let mut lines: Vec<String> = Vec::new();
535
+ for (key, item) in pairs {
536
+ if item.is_empty_list() {
537
+ lines.push(format!("{pad}{key}: []"));
538
+ } else if item.is_empty_map() {
539
+ lines.push(format!("{pad}{key}: {{}}"));
540
+ } else if item.is_collection() {
541
+ lines.push(format!("{pad}{key}:"));
542
+ lines.extend(dump(item, indent + 2));
543
+ } else if let Value::Str(s) = item {
544
+ if s.contains('\n') {
545
+ lines.push(format!("{pad}{key}: |"));
546
+ for block_line in py_splitlines(py_rstrip_newlines(s)) {
547
+ lines.push(format!("{pad} {block_line}"));
548
+ }
549
+ } else {
550
+ lines.push(format!("{pad}{key}: {}", format_scalar(item)));
551
+ }
552
+ } else {
553
+ lines.push(format!("{pad}{key}: {}", format_scalar(item)));
554
+ }
555
+ }
556
+ lines
557
+ }
558
+ Value::List(items) => {
559
+ let mut lines: Vec<String> = Vec::new();
560
+ for item in items {
561
+ match item {
562
+ Value::Map(pairs) => {
563
+ if pairs.is_empty() {
564
+ lines.push(format!("{pad}- {{}}"));
565
+ continue;
566
+ }
567
+ let mut first = true;
568
+ for (key, child) in pairs {
569
+ let prefix = if first { "- " } else { " " };
570
+ if child.is_empty_list() {
571
+ lines.push(format!("{pad}{prefix}{key}: []"));
572
+ } else if child.is_empty_map() {
573
+ lines.push(format!("{pad}{prefix}{key}: {{}}"));
574
+ } else if child.is_collection() {
575
+ lines.push(format!("{pad}{prefix}{key}:"));
576
+ lines.extend(dump(child, indent + 4));
577
+ } else {
578
+ // 注意:此分支**不**对多行字符串做 `|`,与 Python 一致。
579
+ lines.push(format!(
580
+ "{pad}{prefix}{key}: {}",
581
+ format_scalar(child)
582
+ ));
583
+ }
584
+ first = false;
585
+ }
586
+ }
587
+ Value::List(_) => {
588
+ lines.push(format!("{pad}-"));
589
+ lines.extend(dump(item, indent + 2));
590
+ }
591
+ _ => {
592
+ lines.push(format!("{pad}- {}", format_scalar(item)));
593
+ }
594
+ }
595
+ }
596
+ lines
597
+ }
598
+ _ => vec![format!("{pad}{}", format_scalar(value))],
599
+ }
600
+ }
601
+
602
+ /// 移植 `_format_scalar`。非 None/bool/int → `json.dumps(str(value), ensure_ascii=False)`。
603
+ fn format_scalar(value: &Value) -> String {
604
+ match value {
605
+ Value::Null => "null".to_string(),
606
+ Value::Bool(true) => "true".to_string(),
607
+ Value::Bool(false) => "false".to_string(),
608
+ Value::Int(n) => n.to_string(),
609
+ Value::Float(f) => json_quote(&py_float_str(*f)),
610
+ Value::Str(s) => json_quote(s),
611
+ // collection / empty 由 dump() 的上层分支拦截;此处理论不可达,保守按 str 化。
612
+ Value::List(_) | Value::Map(_) => json_quote("[object]"),
613
+ }
614
+ }
615
+
616
+ /// `json.dumps(s, ensure_ascii=False)`:Rust `serde_json` 的字符串转义与 Python 对齐
617
+ /// (`\b \f \n \r \t`、控制字符 `\uXXXX`、`/` 不转义、非 ASCII 原样保留)。
618
+ fn json_quote(s: &str) -> String {
619
+ // serde_json::to_string 对 &str 不会失败(无 IO),但 §10 禁 unwrap:回退为手工引号。
620
+ serde_json::to_string(s).unwrap_or_else(|_| format!("\"{s}\""))
621
+ }
622
+
623
+ /// Python `str(float)`:对本语料(仅 JSON 路径偶发)足够。整数值浮点带 `.0`。
624
+ fn py_float_str(f: f64) -> String {
625
+ if f.is_nan() {
626
+ return "nan".to_string();
627
+ }
628
+ if f.is_infinite() {
629
+ return if f > 0.0 { "inf" } else { "-inf" }.to_string();
630
+ }
631
+ if f == f.trunc() && f.abs() < 1e16 {
632
+ return format!("{f:.1}");
633
+ }
634
+ // Rust 默认 `{}` 浮点格式化给最短往返表示,与 Python repr/str 在常见小数上一致。
635
+ format!("{f}")
636
+ }
637
+
638
+ // ---------------------------------------------------------------------------
639
+ // helpers
640
+ // ---------------------------------------------------------------------------
641
+
642
+ /// `_content`:非空且非 `#` 开头(`strip()` 后)。
643
+ fn content(line: &str) -> bool {
644
+ let s = line.trim();
645
+ !s.is_empty() && !s.starts_with('#')
646
+ }
647
+
648
+ fn skip_blank(lines: &[&str], mut index: usize) -> usize {
649
+ while index < lines.len() && !content(lines[index]) {
650
+ index += 1;
651
+ }
652
+ index
653
+ }
654
+
655
+ /// `_indent`:前导**空格**数(`lstrip(" ")` 只剥空格,不含 tab)。
656
+ fn line_indent(line: &str) -> usize {
657
+ line.len() - line.trim_start_matches(' ').len()
658
+ }
659
+
660
+ /// `_split_key_value`:无 `:` 报错;否则按首个 `:` 切,两侧 strip。
661
+ fn split_key_value(stripped: &str, index: usize) -> Result<(String, String), ModelError> {
662
+ match stripped.split_once(':') {
663
+ None => Err(ModelError::Validation(format!(
664
+ "expected key: value at line {}",
665
+ index + 1
666
+ ))),
667
+ Some((key, raw)) => Ok((key.trim().to_string(), raw.trim().to_string())),
668
+ }
669
+ }
670
+
671
+ /// `_looks_like_key_value`:含 `:`,且首个 `:` 之前的 key 非空且只由
672
+ /// 字母数字 / `_` / `-` 组成。
673
+ fn looks_like_key_value(text: &str) -> bool {
674
+ match text.split_once(':') {
675
+ None => false,
676
+ Some((key, _)) => {
677
+ !key.is_empty()
678
+ && key
679
+ .chars()
680
+ .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
681
+ }
682
+ }
683
+ }
684
+
685
+ /// Python `str.rstrip()`(无参):剥尾随空白(空格 / `\t` / `\n` / `\r` / `\f` / `\v` 等)。
686
+ fn py_rstrip(s: &str) -> &str {
687
+ s.trim_end_matches(|c: char| c.is_whitespace())
688
+ }
689
+
690
+ /// Python `str.rstrip("\n")`:仅剥尾随换行符。
691
+ fn py_rstrip_newlines(s: &str) -> &str {
692
+ s.trim_end_matches('\n')
693
+ }
694
+
695
+ /// Python `str.splitlines()`(用于 `_dump` 的 block scalar 拆行)。
696
+ fn py_splitlines(s: &str) -> Vec<&str> {
697
+ if s.is_empty() {
698
+ return Vec::new();
699
+ }
700
+ let mut out: Vec<&str> = Vec::new();
701
+ for part in s.split('\n') {
702
+ out.push(part.strip_suffix('\r').unwrap_or(part));
703
+ }
704
+ if s.ends_with('\n') {
705
+ out.pop();
706
+ }
707
+ out
708
+ }
709
+
710
+ /// 取 `line[indent:]`(字节切片,复刻 Python 切片)。语料缩进与正文为 ASCII/UTF-8,
711
+ /// 缩进位为空格 → `indent` 落在字符边界。越界则返空串。
712
+ fn slice_from_byte(line: &str, indent: usize) -> String {
713
+ if indent >= line.len() {
714
+ return String::new();
715
+ }
716
+ if line.is_char_boundary(indent) {
717
+ line[indent..].to_string()
718
+ } else {
719
+ // 不在字符边界(理论上不会发生,因缩进恒为空格):保守按字符跳过 indent 个码点。
720
+ line.chars().skip(indent).collect()
721
+ }
722
+ }
723
+
724
+ // ---------------------------------------------------------------------------
725
+ // JSON 顶层路径:用 serde 访问者把 JSON 直接读成有序 Value(保留键序)
726
+ // ---------------------------------------------------------------------------
727
+
728
+ impl<'de> Deserialize<'de> for Value {
729
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
730
+ where
731
+ D: Deserializer<'de>,
732
+ {
733
+ struct ValueVisitor;
734
+
735
+ impl<'de> Visitor<'de> for ValueVisitor {
736
+ type Value = Value;
737
+
738
+ fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
739
+ f.write_str("a JSON value")
740
+ }
741
+
742
+ fn visit_unit<E>(self) -> Result<Value, E> {
743
+ Ok(Value::Null)
744
+ }
745
+ fn visit_none<E>(self) -> Result<Value, E> {
746
+ Ok(Value::Null)
747
+ }
748
+ fn visit_bool<E>(self, v: bool) -> Result<Value, E> {
749
+ Ok(Value::Bool(v))
750
+ }
751
+ fn visit_i64<E>(self, v: i64) -> Result<Value, E> {
752
+ Ok(Value::Int(v))
753
+ }
754
+ fn visit_u64<E>(self, v: u64) -> Result<Value, E>
755
+ where
756
+ E: de::Error,
757
+ {
758
+ i64::try_from(v)
759
+ .map(Value::Int)
760
+ .map_err(|_| de::Error::custom("integer out of i64 range"))
761
+ }
762
+ fn visit_f64<E>(self, v: f64) -> Result<Value, E> {
763
+ Ok(Value::Float(v))
764
+ }
765
+ fn visit_str<E>(self, v: &str) -> Result<Value, E> {
766
+ Ok(Value::Str(v.to_string()))
767
+ }
768
+ fn visit_string<E>(self, v: String) -> Result<Value, E> {
769
+ Ok(Value::Str(v))
770
+ }
771
+ fn visit_seq<A>(self, mut seq: A) -> Result<Value, A::Error>
772
+ where
773
+ A: SeqAccess<'de>,
774
+ {
775
+ let mut items: Vec<Value> = Vec::new();
776
+ while let Some(v) = seq.next_element()? {
777
+ items.push(v);
778
+ }
779
+ Ok(Value::List(items))
780
+ }
781
+ fn visit_map<A>(self, mut map: A) -> Result<Value, A::Error>
782
+ where
783
+ A: MapAccess<'de>,
784
+ {
785
+ // 访问者按源文档顺序遍历 → 保留 JSON 键序(serde_json 默认 BTreeMap
786
+ // 会乱序,故此处不经 serde_json::Value)。
787
+ let mut pairs: Vec<(String, Value)> = Vec::new();
788
+ while let Some((k, v)) = map.next_entry::<String, Value>()? {
789
+ insert_ordered(&mut pairs, k, v);
790
+ }
791
+ Ok(Value::Map(pairs))
792
+ }
793
+ }
794
+
795
+ deserializer.deserialize_any(ValueVisitor)
796
+ }
797
+ }
798
+
799
+ #[cfg(test)]
800
+ mod tests;