Resolve what eligibility references, and whether local categories are mutable #31
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Question
Surfaced by the skeptical review pass, then sharpened after the first framing turned out to
be wrong — see the correction comment below.
The category taxonomy contradicts itself about what an eligibility row actually points at.
the portable vendor profile, and the acceptance graph operate on the spine."
meaning exactly what they meant" — and its entire supersession rescue is built on eligibility
referencing spine nodes.
The schema encoded decision 1 (
solicitation.local_category_id). Decision 4's eligibilityparagraph is therefore either wrong text or a second, unacknowledged model.
The question decision 4 was reaching for was never asked of the right object. The spine is
immutable with temporal validity and supersession. Retailer-local categories are the retailer's
own rows and nothing says whether they are mutable at all — and those are the rows eligibility,
group rules, and live solicitations actually reference.
Resolve:
both? Settle it once and correct whichever half of #26 is wrong.
which argues for mutability; they are referenced by eligibility rows, materialised audience
snapshots, and live solicitations, which argues for the same immutability the spine got.
eligibility rows and past solicitations against? A rename is safe if a label is not an identity.
A split is the dangerous one: vendors qualified in "Dairy" are not automatically qualified in
"Fluid Milk" and "Cheese", and guessing wrong in either direction has a cost —
over-qualification leaks an RFP to a vendor the retailer did not intend, which is the failure
this product exists to prevent, while under-qualification silently costs vendors bids.
flag the retailer to remap? Matching degrades either way; the question is whether it degrades
silently.
Constraints to respect: whatever is chosen must be expressible in RLS policy SQL without a
recursive closure (#26 decision 3); authorization-relevant values stay exact and non-derived; and
in-flight RFPs should already be immune via the audience snapshot (#12 decision 3) — confirm rather
than assume.
Parent: #1
Resolve taxonomy supersession against eligibility exactnessto Resolve what eligibility references, and whether local categories are mutableCorrection to the framing that created this ticket
The review pass claimed a spine split silently un-qualifies vendors. That was wrong: it
conflated retailer-local categories with spine leaves. Under decision 1 — which is what
the schema actually encoded — eligibility never references the spine, so a spine split
cannot touch it.
What survives the correction is sharper, and is now the ticket above:
mechanism only works under the reading the schema did not adopt.
of local categories, which they author freely — and those are the rows eligibility and live
solicitations point at.
Noted on the category taxonomy as well.
Resolution
Four decisions. The through-line: authorization never derives, and a taxonomy edit never changes
what was approved. Everything below falls out of holding those two lines together, which is what
the category taxonomy failed to do — it held the first for the spine and the second for
matching, and left the retailer's own rows in between unaddressed.
1. Eligibility references the retailer-local category, and only that
An eligibility row points at a
local_category. The spine is reached transitively through themapping, only by matching and the acceptance graph, and never by anything that decides access.
This confirms decision 1 of #26 and corrects decision 4. Decision 4's sentence — "old
eligibility rows and acceptance edges keep pointing at their node" — is only true of acceptance
edges. It was written as though eligibility referenced the spine, and its supersession rescue was
built on that reading. The schema had already encoded the other one
(
solicitation.local_category_id). Decision 4's eligibility clause is struck; its acceptance-edgeclause stands.
What this buys, beyond consistency. Spine churn can now never reach an authorization decision.
Taxonomy politics, external UNSPSC reissues, our own curation mistakes — none of it can move who
sees an RFP. The eligibility policy stays a plain equality on
local_category_idwith no closureand no traversal, which is exactly the RLS-expressibility constraint #26 decision 3 was protecting.
What it costs. Any question of the form "which vendors are qualified for this spine leaf, across
retailers" must join through the mapping and inherits its staleness. That is a matching-quality
problem, never an access problem — which is the correct place for it to be.
2. Local categories get the spine's mutability model
One model across both category kinds, not two sets of rules.
UPDATE label. A label is not an identity — carried over from #26 decision 4.valid_to. Rows referencing it still resolve and still mean what they meant.superseded_byrecords the edge.Why not free mutability. These rows are referenced by eligibility grants, materialised audience
snapshots, and issued solicitations. Free mutability permits repurposing — editing "Dairy" into
"Frozen" retroactively rewrites what every one of those records claims, and audit reconstruction
stops being trustworthy. Awards get protested years later; the record has to still be readable.
Why not push durability into consumers (each solicitation and grant carrying its own label copy).
It works, and it is more denormalisation than a POC needs — the same fact in four places, diverging
by design, with "who is currently eligible for X" becoming a harder question than it should be.
What it costs. A retailer cannot delete a category created by mistake, only retire it. Retiring
needs a real UI story, and "retired" must not read as an error state.
3. Split propagates to every successor; merge propagates only the intersection
The asymmetry is the substance. A split does not narrow an approval — it renames its parts. A
vendor approved for "Dairy" was approved for cheese, by the retailer's own act, because Dairy
covered cheese. Propagating to every successor therefore preserves the grant exactly; propagating
to fewer would revoke approval the retailer granted, silently.
A merge is the direction that actually widens. A vendor approved only for "Fluid Milk" was never
approved for cheese, so carrying the union into a merged "Dairy" would grant scope the retailer never
gave — and that vendor could receive a cheese RFP before any review happened. So a merge carries only
vendors approved for every input.
Propagated rows are real rows, written once, stamped with the event that caused them — not
derived at read time. #26 decision 3's ban is on read-time derivation inside policy; an explicit
audited write is not that, and the resulting rows are as exact as hand-granted ones. The two
decisions were never actually in conflict; the ticket that framed them as such was reading
"immutability" as "no writes".
Pruning is an explicit act. A retailer who split precisely because they now want finer control
prunes the propagated rows, and each prune is a recorded revocation. That is a new decision they
make deliberately, not something a taxonomy edit performs on their behalf.
What it costs. Propagation can touch thousands of rows and must be transactional with the
structural change — a half-propagated split is a live authorization defect. A merge visibly drops
vendors, and the retailer must be told plainly at the point of merge, not left to discover it
when an audience comes back small.
This is a privileged write path. It writes authorization rows across a retailer's entire vendor
set, triggered by a taxonomy operation rather than a grant. It belongs on the enumerated
privileged-path list the review pass handed to the layout.
4. A deprecated spine leaf auto-resolves when unambiguous, and raises a task when not
There is nothing to decide.
task is raised for the retailer.
So no local category ever silently points at a dead leaf, and the retailer is asked only when their
answer changes the outcome. Matching never goes dark for a decision the retailer did not make.
Why not read-time traversal always (#26 decision 4 as written). Every match read pays the hop
forever, and one hop stops being enough after a second split — #26 accepted mass reorganisation
as a hand-managed event, and always-traverse quietly accrues that debt on the read path instead.
Why not treat it as unmapped. #26 already defines unmapped as a working state, so this is
tempting and cheap. But matching is a paid vendor feature, and switching it off for every vendor
in a category until an unrelated party does remapping work is a real cost imposed for our taxonomy
edit.
What it costs. We write to retailer-owned rows on our schedule. That must be visible in their
history and reversible.
Confirmed rather than assumed
In-flight RFPs are immune, as #26 claimed. Visibility decision 3 materialises the
audience as an explicit vendor list at issue, so no structural change to any category — local or
spine — can alter who was invited to a live solicitation.
Constraints handed to other tickets
local_categorygainsvalid_from,valid_to,superseded_by[]; eligibility gains agrant provenance (
manual/split_event/merge_event, with the event id) so a propagatedgrant is distinguishable from a hand-granted one at audit; a
category_change_evententity recordssplits, merges, retirements, and remaps.
local_category_id— noclosure, no traversal. This is now settled and must not be eroded.
context. Add it to the enumerated list alongside the acceptance-count read and the expiry
scheduler.
— "was this vendor eligible on the date of issue" must be answerable across a split.
screen must state which vendors will be dropped before it commits.
be told about. Folds into notification design.
not the spine's contents.