Skip to content

✨ Schema rules cannot follow inherits: a rule on req misses a safety_req, and rejects one that target_types accepts #2412

Open

Description

@Rubyfi

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.

  1. A rule on a type does not reach its subtypes.
    "Every req names its reviewer" is not checked on a safety_req,
    although a safety_req is accepted wherever a req is (target_types, c.is_a('req')).
  2. A rule rejects a subtype that target_types accepts.
    A network rule requiring verifies targets of type req reports a test verifying a safety_req,
    while the link type's target_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, type

Output:

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_002 is reported, and SR_001 is not, though both lack a reviewer (effect 1).
  • T_001 > verifies > SR_001 is reported by the rule, and not by target_types (no needs.link_target_type) (effect 2).
  • The needtable with c.is_a('req') lists R_001, R_002 and SR_001: filters follow inherits, 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:

  1. 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 a type. facet, holding the type and its ancestors,
    a rule says "select": {"properties": {"type_ancestors": {"contains": {"const": "req"}}}},
    and the same in a network item's local.
    Since 🐛 Schema rules take contains without items, and a needs_fields entry naming a computed core field is reported #2405, contains without items already 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 follow inherits: c.is_a('req') selects a type and the types inheriting from it #2401 left to that work.
  2. A rule key that follows inherits without a new field, read like target_types:
    for example "types": ["req"] on a rule, and on a network item, beside select / local.
    No new public field name, but it is a second way to select needs alongside select,
    and nested network rules 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 on req has to be edited when a subtype is added, and nothing reports a rule that was missed.
  • target_types: follows inherits, 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

  1. chrisjsewell commented on Oct 30, 2026

    @chrisjsewell
    Member

    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()); the type_ancestors field, 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.lineage facet beside type.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 master 8bd3798 with this issue's project:

    Effect 1 ("every req names its reviewer") is a single-field constraint, so a need type's own declaration covers it,
    and declarations are inherited:

    [needs.types.req.fields.reviewer]
    nullable = false
    severity = "warning"   # optional, see below

    R_002 and SR_001 are both reported (fields > nullable, field reviewer), and safety_req needs no entry of its own.
    One difference from a schema rule: at the default severity = "violation", nullable = false is enforced, so the need is not created
    (needs.create_need); with severity = "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 follows inherits:
    T_001 > verifies > SR_001 is accepted, while a verifies link to a test gets
    needs.link_target_type ("target need type 'test' is not one of 'req' (or a type inheriting from it)").
    So the network rule 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 network rule that checks a target's
    fields other than its type, on "a req or a subtype". Of the two proposals:

    1. The lineage as data is where the ruling points, and the better long-term answer: one source for rules, needs.json and ubCode,
      and contains without items already works since 🐛 Schema rules take contains without items, and a needs_fields entry naming a computed core field is reported #2405. The open questions (field or facet, its name, whether it includes the
      type itself) belong to the facets work.
    2. A types key on a rule (read like target_types) adds no public field name, so it is a reasonable stop-gap if the facets work
      is far off; the cost is a second way to select needs beside select, carried into nested network items too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions