The gap
A passport cannot state its product category in any uniform way, so nothing can
read one off a Passport to make a decision with.
Passport has no envelope-level product category. The old product_category
field was renamed to sector, and its doc records why: it "actually held a
sector".
- Three of twelve sectors carry one inside
sector_data, at three different
shapes: electronics.product_category: DeviceType (an enum),
steel.product_category: String, unsold_goods.product_category: String. The
other nine carry none.
SectorDescriptor::product_categories: Vec<String> declares the legal values
per sector and is the obvious authority — but has no reader.
Why the missing reader is not hypothetical
product_categories_are_legal_values_of_their_schema_enum exists because the
catalog said clothing_accessories and starting-lighting-ignition where the
schemas said accessories and starting-lighting-ignition. Its own comment
records the situation exactly:
Neither was load-bearing, because SectorDescriptor::product_categories has
no reader in Rust today; both would have become load-bearing the moment one
appeared.
That test catches catalog-versus-schema drift. It does not give a caller a way
to ask a passport what it is.
The consumer waiting on this
dpp-vc::verify_credential_claims_with_trust already accepts
required_product_category: Option<&str> and applies the same "empty means
unscoped, populated must contain" rule it applies to sectors. The engine passes
None, so a credential scoped to one product category grants its full audience
over every product in a sector it covers — tracked as
odal-node/dpp-engine#64.
The enforcement exists here. The engine cannot use it because there is nothing
to pass.
Shape
The decision is where a product category lives, and it is a modelling question
rather than a plumbing one:
- An envelope field on
Passport, validated against the sector's
SectorDescriptor::product_categories. Uniform across all twelve sectors and
readable without knowing the sector's data type — but the envelope's rule is
additive-only and permanent, so it would have to be Option<String> with
#[serde(default)], and "absent" would need a defined meaning for access
control (almost certainly: unscoped credentials only).
- A
SectorData accessor — fn product_category(&self) -> Option<&str> —
that each variant implements, returning None for the nine that carry
nothing and normalising DeviceType to its wire string. No schema change, no
migration, and it makes the existing three readable today. But it leaves nine
sectors permanently unscopeable.
- Both: the accessor now, the envelope field when a sector needs scoping
that its data does not already express.
(2) is the one that unblocks the engine without touching a persisted shape, and
it is reversible. (1) is the one that makes product-category scope a real
control for every sector.
Worth settling before the first credential is issued against a scope that is
silently not enforced.
Note for whoever picks this up
Do not reach for dpp_domain::ProductCategory — it was decomposed and no longer
exists. Both sides of this are strings now; engine#64's description of an
enum-versus-string mismatch is stale and has been corrected there.
The gap
A passport cannot state its product category in any uniform way, so nothing can
read one off a
Passportto make a decision with.Passporthas no envelope-level product category. The oldproduct_categoryfield was renamed to
sector, and its doc records why: it "actually held asector".
sector_data, at three differentshapes:
electronics.product_category: DeviceType(an enum),steel.product_category: String,unsold_goods.product_category: String. Theother nine carry none.
SectorDescriptor::product_categories: Vec<String>declares the legal valuesper sector and is the obvious authority — but has no reader.
Why the missing reader is not hypothetical
product_categories_are_legal_values_of_their_schema_enumexists because thecatalog said
clothing_accessoriesandstarting-lighting-ignitionwhere theschemas said
accessoriesandstarting-lighting-ignition. Its own commentrecords the situation exactly:
That test catches catalog-versus-schema drift. It does not give a caller a way
to ask a passport what it is.
The consumer waiting on this
dpp-vc::verify_credential_claims_with_trustalready acceptsrequired_product_category: Option<&str>and applies the same "empty meansunscoped, populated must contain" rule it applies to sectors. The engine passes
None, so a credential scoped to one product category grants its full audienceover every product in a sector it covers — tracked as
odal-node/dpp-engine#64.
The enforcement exists here. The engine cannot use it because there is nothing
to pass.
Shape
The decision is where a product category lives, and it is a modelling question
rather than a plumbing one:
Passport, validated against the sector'sSectorDescriptor::product_categories. Uniform across all twelve sectors andreadable without knowing the sector's data type — but the envelope's rule is
additive-only and permanent, so it would have to be
Option<String>with#[serde(default)], and "absent" would need a defined meaning for accesscontrol (almost certainly: unscoped credentials only).
SectorDataaccessor —fn product_category(&self) -> Option<&str>—that each variant implements, returning
Nonefor the nine that carrynothing and normalising
DeviceTypeto its wire string. No schema change, nomigration, and it makes the existing three readable today. But it leaves nine
sectors permanently unscopeable.
that its data does not already express.
(2) is the one that unblocks the engine without touching a persisted shape, and
it is reversible. (1) is the one that makes product-category scope a real
control for every sector.
Worth settling before the first credential is issued against a scope that is
silently not enforced.
Note for whoever picks this up
Do not reach for
dpp_domain::ProductCategory— it was decomposed and no longerexists. Both sides of this are strings now; engine#64's description of an
enum-versus-string mismatch is stale and has been corrected there.