Extensible (SysML2-like) Schema Proposal #1646
Replies: 1 comment
-
Plan: Type-Aware
|
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
| Validate kwargs | iter_link_field_names(), iter_extra_field_names() |
~181–182 | Pass need_type |
| Convert/validate extra fields | iter_extra_fields() |
~266 | Pass need_type |
| Convert/validate link fields | iter_link_fields() |
~285 | Pass need_type |
| Core field conversion | get_core_field(name) via _convert_type_core() (×11) |
~221–253 | Accept and forward need_type |
| Core field defaults | get_core_field("status"), etc. (×10) |
~338–383 | Pass need_type |
| Extra field defaults | get_extra_field(k) |
~386 | Pass need_type |
| Link field defaults | get_link_field(k) |
~392 | Pass need_type |
| Copy links | iter_link_fields() via _copy_links() |
~394 | Pass need_type |
| Final extras validation | get_extra_field(k) |
~489 | Pass need_type |
| Nullable check | get_core_field(name) via _convert_to_none_str_func() |
~662 | Accept and forward need_type |
- Considerations:
need_typeis already a parameter ofgenerate_need()— no plumbing needed to get it.- This is the highest-value change: all ingestion validation and default resolution becomes type-aware.
- Helper functions (
_convert_type_core,_convert_to_none_str_func,_copy_links) need aneed_typeparameter added.
sphinx_needs/directives/need.py — NeedDirective.run()
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
| Build link/extra option sets | iter_link_field_names(), iter_extra_field_names() |
~121–122 | Pass self.name (= need type) |
| Dead link checking | get_link_field() |
~462 | Pass need type from need data |
- Considerations:
- The directive name is the need type — trivial to thread through.
- Controls which options are accepted in the RST/MyST directive.
_check_dead_links()(called during post-processing) has access to each need's type.
sphinx_needs/directives/list2need.py — List2NeedDirective
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
| Build defined options set | iter_extra_field_names() |
~95 | Pass need type |
| Build link keys set | iter_link_field_names() |
~96 | Pass need type |
| Process link options | get_link_field() |
~180–181 | Pass need type |
- Considerations:
- Need type is available via the
typeoption on the directive. - Structurally identical to
NeedDirectiveoption parsing.
- Need type is available via the
Tier 3 — Post-Processing (type is available via need data)
sphinx_needs/functions/functions.py — _resolve_functions_fields_iteration()
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
| Get field for type checking | get_any_field(field_name) |
~287 | Pass need["type"] |
- Considerations:
- Easy change; need data (including type) is available in the resolve loop.
- Enables type-specific constraint validation on dynamic function return values.
sphinx_needs/directives/needextend.py
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
| Validate extended field exists | get_any_field(key) |
~110 | See below |
- Considerations:
- Major design decision:
needextendtargets are determined by a filter that may match needs of different types. Two options:- Validate per-need at apply time (recommended): when extending each matched need, validate the field against that need's type. This is the safest approach and aligns with the Liskov principle.
- Validate against union at parse time: check the field exists in any type's schema. Less strict but catches typos early.
- For the initial implementation (all types share all field names), this is a non-issue — the change is purely mechanical. It becomes meaningful once types can have different field sets.
- Major design decision:
Tier 4 — Layout & Rendering (type available per-need)
sphinx_needs/layout.py — NeedLayout
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
| Schema stored on init | — | ~114 | No change needed |
| Build link name list | iter_link_fields() (×2, for names and _back names) |
~646–647 | Pass self.need["type"] |
| Validate link name | get_link_field(name) |
~679 | Pass self.need["type"] |
Iterate links for meta_links_all |
iter_link_fields() |
~707 | Pass self.need["type"] |
- Considerations:
self.needcontainstype— straightforward.- Enables rendering only the links relevant to the need's type.
Tier 5 — Import / External Needs
sphinx_needs/external_needs.py
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
| Build known keys | iter_extra_field_names(), iter_link_field_names() |
~128, ~134–136 | Potentially per-need |
| Iterate link fields for prefix editing | iter_link_fields() |
~142 | Potentially per-need |
- Considerations:
- Currently builds one
known_keysset before the import loop. - With type-aware schemas: either compute per-need (slower) or use union (fast, current behavior).
- For the initial plan (all types share field names), union is equivalent — no functional change needed.
- Currently builds one
sphinx_needs/directives/needimport.py
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
| Build known keys | iter_extra_field_names(), iter_link_field_names() |
~199, ~206 | Same as above |
| Iterate link fields for prefix editing | iter_link_fields() (×2, for names and _back names) |
~208–214 | Same as above |
- Considerations: Same pattern as
external_needs.py.
Tier 6 — Visualization Directives (type usually NOT available)
These directives display or connect needs of multiple types simultaneously. They should use the base schema (no need_type argument) to get the union of all fields.
| File | Method(s) used | Change needed |
|---|---|---|
sphinx_needs/directives/needtable.py |
iter_link_fields() |
None initially; later may need to handle missing fields gracefully per-type |
sphinx_needs/directives/needflow/_directive.py |
iter_link_field_names() |
None — use base schema |
sphinx_needs/directives/needflow/_graphviz.py |
iter_link_field_names() (×2) |
None — use base schema |
sphinx_needs/directives/needflow/_plantuml.py |
iter_link_field_names(), iter_link_fields() |
None — use base schema |
sphinx_needs/directives/needgantt.py |
get_any_field(), iter_link_field_names() |
None — use base schema |
sphinx_needs/directives/needsequence.py |
iter_link_field_names() |
None — use base schema |
- Considerations:
- These are safe to leave unchanged for this plan since
need_type=Nonereturns the base schema. - Future work: when types can have different field sets, needtable must handle
None/missing values gracefully for columns referencing type-specific fields.
- These are safe to leave unchanged for this plan since
Tier 7 — Schema Export, Validation & Resolution
sphinx_needs/schema/process.py
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
| Build JSON Schema | iter_extra_fields(), iter_core_fields(), iter_link_fields() |
~42–65 | Could produce per-type schemas |
- Considerations:
- For the initial plan (shared field names), the base schema suffices.
- Future: emit per-type schemas in the JSON Schema output for stricter per-type validation.
sphinx_needs/schema/resolve.py
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
| Validate link references in schemas | get_link_field() |
~186 | No change (config-level validation) |
| Inject field types into schema definitions | iter_extra_fields() |
~424 | No change |
| Inject link types into schema definitions | iter_link_fields() |
~433 | No change |
| Inject core types into schema definitions | iter_core_fields() |
~443 | No change |
- Considerations:
- Operates at
env-before-read-docstime, validating/resolving schema configuration. - Config validation is inherently type-agnostic (validates base definitions).
- When per-type overrides are added, this file would need to validate that type-specific overrides are Liskov-compatible with the base schema (handled by
inherit_schema()).
- Operates at
sphinx_needs/needsfile.py
| Call site | Current method | Line(s) | Change |
|---|---|---|---|
Build needs.json schema section |
iter_extra_fields(), iter_link_fields() |
~43–65 | Could include per-type schema info |
- Considerations:
- Future: add a
"schemas"section toneeds.jsonkeyed by type name. - Not needed for the initial plan.
- Future: add a
Tier 8 — Roles & Miscellaneous
| File | Method(s) used | Type available? | Change |
|---|---|---|---|
sphinx_needs/roles/need_outgoing.py |
get_link_field() |
Yes (ref_need available) |
Pass need type |
sphinx_needs/directives/needreport.py |
iter_extra_field_names(), iter_link_field_names() |
No (reporting all) | None — use base schema |
sphinx_needs/utils.py |
iter_link_fields() |
Yes (need available) | Pass need type |
Recommended Implementation Order
Phase A — Foundation (no behavioral change)
1. Add per-type storage to FieldsSchema + need_type parameter to all methods
2. Update create_schema() to populate type overrides (no-op until config exists)
3. All tests pass with no changes — pure additive
Phase B — Thread need_type through creation path
4. api/need.py — generate_need() and helpers
5. directives/need.py — NeedDirective.run() and _check_dead_links()
6. directives/list2need.py — List2NeedDirective
7. All tests pass — behavior unchanged since no type overrides configured
Phase C — Thread need_type through post-processing
8. functions/functions.py — resolve_functions
9. directives/needextend.py — extend_needs_data (per-need validation)
Phase D — Thread need_type through rendering
10. layout.py, roles/need_outgoing.py, utils.py
Phase E — Config & export (enables actual per-type config)
11. Add needs.types.<type>.fields.* config parsing
12. Update schema/resolve.py validation
13. Update needsfile.py / schema/process.py for per-type export
Key Considerations
Backward Compatibility
need_type=Nonealways returns the base schema → all existing code works unchanged.- Per-type overrides are purely opt-in via new config keys.
- No existing config format changes required.
needextend Challenge
- Filters can match needs of different types.
- Recommendation: validate fields per-need at apply time (in
extend_needs_data), not at directive parse time. This is already the natural flow since extensions are applied during post-processing when the target need's type is known.
Filtering / NameError Problem (future)
- When types have different field sets,
eval()-based filters will raiseNameErrorfor fields not on a given type. - Short-term mitigation: pre-populate filter context with
Nonefor all fields from all types. - Long-term: AST-based expression analysis.
- Not in scope for this plan (all types share field names).
Visualization Directives
- These always show mixed types → use base schema (union).
- Future: needtable needs graceful handling of
Nonefor type-specific columns.
Performance
- Per-type dict lookup with fallback is O(1) — negligible overhead.
- No deep copies of
FieldSchemaper call; overrides are pre-built at schema creation time.
Quick Wins
FieldsSchemaAPI addition (Phase A) — zero risk, purely additive.api/need.py(Phase B) — highest value;need_typealready in scope; enables type-specific defaults and validation immediately once config exists.directives/list2need.py(Phase B) — same pattern asNeedDirective; need type available via directive option.functions/functions.py(Phase C) — single call site, need type readily available.
Pain Points
needextend— filter-to-type ambiguity requires per-need validation; adds complexity toextend_needs_data.- Import paths (
external_needs.py,needimport.py) — currently build a singleknown_keysset before the loop; may need restructuring for per-type validation. - Visualization directives — when field sets diverge across types, tables/flows must handle missing fields without crashing. See appendix below for detailed analysis.
Appendix: Visualization Directives and Missing Fields
Context: Today every
NeedItemis initialized with every extra field and link field (set toNone/[]), soneed["some_field"]never raisesKeyError. When types can have different field sets, aNeedItemof typerequirementwould not have a key for a field defined only on typetestcase. This section catalogues where that would break and how to fix it.
Current field-access patterns
| Component | Access pattern | Behavior if key missing | Accesses extra fields? |
|---|---|---|---|
NeedItem.__getitem__ |
need[key] |
Raises KeyError |
N/A (foundation) |
NeedItem.get |
need.get(key) |
Returns None |
N/A (foundation) |
needtable cell rendering (row_col_maker) |
key in need_info and need_info[key] is not None |
Empty cell (silent) | Yes — any column name |
| needtable sorting | need[key] |
Crashes (KeyError) |
Yes — sort key |
| needgantt duration/completion | need[option] |
Crashes (KeyError) |
Yes — configurable fields |
| needflow graphviz (nodes + edges) | need_info[field] |
Crashes (KeyError) |
No — core + links only |
| needflow plantuml (Jinja template) | template.render(**need_info, ...) |
Jinja undefined → empty string |
Yes — template-dependent |
| needflow plantuml (connections) | need_info[link_type.name] |
Crashes (KeyError) |
No — link fields only |
| needsequence | sender[link_type], msg_need["id"], etc. |
Crashes (KeyError) |
No — core + links only |
layout meta() |
try: self.need[name] except KeyError: "" |
Graceful (returns "") |
Yes — any field |
layout meta_all() |
Iterates all keys → delegates to meta() |
Graceful | Yes — all fields |
layout meta_links_all() |
self.need[type_key] (truthy check) |
Crashes (KeyError) if link missing |
Links only |
| layout string replacement | if item not in self.need: raise |
Raises SphinxNeedLayoutException |
Yes — any [[field]] |
| filter context | {**need} → eval(expr, ctx) |
NameError in eval |
All fields in context |
Risk tiers
Already safe (no changes needed):
layout.meta()andlayout.meta_all()— already catchKeyErrorand handleNone/empty.row_col_makerin needtable — useskey in need_infoguard; missing fields produce empty cells.- Plantuml Jinja templates — Jinja's default
undefinedrenders as empty string.
Core-only access (safe as long as core fields remain universal):
- needflow graphviz, needsequence — only access
id,title,type,status,style, etc. and link fields. These would remain on all types even with per-type schemas, so no risk under the current plan (shared field names). Only becomes a concern if link sets diverge across types.
Needs fixing (would crash on missing extra fields):
- needtable sorting —
need[key]with no guard. - needgantt duration/completion —
need[option]with no guard. layout.meta_links_all()—self.need[type_key]with no guard if link sets diverge.- layout string replacement — raises on unknown
[[field]].
Proposed fixes
Strategy 1: NeedItem.get() everywhere (minimal, targeted)
Change the ~4 crash-prone sites to use need.get(key, default) instead of need[key]:
# needtable sorting — return a sort-neutral default for missing fields
def sort(need):
value = need.get(key, "") # was: need[key]
...
# needgantt — treat missing duration/completion as "not applicable"
duration = need.get(duration_option, None)
if not duration:
... # existing fallback logic already handles None/falsy
# layout.meta_links_all — skip links not on this need's type
for link in self.needs_schema.iter_link_fields():
type_key = link.name
if type_key in self.need and self.need[type_key] and type_key not in exclude:
...Pros: Minimal diff, no architectural change, immediately safe.
Cons: Every new visualization site must remember to use .get() — easy to regress.
Strategy 2: NeedItem.__getitem__ returns a sentinel for missing fields
Make NeedItem.__getitem__ return a typed sentinel (e.g. FIELD_NOT_PRESENT) instead of raising KeyError for fields that don't exist on this need's type. Callers can then check explicitly:
value = need[key]
if value is FIELD_NOT_PRESENT:
... # skip or render placeholderPros: Centralised — no crash regardless of access pattern.
Cons: Every need[key] comparison (== None, truthiness, string ops) must be audited to handle the sentinel. High risk of subtle bugs. Changes the contract of __getitem__.
Strategy 3: Keep KeyError but add a has_field() method
Add NeedItem.has_field(key) -> bool that checks whether the field is defined for this need's type schema. Callers that operate on mixed types guard with it:
if need.has_field(key):
value = need[key]
...Pros: Explicit opt-in, clear semantics, no change to __getitem__ contract.
Cons: Still requires callers to remember to check — but the crash on missing field acts as a loud reminder.
Recommended approach
Strategy 1 for the initial plan (all types share field names — this is a no-op in practice, but hardens the code). Strategy 3 for the future when field sets genuinely diverge: add has_field() to NeedItem and audit all visualization sites listed above.
The layout.meta() pattern (try/except KeyError → "") is the gold standard. For needtable and needgantt, switching to .get() with sensible defaults is a ~10-line change per file and eliminates the crash risk entirely.
Filter context (eval-based filtering)
The filter system unpacks all need fields into a dict and calls eval(). A field absent from a need type would not appear in the eval namespace, causing NameError if the filter expression references it.
Short-term: Pre-populate the filter context with None for all fields from all types (union). This is cheap (one dict update per need) and fully backward-compatible — filters like custom_field == "x" simply evaluate to False for needs that don't have custom_field (since None == "x" is False).
Long-term: AST-based expression pre-analysis that can detect which fields are referenced, validate them against the need's type schema, and short-circuit evaluation for needs where a referenced field doesn't exist. This is a larger project and out of scope for the initial plan.
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
This document outlines a plan to make sphinx-needs schemas composable and extensible, inspired by SysML2.
The suggested implementation proceeds in phases:
needs.extra_optionstoneeds.fields✅,needs.extra_linkstoneeds.links🚧, andGoal
We would like to allow for composable and extensible schemas for defining "needs" in Sphinx-needs, similar to how SysML2 allows for core definitions to be extended and specialized.
An "ideal" configuration format could look something like this:
This would then lead to needs like:
Where the
requirementTypefield is only available for needs of typerequirementor its subtypes thecustom1field has different constraints depending on the need type, and thetestcaseSpecificFieldis only available for needs of typetestcase.Inspiration from SysML2
This proposed configuration format draws significant inspiration from SysML2, the next-generation systems modeling language. SysML2 provides a powerful approach to defining and specializing system elements that we aim to adapt for sphinx-needs.
Key SysML2 Concepts Adopted
Specialization and Inheritance: In SysML2, elements can
specializeother elements, inheriting their attributes and constraints while adding or refining them. Ourinheritskey mirrors this concept:becomes:
Attribute Redefinition: SysML2 allows
redefineto override attributes with stricter types or constraints, following the Liskov substitution principle. For example:Our schema allows similar redefinition by narrowing constraints:
Default Values: In SysML2, attributes can have default values specified with the
= valuesyntax (e.g.,attribute priority : Integer [0..1] = 3;). We mirror this with thedefaultkey:Constraints: SysML2 supports inline constraints on attributes (e.g.,
priority >= 1 and priority <= 5). We express these using JSON Schema validation:Link Definitions: SysML2 defines connections between elements using
link def, specifying source and target types:We adopt this with
linksToandlinksFromto constrain which need types a link can reference:Additional SysML2 Concepts to Consider
The following SysML2 concepts are not yet adopted but could be valuable for future enhancements:
[0..1],[1..*]for cardinalityminItems/maxItems; could add shorthand syntaxabstract requirement defcannot be instantiatedabstract = truetoneeds.typesto define base types that can only be specializedderived attribute x = ...computed from other attributespackage Requirements { ... }for organizationsatisfy,verify,refine,tracelinks, but could add semantic meaning to link types (e.g., for traceability matrices)Benefits of the SysML2-Inspired Approach
Implementation
Sphinx Build Lifecycle Integration
Understanding when schema operations occur in the Sphinx build is crucial for this implementation. Based on the event flow (see the sphinx-needs
AGENTS.mdfor details):config-initedFieldSchema/LinkSchemainstancesenv-before-read-docswrite-startedpost_process_needs_data: extend, resolve functions, back-links, constraints, before needs become immutablebuild-finishedneeds.jsonKey Data Structures
The proposal builds on existing data structures:
From sphinx_needs/data.py:
NeedsCoreFields: Defines core field parameters (description,schema,allow_default,allow_df,allow_extend, etc.). Per-type schemas would extend this with type-specific field definitions.NeedItem: The runtime representation of a need. Per-type schemas may require a more flexible type (or remain a single type with optional fields).SphinxNeedsData: Central access point for needs data in the build environment. Providesget_schema()to access the currentFieldsSchema. For per-type schemas, would need additional methods likeget_schema_for_type(type_name).From sphinx_needs/needs_schema.py:
FieldSchema: Immutable dataclass defining a single field's metadata—name, description, JSON Schema constraints, nullability, whether it allows defaults/extend/dynamic functions/variants, and default values. Providesconvert_directive_option()for parsing RST option values andtype_check()for validation.LinkSchema: Similar toFieldSchemabut specialized for link fields (arrays of need IDs).FieldsSchema: Container class holding all field and link schemas. Provides methods to add fields and iterate over them.How the schema is built (
create_schemain sphinx_needs/needs.py):During
env-before-read-docs, thecreate_schema()function constructs theFieldsSchema,from information obtained from the
NeedsCoreFieldsand from user defined configuration.For per-type schemas, this process would need to:
needs.types, create a specialized schema that inherits from the base (or another type)SphinxNeedsData.get_schema_for_type(type_name)Current sphinx-needs v6
In the current version of sphinx-needs (v6), the schema is "monolithic", in that the same set of fields and links apply to all need types, with only limited per-type customization.
The core fields are defined in
NeedsCoreFields(see sphinx_needs/data.py) with parameters controlling behavior:add_to_field_schema: Whether the field appears in user-facing schemaallow_default: Whether custom defaults can be setallow_df: Whether dynamic functions are allowedallow_extend: Whetherneedextendcan modify the fieldallow_variants: Whether variant parsing appliesThese parameters would need to be respected by per-type schema definitions.
User extensions are limited:
needs.extra_options: A singular list of fields that cannot override core fieldsneeds.extra_links: A singular list of link types that can override core link behavior, but in a limited wayThere are also several scattered configuration options that affect field behavior globally, relying on the monolithic schema:
needs.global_optionsneeds.statusesneeds.tagsneeds.variant_optionsProcessing and analysis of needs is also dependent on this monolithic schema, for example:
Moving from
needs.extra_optionstoneeds_fieldsThe key change is migrating from
needs_extra_options(a simple list of option names) toneeds_fields(a dictionary-based format). This new format allows for:redefine)needs_global_options,needs_statuses,needs_tags,needs_variant_options) into a single per-field definitionOld configuration (scattered across multiple options):
New configuration (consolidated per-field):
Internally, this is powered by the
FieldSchemadataclass (see sphinx_needs/needs_schema.py), which serves as a single source of truth for each field's schema. Rather than scattering field configuration across multiple config options and accessing them directly during processing, all field metadata is resolved intoFieldSchemainstances at configuration time. These instances then drive all downstream processing—validation, filtering, visualization, and ensure consistency.Relevant modules:
FieldSchemaandLinkSchemadataclass definitionsMoving from
needs.extra_linkstoneeds.linksSimilar to the
needs_fieldsmigration, we are also moving link configuration fromneeds_extra_linksto a newneeds.linksdictionary-based format. This work has begun with commit 4cf3021, which centralizes link schema definitions onto theLinkSchemaclass.Key changes so far:
schemafield toLinkSchemawith validation in__post_init__LinkSchema.schemainstead of extracting from configFieldSchemaPlanned migration:
This change brings links in line with fields, using
LinkSchema(see sphinx_needs/needs_schema.py) as the single source of truth for link metadata. LikeFieldSchema, theLinkSchemainstances will drive all downstream processing, ensuring that link validation, back-link generation, and visualization use consistent schema information.Work to do:
Add missing fields to
LinkSchema: The currentLinkSchemaonly hasschema-related fields. We need to add:outgoing: The label for the outgoing link (e.g., "tests")incoming: The label for the incoming/back link (e.g., "is_tested_by")copy: Whether to copy the link to child needsallow_dead_links: Whether to allow links to non-existent needsstyle: Styling information for visualizationstyle_part: Styling for need partsstyle_start: Start arrow stylestyle_end: End arrow styleMigrate codebase from
needs_config.extra_linkstoLinkSchema: After configuration resolution, all access to link information should useLinkSchemainstances instead of the raw config dictionaries. Files to update include:_directive.py,_graphviz.py,_plantuml.py)Deprecate direct access to
needs_config.extra_links: Once all code usesLinkSchema, mark direct config access as deprecated changeneeds_config.extra_linksto a private attribute (e.g.,_extra_links) to signal it should not be used directly anymore.Moving from monolithic need schema to per-type schemas
This will be a larger effort, as many parts of the codebase currently assume a single schema for all needs,
and it will also be challenging to maintain backward compatibility.
Ingestion of needs directives
When ingesting needs directives from RST files, we will need to determine the need type (from the directive name) and then validate the fields and links against that need type's schema.
Storage of need data may also need to be updated to consider the need type.
Interaction with
needextendThe
needextenddirective modifies needs after initial parsing (duringextend_needs_datain post-processing). With per-type schemas:needextendshould only allow fields valid for the target need's typeallow_extendflag: Core fields already have anallow_extendparameter; per-type schemas should inherit/override thisInteraction with dynamic functions
Dynamic functions (
resolve_functionsin post-processing) can set field values programmatically. With per-type schemas:allow_dfflag: Core fields have this; per-type schemas should respect/override itFiltering (a.k.a need querying)
One of the biggest challenges will be updating how need filtering works.
Current implementation
See sphinx_needs/filter_common.py.
The
filter_needs_and_partsfunction evaluates Python expressions against a context built from each need's data:Users write filters like
status == "approved" and "security" in tags.Problems with per-type schemas
NameErrorfor missing fields: IftestcaseSpecificFieldonly exists on testcase needs, evaluatingtestcaseSpecificField == TrueraisesNameErrorfor other need types. Currently avoided because all needs share the same monolithic schema.Type guards don't help: You might think
type == "testcase" and testcaseSpecificField == Truewould work, but Python resolves all variable names before evaluating logic—theNameErroroccurs during compilation, not evaluation, so short-circuitanddoesn't help.Performance limitations:
typefirst)eval()overhead for each needHow SysML2 handles this
SysML2 uses type-scoped, OCL-inspired queries:
This provides type scoping (only
Requirementinstances considered) and type safety (compiler validatesverificationStatusexists onRequirement).Possible approaches
get()functionget("field") == valueeval()NoneNonedefaultNameErrorFalsetestcase[field = true]Recommended approach
Short term: Pre-populate the filter context with
Nonefor all fields from all schemas. This maintains backward compatibility while enabling per-type schemas.Long term: Invest in AST-based expression analysis:
astmodule (already partially done infilter_common.py)Visualizations / renderings
Many visualizations (needflow, needgantt, needsequence) currently assume a monolithic schema. These will need updates to handle per-type schemas:
For renderings of actual needs.
n needflow diagrams, link definitions may need to vary by need type. We will need to update the visualization code to reference the appropriate
FieldSchemaandLinkSchemabased on each need's type.For needgantt and needsequence, any field-specific logic (e.g., date fields for scheduling) will need to be aware of per-type schemas.
For needtable, we will need to ensure that column definitions can reference fields that may not exist on all need types, and handle missing data gracefully.
Exchange formats (
needs.json)The
needs.jsonexport will need to include schema information for each need type,so that consumers of the JSON can understand the fields and links available for each need.
This may involve adding a
schemassection to the JSON output that describes each need type's schema.{ "versions": { ... }, "schemas": { "requirement": { "inherits": null, "fields": { "requirementType": { "type": "string" } }, "links": { ... } }, "testcase": { "inherits": "requirement", "fields": { "testcaseSpecificField": { "type": "boolean" } } } }, "needs": { ... } }Consideration for how
needs.jsonare consumed in sphinx-needs (fromneeds.external_needsandneedsimport) will also be necessary:Sealing and immutability
After post-processing, needs are "sealed" (made immutable) in the
ensure_post_process_needs_datacallbacke function (which callspost_process_needs_data).Per-type schema validation should complete before sealing:
needextendmodifications appliedOrthogonal Consideration: Type-Namespaced Need IDs
An orthogonal change to consider is namespacing need IDs by type—IDs unique within their type rather than globally.
Current Behavior
Currently, sphinx-needs requires all need IDs to be globally unique. With type-namespaced IDs, simple sequential IDs could be used per type:
How SysML2 Handles This
In SysML2, elements are identified within their containing package/namespace:
Proposed Configuration
need parts
Need parts could also be worked into this namespacing scheme, e.g.,
requirement.001.part1, for:ID Resolution (Sphinx-Inspired)
Inspired by Sphinx Python domain resolution, which searches for targets by progressively adding qualification (module, class). We could adapt this:
nc.testcase.001001Example usage:
Trade-offs
Recommendation
This is orthogonal to the extensible schema work but becomes more natural if per-type schemas are adopted. Implement as opt-in (
needs.namespaced_ids = true) to maintain backward compatibility.Beta Was this translation helpful? Give feedback.
All reactions