Repository navigation
Description
Package
sphinx-needs
The problem
#2322 asked for filters and schema rules to follow inherits.
#2401 closed it with c.is_a(), which covers filters only.
The half that served schema rules, the type_ancestors field, was removed from #2401 and deferred to "the facets work".
No open issue tracks that work, so schema rules are where #2322 started:
a rule's select and local see the need's type as a string, and "type": {"const": "req"} matches req alone.
This is documented (docs/schema/index.rst:378), so it is not a bug, but it has two effects a project modelling with inherits runs into.
- A rule on a type does not reach its subtypes.
"Everyreqnames its reviewer" is not checked on asafety_req,
although asafety_reqis accepted wherever areqis (target_types,c.is_a('req')). - A rule rejects a subtype that
target_typesaccepts.
Anetworkrule requiringverifiestargets of typereqreports a test verifying asafety_req,
while the link type'starget_types = ["req"]accepts the same link.
The only workaround is to list every subtype by hand ("enum": ["req", "safety_req"]) in each rule,
and that list goes out of date silently when a type inheriting from req is added.
Reproduction
On master at c54fc77 (after #2401 and #2405):
ubproject.toml
[needs]
build_json = true
schema_definitions_from_json = "schemas.json"
[needs.fields.reviewer]
nullable = true
schema = { type = "string" }
[needs.types.req]
title = "Requirement"
prefix = "R_"
[needs.types.safety_req]
title = "Safety requirement"
prefix = "SR_"
inherits = "req"
[needs.types.test]
title = "Test case"
prefix = "T_"
[needs.types.test.links.verifies]
outgoing = "verifies"
incoming = "is verified by"
target_types = ["req"]schemas.json
{
"schemas": [
{
"id": "req-has-reviewer",
"severity": "violation",
"message": "every requirement names its reviewer",
"select": { "properties": { "type": { "const": "req" } } },
"validate": { "local": { "properties": { "reviewer": { "type": "string" } }, "required": ["reviewer"] } }
},
{
"id": "test-verifies-req",
"severity": "violation",
"message": "a test verifies requirements",
"select": { "properties": { "type": { "const": "test" } } },
"validate": { "network": { "verifies": { "items": { "local": { "properties": { "type": { "const": "req" } } } } } } }
}
]
}index.rst
.. req:: A req with a reviewer
:id: R_001
:reviewer: alice
.. req:: A req without a reviewer
:id: R_002
.. safety_req:: A safety_req without a reviewer
:id: SR_001
.. test:: Verifies a req and a safety_req
:id: T_001
:verifies: R_001, SR_001
.. needtable::
:filter: c.is_a('req')
:columns: id, typeOutput:
ERROR: Need 'R_002' has schema violations:
Severity: violation
Need path: R_002
Schema path: req-has-reviewer[0] > local > required
User message: every requirement names its reviewer
Schema message: "reviewer" is a required property [sn_schema_violation.local_fail]
ERROR: Need 'T_001' has schema violations:
Severity: violation
Field: type
Need path: T_001 > verifies > SR_001
Schema path: test-verifies-req[1] > validate > network > verifies > items > local > properties > type > const
Schema message: "req" was expected [sn_schema_violation.network_local_fail]
R_002is reported, andSR_001is not, though both lack a reviewer (effect 1).T_001 > verifies > SR_001is reported by the rule, and not bytarget_types(noneeds.link_target_type) (effect 2).- The
needtablewithc.is_a('req')listsR_001,R_002andSR_001: filters followinherits, rules do not.
Proposed behaviour
A rule can select, and check in local, "a req or a type inheriting from it" without listing the subtypes.
"type": {"const": "req"} keeps comparing the name alone,
so "a req but not a safety_req" stays expressible.
Two ways to get there, for discussion:
- The type lineage as data a schema can read (the facets work ✨ Filters follow
inherits:c.is_a('req')selects a type and the types inheriting from it #2401 deferred).
With a field, or atype.facet, holding the type and its ancestors,
a rule says"select": {"properties": {"type_ancestors": {"contains": {"const": "req"}}}},
and the same in anetworkitem'slocal.
Since 🐛 Schema rules takecontainswithoutitems, and aneeds_fieldsentry naming a computed core field is reported #2405,containswithoutitemsalready works on an array field, so nothing else in the rule schema has to change.
It also serves needs.json consumers.
Whether it is a field, its name, and whether it includes the type itself are the open questions ✨ Filters followinherits:c.is_a('req')selects a type and the types inheriting from it #2401 left to that work. - A rule key that follows
inheritswithout a new field, read liketarget_types:
for example"types": ["req"]on a rule, and on anetworkitem, besideselect/local.
No new public field name, but it is a second way to select needs alongsideselect,
and nestednetworkrules need it too.
ubCode mirrors whichever is chosen, as it did for c.is_a (useblocks/ubcode#4446).
Alternatives considered
- Listing subtypes by hand (
"enum": ["req", "safety_req"]): works today,
but every rule onreqhas to be edited when a subtype is added, and nothing reports a rule that was missed. target_types: followsinherits, but only says what type a direct link target has.
It cannot say anything about the target's other fields, or about the need itself.- The type's own declarations (
[needs.types.req.fields.*],[needs.types.req.back_links.*]): inherited by subtypes,
but only say what a single field or link count may be. Cross-field and network constraints still need a rule. c.is_a()in a rule: rules select needs with JSON Schema, not filter strings, so it cannot be used there.
Activity
chrisjsewell commentedon Oct 30, 2026
Thanks, the reproduction is accurate, and this is a known, deliberate gap rather than a bug.
#2401 shipped the filter half of #2322 (
c.is_a()); thetype_ancestorsfield, which was the schema-rules half,was dropped by a maintainer ruling, and the listed form of a type's lineage was deferred to the facets work
(the recommendation there is a self-inclusive
type.lineagefacet besidetype.parent).Nothing tracked that work yet, so let's keep this issue as its schema-rules consumer.
Meanwhile, the two effects in the example can be met today without listing subtypes; checked on
master8bd3798 with this issue's project:Effect 1 ("every
reqnames its reviewer") is a single-field constraint, so a need type's own declaration covers it,and declarations are inherited:
R_002andSR_001are both reported (fields > nullable, fieldreviewer), andsafety_reqneeds no entry of its own.One difference from a schema rule: at the default
severity = "violation",nullable = falseis enforced, so the need is not created(
needs.create_need); withseverity = "warning"(or"info") the need is kept and reported at that level."Report as a violation but keep the need" is not expressible this way.
Effect 2 (a test verifying a
safety_req):target_types = ["req"]already checks a link target's type and followsinherits:T_001 > verifies > SR_001is accepted, while averifieslink to atestgetsneeds.link_target_type("target need type 'test' is not one of 'req' (or a type inheriting from it)").So the
networkrule in the example is redundant for the type check, and can be dropped.What still needs this issue is a rule that constrains more than one field (cross-field), or a
networkrule that checks a target'sfields other than its type, on "a
reqor a subtype". Of the two proposals:and
containswithoutitemsalready works since 🐛 Schema rules takecontainswithoutitems, and aneeds_fieldsentry naming a computed core field is reported #2405. The open questions (field or facet, its name, whether it includes thetype itself) belong to the facets work.
typeskey on a rule (read liketarget_types) adds no public field name, so it is a reasonable stop-gap if the facets workis far off; the cost is a second way to select needs beside
select, carried into nestednetworkitems too.