Brianferrell787 commited on
Commit
9b4dbef
·
verified ·
1 Parent(s): f8768df

Refresh RANGE with maintained CPU runtime, benchmarks and roadmap

Browse files

Standalone RANGE presentation; Python package coming soon. Rebuilt Linux CPU wheel passed10 contract tests plus local single/batch API checks. CUDA benchmark record is historical and distinct from live CPU demo.

BENCHMARKS.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # RANGE benchmark record
2
+
3
+ Updated 2026-09-12. These results describe compact screening of a staged corpus,
4
+ not the live Space, rich explanations, disk ingestion, or a service guarantee.
5
+
6
+ | Evidence | Articles/s | Workload and status |
7
+ | --- | ---: | --- |
8
+ | Maintained CPU, Phase24 | 489,759 | 10 million articles; qualified warm screening |
9
+ | CUDA historical record, Phase60 | 1,833,059 | 10 million articles; exact outputs, stable timing |
10
+ | Earlier installed CUDA, Phase61 | 1,809,133 | Separate qualified local installation |
11
+ | Newer retained CUDA installed, Phase65 | 958,155 | Separate qualified local installation of Phase64 |
12
+
13
+ Hardware: Intel i7-10750H, 10 CPU workers, Windows, RTX 2070 Max-Q (sm75) for CUDA.
14
+ The CUDA figures include input staging/transfers, exact CPU fallback and output
15
+ assembly within classification calls. Dataset loading, imports, configuration
16
+ construction and result verification are outside timing. CPU figures cover warm
17
+ compact screening, excluding loading and rich explanation construction.
18
+
19
+ These are separate experiments. They do not establish a paired CPU/GPU speedup.
20
+ The later Phase64 algorithm won its own controlled comparison, but that does not
21
+ allow multiplying its improvement by the historical Phase60 peak. Session timing
22
+ varied substantially; the cause of cross-session changes is not established.
23
+
24
+ ## Evidence identities
25
+
26
+ The 10-million-row Arrow stage SHA-256 is
27
+ 769d7de4a8295f4ce3f3d7508774e57ebdeaf2de9b4f4d7e31d1bb5a84d2865f.
28
+
29
+ Phase60's complete-call median was5.45536245 seconds versus6.17977660 for its
30
+ Phase59 control. Its18 calls matched all nine output fields. Both arms passed
31
+ the5% cycle-drift threshold; the same-run improvement was13.2789%.
32
+ Its actual CUDA binary SHA-256 was
33
+ dd943d9adac702e41c2bb0a2b58f78698498edd8a3dfab16cf9d224c8c6a83fa.
34
+ Source checkpoint:8f3fa47, with the original failed finite-gate attempt retained
35
+ in the internal append-only experiment record.
36
+
37
+ Phase65 installed the Phase64 algorithm in a fresh target using existing
38
+ dependencies. It measured958,155 articles/s versus956,701/s for its research
39
+ control. The companion wheel SHA-256 was
40
+ 2269c6d41c8ca59bfd48468e5d0c9a00a7abab99297f0194e0227730a2e92643.
41
+
42
+ ## What is available
43
+
44
+ The Space runs the maintained CPU/Rust rich-explanation demo. It does not execute
45
+ the CUDA research engine. The Python package is coming soon; no public package
46
+ release or general CUDA portability is claimed here. Current CUDA installation
47
+ qualification is limited to the tested Windows CPython3.10/sm75 environment.
48
+
49
+ ## Research still open
50
+
51
+ Release packaging and platform validation come first. Other opportunities include
52
+ sharing work across more taxonomies, workload-based CPU/CUDA selection, direct
53
+ StringZilla integration, other CUSMART methods, and state/compiler optimization
54
+ trained on separate representative development data. These are proposals.
55
+
56
+ Selected alternatives already tested include PFAC, Set Horspool, Wu-Manber,
57
+ SOG-style filtering, SBOM and two CUSMART-listed methods (BNDM/Quick Search).
58
+ Their tested adaptations did not replace the retained matcher. Phase81's two
59
+ methods produced exact outputs but failed timing stability and were much slower.
60
+ This does not reject all variants or all64 CUSMART algorithms.
CHANGELOG.md CHANGED
@@ -7,8 +7,8 @@ versioning and uses Python's pre-release version syntax.
7
 
8
  ### Added
9
 
10
- - The public `range_news` Python namespace and **RANGE** product name: Rapid Analysis
11
- of News for the General Economy.
12
  - An exact CPU/Rust path for rich predictions, compact list screening, and Arrow screening.
13
  - A conservative, offline general-economy taxonomy covering monetary policy, inflation,
14
  labor markets, growth, rates, credit, fiscal policy, trade, supply disruptions,
@@ -17,40 +17,44 @@ versioning and uses Python's pre-release version syntax.
17
  ranking, and editable taxonomy copies.
18
  - Wheel and source-distribution verification on Windows and Linux CI.
19
  - Runnable README examples with visible output.
20
- - A Space-ready Gradio demo under `app/` with highlighted evidence, evidence JSON,
21
- batch screening, downloadable outputs, named API endpoints, a multi-stage Docker
22
- build, and a deterministic offline export manifest.
23
- - Separate deployment exports for local Docker and free Gradio/ZeroGPU hosting. The
24
- hosted export vendors the runtime payload from an inspected CPython 3.10
25
- manylinux2014 wheel while keeping prediction on the compiled CPU/Rust path.
26
  - CI validation and a Linux container smoke test for the future Hugging Face Space.
27
 
28
- ### Changed
29
-
30
- - The project is now named **RANGE** throughout its distribution, Python namespace,
31
- native module, documentation, deployment identifiers, and demo.
32
- - The README now leads with the job-to-be-done, a working result, intended users,
33
- an architecture overview, a mechanism-level speed explanation, API selection
34
- guidance, and carefully qualified benchmark context.
35
- - The Space demo now separates article relevance from supported exposure, distinguishes
36
- unresolved evidence paths visually, and includes a verified private-deployment
37
- runbook.
38
- - The public package identity, examples, and documentation are no longer tied to the
39
- Attention2Alpha project or a particular leveraged-ETF portfolio. This presentation
 
 
 
 
40
  pass leaves the established default taxonomy semantics unchanged.
41
- - The package version is `0.1.0a1`, explicitly identifying a research alpha.
42
- - The hosted demo now rejects oversized or malformed requests before inference,
43
- shares one bounded execution queue across its public endpoints, and hides internal
44
- error details in production.
45
- - Evidence downloads now omit screened article text, retain a SHA-256 identifier and text
46
- lengths, live in Gradio's managed cache, and become eligible for best-effort cleanup
47
- after five minutes.
48
- - Hosted dependencies, Docker base images, and the Dockerfile frontend are pinned for
49
- reproducible deployment builds. Managed networks can supply a trusted build root as
50
- a temporary secret without disabling TLS verification.
51
 
52
  ### Not included
53
 
54
  - No GPU backend.
55
- - No public or production hosted service, model download, live-data integration,
56
- trading recommendation, release tag, or package-index publication.
 
7
 
8
  ### Added
9
 
10
+ - The public `range_news` Python namespace and **RANGE** product name: Rapid Analysis
11
+ of News for the General Economy.
12
  - An exact CPU/Rust path for rich predictions, compact list screening, and Arrow screening.
13
  - A conservative, offline general-economy taxonomy covering monetary policy, inflation,
14
  labor markets, growth, rates, credit, fiscal policy, trade, supply disruptions,
 
17
  ranking, and editable taxonomy copies.
18
  - Wheel and source-distribution verification on Windows and Linux CI.
19
  - Runnable README examples with visible output.
20
+ - A Space-ready Gradio demo under `app/` with highlighted evidence, evidence JSON,
21
+ batch screening, downloadable outputs, named API endpoints, a multi-stage Docker
22
+ build, and a deterministic offline export manifest.
23
+ - Separate deployment exports for local Docker and free Gradio/ZeroGPU hosting. The
24
+ hosted export vendors the runtime payload from an inspected CPython 3.10
25
+ manylinux2014 wheel while keeping prediction on the compiled CPU/Rust path.
26
  - CI validation and a Linux container smoke test for the future Hugging Face Space.
27
 
28
+ ### Changed
29
+
30
+ - Refreshed the standalone RANGE Space with qualified CPU/CUDA benchmark context,
31
+ a package-coming-soon notice, a research roadmap and a rebuilt maintained CPU
32
+ runtime for Linux. Exported documentation omits monorepo-specific branding.
33
+
34
+ - The project is now named **RANGE** throughout its distribution, Python namespace,
35
+ native module, documentation, deployment identifiers, and demo.
36
+ - The README now leads with the job-to-be-done, a working result, intended users,
37
+ an architecture overview, a mechanism-level speed explanation, API selection
38
+ guidance, and carefully qualified benchmark context.
39
+ - The Space demo now separates article relevance from supported exposure, distinguishes
40
+ unresolved evidence paths visually, and includes a verified private-deployment
41
+ runbook.
42
+ - The public package identity, examples, and documentation are no longer tied to the
43
+ parent research project project or a particular leveraged-ETF portfolio. This presentation
44
  pass leaves the established default taxonomy semantics unchanged.
45
+ - The package version is `0.1.0a1`, explicitly identifying a research alpha.
46
+ - The hosted demo now rejects oversized or malformed requests before inference,
47
+ shares one bounded execution queue across its public endpoints, and hides internal
48
+ error details in production.
49
+ - Evidence downloads now omit screened article text, retain a SHA-256 identifier and text
50
+ lengths, live in Gradio's managed cache, and become eligible for best-effort cleanup
51
+ after five minutes.
52
+ - Hosted dependencies, Docker base images, and the Dockerfile frontend are pinned for
53
+ reproducible deployment builds. Managed networks can supply a trusted build root as
54
+ a temporary secret without disabling TLS verification.
55
 
56
  ### Not included
57
 
58
  - No GPU backend.
59
+ - No public or production hosted service, model download, live-data integration,
60
+ trading recommendation, release tag, or package-index publication.
Cargo.toml CHANGED
@@ -7,13 +7,13 @@ publish = false
7
  license = "MIT"
8
  description = "Native engine for RANGE financial-news relevance screening"
9
 
10
- [lib]
11
- name = "_native"
12
- path = "rust/lib.rs"
13
- crate-type = ["cdylib"]
14
-
15
- [dependencies]
16
- aho-corasick = "1.1.5"
17
  arrow-array = "59"
18
  arrow-schema = "59"
19
  pyo3 = { version = "0.29.2", features = ["extension-module"] }
 
7
  license = "MIT"
8
  description = "Native engine for RANGE financial-news relevance screening"
9
 
10
+ [lib]
11
+ name = "_native"
12
+ path = "rust/lib.rs"
13
+ crate-type = ["cdylib"]
14
+
15
+ [dependencies]
16
+ aho-corasick = "1.1.5"
17
  arrow-array = "59"
18
  arrow-schema = "59"
19
  pyo3 = { version = "0.29.2", features = ["extension-module"] }
DEPLOYMENT.md CHANGED
@@ -1,202 +1,204 @@
1
- # Run RANGE locally or deploy it free on Hugging Face
2
-
3
- RANGE supports two deployments from the same Gradio app:
4
-
5
- | Route | Best for | Runtime |
6
- | --- | --- | --- |
7
- | Local Docker | Private local use and a portable container | Docker with the compiled CPU/Rust engine |
8
- | Private Hugging Face Space | A shareable demo without a paid plan | Gradio on the free ZeroGPU runtime, with the same CPU/Rust engine |
9
-
10
- The Hugging Face route does **not** turn RANGE into a GPU application. Its real
11
- `/analyze` and `/batch` endpoints remain on CPU and never request a GPU allocation.
12
- The app contains one unused `@spaces.GPU` function solely because the free ZeroGPU
13
- runtime requires a decorated function.
14
-
15
- Free ZeroGPU hosting is currently available to eligible personal accounts in good
16
- standing, with a verified email, an account older than 30 days, and no more than two
17
- hosted ZeroGPU Spaces. Hugging Face can change these limits. See the
18
- [ZeroGPU documentation](https://huggingface.co/docs/hub/spaces-zerogpu) before a new
19
- deployment.
20
-
21
- ## Prepare the repository
22
-
23
- Run these commands from `dataset/news_relevance/range_news` on the `codex` branch.
24
- The existing project environment is sufficient for validation:
25
-
26
- ```powershell
27
- conda activate attention2alpha
28
- python app/validate_space.py
29
- python app/export_space.py `
30
- --target docker `
31
- --output build/huggingface-space `
32
- --force
33
- ```
34
-
35
- The exporter writes only below `build/` and replaces a directory only when its
36
- manifest proves that it is an earlier RANGE export.
37
-
38
- ## Route A: run the Docker app locally
39
-
40
- ```powershell
41
- docker build `
42
- --tag range-news-space:0.1.0a1 `
43
- build/huggingface-space
44
-
45
- docker run --rm --name range-news-space `
46
- --publish 127.0.0.1:7860:7860 `
47
- range-news-space:0.1.0a1
48
- ```
49
-
50
- Open `http://127.0.0.1:7860`. Stop the container with `Ctrl+C`.
51
-
52
- The Docker bundle is self-contained source: it builds the Rust extension, installs
53
- the package and Gradio, and runs as an unprivileged user. It does not need a Hugging
54
- Face account or network access for inference.
55
-
56
- If downloads fail with an `UnknownIssuer` certificate error on a managed network,
57
- export that network's trusted root certificate as a PEM file and add
58
- `--secret "id=build_ca,src=C:\path\to\trusted-root.pem"` to the Docker build command.
59
- The certificate is available only while dependencies download and is not copied into
60
- the image. Do not disable TLS verification or add a trusted-host exception.
61
-
62
- ## Route B: build the free Hugging Face bundle
63
-
64
- Hugging Face does not compile the Rust extension for this route. Export the portable
65
- CPython 3.10 manylinux2014 wheel from the Docker build first:
66
-
67
- ```powershell
68
- docker build `
69
- --platform linux/amd64 `
70
- --target wheel-export `
71
- --output "type=local,dest=build/manylinux2014-wheel" `
72
- build/huggingface-space
73
-
74
- $Wheels = @(Get-ChildItem `
75
- -LiteralPath build/manylinux2014-wheel `
76
- -Filter "range_news-0.1.0a1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
77
-
78
- if ($Wheels.Count -ne 1) {
79
- throw "Expected exactly one portable RANGE x86_64 wheel."
80
- }
81
- $Wheel = $Wheels[0]
82
-
83
- python app/export_space.py `
84
- --target zerogpu `
85
- --wheel $Wheel.FullName `
86
- --output build/huggingface-zerogpu-space `
87
- --force
88
- ```
89
-
90
- The second export contains Gradio Space metadata, the app, package documentation and
91
- source, and a runtime copy extracted from the validated wheel. Gradio Spaces install
92
- `requirements.txt` before copying repository files, so a relative wheel reference
93
- cannot work during their build. Vendoring the inspected wheel payload keeps the build
94
- self-contained without publishing the package and makes the native extension available
95
- after the source copy. The requirements file installs RANGE's small Python
96
- dependencies; the platform supplies Gradio, `spaces`, and `huggingface_hub`.
97
-
98
- Before uploading, inspect `build/huggingface-zerogpu-space`. Do not add credentials,
99
- private dataset rows, licensed articles, caches, or machine-specific paths. A public
100
- Space exposes this bundle as a public repository, including the app source, bundled
101
- RANGE source and native runtime, package documentation, and default taxonomy. Review
102
- the uploaded revision history as well as the latest files before changing visibility.
103
-
104
- ## Create and upload the private Space
105
-
106
- Use a small deployment environment to keep Hub tooling separate from the research
107
- environment. Create it once; skip the first command when it already exists:
108
-
109
- ```powershell
110
- conda create --name range-news-hf-deploy --yes python=3.10
111
- conda activate range-news-hf-deploy
112
- python -m pip install `
113
- "huggingface_hub>=1.16,<2" `
114
- "gradio-client==2.6.1" `
115
- "pip-system-certs>=5,<6"
116
- hf auth login
117
- hf auth whoami --format json
118
- ```
119
-
120
- `pip-system-certs` lets the Python CLI use the Windows certificate store, including
121
- institution-managed certificate authorities.
122
-
123
- Create the private Gradio Space on the free ZeroGPU hardware and upload the prepared
124
- directory:
125
-
126
- ```powershell
127
- $SpaceId = "Brianferrell787/range-news"
128
-
129
- hf repos create $SpaceId `
130
- --type space `
131
- --sdk gradio `
132
- --flavor zero-a10g `
133
- --private
134
-
135
- hf upload $SpaceId build/huggingface-zerogpu-space . `
136
- --repo-type space `
137
- --commit-message "Deploy private RANGE 0.1.0a1 demo"
138
- ```
139
-
140
- Do not run the upload if creation fails. For an existing Space, first verify that its
141
- repository is private, its SDK is Gradio, and its hardware is ZeroGPU.
142
-
143
- ## Verify the live deployment
144
-
145
- ```powershell
146
- hf spaces wait $SpaceId --timeout 30m
147
- hf spaces logs $SpaceId --build -n 200
148
- hf spaces logs $SpaceId -n 200
149
- hf spaces info $SpaceId --expand sdk,runtime --format json
150
- ```
151
-
152
- Open `https://huggingface.co/spaces/Brianferrell787/range-news` and test all three
153
- single-article examples plus the batch example. The Space can sleep when idle, so the
154
- first request may include a cold start.
155
-
156
- Private API calls use the token already stored by `hf auth login`:
157
-
158
- ```python
159
- from gradio_client import Client
160
- from huggingface_hub import get_token
161
-
162
- client = Client("Brianferrell787/range-news", token=get_token())
163
- client.view_api()
164
- ```
165
-
166
- The generated `agents.md` points coding agents to the API schema, which lists the
167
- named `/analyze` and `/batch` endpoints.
168
-
169
- ## Privacy and performance boundaries
170
-
171
- - Do not add an HF token as a Space secret; RANGE needs no runtime credential.
172
- - Text submitted to the hosted demo is processed on Hugging Face infrastructure. Do
173
- not submit confidential or unlicensed material.
174
- - The interface and named API endpoints reject more than 100 rows, more than 20,000
175
- characters or 80,000 UTF-8 bytes in one article, and more than 200,000 characters
176
- or 800,000 UTF-8 bytes across a batch. Use the package's compact or Arrow API for
177
- large datasets. Batch rows must have exactly four columns. Publication timestamps
178
- are capped at 128 characters, while article IDs and sources are capped at 256
179
- characters each. Metadata across the batch is capped at 64,000 characters and
180
- 256,000 UTF-8 bytes, and combined article and metadata cell content is capped at
181
- 264,000 characters and 1,056,000 UTF-8 bytes. These caps do not count JSON framing
182
- or Gradio's transport envelope. A separate 4 MiB server limit covers the complete
183
- HTTP request body before JSON parsing.
184
- - Downloaded evidence identifies the screened article by text length and SHA-256 hash
185
- instead of copying the screened text. RANGE removes leading and trailing whitespace
186
- before screening. Download files become eligible for best-effort
187
- deletion after five minutes, cleanup runs once a minute, and a Space restart may
188
- remove them sooner. They are not durable or private storage.
189
- - Production responses hide internal error details. Use the Space logs to diagnose a
190
- failure; do not expose a traceback or exception text in the browser.
191
- - Package benchmark rates describe the native batch engine on the recorded hardware,
192
- not the latency or throughput of this shared two-core hosted demo.
193
- - A `relevant` result means the configured financial and economic policy accepted the
194
- article. It is not sentiment, materiality, a return forecast, or investment advice.
195
-
196
- ## Planned Python 3.12 migration
197
-
198
- The current alpha bundle uses CPython 3.10. Python 3.12 support is a planned follow-up,
199
- not part of this deployment. Because RANGE includes a compiled Rust extension, that
200
- migration must update and test the package metadata, build image, native wheel, Space
201
- runtime, exported bundle, and clean-install checks together. Do not change only the
202
- Space metadata or try to load the existing `cp310` wheel in Python 3.12.
 
 
 
1
+ # Run RANGE locally or deploy it free on Hugging Face
2
+
3
+ RANGE supports two deployments from the same Gradio app:
4
+
5
+ | Route | Best for | Runtime |
6
+ | --- | --- | --- |
7
+ | Local Docker | Private local use and a portable container | Docker with the compiled CPU/Rust engine |
8
+ | Private Hugging Face Space | A shareable demo without a paid plan | Gradio on the free ZeroGPU runtime, with the same CPU/Rust engine |
9
+
10
+ The Hugging Face route does **not** turn RANGE into a GPU application. Its real
11
+ `/analyze` and `/batch` endpoints remain on CPU and never request a GPU allocation.
12
+ The app contains one unused `@spaces.GPU` function solely because the free ZeroGPU
13
+ runtime requires a decorated function.
14
+
15
+ Free ZeroGPU hosting is currently available to eligible personal accounts in good
16
+ standing, with a verified email, an account older than 30 days, and no more than two
17
+ hosted ZeroGPU Spaces. Hugging Face can change these limits. See the
18
+ [ZeroGPU documentation](https://huggingface.co/docs/hub/spaces-zerogpu) before a new
19
+ deployment.
20
+
21
+ ## Prepare the repository
22
+
23
+ Run these commands from `dataset/news_relevance/range_news` on the `codex` branch.
24
+ The existing project environment is sufficient for validation:
25
+
26
+ ```powershell
27
+ python -m venv .venv
28
+ .\.venv\Scripts\Activate.ps1
29
+ python -m pip install '.[demo]'
30
+ python app/validate_space.py
31
+ python app/export_space.py `
32
+ --target docker `
33
+ --output build/huggingface-space `
34
+ --force
35
+ ```
36
+
37
+ The exporter writes only below `build/` and replaces a directory only when its
38
+ manifest proves that it is an earlier RANGE export.
39
+
40
+ ## Route A: run the Docker app locally
41
+
42
+ ```powershell
43
+ docker build `
44
+ --tag range-news-space:0.1.0a1 `
45
+ build/huggingface-space
46
+
47
+ docker run --rm --name range-news-space `
48
+ --publish 127.0.0.1:7860:7860 `
49
+ range-news-space:0.1.0a1
50
+ ```
51
+
52
+ Open `http://127.0.0.1:7860`. Stop the container with `Ctrl+C`.
53
+
54
+ The Docker bundle is self-contained source: it builds the Rust extension, installs
55
+ the package and Gradio, and runs as an unprivileged user. It does not need a Hugging
56
+ Face account or network access for inference.
57
+
58
+ If downloads fail with an `UnknownIssuer` certificate error on a managed network,
59
+ export that network's trusted root certificate as a PEM file and add
60
+ `--secret "id=build_ca,src=C:\path\to\trusted-root.pem"` to the Docker build command.
61
+ The certificate is available only while dependencies download and is not copied into
62
+ the image. Do not disable TLS verification or add a trusted-host exception.
63
+
64
+ ## Route B: build the free Hugging Face bundle
65
+
66
+ Hugging Face does not compile the Rust extension for this route. Export the portable
67
+ CPython 3.10 manylinux2014 wheel from the Docker build first:
68
+
69
+ ```powershell
70
+ docker build `
71
+ --platform linux/amd64 `
72
+ --target wheel-export `
73
+ --output "type=local,dest=build/manylinux2014-wheel" `
74
+ build/huggingface-space
75
+
76
+ $Wheels = @(Get-ChildItem `
77
+ -LiteralPath build/manylinux2014-wheel `
78
+ -Filter "range_news-0.1.0a1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
79
+
80
+ if ($Wheels.Count -ne 1) {
81
+ throw "Expected exactly one portable RANGE x86_64 wheel."
82
+ }
83
+ $Wheel = $Wheels[0]
84
+
85
+ python app/export_space.py `
86
+ --target zerogpu `
87
+ --wheel $Wheel.FullName `
88
+ --output build/huggingface-zerogpu-space `
89
+ --force
90
+ ```
91
+
92
+ The second export contains Gradio Space metadata, the app, package documentation and
93
+ source, and a runtime copy extracted from the validated wheel. Gradio Spaces install
94
+ `requirements.txt` before copying repository files, so a relative wheel reference
95
+ cannot work during their build. Vendoring the inspected wheel payload keeps the build
96
+ self-contained without publishing the package and makes the native extension available
97
+ after the source copy. The requirements file installs RANGE's small Python
98
+ dependencies; the platform supplies Gradio, `spaces`, and `huggingface_hub`.
99
+
100
+ Before uploading, inspect `build/huggingface-zerogpu-space`. Do not add credentials,
101
+ private dataset rows, licensed articles, caches, or machine-specific paths. A public
102
+ Space exposes this bundle as a public repository, including the app source, bundled
103
+ RANGE source and native runtime, package documentation, and default taxonomy. Review
104
+ the uploaded revision history as well as the latest files before changing visibility.
105
+
106
+ ## Create and upload the private Space
107
+
108
+ Use a small deployment environment to keep Hub tooling separate from the research
109
+ environment. Create it once; skip the first command when it already exists:
110
+
111
+ ```powershell
112
+ conda create --name range-news-hf-deploy --yes python=3.10
113
+ conda activate range-news-hf-deploy
114
+ python -m pip install `
115
+ "huggingface_hub>=1.16,<2" `
116
+ "gradio-client==2.6.1" `
117
+ "pip-system-certs>=5,<6"
118
+ hf auth login
119
+ hf auth whoami --format json
120
+ ```
121
+
122
+ `pip-system-certs` lets the Python CLI use the Windows certificate store, including
123
+ institution-managed certificate authorities.
124
+
125
+ Create the private Gradio Space on the free ZeroGPU hardware and upload the prepared
126
+ directory:
127
+
128
+ ```powershell
129
+ $SpaceId = "Brianferrell787/range-news"
130
+
131
+ hf repos create $SpaceId `
132
+ --type space `
133
+ --sdk gradio `
134
+ --flavor zero-a10g `
135
+ --private
136
+
137
+ hf upload $SpaceId build/huggingface-zerogpu-space . `
138
+ --repo-type space `
139
+ --commit-message "Deploy private RANGE 0.1.0a1 demo"
140
+ ```
141
+
142
+ Do not run the upload if creation fails. For an existing Space, first verify that its
143
+ repository is private, its SDK is Gradio, and its hardware is ZeroGPU.
144
+
145
+ ## Verify the live deployment
146
+
147
+ ```powershell
148
+ hf spaces wait $SpaceId --timeout 30m
149
+ hf spaces logs $SpaceId --build -n 200
150
+ hf spaces logs $SpaceId -n 200
151
+ hf spaces info $SpaceId --expand sdk,runtime --format json
152
+ ```
153
+
154
+ Open `https://huggingface.co/spaces/Brianferrell787/range-news` and test all three
155
+ single-article examples plus the batch example. The Space can sleep when idle, so the
156
+ first request may include a cold start.
157
+
158
+ Private API calls use the token already stored by `hf auth login`:
159
+
160
+ ```python
161
+ from gradio_client import Client
162
+ from huggingface_hub import get_token
163
+
164
+ client = Client("Brianferrell787/range-news", token=get_token())
165
+ client.view_api()
166
+ ```
167
+
168
+ The generated `agents.md` points coding agents to the API schema, which lists the
169
+ named `/analyze` and `/batch` endpoints.
170
+
171
+ ## Privacy and performance boundaries
172
+
173
+ - Do not add an HF token as a Space secret; RANGE needs no runtime credential.
174
+ - Text submitted to the hosted demo is processed on Hugging Face infrastructure. Do
175
+ not submit confidential or unlicensed material.
176
+ - The interface and named API endpoints reject more than 100 rows, more than 20,000
177
+ characters or 80,000 UTF-8 bytes in one article, and more than 200,000 characters
178
+ or 800,000 UTF-8 bytes across a batch. Use the package's compact or Arrow API for
179
+ large datasets. Batch rows must have exactly four columns. Publication timestamps
180
+ are capped at 128 characters, while article IDs and sources are capped at 256
181
+ characters each. Metadata across the batch is capped at 64,000 characters and
182
+ 256,000 UTF-8 bytes, and combined article and metadata cell content is capped at
183
+ 264,000 characters and 1,056,000 UTF-8 bytes. These caps do not count JSON framing
184
+ or Gradio's transport envelope. A separate 4 MiB server limit covers the complete
185
+ HTTP request body before JSON parsing.
186
+ - Downloaded evidence identifies the screened article by text length and SHA-256 hash
187
+ instead of copying the screened text. RANGE removes leading and trailing whitespace
188
+ before screening. Download files become eligible for best-effort
189
+ deletion after five minutes, cleanup runs once a minute, and a Space restart may
190
+ remove them sooner. They are not durable or private storage.
191
+ - Production responses hide internal error details. Use the Space logs to diagnose a
192
+ failure; do not expose a traceback or exception text in the browser.
193
+ - Package benchmark rates describe the native batch engine on the recorded hardware,
194
+ not the latency or throughput of this shared two-core hosted demo.
195
+ - A `relevant` result means the configured financial and economic policy accepted the
196
+ article. It is not sentiment, materiality, a return forecast, or investment advice.
197
+
198
+ ## Planned Python 3.12 migration
199
+
200
+ The current alpha bundle uses CPython 3.10. Python 3.12 support is a planned follow-up,
201
+ not part of this deployment. Because RANGE includes a compiled Rust extension, that
202
+ migration must update and test the package metadata, build image, native wheel, Space
203
+ runtime, exported bundle, and clean-install checks together. Do not change only the
204
+ Space metadata or try to load the existing `cp310` wheel in Python 3.12.
PACKAGE_README.md CHANGED
@@ -20,6 +20,12 @@ RANGE answers three practical questions for each article:
20
 
21
  It runs on CPU, makes no network request during inference, and does not depend on an embedding model, an external API, or an LLM. The `0.1.0a1` source and CI artifacts are prepared for review but are **not published to PyPI**.
22
 
 
 
 
 
 
 
23
  ## See it in 30 seconds
24
 
25
  Install from this checkout:
@@ -116,7 +122,16 @@ The result is low-overhead, predictable CPU screening with a rich Python path av
116
 
117
  ### Research benchmark context
118
 
119
- In our fastest recorded CPU test, RANGE screened **457,735 articles/second in a 500,000-article run** and **395,017 articles/second in a 10,000,000-article run**. We measured this with a specially optimized RANGE build on one Intel i7-10750H Windows laptop using 10 CPU workers. These are the fastest recorded results for the screening step alone. They do not include file loading or the rest of a data pipeline, and performance varies with hardware, article length, and screening rules.
 
 
 
 
 
 
 
 
 
120
 
121
  ## Choose the right API
122
 
 
20
 
21
  It runs on CPU, makes no network request during inference, and does not depend on an embedding model, an external API, or an LLM. The `0.1.0a1` source and CI artifacts are prepared for review but are **not published to PyPI**.
22
 
23
+ A separate optional GPU research companion now has local Windows CPython3.10/sm75
24
+ installation qualification for bulk Arrow screening. See the
25
+ [GPU research installation and results](https://huggingface.co/spaces/Brianferrell787/range-news/blob/main/BENCHMARKS.md)
26
+ for the local artifacts, measured performance and supported scope. The CPU
27
+ installation below remains the maintained package default.
28
+
29
  ## See it in 30 seconds
30
 
31
  Install from this checkout:
 
122
 
123
  ### Research benchmark context
124
 
125
+ **Python package coming soon.** The maintained CPU implementation recorded
126
+ **489,759 articles/s** in a qualified 10-million-article warm screening run.
127
+ CUDA research recorded a historical qualified **1,833,059 articles/s** on that
128
+ workload size; the newer retained CUDA installation separately measured
129
+ **958,155 articles/s**. These are different sessions, not a paired speedup.
130
+
131
+ Measurements used an Intel i7-10750H Windows laptop with 10 CPU workers and an
132
+ RTX 2070 Max-Q for CUDA. Timings cover compact screening, including CUDA
133
+ transfers/fallback/assembly, but exclude loading and initialization. They are
134
+ not hosted-demo performance. See the Space's BENCHMARKS.md for provenance.
135
 
136
  ## Choose the right API
137
 
README.md CHANGED
@@ -23,6 +23,8 @@ tags:
23
 
24
  **Rapid Analysis of News for the General Economy**
25
 
 
 
26
  RANGE checks timestamped financial news, identifies whether it matches the current
27
  financial and economic rules, shows any supported broad-market connection, and
28
  explains exactly which words and rules produced the result.
@@ -76,23 +78,48 @@ eligible for deletion after five minutes, and the app makes a best-effort cleanu
76
  once a minute. The host may remove it sooner when the Space restarts. Save a file
77
  promptly if you need it, and do not rely on the download link as an archive.
78
 
79
- ## Why it is fast
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- RANGE is a compiled screening engine rather than an open-ended language model:
82
 
83
- - Rust Aho-Corasick automata scan many configured phrases together.
84
- - Rayon distributes independent articles across CPU workers.
85
- - Worker-local buffers are reused instead of rebuilt for every row.
86
- - Common ASCII news text uses a specialized normalization path, while frozen Unicode
87
- tables keep fallback behavior deterministic.
88
- - Compact and Arrow package APIs represent the common no-match case sparsely.
 
 
 
 
89
 
90
- In our fastest recorded CPU test, RANGE screened **457,735 articles/second in a
91
- 500,000-article run** and **395,017 articles/second in a 10,000,000-article run**.
92
- We measured this with a specially optimized RANGE build on one Intel i7-10750H Windows
93
- laptop using 10 CPU workers. These are the fastest recorded results for the screening
94
- step alone. Loading data, running a complete pipeline, and using the hosted demo will
95
- be slower. Speed varies with hardware, article length, and screening rules.
96
 
97
  ## What the result means
98
 
 
23
 
24
  **Rapid Analysis of News for the General Economy**
25
 
26
+ **Python package coming soon. CPU today, optional CUDA for bulk screening.**
27
+
28
  RANGE checks timestamped financial news, identifies whether it matches the current
29
  financial and economic rules, shows any supported broad-market connection, and
30
  explains exactly which words and rules produced the result.
 
78
  once a minute. The host may remove it sooner when the Space restarts. Save a file
79
  promptly if you need it, and do not rely on the download link as an archive.
80
 
81
+ ## Measured speed. Clear boundaries.
82
+
83
+ | Implementation | Articles / second | Measurement |
84
+ | --- | ---: | --- |
85
+ | CUDA research record | **1,833,059** | Qualified warm screening of 10 million articles |
86
+ | Maintained CPU implementation | **489,759** | Qualified warm screening of 10 million articles |
87
+ | Newer retained CUDA installation | **958,155** | Separate local installed-package qualification |
88
+
89
+ Measured on an Intel i7-10750H Windows laptop with 10 CPU workers; CUDA used an
90
+ RTX 2070 Max-Q. These are separate experiments, not a same-run CPU/GPU speedup.
91
+ The CUDA peak belongs to an earlier qualified implementation; later sessions
92
+ varied substantially. Do not combine its peak with later percentage gains.
93
+
94
+ Screening timings exclude file loading, imports, initialization and verification.
95
+ CUDA timings include transfers, exact CPU fallback and result assembly.
96
+ The live demo uses a different CPU environment and rich explanations, so these
97
+ figures are not its throughput or a service guarantee.
98
+
99
+ [Read the benchmark record](https://huggingface.co/spaces/Brianferrell787/range-news/blob/main/BENCHMARKS.md)
100
+
101
+ ## Python package coming soon
102
+
103
+ We are preparing RANGE for straightforward Python installation, with CPU support
104
+ and an optional CUDA backend. Public package release, clean installation across
105
+ supported platforms and broader GPU compatibility are still ahead.
106
+ The current Space lets you explore the engine today.
107
 
108
+ ## What we want to try next
109
 
110
+ - **Finish the release:** clean installs, supported platforms, documentation and
111
+ validation of the exact wheels people will use.
112
+ - **Share work across taxonomies:** build on successful two-taxonomy experiments,
113
+ then extend packaging and memory scheduling for more rule sets.
114
+ - **Choose CPU or CUDA by workload:** measure small batches, large batches and
115
+ routing overhead before introducing automatic selection.
116
+ - **Target remaining matching ideas:** direct StringZilla integration and other
117
+ CUSMART methods remain candidates, not demonstrated improvements.
118
+ - **Train optimizations on separate data:** investigate corpus-trained automata
119
+ and fresh compiler profiles using representative development articles.
120
 
121
+ Recent alternatives did not beat the retained matcher. We keep negative results
122
+ and require exact outputs before considering a speed improvement.
 
 
 
 
123
 
124
  ## What the result means
125
 
SPACE_BUNDLE_MANIFEST.json CHANGED
@@ -1,21 +1,21 @@
1
  {
2
  "files": {
3
  ".gitattributes": "30af955bc8b9589ff13954efae1020fb4fcbd3e2114ce2530c1e19a198331490",
4
- "CHANGELOG.md": "04441fe7ba8cc83498bf014cda1b29bcb6f7c148cfbbe3452dc7a9dea9af85ef",
 
5
  "CITATION.cff": "acf3bf2c922c2afd0f8cc336024f525d9334c4b94b83efaa96dc36a1df5c6198",
6
  "Cargo.lock": "64eaf82038b21074bf9d6b30e00bf05536958b08463dbe4a4c9a88a86e28a2d8",
7
- "Cargo.toml": "f58a8af353ccba4309f9ddddc3e138634093ab2545bfef5214b6a08815d37d59",
8
- "DEPLOYMENT.md": "2d005b4059d06bc5e722d67acfe33b97a1b631ac9537e69721e0920b26ba2c0d",
9
  "LICENSE": "98564786fde1cf443f2b761bc9c3b329dcac3528b3c383a66b25101ee9670585",
10
- "PACKAGE_README.md": "e6fe56bcb6c0cf24a6c7b3859527ab043c341a7c40b5eb3f919a9bccab33382c",
11
- "README.md": "423104b0ef72e4e605c4facf10681f0741d43eb8a70ea6932fc945e84d474048",
12
- "app.py": "138adcabfb167a19a499d3c0f80bfa63b565c61c3fac1e1d31c04f12bcc5e622",
13
- "docs/attention2alpha-compatibility-review.md": "c6e28e67b5528c5683efa6cb98603a9f46e42430c96bee6da4a3a5af9bf94c13",
14
- "docs/performance.md": "9d4c99009993527e49f2ce3bb38d3156f1b7e1425723f0093c7873c544261b24",
15
- "docs/readme-research.md": "f69255682495de999875c91a8c4dee2e59e1b185877920cf4c85f0ccd49053f3",
16
- "docs/release.md": "ce56bb5c7f456a710c90bed63ce2dfe90632d1b0208cc16d06e0b37ad0470bbe",
17
- "docs/taxonomy.md": "899b86c816ead8cd9bae9b58f96d0d21a1439cc778e6b61e7c17644a64dc1abf",
18
- "docs/technical-signoff.md": "4c1efa828b2460ff5092f07df30319ca7775ec3118ccdfc573aaa5b2980928c5",
19
  "examples.py": "94da2be9d8e523a54b6aae6fbd365c5e3456e836b120e4a4a0bd78acb5969d10",
20
  "examples/app.py": "53c76cc3834e73435483db6fbf89d8125a0f7aa590269d880cffdaa8e40097f5",
21
  "examples/benchmark_hf.py": "5df4e9d756a6447eb5f462ac7b72aaf2a8c1d92558a58620e5abb51e97e62bca",
@@ -23,12 +23,12 @@
23
  "examples/quickstart.py": "19bdd779ec202957c72c2b49e7bf778a13ea1b94d0a52f1bba5521810eb71626",
24
  "pyproject.toml": "d40182524a367d9ad2d08892450b4bcecfb2133c92bf45867c4e8f4a05910300",
25
  "requirements.txt": "94f5ee00edc14a89e5bf6f5fcb90261cde4a57dc3ebc20e0639e68ec504cc220",
26
- "rust/core.rs": "9ec890063a3485ab90f9f517a0e8c17b4ba52d66d937f2b9f6cf521889590f1a",
27
  "rust/generated/python_whitespace.bin": "6c314375b6c075589b515139e50068d72c0cbd7daf4246e555a9dcfc9748ecc9",
28
  "rust/generated/unicode_index.bin": "fe152ac184aa77c40d0f040ba725140b17d845bae9fae255b2b3d3c43bb79c8f",
29
  "rust/generated/unicode_meta.txt": "d7b7f5d807b712597a04e8cf1394c1734962c4e042057cc7fca85f5173919d38",
30
  "rust/generated/unicode_pool.bin": "109a8da1563408ad1da82919a5dfa1ab9fa18870f0f4d22e2818857b5a4fc30f",
31
- "rust/lib.rs": "f6a2205062c2272b13b925deb1883ff1fab10aa5e40a46695bfe96a5626b5753",
32
  "src/range_news/__init__.py": "816ba0ee72545cc03d6e5183d46530acd026e67f4b38d6d972a1abd520eb899d",
33
  "src/range_news/_core/__init__.py": "7e6c8615e347bca422f9b1e6e10160ac277c346956adf9e86a8a08419f879e96",
34
  "src/range_news/_core/accelerated/__init__.py": "ed04df1ca0da8254d9558201c6172969626aced74864842c5a17068ce87a6999",
@@ -59,7 +59,7 @@
59
  "vendor/range_news/_core/configs/default.yml": "6e443b14b9e7c4524e8dbdd3035726627345d01d02cc21de7a53a133dea5802c",
60
  "vendor/range_news/_core/indicator.py": "488178693d90e7a7f329b40309eb4583908d5a990cd0b293b272c5fbb192f3a4",
61
  "vendor/range_news/_core/models.py": "e152077ecd18c31eed76b150e234934180a5ffad061d2bf26e35229160cf2733",
62
- "vendor/range_news/_native.cpython-310-x86_64-linux-gnu.so": "45e05c00ab729ead671436ed8bd392c016dfa1e44ef42fdfade758902b1519d1",
63
  "vendor/range_news/_taxonomy_impl.py": "db6802ef00114e33bbb47b2d71886f037e151f397ae82258d1a6760f43bdfed1",
64
  "vendor/range_news/api.py": "1d165a3211fad9a9de1c274320dc21e787303fcd1a37599e399ba82881d4886d",
65
  "vendor/range_news/backends.py": "7a25879d21ed9992aeecc9dc5f4093de00ae68c117646b132140d663987829ca",
@@ -73,7 +73,7 @@
73
  "runtime_package": "vendor/range_news",
74
  "sdk": "gradio",
75
  "source_wheel": "range_news-0.1.0a1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
76
- "source_wheel_sha256": "c72a0f0d174e028f0a3db338c13e1dbf3e905ff2f67bacc0b3966a2386c2681b",
77
  "target": "zerogpu",
78
  "version": "0.1.0a1"
79
  }
 
1
  {
2
  "files": {
3
  ".gitattributes": "30af955bc8b9589ff13954efae1020fb4fcbd3e2114ce2530c1e19a198331490",
4
+ "BENCHMARKS.md": "7035a05f986a6c28a91262d34e499ae3d2612931673a82d7ad778f1bad9c15da",
5
+ "CHANGELOG.md": "63ef44e35ad6fbc3184d93141e0494eeb18a0289c5adda22ab1b0f8abebaa35b",
6
  "CITATION.cff": "acf3bf2c922c2afd0f8cc336024f525d9334c4b94b83efaa96dc36a1df5c6198",
7
  "Cargo.lock": "64eaf82038b21074bf9d6b30e00bf05536958b08463dbe4a4c9a88a86e28a2d8",
8
+ "Cargo.toml": "962f3ed0cdcf18aafd4b83f3073fb087934472a8f718f87a37885318804ba665",
9
+ "DEPLOYMENT.md": "b370e9b8c3f2b3118b2298a21dda028b1aad9b5b588cf00235ffbaedd4bd6dde",
10
  "LICENSE": "98564786fde1cf443f2b761bc9c3b329dcac3528b3c383a66b25101ee9670585",
11
+ "PACKAGE_README.md": "0652c0f42333f96de7a7f35c5fa15d2a20a496df7c0f5cfa78ab63e2ebffbdb0",
12
+ "README.md": "0b2a476a20c8502e0274c01c643f6d5152aeafe92a2f3f70a4b16a80b280bd4a",
13
+ "app.py": "d666b475abadbb723baa1cc5f1ef5ca981e682f4839e448547febfac1a614845",
14
+ "docs/performance.md": "b25202cb154bb2bcdef48c14af8cedc821a2dd298fe32d53608d4dcc0f1f7fbb",
15
+ "docs/readme-research.md": "2f8e2f3e7ed2dc386995ae6c9abca89b7e910209716ad31102f633a9a2bb7fe7",
16
+ "docs/release.md": "e449a9001d97b20ea1f9b505d9982eec5b97ec068cae3e0574a498949ac0363e",
17
+ "docs/taxonomy.md": "c1a33b8efd93a87d3ccfc413a879f11a088290ec9859a5c4254cf88d86e21998",
18
+ "docs/technical-signoff.md": "ba912d29e0ba23d1c8d42ad5616663b811b610a98af9941ed770de1a89ea88a8",
 
19
  "examples.py": "94da2be9d8e523a54b6aae6fbd365c5e3456e836b120e4a4a0bd78acb5969d10",
20
  "examples/app.py": "53c76cc3834e73435483db6fbf89d8125a0f7aa590269d880cffdaa8e40097f5",
21
  "examples/benchmark_hf.py": "5df4e9d756a6447eb5f462ac7b72aaf2a8c1d92558a58620e5abb51e97e62bca",
 
23
  "examples/quickstart.py": "19bdd779ec202957c72c2b49e7bf778a13ea1b94d0a52f1bba5521810eb71626",
24
  "pyproject.toml": "d40182524a367d9ad2d08892450b4bcecfb2133c92bf45867c4e8f4a05910300",
25
  "requirements.txt": "94f5ee00edc14a89e5bf6f5fcb90261cde4a57dc3ebc20e0639e68ec504cc220",
26
+ "rust/core.rs": "db6d48765f3a551bf81bc0fd6b471306a587f75e265c35f2f277b3115ffba172",
27
  "rust/generated/python_whitespace.bin": "6c314375b6c075589b515139e50068d72c0cbd7daf4246e555a9dcfc9748ecc9",
28
  "rust/generated/unicode_index.bin": "fe152ac184aa77c40d0f040ba725140b17d845bae9fae255b2b3d3c43bb79c8f",
29
  "rust/generated/unicode_meta.txt": "d7b7f5d807b712597a04e8cf1394c1734962c4e042057cc7fca85f5173919d38",
30
  "rust/generated/unicode_pool.bin": "109a8da1563408ad1da82919a5dfa1ab9fa18870f0f4d22e2818857b5a4fc30f",
31
+ "rust/lib.rs": "6d099c6cbc33b85bd7633a790f2b8d3a0296d74d604fbabf14806634e538fb0a",
32
  "src/range_news/__init__.py": "816ba0ee72545cc03d6e5183d46530acd026e67f4b38d6d972a1abd520eb899d",
33
  "src/range_news/_core/__init__.py": "7e6c8615e347bca422f9b1e6e10160ac277c346956adf9e86a8a08419f879e96",
34
  "src/range_news/_core/accelerated/__init__.py": "ed04df1ca0da8254d9558201c6172969626aced74864842c5a17068ce87a6999",
 
59
  "vendor/range_news/_core/configs/default.yml": "6e443b14b9e7c4524e8dbdd3035726627345d01d02cc21de7a53a133dea5802c",
60
  "vendor/range_news/_core/indicator.py": "488178693d90e7a7f329b40309eb4583908d5a990cd0b293b272c5fbb192f3a4",
61
  "vendor/range_news/_core/models.py": "e152077ecd18c31eed76b150e234934180a5ffad061d2bf26e35229160cf2733",
62
+ "vendor/range_news/_native.cpython-310-x86_64-linux-gnu.so": "e05b832a8b33169dd988b4bc88a36bfb1f5b40f70676a09ecda727961d123c9c",
63
  "vendor/range_news/_taxonomy_impl.py": "db6802ef00114e33bbb47b2d71886f037e151f397ae82258d1a6760f43bdfed1",
64
  "vendor/range_news/api.py": "1d165a3211fad9a9de1c274320dc21e787303fcd1a37599e399ba82881d4886d",
65
  "vendor/range_news/backends.py": "7a25879d21ed9992aeecc9dc5f4093de00ae68c117646b132140d663987829ca",
 
73
  "runtime_package": "vendor/range_news",
74
  "sdk": "gradio",
75
  "source_wheel": "range_news-0.1.0a1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
76
+ "source_wheel_sha256": "48581327a16ef46a6da9dbf7f00a296b7037dd7ba1b3af3e9108a945729174b7",
77
  "target": "zerogpu",
78
  "version": "0.1.0a1"
79
  }
app.py CHANGED
@@ -1,14 +1,14 @@
1
- """Space-ready Gradio demo for explainable financial-news relevance screening."""
2
 
3
  from __future__ import annotations
4
 
5
- import dataclasses
6
- import hashlib
7
- import html
8
- import json
9
- import os
10
- import sys
11
- import uuid
12
  from collections.abc import Mapping, Sequence
13
  from datetime import datetime
14
  from enum import Enum
@@ -16,21 +16,21 @@ from functools import lru_cache
16
  from pathlib import Path
17
  from typing import Any
18
 
19
- import gradio as gr
20
- from starlette.middleware import Middleware
21
- from starlette.middleware.body_limit import RequestBodyLimitMiddleware
22
- from gradio.utils import get_upload_folder
23
-
24
- VENDORED_PACKAGE_ROOT = Path(__file__).resolve().parent / "vendor"
25
- if VENDORED_PACKAGE_ROOT.is_dir():
26
- sys.path.insert(0, str(VENDORED_PACKAGE_ROOT))
27
-
28
- try:
29
- import spaces
30
  except ModuleNotFoundError: # Local installs and the Docker image do not need it.
31
  spaces = None
32
 
33
- from range_news import RelevanceEngine, __version__
34
 
35
  from examples import ARTICLE_EXAMPLES, BATCH_ROWS, SINGLE_EXAMPLES
36
 
@@ -46,23 +46,23 @@ if spaces is not None:
46
 
47
  GRADIO_MAJOR = int(gr.__version__.split(".", 1)[0])
48
  THEME = gr.themes.Soft(primary_hue="teal", neutral_hue="slate")
49
- MAX_BATCH_ROWS = 100
50
- EXPECTED_BATCH_COLUMNS = 4
51
- MAX_ARTICLE_CHARS = 20_000
52
- MAX_ARTICLE_BYTES = 80_000
53
- MAX_TIMESTAMP_CHARS = 128
54
- MAX_TIMESTAMP_BYTES = 512
55
- MAX_ARTICLE_ID_CHARS = 256
56
- MAX_ARTICLE_ID_BYTES = 1_024
57
- MAX_SOURCE_CHARS = 256
58
- MAX_SOURCE_BYTES = 1_024
59
- MAX_BATCH_CHARS = 200_000
60
- MAX_BATCH_BYTES = 800_000
61
- MAX_BATCH_METADATA_CHARS = 64_000
62
- MAX_BATCH_METADATA_BYTES = 256_000
63
- MAX_BATCH_CONTENT_CHARS = MAX_BATCH_CHARS + MAX_BATCH_METADATA_CHARS
64
- MAX_BATCH_CONTENT_BYTES = MAX_BATCH_BYTES + MAX_BATCH_METADATA_BYTES
65
- MAX_HTTP_REQUEST_BYTES = 4 * 1024 * 1024
66
  EVIDENCE_HEADERS = [
67
  "evidence path",
68
  "matched text",
@@ -81,98 +81,103 @@ BATCH_HEADERS = [
81
  "why",
82
  ]
83
 
84
- CSS = """
85
- html, body {width: 100%; margin: 0 !important; overflow-x: clip;}
86
- .gradio-container {
87
- width: 100% !important;
88
- max-width: none !important;
89
- margin: 0 !important;
90
- padding: 0 0 2rem !important;
91
- }
92
- .hero-host {
93
- width: 100vw !important;
94
- max-width: none !important;
95
- margin-inline: calc(50% - 50vw) !important;
96
- padding: 0 !important;
97
- border: 0 !important;
98
- box-sizing: border-box !important;
99
- }
100
- .hero {
101
- position: relative;
102
- width: 100%;
103
- margin: 0;
104
- overflow: hidden;
105
- box-sizing: border-box;
106
- border-block: 1px solid var(--border-color-primary);
107
- border-inline: 0;
108
- background:
109
- radial-gradient(circle at 92% 15%, rgba(13, 148, 136, 0.18), transparent 34%),
110
- radial-gradient(circle at 8% 88%, rgba(37, 99, 235, 0.12), transparent 30%),
111
- linear-gradient(145deg, var(--background-fill-primary), var(--background-fill-secondary));
112
- box-shadow: 0 18px 48px rgba(15, 23, 42, 0.08);
113
- }
114
- .hero-inner {
115
- width: min(calc(100% - 2rem), 1180px);
116
- margin-inline: auto;
117
- padding: clamp(2.2rem, 5vw, 4.5rem) clamp(1.25rem, 4vw, 3.2rem);
118
- box-sizing: border-box;
119
- text-align: center;
120
- }
121
- .hero-eyebrow,
122
- .hero h1,
123
- .hero-expansion,
124
- .hero-copy,
125
- .benchmark-note {
126
- width: 100%;
127
- text-align: center !important;
128
- }
129
- .hero-eyebrow {
130
- margin: 0 0 0.8rem;
131
- color: #0f766e;
 
132
  font-size: 0.78rem;
133
  font-weight: 750;
134
  letter-spacing: 0.12em;
135
  text-transform: uppercase;
136
  }
137
- .hero h1 {
138
- max-width: 960px;
139
- margin: 0 auto;
140
- font-size: clamp(3.4rem, 10vw, 7rem);
141
- line-height: 0.92;
142
- letter-spacing: -0.065em;
143
- }
144
- .hero-expansion {
145
- max-width: 960px;
146
- margin: 1rem auto 0;
147
- font-size: clamp(1.08rem, 2.5vw, 1.55rem);
148
- font-weight: 720;
149
- line-height: 1.35;
150
- }
151
- .hero-copy {
152
- max-width: 900px;
153
- margin: 1rem auto 0;
154
- font-size: clamp(1rem, 1.5vw, 1.16rem);
155
- line-height: 1.65;
156
- opacity: 0.88;
157
- }
158
- .speed-grid {
159
- display: grid;
160
- grid-template-columns: repeat(3, minmax(0, 1fr));
161
- gap: 0.75rem;
162
- max-width: 900px;
163
- margin: 1.6rem auto 0;
164
- }
165
- .speed-stat {
166
- padding: 0.9rem 1rem;
167
- border: 1px solid var(--border-color-primary);
168
- border-radius: 14px;
169
- background: var(--background-fill-primary);
170
- text-align: center;
171
- }
172
- .speed-stat strong {display: block; font-size: clamp(1.2rem, 2.6vw, 1.75rem); line-height: 1.15;}
173
- .speed-stat span {display: block; margin-top: 0.25rem; font-size: 0.8rem; line-height: 1.35; opacity: 0.72;}
174
- .benchmark-note {max-width: 900px; margin: 0.75rem auto 0; font-size: 0.86rem; line-height: 1.5; opacity: 0.72;}
175
- .trust-row {display: flex; justify-content: center; flex-wrap: wrap; gap: 0.55rem; margin-top: 1.25rem;}
 
 
 
 
176
  .trust-pill {
177
  padding: 0.36rem 0.68rem;
178
  border: 1px solid var(--border-color-primary);
@@ -181,26 +186,26 @@ html, body {width: 100%; margin: 0 !important; overflow-x: clip;}
181
  font-size: 0.82rem;
182
  font-weight: 650;
183
  }
184
- .value-grid {
185
- display: grid;
186
- grid-template-columns: repeat(3, minmax(0, 1fr));
187
- gap: 0.8rem;
188
- width: min(calc(100% - 2rem), 1120px);
189
- max-width: 1120px;
190
- margin: 1.15rem auto 1.35rem;
191
- }
192
- .content-shell {
193
- width: min(calc(100% - 2rem), 1120px) !important;
194
- max-width: 1120px !important;
195
- margin-inline: auto !important;
196
- align-self: center !important;
197
- }
198
- .value-card {
199
  padding: 1rem 1.05rem;
200
  border: 1px solid var(--border-color-primary);
201
- border-radius: 14px;
202
- background: var(--background-fill-primary);
203
- text-align: center;
204
  }
205
  .value-card strong {display: block; margin-bottom: 0.25rem; font-size: 0.95rem;}
206
  .value-card span {font-size: 0.88rem; line-height: 1.45; opacity: 0.78;}
@@ -266,107 +271,135 @@ mark.evidence-observed {background: #bfdbfe;}
266
  font-size: 0.88rem;
267
  line-height: 1.5;
268
  }
269
- @media (max-width: 760px) {
270
- .speed-grid, .value-grid, .result-grid {grid-template-columns: 1fr;}
271
- .hero-inner {padding: 1.8rem 1.25rem 2rem;}
272
- .speed-stat {text-align: center;}
273
- .status-card {min-height: auto;}
274
- }
275
  """
276
 
277
  ABOUT = """
278
- ## What RANGE does
279
-
280
- RANGE is a **deterministic screening layer** for timestamped economic and financial
281
- news. It answers two related questions separately:
282
-
283
- 1. Did the article pass the configured financial and economic relevance screen?
284
- 2. Did that evidence support a configured broad-market exposure?
285
-
286
- It also preserves the exact text, offsets, rules, time-valid identities, and
287
- relationships behind the result. The screening logic is deterministic and versioned.
288
-
289
- ### From article to result
290
-
291
- ```text
292
- article + publication time
293
- → literal phrase and identifier matches
294
- → context, identity, and effective-date checks
295
- → accepted / review / rejected evidence paths
296
- → article relevance + supported broad-market exposure
297
- ```
298
-
299
- ### Why it is fast
300
-
301
- - **Compile once, scan together.** Rust Aho-Corasick automata check many configured
302
- phrases in one pass instead of running a Python loop for every rule.
303
- - **Use the CPU cores.** Rayon processes independent articles in parallel.
304
- - **Avoid repeated allocation.** Worker-local scratch buffers are reused across rows.
305
- - **Optimize common news text.** ASCII input takes a specialized path while frozen
306
- Unicode tables preserve deterministic fallback behavior.
307
- - **Keep empty results cheap.** Compact list and Arrow interfaces return sparse facts
308
- rather than building a rich Python object for every no-match row.
309
-
310
- In our fastest recorded CPU test, RANGE screened **457,735 articles/second in a
311
- 500,000-article run** and **395,017 articles/second in a 10,000,000-article run**.
312
- We measured this with a specially optimized RANGE build on one Intel i7-10750H Windows
313
- laptop using 10 CPU workers. These are the fastest recorded results for the screening
314
- step alone. Loading data, running a complete pipeline, and using the hosted demo will
315
- be slower. Speed varies with hardware, article length, and screening rules.
316
-
317
- ### What it is not
318
-
319
- This demo does not infer sentiment, forecast returns, recommend a position, or use an
320
- LLM. A `relevant` result means the article passed the bundled financial and economic
321
- relevance policy; evidence-path state and supported exposure are reported separately.
322
- Neither result means the news is bullish, bearish, material, complete, or predictive
323
- of returns.
324
-
325
- The hosted demo is intended for short evaluation inputs. The downloadable package APIs
326
- are the appropriate interface for large datasets.
327
  """
328
 
329
- HERO_HTML = """
330
- <section class="hero">
331
- <div class="hero-inner">
332
- <p class="hero-eyebrow">Fast · consistent · shows its work</p>
333
- <h1>RANGE</h1>
334
- <p class="hero-expansion">Rapid Analysis of News for the General Economy</p>
335
- <p class="hero-copy">
336
- RANGE quickly checks financial news for economic and broad-market relevance.
337
- It tells you whether an article matches, which broad market or index it relates to, and
338
- the exact words and rule behind the result—without using an LLM or sending the
339
- article to an external API.
340
- </p>
341
- <div class="speed-grid" aria-label="Recorded CPU benchmark highlights">
342
- <div class="speed-stat"><strong>457,735</strong><span>articles screened per second in a 500,000-article test</span></div>
343
- <div class="speed-stat"><strong>395,017</strong><span>articles screened per second in a 10-million-article test</span></div>
344
- <div class="speed-stat"><strong>Clear reasons</strong><span>Every result includes the matched words and rule</span></div>
345
- </div>
346
- <p class="benchmark-note">Measured with a specially optimized RANGE build on one Intel i7-10750H Windows laptop using 10 CPU workers. These are the fastest recorded results for the screening step alone. Loading data, running a complete pipeline, and using the hosted demo will be slower. Speed varies with hardware, article length, and screening rules.</p>
347
- <div class="trust-row" aria-label="Product characteristics">
348
- <span class="trust-pill">Rust-accelerated on CPU</span>
349
- <span class="trust-pill">Screening rules you can customize</span>
350
- <span class="trust-pill">Exact matched text and rules</span>
351
- </div>
352
- </div>
353
- </section>
354
- <section class="value-grid" aria-label="What RANGE returns">
355
- <div class="value-card"><strong>Article relevance</strong><span>Whether the article matches the financial and economic topics in the current rules.</span></div>
356
- <div class="value-card"><strong>Related market</strong><span>Broad market, S&amp;P 500, Nasdaq-100, or Russell 2000—when the article supports the connection.</span></div>
357
- <div class="value-card"><strong>Evidence trail</strong><span>Matched words → rule → name valid on the publication date → market connection.</span></div>
358
- </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  """
360
 
361
  INITIAL_STATUS_HTML = """
362
  <div class="status-card status-empty" role="status" aria-live="polite">
363
  <span class="status-label">Screening result</span>
364
  <h3>Your result will appear here</h3>
365
- <p>RANGE reports article relevance and supported broad-market exposure as two
366
  separate outputs.</p>
367
  <div class="result-grid">
368
  <div class="result-cell"><strong>Article relevance</strong><span>Relevant · No configured match</span></div>
369
- <div class="result-cell"><strong>Related market</strong><span>Shown only when the article supports a connection</span></div>
370
  </div>
371
  <p class="boundary-note">This is a relevance screen—not sentiment, materiality, a
372
  return forecast, or investment advice.</p>
@@ -374,90 +407,90 @@ INITIAL_STATUS_HTML = """
374
  """
375
 
376
 
377
- def _text_size(value: str, *, label: str) -> tuple[int, int]:
378
- """Return character and UTF-8 sizes with a public-safe Unicode error."""
379
-
380
- characters = len(value)
381
- try:
382
- utf8_bytes = len(value.encode("utf-8"))
383
- except UnicodeEncodeError as exc:
384
- raise gr.Error(f"{label} contains invalid Unicode text.") from exc
385
- return characters, utf8_bytes
386
-
387
-
388
- def _validate_article_size(text: str, *, label: str = "Article") -> tuple[int, int]:
389
- """Validate public-request bounds before the engine sees article text."""
390
-
391
- characters, utf8_bytes = _text_size(text, label=label)
392
- if characters > MAX_ARTICLE_CHARS:
393
- raise gr.Error(f"{label} exceeds the {MAX_ARTICLE_CHARS:,}-character limit.")
394
- if utf8_bytes > MAX_ARTICLE_BYTES:
395
- raise gr.Error(f"{label} exceeds the {MAX_ARTICLE_BYTES:,}-byte UTF-8 limit.")
396
- return characters, utf8_bytes
397
-
398
-
399
- def _bounded_metadata(
400
- value: Any,
401
- *,
402
- label: str,
403
- max_characters: int,
404
- max_bytes: int,
405
- required: bool = False,
406
- ) -> tuple[str, int, int]:
407
- """Normalize one metadata value after enforcing its raw request bounds."""
408
-
409
- if value is None:
410
- raw_value = ""
411
- elif isinstance(value, str):
412
- raw_value = value
413
- else:
414
- raise gr.Error(f"{label} must be text.")
415
-
416
- characters, utf8_bytes = _text_size(raw_value, label=label)
417
- if characters > max_characters:
418
- raise gr.Error(f"{label} exceeds the {max_characters:,}-character limit.")
419
- if utf8_bytes > max_bytes:
420
- raise gr.Error(f"{label} exceeds the {max_bytes:,}-byte UTF-8 limit.")
421
-
422
- cleaned = raw_value.strip()
423
- if required and not cleaned:
424
- raise gr.Error(f"Enter {label.lower()}.")
425
- return cleaned, characters, utf8_bytes
426
-
427
-
428
- def _batch_cell_text(value: Any, *, label: str) -> str:
429
- """Accept ordinary table scalars without stringifying nested objects."""
430
-
431
- if value is None:
432
- return ""
433
- if isinstance(value, str):
434
- return value
435
- if isinstance(value, (bool, int, float)):
436
- return str(value)
437
- raise gr.Error(f"{label} must be plain text or a number.")
438
-
439
-
440
- def _validate_batch_dimensions(row_count: int, column_count: int) -> None:
441
- """Reject oversized or malformed tables before their cells are copied."""
442
-
443
- if row_count > MAX_BATCH_ROWS:
444
- raise gr.Error(
445
- f"The interactive demo is capped at {MAX_BATCH_ROWS} rows per request."
446
- )
447
- if column_count != EXPECTED_BATCH_COLUMNS:
448
- raise gr.Error(
449
- "Batch input must have exactly four columns: "
450
- "article_id, timestamp, source, and text."
451
- )
452
-
453
-
454
- def _local_error_details_enabled() -> bool:
455
- """Allow detailed tracebacks only through an explicit non-Space opt-in."""
456
-
457
- requested = os.environ.get("RANGE_LOCAL_SHOW_ERROR", "").strip().casefold()
458
- if requested not in {"1", "true", "yes", "on"}:
459
- return False
460
- return not any(os.environ.get(name) for name in ("SPACE_ID", "SPACE_HOST"))
461
 
462
 
463
  @lru_cache(maxsize=1)
@@ -495,10 +528,10 @@ def _jsonable(value: Any) -> Any:
495
  return value
496
 
497
 
498
- def _download_file(payload: Any, *, suffix: str) -> str:
499
- cache_root = Path(get_upload_folder())
500
- cache_root.mkdir(parents=True, exist_ok=True)
501
- path = cache_root / f"relevance-{uuid.uuid4().hex}{suffix}"
502
  if suffix == ".jsonl":
503
  lines = [json.dumps(row, ensure_ascii=False, sort_keys=True) for row in payload]
504
  path.write_text("\n".join(lines) + "\n", encoding="utf-8")
@@ -695,44 +728,44 @@ def _status_card(prediction) -> str:
695
  '<div class="result-grid">'
696
  '<div class="result-cell"><strong>Article relevance</strong>'
697
  f"<span>{html.escape(badge)}</span></div>"
698
- '<div class="result-cell"><strong>Related market</strong>'
699
  f"<span>{html.escape(exposure_value)}</span></div>"
700
  "</div>"
701
- f"<p><strong>Why RANGE returned this:</strong> {html.escape(prediction.why)}</p>"
702
  f"{exposure_note}"
703
  '<p class="boundary-note">Relevant means the article passed the configured '
704
  "screen. It does not mean bullish, bearish, material, or predictive.</p>"
705
- f'<p class="small-note">RANGE {html.escape(__version__)} · '
706
  f"taxonomy {html.escape(_engine().taxonomy.version)} · "
707
  f"CPU workers {_engine().workers}</p>"
708
  "</div>"
709
  )
710
 
711
 
712
- def _audit_payload(prediction) -> dict[str, Any]:
713
- decision = _jsonable(prediction)
714
- article = decision.get("article") if isinstance(decision, dict) else None
715
- if not isinstance(article, dict):
716
- raise RuntimeError("RANGE produced an invalid evidence record.")
717
-
718
- article_text = prediction.article.text
719
- encoded_text = article_text.encode("utf-8")
720
- article.pop("text", None)
721
- article.update(
722
- {
723
- "text_sha256": hashlib.sha256(encoded_text).hexdigest(),
724
- "text_characters": len(article_text),
725
- "text_utf8_bytes": len(encoded_text),
726
- "text_normalization": "leading_and_trailing_whitespace_removed",
727
- }
728
- )
729
- return {
730
- "summary": prediction.summary(),
731
- "package_version": __version__,
732
- "backend": _jsonable(_engine().backend_info),
733
- "taxonomy": _engine().taxonomy.describe(),
734
- "decision": decision,
735
- }
736
 
737
 
738
  def analyze_article(
@@ -744,47 +777,47 @@ def analyze_article(
744
  """Screen one timestamped article for configured financial-news relevance.
745
 
746
  Returns a plain-language result, highlighted evidence, an application-ready
747
- summary, rule path, detailed evidence data, Mermaid source, and a download.
748
  """
749
 
750
- if not isinstance(text, str):
751
- raise gr.Error("Article text must be text.")
752
- _validate_article_size(text)
753
- cleaned_text = text.strip()
754
- if not cleaned_text:
755
- raise gr.Error("Enter non-empty article text.")
756
- resolved_timestamp, _, _ = _bounded_metadata(
757
- timestamp,
758
- label="Publication timestamp",
759
- max_characters=MAX_TIMESTAMP_CHARS,
760
- max_bytes=MAX_TIMESTAMP_BYTES,
761
- required=True,
762
- )
763
- resolved_id, _, _ = _bounded_metadata(
764
- article_id,
765
- label="Article ID",
766
- max_characters=MAX_ARTICLE_ID_CHARS,
767
- max_bytes=MAX_ARTICLE_ID_BYTES,
768
- )
769
- resolved_source, _, _ = _bounded_metadata(
770
- source,
771
- label="Source",
772
- max_characters=MAX_SOURCE_CHARS,
773
- max_bytes=MAX_SOURCE_BYTES,
774
- )
775
-
776
- try:
777
- prediction = _engine().predict(
778
- cleaned_text,
779
- timestamp=resolved_timestamp,
780
- article_id=resolved_id or None,
781
- source=resolved_source or "demo",
782
- )
783
- except (TypeError, ValueError) as exc:
784
- raise gr.Error(
785
- "The article or publication timestamp is invalid. "
786
- "Use an ISO 8601 timestamp such as 2025-01-15T09:30:00-05:00."
787
- ) from exc
788
 
789
  audit = _audit_payload(prediction)
790
  explanation = _engine().explain(prediction, format="text", detail="audit")
@@ -801,43 +834,43 @@ def analyze_article(
801
  )
802
 
803
 
804
- def _rows(value: Any) -> list[list[Any]]:
805
- """Return a small four-column table after validating its shape up front."""
806
-
807
- if value is None:
808
- return []
809
-
810
- shape = getattr(value, "shape", None)
811
- if shape is not None:
812
- try:
813
- dimensions = tuple(int(part) for part in shape)
814
- except (TypeError, ValueError) as exc:
815
- raise gr.Error("Batch input must be a two-dimensional table.") from exc
816
- if len(dimensions) != 2:
817
- raise gr.Error("Batch input must be a two-dimensional table.")
818
- _validate_batch_dimensions(dimensions[0], dimensions[1])
819
- if hasattr(value, "values") and hasattr(value.values, "tolist"):
820
- converted = value.values.tolist()
821
- elif hasattr(value, "tolist"):
822
- converted = value.tolist()
823
- else:
824
- raise gr.Error("Batch input must be a two-dimensional table.")
825
- if not isinstance(converted, list):
826
- raise gr.Error("Batch input must be a two-dimensional table.")
827
- rows: Sequence[Any] = converted
828
- else:
829
- if not isinstance(value, Sequence) or isinstance(
830
- value, (str, bytes, bytearray)
831
- ):
832
- raise gr.Error("Batch input must be a two-dimensional table.")
833
- _validate_batch_dimensions(len(value), EXPECTED_BATCH_COLUMNS)
834
- rows = value
835
-
836
- for index, row in enumerate(rows):
837
- if not isinstance(row, Sequence) or isinstance(row, (str, bytes, bytearray)):
838
- raise gr.Error(f"Batch row {index + 1} must contain four cells.")
839
- _validate_batch_dimensions(len(rows), len(row))
840
- return [list(row) for row in rows]
841
 
842
 
843
  def analyze_batch(data: Any):
@@ -847,103 +880,103 @@ def analyze_batch(data: Any):
847
  or Arrow APIs for high-throughput datasets.
848
  """
849
 
850
- normalized: list[tuple[str, str, str, str]] = []
851
- total_text_characters = 0
852
- total_text_bytes = 0
853
- total_metadata_characters = 0
854
- total_metadata_bytes = 0
855
- for index, row in enumerate(_rows(data)):
856
- raw_article_id, raw_timestamp, raw_source, raw_text = (
857
- _batch_cell_text(cell, label=f"Batch row {index + 1}, column {offset + 1}")
858
- for offset, cell in enumerate(row)
859
- )
860
- article_id, id_characters, id_bytes = _bounded_metadata(
861
- raw_article_id,
862
- label=f"Batch row {index + 1} article ID",
863
- max_characters=MAX_ARTICLE_ID_CHARS,
864
- max_bytes=MAX_ARTICLE_ID_BYTES,
865
- )
866
- timestamp, timestamp_characters, timestamp_bytes = _bounded_metadata(
867
- raw_timestamp,
868
- label=f"Batch row {index + 1} publication timestamp",
869
- max_characters=MAX_TIMESTAMP_CHARS,
870
- max_bytes=MAX_TIMESTAMP_BYTES,
871
- )
872
- source, source_characters, source_bytes = _bounded_metadata(
873
- raw_source,
874
- label=f"Batch row {index + 1} source",
875
- max_characters=MAX_SOURCE_CHARS,
876
- max_bytes=MAX_SOURCE_BYTES,
877
- )
878
- text_characters, text_bytes = _validate_article_size(
879
- raw_text, label=f"Batch row {index + 1} article"
880
- )
881
- text = raw_text.strip()
882
-
883
- metadata_characters = id_characters + timestamp_characters + source_characters
884
- metadata_bytes = id_bytes + timestamp_bytes + source_bytes
885
- total_metadata_characters += metadata_characters
886
- total_metadata_bytes += metadata_bytes
887
- total_text_characters += text_characters
888
- total_text_bytes += text_bytes
889
- total_content_characters = total_metadata_characters + total_text_characters
890
- total_content_bytes = total_metadata_bytes + total_text_bytes
891
-
892
- if total_metadata_characters > MAX_BATCH_METADATA_CHARS:
893
- raise gr.Error(
894
- "Batch metadata exceeds the "
895
- f"{MAX_BATCH_METADATA_CHARS:,}-character aggregate limit."
896
- )
897
- if total_metadata_bytes > MAX_BATCH_METADATA_BYTES:
898
- raise gr.Error(
899
- "Batch metadata exceeds the "
900
- f"{MAX_BATCH_METADATA_BYTES:,}-byte aggregate UTF-8 limit."
901
- )
902
- if total_text_characters > MAX_BATCH_CHARS:
903
- raise gr.Error(
904
- f"Batch article text exceeds the {MAX_BATCH_CHARS:,}-character "
905
- "aggregate limit."
906
- )
907
- if total_text_bytes > MAX_BATCH_BYTES:
908
- raise gr.Error(
909
- f"Batch article text exceeds the {MAX_BATCH_BYTES:,}-byte "
910
- "aggregate UTF-8 limit."
911
- )
912
- if total_content_characters > MAX_BATCH_CONTENT_CHARS:
913
- raise gr.Error(
914
- "Combined batch article and metadata content exceeds the "
915
- f"{MAX_BATCH_CONTENT_CHARS:,}-character limit."
916
- )
917
- if total_content_bytes > MAX_BATCH_CONTENT_BYTES:
918
- raise gr.Error(
919
- "Combined batch article and metadata content exceeds the "
920
- f"{MAX_BATCH_CONTENT_BYTES:,}-byte UTF-8 limit."
921
- )
922
-
923
- if not any((article_id, timestamp, source, text)):
924
- continue
925
- if not text:
926
- raise gr.Error(f"Batch row {index + 1} has no article text.")
927
- if not timestamp:
928
- raise gr.Error(f"Batch row {index + 1} has no timestamp.")
929
- normalized.append(
930
- (article_id or f"row-{index}", timestamp, source or "demo", text)
931
- )
932
 
933
  if not normalized:
934
  raise gr.Error("Add at least one complete batch row.")
935
- try:
936
  predictions = _engine().predict_batch(
937
  [row[3] for row in normalized],
938
  timestamps=[row[1] for row in normalized],
939
  article_ids=[row[0] for row in normalized],
940
  sources=[row[2] for row in normalized],
941
  )
942
- except (TypeError, ValueError) as exc:
943
- raise gr.Error(
944
- "A batch row is invalid. Check that every publication timestamp is "
945
- "valid ISO 8601 text."
946
- ) from exc
947
 
948
  summaries = [prediction.summary() for prediction in predictions]
949
  table = [
@@ -972,62 +1005,62 @@ def analyze_batch(data: Any):
972
  def build_demo() -> gr.Blocks:
973
  """Build the human-facing UI and named Gradio API endpoints."""
974
 
975
- blocks_options: dict[str, Any] = {
976
- "title": "RANGE",
977
- "fill_width": True,
978
- "delete_cache": (60, 300),
979
- }
980
- if GRADIO_MAJOR < 6:
981
- blocks_options.update(css=CSS, theme=THEME)
982
-
983
- def build_content() -> None:
984
- with gr.Tab("Screen one article"):
985
  gr.Markdown(
986
  "### Check an article\n"
987
  "Paste the text and add its publication time. Time is required so "
988
- "RANGE can apply time-valid names and relationships; it is not "
989
  "used to forecast prices."
990
  )
991
  with gr.Row():
992
  with gr.Column(scale=5):
993
- article = gr.Textbox(
994
- label="Article text",
995
- lines=13,
996
- value=SINGLE_EXAMPLES[0][0],
997
- placeholder="Paste a headline, excerpt, or full article...",
998
- max_length=MAX_ARTICLE_CHARS,
999
- )
1000
  with gr.Row():
1001
  timestamp = gr.Textbox(
1002
  label="Publication timestamp",
1003
- value=SINGLE_EXAMPLES[0][1],
1004
- info="Required · ISO 8601; include an offset when known.",
1005
- max_length=MAX_TIMESTAMP_CHARS,
1006
  )
1007
  with gr.Accordion("Optional article metadata", open=False):
1008
  gr.Markdown(
1009
- "Article ID and source make exported records easier to join "
1010
  "back to your data. They do not change text matching."
1011
  )
1012
  with gr.Row():
1013
- article_id = gr.Textbox(
1014
- label="Article ID",
1015
- value=SINGLE_EXAMPLES[0][2],
1016
- max_length=MAX_ARTICLE_ID_CHARS,
1017
- )
1018
- source = gr.Textbox(
1019
- label="Source",
1020
- value=SINGLE_EXAMPLES[0][3],
1021
- max_length=MAX_SOURCE_CHARS,
1022
- )
1023
  analyze = gr.Button("Check relevance", variant="primary")
1024
  with gr.Column(scale=4):
1025
  status = gr.HTML(INITIAL_STATUS_HTML)
1026
  with gr.Accordion("Developer output and download", open=False):
1027
- summary = gr.JSON(label="Application-ready summary")
1028
- result_file = gr.File(
1029
- label="Download evidence record (article text omitted)"
1030
- )
1031
 
1032
  example_options: dict[str, Any] = {
1033
  "examples": SINGLE_EXAMPLES,
@@ -1054,9 +1087,9 @@ def build_demo() -> gr.Blocks:
1054
  interactive=False,
1055
  label="Evidence and rule path",
1056
  )
1057
- with gr.Accordion("Why RANGE returned this result", open=True):
1058
  explanation = gr.Textbox(lines=16, label=None)
1059
- with gr.Accordion("Evidence record (article text omitted)", open=False):
1060
  audit_json = gr.Code(language="json", lines=24, label=None)
1061
  with gr.Accordion("Mermaid evidence-flow source (developer)", open=False):
1062
  diagram = gr.Code(language="markdown", lines=20, label=None)
@@ -1074,10 +1107,10 @@ def build_demo() -> gr.Blocks:
1074
  diagram,
1075
  result_file,
1076
  ],
1077
- api_name="analyze",
1078
- scroll_to_output=True,
1079
- concurrency_limit=1,
1080
- concurrency_id="range-cpu",
1081
  )
1082
 
1083
  with gr.Tab("Batch demo (up to 100)"):
@@ -1120,11 +1153,11 @@ def build_demo() -> gr.Blocks:
1120
  screen.click(
1121
  fn=analyze_batch,
1122
  inputs=batch_input,
1123
- outputs=[batch_results, batch_metrics, batch_file],
1124
- api_name="batch",
1125
- concurrency_limit=1,
1126
- concurrency_id="range-cpu",
1127
- )
1128
 
1129
  with gr.Tab("How it works"):
1130
  gr.Markdown(ABOUT)
@@ -1134,39 +1167,42 @@ def build_demo() -> gr.Blocks:
1134
  "API documentation and `agents.md` instructions for coding agents."
1135
  )
1136
 
1137
- gr.Markdown(
1138
- "---\n"
1139
- '<div class="privacy-note"><strong>Research alpha · privacy notice.</strong> '
1140
- "RANGE records configured evidence; it does not provide investment "
 
 
 
1141
  "advice or a forecast. In a hosted Space, submitted text and generated "
1142
  "downloads are processed temporarily on the Space host. Do not submit "
1143
- "private, licensed, or confidential text unless you have permission.</div>"
1144
- )
1145
-
1146
- with gr.Blocks(**blocks_options) as demo:
1147
- gr.HTML(HERO_HTML, elem_classes=["hero-host"])
1148
- with gr.Column(elem_classes=["content-shell"]):
1149
- build_content()
1150
- return demo
1151
 
1152
 
1153
  demo = build_demo()
1154
 
1155
 
1156
  if __name__ == "__main__":
1157
- launch_options: dict[str, Any] = {
1158
- "server_name": os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
1159
- "server_port": int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
1160
- "show_error": _local_error_details_enabled(),
1161
- "app_kwargs": {
1162
- "middleware": [
1163
- Middleware(
1164
- RequestBodyLimitMiddleware,
1165
- max_body_size=MAX_HTTP_REQUEST_BYTES,
1166
- )
1167
- ]
1168
- },
1169
- }
1170
  if GRADIO_MAJOR >= 6:
1171
  launch_options.update(css=CSS, theme=THEME)
1172
- demo.queue(api_open=False, max_size=32).launch(**launch_options)
 
1
+ """Space-ready Gradio demo for explainable financial-news relevance screening."""
2
 
3
  from __future__ import annotations
4
 
5
+ import dataclasses
6
+ import hashlib
7
+ import html
8
+ import json
9
+ import os
10
+ import sys
11
+ import uuid
12
  from collections.abc import Mapping, Sequence
13
  from datetime import datetime
14
  from enum import Enum
 
16
  from pathlib import Path
17
  from typing import Any
18
 
19
+ import gradio as gr
20
+ from starlette.middleware import Middleware
21
+ from starlette.middleware.body_limit import RequestBodyLimitMiddleware
22
+ from gradio.utils import get_upload_folder
23
+
24
+ VENDORED_PACKAGE_ROOT = Path(__file__).resolve().parent / "vendor"
25
+ if VENDORED_PACKAGE_ROOT.is_dir():
26
+ sys.path.insert(0, str(VENDORED_PACKAGE_ROOT))
27
+
28
+ try:
29
+ import spaces
30
  except ModuleNotFoundError: # Local installs and the Docker image do not need it.
31
  spaces = None
32
 
33
+ from range_news import RelevanceEngine, __version__
34
 
35
  from examples import ARTICLE_EXAMPLES, BATCH_ROWS, SINGLE_EXAMPLES
36
 
 
46
 
47
  GRADIO_MAJOR = int(gr.__version__.split(".", 1)[0])
48
  THEME = gr.themes.Soft(primary_hue="teal", neutral_hue="slate")
49
+ MAX_BATCH_ROWS = 100
50
+ EXPECTED_BATCH_COLUMNS = 4
51
+ MAX_ARTICLE_CHARS = 20_000
52
+ MAX_ARTICLE_BYTES = 80_000
53
+ MAX_TIMESTAMP_CHARS = 128
54
+ MAX_TIMESTAMP_BYTES = 512
55
+ MAX_ARTICLE_ID_CHARS = 256
56
+ MAX_ARTICLE_ID_BYTES = 1_024
57
+ MAX_SOURCE_CHARS = 256
58
+ MAX_SOURCE_BYTES = 1_024
59
+ MAX_BATCH_CHARS = 200_000
60
+ MAX_BATCH_BYTES = 800_000
61
+ MAX_BATCH_METADATA_CHARS = 64_000
62
+ MAX_BATCH_METADATA_BYTES = 256_000
63
+ MAX_BATCH_CONTENT_CHARS = MAX_BATCH_CHARS + MAX_BATCH_METADATA_CHARS
64
+ MAX_BATCH_CONTENT_BYTES = MAX_BATCH_BYTES + MAX_BATCH_METADATA_BYTES
65
+ MAX_HTTP_REQUEST_BYTES = 4 * 1024 * 1024
66
  EVIDENCE_HEADERS = [
67
  "evidence path",
68
  "matched text",
 
81
  "why",
82
  ]
83
 
84
+ CSS = """
85
+ html, body {width: 100%; margin: 0 !important; overflow-x: clip;}
86
+ .gradio-container {
87
+ width: 100% !important;
88
+ max-width: none !important;
89
+ margin: 0 !important;
90
+ padding: 0 0 2rem !important;
91
+ }
92
+ .hero-host {
93
+ width: 100vw !important;
94
+ max-width: none !important;
95
+ margin-inline: calc(50% - 50vw) !important;
96
+ padding: 0 !important;
97
+ border: 0 !important;
98
+ box-sizing: border-box !important;
99
+ }
100
+ .hero {
101
+ position: relative;
102
+ width: 100%;
103
+ margin: 0;
104
+ overflow: hidden;
105
+ box-sizing: border-box;
106
+ color: #f1f5f9;
107
+ border-block: 1px solid #1e3448;
108
+ border-inline: 0;
109
+ background:
110
+ radial-gradient(circle at 92% 15%, rgba(13, 148, 136, 0.18), transparent 34%),
111
+ radial-gradient(circle at 8% 88%, rgba(37, 99, 235, 0.12), transparent 30%),
112
+ linear-gradient(145deg, #101f30, #123b43);
113
+ box-shadow: 0 18px 48px rgba(15, 23, 42, 0.08);
114
+ }
115
+ .hero-inner {
116
+ width: min(calc(100% - 2rem), 1180px);
117
+ margin-inline: auto;
118
+ padding: clamp(2rem, 4vw, 3rem) clamp(1.25rem, 4vw, 3.2rem);
119
+ box-sizing: border-box;
120
+ text-align: center;
121
+ }
122
+ .hero-eyebrow,
123
+ .hero h1,
124
+ .hero-expansion,
125
+ .hero-copy,
126
+ .benchmark-note {
127
+ width: 100%;
128
+ text-align: center !important;
129
+ }
130
+ .hero-eyebrow {
131
+ margin: 0 0 0.8rem;
132
+ color: #7de8d5;
133
  font-size: 0.78rem;
134
  font-weight: 750;
135
  letter-spacing: 0.12em;
136
  text-transform: uppercase;
137
  }
138
+ .hero h1 {
139
+ max-width: 960px;
140
+ margin: 0 auto;
141
+ font-size: clamp(3.4rem, 9vw, 6.2rem);
142
+ line-height: 0.92;
143
+ letter-spacing: -0.065em;
144
+ }
145
+ .hero-expansion {
146
+ max-width: 960px;
147
+ margin: 1rem auto 0;
148
+ font-size: clamp(1.08rem, 2.5vw, 1.55rem);
149
+ font-weight: 720;
150
+ line-height: 1.35;
151
+ }
152
+ .hero-copy {
153
+ max-width: 900px;
154
+ margin: 1rem auto 0;
155
+ font-size: clamp(1rem, 1.5vw, 1.16rem);
156
+ line-height: 1.65;
157
+ opacity: 0.88;
158
+ }
159
+ .speed-grid {
160
+ display: grid;
161
+ grid-template-columns: repeat(3, minmax(0, 1fr));
162
+ gap: 0.75rem;
163
+ max-width: 900px;
164
+ margin: 1.6rem auto 0;
165
+ }
166
+ .speed-stat {
167
+ padding: 0.9rem 1rem;
168
+ border: 1px solid var(--border-color-primary);
169
+ border-radius: 14px;
170
+ background: var(--background-fill-primary);
171
+ text-align: center;
172
+ }
173
+ .hero h1, .hero-expansion, .hero-copy, .hero .benchmark-note, .hero .speed-stat span {color: #edf8f7 !important;}
174
+ .hero .speed-stat, .hero .trust-pill {background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.2); color: #f1f5f9;}
175
+ .hero .package-pill {background: #a4f4da; color: #12352f; border-color: #a4f4da;}
176
+ .hero .speed-stat strong {color: #a4f4da;}
177
+ .speed-stat strong {display: block; font-size: clamp(1.2rem, 2.6vw, 1.75rem); line-height: 1.15;}
178
+ .speed-stat span {display: block; margin-top: 0.25rem; font-size: 0.8rem; line-height: 1.35; opacity: 0.72;}
179
+ .benchmark-note {max-width: 900px; margin: 0.75rem auto 0; font-size: 0.86rem; line-height: 1.5; opacity: 0.72;}
180
+ .trust-row {display: flex; justify-content: center; flex-wrap: wrap; gap: 0.55rem; margin-top: 1.25rem;}
181
  .trust-pill {
182
  padding: 0.36rem 0.68rem;
183
  border: 1px solid var(--border-color-primary);
 
186
  font-size: 0.82rem;
187
  font-weight: 650;
188
  }
189
+ .value-grid {
190
+ display: grid;
191
+ grid-template-columns: repeat(3, minmax(0, 1fr));
192
+ gap: 0.8rem;
193
+ width: min(calc(100% - 2rem), 1120px);
194
+ max-width: 1120px;
195
+ margin: 1.15rem auto 1.35rem;
196
+ }
197
+ .content-shell {
198
+ width: min(calc(100% - 2rem), 1120px) !important;
199
+ max-width: 1120px !important;
200
+ margin-inline: auto !important;
201
+ align-self: center !important;
202
+ }
203
+ .value-card {
204
  padding: 1rem 1.05rem;
205
  border: 1px solid var(--border-color-primary);
206
+ border-radius: 14px;
207
+ background: var(--background-fill-primary);
208
+ text-align: center;
209
  }
210
  .value-card strong {display: block; margin-bottom: 0.25rem; font-size: 0.95rem;}
211
  .value-card span {font-size: 0.88rem; line-height: 1.45; opacity: 0.78;}
 
271
  font-size: 0.88rem;
272
  line-height: 1.5;
273
  }
274
+ @media (max-width: 760px) {
275
+ .speed-grid, .value-grid, .result-grid {grid-template-columns: 1fr;}
276
+ .hero-inner {padding: 1.8rem 1.25rem 2rem;}
277
+ .speed-stat {text-align: center;}
278
+ .status-card {min-height: auto;}
279
+ }
280
  """
281
 
282
  ABOUT = """
283
+ ## Financial news, with the evidence attached
284
+
285
+ RANGE screens timestamped articles for economic and financial relevance, maps
286
+ supported evidence to broad-market exposures, and shows exactly why it made each
287
+ decision. It is a deterministic engine: the same text, timestamp and taxonomy
288
+ produce the same result.
289
+
290
+ ### Follow every decision
291
+ 1. Normalize the text while preserving its original locations.
292
+ 2. Match configured phrases and identifiers.
293
+ 3. Apply context checks and names valid on the publication date.
294
+ 4. Return relevance, supported market connections and the complete evidence trail.
295
+
296
+ ### Built for volume
297
+ The CPU engine uses compiled Rust multi-pattern matching and parallel workers.
298
+ The optional CUDA research engine distributes normalization and matching across
299
+ the GPU, overlaps transfers with work, and preserves exact CPU handling where needed.
300
+ Compact Arrow results avoid creating a large Python object for every article.
301
+
302
+ ### Your taxonomy, your rules
303
+ RANGE's bundled taxonomy covers economic and financial news. Taxonomies are
304
+ versioned configuration: add topics, aliases, context rules and relationships
305
+ without retraining a language model. New taxonomies still need correctness and
306
+ performance checks; their speed is not guaranteed by the default benchmark.
307
+
308
+ ### What you are trying here
309
+ This live demo runs on CPU and returns rich explanations. Its small batches are
310
+ for exploring decisions. The high-throughput benchmarks measure compact bulk
311
+ screening, including GPU transfers and CPU fallback where applicable.
312
+
313
+ A relevant result means the configured screen accepted the article. It does not
314
+ infer sentiment, predict returns, or recommend a trade. No LLM, API key or model
315
+ download is required for inference.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  """
317
 
318
+ PERFORMANCE = """
319
+ ## Measured speed. Clear boundaries.
320
+
321
+ | Implementation | Articles / second | Measurement |
322
+ | --- | ---: | --- |
323
+ | CUDA research record | **1,833,059** | Qualified warm screening of 10 million articles |
324
+ | Maintained CPU implementation | **489,759** | Qualified warm screening of 10 million articles |
325
+ | Newer retained CUDA installation | **958,155** | Separate local installed-package qualification |
326
+
327
+ Measured on an Intel i7-10750H Windows laptop with 10 CPU workers; CUDA used an
328
+ RTX 2070 Max-Q. These are separate experiments, not a same-run CPU/GPU speedup.
329
+ The CUDA peak belongs to an earlier qualified implementation; later sessions
330
+ varied substantially. Do not combine its peak with later percentage gains.
331
+
332
+ Screening timings exclude file loading, imports, initialization and verification.
333
+ CUDA timings include transfers, exact CPU fallback and result assembly.
334
+ The live demo uses a different CPU environment and rich explanations, so these
335
+ figures are not its throughput or a service guarantee.
336
+
337
+ [Read the benchmark record](https://huggingface.co/spaces/Brianferrell787/range-news/blob/main/BENCHMARKS.md)
338
+
339
+ ## Python package coming soon
340
+
341
+ We are preparing RANGE for straightforward Python installation, with CPU support
342
+ and an optional CUDA backend. Public package release, clean installation across
343
+ supported platforms and broader GPU compatibility are still ahead.
344
+ The current Space lets you explore the engine today.
345
+
346
+ ## What we want to try next
347
+
348
+ - **Finish the release:** clean installs, supported platforms, documentation and
349
+ validation of the exact wheels people will use.
350
+ - **Share work across taxonomies:** build on successful two-taxonomy experiments,
351
+ then extend packaging and memory scheduling for more rule sets.
352
+ - **Choose CPU or CUDA by workload:** measure small batches, large batches and
353
+ routing overhead before introducing automatic selection.
354
+ - **Target remaining matching ideas:** direct StringZilla integration and other
355
+ CUSMART methods remain candidates, not demonstrated improvements.
356
+ - **Train optimizations on separate data:** investigate corpus-trained automata
357
+ and fresh compiler profiles using representative development articles.
358
+
359
+ Recent alternatives did not beat the retained matcher. We keep negative results
360
+ and require exact outputs before considering a speed improvement.
361
+ """
362
+
363
+ HERO_HTML = """
364
+ <section class="hero">
365
+ <div class="hero-inner">
366
+ <p class="hero-eyebrow">Fast · consistent · shows its work</p>
367
+ <h1>RANGE</h1>
368
+ <p class="hero-expansion">Rapid Analysis of News for the General Economy</p>
369
+ <p class="hero-copy">Find the financial news that matters to your rules.<br>
370
+ Screen articles, trace market connections, and see the exact evidence behind
371
+ every decision.</p>
372
+ <div class="trust-row" aria-label="Product characteristics">
373
+ <span class="trust-pill package-pill">Python package coming soon</span>
374
+ <span class="trust-pill">No LLM required</span>
375
+ <span class="trust-pill">Customizable taxonomies</span>
376
+ </div>
377
+ <div class="speed-grid" aria-label="Recorded bulk screening benchmarks">
378
+ <div class="speed-stat"><strong>1.83M</strong><span>articles/s · CUDA research record</span></div>
379
+ <div class="speed-stat"><strong>489,759</strong><span>articles/s · maintained CPU result</span></div>
380
+ <div class="speed-stat"><strong>Exact evidence</strong><span>Matched text, rules and market connections</span></div>
381
+ </div>
382
+ <p class="benchmark-note">Separate qualified 10-million-article screening runs on one Windows laptop.
383
+ CUDA peak is historical; later installed CUDA measured 958,155 articles/s.
384
+ This live demo runs on CPU. See Performance &amp; roadmap for conditions.</p>
385
+ </div>
386
+ </section>
387
+ <section class="value-grid" aria-label="What RANGE returns">
388
+ <div class="value-card"><strong>Screen the article</strong><span>Find economic and financial topics covered by your configured rules.</span></div>
389
+ <div class="value-card"><strong>Connect the evidence</strong><span>See supported broad-market and index relationships, with time-valid identities.</span></div>
390
+ <div class="value-card"><strong>Inspect the reason</strong><span>Follow each result back to the original words and the rule that accepted them.</span></div>
391
+ </section>
392
  """
393
 
394
  INITIAL_STATUS_HTML = """
395
  <div class="status-card status-empty" role="status" aria-live="polite">
396
  <span class="status-label">Screening result</span>
397
  <h3>Your result will appear here</h3>
398
+ <p>RANGE reports article relevance and supported broad-market exposure as two
399
  separate outputs.</p>
400
  <div class="result-grid">
401
  <div class="result-cell"><strong>Article relevance</strong><span>Relevant · No configured match</span></div>
402
+ <div class="result-cell"><strong>Related market</strong><span>Shown only when the article supports a connection</span></div>
403
  </div>
404
  <p class="boundary-note">This is a relevance screen—not sentiment, materiality, a
405
  return forecast, or investment advice.</p>
 
407
  """
408
 
409
 
410
+ def _text_size(value: str, *, label: str) -> tuple[int, int]:
411
+ """Return character and UTF-8 sizes with a public-safe Unicode error."""
412
+
413
+ characters = len(value)
414
+ try:
415
+ utf8_bytes = len(value.encode("utf-8"))
416
+ except UnicodeEncodeError as exc:
417
+ raise gr.Error(f"{label} contains invalid Unicode text.") from exc
418
+ return characters, utf8_bytes
419
+
420
+
421
+ def _validate_article_size(text: str, *, label: str = "Article") -> tuple[int, int]:
422
+ """Validate public-request bounds before the engine sees article text."""
423
+
424
+ characters, utf8_bytes = _text_size(text, label=label)
425
+ if characters > MAX_ARTICLE_CHARS:
426
+ raise gr.Error(f"{label} exceeds the {MAX_ARTICLE_CHARS:,}-character limit.")
427
+ if utf8_bytes > MAX_ARTICLE_BYTES:
428
+ raise gr.Error(f"{label} exceeds the {MAX_ARTICLE_BYTES:,}-byte UTF-8 limit.")
429
+ return characters, utf8_bytes
430
+
431
+
432
+ def _bounded_metadata(
433
+ value: Any,
434
+ *,
435
+ label: str,
436
+ max_characters: int,
437
+ max_bytes: int,
438
+ required: bool = False,
439
+ ) -> tuple[str, int, int]:
440
+ """Normalize one metadata value after enforcing its raw request bounds."""
441
+
442
+ if value is None:
443
+ raw_value = ""
444
+ elif isinstance(value, str):
445
+ raw_value = value
446
+ else:
447
+ raise gr.Error(f"{label} must be text.")
448
+
449
+ characters, utf8_bytes = _text_size(raw_value, label=label)
450
+ if characters > max_characters:
451
+ raise gr.Error(f"{label} exceeds the {max_characters:,}-character limit.")
452
+ if utf8_bytes > max_bytes:
453
+ raise gr.Error(f"{label} exceeds the {max_bytes:,}-byte UTF-8 limit.")
454
+
455
+ cleaned = raw_value.strip()
456
+ if required and not cleaned:
457
+ raise gr.Error(f"Enter {label.lower()}.")
458
+ return cleaned, characters, utf8_bytes
459
+
460
+
461
+ def _batch_cell_text(value: Any, *, label: str) -> str:
462
+ """Accept ordinary table scalars without stringifying nested objects."""
463
+
464
+ if value is None:
465
+ return ""
466
+ if isinstance(value, str):
467
+ return value
468
+ if isinstance(value, (bool, int, float)):
469
+ return str(value)
470
+ raise gr.Error(f"{label} must be plain text or a number.")
471
+
472
+
473
+ def _validate_batch_dimensions(row_count: int, column_count: int) -> None:
474
+ """Reject oversized or malformed tables before their cells are copied."""
475
+
476
+ if row_count > MAX_BATCH_ROWS:
477
+ raise gr.Error(
478
+ f"The interactive demo is capped at {MAX_BATCH_ROWS} rows per request."
479
+ )
480
+ if column_count != EXPECTED_BATCH_COLUMNS:
481
+ raise gr.Error(
482
+ "Batch input must have exactly four columns: "
483
+ "article_id, timestamp, source, and text."
484
+ )
485
+
486
+
487
+ def _local_error_details_enabled() -> bool:
488
+ """Allow detailed tracebacks only through an explicit non-Space opt-in."""
489
+
490
+ requested = os.environ.get("RANGE_LOCAL_SHOW_ERROR", "").strip().casefold()
491
+ if requested not in {"1", "true", "yes", "on"}:
492
+ return False
493
+ return not any(os.environ.get(name) for name in ("SPACE_ID", "SPACE_HOST"))
494
 
495
 
496
  @lru_cache(maxsize=1)
 
528
  return value
529
 
530
 
531
+ def _download_file(payload: Any, *, suffix: str) -> str:
532
+ cache_root = Path(get_upload_folder())
533
+ cache_root.mkdir(parents=True, exist_ok=True)
534
+ path = cache_root / f"relevance-{uuid.uuid4().hex}{suffix}"
535
  if suffix == ".jsonl":
536
  lines = [json.dumps(row, ensure_ascii=False, sort_keys=True) for row in payload]
537
  path.write_text("\n".join(lines) + "\n", encoding="utf-8")
 
728
  '<div class="result-grid">'
729
  '<div class="result-cell"><strong>Article relevance</strong>'
730
  f"<span>{html.escape(badge)}</span></div>"
731
+ '<div class="result-cell"><strong>Related market</strong>'
732
  f"<span>{html.escape(exposure_value)}</span></div>"
733
  "</div>"
734
+ f"<p><strong>Why RANGE returned this:</strong> {html.escape(prediction.why)}</p>"
735
  f"{exposure_note}"
736
  '<p class="boundary-note">Relevant means the article passed the configured '
737
  "screen. It does not mean bullish, bearish, material, or predictive.</p>"
738
+ f'<p class="small-note">RANGE {html.escape(__version__)} · '
739
  f"taxonomy {html.escape(_engine().taxonomy.version)} · "
740
  f"CPU workers {_engine().workers}</p>"
741
  "</div>"
742
  )
743
 
744
 
745
+ def _audit_payload(prediction) -> dict[str, Any]:
746
+ decision = _jsonable(prediction)
747
+ article = decision.get("article") if isinstance(decision, dict) else None
748
+ if not isinstance(article, dict):
749
+ raise RuntimeError("RANGE produced an invalid evidence record.")
750
+
751
+ article_text = prediction.article.text
752
+ encoded_text = article_text.encode("utf-8")
753
+ article.pop("text", None)
754
+ article.update(
755
+ {
756
+ "text_sha256": hashlib.sha256(encoded_text).hexdigest(),
757
+ "text_characters": len(article_text),
758
+ "text_utf8_bytes": len(encoded_text),
759
+ "text_normalization": "leading_and_trailing_whitespace_removed",
760
+ }
761
+ )
762
+ return {
763
+ "summary": prediction.summary(),
764
+ "package_version": __version__,
765
+ "backend": _jsonable(_engine().backend_info),
766
+ "taxonomy": _engine().taxonomy.describe(),
767
+ "decision": decision,
768
+ }
769
 
770
 
771
  def analyze_article(
 
777
  """Screen one timestamped article for configured financial-news relevance.
778
 
779
  Returns a plain-language result, highlighted evidence, an application-ready
780
+ summary, rule path, detailed evidence data, Mermaid source, and a download.
781
  """
782
 
783
+ if not isinstance(text, str):
784
+ raise gr.Error("Article text must be text.")
785
+ _validate_article_size(text)
786
+ cleaned_text = text.strip()
787
+ if not cleaned_text:
788
+ raise gr.Error("Enter non-empty article text.")
789
+ resolved_timestamp, _, _ = _bounded_metadata(
790
+ timestamp,
791
+ label="Publication timestamp",
792
+ max_characters=MAX_TIMESTAMP_CHARS,
793
+ max_bytes=MAX_TIMESTAMP_BYTES,
794
+ required=True,
795
+ )
796
+ resolved_id, _, _ = _bounded_metadata(
797
+ article_id,
798
+ label="Article ID",
799
+ max_characters=MAX_ARTICLE_ID_CHARS,
800
+ max_bytes=MAX_ARTICLE_ID_BYTES,
801
+ )
802
+ resolved_source, _, _ = _bounded_metadata(
803
+ source,
804
+ label="Source",
805
+ max_characters=MAX_SOURCE_CHARS,
806
+ max_bytes=MAX_SOURCE_BYTES,
807
+ )
808
+
809
+ try:
810
+ prediction = _engine().predict(
811
+ cleaned_text,
812
+ timestamp=resolved_timestamp,
813
+ article_id=resolved_id or None,
814
+ source=resolved_source or "demo",
815
+ )
816
+ except (TypeError, ValueError) as exc:
817
+ raise gr.Error(
818
+ "The article or publication timestamp is invalid. "
819
+ "Use an ISO 8601 timestamp such as 2025-01-15T09:30:00-05:00."
820
+ ) from exc
821
 
822
  audit = _audit_payload(prediction)
823
  explanation = _engine().explain(prediction, format="text", detail="audit")
 
834
  )
835
 
836
 
837
+ def _rows(value: Any) -> list[list[Any]]:
838
+ """Return a small four-column table after validating its shape up front."""
839
+
840
+ if value is None:
841
+ return []
842
+
843
+ shape = getattr(value, "shape", None)
844
+ if shape is not None:
845
+ try:
846
+ dimensions = tuple(int(part) for part in shape)
847
+ except (TypeError, ValueError) as exc:
848
+ raise gr.Error("Batch input must be a two-dimensional table.") from exc
849
+ if len(dimensions) != 2:
850
+ raise gr.Error("Batch input must be a two-dimensional table.")
851
+ _validate_batch_dimensions(dimensions[0], dimensions[1])
852
+ if hasattr(value, "values") and hasattr(value.values, "tolist"):
853
+ converted = value.values.tolist()
854
+ elif hasattr(value, "tolist"):
855
+ converted = value.tolist()
856
+ else:
857
+ raise gr.Error("Batch input must be a two-dimensional table.")
858
+ if not isinstance(converted, list):
859
+ raise gr.Error("Batch input must be a two-dimensional table.")
860
+ rows: Sequence[Any] = converted
861
+ else:
862
+ if not isinstance(value, Sequence) or isinstance(
863
+ value, (str, bytes, bytearray)
864
+ ):
865
+ raise gr.Error("Batch input must be a two-dimensional table.")
866
+ _validate_batch_dimensions(len(value), EXPECTED_BATCH_COLUMNS)
867
+ rows = value
868
+
869
+ for index, row in enumerate(rows):
870
+ if not isinstance(row, Sequence) or isinstance(row, (str, bytes, bytearray)):
871
+ raise gr.Error(f"Batch row {index + 1} must contain four cells.")
872
+ _validate_batch_dimensions(len(rows), len(row))
873
+ return [list(row) for row in rows]
874
 
875
 
876
  def analyze_batch(data: Any):
 
880
  or Arrow APIs for high-throughput datasets.
881
  """
882
 
883
+ normalized: list[tuple[str, str, str, str]] = []
884
+ total_text_characters = 0
885
+ total_text_bytes = 0
886
+ total_metadata_characters = 0
887
+ total_metadata_bytes = 0
888
+ for index, row in enumerate(_rows(data)):
889
+ raw_article_id, raw_timestamp, raw_source, raw_text = (
890
+ _batch_cell_text(cell, label=f"Batch row {index + 1}, column {offset + 1}")
891
+ for offset, cell in enumerate(row)
892
+ )
893
+ article_id, id_characters, id_bytes = _bounded_metadata(
894
+ raw_article_id,
895
+ label=f"Batch row {index + 1} article ID",
896
+ max_characters=MAX_ARTICLE_ID_CHARS,
897
+ max_bytes=MAX_ARTICLE_ID_BYTES,
898
+ )
899
+ timestamp, timestamp_characters, timestamp_bytes = _bounded_metadata(
900
+ raw_timestamp,
901
+ label=f"Batch row {index + 1} publication timestamp",
902
+ max_characters=MAX_TIMESTAMP_CHARS,
903
+ max_bytes=MAX_TIMESTAMP_BYTES,
904
+ )
905
+ source, source_characters, source_bytes = _bounded_metadata(
906
+ raw_source,
907
+ label=f"Batch row {index + 1} source",
908
+ max_characters=MAX_SOURCE_CHARS,
909
+ max_bytes=MAX_SOURCE_BYTES,
910
+ )
911
+ text_characters, text_bytes = _validate_article_size(
912
+ raw_text, label=f"Batch row {index + 1} article"
913
+ )
914
+ text = raw_text.strip()
915
+
916
+ metadata_characters = id_characters + timestamp_characters + source_characters
917
+ metadata_bytes = id_bytes + timestamp_bytes + source_bytes
918
+ total_metadata_characters += metadata_characters
919
+ total_metadata_bytes += metadata_bytes
920
+ total_text_characters += text_characters
921
+ total_text_bytes += text_bytes
922
+ total_content_characters = total_metadata_characters + total_text_characters
923
+ total_content_bytes = total_metadata_bytes + total_text_bytes
924
+
925
+ if total_metadata_characters > MAX_BATCH_METADATA_CHARS:
926
+ raise gr.Error(
927
+ "Batch metadata exceeds the "
928
+ f"{MAX_BATCH_METADATA_CHARS:,}-character aggregate limit."
929
+ )
930
+ if total_metadata_bytes > MAX_BATCH_METADATA_BYTES:
931
+ raise gr.Error(
932
+ "Batch metadata exceeds the "
933
+ f"{MAX_BATCH_METADATA_BYTES:,}-byte aggregate UTF-8 limit."
934
+ )
935
+ if total_text_characters > MAX_BATCH_CHARS:
936
+ raise gr.Error(
937
+ f"Batch article text exceeds the {MAX_BATCH_CHARS:,}-character "
938
+ "aggregate limit."
939
+ )
940
+ if total_text_bytes > MAX_BATCH_BYTES:
941
+ raise gr.Error(
942
+ f"Batch article text exceeds the {MAX_BATCH_BYTES:,}-byte "
943
+ "aggregate UTF-8 limit."
944
+ )
945
+ if total_content_characters > MAX_BATCH_CONTENT_CHARS:
946
+ raise gr.Error(
947
+ "Combined batch article and metadata content exceeds the "
948
+ f"{MAX_BATCH_CONTENT_CHARS:,}-character limit."
949
+ )
950
+ if total_content_bytes > MAX_BATCH_CONTENT_BYTES:
951
+ raise gr.Error(
952
+ "Combined batch article and metadata content exceeds the "
953
+ f"{MAX_BATCH_CONTENT_BYTES:,}-byte UTF-8 limit."
954
+ )
955
+
956
+ if not any((article_id, timestamp, source, text)):
957
+ continue
958
+ if not text:
959
+ raise gr.Error(f"Batch row {index + 1} has no article text.")
960
+ if not timestamp:
961
+ raise gr.Error(f"Batch row {index + 1} has no timestamp.")
962
+ normalized.append(
963
+ (article_id or f"row-{index}", timestamp, source or "demo", text)
964
+ )
965
 
966
  if not normalized:
967
  raise gr.Error("Add at least one complete batch row.")
968
+ try:
969
  predictions = _engine().predict_batch(
970
  [row[3] for row in normalized],
971
  timestamps=[row[1] for row in normalized],
972
  article_ids=[row[0] for row in normalized],
973
  sources=[row[2] for row in normalized],
974
  )
975
+ except (TypeError, ValueError) as exc:
976
+ raise gr.Error(
977
+ "A batch row is invalid. Check that every publication timestamp is "
978
+ "valid ISO 8601 text."
979
+ ) from exc
980
 
981
  summaries = [prediction.summary() for prediction in predictions]
982
  table = [
 
1005
  def build_demo() -> gr.Blocks:
1006
  """Build the human-facing UI and named Gradio API endpoints."""
1007
 
1008
+ blocks_options: dict[str, Any] = {
1009
+ "title": "RANGE",
1010
+ "fill_width": True,
1011
+ "delete_cache": (60, 300),
1012
+ }
1013
+ if GRADIO_MAJOR < 6:
1014
+ blocks_options.update(css=CSS, theme=THEME)
1015
+
1016
+ def build_content() -> None:
1017
+ with gr.Tab("Screen one article"):
1018
  gr.Markdown(
1019
  "### Check an article\n"
1020
  "Paste the text and add its publication time. Time is required so "
1021
+ "RANGE can apply time-valid names and relationships; it is not "
1022
  "used to forecast prices."
1023
  )
1024
  with gr.Row():
1025
  with gr.Column(scale=5):
1026
+ article = gr.Textbox(
1027
+ label="Article text",
1028
+ lines=13,
1029
+ value=SINGLE_EXAMPLES[0][0],
1030
+ placeholder="Paste a headline, excerpt, or full article...",
1031
+ max_length=MAX_ARTICLE_CHARS,
1032
+ )
1033
  with gr.Row():
1034
  timestamp = gr.Textbox(
1035
  label="Publication timestamp",
1036
+ value=SINGLE_EXAMPLES[0][1],
1037
+ info="Required · ISO 8601; include an offset when known.",
1038
+ max_length=MAX_TIMESTAMP_CHARS,
1039
  )
1040
  with gr.Accordion("Optional article metadata", open=False):
1041
  gr.Markdown(
1042
+ "Article ID and source make exported records easier to join "
1043
  "back to your data. They do not change text matching."
1044
  )
1045
  with gr.Row():
1046
+ article_id = gr.Textbox(
1047
+ label="Article ID",
1048
+ value=SINGLE_EXAMPLES[0][2],
1049
+ max_length=MAX_ARTICLE_ID_CHARS,
1050
+ )
1051
+ source = gr.Textbox(
1052
+ label="Source",
1053
+ value=SINGLE_EXAMPLES[0][3],
1054
+ max_length=MAX_SOURCE_CHARS,
1055
+ )
1056
  analyze = gr.Button("Check relevance", variant="primary")
1057
  with gr.Column(scale=4):
1058
  status = gr.HTML(INITIAL_STATUS_HTML)
1059
  with gr.Accordion("Developer output and download", open=False):
1060
+ summary = gr.JSON(label="Application-ready summary")
1061
+ result_file = gr.File(
1062
+ label="Download evidence record (article text omitted)"
1063
+ )
1064
 
1065
  example_options: dict[str, Any] = {
1066
  "examples": SINGLE_EXAMPLES,
 
1087
  interactive=False,
1088
  label="Evidence and rule path",
1089
  )
1090
+ with gr.Accordion("Why RANGE returned this result", open=True):
1091
  explanation = gr.Textbox(lines=16, label=None)
1092
+ with gr.Accordion("Evidence record (article text omitted)", open=False):
1093
  audit_json = gr.Code(language="json", lines=24, label=None)
1094
  with gr.Accordion("Mermaid evidence-flow source (developer)", open=False):
1095
  diagram = gr.Code(language="markdown", lines=20, label=None)
 
1107
  diagram,
1108
  result_file,
1109
  ],
1110
+ api_name="analyze",
1111
+ scroll_to_output=True,
1112
+ concurrency_limit=1,
1113
+ concurrency_id="range-cpu",
1114
  )
1115
 
1116
  with gr.Tab("Batch demo (up to 100)"):
 
1153
  screen.click(
1154
  fn=analyze_batch,
1155
  inputs=batch_input,
1156
+ outputs=[batch_results, batch_metrics, batch_file],
1157
+ api_name="batch",
1158
+ concurrency_limit=1,
1159
+ concurrency_id="range-cpu",
1160
+ )
1161
 
1162
  with gr.Tab("How it works"):
1163
  gr.Markdown(ABOUT)
 
1167
  "API documentation and `agents.md` instructions for coding agents."
1168
  )
1169
 
1170
+ with gr.Tab("Performance & roadmap"):
1171
+ gr.Markdown(PERFORMANCE)
1172
+
1173
+ gr.Markdown(
1174
+ "---\n"
1175
+ '<div class="privacy-note"><strong>Research alpha · privacy notice.</strong> '
1176
+ "RANGE records configured evidence; it does not provide investment "
1177
  "advice or a forecast. In a hosted Space, submitted text and generated "
1178
  "downloads are processed temporarily on the Space host. Do not submit "
1179
+ "private, licensed, or confidential text unless you have permission.</div>"
1180
+ )
1181
+
1182
+ with gr.Blocks(**blocks_options) as demo:
1183
+ gr.HTML(HERO_HTML, elem_classes=["hero-host"])
1184
+ with gr.Column(elem_classes=["content-shell"]):
1185
+ build_content()
1186
+ return demo
1187
 
1188
 
1189
  demo = build_demo()
1190
 
1191
 
1192
  if __name__ == "__main__":
1193
+ launch_options: dict[str, Any] = {
1194
+ "server_name": os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
1195
+ "server_port": int(os.environ.get("GRADIO_SERVER_PORT", "7860")),
1196
+ "show_error": _local_error_details_enabled(),
1197
+ "app_kwargs": {
1198
+ "middleware": [
1199
+ Middleware(
1200
+ RequestBodyLimitMiddleware,
1201
+ max_body_size=MAX_HTTP_REQUEST_BYTES,
1202
+ )
1203
+ ]
1204
+ },
1205
+ }
1206
  if GRADIO_MAJOR >= 6:
1207
  launch_options.update(css=CSS, theme=THEME)
1208
+ demo.queue(api_open=False, max_size=32).launch(**launch_options)
docs/attention2alpha-compatibility-review.md DELETED
@@ -1,46 +0,0 @@
1
- # Attention2Alpha compatibility-change safety review
2
-
3
- Status: **safe to retain; no revert recommended**
4
-
5
- Scope: non-package files changed in PR #157 while preparing the standalone RANGE
6
- package and its validation gates.
7
-
8
- This review distinguishes narrow compatibility and correctness repairs from changes to
9
- the trading or learning objective. The reviewed edits do not alter the RANGE
10
- taxonomy and do not redefine Attention2Alpha reward, order execution, portfolio
11
- accounting, replay semantics, or the policy-learning objective.
12
-
13
- ## File review
14
-
15
- | file | change | safety assessment |
16
- | --- | --- | --- |
17
- | `attention2alpha/data_split.py` | preserves opaque string split keys instead of coercing them to timestamps | correct for fraction-mode and synthetic indexes; timestamp inputs retain existing handling |
18
- | `attention2alpha/embeddings/embedding_base.py` | serializes optional configuration objects without importing full optional stacks; makes checkpoint-probe publication failure-safe; clarifies validation errors | reduces import coupling and prevents a stale probe from describing an incomplete artifact |
19
- | `attention2alpha/embeddings/embedding_flair.py` | supports legacy and lightweight adapters and validates persisted round-trip probes on legacy loads | compatibility hardening; production `nn.Module` behavior remains supported |
20
- | `attention2alpha/environments/leveraged_etf_portfolio_env.py` | permits validation with W&B disabled while re-raising failures when an active run exists | reporting-path resilience only; no trade or reward calculation changes |
21
- | `attention2alpha/state_features.py` | uses `min(2, window)` for rolling standard deviation | fixes the invalid `window=1, min_periods=2` case while preserving behavior for windows of two or more |
22
- | `attention2alpha/train_eval.py` | filters loader keyword arguments against legacy signatures, restores fallback metadata, and reuses the production order-matrix builder for simple adapters | compatibility and best-effort reporting improvements; no training-objective change |
23
-
24
- ## Risk notes
25
-
26
- The W&B guard catches a broad exception because W&B versions expose different
27
- uninitialized-run failures. It re-raises when `wandb.run` exists, so failures during an
28
- active tracked run are not silently hidden. A future cleanup may narrow the exception
29
- after the supported W&B range is frozen, but the current behavior is safe for this PR.
30
-
31
- The one-period rolling-volatility change is mathematically well-defined: sample
32
- standard deviation remains `NaN` until enough observations exist, but Pandas no longer
33
- rejects an impossible `min_periods` setting.
34
-
35
- ## Evidence
36
-
37
- The repository deterministic suite exercises checkpoint serialization, round-trip
38
- validation, train/eval compatibility, state features, and environment guards. The
39
- package workflow is separate and verifies that the RANGE wheel does not import the
40
- Attention2Alpha package.
41
-
42
- ## Decision
43
-
44
- Keep the changes. When RANGE moves to a dedicated repository, copy only the package
45
- and `app/` sources; these monorepo compatibility repairs remain part of Attention2Alpha
46
- history rather than becoming dependencies of the standalone package.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/performance.md CHANGED
@@ -1,6 +1,6 @@
1
  # Performance and architecture
2
 
3
- RANGE is optimized as an exact information-retrieval system. It is not a language
4
  model and it does not use an external inference service.
5
 
6
  ## Shipping path
@@ -80,4 +80,4 @@ The strongest portable claim for `0.1.0a1` is architectural:
80
 
81
  > Relevance replaces per-pattern Python work and neural inference with compiled
82
  > multi-pattern CPU retrieval, parallel article processing, and sparse or columnar
83
- > result paths while preserving a traceable evidence contract.
 
1
  # Performance and architecture
2
 
3
+ RANGE is optimized as an exact information-retrieval system. It is not a language
4
  model and it does not use an external inference service.
5
 
6
  ## Shipping path
 
80
 
81
  > Relevance replaces per-pattern Python work and neural inference with compiled
82
  > multi-pattern CPU retrieval, parallel article processing, and sparse or columnar
83
+ > result paths while preserving a traceable evidence contract.
docs/readme-research.md CHANGED
@@ -30,7 +30,7 @@ was to extract durable communication patterns rather than copy rankings or wordi
30
 
31
  | observed pattern | application in Relevance |
32
  | --- | --- |
33
- | one-sentence value proposition | “Fast financial-news screening that shows its work.” |
34
  | show a useful result early | the README opens with real quickstart stdout |
35
  | put runnable code before exhaustive reference | a complete one-article example appears near the top |
36
  | explain performance mechanistically | Rust, automata, Rayon, sparse output, and Arrow are mapped to their practical effects |
@@ -44,7 +44,7 @@ was to extract durable communication patterns rather than copy rankings or wordi
44
  ## Marketing standard
45
 
46
  The README is intentionally specific rather than superlative. It explains why the
47
- system can be fast and traceable, but it does not claim to be the fastest system in all
48
  settings. It shows the output contract before implementation details and keeps
49
  limitations close to the value proposition.
50
 
 
30
 
31
  | observed pattern | application in Relevance |
32
  | --- | --- |
33
+ | one-sentence value proposition | “Fast financial-news screening that shows its work.” |
34
  | show a useful result early | the README opens with real quickstart stdout |
35
  | put runnable code before exhaustive reference | a complete one-article example appears near the top |
36
  | explain performance mechanistically | Rust, automata, Rayon, sparse output, and Arrow are mapped to their practical effects |
 
44
  ## Marketing standard
45
 
46
  The README is intentionally specific rather than superlative. It explains why the
47
+ system can be fast and traceable, but it does not claim to be the fastest system in all
48
  settings. It shows the output contract before implementation details and keeps
49
  limitations close to the value proposition.
50
 
docs/release.md CHANGED
@@ -1,19 +1,19 @@
1
- # RANGE 0.1.0a1 release-candidate contract
2
 
3
- This directory is the authoritative source for the **RANGE** research alpha.
4
 
5
  ## Identity
6
 
7
- - Product: `RANGE`
8
- - Distribution: `range-news`
9
- - Import namespace: `range_news`
10
  - Version: `0.1.0a1`
11
  - Supported runtime: CPython 3.10
12
  - Supported backend: CPU
13
  - License: MIT
14
  - Publication state: not published
15
 
16
- The distribution name is descriptive while the product name remains RANGE.
17
 
18
  ## Required artifacts
19
 
@@ -30,7 +30,7 @@ A candidate build must produce:
30
 
31
  ## Required validation
32
 
33
- CI is deliberately small and manually invoked. The `range-news-release-candidate`
34
  workflow has only two jobs:
35
 
36
  1. **Linux package + Space smoke** validates source syntax, the Space scaffold, the
@@ -41,13 +41,13 @@ workflow has only two jobs:
41
  metadata and contents, clean installations, public contracts, PyArrow support, and
42
  README output.
43
 
44
- The broader Attention2Alpha and historical acceleration suites are not package-release
45
  gates. They remain available through the separate manual/main-only `repository-tests`
46
  workflow.
47
 
48
  ## Why the workflow is manual
49
 
50
- While RANGE remains inside a larger private monorepo, every feature-branch push can
51
  otherwise produce duplicate `push` and `pull_request` runs. Manual invocation keeps the
52
  PR readable and conserves hosted-runner minutes while preserving the checks that matter
53
  before publication or final technical sign-off.
@@ -73,7 +73,7 @@ Publishing requires a separate, explicit human decision after:
73
 
74
  `0.1.0a1` makes a narrow claim:
75
 
76
- > An exact, CPU-first, research-alpha package for traceable financial-news relevance
77
  > screening against a versioned general-economy taxonomy.
78
 
79
  It does not claim complete semantic recall, investment usefulness, universal
 
1
+ # RANGE 0.1.0a1 release-candidate contract
2
 
3
+ This directory is the authoritative source for the **RANGE** research alpha.
4
 
5
  ## Identity
6
 
7
+ - Product: `RANGE`
8
+ - Distribution: `range-news`
9
+ - Import namespace: `range_news`
10
  - Version: `0.1.0a1`
11
  - Supported runtime: CPython 3.10
12
  - Supported backend: CPU
13
  - License: MIT
14
  - Publication state: not published
15
 
16
+ The distribution name is descriptive while the product name remains RANGE.
17
 
18
  ## Required artifacts
19
 
 
30
 
31
  ## Required validation
32
 
33
+ CI is deliberately small and manually invoked. The `range-news-release-candidate`
34
  workflow has only two jobs:
35
 
36
  1. **Linux package + Space smoke** validates source syntax, the Space scaffold, the
 
41
  metadata and contents, clean installations, public contracts, PyArrow support, and
42
  README output.
43
 
44
+ The broader parent research project and historical acceleration suites are not package-release
45
  gates. They remain available through the separate manual/main-only `repository-tests`
46
  workflow.
47
 
48
  ## Why the workflow is manual
49
 
50
+ While RANGE remains inside a larger private monorepo, every feature-branch push can
51
  otherwise produce duplicate `push` and `pull_request` runs. Manual invocation keeps the
52
  PR readable and conserves hosted-runner minutes while preserving the checks that matter
53
  before publication or final technical sign-off.
 
73
 
74
  `0.1.0a1` makes a narrow claim:
75
 
76
+ > An exact, CPU-first, research-alpha package for traceable financial-news relevance
77
  > screening against a versioned general-economy taxonomy.
78
 
79
  It does not claim complete semantic recall, investment usefulness, universal
docs/taxonomy.md CHANGED
@@ -1,12 +1,12 @@
1
  # The bundled general-economy taxonomy
2
 
3
- The default policy in RANGE is a conservative, offline taxonomy for economic and
4
  financial-news screening. It is not a portfolio definition and it is not a sentiment
5
  or trading model.
6
 
7
  ## Decision layers
8
 
9
- Each article is processed through four traceable layers:
10
 
11
  1. **Observation** — literal text spans are matched by effective-dated aliases,
12
  normalized phrases, context-qualified acronyms, or conjunction rules.
@@ -64,7 +64,7 @@ guess a security or broad benchmark when no explicit relationship exists.
64
 
65
  ## Direction neutrality
66
 
67
- RANGE answers whether evidence is connected to a maintained economic or market
68
  concept. It does not infer whether the evidence is:
69
 
70
  - bullish or bearish;
@@ -103,7 +103,7 @@ documented America/New_York convention.
103
  ## Inspect the taxonomy
104
 
105
  ```python
106
- from range_news import GeneralEconomyTaxonomy
107
 
108
  taxonomy = GeneralEconomyTaxonomy()
109
 
@@ -117,7 +117,7 @@ print(taxonomy.lexical_rule_ids)
117
  ## Copy and edit the complete bundle
118
 
119
  ```python
120
- from range_news import RelevanceEngine, copy_default_taxonomy
121
 
122
  path = copy_default_taxonomy("my_taxonomy")
123
  # Edit my_taxonomy/taxonomy.yml and my_taxonomy/catalogs/*.yml.
@@ -143,7 +143,7 @@ Unknown fields fail closed. Existing files are not overwritten unless
143
  new immutable taxonomy objects and never mutate the bundled default.
144
 
145
  ```python
146
- from range_news import GeneralEconomyTaxonomy
147
 
148
 
149
  def add_indicator(builder):
 
1
  # The bundled general-economy taxonomy
2
 
3
+ The default policy in RANGE is a conservative, offline taxonomy for economic and
4
  financial-news screening. It is not a portfolio definition and it is not a sentiment
5
  or trading model.
6
 
7
  ## Decision layers
8
 
9
+ Each article is processed through four traceable layers:
10
 
11
  1. **Observation** — literal text spans are matched by effective-dated aliases,
12
  normalized phrases, context-qualified acronyms, or conjunction rules.
 
64
 
65
  ## Direction neutrality
66
 
67
+ RANGE answers whether evidence is connected to a maintained economic or market
68
  concept. It does not infer whether the evidence is:
69
 
70
  - bullish or bearish;
 
103
  ## Inspect the taxonomy
104
 
105
  ```python
106
+ from range_news import GeneralEconomyTaxonomy
107
 
108
  taxonomy = GeneralEconomyTaxonomy()
109
 
 
117
  ## Copy and edit the complete bundle
118
 
119
  ```python
120
+ from range_news import RelevanceEngine, copy_default_taxonomy
121
 
122
  path = copy_default_taxonomy("my_taxonomy")
123
  # Edit my_taxonomy/taxonomy.yml and my_taxonomy/catalogs/*.yml.
 
143
  new immutable taxonomy objects and never mutate the bundled default.
144
 
145
  ```python
146
+ from range_news import GeneralEconomyTaxonomy
147
 
148
 
149
  def add_indicator(builder):
docs/technical-signoff.md CHANGED
@@ -1,4 +1,4 @@
1
- # RANGE 0.1.0a1 technical sign-off
2
 
3
  This document defines the bounded technical sign-off for the unpublished research
4
  alpha. It does not authorize publication, tagging, release creation, Space deployment,
@@ -6,22 +6,22 @@ or merge.
6
 
7
  ## Identity
8
 
9
- - Product: **RANGE**
10
- - Distribution: `range-news`
11
- - Python namespace: `range_news`
12
- - Native Rust crate: `range_news_native`
13
  - Version: `0.1.0a1`
14
  - Runtime: CPython 3.10
15
  - Shipping backend: exact CPU/Rust
16
 
17
- Shipping source, wheels, and source distributions must not contain retired package
18
- identifiers. The repository URL may retain the parent repository's historical name;
19
- that is not a package identity.
20
 
21
  ## Required current-head evidence
22
 
23
  Technical sign-off intentionally has only two required jobs. Manually run the
24
- `range-news-release-candidate` workflow on the exact commit being reviewed and require:
25
 
26
  1. **Linux package + Space smoke** — source validation, Rust lock/format checks, wheel
27
  and source-distribution construction, metadata and archive inspection, clean
@@ -32,8 +32,8 @@ Technical sign-off intentionally has only two required jobs. Manually run the
32
  metadata/archive inspection, clean installs, package contracts, PyArrow coverage,
33
  and README output verification.
34
 
35
- The full Attention2Alpha deterministic suite and historical acceleration suite are not
36
- RANGE package sign-off gates. They remain available in `repository-tests` for a
37
  manual run and execute after integration to `main`.
38
 
39
  A success recorded for an older commit is not sufficient.
 
1
+ # RANGE 0.1.0a1 technical sign-off
2
 
3
  This document defines the bounded technical sign-off for the unpublished research
4
  alpha. It does not authorize publication, tagging, release creation, Space deployment,
 
6
 
7
  ## Identity
8
 
9
+ - Product: **RANGE**
10
+ - Distribution: `range-news`
11
+ - Python namespace: `range_news`
12
+ - Native Rust crate: `range_news_native`
13
  - Version: `0.1.0a1`
14
  - Runtime: CPython 3.10
15
  - Shipping backend: exact CPU/Rust
16
 
17
+ Shipping source, wheels, and source distributions must not contain retired package
18
+ identifiers. The repository URL may retain the parent repository's historical name;
19
+ that is not a package identity.
20
 
21
  ## Required current-head evidence
22
 
23
  Technical sign-off intentionally has only two required jobs. Manually run the
24
+ `range-news-release-candidate` workflow on the exact commit being reviewed and require:
25
 
26
  1. **Linux package + Space smoke** — source validation, Rust lock/format checks, wheel
27
  and source-distribution construction, metadata and archive inspection, clean
 
32
  metadata/archive inspection, clean installs, package contracts, PyArrow coverage,
33
  and README output verification.
34
 
35
+ The full parent research project deterministic suite and historical acceleration suite are not
36
+ RANGE package sign-off gates. They remain available in `repository-tests` for a
37
  manual run and execute after integration to `main`.
38
 
39
  A success recorded for an older commit is not sufficient.
rust/core.rs CHANGED
The diff for this file is too large to render. See raw diff
 
rust/lib.rs CHANGED
@@ -200,7 +200,7 @@ fn facts_to_record_batch(rows: Vec<FactRow>) -> Result<RecordBatch, String> {
200
  RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).map_err(|error| error.to_string())
201
  }
202
 
203
- #[pyclass(frozen, module = "range_news._native")]
204
  struct NativeArrowCompactFactScanner {
205
  inner: NativeCompactFactScanner,
206
  }
@@ -276,13 +276,13 @@ impl NativeArrowCompactFactScanner {
276
  let batch_rows: Result<Vec<FactRow>, String> = pool.install(|| {
277
  (0..batch.num_rows())
278
  .into_par_iter()
279
- .map_init(ScanScratch::default, |scratch, index| {
280
- let epoch = timestamps.value_microseconds(index)?;
281
- Ok(scan_one(
282
- &inner.automaton,
283
- &inner.pattern_consumers,
284
- &inner.consumers,
285
- &inner.exact_specs,
286
  &inner.aliases,
287
  &inner.lexical,
288
  &inner.contexts,
@@ -309,9 +309,9 @@ impl NativeArrowCompactFactScanner {
309
 
310
  let record_batch = facts_to_record_batch(result.map_err(PyValueError::new_err)?)
311
  .map_err(PyValueError::new_err)?;
312
- Ok(PyRecordBatch::new(record_batch))
313
- }
314
- }
315
 
316
  #[pymodule]
317
  fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> {
 
200
  RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).map_err(|error| error.to_string())
201
  }
202
 
203
+ #[pyclass(frozen, module = "range_news._native")]
204
  struct NativeArrowCompactFactScanner {
205
  inner: NativeCompactFactScanner,
206
  }
 
276
  let batch_rows: Result<Vec<FactRow>, String> = pool.install(|| {
277
  (0..batch.num_rows())
278
  .into_par_iter()
279
+ .map_init(ScanScratch::default, |scratch, index| {
280
+ let epoch = timestamps.value_microseconds(index)?;
281
+ Ok(scan_one(
282
+ &inner.automaton,
283
+ &inner.pattern_consumers,
284
+ &inner.consumers,
285
+ &inner.exact_specs,
286
  &inner.aliases,
287
  &inner.lexical,
288
  &inner.contexts,
 
309
 
310
  let record_batch = facts_to_record_batch(result.map_err(PyValueError::new_err)?)
311
  .map_err(PyValueError::new_err)?;
312
+ Ok(PyRecordBatch::new(record_batch))
313
+ }
314
+ }
315
 
316
  #[pymodule]
317
  fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> {
vendor/range_news/_native.cpython-310-x86_64-linux-gnu.so CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:45e05c00ab729ead671436ed8bd392c016dfa1e44ef42fdfade758902b1519d1
3
- size 15250792
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e05b832a8b33169dd988b4bc88a36bfb1f5b40f70676a09ecda727961d123c9c
3
+ size 15254376