fix(gp): NumericCategory is a filed gap, not a permanent refusal

Review findings on this branch, all three in the gp commit.

The prose was the worst of them: §11 called Artefact::NumericCategory a
permanent refusal that "no vocabulary closes". It is not. fit_measure hands
preprocess an Overrides::default(), so it never says which header columns are
categories — although the next line tells Features::of — and preprocess then
infers Numeric -> Date -> Categorical from the values. A `Year` category with
items 2024/2025 becomes a numeric column and is refused, with `Year IS 2024`
spellable all along. Forced::Categorical is the existing knob. Filed as
improvise-zv8t. Prose that calls something permanent is what stops the ticket
being filed, which makes this the branch's own defect class one level up.

MissingFlag's words said "that measure", but its arm is not conditioned on
is_category and dataset.rs writes "" for an absent coordinate, so a .missing
flag over a category is reachable — the same "declined in words naming
something they are not" the commit was written to fix. Now "that column".
ValueOfAMeasure's tail likewise covered only the present case.

The test module claimed "each artefact family" while its fixture reaches three
of five; named for what it reaches. Re-injected the collapsed-reason fault
after the reword: still fails, so the negative clauses still bite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-20 22:06:34 -07:00
parent 22bb621dd8
commit 630b84066c
3 changed files with 43 additions and 14 deletions
+26 -4
View File
@@ -369,10 +369,32 @@ than storing a formula whose every cell is a reference error (improvise-8xhy).
`improvise-4zl7`'s route-then-aggregate rule (`context/code/core.md` §13) is
untouched — nothing new is emitted, so there is nothing new for it to hold over.
The other three families are declined permanently rather than pending: a one-hot
over a measure and the `(missing)` pseudo-item name no coordinate *by
construction*, and a numeric category column is a name collision the document
itself creates. No vocabulary closes those; they are refusals, not gaps.
Two of the other three are declined permanently: a one-hot over a measure and
the `(missing)` pseudo-item name no coordinate *by construction*, so no
vocabulary closes them. They are refusals rather than gaps.
**`NumericCategory` is not, and this paragraph said it was** — which is the
more useful half of improvise-wd6n, because prose that calls something a
permanent refusal is what stops anyone filing the ticket. `fit_measure` hands
`preprocess` an `Overrides::default()`, so it never says which header columns
are document categories, although the very next line tells `Features::of`.
`preprocess` then infers `Numeric` → `Date` → `Categorical` from the cell
values, so a `Year` category with items `2024`/`2025` becomes a *numeric column*
and a category of ISO dates becomes a *date column* — and both are refused,
`Year IS 2024` being exactly spellable all along. That needs no
formula-language change whatever: `Forced::Categorical` already exists and is
the knob. Filed as **improvise-zv8t**. The same accident is why a `DatePart`
can arrive over a category column, so the date family's decline above is
narrower than it looks: it covers a genuine date *measure*, not a category
misread as one.
**And the refusal's own words must not name a column kind the arm does not
know.** `FeatureRole::Missing` is *not* conditioned on `is_category` — unlike
its `Numeric` and `OneHot` neighbours — and `dataset.rs` writes `""` for a
record with no coordinate for a category, so a `.missing` flag over a
**category** is reachable. Its clause therefore says "that column" and not
"that measure": naming the wrong kind there would be this section's own charge
— declined in words naming something they are not — surviving one level down.
## 12. A fit is persisted as its formula, and nothing else (improvise-7na)
+10 -4
View File
@@ -603,7 +603,13 @@ precisely the state the ticket describes. Fault injected: the five arms of
family, and with the injection removed all three arms pass.
The fixture reaches three families from one `preprocess` call (a text measure
column, a measure with a hole in it, a column of ISO dates). The remaining two —
`AbsentCoordinate` and `NumericCategory` — are not reachable from it and are not
asserted here; they are refusals rather than pending gaps, argued in
`context/code/gp.md` §11.
column, a measure with a hole in it, a column of ISO dates), which is why the
module is named for what it reaches rather than for "each family". The
remaining two are not reachable from it and are not asserted here.
`AbsentCoordinate` is a permanent refusal; `NumericCategory` is **not**, and
the first draft of this entry and of `gp.md` §11 both called it one — review
found that `fit_measure` simply never tells `preprocess` which columns are
categories, so it is a filed gap (improvise-zv8t) rather than a refusal. Worth
recording as the failure mode: the branch that *invented* a classification also
wrote the prose declaring one of its arms unfixable, and nothing but a reader
could have caught that.
+7 -6
View File
@@ -33,7 +33,8 @@ pub enum Spelling {
/// different vocabulary to close. `context/code/gp.md` §11 carries the arguments.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Artefact {
/// `col.missing`: "this record has no value for X" (improvise-wd6n).
/// `col.missing`: "this record has no value for that column" — of a measure
/// or of a category, so the words must not claim either (improvise-wd6n).
MissingFlag,
/// `when.year` and its siblings: date arithmetic (improvise-wd6n).
DatePart,
@@ -51,7 +52,7 @@ impl Artefact {
fn reason(self) -> &'static str {
match self {
Self::MissingFlag => {
"says this record has no value for that measure, which the formula language \
"says this record has no value for that column, which the formula language \
has no word for: a reference to an absent cell is an error, not a zero"
}
Self::DatePart => {
@@ -60,7 +61,7 @@ impl Artefact {
}
Self::ValueOfAMeasure => {
"is a value of a measure rather than a coordinate, so `IS` cannot ask about it \
— the record's value for that column is text"
— a measure's values are text, and its absent case is no value at all"
}
Self::AbsentCoordinate => {
"is the absence of a coordinate rather than an item, which `IS` reads as false \
@@ -843,9 +844,9 @@ mod tests {
assert!(fitted_formula(&tree, &features, TARGET, MEASURE, &model).is_ok());
}
}
/// improvise-wd6n. The artefact families are refused *separately*, because
/// they are declined for different reasons. Evidence: `context/test-evidence/gp.md`.
mod each_artefact_family_is_refused_in_its_own_words {
/// improvise-wd6n. The families a `preprocess` run reaches — three of five —
/// are refused separately. Evidence: `context/test-evidence/gp.md`.
mod the_artefact_families_this_fixture_reaches_are_refused_separately {
use super::{MEASURE, TARGET, leaf, split};
use crate::{Features, fitted_formula};
use gp_tree::data::{Overrides, preprocess};