berylx-py 0.1.0__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 (59) hide show
  1. berylx_py-0.1.0/.github/workflows/ci.yml +30 -0
  2. berylx_py-0.1.0/.github/workflows/publish.yml +90 -0
  3. berylx_py-0.1.0/LICENSE +21 -0
  4. berylx_py-0.1.0/MANIFEST.in +6 -0
  5. berylx_py-0.1.0/PKG-INFO +181 -0
  6. berylx_py-0.1.0/README.md +160 -0
  7. berylx_py-0.1.0/docs/api.md +79 -0
  8. berylx_py-0.1.0/docs/equivalence.md +110 -0
  9. berylx_py-0.1.0/docs/host-and-delivery.md +62 -0
  10. berylx_py-0.1.0/docs/source-audit.md +59 -0
  11. berylx_py-0.1.0/docs/spec/berylx-py.spec +65 -0
  12. berylx_py-0.1.0/docs/timeline.md +192 -0
  13. berylx_py-0.1.0/evidence/build.txt +193 -0
  14. berylx_py-0.1.0/evidence/delivery.txt +29 -0
  15. berylx_py-0.1.0/evidence/final-gates.txt +221 -0
  16. berylx_py-0.1.0/evidence/frozen-sha256.txt +35 -0
  17. berylx_py-0.1.0/evidence/independent-final.txt +446 -0
  18. berylx_py-0.1.0/evidence/p2-oracle.txt +133 -0
  19. berylx_py-0.1.0/evidence/p2-red.txt +35 -0
  20. berylx_py-0.1.0/evidence/p3-core.txt +4 -0
  21. berylx_py-0.1.0/evidence/p3-integrated.txt +4 -0
  22. berylx_py-0.1.0/evidence/p4-property.txt +4 -0
  23. berylx_py-0.1.0/evidence/pypi-preflight.txt +258 -0
  24. berylx_py-0.1.0/evidence/python-matrix.txt +181 -0
  25. berylx_py-0.1.0/evidence/rebuild-final-docs.txt +181 -0
  26. berylx_py-0.1.0/evidence/review-cray-red.txt +20 -0
  27. berylx_py-0.1.0/evidence/review-red.txt +57 -0
  28. berylx_py-0.1.0/evidence/spec-sat.txt +4 -0
  29. berylx_py-0.1.0/evidence/upstream.txt +76 -0
  30. berylx_py-0.1.0/evidence/wheel-install.txt +3 -0
  31. berylx_py-0.1.0/evidence/wheel-smoke.txt +27 -0
  32. berylx_py-0.1.0/pyproject.toml +39 -0
  33. berylx_py-0.1.0/requirements-dev.txt +16 -0
  34. berylx_py-0.1.0/scripts/check_readme.py +10 -0
  35. berylx_py-0.1.0/scripts/ts_oracle.mjs +22 -0
  36. berylx_py-0.1.0/setup.cfg +4 -0
  37. berylx_py-0.1.0/src/berylx/__init__.py +129 -0
  38. berylx_py-0.1.0/src/berylx/adapters.py +96 -0
  39. berylx_py-0.1.0/src/berylx/core.py +514 -0
  40. berylx_py-0.1.0/src/berylx/darkcore/__init__.py +331 -0
  41. berylx_py-0.1.0/src/berylx/darkcore/io.py +153 -0
  42. berylx_py-0.1.0/src/berylx/effect_tree.py +305 -0
  43. berylx_py-0.1.0/src/berylx/graph.py +101 -0
  44. berylx_py-0.1.0/src/berylx/merge.py +99 -0
  45. berylx_py-0.1.0/src/berylx/nodes.py +292 -0
  46. berylx_py-0.1.0/src/berylx/timeline.py +614 -0
  47. berylx_py-0.1.0/src/berylx/timeline_berylx.py +253 -0
  48. berylx_py-0.1.0/src/berylx_py.egg-info/PKG-INFO +181 -0
  49. berylx_py-0.1.0/src/berylx_py.egg-info/SOURCES.txt +57 -0
  50. berylx_py-0.1.0/src/berylx_py.egg-info/dependency_links.txt +1 -0
  51. berylx_py-0.1.0/src/berylx_py.egg-info/requires.txt +7 -0
  52. berylx_py-0.1.0/src/berylx_py.egg-info/top_level.txt +1 -0
  53. berylx_py-0.1.0/tests/fixtures/ts-observations.json +112 -0
  54. berylx_py-0.1.0/tests/test_boundaries.py +250 -0
  55. berylx_py-0.1.0/tests/test_darkcore.py +90 -0
  56. berylx_py-0.1.0/tests/test_oracle.py +65 -0
  57. berylx_py-0.1.0/tests/test_properties.py +283 -0
  58. berylx_py-0.1.0/tests/test_regression.py +267 -0
  59. berylx_py-0.1.0/tests/test_timeline.py +460 -0
@@ -0,0 +1,30 @@
1
+ name: ci
2
+ on:
3
+ push:
4
+ pull_request:
5
+ workflow_call:
6
+ jobs:
7
+ test:
8
+ runs-on: ubuntu-latest
9
+ strategy:
10
+ matrix:
11
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: ${{ matrix.python-version }}
17
+ - run: python -m pip install -r requirements-dev.txt
18
+ - run: python -m pip install -e .
19
+ - name: Regression and Unit barrier
20
+ run: python -m pytest -q tests/test_regression.py tests/test_darkcore.py tests/test_oracle.py tests/test_boundaries.py tests/test_timeline.py
21
+ - name: Property barrier
22
+ run: python -m pytest -q tests/test_properties.py
23
+ - run: ruff check src tests scripts
24
+ - run: ruff format --check src tests scripts
25
+ - run: mypy src/berylx
26
+ - run: python -m build
27
+ - uses: actions/setup-node@v4
28
+ with:
29
+ node-version: "22"
30
+ - run: npx --yes prettier@3.6.2 --check README.md 'docs/**/*.md' '.github/**/*.yml' 'tests/fixtures/*.json'
@@ -0,0 +1,90 @@
1
+ name: Publish to PyPI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ paths: [pyproject.toml]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: pypi-publication
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ eligibility:
17
+ if: github.ref == 'refs/heads/main'
18
+ runs-on: ubuntu-latest
19
+ outputs:
20
+ needed: ${{ steps.version.outputs.needed }}
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: "3.14"
26
+ - name: Check whether this version needs publication
27
+ id: version
28
+ shell: python
29
+ run: |
30
+ import os
31
+ import tomllib
32
+ from urllib.error import HTTPError
33
+ from urllib.parse import quote
34
+ from urllib.request import urlopen
35
+
36
+ with open("pyproject.toml", "rb") as source:
37
+ project = tomllib.load(source)["project"]
38
+ name, version = project["name"], project["version"]
39
+ url = f"https://pypi.org/pypi/{quote(name, safe='')}/{quote(version, safe='')}/json"
40
+ try:
41
+ with urlopen(url, timeout=30) as response:
42
+ if response.status != 200:
43
+ raise RuntimeError(f"Unexpected PyPI response: {response.status}")
44
+ needed = "false"
45
+ print(f"::notice::{name} {version} is already published; skipping upload")
46
+ except HTTPError as error:
47
+ if error.code != 404:
48
+ raise
49
+ needed = "true"
50
+ print(f"{name} {version} is not published; proceeding through CI")
51
+ with open(os.environ["GITHUB_OUTPUT"], "a") as output:
52
+ print(f"needed={needed}", file=output)
53
+
54
+ validate:
55
+ needs: eligibility
56
+ if: needs.eligibility.outputs.needed == 'true'
57
+ uses: ./.github/workflows/ci.yml
58
+
59
+ build:
60
+ needs: validate
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ - uses: actions/checkout@v4
64
+ - uses: actions/setup-python@v5
65
+ with:
66
+ python-version: "3.14"
67
+ - run: python -m pip install -r requirements-dev.txt 'twine>=6,<7'
68
+ - run: python -m build
69
+ - run: python -m twine check --strict dist/*
70
+ - uses: actions/upload-artifact@v4
71
+ with:
72
+ name: python-distributions
73
+ path: dist/*
74
+ if-no-files-found: error
75
+
76
+ publish:
77
+ needs: build
78
+ runs-on: ubuntu-latest
79
+ environment:
80
+ name: pypi
81
+ url: https://pypi.org/project/berylx-py/
82
+ permissions:
83
+ id-token: write
84
+ steps:
85
+ - uses: actions/download-artifact@v4
86
+ with:
87
+ name: python-distributions
88
+ path: dist/
89
+ - name: Publish distributions to PyPI
90
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 minamorl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ include requirements-dev.txt
2
+ recursive-include tests *.py *.json
3
+ recursive-include docs *.md *.spec
4
+ recursive-include evidence *.txt
5
+ recursive-include scripts *.py *.mjs
6
+ recursive-include .github *.yml
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: berylx-py
3
+ Version: 0.1.0
4
+ Summary: Immutable state workflows over a tagged Freer effect monad
5
+ Author: minamorl
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/minamorl/berylx-py
8
+ Project-URL: Repository, https://github.com/minamorl/berylx-py
9
+ Project-URL: Issues, https://github.com/minamorl/berylx-py/issues
10
+ Project-URL: Documentation, https://github.com/minamorl/berylx-py#readme
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8; extra == "dev"
16
+ Requires-Dist: hypothesis>=6; extra == "dev"
17
+ Requires-Dist: build>=1; extra == "dev"
18
+ Requires-Dist: ruff>=0.9; extra == "dev"
19
+ Requires-Dist: mypy>=1; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # berylx-py
23
+
24
+ Immutable state workflows interpreted through a tagged Freer effect monad. Python
25
+ port of [minamorl/berylx-ts](https://github.com/minamorl/berylx-ts), frozen at
26
+ [`24aabcd6059547f15b5b2817592e3d0dd4f258bb`](https://github.com/minamorl/berylx-ts/tree/24aabcd6059547f15b5b2817592e3d0dd4f258bb)
27
+ (TS 0.4.0). The frozen revision equals upstream HEAD observed on 2026-10-29.
28
+
29
+ ## Install
30
+
31
+ Python 3.11 or newer. After the first PyPI release is published, install with:
32
+
33
+ ```bash
34
+ python -m pip install berylx-py
35
+ ```
36
+
37
+ Or install from this source directory:
38
+
39
+ ```bash
40
+ python -m pip install .
41
+ ```
42
+
43
+ The public repository is https://github.com/minamorl/berylx-py .
44
+ Install directly from GitHub:
45
+
46
+ ```bash
47
+ python -m pip install 'git+https://github.com/minamorl/berylx-py.git'
48
+ ```
49
+
50
+ For development from this source directory:
51
+
52
+ ```bash
53
+ python -m venv .venv
54
+ . .venv/bin/activate
55
+ python -m pip install -r requirements-dev.txt
56
+ python -m pip install -e .
57
+ ```
58
+
59
+ ## Releasing
60
+
61
+ Update `project.version` in `pyproject.toml`, then commit and push to `main`.
62
+ The publishing workflow checks whether that version already exists on PyPI. A new
63
+ version runs the complete Python 3.11–3.14 CI matrix, builds and checks the
64
+ artifacts, then publishes through the registered Trusted Publisher. An existing
65
+ version is skipped. PyPI or network errors fail the check instead of uploading.
66
+ The workflow can also be dispatched manually from `main` to retry an unpublished
67
+ version; no PyPI token needs to be stored in GitHub.
68
+
69
+ ## Workflows
70
+
71
+ ```python
72
+ from berylx import Root, Task, Ok
73
+
74
+ root = Root.of({'user': {'name': ' mina '}, 'total': 0})
75
+ trim = Task.of('trim', lambda f: f.at('user').at('name').update(str.strip))
76
+ charge = Task.of('charge', lambda f: f.at('total').set(100))
77
+ result = root.call(trim.then(charge))
78
+ assert isinstance(result, Ok)
79
+ assert root.at('user').at('name').get() == 'mina'
80
+ assert root.state()['total'] == 100
81
+ ```
82
+
83
+ `Focus`/`Lay` is a snapshot; `at` navigates, `set`/`put`/`update` return a new root
84
+ focus. Dicts and lists are defensively copied into read-only views. `Root` commits
85
+ only successful results automatically. An `Err` carries its partial focus, structured
86
+ error, cause and trace; `unwrap()` explicitly raises. `Catch` recovers at a sequence
87
+ boundary; `rescue_with` wraps a whole workflow.
88
+
89
+ `a.par(b)` starts each branch from the same snapshot. Default strict three-way merge
90
+ preserves independent changes to existing keys and reports `merge_conflict` for
91
+ incompatible same-path changes. `.reduce(Merge.deep())` explicitly selects
92
+ right-biased two-way merging. `.accumulate()` collects failures. The default
93
+ `short_circuit` selects the first failure **after every branch settles**; it does
94
+ not cancel work. `AsyncTask` runs with `await EffectTree.run_async(...)`.
95
+
96
+ ## Monads and effect interpretation
97
+
98
+ ```python
99
+ from berylx import darkcore as d
100
+
101
+ program = d.op('lookup', {'id': 7}, int).bind(lambda n: d.pure(n + 1))
102
+ assert d.run(program, {'lookup': lambda payload: 41}) == 42
103
+ assert d.run(program, {'lookup': lambda payload: 9}) == 10
104
+
105
+ stateful = d.State.get().bind(
106
+ lambda n: d.State.put(n + 1).seq(d.State.get())
107
+ )
108
+ assert stateful.run(10) == (11, 11)
109
+ assert d.Err('failed').bind(lambda x: d.Ok(x + 1)) == d.Err('failed')
110
+ ```
111
+
112
+ Darkcore supplies `Just/Nothing`, `Right/Left`, `Ok/Err`, `Success/Failure`, `State`
113
+ and `Effect`. These value result classes are distinct from workflow `Ok/Err`.
114
+ `Validation.bind` short-circuits; `Validation.ap` accumulates errors and intentionally
115
+ differs from the application operation induced by its bind.
116
+
117
+ Tasks compile into inspectable tagged payloads `(node, focus)` over the same Effect
118
+ substrate. Handler maps interpret the tree; `EffectTree.around`/`around_async` wrap
119
+ nested tasks, effects and recovery. Callbacks declaring a required second positional
120
+ parameter receive `Perform`; recovery callbacks receive it as their third.
121
+
122
+ ```python
123
+ from berylx import Task, EffectTree
124
+
125
+ fetch = Task('fetch', lambda f, p: f.put('answer', p.perform('lookup', 7)))
126
+ handlers = EffectTree.real_handlers({'lookup': lambda key: 42})
127
+ assert EffectTree.run(fetch, {}, handlers).focus.get()['answer'] == 42
128
+ assert EffectTree.dry_run(fetch, {}).steps == ['fetch']
129
+ ```
130
+
131
+ Dry-run skips task bodies but still evaluates predicates. It is a planning
132
+ interpreter, not proof of behavioral equivalence. Real and virtual IO handlers
133
+ interpret the same `darkcore.IOEffects` programs.
134
+
135
+ ## Equivalence and differences
136
+
137
+ The supported runtime contract includes monad values/state/effects, immutable
138
+ focus, structured failures, sequence/parallel/branch/recovery, async interpretation,
139
+ Root history/subscriptions, scoped factories, graph output and Cray adapters.
140
+ Timeline scheduling and Root integration are exposed in `berylx.timeline` and
141
+ `berylx.timeline_berylx` (clock injection, human waits, recurring origins and
142
+ concurrent commit merge). See [the frozen contract](docs/equivalence.md) and
143
+ [API mapping](docs/api.md) for exact boundaries.
144
+
145
+ Python names are snake_case; `or_else`/`and_then` avoid keywords. Python exceptions,
146
+ truthiness and numbers follow Python rules. `None` combines JavaScript null and
147
+ undefined. Python generics do not reproduce TS compile-time path-key inference.
148
+ Browser/npm exports are not applicable. The port does not claim unrestricted
149
+ cross-language equivalence for arbitrary objects, numeric edge cases or deletions
150
+ through parallel's strict reducer. Timeline uses a separate deletion-aware merge.
151
+
152
+ ## Validation
153
+
154
+ Regression/unit tests and independently captured TypeScript observations were
155
+ written and run RED before implementation. After all regression/unit gates pass,
156
+ Hypothesis checks generated monad/composition/state/failure/merge properties.
157
+
158
+ ```bash
159
+ python -m pytest -q tests/test_regression.py tests/test_darkcore.py tests/test_oracle.py tests/test_boundaries.py tests/test_timeline.py
160
+ python -m pytest -q tests/test_properties.py
161
+ python -m pytest -q
162
+ ruff check src tests
163
+ ruff format --check src tests
164
+ mypy src/berylx
165
+ python -m build
166
+ ```
167
+
168
+ The type gate checks implementation bodies; it does not prove TS-style path types.
169
+ [Recorded evidence](evidence/) includes phase gates. To regenerate the independent
170
+ oracle, build a separate TS checkout at the frozen revision, then run:
171
+
172
+ ```bash
173
+ node scripts/ts_oracle.mjs /absolute/path/to/berylx-ts/dist/index.js
174
+ ```
175
+
176
+ Its output must match `tests/fixtures/ts-observations.json`. Python compares state,
177
+ error envelopes, handler order, State and value-monad observations against that
178
+ fixture, without importing or executing the TS implementation.
179
+
180
+ MIT licensed; original TypeScript implementation copyright 2026 minamorl is
181
+ retained in [LICENSE](LICENSE).
@@ -0,0 +1,160 @@
1
+ # berylx-py
2
+
3
+ Immutable state workflows interpreted through a tagged Freer effect monad. Python
4
+ port of [minamorl/berylx-ts](https://github.com/minamorl/berylx-ts), frozen at
5
+ [`24aabcd6059547f15b5b2817592e3d0dd4f258bb`](https://github.com/minamorl/berylx-ts/tree/24aabcd6059547f15b5b2817592e3d0dd4f258bb)
6
+ (TS 0.4.0). The frozen revision equals upstream HEAD observed on 2026-10-29.
7
+
8
+ ## Install
9
+
10
+ Python 3.11 or newer. After the first PyPI release is published, install with:
11
+
12
+ ```bash
13
+ python -m pip install berylx-py
14
+ ```
15
+
16
+ Or install from this source directory:
17
+
18
+ ```bash
19
+ python -m pip install .
20
+ ```
21
+
22
+ The public repository is https://github.com/minamorl/berylx-py .
23
+ Install directly from GitHub:
24
+
25
+ ```bash
26
+ python -m pip install 'git+https://github.com/minamorl/berylx-py.git'
27
+ ```
28
+
29
+ For development from this source directory:
30
+
31
+ ```bash
32
+ python -m venv .venv
33
+ . .venv/bin/activate
34
+ python -m pip install -r requirements-dev.txt
35
+ python -m pip install -e .
36
+ ```
37
+
38
+ ## Releasing
39
+
40
+ Update `project.version` in `pyproject.toml`, then commit and push to `main`.
41
+ The publishing workflow checks whether that version already exists on PyPI. A new
42
+ version runs the complete Python 3.11–3.14 CI matrix, builds and checks the
43
+ artifacts, then publishes through the registered Trusted Publisher. An existing
44
+ version is skipped. PyPI or network errors fail the check instead of uploading.
45
+ The workflow can also be dispatched manually from `main` to retry an unpublished
46
+ version; no PyPI token needs to be stored in GitHub.
47
+
48
+ ## Workflows
49
+
50
+ ```python
51
+ from berylx import Root, Task, Ok
52
+
53
+ root = Root.of({'user': {'name': ' mina '}, 'total': 0})
54
+ trim = Task.of('trim', lambda f: f.at('user').at('name').update(str.strip))
55
+ charge = Task.of('charge', lambda f: f.at('total').set(100))
56
+ result = root.call(trim.then(charge))
57
+ assert isinstance(result, Ok)
58
+ assert root.at('user').at('name').get() == 'mina'
59
+ assert root.state()['total'] == 100
60
+ ```
61
+
62
+ `Focus`/`Lay` is a snapshot; `at` navigates, `set`/`put`/`update` return a new root
63
+ focus. Dicts and lists are defensively copied into read-only views. `Root` commits
64
+ only successful results automatically. An `Err` carries its partial focus, structured
65
+ error, cause and trace; `unwrap()` explicitly raises. `Catch` recovers at a sequence
66
+ boundary; `rescue_with` wraps a whole workflow.
67
+
68
+ `a.par(b)` starts each branch from the same snapshot. Default strict three-way merge
69
+ preserves independent changes to existing keys and reports `merge_conflict` for
70
+ incompatible same-path changes. `.reduce(Merge.deep())` explicitly selects
71
+ right-biased two-way merging. `.accumulate()` collects failures. The default
72
+ `short_circuit` selects the first failure **after every branch settles**; it does
73
+ not cancel work. `AsyncTask` runs with `await EffectTree.run_async(...)`.
74
+
75
+ ## Monads and effect interpretation
76
+
77
+ ```python
78
+ from berylx import darkcore as d
79
+
80
+ program = d.op('lookup', {'id': 7}, int).bind(lambda n: d.pure(n + 1))
81
+ assert d.run(program, {'lookup': lambda payload: 41}) == 42
82
+ assert d.run(program, {'lookup': lambda payload: 9}) == 10
83
+
84
+ stateful = d.State.get().bind(
85
+ lambda n: d.State.put(n + 1).seq(d.State.get())
86
+ )
87
+ assert stateful.run(10) == (11, 11)
88
+ assert d.Err('failed').bind(lambda x: d.Ok(x + 1)) == d.Err('failed')
89
+ ```
90
+
91
+ Darkcore supplies `Just/Nothing`, `Right/Left`, `Ok/Err`, `Success/Failure`, `State`
92
+ and `Effect`. These value result classes are distinct from workflow `Ok/Err`.
93
+ `Validation.bind` short-circuits; `Validation.ap` accumulates errors and intentionally
94
+ differs from the application operation induced by its bind.
95
+
96
+ Tasks compile into inspectable tagged payloads `(node, focus)` over the same Effect
97
+ substrate. Handler maps interpret the tree; `EffectTree.around`/`around_async` wrap
98
+ nested tasks, effects and recovery. Callbacks declaring a required second positional
99
+ parameter receive `Perform`; recovery callbacks receive it as their third.
100
+
101
+ ```python
102
+ from berylx import Task, EffectTree
103
+
104
+ fetch = Task('fetch', lambda f, p: f.put('answer', p.perform('lookup', 7)))
105
+ handlers = EffectTree.real_handlers({'lookup': lambda key: 42})
106
+ assert EffectTree.run(fetch, {}, handlers).focus.get()['answer'] == 42
107
+ assert EffectTree.dry_run(fetch, {}).steps == ['fetch']
108
+ ```
109
+
110
+ Dry-run skips task bodies but still evaluates predicates. It is a planning
111
+ interpreter, not proof of behavioral equivalence. Real and virtual IO handlers
112
+ interpret the same `darkcore.IOEffects` programs.
113
+
114
+ ## Equivalence and differences
115
+
116
+ The supported runtime contract includes monad values/state/effects, immutable
117
+ focus, structured failures, sequence/parallel/branch/recovery, async interpretation,
118
+ Root history/subscriptions, scoped factories, graph output and Cray adapters.
119
+ Timeline scheduling and Root integration are exposed in `berylx.timeline` and
120
+ `berylx.timeline_berylx` (clock injection, human waits, recurring origins and
121
+ concurrent commit merge). See [the frozen contract](docs/equivalence.md) and
122
+ [API mapping](docs/api.md) for exact boundaries.
123
+
124
+ Python names are snake_case; `or_else`/`and_then` avoid keywords. Python exceptions,
125
+ truthiness and numbers follow Python rules. `None` combines JavaScript null and
126
+ undefined. Python generics do not reproduce TS compile-time path-key inference.
127
+ Browser/npm exports are not applicable. The port does not claim unrestricted
128
+ cross-language equivalence for arbitrary objects, numeric edge cases or deletions
129
+ through parallel's strict reducer. Timeline uses a separate deletion-aware merge.
130
+
131
+ ## Validation
132
+
133
+ Regression/unit tests and independently captured TypeScript observations were
134
+ written and run RED before implementation. After all regression/unit gates pass,
135
+ Hypothesis checks generated monad/composition/state/failure/merge properties.
136
+
137
+ ```bash
138
+ python -m pytest -q tests/test_regression.py tests/test_darkcore.py tests/test_oracle.py tests/test_boundaries.py tests/test_timeline.py
139
+ python -m pytest -q tests/test_properties.py
140
+ python -m pytest -q
141
+ ruff check src tests
142
+ ruff format --check src tests
143
+ mypy src/berylx
144
+ python -m build
145
+ ```
146
+
147
+ The type gate checks implementation bodies; it does not prove TS-style path types.
148
+ [Recorded evidence](evidence/) includes phase gates. To regenerate the independent
149
+ oracle, build a separate TS checkout at the frozen revision, then run:
150
+
151
+ ```bash
152
+ node scripts/ts_oracle.mjs /absolute/path/to/berylx-ts/dist/index.js
153
+ ```
154
+
155
+ Its output must match `tests/fixtures/ts-observations.json`. Python compares state,
156
+ error envelopes, handler order, State and value-monad observations against that
157
+ fixture, without importing or executing the TS implementation.
158
+
159
+ MIT licensed; original TypeScript implementation copyright 2026 minamorl is
160
+ retained in [LICENSE](LICENSE).
@@ -0,0 +1,79 @@
1
+ # API correspondence and deliberate boundaries
2
+
3
+ | TypeScript surface | Python surface | Contract |
4
+ | ------------------------------------------------------- | ---------------------------------------- | -------------------------------------------------------- |
5
+ | Focus / Lay | Focus / Lay | immutable snapshot and lazy path operations |
6
+ | toObject / toLay / toState | to_object / to_lay / to_state | same state boundaries |
7
+ | Root / RootOptions.historyLimit | Root(..., history_limit=N) | success-only commit, bounded history |
8
+ | Flow / State.and | Flow / State.and_then | immediate execution / pending composition |
9
+ | Task / AsyncTask | Task / AsyncTask | sync / async callback boundaries |
10
+ | then / par / rescueWith | then / par / rescue_with | sequence / shared-base parallel / recovery |
11
+ | When / Else / Branch.or | When / Else / Branch.or_else | ordered predicates and fallback |
12
+ | Catch / Rescue / RescueBlock | same names | partial-state recovery |
13
+ | ResultOps | ResultOps | normalization and failure-preserving bind/map |
14
+ | BerylxError.failedNode/parallelErrors/withContext | failed_node/parallel_errors/with_context | structured error properties |
15
+ | error.toObject | error.to_object | serialized keys retain TS camelCase for interoperability |
16
+ | Merge.keepLeft/keepRight/deep/strict | keep_left/keep_right/deep/strict | same reducer policies |
17
+ | EffectTree build/run/runAsync | build/run/run_async | same tagged Freer representation |
18
+ | realHandlers/asyncRealHandlers/around/aroundAsync | snake_case counterparts | propagating handler maps |
19
+ | runSubtree/runSubtreeAsync/branchMatches | snake_case counterparts | interpreter utilities |
20
+ | EffectTree dryRun | dry_run | task names + Ok unchanged focus |
21
+ | Perform | Perform | custom effect dispatch |
22
+ | ControlSignal | ControlSignal | ordinary Exception marker escaping task conversion |
23
+ | Darkcore Maybe/Either/Result/Validation/State/Effect | berylx.darkcore (also Darkcore) | distinct value monads, state and Freer effects |
24
+ | Darkcore fold/foldAsync/run/runAsync | fold/fold_async/run/run_async | synchronous/asynchronous handlers |
25
+ | IOEffects / VirtualWorld / realHandlers | IOEffects / VirtualWorld / real_handlers | explicit real/virtual IO |
26
+ | Workflow / Graph | Workflow / Graph | workflow wrapper, structural DOT/Mermaid |
27
+ | berylx<S>() | berylx() | scoped constructors; no recursive static path inference |
28
+ | attachRoot | attach_root | projection, snapshot/commit callbacks, unsubscribe |
29
+ | Cray/fromCrayResult/toCrayResult | Cray/from_cray_result/to_cray_result | structural result adapter, Python method spellings |
30
+ | Timeline / ExecutablePlan / KeyframeTrack | berylx.timeline equivalents | clock-driven scheduler |
31
+ | BerylxTimeline / BerylxTimelinePlan / waitForHumanLogin | berylx.timeline_berylx snake_case | async effect integration, concurrent commit |
32
+
33
+ ## Explicit exclusions and adaptations
34
+
35
+ - EffectTree's low-level public TS helpers `recover`, `runParallel`, `runBranch`,
36
+ `runRescue`, `realRecover` and their async variants are not exported as public
37
+ Python helpers. Equivalent execution is available through `run`/`run_async` and
38
+ handler maps. In particular, the TS legacy `recover(RescueBlock, Err)` inconsistency
39
+ is not reproduced as an extra path.
40
+ - TS `deriveFmap`/`deriveAp` helper exports are not public here; each monad implements
41
+ their derivations. Python Effect additionally provides fmap/map/ap.
42
+ - The JSON-shaped equivalence domain is string-keyed mappings, finite numbers,
43
+ booleans, strings, lists and None; no symbols, prototype traversal, sparse array
44
+ holes or JavaScript bigint/undefined identity is claimed. List growth uses None.
45
+ Nonplain objects retain identity; updates use Python `with_patch` instead of JS
46
+ `with`. Dict/list cycles are preserved; repeated mutable children are cloned
47
+ independently. Tuples are treated as application values and are not deeply frozen.
48
+ - to_object/get expose read-only mapping/list views. Use `berylx.core.plain` to make
49
+ ordinary containers for JSON serialization. Cyclic values are not JSON encodable.
50
+ Merge's structural equality/recursion is intended for acyclic values, as upstream.
51
+ - Python truthiness (empty containers false), arbitrary-precision ints, numeric
52
+ conversion, exception names/messages, and object equality follow Python. Custom
53
+ object comparison is not promised equivalent to JS identity. Inputs for matching
54
+ numeric observations should use finite safe JS integers and explicit predicates.
55
+ - `history_limit=True` is rejected (Python bool is an int subtype; JS boolean also
56
+ fails Number.isInteger). Pure callback arity counts required positional parameters
57
+ before the first default, analogous to Function.length. Zero-argument callbacks
58
+ receive no arguments because Python cannot ignore surplus positional arguments.
59
+ - Python cancellation/KeyboardInterrupt/SystemExit are BaseException control paths,
60
+ not ordinary Err. Cancellation semantics are native asyncio, not JS Promise.
61
+ Recovery blocks are synchronous; use AsyncTask recovery for awaited operations.
62
+ - The fold loop executes long right-associated programs iteratively; arbitrary deeply
63
+ left-associated bind closures are not guaranteed stack-safe, matching TS's limitation.
64
+ - Parallel strict follows upstream missing-key behavior, not deletion semantics:
65
+ one missing side adopts the present side, and a key missing from both sides can
66
+ expose an internal missing sentinel. Properties exclude deletions and incompatible
67
+ container shape changes. Timeline's separate commit join handles deletions.
68
+ - Graph node/edge structure is preserved, with JSON-safe DOT quoting (upstream raw
69
+ quotes can produce invalid DOT). Rendering is not claimed byte-identical for
70
+ escaped names. Cray duck typing recognizes Python is_success/is_failure methods,
71
+ plus success/ok/tag fields; JS camelCase-only foreign methods need an adapter.
72
+ - IO is UTF-8 text. Real now returns timezone-aware datetime; ask returns an empty
73
+ string at EOF, virtual exhausted input/env return None. Virtual random equivalence
74
+ is limited to nonempty sequences of nonnegative integers and positive n; negative
75
+ modulo, empty sequences and zero divisors use Python behavior. OS error types,
76
+ console rendering and shell behavior are platform-specific. No external IO occurs
77
+ merely by constructing an IOEffects tree.
78
+ - Python package version starts at 0.1.0 independently of TS 0.4.0. No PyPI artifact,
79
+ browser bundle, Node module shim, or TypeScript compile-time fixture parity claimed.