neomundi 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. neomundi-2.0.0/KNOWN_LIMITATIONS.md +857 -0
  2. neomundi-2.0.0/LICENSE +201 -0
  3. neomundi-2.0.0/LICENSE_NOTICE.md +17 -0
  4. neomundi-2.0.0/MANIFEST.in +12 -0
  5. neomundi-2.0.0/PILOT_INTEGRATION_GUIDE.md +721 -0
  6. neomundi-2.0.0/PKG-INFO +188 -0
  7. neomundi-2.0.0/README.md +165 -0
  8. neomundi-2.0.0/docs/AUDIT_SDK_V2.md +62 -0
  9. neomundi-2.0.0/docs/COMPATIBILITY_350.md +130 -0
  10. neomundi-2.0.0/docs/CONTRAT_API.md +347 -0
  11. neomundi-2.0.0/docs/LIMITES.md +524 -0
  12. neomundi-2.0.0/docs/MIGRATION_V2.md +30 -0
  13. neomundi-2.0.0/docs/PYPI_RELEASE_READINESS.md +48 -0
  14. neomundi-2.0.0/docs/QUALIFICATION_LIVE_SDK_V2.md +41 -0
  15. neomundi-2.0.0/docs/STREAMING.md +124 -0
  16. neomundi-2.0.0/docs/STREAMING_FR.md +135 -0
  17. neomundi-2.0.0/docs/VERIFICATION_V2.md +61 -0
  18. neomundi-2.0.0/examples/01_premiere_mesure.py +125 -0
  19. neomundi-2.0.0/examples/02_mesure_partielle.py +148 -0
  20. neomundi-2.0.0/examples/03_gestion_erreurs.py +248 -0
  21. neomundi-2.0.0/examples/04_streaming_sse.py +56 -0
  22. neomundi-2.0.0/examples/90_cle_reelle.py.disabled +119 -0
  23. neomundi-2.0.0/pyproject.toml +66 -0
  24. neomundi-2.0.0/setup.cfg +4 -0
  25. neomundi-2.0.0/src/neomundi/__init__.py +165 -0
  26. neomundi-2.0.0/src/neomundi/_retry.py +114 -0
  27. neomundi-2.0.0/src/neomundi/client.py +637 -0
  28. neomundi-2.0.0/src/neomundi/errors.py +357 -0
  29. neomundi-2.0.0/src/neomundi/models.py +1275 -0
  30. neomundi-2.0.0/src/neomundi/py.typed +0 -0
  31. neomundi-2.0.0/src/neomundi/streaming.py +501 -0
  32. neomundi-2.0.0/src/neomundi.egg-info/PKG-INFO +188 -0
  33. neomundi-2.0.0/src/neomundi.egg-info/SOURCES.txt +62 -0
  34. neomundi-2.0.0/src/neomundi.egg-info/dependency_links.txt +1 -0
  35. neomundi-2.0.0/src/neomundi.egg-info/requires.txt +4 -0
  36. neomundi-2.0.0/src/neomundi.egg-info/top_level.txt +1 -0
  37. neomundi-2.0.0/tests/conftest.py +161 -0
  38. neomundi-2.0.0/tests/fixtures/errors/e401.json +1 -0
  39. neomundi-2.0.0/tests/fixtures/errors/e402.json +1 -0
  40. neomundi-2.0.0/tests/fixtures/errors/e413.json +1 -0
  41. neomundi-2.0.0/tests/fixtures/errors/e422.json +1 -0
  42. neomundi-2.0.0/tests/fixtures/errors/e429.json +1 -0
  43. neomundi-2.0.0/tests/fixtures/errors/e500.json +1 -0
  44. neomundi-2.0.0/tests/fixtures/errors/e503.json +1 -0
  45. neomundi-2.0.0/tests/fixtures/full_measurement.json +178 -0
  46. neomundi-2.0.0/tests/fixtures/gated_tier01.json +104 -0
  47. neomundi-2.0.0/tests/fixtures/no_check_attempted.json +110 -0
  48. neomundi-2.0.0/tests/fixtures/partial_measurement.json +175 -0
  49. neomundi-2.0.0/tests/fixtures/production_331/rest_complete_allow.json +202 -0
  50. neomundi-2.0.0/tests/fixtures/synthetic_drift.json +66 -0
  51. neomundi-2.0.0/tests/test_350_degraded_errors_synthetic.py +144 -0
  52. neomundi-2.0.0/tests/test_350_wire_extension_synthetic.py +87 -0
  53. neomundi-2.0.0/tests/test_audit_distribution.py +89 -0
  54. neomundi-2.0.0/tests/test_captured_fixtures.py +206 -0
  55. neomundi-2.0.0/tests/test_client.py +278 -0
  56. neomundi-2.0.0/tests/test_contract_drift.py +285 -0
  57. neomundi-2.0.0/tests/test_errors.py +305 -0
  58. neomundi-2.0.0/tests/test_examples.py +85 -0
  59. neomundi-2.0.0/tests/test_measurement_version_331.py +272 -0
  60. neomundi-2.0.0/tests/test_models.py +521 -0
  61. neomundi-2.0.0/tests/test_no_secret_leak.py +154 -0
  62. neomundi-2.0.0/tests/test_pypi_release_guard.py +42 -0
  63. neomundi-2.0.0/tests/test_retry.py +190 -0
  64. neomundi-2.0.0/tests/test_streaming.py +375 -0
@@ -0,0 +1,857 @@
1
+ # Known Limitations — NeoMundi Python SDK v2.0.0
2
+
3
+ [English](KNOWN_LIMITATIONS.md) · [Français](KNOWN_LIMITATIONS_FR.md)
4
+
5
+ **Status: v2.0.0 — Experimental / Pilot Evaluation**
6
+
7
+ Last updated: 2026-10-26; older backend observations remain dated below
8
+ Audited backend reference: commit `68d6057cf620e430e6488da3cfcec9c471c75ed6`, measurement version `3.3.0`
9
+ REST surface additionally verified against measurement version **`3.3.1`** (deployed commit `aeff13c`) — see §6
10
+
11
+ This document is for developers evaluating the SDK in a pilot context. It
12
+ states what works, what was verified and how, and which backend defects
13
+ were observed. Nothing here is softened: a measurement instrument that
14
+ hides its limits cannot be trusted with the measurements it reports.
15
+
16
+ > **Reading rule.** Where this document says *observed*, a value was
17
+ > recorded from a running instance and the conditions are stated. Where it
18
+ > says *derived from code*, the statement comes from reading the backend
19
+ > source at the commit above. The two are never mixed.
20
+
21
+ > **V2 update:** `stream()` / `govern_stream()` now support SSE with a
22
+ > separate terminal result and typed interruption errors. Historical
23
+ > backend findings below are not claims of a fresh production audit.
24
+ > See [the V2 audit](docs/AUDIT_SDK_V2.md).
25
+
26
+ ---
27
+
28
+ ## 1. SDK status
29
+
30
+ | | |
31
+ |---|---|
32
+ Version | `2.0.0` |
33
+ Maturity | **Experimental.** Suitable for pilot evaluation, not for production reliance. |
34
+ Scope | REST `POST /v1/govern` and synchronous SSE `POST /v1/govern/stream` |
35
+ Runtime dependency | `httpx` only |
36
+ Python | ≥ 3.9; V2 validated locally on 3.12; CI passed on 3.9, 3.11, 3.12 and 3.13 |
37
+ Distribution | **Not on PyPI.** Install from this public repository or an identified wheel. |
38
+ Licence | **Not settled** — see `LICENSE_NOTICE.md` |
39
+
40
+ ### What "locally validated" means, precisely
41
+
42
+ **Current V2 suite:** 282 passed, 12 skipped; CI passed on Python 3.9,
43
+ 3.11, 3.12 and 3.13. Live REST/SSE qualification remains pending; see
44
+ [VERIFICATION_V2.md](docs/VERIFICATION_V2.md).
45
+
46
+ The two scopes below are **historical, before the V2 additions**, not current
47
+ V2 test totals. Conflating these scopes would overstate what was verified.
48
+
49
+ | Scope | Count | What it establishes |
50
+ |---|---|---|
51
+ **Reference suite** (what CI runs) | **225 passed, 12 skipped** | the SDK behaves correctly against payloads **derived from reading** the backend contract, and against the sanitised `3.3.1` production payload shipped in `tests/fixtures/production_331/` |
52
+ **Local suite** (reference + captured payloads) | **347 passed** | the same, **plus** that the modelled contract matches what a running instance actually emitted |
53
+
54
+ The 12 skips are the `3.3.0` captured-payload tests. Those captures are
55
+ execution data and are **excluded from this repository**
56
+ (`tests/fixtures/captured/` is in `.gitignore`), so those tests skip
57
+ wherever the directory is absent — including CI. They are not optional
58
+ checks that were turned off; they are checks that require data this
59
+ repository deliberately does not carry.
60
+
61
+ The `3.3.1` payload is the exception, and deliberately so: it carries no
62
+ prompt, no response text and no evaluator identity, so it could be
63
+ sanitised and shipped. What was replaced is listed field by field in its
64
+ `_fixture_redacted` key.
65
+
66
+ ### Deployment compatibility — the central caveat
67
+
68
+ The SDK's models were derived from the backend's request and response
69
+ models **at one named commit**. No frozen OpenAPI specification exists
70
+ server-side, and `/openapi.json` is disabled in production builds.
71
+
72
+ Compatibility was verified against **a local instance built from that
73
+ commit**, and the REST contract was additionally verified against
74
+ measurement version **`3.3.1`** by source comparison and by replaying a
75
+ real production response (§6). It has **not** been verified by this SDK
76
+ **calling** any hosted deployment. A deployment serving a
77
+ `measurement_version` outside the verified list, or carrying fields added
78
+ since, may behave differently.
79
+
80
+ ```python
81
+ import neomundi
82
+ result = client.govern(...)
83
+
84
+ served = result.audit.measurement_version
85
+
86
+ if served not in neomundi.API_CONTRACT["verified_measurement_versions"]:
87
+ # Unverified contract: fields may have changed meaning. Stop here.
88
+ ...
89
+ elif served != neomundi.API_CONTRACT["measurement_version"]:
90
+ # Parsing is safe, but the series is not comparable with one produced
91
+ # under a different measurement version. Record `served` alongside
92
+ # every observation.
93
+ ...
94
+ ```
95
+
96
+ **The two checks answer different questions, and collapsing them is the
97
+ mistake this split exists to prevent.** The first asks *can this client
98
+ read the response correctly* — a transport question. The second asks *may
99
+ these two numbers sit in the same series* — a scientific one. `3.3.1`
100
+ passes the first and fails the second against a `3.3.0` series.
101
+
102
+ ---
103
+
104
+ ## 2. Supported functionality
105
+
106
+ ### Verified by execution against a running instance
107
+
108
+ A campaign of ten REST calls was run against a local instance built from
109
+ the reference commit, at tier `TIER_02`, with all three evaluators live
110
+ (Llama Guard 3 1B, nomic-embed-text, Llama 3.2 3B on a local Ollama).
111
+
112
+ | Capability | Result |
113
+ |---|---|
114
+ `POST /v1/govern`, request construction and response parsing | 10/10 calls parsed |
115
+ **Zero type discrepancies** (`parse_warnings`) | 0 across all captured payloads |
116
+ **Zero unmodelled fields** (`unknown_fields`) | 0 across all captured payloads |
117
+ `measurement_version` served | `3.3.0`, matching the SDK's declared contract |
118
+ Round-trip fidelity (`to_dict()` returns the payload unchanged) | verified on every captured payload |
119
+ Root aliases agree with their blocks (`g_score`/`g_final`) | verified on every captured payload |
120
+ Coverage is re-derivable from the published counters | verified on every captured payload |
121
+ Three decision bases exercised | `measured_thresholds`, `incomplete_measurement`, `no_check_attempted` |
122
+ **Degraded path**: all three evaluators unreachable | aggregates `null`, decision `FLAG`, basis `incomplete_measurement`, coverage `0/3`, each cause named (`moderation_service_unavailable`, `coherence_service_unavailable`, `hallucination_judge_unavailable`) |
123
+ Partial coverage | observed at `2/3` (missing coherence) and `0/3` |
124
+ No secrets in responses | the redaction pass found **nothing to redact** in any captured payload |
125
+
126
+ ### Verified by replaying a production response served under `3.3.1`
127
+
128
+ One response really emitted by the production deployment on **2026-10-21**
129
+ under `measurement_version 3.3.1` was replayed through the client over a
130
+ simulated transport on **2026-10-22**. A sanitised copy ships as
131
+ `tests/fixtures/production_331/rest_complete_allow.json` and is exercised
132
+ by `tests/test_measurement_version_331.py`.
133
+
134
+ | Checked on the `3.3.1` payload | Result |
135
+ |---|---|
136
+ `parse_warnings` | **0** |
137
+ `unknown_fields` — a root field `3.3.1` would have added | **0** |
138
+ Round-trip (`to_dict()` returns the payload unchanged) | exact |
139
+ Root aliases agree with their blocks (`g_score`, `g_final`) | yes |
140
+ Coverage re-derivable from the published counters (`3/3 → 1.0`) | yes |
141
+ Four required blocks present | yes |
142
+ Twelve per-signal provenance fields exposed, `null` included | yes |
143
+ `trajectory_points` and `provider` still `None` on REST | yes |
144
+ `quality.stability_score` + `metrics.stability_score` = 1.0 — the inverted homonyms still coexist | yes |
145
+ `baseline.delta_E` still equals `reference − observed` (the implementation, not the description) | yes |
146
+ `decision` / `decision_basis` | `ALLOW` / `measured_thresholds` |
147
+
148
+ **This is a replay, not a live call.** The SDK did not reach the hosted
149
+ service: the payload was served to it by `httpx.MockTransport`. What it
150
+ establishes is that the contract this client models is the contract the
151
+ production deployment emitted — not that an end-to-end call succeeds.
152
+ See §6 for what remains missing and why.
153
+
154
+ **One finding worth carrying into any comparison.** On that response, the
155
+ three configuration fingerprints (`prompt_template_hash`,
156
+ `inference_config_hash`, `config_fingerprint`) were populated **only for
157
+ the two signals produced by the factuality judge**. `stability_score`,
158
+ `coherence_score` and `semantic_risk` carried `status: "measured"` with
159
+ all three fingerprints `null`. Two observations therefore cannot be shown
160
+ to share an evaluator configuration on those three signals — the data to
161
+ check it is not published. The SDK exposes the `null`s; it invents
162
+ nothing. A test holds this observation so that a change to it is noticed.
163
+
164
+ ### Verified by the test suite, without a running instance
165
+
166
+ | Capability | Where |
167
+ |---|---|
168
+ `X-API-Key` authentication; key **format** validated before any network call | `test_client.py` |
169
+ Nine error families: 401, 402, 413, 422, 429, 5xx, timeout, network, invalid response | `test_errors.py` (39 tests) |
170
+ **No automatic retry**, on any status; a timeout is never retried even with an explicit policy | `test_retry.py` (26 tests) |
171
+ `null` preserved; `None` never substituted by `0.0`; `None` distinct from `[]` | `test_models.py` |
172
+ Unknown fields preserved in `raw` and listed, never fatal | `test_models.py` |
173
+ No numeric default anywhere in the sources (AST-checked, and the guard is itself tested) | `test_contract_drift.py` |
174
+ No backend import, no undeclared dependency, no embedded threshold | `test_contract_drift.py` |
175
+ No key in REST `repr`, exception messages, logs, request body or URL; repository-wide key sweep | `test_no_secret_leak.py` (11 tests) |
176
+ The three historical REST examples run and exit 0; V2 adds an SSE example | `test_examples.py` |
177
+
178
+ ### Not verified
179
+
180
+ - **any call made by this SDK to a hosted deployment** — no end-to-end
181
+ call has ever been executed, under `3.3.0` or `3.3.1`. The credential
182
+ needed for one was not available to the verification session;
183
+ - consequently: authentication, error statuses, timeouts, rate limiting
184
+ and quota behaviour **against production** — all nine error families
185
+ are exercised against a simulated transport only;
186
+ - behaviour under concurrency or sustained load;
187
+ - behaviour against a measurement version outside
188
+ `API_CONTRACT["verified_measurement_versions"]`;
189
+ - the `3.3.1` **incomplete-measurement** path: the single `3.3.1`
190
+ response available is a complete measurement, so the withheld-aggregate
191
+ behaviour has been re-verified at `3.3.1` **by source identity only**,
192
+ not by observation;
193
+ - repeat-call variance of the factuality judge (see SL-005).
194
+
195
+ ---
196
+
197
+ ## 3. Known backend limitations
198
+
199
+ These are **backend** defects observed or derived at the reference commit.
200
+ The SDK does not work around them — it surfaces them. Working around a
201
+ measurement defect in a client would make the defect invisible without
202
+ making it go away.
203
+
204
+ > **All three were re-read against the deployed commit `aeff13c` /
205
+ > `3.3.1` on 2026-10-22.** The method was source comparison between
206
+ > `68d6057` and `aeff13c`, plus the replayed production response. **A
207
+ > version number changing is not evidence that a defect closed**, and no
208
+ > entry below was weakened on that basis: BL-001 and BL-003 live in files
209
+ > that are byte-identical between the two commits, so they stand
210
+ > unchanged. BL-002 is the one entry that moves, because an actual
211
+ > correction was shipped — and it closes one named class, not the entry.
212
+
213
+ ---
214
+
215
+ ### BL-001 — TRIAL account and key status inconsistency
216
+
217
+ | | |
218
+ |---|---|
219
+ **Status** | Open — observed |
220
+ **Affected version** | backend commit `68d6057`; measurement version `3.3.0`. **Re-read at `aeff13c` / `3.3.1` on 2026-10-22: unchanged, still open** |
221
+ **Scope** | accounts whose `tier` is `TRIAL` **and** whose `status` is `ACTIVE` |
222
+ **Severity for a pilot** | **High** — blocks use of a trial key |
223
+ **SDK impact** | none; the SDK reports the server's responses faithfully |
224
+
225
+ > **Re-read at `3.3.1`.** `app/auth/api_key.py` is **byte-identical**
226
+ > between `68d6057` and the deployed `aeff13c`. The quota debit still
227
+ > carries `AND status = 'TRIAL'`, and the statement that suspends the
228
+ > account's keys still carries no equivalent guard. The defect therefore
229
+ > stands at `3.3.1`, established by **source comparison** — the account
230
+ > journey was not re-run, and no trial key was available to re-observe it.
231
+
232
+ **What was observed.** On a local instance built from the reference
233
+ commit, a freshly provisioned trial account was taken through the
234
+ documented journey (registration, e-mail verification, administrator
235
+ approval). The resulting key was accepted by an authenticated non-metered
236
+ endpoint. The **first** `POST /v1/govern` call then returned
237
+ **HTTP 402** with a payload stating the trial quota was exhausted, and
238
+ **every subsequent call returned HTTP 401**.
239
+
240
+ Database state immediately afterwards:
241
+
242
+ | Field | Value |
243
+ |---|---|
244
+ `accounts.status` | `ACTIVE` |
245
+ `accounts.tier` | `TRIAL` |
246
+ `accounts.trial_requests_remaining` | `500` |
247
+ `accounts.trial_requests_total` | `500` |
248
+ `api_keys.status` | **`SUSPENDED`** |
249
+
250
+ The quota was **not** exhausted — none of it had been consumed — yet the
251
+ key was permanently suspended.
252
+
253
+ **Conditions reproduced.** One account, one local instance, the documented
254
+ provisioning journey. **Not generalised**: this was not tested against any
255
+ hosted deployment, nor against accounts provisioned by other means, nor
256
+ against accounts whose `status` is something other than `ACTIVE`. Whether
257
+ hosted deployments carry the same account state is unverified.
258
+
259
+ **Technical cause (confirmed by reading the source).** The quota debit is
260
+ guarded by a condition on `accounts.status`:
261
+
262
+ ```sql
263
+ UPDATE accounts
264
+ SET trial_requests_remaining = trial_requests_remaining - 1
265
+ WHERE id = :account_id
266
+ AND status = 'TRIAL' -- the condition in question
267
+ AND trial_requests_remaining > 0
268
+ RETURNING trial_requests_remaining
269
+ ```
270
+
271
+ A database migration included at the reference commit records that trial
272
+ detection was moved from `status` to `tier`, stating that `status`
273
+ describes whether an account is running and that *"trial detection
274
+ everywhere now keys off `tier=='TRIAL'`"*. The portal consequently writes
275
+ `status='ACTIVE'`, `tier='TRIAL'`. The `UPDATE` above therefore matches no
276
+ row; an empty result is interpreted as an exhausted quota; the suspension
277
+ branch then runs, and the statement that suspends the keys carries no
278
+ equivalent guard, so the keys are suspended while the account is not.
279
+
280
+ **Workaround for a pilot.** None available from the client side. A pilot
281
+ needs a key on an account the server's quota path accepts — in practice a
282
+ paid tier with a funded wallet, or an account whose state the operator has
283
+ adjusted. **Request this from your NeoMundi contact rather than working
284
+ around it.**
285
+
286
+ **Internal tracking.** Backend correction plan, item BL-001.
287
+
288
+ ---
289
+
290
+ ### BL-002 — SSE chunking-dependent governance decision
291
+
292
+ | | |
293
+ |---|---|
294
+ **Status** | **Partly corrected in `3.3.1` — `Fixed — pending verification` for the named class; the entry stays open for the residue** |
295
+ **Affected version** | observed at backend commit `68d6057` / `3.3.0`. **Corrected at `aeff13c` / `3.3.1`, for one class only** |
296
+ **Scope** | `POST /v1/govern/stream` only. **REST is unaffected.** |
297
+ **Severity for a pilot** | **High for streaming**; applies when using the V2 SSE client |
298
+ **SDK impact** | **applies to the SSE path; V2 transports the reported trajectory without repairing it** |
299
+
300
+ > **SDK 2 transports SSE but does not correct this backend defect.**
301
+ > Read [STREAMING.md](docs/STREAMING.md) for integrity and termination
302
+ > checks. These checks do not validate the trajectory or the metrics.
303
+
304
+ #### What `3.3.1` changed, and what it did not
305
+
306
+ **`3.3.1` exists for this defect, and for nothing else.** Between
307
+ `68d6057` and the deployed `aeff13c`, one application file changed —
308
+ `app/api/v1/streaming.py` — plus the version string in `app/config.py`.
309
+ The trajectory rules R1/R2/R3 now read **only** the trajectory points
310
+ whose coherence *and* risk were really measured, plus the final point.
311
+ The published trajectory itself (`series`, `profile`, `total_variation`,
312
+ `min_stability`, `max_stability`) is unchanged, so no stored series is
313
+ reinterpreted.
314
+
315
+ Measured in the backend's own regression suite, on one text with one set
316
+ of judge verdicts and only the slicing varying:
317
+
318
+ | Fragments | Decision before | Decision after |
319
+ |---:|---|---|
320
+ | 1 | `ALLOW` | `ALLOW` |
321
+ | 10 | `ALLOW` | `ALLOW` |
322
+ | 20 | **`FLAG`** | `ALLOW` |
323
+ | 50 | **`FLAG`** | `ALLOW` |
324
+ | 300 | **`FLAG`** | `ALLOW` |
325
+
326
+ **What is still true at `3.3.1`, and the backend locks it with tests so
327
+ that it stays visible:**
328
+
329
+ - **ΔG is still not invariant to the slicing.** The published `series`,
330
+ `profile` and `total_variation` still vary with fragmentation; a
331
+ backend test *requires* that they do, so the residue stays measured
332
+ rather than presumed gone;
333
+ - **a stream sliced too coarsely has no measured trajectory at all** —
334
+ with default intervals the first window carrying both signals arrives
335
+ at fragment 120. Below that, no rule can fire, so whether a drift is
336
+ *observable* still depends on the framing;
337
+ - the asynchronous race between evaluators subsists; the correction
338
+ discards windows whose signals had not landed, it does not make their
339
+ arrival deterministic;
340
+ - four decision thresholds remain hard-coded, and two of them appear in
341
+ the production environment template **without being read by any line of
342
+ code**.
343
+
344
+ **Consequences for a pilot using SSE directly — unchanged.** Do not
345
+ compare `delta_g.profile` or `delta_g.total_variation` across providers,
346
+ clients or versions. Prefer REST for anything that must be reproducible.
347
+
348
+ **Evidence, and its limits.** The above is **derived from the backend
349
+ source and its regression suite at `aeff13c`** — read on 2026-10-22. No
350
+ production SSE stream under `3.3.1` was examined in that 22 October
351
+ verification campaign. A later transport success alone would not establish
352
+ invariance to fragmentation. The correction is therefore
353
+ `Fixed — pending verification`, and the entry as a whole stays open.
354
+
355
+ **What was observed.** Four streams were run against a local instance,
356
+ each with a correct answer, each with the factuality judge reporting
357
+ `factual_hallucination_score = 0.0` — that is, no hallucination measured —
358
+ and final stability between 0.86 and 0.94.
359
+
360
+ | Content fragments | ΔG profile | Total variation | **Decision** |
361
+ |---:|---|---:|---|
362
+ 1 | `FLAT` | 0.0 | **ALLOW** |
363
+ 1 | `FLAT` | 0.0 | **ALLOW** |
364
+ 10 | `FLAT` | 0.0 | **ALLOW** |
365
+ 300 | `DROP` | 0.158 | **FLAG** |
366
+
367
+ The measured quality was equivalent across all four. The decision was
368
+ not.
369
+
370
+ **Mechanism (observed).** Intermediate governance windows fire every
371
+ *N* **network fragments**, not every *N* tokens. Each window computes a
372
+ provisional stability from whichever rolling signals have reported by that
373
+ point; the factuality judge runs only at end of stream, so intermediate
374
+ windows use a neutral default for it. The published trajectory of one
375
+ 300-fragment stream:
376
+
377
+ ```
378
+ fragment 20 → stability 1.0000 delta 0.0000
379
+ fragment 40 → stability 1.0000 delta 0.0000
380
+ fragment 60 → stability 0.8896 delta −0.1104 ← coherence reports here
381
+ fragment 80 → stability 0.8896 delta 0.0000
382
+ fragment 100 → stability 0.8867 delta −0.0029
383
+ ```
384
+
385
+ The "drop" is the coherence measurement **arriving**, not a degradation
386
+ during generation. A profile rule then reads that drop as instability and
387
+ escalates the decision.
388
+
389
+ A stream short enough that no intermediate window fires produces a
390
+ single-point trajectory, classified `FLAT`, and no escalation occurs.
391
+
392
+ **Two further observations on the same responses.** On a flagged stream,
393
+ the published `reasons` array contained both
394
+ *"all metrics within bounds"* and *"possible hallucination or drift in
395
+ progress"*, while the judge had measured no hallucination at all; and
396
+ `decision_basis` was `measured_thresholds` — the field that exists to
397
+ explain why a decision was taken classified this escalation as resting on
398
+ measured thresholds.
399
+
400
+ **Guidance for a pilot using SSE directly.**
401
+
402
+ - Do not compare `delta_g.profile`, `delta_g.total_variation` or the
403
+ streaming `decision` across providers, SDK versions or client libraries:
404
+ fragmentation differs between them.
405
+ - Treat intermediate `governance_check` and `stability_check` events as
406
+ **provisional** — the backend marks them `provisional: true` and that
407
+ flag is meaningful.
408
+ - Prefer the REST route for anything that must be reproducible.
409
+
410
+ **Internal tracking.** Backend correction plan, item BL-002.
411
+
412
+ ---
413
+
414
+ ### BL-003 — `delta_g` published while the aggregate is withheld
415
+
416
+ | | |
417
+ |---|---|
418
+ **Status** | Open — observed. **Contract interpretation under review.** |
419
+ **Affected version** | backend commit `68d6057`; measurement version `3.3.0`. **Re-read at `aeff13c` / `3.3.1` on 2026-10-22: unchanged, still open** |
420
+ **Scope** | `POST /v1/govern`, when a required signal was not measured |
421
+ **Severity for a pilot** | **Medium** — a misread field, not a corrupted measurement |
422
+ **SDK impact** | none; the SDK exposes the raw value and documents the caveat |
423
+
424
+ > **Re-read at `3.3.1`.** `app/api/v1/governance.py` and everything under
425
+ > `app/models/` are **byte-identical** between `68d6057` and `aeff13c`:
426
+ > the code that publishes `delta_g` and the code that withholds the
427
+ > aggregates are the same code. The limitation therefore stands,
428
+ > established by **source comparison**. It was **not** re-observed: the
429
+ > only `3.3.1` response available is a complete measurement, so it
430
+ > exhibits neither the withheld aggregate nor the hazard. A `3.3.1`
431
+ > response with an unmeasured required signal would be needed to
432
+ > re-observe it, and none exists.
433
+
434
+ **What was observed.** On responses where a required signal could not be
435
+ measured, the server correctly withheld the aggregates — `g_score`,
436
+ `quality.stability_score` and `g_final` were all `null`, the decision was
437
+ `FLAG`, and `decision_basis` was `incomplete_measurement` — while
438
+ `delta_g` carried a number. Two of ten REST captures showed this, as did
439
+ the fully degraded capture (`g_score: null`, `delta_g: −0.56199`).
440
+
441
+ **What the contract says, and why this is stated carefully.** `delta_g` is
442
+ documented as the variation in `G` since the account's previous request.
443
+ It is a **separate field** from the aggregates, and the withholding rule
444
+ that the backend documents applies explicitly to `g_score`,
445
+ `quality.stability_score` and `g_final` — `delta_g` is not named in it.
446
+ On that reading, a populated `delta_g` beside a null `g_score` is the
447
+ contract behaving as written rather than a violation of it.
448
+
449
+ **What is nonetheless a real hazard, independent of that reading.** The
450
+ value is computed from the current request's internal `G`, which in this
451
+ case is precisely the figure the withholding rule exists to suppress —
452
+ and the reference stored for the *next* request is that same unpublished
453
+ figure. A series of `delta_g` values spanning a degraded request is
454
+ therefore not interpretable, whatever the contract intends. That is why
455
+ this is listed rather than dismissed.
456
+
457
+ **Guidance — this is the part that matters for your integration.**
458
+
459
+ Respect statuses, coverage and the aggregate publication rule rather than
460
+ reading numbers directly:
461
+
462
+ ```python
463
+ if result.measurement_incomplete:
464
+ # Aggregates are withheld. Do not read delta_g as a quality signal
465
+ # for this observation, and do not chain it with neighbouring
466
+ # observations.
467
+ result.missing_required_signals # which signals were not measured
468
+ result.measurement.coverage # how much of the measurement happened
469
+ result.governance.decision_basis # 'incomplete_measurement'
470
+ ```
471
+
472
+ And in general: **do not read `delta_g` as a drift measurement.** It
473
+ compares one request to whatever the account's previous request happened
474
+ to be, whatever question that request asked, within a one-hour window.
475
+ Two unrelated tasks from the same account produce a `delta_g` that
476
+ describes the change of task, not a change in the system.
477
+
478
+ **Internal tracking.** Backend correction plan, item BL-003.
479
+
480
+ ---
481
+
482
+ ## 4. SDK-specific limitations
483
+
484
+ These are scope decisions. V2 adds SSE transport; the scientific limits below remain.
485
+
486
+ | ID | Not included | Why |
487
+ |---|---|---|
488
+ **SL-001** | **Live deployment/provider validation of V2 SSE** | Transport is implemented and tested offline. Scientific/backend limitations in BL-002 remain; no new hosted SDK SSE call was made. |
489
+ **SL-002** | **TypeScript / JavaScript SDK** | requires a frozen OpenAPI specification, which does not exist server-side |
490
+ **SL-003** | **Longitudinal processing** — series, trends, drift | comparing two observations requires conditions the SDK cannot verify for you (same measurement version, same coverage, same evaluator models, same β weights, same route). Deciding that a series is valid is a methodological judgement, not a transport concern. |
491
+ **SL-004** | **Automatic score interpretation** — no `is_safe()`, no quality level, no embedded threshold | thresholds are server-side configuration and are not publicly calibrated. A client-side threshold would be an invention. Verified absent by AST analysis of the sources. |
492
+ **SL-005** | **Correction of backend defects** | the SDK surfaces BL-001 to BL-003; it does not compensate for them. Related: the factuality judge runs at a non-zero sampling temperature, so two identical calls can return different scores. **That spread has not been measured.** Until it is, no difference between two observations is interpretable. |
493
+ | | Also absent | asynchronous client, other REST routes (replay, exports, RGC), caching, pagination, PDF certificate verification |
494
+
495
+ ---
496
+
497
+ ## 5. Developer guidance
498
+
499
+ ### 5.1 Check measurement coverage before reading any score
500
+
501
+ This is the first thing to read in a response, not the last.
502
+
503
+ ```python
504
+ result = client.govern(..., llm_response=text, llm_prompt=prompt)
505
+
506
+ if result.measurement_incomplete:
507
+ print("incomplete:", result.missing_required_signals)
508
+ # The FLAG you are about to see is NOT a statement about the content.
509
+
510
+ m = result.measurement
511
+ m.status # 'complete' | 'partial' | 'not_attempted'
512
+ m.checks_attempted # checks actually launched
513
+ m.checks_succeeded # of those, how many produced a usable result
514
+ m.coverage # the ratio — and the value of `confidence`
515
+ ```
516
+
517
+ **`coverage == 1.0` does not mean "fully measured".** It is `1.0` when
518
+ nothing was attempted. Always read it together with `status`:
519
+
520
+ ```python
521
+ if m.status == "not_attempted":
522
+ # No server-side check ran. coverage 1.0 means "nothing was expected",
523
+ # not "everything was measured".
524
+ ...
525
+ ```
526
+
527
+ `coverage` is re-derivable, so you can verify it rather than trust it:
528
+
529
+ ```python
530
+ if m.checks_attempted:
531
+ assert round(m.checks_succeeded / m.checks_attempted, 6) == m.coverage
532
+ ```
533
+
534
+ ### 5.2 Handling `null`
535
+
536
+ **Never substitute a number for a `null`.** The SDK contains no numeric
537
+ default anywhere in its sources, and a test enforces that. Do not
538
+ reintroduce one in your integration.
539
+
540
+ ```python
541
+ # For display — never raises.
542
+ result.format_score("g_score", nd=4) # '0.4920' | 'non mesuré'
543
+ result.format_score("g_score", when_none="n/a")
544
+
545
+ # For a computation — raises an exception that names the cause.
546
+ from neomundi import AggregateWithheldError
547
+ try:
548
+ value = result.require("g_score")
549
+ except AggregateWithheldError as exc:
550
+ exc.missing_required_signals
551
+ exc.decision_basis
552
+
553
+ # What not to do:
554
+ f"{result.g_score:.2f}" # TypeError as soon as the aggregate is withheld
555
+ ```
556
+
557
+ A `null` has **three** possible meanings. Distinguish them:
558
+
559
+ | Meaning | How to recognise it |
560
+ |---|---|
561
+ **not measured** — the check ran, produced nothing | the signal is in `unmeasured_signals`, with its cause |
562
+ **not sold at your tier** | the signal is in neither signal list, and `measurement.status` is `complete` |
563
+ **not applicable on this route** | `trajectory_points` and `provider` are always `None` on REST — "not applicable here", not "zero" |
564
+
565
+ ### 5.3 "Measured as zero" is not "not measured"
566
+
567
+ This distinction is the product. In every failure mode, the backend's
568
+ instruments return the **most reassuring value they can** — risk `0.0`,
569
+ coherence `1.0`, factuality `0.0`. A signal's validity is therefore
570
+ **never** readable from its numeric value.
571
+
572
+ ```python
573
+ # A measured zero:
574
+ result.quality.semantic_risk # 0.0 -> measured, nothing detected
575
+ result.is_measured("semantic_risk") # True
576
+
577
+ # An absence:
578
+ result.quality.factual_hallucination_score # None -> not measured
579
+ result.unmeasured_reason("factual_hallucination_score")
580
+ # 'hallucination_judge_unavailable'
581
+
582
+ # Four states, and the SDK does not collapse them:
583
+ result.measurement_state("coherence_score")
584
+ # MeasurementState.MEASURED
585
+ # MeasurementState.NOT_MEASURED -> a cause is available
586
+ # MeasurementState.NOT_ATTEMPTED_OR_GATED -> not launched OR gated by tier
587
+ # MeasurementState.UNKNOWN -> the response does not say
588
+ ```
589
+
590
+ `NOT_ATTEMPTED_OR_GATED` is **deliberately ambiguous**: the server does not
591
+ distinguish "never launched" from "withheld by commercial tier" on the
592
+ wire. The SDK does not invent that distinction. Read
593
+ `measurement.status` and `measurement.checks_attempted` to narrow it.
594
+
595
+ One observed consequence worth knowing: when no check is launched at all,
596
+ the response carried `quality.coherence_score = 1.0` as a value while the
597
+ factuality fields were `null`, and the aggregates were published at their
598
+ maximum. A `1.0` coherence is therefore not always an observation.
599
+
600
+ ### 5.4 Keep the audit and version metadata
601
+
602
+ Keep these with every observation you intend to use as evidence or compare
603
+ later. Without them an observation is a number without provenance.
604
+
605
+ ```python
606
+ result.request_id # the observation's identity
607
+ result.timestamp_raw # the server's instant, as sent
608
+ result.audit.measurement_version # the only version with scientific meaning
609
+ result.audit.normalizer_version
610
+ result.measurement.coverage # how much of the measurement happened
611
+ result.evaluators # per-signal instrument, model, cause
612
+
613
+ # The three fingerprints are the only way to verify that two observations
614
+ # were produced under the same evaluator configuration — `measurement_version`
615
+ # does not cover evaluator identity.
616
+ ev = result.evaluator_of("factual_hallucination_score")
617
+ ev["model_used"], ev["config_fingerprint"], ev["prompt_template_hash"]
618
+
619
+ result.esi.betas if result.esi else None # the weights actually applied
620
+ result.to_dict() # the full raw payload
621
+ ```
622
+
623
+ Two results are comparable only under **all** of: same
624
+ `measurement_version`, same coverage, same evaluator models, same β
625
+ weights, same route. The SDK gives you what you need to check that; it
626
+ does not check it for you (SL-003).
627
+
628
+ ### 5.5 A `FLAG` is not evidence of hallucination
629
+
630
+ A `FLAG` is **advisory**. NeoMundi grants, refuses, suspends and modifies
631
+ no execution, and the signed contract format states this as a structural
632
+ invariant.
633
+
634
+ Read `decision_basis` before acting on a decision:
635
+
636
+ | Value | What the FLAG means |
637
+ |---|---|
638
+ `measured_thresholds` | a measured value crossed a configured threshold. The FLAG is about the content. |
639
+ `incomplete_measurement` | a required signal could not be measured. **The FLAG is not about the content** — it says the measurement did not complete. |
640
+ `no_check_attempted` | no server-side check ran. Nothing here was measured by the service. |
641
+ `None` | the observation predates this field. **Do not assume `measured_thresholds`.** |
642
+
643
+ Two further cautions, both grounded in observations above:
644
+
645
+ - the factuality judge is a language model scoring a text with no external
646
+ source, at a non-zero temperature. A high score is its opinion, traceable
647
+ and reproducible to the configuration — not a verified fact;
648
+ - `decision_basis` was observed reporting `measured_thresholds` on a
649
+ streaming escalation that came from the ΔG profile rule (BL-002). On the
650
+ streaming route, read `reasons` as well as `decision_basis`.
651
+
652
+ ### 5.6 Reporting a discrepancy
653
+
654
+ The SDK is built so that a contract discrepancy is **visible rather than
655
+ absorbed**. Two fields carry it:
656
+
657
+ ```python
658
+ result.parse_warnings # a field could not be read at the expected type
659
+ result.unknown_fields # a root field this SDK version does not model
660
+ ```
661
+
662
+ | What you see | What it means | What to do |
663
+ |---|---|---|
664
+ `parse_warnings` non-empty | the instance emits something other than what the modelled contract declares | **report it.** This is an audit finding, not a client bug. Do not widen the parsing to make it go away. |
665
+ `unknown_fields` non-empty | the backend has gained a field since the reference commit | report it; the SDK needs extending and its version incrementing |
666
+ `InvalidResponseError` on a 2xx | a required block is missing, or the body is not JSON | report it with the exception's `body` |
667
+ `measurement_version` **outside** `verified_measurement_versions` | you are talking to a contract this SDK has not been checked against | **report it before relying on anything**, parsing included |
668
+ `measurement_version` in the verified list but ≠ `measurement_version` | parsing is safe; the **series** is not comparable with one produced under the declared version | record it with every observation; no report needed |
669
+
670
+ When reporting, include: the `request_id`, `result.audit.measurement_version`,
671
+ the `parse_warnings` entries verbatim, the base URL's **environment**
672
+ (not its credentials), and the SDK version. **Never include your API key**,
673
+ and redact any response content you are not permitted to share.
674
+
675
+ Use your NeoMundi technical contact for confidential material. Issues on
676
+ this public repository may contain only sanitised, non-sensitive reproductions.
677
+
678
+ ---
679
+
680
+ ## 6. Version compatibility
681
+
682
+ > **REST contract compatibility of `POST /v1/govern` with measurement
683
+ > version `3.3.1` is verified by local tests and by replaying a
684
+ > production capture. A direct call of the SDK against production has not
685
+ > been performed.**
686
+
687
+ | | |
688
+ |---|---|
689
+ **Audited reference** | backend commit `68d6057cf620e430e6488da3cfcec9c471c75ed6` |
690
+ **Measurement version of that reference** | `3.3.0` |
691
+ **Measurement versions whose REST surface is verified** | **`3.3.0` and `3.3.1`** |
692
+ **Normalizer version** | `1.0.0` — unchanged across both |
693
+ **Contract source** | the backend's request and response models at that commit |
694
+ **Frozen OpenAPI specification** | **none exists** — this is why a commit is named |
695
+
696
+ ```python
697
+ import neomundi
698
+ neomundi.API_CONTRACT
699
+ # {'backend_commit': '68d6057cf620e430e6488da3cfcec9c471c75ed6',
700
+ # 'measurement_version': '3.3.0',
701
+ # 'verified_measurement_versions': ('3.3.0', '3.3.1'),
702
+ # 'normalizer_version': '1.0.0',
703
+ # 'route': 'POST /v1/govern',
704
+ # 'contract_source': 'govern-v3/app/models/responses.py, requests.py',
705
+ # 'openapi_frozen': False}
706
+ ```
707
+
708
+ ### Four version numbers, and they are not interchangeable
709
+
710
+ Confusing them is the most common way to reach a wrong conclusion about
711
+ this SDK, so they are named apart:
712
+
713
+ | | Value observed | Where it is read |
714
+ |---|---|---|
715
+ **Application build** | `3.0.0` | `GET /v1/health`, field `version` |
716
+ **Measurement version** | `3.3.1` | `result.audit.measurement_version` — **a response, and nowhere else** |
717
+ **Normalizer version** | `1.0.0` | `result.audit.normalizer_version` |
718
+ **RGC contract schema** | `0.1.0` | the signed contract, a route this SDK does not cover |
719
+
720
+ **`GET /v1/health` does not publish the measurement version.** Verified
721
+ again on 2026-10-22 against the production deployment: `HTTP 200`,
722
+ `version: 3.0.0`, `environment: production`, and no
723
+ `measurement_version` anywhere in the body. `/openapi.json`, `/docs`,
724
+ `/v1/version` and `/version` were all recorded as `404` on 2026-10-21.
725
+ **Establishing the measurement version therefore costs one metered
726
+ observation. There is no free way to read it.**
727
+
728
+ ### Why `3.3.1` is in the verified list — the evidence, in full
729
+
730
+ Two pieces, both dated 2026-10-22, and **neither is a call made by this
731
+ SDK**.
732
+
733
+ **1. Source comparison, `68d6057` → `aeff13c` (the deployed commit).**
734
+ Seven files differ in total. Of the application code, exactly **two**:
735
+ `app/api/v1/streaming.py` and the version string in `app/config.py`. The
736
+ remaining five are documentation and tests. Specifically:
737
+
738
+ - **nothing under `app/models/`** — the declared source of this SDK's
739
+ contract — changed;
740
+ - `app/api/v1/governance.py`, the REST route itself, did not change;
741
+ - `app/auth/`, `app/core/`, `app/rgc/` and `app/main.py` did not change;
742
+ - the backend's own 1 266-line golden file locking its measurement
743
+ surface differs by **one line**: the version string. Every signal,
744
+ coverage state, gated field and provenance entry in it is identical.
745
+
746
+ The backend states the consequence itself, and it is worth having
747
+ verbatim: under `3.3.0` and `3.3.1` results differ for the same input
748
+ **only** on a streaming observation left at `ALLOW` by the measured
749
+ signals that produced a `DROP` or `OSCILLATING` profile from unmeasured
750
+ points. *"Everywhere else — the whole REST path, every SSE observation
751
+ whose decision came from the measured signals, every aggregate, every
752
+ coverage status, every gated field — the two are identical."*
753
+
754
+ **2. Replay of a real `3.3.1` production response.** One response emitted
755
+ by the production deployment on 2026-10-21 was replayed through the
756
+ client: **0 `parse_warnings`, 0 unknown root fields**, exact round-trip,
757
+ aliases agreeing, coverage re-derivable. Details and the full check list
758
+ are in §2.
759
+
760
+ **A note on which process produced it.** The deployment's `uptime` read
761
+ on 2026-10-22 places the start of the running process at
762
+ `2026-10-20T21:16:45`, the same instant computed from an independent
763
+ reading on 2026-10-21. The process that produced the replayed response is
764
+ therefore **the process still serving today** — it has not been restarted
765
+ since. That removes one doubt. It does not create an end-to-end call.
766
+
767
+ ### What the verified list does *not* mean
768
+
769
+ - it does **not** mean an end-to-end call has succeeded. **No call by
770
+ this SDK to any hosted deployment has ever been made**, under either
771
+ version. Authentication, error statuses, timeouts and quota behaviour
772
+ against production remain unverified;
773
+ - it does **not** make two observations comparable across the two
774
+ versions. Comparability is a separate question, answered below;
775
+ - these REST compatibility proofs do **not** cover the streaming route.
776
+ V2 implements SSE; its source-derived contract and offline tests are
777
+ documented separately in [STREAMING.md](docs/STREAMING.md).
778
+
779
+ ### Minimum check before a pilot campaign
780
+
781
+ 1. call `GET /v1/health` — unauthenticated, consumes no quota — and
782
+ record the build version and the state of the three evaluators.
783
+ **Do not read a measurement version from it; it does not carry one.**
784
+ A `degraded` on `judge_factuality` can coexist with successful measurements.
785
+ A configured-name/catalogue mismatch was documented historically; do not
786
+ assume that cause for every new degraded probe. The replayed
787
+ `3.3.1` response was produced by a deployment reporting exactly that,
788
+ with the judge's own `status` reading `measured`;
789
+ 2. make one `POST /v1/govern` call and record
790
+ `result.audit.measurement_version`;
791
+ 3. confirm it is in `API_CONTRACT["verified_measurement_versions"]`;
792
+ 4. confirm `result.parse_warnings` and `result.unknown_fields` are both
793
+ empty;
794
+ 5. record `result.evaluator_of(...)["config_fingerprint"]` and confirm it
795
+ does not change during the campaign — noting that on `3.3.1` only the
796
+ factuality judge's two signals publish one at all (§2).
797
+
798
+ ### Comparability — the rule, and where `3.3.1` sits in it
799
+
800
+ The backend documents its own rule, and it is worth repeating verbatim:
801
+ results under measurement versions `3.2.0` and `3.3.0` are **not**
802
+ comparable for the same input wherever the observation came from the
803
+ streaming path, a required signal went unmeasured, or the client is on an
804
+ entry tier. Any longitudinal study must sit entirely on one side of that
805
+ boundary.
806
+
807
+ For `3.3.0` against `3.3.1` the backend's statement is narrower, and
808
+ quoted above: on the REST path the two are identical. **Treat that as the
809
+ backend's claim about its own formula, not as a licence to mix series.**
810
+ The conservative practice holds and costs nothing: record
811
+ `result.audit.measurement_version` with every observation, and keep a
812
+ series on one side of the boundary. A measurement whose version is not
813
+ recorded cannot be rescued later.
814
+
815
+ ---
816
+
817
+ ## 7. Known limitations lifecycle
818
+
819
+ Each entry carries a stable identifier. Identifiers are never reused or
820
+ renumbered; a resolved entry keeps its identifier and changes status.
821
+
822
+ **Status vocabulary**
823
+
824
+ | Status | Meaning |
825
+ |---|---|
826
+ `Open — observed` | reproduced on a running instance; conditions stated |
827
+ `Open — derived from code` | established by reading the source; not yet reproduced |
828
+ `Under review` | the intended contract is being clarified before any fix |
829
+ `Fixed — pending verification` | corrected, not yet re-verified against an instance |
830
+ `Fixed — verified` | corrected **and** re-verified, with evidence referenced |
831
+ `By design` | not a defect; a documented scope decision |
832
+
833
+ ### Register
834
+
835
+ | ID | Title | Status | Affected version | Scope | Internal plan |
836
+ |---|---|---|---|---|---|
837
+ **BL-001** | TRIAL account and key status inconsistency | `Open — observed` · **re-read at `3.3.1`, unchanged** | backend `68d6057` / `3.3.0`, **and `aeff13c` / `3.3.1`** | accounts with `tier=TRIAL` and `status=ACTIVE`, on a local instance | plan item BL-001 |
838
+ **BL-002** | SSE chunking-dependent governance decision | **one class `Fixed — pending verification` at `3.3.1`; entry stays `Open` for the residue** | observed at `68d6057` / `3.3.0`; corrected at `aeff13c` / `3.3.1` | `POST /v1/govern/stream` only; REST unaffected | plan item BL-002 |
839
+ **BL-003** | `delta_g` published while the aggregate is withheld | `Open — observed`, contract `Under review` · **re-read at `3.3.1`, unchanged** | backend `68d6057` / `3.3.0`, **and `aeff13c` / `3.3.1`** | `POST /v1/govern` when a required signal is unmeasured | plan item BL-003 |
840
+ **SL-001** | **Live deployment/provider validation of V2 SSE** | Pending; transport implemented and tested offline | SDK `2.0.0` | V2 SSE qualification; BL-002 remains separate | — |
841
+ **SL-002** | No TypeScript SDK | `By design` | SDK `0.1.0` | SDK scope | — |
842
+ **SL-003** | No longitudinal processing | `By design` | SDK `0.1.0` | SDK scope | — |
843
+ **SL-004** | No automatic score interpretation | `By design` | SDK `0.1.0` | SDK scope | — |
844
+ **SL-005** | Backend defects not worked around; judge variance unmeasured | `By design` / `Open` | SDK `0.1.0`; backend `3.3.0` | SDK scope; measurement methodology | plan, additional risks |
845
+
846
+ **Internal plan.** Each `BL-*` entry maps to an item in an internal
847
+ backend correction plan holding reproduction steps, source locations,
848
+ candidate corrections, regression tests and acceptance criteria. That plan
849
+ is **not published in this repository** and is not distributed to pilots.
850
+ Its existence is recorded here so that a reader knows these items are
851
+ tracked rather than merely noted.
852
+
853
+ **Keeping this document honest.** An entry moves to `Fixed — verified`
854
+ only when a re-run against an instance demonstrates it, and the evidence
855
+ is referenced. No entry is removed, softened or silently reworded. If you
856
+ find a limitation that is not listed, that omission is itself worth
857
+ reporting (§5.6).