ledgerworks 0.2.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.
- ledgerworks-0.2.0/.gitignore +13 -0
- ledgerworks-0.2.0/LICENSE +21 -0
- ledgerworks-0.2.0/PKG-INFO +184 -0
- ledgerworks-0.2.0/README.md +147 -0
- ledgerworks-0.2.0/examples/codecheck/README.md +220 -0
- ledgerworks-0.2.0/examples/codecheck/data/raw/README.md +12 -0
- ledgerworks-0.2.0/examples/codecheck/expected/README.md +10 -0
- ledgerworks-0.2.0/examples/codecheck/expected/fomc_embeddings/README.md +82 -0
- ledgerworks-0.2.0/examples/codecheck/expected/fomc_narrative_scores/README.md +82 -0
- ledgerworks-0.2.0/examples/codecheck/expected/fomc_statements/README.md +46 -0
- ledgerworks-0.2.0/examples/codecheck/pyproject.toml +67 -0
- ledgerworks-0.2.0/examples/codecheck/tests/__init__.py +0 -0
- ledgerworks-0.2.0/examples/codecheck/tests/conftest.py +166 -0
- ledgerworks-0.2.0/examples/codecheck/tests/test_fetch.py +81 -0
- ledgerworks-0.2.0/examples/codecheck/tests/test_model_backend.py +109 -0
- ledgerworks-0.2.0/examples/codecheck/tests/test_pipeline.py +206 -0
- ledgerworks-0.2.0/pyproject.toml +77 -0
- ledgerworks-0.2.0/src/ledgerworks/__init__.py +71 -0
- ledgerworks-0.2.0/src/ledgerworks/artifact.py +396 -0
- ledgerworks-0.2.0/src/ledgerworks/cli.py +439 -0
- ledgerworks-0.2.0/src/ledgerworks/equivalence.py +554 -0
- ledgerworks-0.2.0/src/ledgerworks/jobs.py +811 -0
- ledgerworks-0.2.0/src/ledgerworks/jobs_cli.py +171 -0
- ledgerworks-0.2.0/src/ledgerworks/layout.py +158 -0
- ledgerworks-0.2.0/src/ledgerworks/ledger.py +908 -0
- ledgerworks-0.2.0/src/ledgerworks/lineage.py +87 -0
- ledgerworks-0.2.0/src/ledgerworks/meta.py +419 -0
- ledgerworks-0.2.0/src/ledgerworks/periods.py +229 -0
- ledgerworks-0.2.0/src/ledgerworks/plugins.py +49 -0
- ledgerworks-0.2.0/src/ledgerworks/relink.py +460 -0
- ledgerworks-0.2.0/src/ledgerworks/schema.sql +85 -0
- ledgerworks-0.2.0/src/ledgerworks/verify.py +362 -0
- ledgerworks-0.2.0/tests/__init__.py +0 -0
- ledgerworks-0.2.0/tests/test_cli.py +38 -0
- ledgerworks-0.2.0/tests/test_groups.py +262 -0
- ledgerworks-0.2.0/tests/test_jobs.py +322 -0
- ledgerworks-0.2.0/tests/test_jobs_cli.py +182 -0
- ledgerworks-0.2.0/tests/test_layout.py +72 -0
- ledgerworks-0.2.0/tests/test_ledger.py +1009 -0
- ledgerworks-0.2.0/tests/test_lineage.py +49 -0
- ledgerworks-0.2.0/tests/test_periods.py +117 -0
- ledgerworks-0.2.0/tests/test_plugins.py +40 -0
- ledgerworks-0.2.0/tests/test_relink.py +610 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gabin Taibi
|
|
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,184 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ledgerworks
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Run, record, verify and trace pipeline artifacts: immutable outputs with provenance sidecars, lineage and a rebuildable index
|
|
5
|
+
Project-URL: Homepage, https://github.com/GabinTB/ledgerworks
|
|
6
|
+
Project-URL: Repository, https://github.com/GabinTB/ledgerworks
|
|
7
|
+
Project-URL: Issues, https://github.com/GabinTB/ledgerworks/issues
|
|
8
|
+
Author: Gabin Taibi
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: artifacts,data-management,lineage,provenance,reproducibility,research
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Provides-Extra: all
|
|
23
|
+
Requires-Dist: numpy>=1.26; extra == 'all'
|
|
24
|
+
Requires-Dist: pandas>=2.0; extra == 'all'
|
|
25
|
+
Requires-Dist: polars>=0.20; extra == 'all'
|
|
26
|
+
Requires-Dist: pyarrow>=14.0; extra == 'all'
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0; extra == 'all'
|
|
28
|
+
Provides-Extra: cli
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0; extra == 'cli'
|
|
30
|
+
Provides-Extra: pandas
|
|
31
|
+
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
32
|
+
Provides-Extra: relink
|
|
33
|
+
Requires-Dist: numpy>=1.26; extra == 'relink'
|
|
34
|
+
Requires-Dist: polars>=0.20; extra == 'relink'
|
|
35
|
+
Requires-Dist: pyarrow>=14.0; extra == 'relink'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# ledgerworks
|
|
39
|
+
|
|
40
|
+
Run, record, verify and trace pipeline artifacts.
|
|
41
|
+
|
|
42
|
+
Every output of a pipeline becomes an immutable artifact directory with a
|
|
43
|
+
`meta.json` sidecar that says what it is, what it was built from, which code
|
|
44
|
+
and model produced it, and what every file hashed to when it was written.
|
|
45
|
+
A SQLite index makes lookups fast and can always be rebuilt from the sidecars,
|
|
46
|
+
so the files are the source of truth and the index is a cache.
|
|
47
|
+
|
|
48
|
+
| | |
|
|
49
|
+
|---|---|
|
|
50
|
+
| PyPI | `pip install ledgerworks` |
|
|
51
|
+
| Python | >= 3.11 |
|
|
52
|
+
| Core dependencies | none (stdlib only) |
|
|
53
|
+
| License | MIT |
|
|
54
|
+
|
|
55
|
+
## What it does
|
|
56
|
+
|
|
57
|
+
| Layer | What you get |
|
|
58
|
+
|---|---|
|
|
59
|
+
| **Record** | A deterministic artifact id (`{kind}__{model}__{version}__{params}__{minute}`), a `meta.json` and `README.md` sidecar per artifact, one `RunRecord` per execution (start, end, git commit, files produced, Python, platform, `uv.lock` digest and direct dependency versions), blake2b hash and size of every output file |
|
|
60
|
+
| **Query** | `latest(kind)`, `get(id)`, `list(...)`, `parents` / `children` / `ancestors` / `descendants`, DuckDB views by kind |
|
|
61
|
+
| **Verify** | Re-hash files, flag crashed (partial) runs and stray temp files, find lineage edges that point to nothing, run per-kind content checks you register |
|
|
62
|
+
| **Run** | `ledgerworks.jobs`: long pipelines as ordered work units with resume, update, pause, a heartbeat lock and a status file |
|
|
63
|
+
| **Migrate** | `ledgerworks.relink`: re-register an artifact's files under new inputs or new code without recomputing, after equivalence checks (needs the `relink` extra) |
|
|
64
|
+
|
|
65
|
+
Guarantees that hold everywhere:
|
|
66
|
+
|
|
67
|
+
- a run is **partial** until it exits cleanly, and `latest()` never returns a partial artifact;
|
|
68
|
+
- nothing is mutated or deleted: new data appends an execution, a wrong artifact is deprecated;
|
|
69
|
+
- the index holds nothing the sidecars lack, so `reindex` recovers it from disk at any time.
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install ledgerworks # core, stdlib only
|
|
75
|
+
pip install "ledgerworks[cli]" # + .env support for the command lines
|
|
76
|
+
pip install "ledgerworks[relink]" # + numpy / polars / pyarrow for relink checks
|
|
77
|
+
pip install "ledgerworks[all]"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For development:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/GabinTB/ledgerworks
|
|
84
|
+
cd ledgerworks
|
|
85
|
+
uv sync --group dev
|
|
86
|
+
uv run pytest
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Quick start
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from ledgerworks import Ledger, ModelCard
|
|
93
|
+
|
|
94
|
+
ledger = Ledger("/data/ledger")
|
|
95
|
+
|
|
96
|
+
# 1. register a raw input
|
|
97
|
+
with ledger.run(kind="documents", pipeline="my-project", pipeline_version="v0.1.0",
|
|
98
|
+
hyperparams={"source": "fomc", "start": "2021-01-01"},
|
|
99
|
+
layer="raw") as run:
|
|
100
|
+
write_parquet(run.out_dir / "documents.parquet", docs)
|
|
101
|
+
|
|
102
|
+
# 2. derive something from it, with lineage and a model card
|
|
103
|
+
source = ledger.latest("documents")
|
|
104
|
+
card = ModelCard(model_id="my-embedder", version="1.0", repo="https://hf.co/...",
|
|
105
|
+
weights_public=True, dim=768)
|
|
106
|
+
|
|
107
|
+
with ledger.run(kind="document_embeddings", pipeline="my-project", pipeline_version="v0.1.0",
|
|
108
|
+
hyperparams={"pooling": "mean"}, sources=[source], model_card=card,
|
|
109
|
+
verifier="document_embeddings") as run:
|
|
110
|
+
embed(source.path, run.out_dir)
|
|
111
|
+
|
|
112
|
+
# 3. query
|
|
113
|
+
art = ledger.latest("document_embeddings")
|
|
114
|
+
print(art.artifact_id, art.meta.pipeline_commit, art.file_hashes)
|
|
115
|
+
print(ledger.ancestors(art.artifact_id))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
On clean exit the run's record is marked complete, the outputs are hashed and
|
|
119
|
+
the sidecars written. On an exception the record stays partial, so the crash is
|
|
120
|
+
visible instead of silent.
|
|
121
|
+
|
|
122
|
+
The artifact directory:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
{root}/derived/document_embeddings/document_embeddings__my-embedder-1.0__v0.1.0__poolingmean__20261014T1530/
|
|
126
|
+
meta.json # the record: identity, hyperparams, sources, model card, runs[], file hashes
|
|
127
|
+
README.md # the same, rendered for humans
|
|
128
|
+
2021-01.parquet
|
|
129
|
+
...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Command lines
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
ledgerworks init | ls | show ID | verify | lineage ID | deprecate ID --reason ... | reindex | move | relink
|
|
136
|
+
ledgerworks-jobs start KIND | resume ID | update ID | pause ID | status | list | check ID
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The root comes from `--root`, else `$LEDGER_ROOT` (also read from `.env` when
|
|
140
|
+
the `cli` extra is installed).
|
|
141
|
+
|
|
142
|
+
## Plugging in your own kinds
|
|
143
|
+
|
|
144
|
+
A package contributes jobs, content verifiers and equivalence checks through
|
|
145
|
+
entry points in its `pyproject.toml`:
|
|
146
|
+
|
|
147
|
+
```toml
|
|
148
|
+
[project.entry-points."ledgerworks.jobs"]
|
|
149
|
+
document_embeddings = "my_project.embed:EmbedJob"
|
|
150
|
+
|
|
151
|
+
[project.entry-points."ledgerworks.verifiers"]
|
|
152
|
+
document_embeddings = "my_project.embed:verify_artifact"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A `Job` declares what it computes (`kind`, `pipeline`, `pipeline_version`,
|
|
156
|
+
`params()`, `sources()`, `units()`, `run_unit()`); `JobRunner` owns how it
|
|
157
|
+
runs. See `ledgerworks/jobs.py` for the contract and `examples/codecheck` for
|
|
158
|
+
a complete job.
|
|
159
|
+
|
|
160
|
+
## Examples
|
|
161
|
+
|
|
162
|
+
- [`examples/codecheck`](examples/codecheck): a self-contained reproducibility
|
|
163
|
+
package (FOMC statements, an open embedding model, a macro-narrative
|
|
164
|
+
taxonomy) submitted to the University of Twente CodeCheck 2026. Run it with
|
|
165
|
+
one command and compare the outputs to the committed reference.
|
|
166
|
+
Reproduce it: `cd examples/codecheck && uv sync --extra model && uv run codecheck-fomc all`
|
|
167
|
+
(see [`examples/codecheck/README.md`](examples/codecheck/README.md)).
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
uv run pytest # tests
|
|
173
|
+
uv run ruff check . # lint
|
|
174
|
+
uv run ruff format . # format
|
|
175
|
+
uv build # wheel + sdist
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Releases: push a tag `vX.Y.Z` matching the version in `pyproject.toml`. The
|
|
179
|
+
release workflow runs CI, publishes to TestPyPI, installs from there as a
|
|
180
|
+
smoke test, then publishes to PyPI and creates a GitHub release.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# ledgerworks
|
|
2
|
+
|
|
3
|
+
Run, record, verify and trace pipeline artifacts.
|
|
4
|
+
|
|
5
|
+
Every output of a pipeline becomes an immutable artifact directory with a
|
|
6
|
+
`meta.json` sidecar that says what it is, what it was built from, which code
|
|
7
|
+
and model produced it, and what every file hashed to when it was written.
|
|
8
|
+
A SQLite index makes lookups fast and can always be rebuilt from the sidecars,
|
|
9
|
+
so the files are the source of truth and the index is a cache.
|
|
10
|
+
|
|
11
|
+
| | |
|
|
12
|
+
|---|---|
|
|
13
|
+
| PyPI | `pip install ledgerworks` |
|
|
14
|
+
| Python | >= 3.11 |
|
|
15
|
+
| Core dependencies | none (stdlib only) |
|
|
16
|
+
| License | MIT |
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
| Layer | What you get |
|
|
21
|
+
|---|---|
|
|
22
|
+
| **Record** | A deterministic artifact id (`{kind}__{model}__{version}__{params}__{minute}`), a `meta.json` and `README.md` sidecar per artifact, one `RunRecord` per execution (start, end, git commit, files produced, Python, platform, `uv.lock` digest and direct dependency versions), blake2b hash and size of every output file |
|
|
23
|
+
| **Query** | `latest(kind)`, `get(id)`, `list(...)`, `parents` / `children` / `ancestors` / `descendants`, DuckDB views by kind |
|
|
24
|
+
| **Verify** | Re-hash files, flag crashed (partial) runs and stray temp files, find lineage edges that point to nothing, run per-kind content checks you register |
|
|
25
|
+
| **Run** | `ledgerworks.jobs`: long pipelines as ordered work units with resume, update, pause, a heartbeat lock and a status file |
|
|
26
|
+
| **Migrate** | `ledgerworks.relink`: re-register an artifact's files under new inputs or new code without recomputing, after equivalence checks (needs the `relink` extra) |
|
|
27
|
+
|
|
28
|
+
Guarantees that hold everywhere:
|
|
29
|
+
|
|
30
|
+
- a run is **partial** until it exits cleanly, and `latest()` never returns a partial artifact;
|
|
31
|
+
- nothing is mutated or deleted: new data appends an execution, a wrong artifact is deprecated;
|
|
32
|
+
- the index holds nothing the sidecars lack, so `reindex` recovers it from disk at any time.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ledgerworks # core, stdlib only
|
|
38
|
+
pip install "ledgerworks[cli]" # + .env support for the command lines
|
|
39
|
+
pip install "ledgerworks[relink]" # + numpy / polars / pyarrow for relink checks
|
|
40
|
+
pip install "ledgerworks[all]"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For development:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/GabinTB/ledgerworks
|
|
47
|
+
cd ledgerworks
|
|
48
|
+
uv sync --group dev
|
|
49
|
+
uv run pytest
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from ledgerworks import Ledger, ModelCard
|
|
56
|
+
|
|
57
|
+
ledger = Ledger("/data/ledger")
|
|
58
|
+
|
|
59
|
+
# 1. register a raw input
|
|
60
|
+
with ledger.run(kind="documents", pipeline="my-project", pipeline_version="v0.1.0",
|
|
61
|
+
hyperparams={"source": "fomc", "start": "2021-01-01"},
|
|
62
|
+
layer="raw") as run:
|
|
63
|
+
write_parquet(run.out_dir / "documents.parquet", docs)
|
|
64
|
+
|
|
65
|
+
# 2. derive something from it, with lineage and a model card
|
|
66
|
+
source = ledger.latest("documents")
|
|
67
|
+
card = ModelCard(model_id="my-embedder", version="1.0", repo="https://hf.co/...",
|
|
68
|
+
weights_public=True, dim=768)
|
|
69
|
+
|
|
70
|
+
with ledger.run(kind="document_embeddings", pipeline="my-project", pipeline_version="v0.1.0",
|
|
71
|
+
hyperparams={"pooling": "mean"}, sources=[source], model_card=card,
|
|
72
|
+
verifier="document_embeddings") as run:
|
|
73
|
+
embed(source.path, run.out_dir)
|
|
74
|
+
|
|
75
|
+
# 3. query
|
|
76
|
+
art = ledger.latest("document_embeddings")
|
|
77
|
+
print(art.artifact_id, art.meta.pipeline_commit, art.file_hashes)
|
|
78
|
+
print(ledger.ancestors(art.artifact_id))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
On clean exit the run's record is marked complete, the outputs are hashed and
|
|
82
|
+
the sidecars written. On an exception the record stays partial, so the crash is
|
|
83
|
+
visible instead of silent.
|
|
84
|
+
|
|
85
|
+
The artifact directory:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
{root}/derived/document_embeddings/document_embeddings__my-embedder-1.0__v0.1.0__poolingmean__20261014T1530/
|
|
89
|
+
meta.json # the record: identity, hyperparams, sources, model card, runs[], file hashes
|
|
90
|
+
README.md # the same, rendered for humans
|
|
91
|
+
2021-01.parquet
|
|
92
|
+
...
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Command lines
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
ledgerworks init | ls | show ID | verify | lineage ID | deprecate ID --reason ... | reindex | move | relink
|
|
99
|
+
ledgerworks-jobs start KIND | resume ID | update ID | pause ID | status | list | check ID
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The root comes from `--root`, else `$LEDGER_ROOT` (also read from `.env` when
|
|
103
|
+
the `cli` extra is installed).
|
|
104
|
+
|
|
105
|
+
## Plugging in your own kinds
|
|
106
|
+
|
|
107
|
+
A package contributes jobs, content verifiers and equivalence checks through
|
|
108
|
+
entry points in its `pyproject.toml`:
|
|
109
|
+
|
|
110
|
+
```toml
|
|
111
|
+
[project.entry-points."ledgerworks.jobs"]
|
|
112
|
+
document_embeddings = "my_project.embed:EmbedJob"
|
|
113
|
+
|
|
114
|
+
[project.entry-points."ledgerworks.verifiers"]
|
|
115
|
+
document_embeddings = "my_project.embed:verify_artifact"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
A `Job` declares what it computes (`kind`, `pipeline`, `pipeline_version`,
|
|
119
|
+
`params()`, `sources()`, `units()`, `run_unit()`); `JobRunner` owns how it
|
|
120
|
+
runs. See `ledgerworks/jobs.py` for the contract and `examples/codecheck` for
|
|
121
|
+
a complete job.
|
|
122
|
+
|
|
123
|
+
## Examples
|
|
124
|
+
|
|
125
|
+
- [`examples/codecheck`](examples/codecheck): a self-contained reproducibility
|
|
126
|
+
package (FOMC statements, an open embedding model, a macro-narrative
|
|
127
|
+
taxonomy) submitted to the University of Twente CodeCheck 2026. Run it with
|
|
128
|
+
one command and compare the outputs to the committed reference.
|
|
129
|
+
Reproduce it: `cd examples/codecheck && uv sync --extra model && uv run codecheck-fomc all`
|
|
130
|
+
(see [`examples/codecheck/README.md`](examples/codecheck/README.md)).
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
uv run pytest # tests
|
|
136
|
+
uv run ruff check . # lint
|
|
137
|
+
uv run ruff format . # format
|
|
138
|
+
uv build # wheel + sdist
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Releases: push a tag `vX.Y.Z` matching the version in `pyproject.toml`. The
|
|
142
|
+
release workflow runs CI, publishes to TestPyPI, installs from there as a
|
|
143
|
+
smoke test, then publishes to PyPI and creates a GitHub release.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# CodeCheck 2026: FOMC statements scored against a monetary-policy narrative taxonomy
|
|
2
|
+
|
|
3
|
+
A self-contained reproducibility package, submitted to the University of Twente
|
|
4
|
+
CodeCheck / Reproducibility Hackathon (20 October 2026). It is one slice of an
|
|
5
|
+
ongoing PhD project on narrative dynamics in financial markets (Gabin Taibi,
|
|
6
|
+
University of Twente, BMS-IEBIS), rebuilt on open data and an open model so
|
|
7
|
+
that every step can be re-run and checked by a reviewer.
|
|
8
|
+
|
|
9
|
+
**What is reproduced:** the two computational steps at the core of the research
|
|
10
|
+
pipeline, (1) embedding a corpus of documents with a finance-adapted language
|
|
11
|
+
model and (2) scoring every document against a taxonomy of narratives, applied
|
|
12
|
+
to FOMC statements. The outputs are the embedding tables and the score tables
|
|
13
|
+
under `expected/`. There is no paper table to match: this is the "ongoing
|
|
14
|
+
project" route of the CodeCheck call, and the research taxonomy itself is not
|
|
15
|
+
published here (the file in `taxonomy/` is a public stand-in with the same
|
|
16
|
+
structure).
|
|
17
|
+
|
|
18
|
+
**What the reviewer checks:** that `codecheck-fomc all` rebuilds, from the files
|
|
19
|
+
in this directory, outputs equal to `expected/` within the stated tolerances,
|
|
20
|
+
and that every artifact's provenance record (`meta.json`) is intact. The
|
|
21
|
+
package is also the end-to-end example of
|
|
22
|
+
[`ledgerworks`](../../README.md), the artifact-provenance library the project
|
|
23
|
+
is built on.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
Requirements: Python >= 3.11, [uv](https://docs.astral.sh/uv/), about 2.7 GB of
|
|
28
|
+
disk (measured on macOS, Apple silicon: 1.2 GB for `.venv`, 1.5 GB for the model
|
|
29
|
+
in the Hugging Face cache), a network connection the first time (to
|
|
30
|
+
download the model from the Hugging Face Hub). CPU is enough. On Linux, `uv.lock`
|
|
31
|
+
takes torch from the PyTorch CPU index (`2.14.1+cpu`), not PyPI, whose Linux
|
|
32
|
+
wheel pulls several GB of CUDA libraries a CPU run never loads; macOS and
|
|
33
|
+
Windows use PyPI's torch, which is CPU-only there.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/GabinTB/ledgerworks
|
|
37
|
+
cd ledgerworks/examples/codecheck
|
|
38
|
+
uv sync --extra model # creates .venv from uv.lock (pinned)
|
|
39
|
+
uv run codecheck-fomc all # ingest, embed, score, verify, compare
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The corpus is 46 statements from 2021-01-27 to 2026-09-16, split into 190
|
|
43
|
+
paragraphs. Measured runtime on an Apple M4 Pro (CPU), model already cached:
|
|
44
|
+
about 15 seconds wall clock, almost all of it in the embedding step (a
|
|
45
|
+
395M-parameter model over 190 paragraphs). The first run also downloads the
|
|
46
|
+
model weights (1.6 GB). The last lines of the output are the comparison with
|
|
47
|
+
`expected/` and a verdict (here, on the machine that produced `expected/`):
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
fomc_narrative_scores: ...
|
|
51
|
+
ok fomc_narrative_scores/2021.parquet: 80 scores, max|diff| 0.00e+00
|
|
52
|
+
...
|
|
53
|
+
ok fomc_narrative_scores/2026.parquet: 60 scores, max|diff| 0.00e+00
|
|
54
|
+
ok fomc_narrative_scores/narratives.parquet: 10 vectors, max|diff| 0.00e+00, min cosine 1.000000
|
|
55
|
+
7/7 files hash-identical to the reference
|
|
56
|
+
|
|
57
|
+
REPRODUCED
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The score tables hold 460 rows in total (46 statements × 10 narratives).
|
|
61
|
+
|
|
62
|
+
`uv run codecheck-fomc --help` lists the individual steps. Everything lands in
|
|
63
|
+
`./ledger/` (deleted and rebuilt freely; it is in `.gitignore`).
|
|
64
|
+
|
|
65
|
+
To check the plumbing without the model (seconds, no download):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv sync
|
|
69
|
+
uv run codecheck-fomc --backend hashing all # NOT REPRODUCED is expected here:
|
|
70
|
+
# expected/ was produced with the real model
|
|
71
|
+
uv run pytest # offline tests on a fixture corpus
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## What is in the box
|
|
75
|
+
|
|
76
|
+
| Path | What | Who produced it |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `data/raw/html/*.htm` | The FOMC statement pages, byte for byte as served by federalreserve.gov | author, with `fetch_fomc.py` |
|
|
79
|
+
| `data/raw/MANIFEST.json` | URL, date, SHA-256 and retrieval time of every page | `fetch_fomc.py` |
|
|
80
|
+
| `taxonomy/monetary_policy.yaml` | Ten monetary-policy narratives: label, description, seed phrases | author |
|
|
81
|
+
| `codecheck_fomc/` | The pipeline: three `ledgerworks` jobs, the embedding backends, the comparison | author |
|
|
82
|
+
| `expected/` | The reference run: the three artifact directories with their `meta.json` | author, with `codecheck-fomc freeze` |
|
|
83
|
+
| `uv.lock` | Every Python dependency pinned, model stack included | `uv` |
|
|
84
|
+
| `tests/` | Offline tests of the whole pipeline on a fixture corpus (hashing backend) | author |
|
|
85
|
+
|
|
86
|
+
### Data
|
|
87
|
+
|
|
88
|
+
FOMC statements from January 2021 to the retrieval date, one HTML page per
|
|
89
|
+
meeting statement (other FOMC press releases, such as the August 2025 update of
|
|
90
|
+
the Statement on Longer-Run Goals and Monetary Policy Strategy, are excluded), downloaded from the Board's
|
|
91
|
+
[FOMC calendars page](https://www.federalreserve.gov/monetarypolicy/fomccalendars.htm).
|
|
92
|
+
Content on the Board's website is in the public domain and may be copied and
|
|
93
|
+
distributed without permission
|
|
94
|
+
([disclaimer](https://www.federalreserve.gov/disclaimer.htm)); the Board of
|
|
95
|
+
Governors of the Federal Reserve System is the source. The committed HTML is the
|
|
96
|
+
immutable input: parsing it into a table is the first job of the pipeline, so
|
|
97
|
+
it is recorded and re-run like every other step. `fetch_fomc.py` is kept for
|
|
98
|
+
provenance; the reviewer does not need to run it.
|
|
99
|
+
|
|
100
|
+
### Model
|
|
101
|
+
|
|
102
|
+
[`BatuhanECB/FinModernBERT-embed-large-v1`](https://huggingface.co/BatuhanECB/FinModernBERT-embed-large-v1),
|
|
103
|
+
a ModernBERT-large encoder domain-adapted on financial text and contrastively
|
|
104
|
+
fine-tuned for embeddings (Apache-2.0, 395M parameters, 1024 dimensions). It is
|
|
105
|
+
pinned to Hub commit `4fd33d2335e75c79df57b345aee06d49c5f85bc8` in
|
|
106
|
+
`codecheck_fomc/backends.py` and loaded through `sentence-transformers`; the
|
|
107
|
+
exact library versions are in `uv.lock` and in every artifact's model card
|
|
108
|
+
(`meta.json` → `model_card.serving`). It runs on CPU in float32 with
|
|
109
|
+
`torch.use_deterministic_algorithms(True)`.
|
|
110
|
+
|
|
111
|
+
### Taxonomy
|
|
112
|
+
|
|
113
|
+
`taxonomy/monetary_policy.yaml` holds ten narratives. Where a label coincides
|
|
114
|
+
with a topic of Bybee, Kelly, Manela and Xiu (2024), *Business News and
|
|
115
|
+
Business Cycles*, Journal of Finance 79(5),
|
|
116
|
+
[doi:10.1111/jofi.13377](https://doi.org/10.1111/jofi.13377), the file says so
|
|
117
|
+
(`bkmx: true`); the descriptions and seed phrases are this package's own and are
|
|
118
|
+
the part a researcher swaps for their own taxonomy.
|
|
119
|
+
|
|
120
|
+
## The pipeline
|
|
121
|
+
|
|
122
|
+
Three jobs, each producing one `ledgerworks` artifact (an immutable directory
|
|
123
|
+
with a `meta.json` record), each partitioned one file per year:
|
|
124
|
+
|
|
125
|
+
| Step | Kind | Input | Output |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| 1 ingest | `fomc_statements` (raw layer) | `data/raw/` | one row per statement: `doc_id`, `date`, `url`, `html_sha256`, `paragraphs`, `text` |
|
|
128
|
+
| 2 embed | `fomc_embeddings` | step 1 | one row per paragraph: `doc_id`, `para_idx`, `text_sha256`, `embedding` (unit norm) |
|
|
129
|
+
| 3 score | `fomc_narrative_scores` | step 2 + taxonomy | one row per statement × narrative: `cos_doc`, `cos_para_mean`, `cos_para_max`; plus `narratives.parquet` (the narrative vectors) |
|
|
130
|
+
|
|
131
|
+
Paragraphs are the embedding unit because FOMC statements are written as short
|
|
132
|
+
self-contained paragraphs and the model was trained at 512 tokens. The parser
|
|
133
|
+
(`codecheck_fomc/corpus.py`, `PARSER_VERSION = "3"`, recorded in the
|
|
134
|
+
`fomc_statements` hyperparameters) keeps only what the Committee says. From the
|
|
135
|
+
`<p>` elements of the press release it drops:
|
|
136
|
+
|
|
137
|
+
- page furniture ("For release at", "Share", "Implementation Note", "Last Update", ...);
|
|
138
|
+
- the release-date line printed above the title ("January 27, 2021");
|
|
139
|
+
- sidebar panels (`<div class="panel ...">`, "Related Information" links);
|
|
140
|
+
- vote paragraphs, in both formats: "Voting for/against the monetary policy
|
|
141
|
+
action ..." (with any dissents inside it), the approval line that opens
|
|
142
|
+
statements from June 2026 ("The Federal Open Market Committee approved the
|
|
143
|
+
following statement for release by a 12 – 0 vote:"), and standalone dissent
|
|
144
|
+
paragraphs ("<Name>, who preferred ...", "<Name> dissented").
|
|
145
|
+
A narrative
|
|
146
|
+
vector is the mean of the embeddings of its description and seed phrases, from
|
|
147
|
+
the same model. `cos_doc` compares the narrative with the mean of a statement's
|
|
148
|
+
paragraph vectors; `cos_para_mean` and `cos_para_max` aggregate paragraph-level
|
|
149
|
+
cosines. These are raw scores; calibration is downstream work and out of scope.
|
|
150
|
+
|
|
151
|
+
Every artifact records: its hyperparameters (which define its id), its input
|
|
152
|
+
artifact (lineage), the model card (id, revision, dimension, pooling, library
|
|
153
|
+
versions, device), the git commit of the code, start and end times, the
|
|
154
|
+
execution environment (Python, platform, `uv.lock` digest, versions of the
|
|
155
|
+
direct dependencies), and the blake2b digest of every output file. `ledgerworks verify --root ledger` re-hashes
|
|
156
|
+
everything and checks the lineage; `ledgerworks lineage ID` prints the graph.
|
|
157
|
+
|
|
158
|
+
## How the comparison works
|
|
159
|
+
|
|
160
|
+
`codecheck-fomc compare` joins each table of the fresh run with the reference
|
|
161
|
+
on its keys and checks:
|
|
162
|
+
|
|
163
|
+
| Table | Check |
|
|
164
|
+
|---|---|
|
|
165
|
+
| statements | rows identical (text and HTML digest) |
|
|
166
|
+
| embeddings | same paragraphs (text digest); vectors within `--atol 1e-4` element-wise and cosine >= `--min-cos 0.9999` |
|
|
167
|
+
| scores | within `--atol 1e-4` |
|
|
168
|
+
|
|
169
|
+
Neural-network outputs are reproducible to floating-point tolerance across
|
|
170
|
+
machines, not bit for bit (different BLAS kernels, instruction sets and thread
|
|
171
|
+
counts reorder floating-point sums), hence the tolerances. The ingest step and
|
|
172
|
+
the hashing test backend are exact, and `compare` reports how many files are
|
|
173
|
+
hash-identical to the reference. The comparison exits non-zero on any failure;
|
|
174
|
+
with no reference in `expected/` yet (first run) it says so and exits 0.
|
|
175
|
+
|
|
176
|
+
## Cross-machine check
|
|
177
|
+
|
|
178
|
+
`expected/` was produced on an Apple M4 Pro (macOS, arm64, CPython 3.12.13, PyPI
|
|
179
|
+
torch 2.14.1; Python is pinned by `.python-version`). The manual workflow
|
|
180
|
+
[`reproduce.yml`](../../.github/workflows/reproduce.yml) rebuilds everything on
|
|
181
|
+
GitHub-hosted runners with `uv sync --extra model` and `codecheck-fomc all`, and
|
|
182
|
+
uploads every `meta.json` and the comparison output. Run of 14 October 2026 at
|
|
183
|
+
commit `7619ab4`:
|
|
184
|
+
|
|
185
|
+
| | Linux (ubuntu-latest) | Windows (windows-latest) |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| Machine | x86_64, CPython 3.12.3, torch 2.14.1+cpu | AMD64, CPython 3.12.10, torch 2.14.1 |
|
|
188
|
+
| Verdict | REPRODUCED | REPRODUCED |
|
|
189
|
+
| statements | 46 identical, 6/6 files hash-identical | 46 identical, 6/6 files hash-identical |
|
|
190
|
+
| embeddings, max\|diff\| | 6.97e-06 | 5.45e-06 |
|
|
191
|
+
| embeddings, min cosine | 1.000000 | 1.000000 |
|
|
192
|
+
| scores, max\|diff\| | 5.01e-06 | 3.76e-06 |
|
|
193
|
+
| narrative vectors, max\|diff\| / min cosine | 3.73e-07 / 1.000000 | 3.80e-07 / 1.000000 |
|
|
194
|
+
| Wall time, `all` (model cached) | 135 s | 175 s |
|
|
195
|
+
|
|
196
|
+
The largest difference is about 14 times below the tolerance (`atol 1e-4`), and
|
|
197
|
+
no file of the embedding or score tables is bit-identical across machines, as
|
|
198
|
+
expected from different CPUs and BLAS kernels. The artifact ids (up to the
|
|
199
|
+
creation minute) and the recorded `uv.lock` digest are the same on all three
|
|
200
|
+
machines: `.gitattributes` keeps text files in LF on every checkout and the raw
|
|
201
|
+
HTML byte for byte.
|
|
202
|
+
|
|
203
|
+
## Reproducing the reference run (author's procedure)
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
python fetch_fomc.py # network: download the statements
|
|
207
|
+
git add data/raw && git commit -m "data: FOMC statements to <date>"
|
|
208
|
+
uv sync --extra model
|
|
209
|
+
uv run codecheck-fomc all # from a clean tree, so the commit is recorded
|
|
210
|
+
uv run codecheck-fomc freeze # copy the run into expected/
|
|
211
|
+
git add expected && git commit -m "expected: reference run"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
A dirty working tree is refused by the jobs unless `--allow-dirty`, and
|
|
215
|
+
`verify` flags artifacts whose recorded commit ends in `-dirty`.
|
|
216
|
+
|
|
217
|
+
## Contact
|
|
218
|
+
|
|
219
|
+
Gabin Taibi, University of Twente (BMS-IEBIS), gabin.taibi@gmail.com.
|
|
220
|
+
Issues and questions: https://github.com/GabinTB/ledgerworks/issues.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# data/raw
|
|
2
|
+
|
|
3
|
+
Produced once by the package author with `python fetch_fomc.py` (see the
|
|
4
|
+
example README), then committed:
|
|
5
|
+
|
|
6
|
+
- `html/monetaryYYYYMMDDa.htm`: each FOMC statement page, byte for byte as
|
|
7
|
+
served by federalreserve.gov (public domain; source: Board of Governors of the
|
|
8
|
+
Federal Reserve System).
|
|
9
|
+
- `MANIFEST.json`: URL, date, SHA-256, size and retrieval time of every page.
|
|
10
|
+
|
|
11
|
+
The pipeline's first job (`codecheck-fomc ingest`) parses these files; it
|
|
12
|
+
refuses a file whose digest no longer matches the manifest.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# expected
|
|
2
|
+
|
|
3
|
+
The reference run, produced by the package author with
|
|
4
|
+
`codecheck-fomc all` followed by `codecheck-fomc freeze`: one directory per
|
|
5
|
+
artifact kind (`fomc_statements`, `fomc_embeddings`, `fomc_narrative_scores`),
|
|
6
|
+
each a verbatim copy of the artifact directory including its `meta.json`
|
|
7
|
+
(hyperparameters, inputs, model card, git commit, file digests), plus
|
|
8
|
+
`RUN.json` with the three artifact ids.
|
|
9
|
+
|
|
10
|
+
`codecheck-fomc compare` checks a fresh run against these files.
|