assay-engine 0.5.0.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
assay/weighted_mean.py ADDED
@@ -0,0 +1,109 @@
1
+ """Normalized positive weighted-mean composition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from assay.composite import finite_output, inputs_hash, interval_or_none, left_add
6
+ from assay.contracts import (
7
+ Component,
8
+ ExplainedComponent,
9
+ Interval,
10
+ Method,
11
+ Operation,
12
+ ScoreResult,
13
+ WeightedMeanRequest,
14
+ )
15
+ from assay.normalize import normalize
16
+
17
+
18
+ def _weight(component: Component) -> float:
19
+ if component.weight is None: # pragma: no cover - the request contract rejects this
20
+ raise AssertionError("validated weighted component has no weight")
21
+ return component.weight
22
+
23
+
24
+ def _total_weight(request: WeightedMeanRequest) -> float:
25
+ return left_add(_weight(component) for component in request.components)
26
+
27
+
28
+ def _explain(
29
+ component: Component, spec: WeightedMeanRequest, coefficient: float
30
+ ) -> ExplainedComponent:
31
+ normalized = normalize(component.value, component.scale, spec.clamp)
32
+ contribution = finite_output(normalized * coefficient)
33
+ return ExplainedComponent(
34
+ id=component.id,
35
+ raw=component.value,
36
+ normalized=normalized,
37
+ declared_weight=_weight(component),
38
+ operation=Operation.ADD,
39
+ coefficient=coefficient,
40
+ contribution=contribution,
41
+ contribution_interval=_contribution_interval(component, spec, coefficient),
42
+ )
43
+
44
+
45
+ def _normalized_bounds(component: Component, request: WeightedMeanRequest) -> tuple[float, float]:
46
+ interval = component.interval
47
+ if interval is None:
48
+ point = normalize(component.value, component.scale, request.clamp)
49
+ return point, point
50
+ first = normalize(interval.low, component.scale, request.clamp)
51
+ second = normalize(interval.high, component.scale, request.clamp)
52
+ return min(first, second), max(first, second)
53
+
54
+
55
+ def _contribution_interval(
56
+ component: Component, request: WeightedMeanRequest, coefficient: float
57
+ ) -> Interval | None:
58
+ if component.interval is None:
59
+ return None
60
+ low, high = _normalized_bounds(component, request)
61
+ return interval_or_none(finite_output(low * coefficient), finite_output(high * coefficient))
62
+
63
+
64
+ def _coefficients(request: WeightedMeanRequest, total: float) -> tuple[float, ...]:
65
+ return tuple(finite_output(_weight(component) / total) for component in request.components)
66
+
67
+
68
+ def _weighted_bound(rows: tuple[ExplainedComponent, ...], *, high: bool) -> float:
69
+ index = 1 if high else 0
70
+ bounds = (
71
+ row.contribution
72
+ if row.contribution_interval is None
73
+ else (row.contribution_interval.low, row.contribution_interval.high)[index]
74
+ for row in rows
75
+ )
76
+ return left_add(bounds)
77
+
78
+
79
+ def _result_interval(rows: tuple[ExplainedComponent, ...]) -> Interval | None:
80
+ if not any(row.contribution_interval is not None for row in rows):
81
+ return None
82
+ low = _weighted_bound(rows, high=False)
83
+ high = _weighted_bound(rows, high=True)
84
+ return interval_or_none(low, high)
85
+
86
+
87
+ def _rows(request: WeightedMeanRequest, total: float) -> tuple[ExplainedComponent, ...]:
88
+ coefficients = _coefficients(request, total)
89
+ return tuple(
90
+ _explain(component, request, coefficient)
91
+ for component, coefficient in zip(request.components, coefficients, strict=True)
92
+ )
93
+
94
+
95
+ def weighted_mean(request: WeightedMeanRequest) -> ScoreResult:
96
+ """Compose a validated normalized weighted mean in declared input order."""
97
+ validated = WeightedMeanRequest.model_validate(request)
98
+ total = _total_weight(validated)
99
+ rows = _rows(validated, total)
100
+ return ScoreResult(
101
+ method=Method(id=validated.method, version=validated.method_version),
102
+ score=left_add(row.contribution for row in rows),
103
+ interval=_result_interval(rows),
104
+ clamp=validated.clamp,
105
+ intercept=None,
106
+ weight_total=total,
107
+ components=rows,
108
+ inputs_hash=inputs_hash(validated),
109
+ )
@@ -0,0 +1,250 @@
1
+ Metadata-Version: 2.4
2
+ Name: assay-engine
3
+ Version: 0.5.0.dev2
4
+ Summary: Explainable composition of measurements recorded on heterogeneous scales.
5
+ Project-URL: Homepage, https://github.com/hseshadr/assay
6
+ Project-URL: Repository, https://github.com/hseshadr/assay
7
+ Project-URL: Issues, https://github.com/hseshadr/assay/issues
8
+ Author-email: Harish Seshadri <harish.seshadri@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.13
18
+ Requires-Dist: pydantic>=2.11
19
+ Provides-Extra: cli
20
+ Requires-Dist: typer>=0.27; extra == 'cli'
21
+ Provides-Extra: metrics
22
+ Requires-Dist: ir-measures>=0.4.3; extra == 'metrics'
23
+ Requires-Dist: numpy>=2.5; extra == 'metrics'
24
+ Requires-Dist: pydantic-settings>=2.11; extra == 'metrics'
25
+ Requires-Dist: scikit-learn>=1.9; extra == 'metrics'
26
+ Requires-Dist: scipy>=1.18; extra == 'metrics'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Assay
30
+
31
+ > **TL;DR:** Assay combines measurements recorded on different scales into one explainable score while preserving every input, transformation, and contribution.
32
+
33
+ Assay is a small scoring engine for formulas you can write down and replay. Give it
34
+ measurements, their native scales, and one explicit combining method. It returns the
35
+ score and the arithmetic behind every row.
36
+
37
+ ## Installation status
38
+
39
+ > **Status:** `assay-engine` 0.5.0.dev2 and `@edgeproc/assay` 0.5.0-dev.2 are local split candidates. Neither package is published.
40
+
41
+ The future authorized registry commands are `pip install assay-engine` and
42
+ `npm install @edgeproc/assay`. They are shown for identity only; do not run them until
43
+ a release is explicitly authorized. The runnable candidate path builds from this
44
+ checkout.
45
+
46
+ ## Run the Northstar example
47
+
48
+ From the checkout root, run:
49
+
50
+ ```bash
51
+ bash examples/run_composite.sh
52
+ ```
53
+
54
+ The script builds the real Python wheel and npm tarball, installs each in an isolated
55
+ temporary environment, computes through both public package surfaces, checks every
56
+ typed field and binary64 value against the committed oracle, and prints one explanation:
57
+
58
+ ```text
59
+ Northstar weighted score: 0.92
60
+ Method: weighted_mean @ northstar.2026-08-12
61
+ Interval: null — all inputs are deterministic
62
+
63
+ security 19/20 -> 0.950000 × 0.20 = 0.19
64
+ privacy 15/15 -> 1.000000 × 0.15 = 0.15
65
+ reliability 15/15 -> 1.000000 × 0.15 = 0.15
66
+ performance 12/15 -> 0.800000 × 0.15 = 0.12
67
+ correctness 15/15 -> 1.000000 × 0.15 = 0.15
68
+ clarity 14/15 -> 0.933333 × 0.15 = 0.14
69
+ production 2/5 -> 0.400000 × 0.05 = 0.02
70
+
71
+ Total: 0.92
72
+ inputs_hash: sha256:0266b1c59c97bacf85dc945685c55bb4386856b525249c7d5663a8edf020ba06
73
+ Parity: Python and TypeScript fields and values match
74
+ ```
75
+
76
+ This is uncapped arithmetic only. Northstar hard caps, evidence grades, release
77
+ decisions, and other product policies remain outside Assay.
78
+
79
+ ## How the score is calculated
80
+
81
+ The example declares seven components on three native scales. Assay first normalizes
82
+ each value to 0–1, divides its positive weight by the declared total of 100, then adds
83
+ the contributions in declaration order:
84
+
85
+ ```text
86
+ security: (19 - 0) / (20 - 0) × 20/100 = 0.19
87
+ privacy: (15 - 0) / (15 - 0) × 15/100 = 0.15
88
+ reliability: (15 - 0) / (15 - 0) × 15/100 = 0.15
89
+ performance: (12 - 0) / (15 - 0) × 15/100 = 0.12
90
+ correctness: (15 - 0) / (15 - 0) × 15/100 = 0.15
91
+ clarity: (14 - 0) / (15 - 0) × 15/100 = 0.14
92
+ production: ( 2 - 0) / ( 5 - 0) × 5/100 = 0.02
93
+ total: 0.92
94
+ ```
95
+
96
+ Assay's portable typed API supports exactly three composition methods:
97
+
98
+ - `weighted_mean` normalizes components, converts positive declared weights into
99
+ coefficients that sum to one, and adds their contributions.
100
+ - `additive` applies each raw term's explicit add or subtract operation and coefficient,
101
+ then optionally clamps the final total.
102
+ - `minimum` normalizes components and selects the first lowest value, making declaration
103
+ order the tie-breaker.
104
+
105
+ The method is chosen by the application because it owns the formula. Assay never
106
+ silently replaces a shipped formula with an average. See
107
+ [Methods](https://github.com/hseshadr/assay/blob/main/docs/METHODS.md) for validation,
108
+ uncertainty, and exact arithmetic rules.
109
+
110
+ ### Legacy Python compatibility
111
+
112
+ The wheel retains a Python-only migration adapter at the deep import `assay.composite`:
113
+ `SubScore` plus `composite(...)`. It is not exported from the package root, does not
114
+ return the typed method or `inputs_hash` fields, and has no TypeScript equivalent. For
115
+ all new code, use package-root `parse_request()` and `compose()` with one of the three
116
+ portable methods above.
117
+
118
+ Every result field is explicit:
119
+
120
+ | Field | Meaning |
121
+ |---|---|
122
+ | `schema` | Serialized result contract, currently `assay.result/v1`. |
123
+ | `method.id` | One of the three portable typed composition methods. |
124
+ | `method.version` | Caller-declared provenance for this formula revision. |
125
+ | `score` | Final finite binary64 result. |
126
+ | `interval` | Propagated uncertainty bounds, or `null` for deterministic inputs. |
127
+ | `clamp` | Requested boundary policy, or `null` only for unclamped additive scoring. |
128
+ | `intercept` | Additive starting value; `null` for the other methods. |
129
+ | `weight_total` | Weighted-mean declared weight total; otherwise `null`. |
130
+ | `components` | Ordered arithmetic rows retained for replay. |
131
+ | `id` | Stable input identifier for one row. |
132
+ | `raw` | Original finite input value; it may be sensitive. |
133
+ | `normalized` | 0–1 transformed value, or `null` for additive rows. |
134
+ | `declared_weight` | Original weighted-mean weight, otherwise `null`. |
135
+ | `operation` | `add` or `subtract`; normalized methods use `add`. |
136
+ | `coefficient` | Effective multiplier used for the row. |
137
+ | `contribution` | Pre-operation product: `normalized × coefficient` or `raw × coefficient`. For additive rows, `operation` controls how it changes the running total. |
138
+ | `contribution_interval` | Row uncertainty contribution, or `null`. |
139
+ | `inputs_hash` | Order-preserving request fingerprint used for replay comparison. |
140
+ | `selected_component_id` | Minimum-method bottleneck ID; otherwise `null`. |
141
+
142
+ Python and TypeScript parity covers the three methods, typed field/value structure,
143
+ field and component order, IEEE-754 binary64 values, and the exact `inputs_hash`. It
144
+ does not promise byte-identical output from language-native JSON serializers; for
145
+ example, one serializer may spell the same number `19.0` and another `19`.
146
+
147
+ ## What this proves
148
+
149
+ For a validated request, the result exposes the selected method and version, preserves
150
+ the scored inputs in declaration order, shows every transformation and contribution,
151
+ and can be replayed under the same contract. The committed vectors prove the Python and
152
+ TypeScript composition surfaces agree semantically on all three methods and on the
153
+ exact request fingerprint.
154
+
155
+ ## What this does not prove
156
+
157
+ Assay does not prove input truth, completeness, fairness, freshness, authenticity,
158
+ policy compliance, or decision quality. `inputs_hash` is a deterministic fingerprint,
159
+ not authentication or tamper evidence. A caller-declared method version records
160
+ provenance; it does not validate the methodology.
161
+
162
+ Application-owned bands, thresholds, hard gates, fairness review, abstention policy,
163
+ release decisions, and other downstream decisions remain application-owned. Results
164
+ retain raw values, so callers must treat them according to the sensitivity of their
165
+ inputs.
166
+
167
+ ## Architecture
168
+
169
+ There are exactly two production source-to-artifact mappings:
170
+
171
+ ```text
172
+ src/assay/ ──> assay-engine wheel ──> import assay
173
+ ts/src/ ──> @edgeproc/assay npm tarball ──> import "@edgeproc/assay"
174
+ ```
175
+
176
+ `examples/`, `docs/`, `tests/`, and `testdata/` are repository support files, not
177
+ runtime packages. The Python package is the broader surface: composition is in the
178
+ base wheel, the command line uses the `cli` extra, and scientific calculators use the
179
+ `metrics` extra. The npm tarball provides composition plus a smaller set of optional
180
+ binary and ranking calculators.
181
+
182
+ This README is self-contained because the Python source distribution currently ships
183
+ it, but does not ship the repository's quickstart, docs, or examples. The detailed
184
+ [architecture](https://github.com/hseshadr/assay/blob/main/docs/ARCHITECTURE.md),
185
+ [operations contract](https://github.com/hseshadr/assay/blob/main/docs/OPERATIONS.md),
186
+ and [quickstart](https://github.com/hseshadr/assay/blob/main/QUICKSTART.md) are available
187
+ in the source checkout.
188
+
189
+ ## Use the local candidate directly
190
+
191
+ Python 3.13 code imports `assay` from the distribution named `assay-engine`:
192
+
193
+ ```python
194
+ from assay import compose, parse_request
195
+
196
+ request = parse_request(
197
+ {
198
+ "method": "minimum",
199
+ "method_version": "service-health.v1",
200
+ "components": [
201
+ {
202
+ "id": "availability",
203
+ "label": "Availability",
204
+ "value": 99.9,
205
+ "scale": {"minimum": 99.0, "maximum": 100.0, "direction": "higher_is_better"},
206
+ "interval": None,
207
+ "weight": None,
208
+ },
209
+ {
210
+ "id": "latency",
211
+ "label": "Latency",
212
+ "value": 180.0,
213
+ "scale": {"minimum": 100.0, "maximum": 500.0, "direction": "lower_is_better"},
214
+ "interval": None,
215
+ "weight": None,
216
+ },
217
+ ],
218
+ "clamp": "reject",
219
+ }
220
+ )
221
+
222
+ result = compose(request)
223
+ print(result.score, result.selected_component_id)
224
+ ```
225
+
226
+ The command line accepts typed JSON for `assay compose`, `assay measure`, and
227
+ `assay explain`. Build and installation commands for the unpublished checkout are in
228
+ the [quickstart](https://github.com/hseshadr/assay/blob/main/QUICKSTART.md).
229
+
230
+ ## Optional calculators
231
+
232
+ Python's optional scientific surface calculates typed binary-classification, ranking,
233
+ calibration, agreement, and uncertainty reports. TypeScript exposes a smaller binary
234
+ and ranking calculator set. Complete optional-metric parity is not claimed, and the
235
+ calculator resource ceilings do not limit core composition. See
236
+ [Methods](https://github.com/hseshadr/assay/blob/main/docs/METHODS.md) and
237
+ [Operations](https://github.com/hseshadr/assay/blob/main/docs/OPERATIONS.md) for the
238
+ exact boundary.
239
+
240
+ ## Optional integration
241
+
242
+ Assay computes scores; Avow seals evidence. They are separate products in separate repositories, and neither imports or requires the other. The already-published `avow` 0.4.1 and `@edgeproc/avow` 0.4.1 artifacts remain unchanged.
243
+
244
+ An application may pass an ordinary Assay result to a separately selected evidence
245
+ system. That adapter belongs to the application or to its own versioned integration
246
+ package, never to either core scoring package.
247
+
248
+ ## License
249
+
250
+ MIT © Harish Seshadri
@@ -0,0 +1,30 @@
1
+ assay/__init__.py,sha256=1tioK-81DFP5WbzFHvBWYZWSw-JXJfVivWI1vK10O7E,1905
2
+ assay/_cli_app.py,sha256=G0F20IIzgDy7sQt3ELqS52jQAnFX0sHnQiAjqI3n_9s,3500
3
+ assay/_cli_io.py,sha256=Fj61uCzeV25OaofgL6YiNrocx1gWBgadUyxy3_uwPqs,3794
4
+ assay/_json.py,sha256=x3fsaJ_GA7QQVuS23uyU3o6i5YJtre3BrCxIGOrE8BQ,1096
5
+ assay/_optional.py,sha256=7reiuLc3Nv3Mq0GiA2XMELYipaRbTXB78RUNQVwBURo,1903
6
+ assay/_version.py,sha256=nWTQA8HHLTrhgO8J6vSrqriIp5mC8OUE_9b9jOexrQ8,136
7
+ assay/additive.py,sha256=45ZQmj555qv1yc8ltQ5W03zqYjoauRWKRXzANnrM_9Y,3526
8
+ assay/agreement.py,sha256=C_s9Dy9_10as1Hv-UNWhF2UeylczSvYc8hf2HISgXbk,13229
9
+ assay/calibration.py,sha256=TyIaLzhhTIKXsi9Kbj-sOkkiM_nqeGpVHz6V-9LIaj8,5668
10
+ assay/cli.py,sha256=kMtTSb58KgbZNUIjSrVY_-iopeaUVrY4H1Xiy26w41E,1743
11
+ assay/compose.py,sha256=JdN_bxGOT9PJNsLAccZ47KRGf1sa8eKmltXKg2BG-Ps,927
12
+ assay/composite.py,sha256=FZBs-X_0mJyUBNuI8U67atuAIBPpNnkSogPGAhwYLYI,6890
13
+ assay/contracts.py,sha256=2whtkcpoSYeZ-6gUGUoi4RrgEN8OyjwJydv65bjBvpE,28291
14
+ assay/errors.py,sha256=NqeyhOQ7LANEX-xvmDxkGyeCxzjf4iYX_0eODXIhX7Y,4663
15
+ assay/limits.py,sha256=VoelwVfVK98NVZBpLJOV7j2bcGReA719kv5SWCOKVdc,372
16
+ assay/measurement.py,sha256=DxRDQ0AZEVH-c4eBJKlUvuhTlIxRDhyJD-ga1lDK8ao,44999
17
+ assay/metrics.py,sha256=ZhJW7ukWXWBW53Cedbuv3-CynDXsb0uO918tIVusGig,8431
18
+ assay/minimum.py,sha256=LHt_QQGWfwGGocmQw8y8gQrcRYPVTE2ifvTSLBEijf0,2880
19
+ assay/models.py,sha256=vJ2X7EJzY6ftuuMXSpzDmoC57n4ECydXNar98HmVfds,3256
20
+ assay/normalize.py,sha256=PmGp5loJ7D-UqB0Pao2G1WhcBsCBiQ2PsAB0FVR2zHg,2369
21
+ assay/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ assay/ranking.py,sha256=dqs8W2gUHyreGl6FykySUJR2r-V8qhYHriVLMu5GxqE,14922
23
+ assay/settings.py,sha256=iXYAeB1tn5CUzSNErPQkGGfaihbGcKc6r4kkx5HUCo0,3223
24
+ assay/uncertainty.py,sha256=WC_wi2UWy6JfaKBcazC8ASX6gxXuENOZnBoLP9rNo-g,5907
25
+ assay/weighted_mean.py,sha256=eAnpw8UG2bSglFajlA9ivhlMwiPydFaXlkgaGLiPsIg,3882
26
+ assay_engine-0.5.0.dev2.dist-info/METADATA,sha256=rqAtIlFmXmPCpW6rpDzwP03gi0d1SF7sXCYqNCFERs0,11047
27
+ assay_engine-0.5.0.dev2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
+ assay_engine-0.5.0.dev2.dist-info/entry_points.txt,sha256=D85UiUsdqfsJPXOkBks8wzt5AC_AhXysNQsRIjbxYvE,41
29
+ assay_engine-0.5.0.dev2.dist-info/licenses/LICENSE,sha256=DKAeGDB_tj9gKaHjTZRtKQTnm6jf2sgCNl5o9H09baE,1072
30
+ assay_engine-0.5.0.dev2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ assay = assay.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harish Seshadri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.