--- id: shape title: A feature's weight is a curve, not a number status: blocked dependsOn: [training, features] exitCriterion: > Every feature contributes f(x) from a stored curve, monotone wherever the semantics demand it, and a decision log still prints named contributions that sum to the score. --- # shape `Weights::score` is `total += weight * reading.value`. One number per column says *more is proportionally better, forever*, and nothing in BattleTech is shaped like that: damage saturates past what a location can absorb, heat has a knee, a piloting roll has a cliff at twenty, distance to an objective is plausibly U-shaped. `overkill` - "the chance this volley puts more damage into some location than that location can absorb" - is a hand-built column for a shape a weight could not express. It is the evidence that some of the 54 columns exist to fake curves. ## The change `weight * value` becomes `f(value)`: a piecewise-linear curve with a handful of knots, stored in the weights file as a short knot table. **Still a dot product over an expanded basis**, so fitting stays convex and `Weights::score` barely moves. A knot table is more legible than a scalar, not less, and the vignette becomes a plot of the curve rather than a number beside a board. ## Monotonicity is the part worth insisting on Constrain the curve's direction wherever the semantics demand one: more `expected_damage` is never worse, more `attacker_self_damage` is never better. That makes the model **structurally unable to learn something a player would call absurd**, and because it is a regulariser it costs less data rather than more - which matters when a config-identical control arm has moved a headline metric by 23 points. ## Interactions, named only A GAM is additive and cannot form a product. That is not theoretical: `value_destroyed` and `target_current_bv` are both fractions, and *absolute* value removed is their product, which no sum of the two can reach. Add a pairwise term only where the mechanism fits in one sentence, each with its own vignette. **Never a search over pairs.** `overkill` is the precedent. ## Fix the fingerprint first `basis_fingerprint` hashes each feature's name, normalisation and description, so it is already blind to a changed computation. Curves make that worse: two fits with identical column names and different knots would be indistinguishable. This has to be extended before the first curve lands. ## Out of scope MLP, embeddings, transformer. Post-hoc explanation is not interpretability - a SHAP value says what an opaque model did on one input; it does not let a player read the reasoning and say it is wrong about BattleTech. The additive decomposition is the product, not the accuracy. - [ ] Extend `basis_fingerprint` to cover knot positions - [ ] A knot representation in the weights file, and `Weights::from_document` reading it beside today's scalars - [ ] Monotone fitting, with the direction declared per feature rather than inferred - [ ] `featuredoc` rendering a curve instead of a number - [ ] Convert the first saturating column, and delete whatever was patching it