Define the category taxonomy #26
Notifications
Due Date
No due date set.
Blocks
#10 Define the common schema for RFPs and responses
christian/helmdocs-proposal-system
Reference: christian/helmdocs-proposal-system#26
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 visibility resolution, where category turned out to be load-bearing in three
places at once: eligibility is scoped to
(retailer, vendor, category), group rules select overit, and vendor-to-RFP matching will operate on it.
The tension is that a shared taxonomy is what makes the cross-retailer rail work — one profile
reaching many buyers only means something if "Produce" means the same thing to Walmart and Kroger —
while retailers already run their own internal category structures and will not abandon them.
Resolve: whether categories are a shared taxonomy, retailer-scoped, or a shared spine with
retailer-specific extensions; who may create a category and what stops the set sprawling; how an
existing retailer taxonomy is mapped in at onboarding; whether a category is a flat list or a
hierarchy, and if hierarchical, whether eligibility inherits down it; and what happens to
eligibility and group rules when a category is renamed, split, or merged.
Note the research finding that no common retail rail exists today precisely because Walmart,
Target, Kroger, and Costco each structure sourcing differently — so a shared taxonomy is an
assertion the product makes, not a fact it can discover.
Parent: #1
Resolution
Category was doing two incompatible jobs. Retailer-facing, eligibility is scoped to it and group
rules select over it, so it must match the retailer's own world or onboarding is a fight they win by
not adopting. Network-facing, matching and the portable vendor profile run on it, so it must
mean the same thing across retailers or one-profile-many-buyers means nothing.
Four decisions separate those jobs and then keep the seam honest.
1. A shared spine, with retailer-local categories mapped onto it
Each retailer keeps their own category names and structure as retailer-scoped rows. Each maps to
a leaf on a global spine.
words.
This is the same resolution as roster entry vs. vendor org, for the same reason: the local thing is
what the customer actually has, the global thing is what makes the network worth joining, and
forcing either to be the other breaks one of them.
Retailers create their own local categories freely — it is their vocabulary, and gatekeeping it
would reintroduce the adoption fight. Each local category should map to a live spine leaf. An
unmapped local category still works locally — a retailer can run an RFP on it, and eligibility
attaches to it normally — it is simply invisible to cross-retailer matching. Same graceful
degradation as an ad-hoc document request in the vault decision: the unusual case never blocks a
sourcing event, it just does not earn network benefit.
Onboarding. A retailer already coded in Ariba or Coupa carries UNSPSC codes, so their mapping is
auto-proposed from those and confirmed rather than authored. A retailer with a homegrown
structure maps by hand against a set small enough to do in a sitting.
What it costs. Mapping is real onboarding work, mappings drift as retailers reorganise, and a bad
mapping degrades matching silently rather than loudly — there is no error, just worse results.
Mapping quality needs to be observable.
2. The spine is curated by us, annotated with UNSPSC
Order of 50–150 nodes, pitched at the level a retailer actually runs a sourcing event at. Each
node carries the UNSPSC families it subsumes, purely as an interop annotation — never as its
identity.
Why not adopt UNSPSC wholesale. Its Family level is too coarse to be a sourcing category and its
Commodity level is far too fine; neither sits where a sourcing category sits. 150k+ codes is a
worse mapping target, not a better one, and it drags in an external reissue cycle that would move
under our eligibility rows.
Why not GS1 GPC. Retail-native and familiar to suppliers, but it classifies items — the wrong
altitude — and has no coverage for services, which the artifacts research found throughout real
solicitations (logistics, facilities, marketing). Half the RFPs would have nowhere to sit.
Same argument as the document catalogue, applied to a second shared vocabulary: its whole value
is that it is small and shared. Self-service fragments it; wholesale adoption drowns it.
What it costs. We own taxonomy politics, and we will be wrong about granularity somewhere — too
coarse and matching is useless, too fine and every retailer maps differently.
3. Two levels; eligibility attaches only to leaves
A shallow group -> category spine. Groups exist for navigation, roll-up reporting, and
match-widening — a vendor qualified in Citrus can be surfaced as a near-fit for a Stone Fruit
RFP because they share a group.
Eligibility, group rules, and the acceptance graph attach only to leaves. Nothing inherits.
Why no inheritance. Eligibility is scoped to category and group rules select over eligibility, so
downward inheritance means a broad approval silently qualifies a vendor for every specialised
niche underneath — and that error does not stay quiet, it propagates directly into who receives
the next RFP. Under RLS it is worse still: a derived eligibility becomes a recursive closure
evaluated inside a policy, which is slow and hard to defend when an award is protested.
This keeps every authorization-relevant value exact and non-derived.
What it costs. Two levels will be wrong for someone, and a vendor genuinely qualified across a
whole group needs N rows rather than one.
4. Nodes are immutable; temporal validity plus supersession
Nodes are never edited or deleted. Labels may be corrected — a label is not an identity.
valid_to is null. New RFPs and new grants select only from this.valid_from <= D and (valid_to is null or valid_to > D). Thehistorical view is a where-clause, derived, not a stored snapshot.
what they meant. A node id never changes meaning, so a row referencing it is self-describing.
superseded_by— a bounded one-hop traversal, not atranslation layer.
Why not full taxonomy versioning. It buys the same property more expensively. Versioning pins
every row to a snapshot of the whole spine, which forces a cross-version translation layer into v1
before anything works. Node-level immutability is per-node versioning without the global snapshot —
the node is the version.
Why not deprecation without supersession. A vendor on a deprecated node would become invisible to
matching the day it is split, with no path from their old qualification to the new one. The split
would silently cost them RFPs.
Where this is genuinely weaker than versioning: a mass reorganisation redrawing the whole
spine at once turns supersession into a many-to-many tangle and makes the as-of query unreadable.
Accepted as a hand-managed event. If it stops being rare, that is the signal to add real versioning.
Already handled, and worth noting: in-flight RFPs are immune to taxonomy churn by construction,
because the visibility decision materialises the audience as an explicit list at issue.
Constraints handed to other tickets
spine nodes carry temporal columns and supersession edges; eligibility references the local
category, the acceptance graph references the spine leaf.
closure. This was the point of decision 3 and must not be eroded later.
Unmapped local categories are simply out of scope for it.
was or was not in an audience at a past date.
a retailer's mapping is poor or stale.
graduated to its own ticket.
Amendment needed — surfaced by a skeptical review pass
Decisions 3 and 4 contradict each other on the retailer-facing side.
Decision 4 splits a node by deprecating it and wiring
superseded_by, with nobody migrated, andrescues matching by following supersession one hop. Decision 3 forbids eligibility and group rules
from inheriting or deriving at all, so that authorization-relevant values stay exact.
Consequence: after a split, new RFPs select only live nodes. A group rule asks "qualified for
new-node-A". The vendor's eligibility row still points at the now-dead node. The vendor is
silently dropped from the audience, with no error anywhere — the exact silent-degradation failure
mode decision 3 existed to prevent, arriving through decision 4.
Supersession fixes the network side and breaks the retailer side. Neither escape is free: migrating
eligibility rows on split breaks "nobody is migrated", and letting group rules follow supersession
puts derivation back into an authorization path.
Graduated to Resolve taxonomy supersession against eligibility exactness.
Correction to the earlier review note
The previous comment claimed decisions 3 and 4 contradict each other via a spine split silently
un-qualifying vendors. That was wrong — eligibility runs on the retailer-local category per
decision 1, which is also what the schema encoded, so a spine split does not reach it.
The real defect is narrower and still real: decision 1 and decision 4 disagree about what an
eligibility row references. Decision 4 states that "old eligibility rows and acceptance edges keep
pointing at their node", and its whole supersession rescue assumes eligibility points at spine
nodes. Decision 1 says it points at the local category.
And the immutability question decision 4 answered so carefully was asked only of the spine — which
retailers never edit — and never of retailer-local categories, which they author freely and
which eligibility, group rules, and live solicitations all reference.
Both are now the subject of
Resolve what eligibility references, and whether local categories are mutable.