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.
- berylx_py-0.1.0/.github/workflows/ci.yml +30 -0
- berylx_py-0.1.0/.github/workflows/publish.yml +90 -0
- berylx_py-0.1.0/LICENSE +21 -0
- berylx_py-0.1.0/MANIFEST.in +6 -0
- berylx_py-0.1.0/PKG-INFO +181 -0
- berylx_py-0.1.0/README.md +160 -0
- berylx_py-0.1.0/docs/api.md +79 -0
- berylx_py-0.1.0/docs/equivalence.md +110 -0
- berylx_py-0.1.0/docs/host-and-delivery.md +62 -0
- berylx_py-0.1.0/docs/source-audit.md +59 -0
- berylx_py-0.1.0/docs/spec/berylx-py.spec +65 -0
- berylx_py-0.1.0/docs/timeline.md +192 -0
- berylx_py-0.1.0/evidence/build.txt +193 -0
- berylx_py-0.1.0/evidence/delivery.txt +29 -0
- berylx_py-0.1.0/evidence/final-gates.txt +221 -0
- berylx_py-0.1.0/evidence/frozen-sha256.txt +35 -0
- berylx_py-0.1.0/evidence/independent-final.txt +446 -0
- berylx_py-0.1.0/evidence/p2-oracle.txt +133 -0
- berylx_py-0.1.0/evidence/p2-red.txt +35 -0
- berylx_py-0.1.0/evidence/p3-core.txt +4 -0
- berylx_py-0.1.0/evidence/p3-integrated.txt +4 -0
- berylx_py-0.1.0/evidence/p4-property.txt +4 -0
- berylx_py-0.1.0/evidence/pypi-preflight.txt +258 -0
- berylx_py-0.1.0/evidence/python-matrix.txt +181 -0
- berylx_py-0.1.0/evidence/rebuild-final-docs.txt +181 -0
- berylx_py-0.1.0/evidence/review-cray-red.txt +20 -0
- berylx_py-0.1.0/evidence/review-red.txt +57 -0
- berylx_py-0.1.0/evidence/spec-sat.txt +4 -0
- berylx_py-0.1.0/evidence/upstream.txt +76 -0
- berylx_py-0.1.0/evidence/wheel-install.txt +3 -0
- berylx_py-0.1.0/evidence/wheel-smoke.txt +27 -0
- berylx_py-0.1.0/pyproject.toml +39 -0
- berylx_py-0.1.0/requirements-dev.txt +16 -0
- berylx_py-0.1.0/scripts/check_readme.py +10 -0
- berylx_py-0.1.0/scripts/ts_oracle.mjs +22 -0
- berylx_py-0.1.0/setup.cfg +4 -0
- berylx_py-0.1.0/src/berylx/__init__.py +129 -0
- berylx_py-0.1.0/src/berylx/adapters.py +96 -0
- berylx_py-0.1.0/src/berylx/core.py +514 -0
- berylx_py-0.1.0/src/berylx/darkcore/__init__.py +331 -0
- berylx_py-0.1.0/src/berylx/darkcore/io.py +153 -0
- berylx_py-0.1.0/src/berylx/effect_tree.py +305 -0
- berylx_py-0.1.0/src/berylx/graph.py +101 -0
- berylx_py-0.1.0/src/berylx/merge.py +99 -0
- berylx_py-0.1.0/src/berylx/nodes.py +292 -0
- berylx_py-0.1.0/src/berylx/timeline.py +614 -0
- berylx_py-0.1.0/src/berylx/timeline_berylx.py +253 -0
- berylx_py-0.1.0/src/berylx_py.egg-info/PKG-INFO +181 -0
- berylx_py-0.1.0/src/berylx_py.egg-info/SOURCES.txt +57 -0
- berylx_py-0.1.0/src/berylx_py.egg-info/dependency_links.txt +1 -0
- berylx_py-0.1.0/src/berylx_py.egg-info/requires.txt +7 -0
- berylx_py-0.1.0/src/berylx_py.egg-info/top_level.txt +1 -0
- berylx_py-0.1.0/tests/fixtures/ts-observations.json +112 -0
- berylx_py-0.1.0/tests/test_boundaries.py +250 -0
- berylx_py-0.1.0/tests/test_darkcore.py +90 -0
- berylx_py-0.1.0/tests/test_oracle.py +65 -0
- berylx_py-0.1.0/tests/test_properties.py +283 -0
- berylx_py-0.1.0/tests/test_regression.py +267 -0
- 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
|
berylx_py-0.1.0/LICENSE
ADDED
|
@@ -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.
|
berylx_py-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|