build(deps): bump mcp from 1.27.0 to 1.28.1 in /clients/python/pymat-mcp - #243
Open
dependabot[bot] wants to merge 1 commit into
Open
build(deps): bump mcp from 1.27.0 to 1.28.1 in /clients/python/pymat-mcp#243dependabot[bot] wants to merge 1 commit into
dependabot[bot] wants to merge 1 commit into
Conversation
Bumps [mcp](https://github.com/modelcontextprotocol/python-sdk) from 1.27.0 to 1.28.1. - [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md) - [Commits](modelcontextprotocol/python-sdk@v1.27.0...v1.28.1) --- updated-dependencies: - dependency-name: mcp dependency-version: 1.28.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
gerchowl
added a commit
that referenced
this pull request
Aug 18, 2026
…ces, finishes The README is generated from tests/test_readme_examples.py, so an addition to the public API that is not exercised there is invisible to users even though it ships. This branch added three user-facing capabilities with no README coverage. Adds three examples, each of which is a real executable test: - wavelength-dependent optics, using BGO to show why it matters — the scalar refractive index is fitted near the emission peak, so n at the blue end of the band is understated, and the clamp-not-extrapolate contract is visible through `range_nm` - declared absences, showing the distinction the mechanism exists for: LYSO's emission spectrum is absent-with-a-reason, while a property nobody has spoken about stays silent - the measured surface-finish catalogue, showing the air-gap versus optical-contact distinction on the same reflector 27 -> 30 examples; README regenerated and matching. Refs: #243
9 tasks
gerchowl
added a commit
that referenced
this pull request
Aug 18, 2026
… uncited value
Two CI failures, both real.
TYPOS. The `typos` hook rewrites three things this branch introduced, and all
three rewrites are wrong:
tio -> to `tio` is TiO2, titanium dioxide reflective paint. It appears in
the Geant4 RealSurface finish names (polishedtioair,
etchedtioair, groundtioair) and in the catalogue keys derived
from them. The rewrite would silently rename three measured
surfaces — and note it caught the KEYS while leaving the
verbatim G4 enum strings alone, so the two would have drifted.
Hass -> Hash G. Hass, "Filmed surfaces for reflecting optics", JOSA 45, 945
(1955). A surname, cited for evaporated-aluminium reflectance.
mis -> miss "mis-parsed" as a prefix, in a Rust doc comment.
TEST. `test_apply_scintillator_to_shape` pinned prelude420's light yield at
34000. That was the uncited round-up this branch corrected to the Luxium data
sheet's 33200, so the test was asserting the defect. Updated with the reason
inline.
Worth recording WHY that one reached CI rather than being caught locally: it is
gated behind `pytest.importorskip("build123d")`, and build123d pins
`python_version < '3.13'` while this machine is on 3.13. The local suite skips
64 tests where CI skips 54, so ten tests never ran here. I swept the
build123d-gated files for any other assertion touching data this branch
changed, and checked the corpus for tests asserting a root material's grade or
vendor IS None — the case my loader fix inverts. Nothing else is affected.
Refs: #243
gerchowl
added a commit
that referenced
this pull request
Aug 18, 2026
…ption BLOCKER from pre-merge review, verified and reproduced identically in both languages: k=0.1, s=572 /cm, d=0.2 mm, Rg=0.9 -> R=95.13 T=7.97 A=-3.09 k=0.001, s=1 /cm, d=1 cm, Rg=0.5 -> R=66.58 T=49.94 A=-16.52 The docstring claimed the tuple sums to 100 "by construction" and that A is "the only true loss". The sum held — because A was DEFINED as 100-R-T — but the interpretation did not. With a reflective backing, Kubelka-Munk's R is the reflectance of the COMPOSITE, layer plus backing, including light that crossed the layer, bounced, and came back. T remains the layer's own transmittance. They are not two parts of one photon budget, so the remainder stops meaning "absorbed" and goes negative. The k=0 branch additionally ignored the parameter outright, so a non-absorbing layer on a perfect mirror returned sd/(1+sd) rather than R -> 1. REMOVED rather than repaired. Getting it right means first deciding what a three-way split even means when R and T are measured against different references, and that is a physics question with no consumer waiting on it — the one downstream user passes the default. Shipping a knob whose documented meaning I got wrong is worse than not shipping it. It can return when there is a physical definition and someone who needs it. At Rg=0 the numbers are unchanged and correct: a transmitted photon is gone, which is the right model for an inter-crystal septum and every use this branch has. Why the suite missed it, which is the part worth keeping: the single test touching the parameter asserted `bright > black` — an ORDERING property. That passes throughout the defect. A conservation claim has to be tested as a conservation claim, and the replacement sweeps 24 wavelength/thickness combinations asserting A >= 0 and R+T+A == 100. Removal is pinned too, so re-adding the parameter is a deliberate act rather than a merge artefact. Also from review: - Rust `Absent::from_toml` accepted any `reason` string while Python enforces a closed vocabulary. Data arriving through `MaterialDb::open` on a hand-authored directory never passed the Python gate, so it now validates against `ABSENT_REASONS` there too rather than inheriting a weaker guarantee from whichever path was taken. - `obliquity_factor`'s cap at 40 fired silently; it now logs at debug when it clamps, so "computed" and "capped" are distinguishable. Refs: #243
gerchowl
added a commit
that referenced
this pull request
Aug 18, 2026
…bering CI's `pymarkdown` hook auto-fixes; the job fails when it changes a file. Two things in the strata response doc: - two indented code blocks converted to fenced (MD046) - an ordered list renumbered 4/5/6 -> 1/2/3 (MD029), which is also the more correct reading: the prose introduces it as "three found while implementing", so it is a new list rather than a continuation. Checked that nothing in the document cross-references those numbers. Ran through the same pinned version (0.9.23) rather than hand-patching to match a diff, and swept every markdown file the hook covers to confirm nothing else drifts. Verified idempotent. Note for anyone else on NixOS: pymarkdown's `pyjson5` dependency needs libstdc++ on the loader path, so it fails to import out of the box. `LD_LIBRARY_PATH=$(nix eval --raw nixpkgs#stdenv.cc.cc.lib)/lib` makes it run. Refs: #243
gerchowl
added a commit
that referenced
this pull request
Aug 18, 2026
… absences, rs-materials 0.3.0 (#244) * fix(vis): make mat_vis_client optional — pymat must import without it `pymat/__init__.py` does `from . import factories, registry, vis` eagerly, and `pymat/vis/__init__.py` imported `mat_vis_client` unguarded. So importing pymat AT ALL required a visualisation client, even to reach `pmma` / `water()`. That makes pymat unusable in every headless context: CI, geometry export, and any consumer that only wants material data. It surfaced downstream in strata, where the NEMA phantom fixtures could not be regenerated because nema_wagi.py does `from pymat.factories import air, water` and mat_vis_client is not installed there (and is not on PyPI) — see gerchowl/strata#1047. Guarded the import the same way PIL is guarded in the test suite (#242). The names stay bound, so module-level references still resolve; calling one without the package raises ImportError naming the missing extra rather than failing at import. Nothing about material data depends on the client: factories.py imports only `.core` and `.properties`, and its `vis={...}` entries are literal appearance dicts (base_color/roughness/ior). The domain types FinishEntry/Vis/VisDeltas come from `pymat.vis._model`, not from the client, so they are unaffected. Verified: `import pymat; water().formula` -> H2O and `pmma` -> "PMMA (Acrylic)" with mat_vis_client absent, and the full strata NEMA fixture regeneration now runs with no stub on PYTHONPATH. Committed locally, not pushed — review before publishing. * feat(optical): wavelength curves, surface-finish catalogue, declared absences Grows the optical schema along the wavelength axis so a downstream Monte Carlo transport engine can stop sampling monochromatically at the emission peak, and adds a catalogue of measured optical interfaces. WavelengthCurve mirrors TempCurve — piecewise linear, clamps rather than extrapolates, validated at construction and therefore at load. TempCurve's public API is unchanged; both now share the validation and interpolation helpers. Accessors n_at(L), absorption_length_at(L), emission_at(L) and the self-absorption pair are deliberately named apart from refractive_index_at(T), which has meant temperature since #148; overloading on argument type would have been a silent behaviour change for every existing caller. emission_at has no scalar fallback — emission_peak is one point on a band, not its shape. Adds the self-absorption split (absorption_length_matrix / _reabs / reemit_qe). A photon absorbed in a doped scintillator has two physically distinct fates and one lumped attenuation length cannot express the difference. Adds Material._absent — a sidecar, keyed by dotted property path with a closed reason vocabulary, that distinguishes "nobody looked" from "we looked and the number does not exist". LYSO needs it immediately: its emission spectrum exists only in paywalled figures, and its self-absorption channel is measured while its matrix-loss channel is not. Adds pymat.surfaces — 30 measured interfaces (21 LBNL + 9 DAVIS) from the Geant4 RealSurface 2.2 data set, each carrying its exact G4OpticalSurfaceFinish spelling and a DOI. Surface is its own type, not a Material: it has no density, formula or mass. Air-gap and optical-contact coupling are a validated closed vocabulary, so the two are finally distinguishable. Geant4's six analytic UNIFIED finishes are excluded — they are model selections, not measurements. Recovers five fields that were on disk with no dataclass slot to land in, so the loader's hasattr guard dropped them on every load since #147: optical reflectivity (ESR's 98.5%), dopant, dopant_pct, compliance flammable and toxic. Moves ferrite's permeability from electrical to magnetic. A regression test now fails if any TOML key anywhere lacks a field to receive it. Fixes a stale short alias: sources.py mapped radiation_length to optical.* after #157 moved the property to nuclear, so mat.cite("radiation_length") silently resolved to a path no TOML writes. Populates LYSO optical data with provenance on every value, and declares five gaps explicitly rather than inventing numbers. Corrects PreLude 420 light yield from an uncited 34000 to the datasheet's 33200. Adds CC-BY-3.0 to the license allow-list for JINST. Refs: #243 * feat(rust): rs-materials 0.3.0 — structured fields, curves, uncertainty, provenance The crate exposed four Option<f64> optical scalars and silently dropped every structured field, every curve, all uncertainty and all provenance. It now types the physics domains in full fidelity — optical, nuclear, mechanical, thermal — including wavelength spectra, temperature curves, decay components, the self-absorption split, per-field stddev, _sources and _absent. manufacturing, compliance, sourcing and vis are deliberately not mirrored. Material::raw() returns the merged TOML table with parent overlay applied, so nothing in the database is ever unreachable from Rust. That is a guarantee; a promise to maintain full parity would decay the first time the two sides were edited a week apart, which is how schema drift arises in the first place. Adds SurfaceDb over the new surfaces.toml, with by_lut_surface() as the reverse index a consumer holding a Geant4 finish name needs, plus family() and with_coupling(). Coupling is an enum, so air-gap and optical-contact cannot be confused. Two parser bugs fixed while in there: - Inheritance only ever consulted one level of parent, so a grandchild such as lyso.Ce.saint_gobain.prelude420 silently lost everything its grandparent declared. - Property groups were replaced wholesale rather than merged key by key, so a child overriding light_yield dropped its parent's refractive_index. PROPERTY_GROUPS was also missing magnetic, vacuum, nuclear, vis and custom; those avoided being mis-parsed as child materials only by accident. Behaviour change: formula no longer inherits from the parent node. py-mat's loader does not inherit it either, so the previous one-level lookup was itself a silent divergence from the source of truth. The radiation_length/interaction_length move to NuclearProperties (#157) was already correct here as of 0.2.0 and is now covered by a test. Refs: #243 * docs(adr): ADR-0004 — optical transport, surface finishes, the substance/assembly line Records the separation-of-concerns decision negotiated with strata, including where py-mat disagreed, and commits the incoming brief so the decision trail is durable rather than living in one agent's context. Accepted: py-mat owns substance physics and measured interfaces; the consuming engine owns what was built and how it is simulated. The operative test is not "is it about light?" but "did someone measure it, and does the number survive moving the part?" Narrowed in two places: - The catalogue holds measured interfaces only. Geant4's six analytic UNIFIED finishes carry no data file and no citation; they are model selections parameterised by a fitted sigma_alpha, so they are run-time policy. The three bare-air LBNL enum members ship no .dat either and are named in a comment rather than given entries. - default_surface on a material is rejected. That a crystal is polished is a fact about the crystal; that a polished crystal is then wrapped in ESR with grease rather than Teflon in an air gap is a fact about a detector somebody built — the brief's own reasoning. It would also inherit down to every vendor variant as a wrapping choice nobody made. The variant itself is accepted and tested. Answers the brief's four open questions with evidence, and corrects three of its claims about py-mat: rs-materials was at 0.2.0 with the #157 migration already applied (strata is pinned to a stale release), the optical scalar count was four not six, and the live drift was on the Python side, in a short alias the brief did not look at. strata-optical-response.md is written for the strata agent to read directly from the filesystem, including the LYSO emission-spectrum finding: no redistributable tabulated source exists, so the band must be synthesised downstream from cited parameters and labelled as synthesised. Refs: #243 * docs(data): state the dispersion-ownership rule once, at the top of scintillators.toml The refractiveindex.info enricher (#164) owns `refractive_index_dispersion` for the six materials in its scope, and hand-authored data there would mask the automated pull. That rule was previously a comment on one ceramic and one scintillator; it now lives once in the file header with the full in-scope and out-of-scope lists, and the per-material comment defers to it. Also records in the strata response which P1 materials were deliberately left alone (gagg, nai_tl, csi_tl, plastics) and why: the literature sweep behind this work covered LYSO and BGO only, and populating the rest would mean asserting sources nobody checked. Refs: #243 * fix(loader): root materials lost grade/temper/treatment/vendor; gate loader parity Two bugs, both found by cross-checking the Python and Rust loaders against each other rather than by reading either one. `loader.py` built every material with grade=grade or parent_material.grade if parent_material else None which Python parses as `(grade or parent.grade) if parent else None`. For a ROOT material `parent_material` is None, so the whole expression collapsed to None and the node's own value was discarded. `pymat.beryllium.grade` was None despite the TOML declaring "S-200F"; `pymat.esr.vendor` was None despite "3M". Eleven grades and six vendors were affected across the corpus, plus temper and treatment. Children were unaffected, which is why it survived this long. The Rust side merged a child's `tags` over its parent's instead of unioning them, so `raw()` reported four tags for stainless.s316L where py-mat reports seven — a divergence from the #132 inherit-and-extend rule. `tags` is now typed on the Rust `Material` too, so consumers do not have to reach through `raw()` and hit the trap. Adds tests/test_rs_python_parity.py, which drives `cargo run --example dump_parity` and diffs 26 fields across every material against the Python loader — 144 materials, ~3700 value pairs. It skips when cargo is absent and runs in the `rust` CI job, the only one with both toolchains. This is the gate the schema has been missing. The two loaders parse the same files independently and have drifted before: #157 moved radiation_length to nuclear and the Rust side lagged, and the strata brief was filed largely because the crate had fallen behind. Conventions did not catch that; a diff does. It was mutation-tested by reintroducing the precedence bug above and confirming the gate fails with a readable diff. Refs: #243 * docs(briefs): record the loader-parity gate and the two bugs it surfaced The drift complaint behind the strata brief deserved a concrete answer, not just a schema that happens to be in sync today. Documents the parity gate, the root-material identity bug and the Rust tag-merge divergence, and why neither was findable by reading either loader on its own. Refs: #243 * fix(schema): close three loader asymmetries found in review None are blockers; all three are the same shape — a code path that fails one way in one branch and another way in the other, which is how a bug hides. 1. `_validate_wavelength_slot` was guarded on `isinstance(value, dict)`, so a scalar written where a spectrum belongs skipped validation entirely, landed in a dict-typed field, and only failed at the first `_at(lambda)` call — far from the file that caused it. It now raises at load with the expected shape named. 2. `emission_at` validated its wavelength argument only after the no-spectrum early return, so `emission_at(300 * ureg.kelvin)` returned None on a material without a spectrum and raised on the next material along. Validated before the return, matching what the other accessors already did. 3. Rust `Curve::from_wavelength_toml` picked the first of `values` / `n` / `intensities` present, where Python raises on ambiguity. A file naming two would have been interpolated against whichever column the function happened to check first. It now refuses, and the loader uses a new keyed variant that names each slot's column explicitly — so writing the wrong column yields no curve rather than a curve built from the wrong data. Also types `temper` on the Rust `Material` and extends the parity gate to 28 fields, now including `temper`, `tags`, and the `_sources`/`_absent` KEY SETS. The Rust provenance parsers use `filter_map` and would drop a malformed row silently where Python raises; comparing key sets turns that silence into a diff. Mutation-tested by simulating a dropped row and confirming the gate names the missing path. Refs: #243 * feat(data): run the refractiveindex.info enricher; derive reflectance from n,k Runs `scripts/enrich_from_refractiveindex.py --write` for all nine in-scope materials, which had been sitting empty since #164 added the tooling. This is the action ADR-0004 §10 called for rather than hand-authoring the tables. Scintillators now carry CC0 dispersion: bgo (Williams 1996, 305-1000 nm), nai, nai.Tl, csi, csi.Tl, csi.Na (Li). The BGO case is the one that mattered — the single scalar 2.15 is fitted near the 480 nm emission peak, so n at the blue edge of the band was ~2% low and every critical angle computed there was wrong in the same direction. Metals now carry n AND k: aluminum (Rakic Lorentz-Drude), copper and gold (Johnson & Christy). With k on disk, reflectance stops being a number someone types and becomes one the database derives: OpticalProperties.k_at(lambda) OpticalProperties.normal_reflectance_at(lambda) # percent The second is the Fresnel result for a semi-infinite medium against vacuum. Aluminium comes out at 92.5% at 420 nm, 92.3% mean across 400-500 nm, with the characteristic interband dip at 800 nm — which is now a test, because if either the CC0 pull or the derivation breaks, that shape is what stops looking right. Deriving beats storing here: a hand-entered reflectivity scalar can drift away from the n,k it is supposed to be consistent with, and nothing would notice. The enricher appends new tables at the end of a material's span, which lands them below the following section banner — `[aluminum.optical]` under "COPPER & BRASS", `[copper.optical]` under "TUNGSTEN". Harmless to the parser, confusing to the next human, so the three blocks are relocated into their own sections. The enricher's placement is worth fixing separately. Refs: #243 * docs(adr): sharpen ADR-0004 §3 — the test is measurement, not LUT-backing The original wording said "measured interfaces only" and then illustrated it entirely with LUT entries, which reads as "LUT entries only". That would exclude a pressed-BaSO4 reflector or an aluminium wrap, which are exactly as measured as a Janecek goniometer sweep — they simply produce R(lambda) rather than an angular table, and the Surface schema has carried `reflectivity` and `reflectivity_spectrum` fields from the start. Restates the line where it actually falls: an entry qualifies when the optical numbers it carries are measured and citable. What stays out is an entry whose only content is a fitted model parameter with no measurement behind it — a `sigma_alpha` tuned until the simulation matched is a knob, not a fact. Also records the consumer-side consequence, which is the reason these entries are named for the reflector and never for the crystal: a `lut` entry hands over an angular distribution directly, while a `diffuse`/`specular` entry hands over R(lambda) and the consumer composes it with the crystal's n(lambda). That composition depends on both materials, so it belongs on the side that knows which two are being joined. Refs: #243 * feat(data): BaSO4 reflectance, SiPM windows, second couplant, non-LUT surfaces Everything the concrete 8x8 LYSO / BaSO4-septum / Al-wrap / grease-SiPM module needs, driven by a downstream sensitivity result showing reflector loss (25% at the readout end to 63% at the far end) dominates bulk loss (13-29%). That makes reflector reflectivity the highest-value number in the whole model, ahead of more scintillator bulk data. BaSO4 as a diffuse reflector. Grum & Luckey 1968 (doi:10.1364/AO.7.002289), the primary reference for pressed BaSO4 as a reflectance standard: 0.999 at 420-470 nm, falling to 0.985 at 350 nm. The consumer's working estimate was 0.97, and over ~40 bounces that is 0.296 against 0.961 — the difference is not a refinement, it is a different model. Recorded with the caveat that matters: these are pressed powder at high packing density in an integrating sphere, and the paper's own BaSO4/PVA paint measures 0.992, so a real septum sits below. Also carries Kubelka-Munk coefficients (Patterson 1977, doi:10.1364/AO.16.000729), which answer a question that decides model structure rather than just a number: s = 572 /cm at 500 nm gives a ~17 um scattering mean free path, so a 0.2 mm septum is ~12 scattering lengths and is effectively optically thick — no need to transport into it. But only just, and looser-packed coatings at that thickness may transmit, which would appear as inter-crystal crosstalk a surface-only model has no channel for. SiPM entrance windows, both variants, because the datasheet distinguishes them and the field does not: S13360 CS packages use SILICONE at n = 1.41, PE packages EPOXY at n = 1.55. The widely quoted "Hamamatsu window is 1.55" is the PE part. From BC-630 grease at 1.465 the two are qualitatively different — CS steps the index DOWN and puts a TIR cone at the readout face, PE steps it up and does not. Also adds DOWSIL Q2-3067 as the named alternative couplant; its 70% transmission at 400 nm against BC-630's flat ~95% is a real difference for a 420 nm emitter. New schema: `reflectivity_spectrum` and `transparency_spectrum`, with `reflectivity_at` / `transparency_at`. The first was already registered in the loader's validation table with no dataclass field behind it — validated, then silently dropped. That is the same bug class this branch audited the corpus for, reintroduced by me, in the one direction the corpus scan cannot see (no file used the slot yet). There is now a structural test that every validated slot has a field. Surface catalogue grows two non-LUT entries, diffuse.baso4_air and specular.aluminium_air, per the sharpened ADR-0004 §3. They reference their reflector's material rather than copying R(lambda), so there is one copy to maintain and nothing to drift. `contact.grease_sipm` was requested and is refused: it would carry no measured number of its own, only a pairing of two materials that each already carry theirs, and pairing is assembly. A test pins its absence so the reasoning is not quietly reversed. Refs: #243 * docs(adr): ADR-0004 §11 — a photodetector is not a material Answers the question strata asked twice and explicitly asked us to decide rather than assume. A SiPM does not go in py-mat: PDE is a device's response at an operating point, not a property of a substance, and it does not survive being re-biased even though it survives being moved. DCR, crosstalk and afterpulse are worse on the same axis — dark count rate varies 2-3x unit to unit inside a single part number. Records where it does go instead, with a real revisit trigger rather than a brush-off, and notes the supporting fact that settles it independently: no redistributable tabulated PDE(lambda) exists for the S13360-3050CS at all. The datasheet has a figure, not a table, and no CC-BY paper measures that exact part — so putting it here would mean shipping a digitised proprietary figure, which the provenance policy already forbids. The two window materials are the part that IS ours, and they are now present. Refs: #243 * docs(briefs): record round 2 — the concrete detector and what re-ordered it The consumer came back with a sensitivity result rather than a request, and it changed the work order: reflector loss dominates bulk loss by roughly 2x with depth, so reflector reflectance provenance outranked more scintillator data. Worth recording as a pattern — stating sensitivity before requesting data is what made the prioritisation correct rather than lucky. Records the BaSO4 result (0.999 against a working estimate of 0.97, which over 40 bounces is 0.96 against 0.30), the optical-thickness finding that decided model structure rather than a value, the derived-not-stored aluminium reflectance, the n=1.41 vs 1.55 window correction, and the two refusals. Also records a bug I introduced and then caught: `reflectivity_spectrum` was validated by the loader with no field behind it. That is the same silent-drop class this branch audited for, in the one direction a corpus scan cannot see — an invariant has to be checked against the schema, not against the data that happens to exist today. Refs: #243 * docs: retract a superseded claim; record that sensitivity is not a property of one parameter The round-2 sensitivity result that re-ordered this branch's priorities has been retracted downstream. It was computed at an ASSUMED reflector reflectance of 0.97; at the cited value the model inverts — far-end wrap loss falls 64% -> 5% while bulk loss rises 29% -> 69%. The reflector does not dominate. The re-prioritisation was still correct, but for the opposite reason to the one given: reflectance provenance mattered because the assumed value was wrong, not because reflector loss is the dominant channel. The claim is struck through in strata-optical-response.md rather than deleted. A durable record that quietly edits away a retracted conclusion is worse than one that never made it — and this is a case of right-answer-from-wrong-reasoning, which is the hardest kind to notice later. Also records the finding underneath it, as ADR-0004 §12: the two parameters interact, so no standalone sensitivity is meaningful. Bulk absorption moves collection efficiency +17% at R=0.97 and +53% at R=0.999. "X is second-order" is never a property of X — it is a property of X at whatever value of Y was assumed, and when Y is the parameter without provenance, the conclusion inherits that gap silently. Two consequences for what this repo ships, both already practised on this branch and now written down: a bracket beats a point estimate when the quantity is consumed non-linearly (BaSO4 enters as R^40, so 0.97 vs 0.999 is 0.30 vs 0.96), and the uncertainty can be load-bearing in both directions at once — the same sweep shows a better reflector destroys DOI resolution, collapsing the depth gradient 8.4:1 -> 1.6:1. Strengthens the `[lyso.optical] absorption_length` source note with the measured interaction, since the 200 mm Monte-Carlo convention is more dangerous than the standalone figure suggested, and raises the recorded priority of the absent matrix-loss channel. Both caveats are now pinned by tests, because the caveat is the part a later editor tidies away and it is the part that was load-bearing. Refs: #243 * feat(optical): Kubelka-Munk coefficients — the crosstalk channel, and a correction A downstream crosstalk measurement (~15% light share into direct neighbours, against a model producing 0% by construction) refuted an argument I made two rounds ago, so this both corrects it and ships what closes it. WHAT I GOT WRONG. I said a 0.2 mm BaSO4 septum is ~12 scattering lengths and therefore "effectively optically thick — you do not need to transport into it". That conflated two different questions. Reflectance converges to its thick-layer limit quickly; transmittance does not. The K-M finite-layer solution at 420 nm gives T = 7.6% through 0.2 mm at pressed-pellet density, and still ~2% at the 0.5-0.6 mm vendors recommend. In a segmented detector that transmission IS the inter-crystal crosstalk channel, so the conclusion was wrong in exactly the way that mattered. Adds `optical.kubelka_munk = {wavelengths_nm, k, s}` as a bundled pair rather than splitting k and s across `absorption_coefficient` and `scattering_length`. They are one model's parameters, only their ratio is meaningful for the thick-layer limit, and `s` is not a transport mean free path — keeping them together under the model's name stops them being read as general optical constants. It is the first multi-column structured slot, so the loader now validates every column, not just the first. Accessors: km_k_at, km_s_at, km_reflectance_infinite_at, km_transmittance_at, mirrored on the Rust side. Verified the hard way — the thick-layer relation reproduces Patterson's own published R_inf (0.9624 / 0.9815 / 0.9846) to five decimal places from the stored coefficients, at all three wavelengths, on both language sides. AND A DEFECT THIS SURFACED IN DATA I ALREADY SHIPPED. baso4 now carries two cited primaries that disagree: Grum & Luckey give 99.90% at 420 nm, Patterson's coefficients imply 97.18% — a 2.7 point gap, 1.3-2.3 points across 300-700 nm. Both are real measurements of pressed BaSO4 at different packing densities. Neither is wrong, and I had shipped both without noticing they were inconsistent. The gap is not noise, it IS the packing-density sensitivity of this material, and it is the physical justification for shipping a bracket rather than a point estimate. Documented on the material and pinned by tests in both languages, because carrying two inconsistent numbers is defensible only if the material says so. Refs: #243 * feat(optical): finite-layer K-M split — R(d), T(d), A(d), and reframe BaSO4 around it The consumer closed the contradiction and it was not the one either of us thought: a high semi-infinite reflectance and a septum that leaks 15% are the SAME finite-thickness solution, not two competing facts. R_inf is what a thick pile returns; a 0.2 mm layer returns ~92% because the balance goes straight through, and that transmission IS the crosstalk channel. The gap that exposed: this repo shipped T(d) but not R(d). The accessor a consumer actually needs was missing, and the one they had — the semi-infinite reflectance — is the one they should not use for a real layer. Adds `km_split_at(lambda, thickness_cm, backing)` returning (R, T, A) in percent, summing to 100 by construction, plus `km_reflectance_at` for the common case. Both mirrored in Rust. Photons are conserved by the return type rather than by convention, which is why the split is one call and not three. Reframes the baso4 header on the consumer's own suggestion, and it is a better shape than what was there: a bracket answers "what is this material's reflectance" when the question a detector builder actually has is "what will MY layer reflect". So the header now ships the formula and a worked R/T/A table by thickness, and says plainly DO NOT adopt reflectivity_at() unless your layer is optically thick. A formula does not need a bracket. It also explains the 0.5-0.6 mm vendor recommendation without hand-waving: that is where transmission falls under ~3% and a coating starts behaving like the reflectance standard it is made of. The earlier packing-quality explanation was pointing at the right region for an adjacent reason; the thickness term does most of the work. One limit pinned because it is easy to get backwards: with k = 0 the thick-layer reflectance is EXACTLY 1, not 0.999. Absorption is not a small correction to a non-absorbing model — it is the entire reason R_inf sits below unity. Finite thickness drives R below R_inf; k sets R_inf itself. The two effects are separable and the tests now say so. The header's worked table is itself pinned by a test, so prose and behaviour cannot drift apart. Refs: #243 * feat(optical): oblique incidence on the K-M accessors; resolve a struck claim to a condition The consumer retracted the packing fit built on my numbers, and the retraction lands on something I had already struck. Both corrections are here. WHAT THEY FOUND. Their 47%-of-pellet packing fit came from identifying per-encounter transmittance with observed light share — different quantities, since a photon meets a septum ~40 times in a 3x3x25 crystal. The real mechanism is angular: TIR-trapped light meets the side walls at ~77 degrees, so the path through the septum is d/cos(theta) ~ 4.3d. At that path the same Patterson pellet coefficients give R = 96.9 / T = 1.4 / A = 1.8, with NO packing correction anywhere. The fitted quantity is geometric and belongs to their crystal, not to this material. That fit was offered here as a property of BaSO4 and was declined on the grounds that a fit against one module measures that module. Worth recording that the instinct paid: accepting it would have entered an arithmetic error into this corpus as a cited material constant. WHAT IT DOES TO MY OWN RETRACTION. I struck "0.2 mm is optically thick" when the finite-layer solution gave T = 7.6%. That figure is normal incidence. At 77 degrees T = 1.35% and R reaches 96.9% against a 97.18% semi-infinite limit — so the original claim holds in the regime that actually applies. It is not reinstated, because an unqualified claim that happens to hold in the applicable regime is still unqualified, and the refutation was missing the same term the claim was. It is recorded at THREE states — asserted, retracted, resolved-to-a-condition — because "struck, then reinstated with a condition" is a shape a two-state record cannot represent, and it is what occurred. The honest form is neither: a 0.2 mm septum is optically thick for grazing light and is not for near-normal light, and which applies is the consumer's fact. THE FIX THEY ASKED FOR. `incidence_deg` on km_split_at / km_reflectance_at / km_transmittance_at, plus a public `obliquity_factor`, mirrored in Rust. Capped at 40 near grazing, where a plane-parallel slab model has stopped describing anything. Verified two ways: it reproduces their 76.7-degree numbers exactly, and doubling the path by angle equals doubling it by thickness. This is the failure mode named last round, arriving from the other side. km_transmittance_at was correct, cited, tested and complete for the question it answered — normal incidence — and it steered a consumer wrong because their question was 77 degrees. Not a wrong value and not a missing one: a right one answering an adjacent question. Defaults are an answer, and this one was answering for a geometry the caller did not have. Refs: #243 * fix(optical): retract the grazing-incidence narrative — a diffuse reflector erases the angle Second retraction on the same number, and this one lands on text I committed last round. The consumer instrumented their proposed mechanism's own prediction rather than the value it was fitted to, and it died: measured mean side-wall incidence is 47.8 degrees, not the claimed 76.7 — a factor 2.9 in path length. The reason was in front of both of us. A BaSO4 septum is LAMBERTIAN, and a diffuse reflector randomises direction on first contact: mean |cos| = 2/3 exactly, i.e. 48.2 degrees, INDEPENDENT of aspect ratio. The grazing-incidence story reasoned carefully about the angular distribution of TIR-trapped light while forgetting that the wall it was reasoning about destroys that distribution. It is only valid for a specular wall. What that retracts here, all of which I had written down: - ceramics.toml told readers to pass incidence_deg=76.7 for a wrapped crystal and asserted a 0.2 mm septum "effectively IS" optically thick at that angle. Both gone. At the Lambertian mean it transmits ~5%, at normal ~7.6%, and it is not optically thick in either case. - The response doc recorded the claim at three states, ending in "resolved to a condition". It now has four, ending in "falsified", and the round-3 retraction stands as originally written. - Tests in both languages asserted 76.7 degrees as the physical angle in a wrapped crystal. The MATHEMATICS was and is correct, so the assertions survive as accessor tests with 76.7 relabelled an arbitrary long-path example; the physical claims around them are replaced. And a trap I built while fixing the last one. Offering `incidence_deg` invites exactly the error that followed, because Kubelka-Munk k and s are ALREADY defined for diffuse flux — the obliquity is averaged into them, which is where the factor 2 in the usual K = 2k convention comes from. For a diffusely-lit layer, plain d is correct and multiplying by 1/cos double-counts. A knob with no statement of when to leave it alone is another way of answering an adjacent question. Docstrings in both languages now lead with when to pass 0. New executable physics, because two plausible mechanisms died the same way: Lambertian mean cosine is 2/3; <1/cos> = 2 while 1/<cos> = 1.5 (Jensen, and the natural mistake is to reach for the second); and evaluating transmittance at a mean angle is not averaging it over the distribution, though here the two agree within ~0.1 points. The lesson, in the consumer's sharper phrasing: the second constraint has to be a prediction of the MECHANISM, not another property of the outcome. Their crosstalk shape was a real second constraint and killed the packing fit, but it said nothing about angle, so it could not distinguish grazing incidence from anything else giving the same transmittance. Only instrumenting the angle could. Refs: #243 * fix(optical): guard the thick-layer overflow; record the degeneracy that hid a wrong mechanism Found while checking a correction from the consumer, which was itself correct: the falsification test I proposed last round CANNOT FAIL. R_inf depends only on k/s, so thickness cancels and a thick-layer check against Patterson's published value passes identically for a multiplier of 1, 4.34 or 10. They ran it instead of accepting it. That is the third instance in this exchange of the same shape: a fit constraint that could not touch the mechanism, a mechanism that could not be separated from its rival by any output, and now a falsification test blind to the error it was proposed for. All three were real checks; none could fail in the relevant way. A test the wrong model passes is not a weak test, it is a non-test, and the way to tell is to ask what would have come back had the thing been wrong. Running that check at a 50 cm layer surfaced a genuine bug: km_split_at raised OverflowError, because cosh overflows an f64 above ~710. Now branched at bsd > 20, where coth is already 1.0 to machine precision. The branch is EXACT rather than asymptotic — coth -> 1 gives R = 1/(a+b), and since (1+x+sqrt(x^2+2x))(1+x-sqrt(x^2+2x)) = 1 identically, that IS R_inf. Fixed in both languages, with continuity across the cutover pinned. Also documents the degeneracy underneath the whole episode: thickness_cm and incidence_deg enter only through their product, so (0.02 cm, 76.7 deg) and (0.087 cm, 0 deg) are byte-identical. That is why a falsified mechanism kept producing correct numbers — "the light arrives at 77 degrees" and "the layer is 4.3x thicker than nominal" are the same claim in different clothes, and no check on the output can separate them. A consumer fitting against R or T is fitting the product and should say so. Refs: #243 * test: mutation-audit the optical gates; close the one non-test it found The consumer applied the "what does this return under the failure you are hunting" diagnostic to their own suite and found a non-test on the first pass. Accepting that lesson without running it here would have been the inflation we had just agreed to stop, so: ten mutations against the load-bearing optical paths, each executed rather than reasoned about. Nine detected. One did not: _absent stops inheriting -> all 1217 tests stayed green A child declaring its own absence would silently drop every absence it inherited. The existing test was blind for a specific reason worth recording: it used the SAME key on parent and child, where `{**parent, **child}` and `dict(child)` are indistinguishable. The distinguishing case is DISJOINT keys, and nothing exercised it. Three further findings from checking rather than assuming: - `_sources` was NOT affected — the pre-existing suite catches the same mutation there, because shipped materials actually depend on that merge. The gap was specific to the newer mechanism, which has no corpus behind it yet. New mechanisms are exactly where this audit pays. - The Rust side had the same hole, AND the cross-language parity gate — built in this branch specifically to catch drift — did not see it either. It compares what the CORPUS exercises, and no shipped material declares its own `_absent` under a parent that also has one, so the branch is dead data-side. A gate can only be as good as the data it runs over. Closed with a synthetic Rust fixture rather than by inventing corpus data to exercise a branch. - The overflow bug fixed last commit was the same shape: an absent assertion in a region nobody was looking at, because the only tested layer was the thin one the author cared about. Adds scripts/mutation_audit.py so this is repeatable rather than a one-off, and verifies the new gates catch the mutations they were written for — auditing the auditor, which is the consumer's phrase and the obviously correct next step. Refs: #243 * docs(briefs): round 5 — the discrepancy did not exist, and the data never moved The consumer's fourth retraction resolves the thread: the measured ~15% crosstalk was per direct neighbour normalised to the central crystal, while the simulation reported direct-neighbour light as a fraction of total collected — a factor of ~3.4. With the target corrected, Patterson's coefficients at nominal 0.2 mm reproduce both crosstalk (15.95% vs ~15%) and energy resolution, with ZERO fitted parameters. Records what this repository got wrong. Three mechanisms were invented to explain a discrepancy that never existed, and this repository ENDORSED the third — "the first version that predicts something outside the model it was fitted to, refutable by a microscope". That endorsement was correct on its merits and wrong in outcome: the microscope would have returned 0.2 mm, and the consumer would have concluded Patterson's coefficients fail for their geometry. A false refutation of correct data, reached through a sound falsification test, one measurement away. The lesson supersedes the earlier four in scope: a wrong comparison manufactures physics to explain itself, and every falsification downstream inherits the error. Rule 4 was "a test the wrong model passes is a non-test"; this is worse — a test the RIGHT model fails, because the target is wrong. Every method in this document was applied correctly and none could see it, because all of them were downstream of the comparison. Also records the tell, which was present twice and read as physics both times: the "wrong tail shape" that killed the first mechanism was the same normalisation error, comparing totals across 4 direct crystals against 59 further ones, which inverts the ordering combinatorially. And the practical form: cheap checks get skipped in proportion to how uninteresting they are, which is uncorrelated with how often they are the answer. This one was on the consumer's own candidate list and was passed over three times. No data changed. Every value survived four retractions unchanged, and the three schema refusals held — which is the strongest available argument for ADR-0004 §1's line that a fit against one module measures that module. Refs: #243 * feat(data): LSO:Ce intrinsic resolution + non-proportionality; declare the 511 keV gap The consumer's model produces an implied LYSO intrinsic resolution of 6.26% and was being judged against a 7-9% band they had asserted from memory — the only uncited quantity in their comparison, in a session where four of their uncited numbers had already been wrong. They asked whether a cited figure belongs here. It does: intrinsic resolution is a property of the scintillator, not of the module, so by ADR-0004 §1 it is ours. What is citable, now landed on `lso.Ce`: intrinsic_resolution_pct_at_662keV = 7.7 +/- 1.0 Chewpraditkul & Moszynski 2011, doi:10.1016/j.phpro.2011.11.035 non_proportionality = 43.0 Khodyuk & Dorenbos 2012, doi:10.1109/TNS.2012.2221094 The extraction method is recorded with the value because the value is inseparable from it. R_int is NOT measured — it is the residual after an assumed photostatistical term is subtracted in quadrature, so it inherits whatever the authors assumed for light collection. Chewpraditkul's figure comes from a 10x10x5 mm crystal on a PMT with N_pe = 6610; reproduced from their stated inputs as 7.72% against a published 7.7%. A consumer extracting R_int from a 3x3x25 mm crystal on a SiPM is not producing a comparable quantity. The +/-1.0 is deliberately NOT the paper's error bar (which is +/-0.3, on the total, not the residual). It widens for two effects the paper cannot capture: extraction-assumption sensitivity, and sample-to-sample spread — Khodyuk & Dorenbos Table I gives published LSO:Ce totals spanning 7.9-11.9%. What is NOT citable, declared absent on both `lso.Ce` and `lyso`: 511 keV: no primary source reports an extracted intrinsic resolution at that energy for either material. Extraction is conventionally done at 662 keV where the subtraction is most stable. R_int is energy-dependent, so the 662 keV figure is not a substitute, and 511 keV values exist only inside paywalled FIGURES. Reading one off would produce a number with the appearance of a citation and none of the substance. LYSO at 662 keV: exists but paywalled (Wanarak 2012, Sreebunpeng 2019). The absence note states explicitly that borrowing the LSO number would bias HIGH, since Wanarak measures LYSO total 8.2% against LSO 10.6%. Also relocates three more enricher-misplaced `refractive_index_dispersion` lines (bgo, csi.Tl, csi.Na) that were filed visually under the following section banner. Same defect fixed in metals.toml earlier; found here because inserting a table adjacent to one of them would have silently captured it. Refs: #243 * test(integrity): gate the banner-capture defect I fixed twice by hand The consumer's closing observation is that all three latent defects in this exchange were POSITIONAL — the enricher appending past a section boundary, their liveness check dead for models the corpus never used, and my `_absent` override untested because parent and child shared a key. Each was invisible to a test of the thing itself and visible only when something adjacent moved. They noted they had no general answer for making that class routine. A partial one, for the shape I hit: when the invariant is about STRUCTURE rather than behaviour, it is often checkable without knowing what anything means. "Every key is adjacent to its table header" needs no domain knowledge, so it can be a lint rather than a judgement. Adds that lint over every shipped TOML. A key separated from its table header by a SECTION BANNER parses as part of the preceding table but reads as part of the following section — correct to the machine, misleading to a human, and a trap for whoever inserts a table beside it next. Which is precisely how it surfaced: adding an `[lso.Ce._absent]` table adjacent to one would have captured it. The distinction that makes the check usable is banner-versus-prose. Most values in these files carry an explanatory comment and flagging those would make the check unusable — an unusable check gets deleted rather than obeyed. Only a box-drawing divider counts, since only that indicates a section boundary. Verified against the real defect rather than only a synthetic one: replanting the historical `[aluminum.optical]` misplacement makes it fail, naming the wrong owner (`[aluminum.a2024.mechanical]`). Also pinned against false positives on prose comments and on multi-line arrays like `decay_components`. Worth noting this was hand-fixed twice — metals.toml, then scintillators.toml — before being written down as an invariant. Fixing an instance twice without gating it is its own smell, and the second fix should have been the trigger. Refs: #243 * build(pre-commit): gate the two silent data-shape failures found in #243 Answers "what should be a hook so this does not recur". Two of the session's defect classes are cheap, structural, and were each invisible for months because NEITHER PRODUCES AN ERROR: - a TOML key with no dataclass field is parsed and then discarded by the loader's `hasattr` guard. `[esr.optical] reflectivity = 98.5` was dropped on every load from #147 until #243; an audit then found four more. - a key appended past a section banner parses correctly but reads as part of the next section, so the next insertion beside it captures it. Hand-fixed in metals.toml, then again in scintillators.toml, before anyone wrote it down as an invariant. Both belong pre-commit rather than only in CI, because both appear at the moment an adjacent thing moves — which is the moment of the edit, not minutes later in a pipeline. ONE IMPLEMENTATION, NOT TWO. The logic lives in scripts/check_data_shape.py and the existing tests now call it: the corpus-level test invokes the script through its real entry point, and the unit-level placement tests import the same function the hook runs. Adding a hook that duplicates a test would create exactly the drift these gates exist to prevent, so it does not. Stdlib-only, matching check_licenses.py: it parses properties.py with `ast` rather than importing pymat, so pre-commit can run it in a clean isolated interpreter with nothing installed. Verified on bare python3. Verified against the real defects, not synthetic ones — replanting the historical `[aluminum.optical]` misplacement and a bogus optical key both fail with the offending path named. Also pinned against the two false positives that would make it noise, and therefore ignored: prose comments before a key, and multi-line arrays like decay_components. NOT made hooks, deliberately: the mutation audit (runs the full suite ten times) and the Rust/Python parity gate (needs cargo) are too slow for the edit loop and stay in CI and manual use. Refs: #243 * docs(readme): cover the new public surface — wavelength optics, absences, finishes The README is generated from tests/test_readme_examples.py, so an addition to the public API that is not exercised there is invisible to users even though it ships. This branch added three user-facing capabilities with no README coverage. Adds three examples, each of which is a real executable test: - wavelength-dependent optics, using BGO to show why it matters — the scalar refractive index is fitted near the emission peak, so n at the blue end of the band is understated, and the clamp-not-extrapolate contract is visible through `range_nm` - declared absences, showing the distinction the mechanism exists for: LYSO's emission spectrum is absent-with-a-reason, while a property nobody has spoken about stays silent - the measured surface-finish catalogue, showing the air-gap versus optical-contact distinction on the same reflector 27 -> 30 examples; README regenerated and matching. Refs: #243 * fix(ci): typos allowlist for TiO2 and Hass; correct a test pinning an uncited value Two CI failures, both real. TYPOS. The `typos` hook rewrites three things this branch introduced, and all three rewrites are wrong: tio -> to `tio` is TiO2, titanium dioxide reflective paint. It appears in the Geant4 RealSurface finish names (polishedtioair, etchedtioair, groundtioair) and in the catalogue keys derived from them. The rewrite would silently rename three measured surfaces — and note it caught the KEYS while leaving the verbatim G4 enum strings alone, so the two would have drifted. Hass -> Hash G. Hass, "Filmed surfaces for reflecting optics", JOSA 45, 945 (1955). A surname, cited for evaporated-aluminium reflectance. mis -> miss "mis-parsed" as a prefix, in a Rust doc comment. TEST. `test_apply_scintillator_to_shape` pinned prelude420's light yield at 34000. That was the uncited round-up this branch corrected to the Luxium data sheet's 33200, so the test was asserting the defect. Updated with the reason inline. Worth recording WHY that one reached CI rather than being caught locally: it is gated behind `pytest.importorskip("build123d")`, and build123d pins `python_version < '3.13'` while this machine is on 3.13. The local suite skips 64 tests where CI skips 54, so ten tests never ran here. I swept the build123d-gated files for any other assertion touching data this branch changed, and checked the corpus for tests asserting a root material's grade or vendor IS None — the case my loader fix inverts. Nothing else is affected. Refs: #243 * fix(optical): remove backing_reflectance — it returned negative absorption BLOCKER from pre-merge review, verified and reproduced identically in both languages: k=0.1, s=572 /cm, d=0.2 mm, Rg=0.9 -> R=95.13 T=7.97 A=-3.09 k=0.001, s=1 /cm, d=1 cm, Rg=0.5 -> R=66.58 T=49.94 A=-16.52 The docstring claimed the tuple sums to 100 "by construction" and that A is "the only true loss". The sum held — because A was DEFINED as 100-R-T — but the interpretation did not. With a reflective backing, Kubelka-Munk's R is the reflectance of the COMPOSITE, layer plus backing, including light that crossed the layer, bounced, and came back. T remains the layer's own transmittance. They are not two parts of one photon budget, so the remainder stops meaning "absorbed" and goes negative. The k=0 branch additionally ignored the parameter outright, so a non-absorbing layer on a perfect mirror returned sd/(1+sd) rather than R -> 1. REMOVED rather than repaired. Getting it right means first deciding what a three-way split even means when R and T are measured against different references, and that is a physics question with no consumer waiting on it — the one downstream user passes the default. Shipping a knob whose documented meaning I got wrong is worse than not shipping it. It can return when there is a physical definition and someone who needs it. At Rg=0 the numbers are unchanged and correct: a transmitted photon is gone, which is the right model for an inter-crystal septum and every use this branch has. Why the suite missed it, which is the part worth keeping: the single test touching the parameter asserted `bright > black` — an ORDERING property. That passes throughout the defect. A conservation claim has to be tested as a conservation claim, and the replacement sweeps 24 wavelength/thickness combinations asserting A >= 0 and R+T+A == 100. Removal is pinned too, so re-adding the parameter is a deliberate act rather than a merge artefact. Also from review: - Rust `Absent::from_toml` accepted any `reason` string while Python enforces a closed vocabulary. Data arriving through `MaterialDb::open` on a hand-authored directory never passed the Python gate, so it now validates against `ABSENT_REASONS` there too rather than inheriting a weaker guarantee from whichever path was taken. - `obliquity_factor`'s cap at 40 fired silently; it now logs at debug when it clamps, so "computed" and "capped" are distinguishable. Refs: #243 * style(docs): apply pymarkdown fixes — fenced code blocks and list numbering CI's `pymarkdown` hook auto-fixes; the job fails when it changes a file. Two things in the strata response doc: - two indented code blocks converted to fenced (MD046) - an ordered list renumbered 4/5/6 -> 1/2/3 (MD029), which is also the more correct reading: the prose introduces it as "three found while implementing", so it is a new list rather than a continuation. Checked that nothing in the document cross-references those numbers. Ran through the same pinned version (0.9.23) rather than hand-patching to match a diff, and swept every markdown file the hook covers to confirm nothing else drifts. Verified idempotent. Note for anyone else on NixOS: pymarkdown's `pyjson5` dependency needs libstdc++ on the loader path, so it fails to import out of the box. `LD_LIBRARY_PATH=$(nix eval --raw nixpkgs#stdenv.cc.cc.lib)/lib` makes it run. Refs: #243
This was referenced Aug 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps mcp from 1.27.0 to 1.28.1.
Release notes
Sourced from mcp's releases.
Commits
777b8d0[v1.x] Support TransportSecuritySettings in the WebSocket server transport (#...4720467[v1.x] Set Development Status classifier to Production/Stable (#2976)6df3d73[v1.x] Buffer per-request StreamableHTTP streams; store priming event before ...32d3290[v1.x] Pass a list to parametrize in test_docs_examples (pytest 9.1.0 compat)...0dca751[v1.x] Deflake the child process cleanup tests (#2839)52258a9[v1.x] Add a v2 status banner to the README (#2835)b8f4917[v1.x] Deprecate the WebSocket transport and the experimental tasks entry poi...2309e5efix: omit null optional fields from task result payloads (#2809)494eb11[v1.x] Support Python 3.14 (#2769)6213787[v1.x] Scope experimental tasks to the session that created them (#2720)Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting
@dependabot rebase.Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
@dependabot rebasewill rebase this PR@dependabot recreatewill recreate this PR, overwriting any edits that have been made to it@dependabot show <dependency name> ignore conditionswill show all of the ignore conditions of the specified dependency@dependabot ignore this major versionwill close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this minor versionwill close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)@dependabot ignore this dependencywill close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)You can disable automated security fix PRs for this repo from the Security Alerts page.