Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ views-frames/
│ ├── module.py # ReconciliationModule (holds the injected mapping)
│ ├── result.py # ReconciliationResult — the frame plus HOW it was made
│ └── conformance.py # assert_reconcile_contract
├── scripts/ # standalone dev tools, none wired into CI
├── scripts/ # dev tools; check_arch_tree + check_doc_refs run in CI
├── examples/ # runnable quickstarts (run by CI)
└── tests/ # flat: test_*.py + fixtures/
```
Expand Down
46 changes: 23 additions & 23 deletions docs/CICs/FrameMetadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ every field in it is a field every consumer must agree on.
| Situation | Behaviour | Loud? |
|---|---|---|
| Unknown key in `from_dict` | **Dropped silently** | ❌ **no** |
| Field set to a non-JSON type, then `save`d via **npz** | **Silently stringified** — `io/npz.py` uses `json.dumps(..., default=str)`, so a `datetime` timestamp reloads as `'2026-01-01 00:00:00'` | ❌ **no** |
| The same value `save`d via **arrow** | `TypeError: Object of type datetime is not JSON serializable` — `io/arrow.py` uses a plain `json.dumps` | ✅ yes |
| Field set to a non-JSON type, then `save`d via **either codec** | `TypeError: Object of type datetime is not JSON serializable` — both `io/npz.py` and `io/arrow.py` use a plain `json.dumps` | ✅ yes |
| Field set to the wrong type at a typed call site | Accepted at runtime | ❌ no — caught by `mypy --strict` **only here** |
| Wrong type arriving from a loaded `header.json` | Accepted; `from_dict` takes `Mapping[str, Any]` and coerces nothing | ❌ **no** — mypy cannot see this path |
| Mutating a field after construction | `FrozenInstanceError` | ✅ yes |
Expand Down Expand Up @@ -177,33 +176,34 @@ level = frame.metadata.run_type.split("_")[0] # ADR-003: no semantic inference
- **Green:** `to_dict`/`from_dict` round-trip preserves set fields
(`test_metadata_to_from_dict_roundtrip`, `test_metadata_provenance_roundtrip`); unset fields
are omitted from `to_dict` (`test_metadata_generic_provenance_fields_default_none`) — all in
`tests/test_frames.py`. Metadata survives row selection (`tests/test_select.py:88`).
`tests/test_frames.py`. Metadata survives row selection (`tests/test_select.py::test_select_preserves_metadata`).
- **Beige:** `with_metadata` allocates no second `values` buffer — the copy-vs-view property
(`tests/test_properties.py::test_with_metadata_shares_the_values_buffer`, register C-07).
- **Red:** mutation raises `FrozenInstanceError` (`test_metadata_is_frozen`).

`test_metadata_ignores_unknown_keys` is **green, not red, and pins less than its name suggests**:
it asserts only `md.model == "x"` after passing an unknown key, so it pins *"does not raise"* and
never asserts the unknown key was discarded rather than stored. The drop documented in §3 and §6
— the behaviour with the durable consequence — is therefore **not pinned by anything**.

**Three guarantees in §3 are not pinned by any test**, found while writing this contract and
recorded rather than glossed:

0. **The unknown-key drop itself** — see immediately above. `assert not hasattr(md, "unknown")`
is the missing line.

1. **The `save`/`load` header round-trip is pinned for `PredictionFrame` only**
(`tests/test_frames.py:107`, `assert loaded.metadata == pf.metadata`). `FeatureFrame` and
`TargetFrame` have no equivalent assertion — `test_feature_frame_save_load_preserves_names`
covers `feature_names`, not the header.
2. **Nothing asserts that a frame built without metadata exposes an empty header** rather than
`None`, though §3 guarantees it and every consumer reading `.metadata` depends on it.

Both are one-line additions to an existing test file. They belong to **S9 (#249)**, which owns
the test suite's self-description under register C-80; noted there. This contract states the
guarantee regardless — that is what a CIC is for (ADR-006: tests are derived *from* the
contract), and naming the gap is better than a §10 that reads complete.
never asserts the unknown key was discarded rather than stored.

**Three guarantees in §3 were unpinned when this contract was written, and all three were pinned
the same day** (2026-08-18, register C-80, commit `5423431`). They are listed here because the
gap is worth remembering, not because it is open — each now names the test that closes it:

0. **The unknown-key drop itself** — that an unknown key is *discarded*, not merely tolerated.
Now pinned by `tests/test_frames.py::test_metadata_unknown_keys_are_dropped_not_stored`.

1. **The `save`/`load` header round-trip was pinned for `PredictionFrame` only**
(`tests/test_frames.py::test_metadata_survives_save_load_for_all_three_frames` now covers all
three; `test_feature_frame_save_load_preserves_names` covers `feature_names`, not the header).

2. **Nothing asserted that a frame built without metadata exposes an empty header** rather than
`None`, though §3 guarantees it and every consumer reading `.metadata` depends on it. Now
pinned by `tests/test_frames.py::test_frame_without_metadata_exposes_an_empty_header_not_none`.

All three were closed by register **C-80**. The gap is recorded rather than deleted because a CIC
states its guarantees whether or not a test exists (ADR-006: tests are derived *from* the
contract), and because this document is the reason the gap was found at all — it was written,
the gaps were noticed while writing it, and they were closed the same day.

---

Expand Down
6 changes: 3 additions & 3 deletions docs/CICs/Reconcile.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,9 @@ ReconciliationModule().reconcile(cm, pgm) # TypeError — map_keys/map
# approximation (C-62 / ADR-024) — read res.mode and treat joint tails as uncalibrated.
res = module.reconcile_result(cm, pgm) # res.mode == ALIGNED_DRAWS is a caveat

# WRONG: mutating the returned frame's value buffer in place (the leaf value buffer is
# immutable-by-convention; see ADR-025 / PredictionFrame CIC §9) -> may corrupt shares
out.values[:] = 0 # unsupported
# WRONG: mutating the returned frame's value buffer in place. Since 2.0.0 the buffer is
# write-protected (ADR-028 / register C-66), so this raises rather than corrupting shares.
out.values[:] = 0 # ValueError: assignment destination is read-only

# WRONG: stamping the mode onto the leaf frame's metadata. It is reported on the result.
out.with_metadata(mode="point-broadcast") # the leaf carries no reconcile vocabulary
Expand Down
3 changes: 2 additions & 1 deletion research/coherent_posterior_summary_note.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ A `k <= 0` floor (too few samples to hold two draws, at small `S`) collapses to

### 3.3 A mass-aware tip — the *shorth* (robust mode, distribution-agnostic zero handling)

The point estimate is the **median of the floor at a configurable `tip_mass`** (default `0.5` —
The point estimate is the **median of the floor at a configurable `tip_mass`** (default `0.25`
since ADR-019 Amendment 3, 2026-07-24; this note was written at v1.3.0 when it was `0.5` —
the *shorth*, the shortest-half mode), **not** the degenerate narrowest floor. Two properties
follow:

Expand Down
Loading