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:
+26
-4
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user