pyattacker 0.3.1__tar.gz → 0.3.3__tar.gz

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 (137) hide show
  1. pyattacker-0.3.3/.github/workflows/docs.yml +55 -0
  2. {pyattacker-0.3.1 → pyattacker-0.3.3}/.gitignore +2 -0
  3. {pyattacker-0.3.1 → pyattacker-0.3.3}/CHANGELOG.md +79 -6
  4. {pyattacker-0.3.1 → pyattacker-0.3.3}/PKG-INFO +48 -20
  5. {pyattacker-0.3.1 → pyattacker-0.3.3}/README.md +46 -18
  6. {pyattacker-0.3.1 → pyattacker-0.3.3}/README.zh-CN.md +35 -12
  7. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/cli.md +25 -13
  8. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/design.md +54 -48
  9. pyattacker-0.3.3/docs/documentation.md +49 -0
  10. pyattacker-0.3.3/docs/hooks.py +15 -0
  11. pyattacker-0.3.3/docs/index.md +48 -0
  12. pyattacker-0.3.3/docs/overrides/main.html +8 -0
  13. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/reference.md +100 -35
  14. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/releasing.md +3 -2
  15. pyattacker-0.3.3/docs/suites.md +276 -0
  16. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/tutorial.md +37 -32
  17. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/zh-CN/cli.md +21 -9
  18. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/zh-CN/design.md +39 -28
  19. pyattacker-0.3.3/docs/zh-CN/documentation.md +48 -0
  20. pyattacker-0.3.3/docs/zh-CN/index.md +48 -0
  21. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/zh-CN/reference.md +87 -34
  22. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/zh-CN/releasing.md +3 -2
  23. pyattacker-0.3.3/docs/zh-CN/suites.md +157 -0
  24. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/zh-CN/tutorial.md +34 -28
  25. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/llm_eval/README.md +1 -1
  26. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/llm_eval/README.zh-CN.md +1 -1
  27. pyattacker-0.3.3/examples/suites/experiment.json +14 -0
  28. pyattacker-0.3.3/examples/suites/suite.json +12 -0
  29. pyattacker-0.3.3/mkdocs.yml +97 -0
  30. {pyattacker-0.3.1 → pyattacker-0.3.3}/pyproject.toml +13 -2
  31. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/__init__.py +6 -1
  32. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/cli.py +84 -18
  33. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/declarative.py +15 -3
  34. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/errors.py +5 -2
  35. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/export.py +52 -8
  36. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/handoff.py +23 -20
  37. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/monitor.py +18 -3
  38. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/pipeline.py +14 -5
  39. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/reported_metrics.py +1 -0
  40. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/runner.py +97 -21
  41. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/server.py +65 -18
  42. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/store/__init__.py +2 -0
  43. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/store/base.py +31 -6
  44. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/store/memory.py +3 -1
  45. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/store/sqlite.py +46 -10
  46. pyattacker-0.3.3/src/pyattacker/store/suite.py +1125 -0
  47. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/store/writebehind.py +51 -14
  48. pyattacker-0.3.3/src/pyattacker/suite.py +340 -0
  49. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/task.py +8 -0
  50. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/helpers.py +9 -0
  51. pyattacker-0.3.3/tests/test_admission.py +224 -0
  52. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_backends.py +1 -0
  53. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_backward.py +1 -1
  54. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_cli.py +2 -2
  55. pyattacker-0.3.3/tests/test_dashboard_security.py +64 -0
  56. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_docs_examples.py +1 -1
  57. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_docs_i18n.py +3 -0
  58. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_export.py +2 -2
  59. pyattacker-0.3.3/tests/test_fact_batches.py +280 -0
  60. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_handoff.py +1 -1
  61. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_reported_metrics.py +2 -2
  62. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_server.py +12 -2
  63. pyattacker-0.3.3/tests/test_suite.py +822 -0
  64. pyattacker-0.3.3/tests/test_suite_queries.py +219 -0
  65. pyattacker-0.3.3/uv.lock +685 -0
  66. pyattacker-0.3.1/uv.lock +0 -172
  67. {pyattacker-0.3.1 → pyattacker-0.3.3}/.github/workflows/ci.yml +0 -0
  68. {pyattacker-0.3.1 → pyattacker-0.3.3}/.github/workflows/release.yml +0 -0
  69. {pyattacker-0.3.1 → pyattacker-0.3.3}/LICENSE +0 -0
  70. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/benchmark.md +0 -0
  71. {pyattacker-0.3.1 → pyattacker-0.3.3}/docs/zh-CN/benchmark.md +0 -0
  72. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/data.jsonl +0 -0
  73. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/live_metrics.py +0 -0
  74. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/llm_eval/__init__.py +0 -0
  75. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/llm_eval/backend.py +0 -0
  76. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/llm_eval/demo.py +0 -0
  77. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/llm_eval/pipelines.py +0 -0
  78. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/plugin_package/README.md +0 -0
  79. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/plugin_package/README.zh-CN.md +0 -0
  80. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/plugin_package/pa_demo_plugin/__init__.py +0 -0
  81. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/plugin_package/pa_demo_plugin/algorithms.py +0 -0
  82. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/plugin_package/pa_demo_plugin/codecs.py +0 -0
  83. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/plugin_package/pa_demo_plugin/tasks.py +0 -0
  84. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/plugin_package/pyproject.toml +0 -0
  85. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/plugin_tasks.yaml +0 -0
  86. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/qa_eval.yaml +0 -0
  87. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/quickstart.py +0 -0
  88. {pyattacker-0.3.1 → pyattacker-0.3.3}/examples/sharded.py +0 -0
  89. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/__main__.py +0 -0
  90. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/algorithm.py +0 -0
  91. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/artifact.py +0 -0
  92. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/backends.py +0 -0
  93. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/benchmark/__init__.py +0 -0
  94. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/benchmark/clock.py +0 -0
  95. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/benchmark/harness.py +0 -0
  96. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/benchmark/metrics.py +0 -0
  97. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/benchmark/provider.py +0 -0
  98. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/benchmark/report.py +0 -0
  99. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/benchmark/scenario.py +0 -0
  100. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/history.py +0 -0
  101. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/merge.py +0 -0
  102. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/plugins.py +0 -0
  103. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/resource.py +0 -0
  104. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/scheduler.py +0 -0
  105. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/shard.py +0 -0
  106. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/store/visits.py +0 -0
  107. {pyattacker-0.3.1 → pyattacker-0.3.3}/src/pyattacker/tasks/__init__.py +0 -0
  108. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_algorithms.py +0 -0
  109. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_artifact.py +0 -0
  110. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_benchmark_cli.py +0 -0
  111. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_benchmark_clock.py +0 -0
  112. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_benchmark_harness.py +0 -0
  113. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_benchmark_provider.py +0 -0
  114. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_benchmark_report.py +0 -0
  115. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_benchmark_scenario.py +0 -0
  116. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_declarative.py +0 -0
  117. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_docs_facts.py +0 -0
  118. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_errors.py +0 -0
  119. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_lease_safety.py +0 -0
  120. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_m2.py +0 -0
  121. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_monitor.py +0 -0
  122. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_optional_yaml.py +0 -0
  123. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_packaging.py +0 -0
  124. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_pipeline.py +0 -0
  125. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_plugins.py +0 -0
  126. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_resume_cursor.py +0 -0
  127. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_resume_identity.py +0 -0
  128. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_retry_policy.py +0 -0
  129. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_runner.py +0 -0
  130. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_scheduler.py +0 -0
  131. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_shard.py +0 -0
  132. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_store.py +0 -0
  133. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_subprocess_lifecycle.py +0 -0
  134. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_task_overrides.py +0 -0
  135. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_tasks.py +0 -0
  136. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_tutorial.py +0 -0
  137. {pyattacker-0.3.1 → pyattacker-0.3.3}/tests/test_worker_liveness.py +0 -0
@@ -0,0 +1,55 @@
1
+ name: Documentation
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: docs-${{ github.ref }}
14
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
15
+
16
+ jobs:
17
+ build:
18
+ name: Build documentation
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v7
22
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
23
+ with:
24
+ enable-cache: true
25
+ python-version: '3.11'
26
+ cache-suffix: docs
27
+ - name: Install dependencies
28
+ run: uv sync --locked --group docs
29
+ - name: Build site and check links
30
+ run: uv run --group docs mkdocs build --strict
31
+ - name: Check bilingual documentation and executable examples
32
+ run: uv run pytest tests/test_docs_i18n.py tests/test_docs_examples.py tests/test_tutorial.py
33
+ - name: Upload Pages artifact
34
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
35
+ uses: actions/upload-pages-artifact@v4
36
+ with:
37
+ path: site
38
+
39
+ deploy:
40
+ name: Deploy documentation
41
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
42
+ needs: build
43
+ runs-on: ubuntu-latest
44
+ permissions:
45
+ pages: write
46
+ id-token: write
47
+ environment:
48
+ name: github-pages
49
+ url: ${{ steps.deployment.outputs.page_url }}
50
+ steps:
51
+ - name: Configure Pages
52
+ uses: actions/configure-pages@v5
53
+ - name: Deploy Pages
54
+ id: deployment
55
+ uses: actions/deploy-pages@v4
@@ -15,3 +15,5 @@ runs/
15
15
  .pyattacker/
16
16
 
17
17
  .scratch/
18
+
19
+ site/
@@ -4,6 +4,78 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [0.3.3] — 2026-09-21
10
+
11
+ ### Fixed
12
+
13
+ * **Dashboard HTML injection** (#78): render event and card values as text rather than
14
+ interpolating untrusted values into HTML.
15
+ * **Atomic write-behind batches** (#79): SQLite commits attempts and events in one transaction
16
+ and assigns IDs only after commit. Failed batches remain recoverable; legacy stores stop
17
+ accepting fact writes after an uncertain failure. Suite eviction and shutdown preserve
18
+ failure boundaries and close remaining resources.
19
+ * **Suite query correctness and cost** (#80): apply experiment/run filters before handoff
20
+ limits, support task limits consistently across built-in stores, and select recent events
21
+ using timestamp indexes. Counts and grouped statistics avoid decoding full history while
22
+ preserving historical run outcomes. Read-only older databases remain supported.
23
+ * Loopback HTTP tests bypass environment proxies (#76).
24
+
25
+ ### Added
26
+
27
+ * **Bounded pipeline admission** (#81): `max_admitted` limits queued, executing and delayed
28
+ pipelines together, while due retries retain their admission. Configure it through Runner,
29
+ the run configuration or `--max-admitted`; Suite members share one Runner budget. Live
30
+ statistics and persisted run configuration expose the effective limit.
31
+ * **Bilingual documentation website** (#82): tutorials, Python API and CLI references at
32
+ <https://hazer-bjtu.github.io/pyattacker/>, with search, strict link checks and automated
33
+ GitHub Pages publishing. The site tracks main and identifies itself as development documentation.
34
+
35
+ ### Changed
36
+
37
+ * Admission is bounded by default to `4 * concurrency`; a positive `max_admitted` overrides
38
+ this. Long retry backoffs can now pause input consumption. This bounds scheduler state
39
+ counts, not payload bytes or total memory; MemoryStore still retains accumulated data.
40
+ * `tasks(limit=0)` returns an empty list; negative and non-integer task limits are rejected.
41
+ Suite timestamp indexes add disk/write and first-writer migration cost. Exact duration
42
+ percentiles still read and sort matching pipeline durations.
43
+ * English and Chinese export documentation now covers the `results` row type (#77), and
44
+ streaming-memory claims describe admission and store retention separately (#81).
45
+ * Development and documentation lockfile downloads use official PyPI URLs for portable CI
46
+ installation, without changing artifact hashes or adding library runtime dependencies (#82).
47
+
48
+ ## [0.3.2] — 2026-09-21
49
+
50
+ ### Changed
51
+
52
+ * **Handoffs and forward/backward jumps are supported public features.** `Handoff.to`,
53
+ `Handoff.end`, `Handoff.rewind`, `Handoff.retry_all` and their `control` declarations now
54
+ follow the project's regular versioning policy, without a separate experimental tier.
55
+ Code documentation and both language editions use this status consistently, including
56
+ the companion payload-history guide. Explicit declarations, finite backward budgets,
57
+ atomic store capabilities, execution semantics and persisted identities remain unchanged.
58
+ Documentation links use the new headings; legacy anchors preserve published bookmarks.
59
+ Earlier release entries below describe their status at the time of release.
60
+
61
+ ### Added
62
+
63
+ * **Experiment suites** (#68): reference existing configurations and schedule their inputs
64
+ round-robin through one Runner with shared global concurrency and explicit local pool bindings.
65
+ Stable Suite/member identities preserve original local keys and isolate checkpoints and metrics.
66
+ * Optional `by_experiment` output directories alongside default `combined` storage, with a
67
+ persistent catalog, source-exhaustion state, selective resume and bounded child connections.
68
+ Directory-aware report/watch/serve/export provide cumulative and durable invocation views,
69
+ preserving historical failures, skips and execution timing after resume. Recovery finalizes
70
+ abandoned catalog runs as interrupted, freezing their elapsed time. Unselected member
71
+ databases remain untouched. Pool environment placeholders allow credential rotation;
72
+ reopening validates the persisted artifact backend.
73
+ * `export --rows results`, `--by-experiment`, member-scoped reported metrics, and task context
74
+ experiment/output-directory fields. Split export filenames follow the selected row kind.
75
+ Single-store exports atomically replace completed files; admitted specs repair migrated repeats.
76
+ * English and Chinese Suite guides and a runnable JSON example. Existing standalone path/ID
77
+ semantics remain unchanged; Suite mode explicitly rejects independent process shards/custom stores.
78
+
7
79
  ## [0.3.1] — 2026-09-19
8
80
 
9
81
  ### Added
@@ -45,7 +117,7 @@ All notable changes to this project are documented here. The format follows
45
117
  keeps the append-only history, and a store that has committed its first revisit records a `base` → `visits-v1`
46
118
  feature level (`store.feature_level()`, `StoreFeatureUnsupported`) so lineage-unaware writers are refused
47
119
  rather than silently mutating the wrong occurrence. See
48
- [reference → advanced: backward traversal](docs/reference.md#advanced-backward-traversal-rewind-retry-all-visits).
120
+ [reference → advanced: backward traversal](docs/reference.md#backward-traversal-rewind-retry-all-visits).
49
121
 
50
122
  * **Advanced feature: handoffs — a task can skip ahead, on the record (opt-in, experimental).** A task may
51
123
  return `Handoff.to(target, value)` to continue at a declared later station, or `Handoff.end(value)` to
@@ -68,9 +140,9 @@ All notable changes to this project are documented here. The format follows
68
140
  commit them atomically; both built-in backends can, and a store that cannot is refused up front with a
69
141
  `ConfigError` rather than silently writing a non-durable jump. Marked *experimental until 1.0*;
70
142
  the forward declaration excludes backward targets; joins and cross-pipeline jumps are not included. See
71
- [`docs/design.md` §4.8](docs/design.md#48-advanced-handoffs--declared-forward-jumps-opt-in-experimental),
72
- the [API reference](docs/reference.md#advanced-handoffs-opt-in) and
73
- [tutorial step 15](docs/tutorial.md#step-15--advanced-skipping-stations-handoffs).
143
+ [`docs/design.md` §4.8](docs/design.md#48-handoffs--declared-forward-jumps-opt-in),
144
+ the [API reference](docs/reference.md#handoffs-opt-in) and
145
+ [tutorial step 15](docs/tutorial.md#step-15--skipping-stations-handoffs).
74
146
 
75
147
  ### Fixed
76
148
 
@@ -206,9 +278,9 @@ All notable changes to this project are documented here. The format follows
206
278
 
207
279
  * **The backward-traversal guide is merged into the tutorial and the reference.** `docs/backward.md` was a
208
280
  seventh document that a reader had to find before they could use the feature; its usage now lives where
209
- the rest of the API does. [Tutorial step 16](docs/tutorial.md#step-16--advanced-regenerating-with-rewind-and-retry-all)
281
+ the rest of the API does. [Tutorial step 16](docs/tutorial.md#step-16--regenerating-with-rewind-and-retry-all)
210
282
  keeps the rewind/retry-all walkthrough and a new step 17 builds an optional `HistoryArtifact` payload,
211
- while [reference → advanced: backward traversal](docs/reference.md#advanced-backward-traversal-rewind-retry-all-visits)
283
+ while [reference → advanced: backward traversal](docs/reference.md#backward-traversal-rewind-retry-all-visits)
212
284
  gains the `Handoff.rewind`/`Handoff.retry_all` signatures, the backward `control` keys, the visit and
213
285
  occurrence model, the budget and recovery rules, and the inspection surface; `HistoryArtifact` is
214
286
  documented next to the codecs it belongs to, and the store capability and compatibility/backup rules moved
@@ -580,6 +652,7 @@ hard limit; asyncio tasks only (wrap blocking code with `asyncio.to_thread`); on
580
652
  balance is statistical; a merged report is a union, not a sum; a pipeline is a linear chain; the HTTP endpoint
581
653
  is unauthenticated.
582
654
 
655
+ [0.3.3]: https://github.com/Hazer-BJTU/pyattacker/releases/tag/v0.3.3
583
656
  [0.3.0]: https://github.com/Hazer-BJTU/pyattacker/releases/tag/v0.3.0
584
657
  [0.2.0]: https://github.com/Hazer-BJTU/pyattacker/releases/tag/v0.2.0
585
658
  [0.1.1]: https://github.com/Hazer-BJTU/pyattacker/releases/tag/v0.1.1
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyattacker
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Artifact-centric, resumable async task orchestration: pipeline / task / resource pool.
5
5
  Project-URL: Homepage, https://github.com/Hazer-BJTU/pyattacker
6
6
  Project-URL: Repository, https://github.com/Hazer-BJTU/pyattacker
7
7
  Project-URL: Issues, https://github.com/Hazer-BJTU/pyattacker/issues
8
8
  Project-URL: Changelog, https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md
9
- Project-URL: Documentation, https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md
9
+ Project-URL: Documentation, https://hazer-bjtu.github.io/pyattacker/
10
10
  Project-URL: Design, https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md
11
11
  Author: pyattacker contributors
12
12
  License: MIT
@@ -34,6 +34,8 @@ Description-Content-Type: text/markdown
34
34
 
35
35
  **English** | [简体中文](README.zh-CN.md)
36
36
 
37
+ [Documentation](https://hazer-bjtu.github.io/pyattacker/)
38
+
37
39
  > Run tens of thousands of independent tasks to completion — resumably, observably, and without
38
40
  > reimplementing endpoint pools, retries and "which rows already ran" for the fifth time.
39
41
 
@@ -93,7 +95,7 @@ Five concepts, and that is the whole vocabulary:
93
95
  | Concept | Meaning | In one line |
94
96
  |---|---|---|
95
97
  | **artifact** | the persisted state of a task | content-addressed, **persisted as soon as it is produced** → checkpoint granularity = task |
96
- | **task** | the smallest unit of scheduling | a unary `(artifact) -> artifact` function, sync or async — or `-> artifact \| Handoff`, to skip ahead ([advanced](#advanced-handoffs-opt-in)) |
98
+ | **task** | the smallest unit of scheduling | a unary `(artifact) -> artifact` function, sync or async — or `-> artifact \| Handoff`, to skip ahead ([handoffs](#handoffs-opt-in)) |
97
99
  | **pipeline** | the unit of completion | `fetch \| ask \| judge \| metrics` chained linearly, semantically independent of each other |
98
100
  | **resource** | a leasable external capability | one endpoint / one key; once pooled, it can be published and subscribed to concurrency-safely |
99
101
  | **algorithm** | the policy for acquiring resources | `wait`, `backoff`, `least_busy`, `failover`, `sticky`, `quota_aware`, `immediate` — orthogonal to "retry on failure" |
@@ -119,7 +121,7 @@ async def fetch(row: dict, ctx) -> dict:
119
121
 
120
122
 
121
123
  def dataset_rows():
122
- """Your dataset. A generator works too: `map` streams it, so memory stays O(concurrency)."""
124
+ """Your dataset. `map` streams generators; max_admitted bounds unfinished pipelines."""
123
125
  return [{"q": f"question-{i}"} for i in range(4)]
124
126
 
125
127
 
@@ -135,6 +137,11 @@ with Runner(store="runs/qa.db", pools=[pool], concurrency=8) as runner:
135
137
  report.export_jsonl("runs/qa.jsonl")
136
138
  ```
137
139
 
140
+ Streaming input uses `max_admitted` (default: `4 * concurrency`) to bound queued,
141
+ running and retry-delayed pipelines together. This is a count limit, not a byte
142
+ limit; task payloads, source buffers and Store retention also consume memory.
143
+ The default `MemoryStore` retains results as data grows. See [RunConfig](docs/reference.md#runconfig).
144
+
138
145
  `ctx.acquire()` falls back to the task's declared `resource`, and `lease.report(ok=...)` feeds the pool's
139
146
  health and quota accounting.
140
147
 
@@ -202,7 +209,7 @@ runner.run(template.map(rows), resume=True) # or pyattacker resume -c config.y
202
209
  * Failed pipelines → continue from **the first task that produced no artifact**: **if task C died, only task C
203
210
  reruns when task B's checkpoint is durable**;
204
211
  * The seed artifact is persisted too → recovery **does not depend on the original dataset file**;
205
- * A pipeline that **handed off** ([advanced](#advanced-handoffs-opt-in)) resumes at the station it jumped to,
212
+ * A pipeline that **handed off** ([handoffs](#handoffs-opt-in)) resumes at the station it jumped to,
206
213
  with the entry state the ledger recorded — the task that handed off is not re-run;
207
214
  * Changed a task's source code (`spec_digest` includes source digests) → treated as a new pipeline, so old
208
215
  results are not incorrectly reused. Factory parameters, fanout children, retry/algorithm policies and
@@ -213,10 +220,13 @@ rerun and old explicit keys conflict. Finish old runs with the old package, then
213
220
  [resume identity and idempotency reference](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#resume-identity)
214
221
  for migration guidance, dynamic functions and external configuration.
215
222
 
216
- ## Advanced: Handoffs (Opt-In)
223
+ <a id="advanced-handoffs-opt-in"></a>
224
+
225
+ ## Handoffs (Opt-In)
226
+
227
+ **Handoffs, forward jumps and rewinds are supported control-flow features, enabled explicitly through `control`.**
217
228
 
218
- **Advanced tier: opt-in, changes the execution model, not needed for ordinary pipelines, experimental until
219
- 1.0.** A task can decide that the rest of the chain no longer needs to run, and *say so* instead of inventing a
229
+ A task can decide that the rest of the chain no longer needs to run, and *say so* instead of inventing a
220
230
  failure or hiding the branch inside one step. It **returns** a directive — `Handoff.to(target, value)` to
221
231
  continue at a declared later station, `Handoff.end(value)` to finish the pipeline right there:
222
232
 
@@ -296,16 +306,16 @@ with Runner(store=":memory:", concurrency=4) as runner:
296
306
  payloads provide explicit snapshots and restoration; ordinary dictionaries remain author-controlled.
297
307
  Visits and exact artifact occurrences retain history and make recovery safe. The APIs, budgets and
298
308
  recovery boundaries are in
299
- [reference → advanced: backward traversal](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#advanced-backward-traversal-rewind-retry-all-visits),
309
+ [reference → backward traversal](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#backward-traversal-rewind-retry-all-visits),
300
310
  with working programs in
301
- [tutorial steps 16–17](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-16--advanced-regenerating-with-rewind-and-retry-all).
311
+ [tutorial steps 16–17](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-16--regenerating-with-rewind-and-retry-all).
302
312
 
303
- The API is one class ([`Handoff`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#advanced-handoffs-opt-in)),
313
+ The API is one class ([`Handoff`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#handoffs-opt-in)),
304
314
  one declaration — `control={"edges": {...}}` for forward jumps, `control.rewind` / `control.retry_all` /
305
315
  `control.max_handoffs` for backward traversal — and one optional store capability; a custom store that cannot
306
316
  commit a handoff atomically is refused up front instead of writing a jump that would not survive a crash.
307
- Walkthrough: [tutorial step 15](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-15--advanced-skipping-stations-handoffs).
308
- Model and rules: [design §4.8](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md#48-advanced-handoffs--declared-forward-jumps-opt-in-experimental).
317
+ Walkthrough: [tutorial step 15](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-15--skipping-stations-handoffs).
318
+ Model and rules: [design §4.8](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md#48-handoffs--declared-forward-jumps-opt-in).
309
319
 
310
320
  When a pipeline restarts from the seed, previous task rows and chain artifacts are reset atomically with the cursor/watermark;
311
321
  previous handoffs stay in its history but no longer act as
@@ -476,8 +486,8 @@ what they do not say.
476
486
 
477
487
  | Document | What is in it |
478
488
  |---|---|
479
- | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md) | seventeen runnable steps, from "one task" to sharded evaluation and the advanced handoff/backward tiers — rewinds, retry-all and payload history included; each one executed by the test suite |
480
- | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md) | every public class and function: signatures, parameters, examples — including the advanced backward-traversal tier and `HistoryArtifact` |
489
+ | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md) | seventeen runnable steps, from "one task" to sharded evaluation, handoffs and backward traversal — rewinds, retry-all and payload history included; each one executed by the test suite |
490
+ | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md) | every public class and function: signatures, parameters, examples — including backward traversal and `HistoryArtifact` |
481
491
  | [`docs/cli.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/cli.md) | every subcommand, every flag, exit codes, config reference |
482
492
  | [`docs/design.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md) | conceptual model, the six invariants, the lease contract, data model, tradeoffs |
483
493
  | [`docs/benchmark.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/benchmark.md) | the algorithm benchmark: what the scenarios assume, what the metrics mean, how to read the table |
@@ -510,7 +520,7 @@ These are design decisions, not missing features:
510
520
  * **Semantic reduction** — accuracy, pass@k, F1 and any cross-pipeline aggregation. Export the artifacts and
511
521
  compute it outside, or write a sink pipeline out of the primitives.
512
522
  * **DAG orchestration** — a pipeline is a linear chain; branch inside a task with `fanout`. The one
513
- qualification is the opt-in [handoff](#advanced-handoffs-opt-in): it changes the traversal of
523
+ qualification is the opt-in [handoff](#handoffs-opt-in): it changes the traversal of
514
524
  the chain along declared edges, never its topology (no joins, no second entry point, no cross-pipeline
515
525
  jumps).
516
526
  * **A serving gateway** — the only HTTP surface is the read-only debug endpoint above.
@@ -521,16 +531,30 @@ every known tradeoff with its reason.
521
531
 
522
532
  ## Status
523
533
 
534
+ **0.3.3 — reliability, bounded admission and online documentation.** Atomic write-behind batches,
535
+ correct and faster Suite queries, dashboard HTML injection fixes, and a configurable
536
+ `max_admitted` limit (default: `4 * concurrency`). The bilingual documentation site is now available.
537
+ See the [release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#033--2026-09-21).
538
+
539
+ **0.3.2 — experiment suites and supported control flow.** Independent experiments can share resource
540
+ pools and global concurrency, with unified run/resume/report commands and optional per-experiment output
541
+ directories. Durable run histories preserve earlier outcomes after resume. Handoffs, forward jumps,
542
+ rewinds and retry-all are supported public features, enabled through explicit `control` declarations.
543
+ See the [Suite guide](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/suites.md) and
544
+ [release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#032--2026-09-21).
545
+
524
546
  **0.3.1 — application-reported live metrics.** Applications can publish values such as accuracy to the
525
547
  read-only monitoring dashboard through `Runner.report_metric()` or `TaskContext.report_metric()`. A completion
526
548
  callback receives committed pipeline results; the application owns the calculation. The dashboard and
527
549
  `watch` now follow one run consistently by default. See [`examples/live_metrics.py`](examples/live_metrics.py).
528
550
 
529
- **0.3.0 — advanced control flow (opt-in), and the documentation in Chinese.** Tasks can now
530
- [hand off](#advanced-handoffs-opt-in): return a `Handoff` to skip declared stations or finish the pipeline
551
+ **0.3.0 — control flow (opt-in), and the documentation in Chinese.** Tasks can now
552
+ [hand off](#handoffs-opt-in): return a `Handoff` to skip declared stations or finish the pipeline
531
553
  early, recorded in a durable ledger that recovery resumes from. It is opt-in and inert — a pipeline without a
532
- `control` block writes no new rows and keeps a byte-identical `spec_digest` — and marked experimental until
533
- 1.0. Backward traversal now adds declared rewind and retry-all with visits and optional payload history. The
554
+ `control` block writes no new rows and keeps a byte-identical `spec_digest`. Handoffs and backward traversal
555
+ were experimental in 0.3.0 and became supported features in 0.3.2
556
+ (see [0.3.2 release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#032--2026-09-21)). Backward traversal adds declared rewind and retry-all with visits
557
+ and optional payload history. The
534
558
  whole documentation set also ships in Simplified Chinese ([`README.zh-CN.md`](README.zh-CN.md),
535
559
  [`docs/zh-CN/`](https://github.com/Hazer-BJTU/pyattacker/tree/main/docs/zh-CN)), kept in sync by CI. One
536
560
  rename to know about when upgrading: `MergedReport.events_total` is now `source_events_total`, because that
@@ -586,3 +610,7 @@ extracted and executed by `tests/test_tutorial.py`, so documentation that rots f
586
610
  ## License
587
611
 
588
612
  MIT — see [LICENSE](https://github.com/Hazer-BJTU/pyattacker/blob/main/LICENSE).
613
+
614
+ ## Experiment suites
615
+
616
+ Combine existing configuration files under one Runner with shared resource pools and total concurrency. Optional per-experiment directories separate checkpoints, results and file artifacts. Suite reports include prior successes across resumes. See the [Suite guide](docs/suites.md) and [runnable configuration](examples/suites/suite.json).
@@ -9,6 +9,8 @@
9
9
 
10
10
  **English** | [简体中文](README.zh-CN.md)
11
11
 
12
+ [Documentation](https://hazer-bjtu.github.io/pyattacker/)
13
+
12
14
  > Run tens of thousands of independent tasks to completion — resumably, observably, and without
13
15
  > reimplementing endpoint pools, retries and "which rows already ran" for the fifth time.
14
16
 
@@ -68,7 +70,7 @@ Five concepts, and that is the whole vocabulary:
68
70
  | Concept | Meaning | In one line |
69
71
  |---|---|---|
70
72
  | **artifact** | the persisted state of a task | content-addressed, **persisted as soon as it is produced** → checkpoint granularity = task |
71
- | **task** | the smallest unit of scheduling | a unary `(artifact) -> artifact` function, sync or async — or `-> artifact \| Handoff`, to skip ahead ([advanced](#advanced-handoffs-opt-in)) |
73
+ | **task** | the smallest unit of scheduling | a unary `(artifact) -> artifact` function, sync or async — or `-> artifact \| Handoff`, to skip ahead ([handoffs](#handoffs-opt-in)) |
72
74
  | **pipeline** | the unit of completion | `fetch \| ask \| judge \| metrics` chained linearly, semantically independent of each other |
73
75
  | **resource** | a leasable external capability | one endpoint / one key; once pooled, it can be published and subscribed to concurrency-safely |
74
76
  | **algorithm** | the policy for acquiring resources | `wait`, `backoff`, `least_busy`, `failover`, `sticky`, `quota_aware`, `immediate` — orthogonal to "retry on failure" |
@@ -94,7 +96,7 @@ async def fetch(row: dict, ctx) -> dict:
94
96
 
95
97
 
96
98
  def dataset_rows():
97
- """Your dataset. A generator works too: `map` streams it, so memory stays O(concurrency)."""
99
+ """Your dataset. `map` streams generators; max_admitted bounds unfinished pipelines."""
98
100
  return [{"q": f"question-{i}"} for i in range(4)]
99
101
 
100
102
 
@@ -110,6 +112,11 @@ with Runner(store="runs/qa.db", pools=[pool], concurrency=8) as runner:
110
112
  report.export_jsonl("runs/qa.jsonl")
111
113
  ```
112
114
 
115
+ Streaming input uses `max_admitted` (default: `4 * concurrency`) to bound queued,
116
+ running and retry-delayed pipelines together. This is a count limit, not a byte
117
+ limit; task payloads, source buffers and Store retention also consume memory.
118
+ The default `MemoryStore` retains results as data grows. See [RunConfig](docs/reference.md#runconfig).
119
+
113
120
  `ctx.acquire()` falls back to the task's declared `resource`, and `lease.report(ok=...)` feeds the pool's
114
121
  health and quota accounting.
115
122
 
@@ -177,7 +184,7 @@ runner.run(template.map(rows), resume=True) # or pyattacker resume -c config.y
177
184
  * Failed pipelines → continue from **the first task that produced no artifact**: **if task C died, only task C
178
185
  reruns when task B's checkpoint is durable**;
179
186
  * The seed artifact is persisted too → recovery **does not depend on the original dataset file**;
180
- * A pipeline that **handed off** ([advanced](#advanced-handoffs-opt-in)) resumes at the station it jumped to,
187
+ * A pipeline that **handed off** ([handoffs](#handoffs-opt-in)) resumes at the station it jumped to,
181
188
  with the entry state the ledger recorded — the task that handed off is not re-run;
182
189
  * Changed a task's source code (`spec_digest` includes source digests) → treated as a new pipeline, so old
183
190
  results are not incorrectly reused. Factory parameters, fanout children, retry/algorithm policies and
@@ -188,10 +195,13 @@ rerun and old explicit keys conflict. Finish old runs with the old package, then
188
195
  [resume identity and idempotency reference](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#resume-identity)
189
196
  for migration guidance, dynamic functions and external configuration.
190
197
 
191
- ## Advanced: Handoffs (Opt-In)
198
+ <a id="advanced-handoffs-opt-in"></a>
199
+
200
+ ## Handoffs (Opt-In)
201
+
202
+ **Handoffs, forward jumps and rewinds are supported control-flow features, enabled explicitly through `control`.**
192
203
 
193
- **Advanced tier: opt-in, changes the execution model, not needed for ordinary pipelines, experimental until
194
- 1.0.** A task can decide that the rest of the chain no longer needs to run, and *say so* instead of inventing a
204
+ A task can decide that the rest of the chain no longer needs to run, and *say so* instead of inventing a
195
205
  failure or hiding the branch inside one step. It **returns** a directive — `Handoff.to(target, value)` to
196
206
  continue at a declared later station, `Handoff.end(value)` to finish the pipeline right there:
197
207
 
@@ -271,16 +281,16 @@ with Runner(store=":memory:", concurrency=4) as runner:
271
281
  payloads provide explicit snapshots and restoration; ordinary dictionaries remain author-controlled.
272
282
  Visits and exact artifact occurrences retain history and make recovery safe. The APIs, budgets and
273
283
  recovery boundaries are in
274
- [reference → advanced: backward traversal](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#advanced-backward-traversal-rewind-retry-all-visits),
284
+ [reference → backward traversal](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#backward-traversal-rewind-retry-all-visits),
275
285
  with working programs in
276
- [tutorial steps 16–17](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-16--advanced-regenerating-with-rewind-and-retry-all).
286
+ [tutorial steps 16–17](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-16--regenerating-with-rewind-and-retry-all).
277
287
 
278
- The API is one class ([`Handoff`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#advanced-handoffs-opt-in)),
288
+ The API is one class ([`Handoff`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md#handoffs-opt-in)),
279
289
  one declaration — `control={"edges": {...}}` for forward jumps, `control.rewind` / `control.retry_all` /
280
290
  `control.max_handoffs` for backward traversal — and one optional store capability; a custom store that cannot
281
291
  commit a handoff atomically is refused up front instead of writing a jump that would not survive a crash.
282
- Walkthrough: [tutorial step 15](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-15--advanced-skipping-stations-handoffs).
283
- Model and rules: [design §4.8](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md#48-advanced-handoffs--declared-forward-jumps-opt-in-experimental).
292
+ Walkthrough: [tutorial step 15](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md#step-15--skipping-stations-handoffs).
293
+ Model and rules: [design §4.8](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md#48-handoffs--declared-forward-jumps-opt-in).
284
294
 
285
295
  When a pipeline restarts from the seed, previous task rows and chain artifacts are reset atomically with the cursor/watermark;
286
296
  previous handoffs stay in its history but no longer act as
@@ -451,8 +461,8 @@ what they do not say.
451
461
 
452
462
  | Document | What is in it |
453
463
  |---|---|
454
- | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md) | seventeen runnable steps, from "one task" to sharded evaluation and the advanced handoff/backward tiers — rewinds, retry-all and payload history included; each one executed by the test suite |
455
- | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md) | every public class and function: signatures, parameters, examples — including the advanced backward-traversal tier and `HistoryArtifact` |
464
+ | [`docs/tutorial.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md) | seventeen runnable steps, from "one task" to sharded evaluation, handoffs and backward traversal — rewinds, retry-all and payload history included; each one executed by the test suite |
465
+ | [`docs/reference.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/reference.md) | every public class and function: signatures, parameters, examples — including backward traversal and `HistoryArtifact` |
456
466
  | [`docs/cli.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/cli.md) | every subcommand, every flag, exit codes, config reference |
457
467
  | [`docs/design.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md) | conceptual model, the six invariants, the lease contract, data model, tradeoffs |
458
468
  | [`docs/benchmark.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/benchmark.md) | the algorithm benchmark: what the scenarios assume, what the metrics mean, how to read the table |
@@ -485,7 +495,7 @@ These are design decisions, not missing features:
485
495
  * **Semantic reduction** — accuracy, pass@k, F1 and any cross-pipeline aggregation. Export the artifacts and
486
496
  compute it outside, or write a sink pipeline out of the primitives.
487
497
  * **DAG orchestration** — a pipeline is a linear chain; branch inside a task with `fanout`. The one
488
- qualification is the opt-in [handoff](#advanced-handoffs-opt-in): it changes the traversal of
498
+ qualification is the opt-in [handoff](#handoffs-opt-in): it changes the traversal of
489
499
  the chain along declared edges, never its topology (no joins, no second entry point, no cross-pipeline
490
500
  jumps).
491
501
  * **A serving gateway** — the only HTTP surface is the read-only debug endpoint above.
@@ -496,16 +506,30 @@ every known tradeoff with its reason.
496
506
 
497
507
  ## Status
498
508
 
509
+ **0.3.3 — reliability, bounded admission and online documentation.** Atomic write-behind batches,
510
+ correct and faster Suite queries, dashboard HTML injection fixes, and a configurable
511
+ `max_admitted` limit (default: `4 * concurrency`). The bilingual documentation site is now available.
512
+ See the [release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#033--2026-09-21).
513
+
514
+ **0.3.2 — experiment suites and supported control flow.** Independent experiments can share resource
515
+ pools and global concurrency, with unified run/resume/report commands and optional per-experiment output
516
+ directories. Durable run histories preserve earlier outcomes after resume. Handoffs, forward jumps,
517
+ rewinds and retry-all are supported public features, enabled through explicit `control` declarations.
518
+ See the [Suite guide](https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/suites.md) and
519
+ [release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#032--2026-09-21).
520
+
499
521
  **0.3.1 — application-reported live metrics.** Applications can publish values such as accuracy to the
500
522
  read-only monitoring dashboard through `Runner.report_metric()` or `TaskContext.report_metric()`. A completion
501
523
  callback receives committed pipeline results; the application owns the calculation. The dashboard and
502
524
  `watch` now follow one run consistently by default. See [`examples/live_metrics.py`](examples/live_metrics.py).
503
525
 
504
- **0.3.0 — advanced control flow (opt-in), and the documentation in Chinese.** Tasks can now
505
- [hand off](#advanced-handoffs-opt-in): return a `Handoff` to skip declared stations or finish the pipeline
526
+ **0.3.0 — control flow (opt-in), and the documentation in Chinese.** Tasks can now
527
+ [hand off](#handoffs-opt-in): return a `Handoff` to skip declared stations or finish the pipeline
506
528
  early, recorded in a durable ledger that recovery resumes from. It is opt-in and inert — a pipeline without a
507
- `control` block writes no new rows and keeps a byte-identical `spec_digest` — and marked experimental until
508
- 1.0. Backward traversal now adds declared rewind and retry-all with visits and optional payload history. The
529
+ `control` block writes no new rows and keeps a byte-identical `spec_digest`. Handoffs and backward traversal
530
+ were experimental in 0.3.0 and became supported features in 0.3.2
531
+ (see [0.3.2 release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#032--2026-09-21)). Backward traversal adds declared rewind and retry-all with visits
532
+ and optional payload history. The
509
533
  whole documentation set also ships in Simplified Chinese ([`README.zh-CN.md`](README.zh-CN.md),
510
534
  [`docs/zh-CN/`](https://github.com/Hazer-BJTU/pyattacker/tree/main/docs/zh-CN)), kept in sync by CI. One
511
535
  rename to know about when upgrading: `MergedReport.events_total` is now `source_events_total`, because that
@@ -561,3 +585,7 @@ extracted and executed by `tests/test_tutorial.py`, so documentation that rots f
561
585
  ## License
562
586
 
563
587
  MIT — see [LICENSE](https://github.com/Hazer-BJTU/pyattacker/blob/main/LICENSE).
588
+
589
+ ## Experiment suites
590
+
591
+ Combine existing configuration files under one Runner with shared resource pools and total concurrency. Optional per-experiment directories separate checkpoints, results and file artifacts. Suite reports include prior successes across resumes. See the [Suite guide](docs/suites.md) and [runnable configuration](examples/suites/suite.json).