Skip to content

Commit 5346c31

Browse files
dmealingclaude
andcommitted
fix(requirements): node-class parity for disposition, trackedBy and the link floor
Two gaps left after the vocabulary port, neither visible to the registry manifest (it gates registration, not accessors). 1. MayReferenceModel was WRONG on a levelled architectural node in C# and Python. Both returned true for ANY architectural requirement -- correct while architectural meant flat, and wrong the moment `@level` became optional, because it lets an "ISO 25010 Security" grouping node at L1 quietly name entities. Now matches TypeScript: an UNLEVELLED architectural node always may (its claim set is the point, and that is the original form), a LEVELLED one obeys the link floor exactly as a functional node does. Levelling is the opt-in; enforcing the floor unconditionally would break every flat policy. 2. Missing accessors. C# and Python could not read the attrs their registries had just started accepting, so a consumer would have had to go through raw attr lookup. Both now carry disposition / trackedBy / isPlanned / hasOutstandingWork, matching TypeScript and the Java accessors added in the previous commit. C# 872 conformance, Python 1571 unit -- both green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent eba03c5 commit 5346c31

2 files changed

Lines changed: 91 additions & 13 deletions

File tree

‎server/csharp/MetaObjects/Meta/MetaRequirement.cs‎

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,59 @@ public IReadOnlyList<string> VerifiedBy
7373
}
7474
}
7575

76+
/// <summary>
77+
/// What was DECIDED about the outstanding work — a different question from whether the
78+
/// work is done, which is what <c>@status</c> answers. <c>null</c> means UNDECIDED, a
79+
/// real state and the one a review exists to find.
80+
/// ADR-0039: resolving — inheritable via extends.
81+
/// </summary>
82+
public string? Disposition => Attr(REQUIREMENT_ATTR_DISPOSITION) as string;
83+
84+
/// <summary>
85+
/// Issue/ticket references for outstanding work. Free-form and NEVER resolved —
86+
/// <c>verify</c> has no network, so unlike <c>@verifiedBy</c> nothing here is checked
87+
/// to exist.
88+
/// ADR-0039: resolving — inheritable via extends.
89+
/// </summary>
90+
public IReadOnlyList<string> TrackedBy
91+
{
92+
get
93+
{
94+
var v = Attr(REQUIREMENT_ATTR_TRACKED_BY);
95+
return v is IReadOnlyList<string> list ? list : [];
96+
}
97+
}
98+
99+
/// <summary>
100+
/// Intended but not built. Its nodes may legitimately not exist yet, and it must NOT
101+
/// count toward object coverage — planning a capability cannot be allowed to silence
102+
/// the warning that nothing implements it.
103+
/// </summary>
104+
public bool IsPlanned() => Status == REQUIREMENT_STATUS_PLANNED;
105+
106+
/// <summary>True when there is outstanding work, so a <c>@disposition</c> says something.</summary>
107+
public bool HasOutstandingWork()
108+
{
109+
string? status = Status;
110+
return status is not null
111+
&& REQUIREMENT_STATUSES_WITH_OUTSTANDING_WORK.Contains(status);
112+
}
113+
76114
/// <summary>
77115
/// True when this requirement is permitted to reference the model at all.
78-
/// Architectural requirements always may (their claim set is the point); functional
79-
/// ones only at or below the link floor, so the organisational tiers stay
80-
/// organisational.
116+
///
117+
/// An UNLEVELLED architectural requirement always may — its claim set is the whole
118+
/// point, and that is the original flat form. Once a level is PRESENT the node has
119+
/// opted into a tree, and the link floor applies to it exactly as it does to a
120+
/// functional one, so an "ISO 25010 Security" grouping node cannot quietly start
121+
/// naming entities. Levelling is the opt-in; enforcing the floor unconditionally would
122+
/// have broken every existing flat policy.
81123
/// </summary>
82124
public bool MayReferenceModel()
83125
{
84-
if (IsArchitectural()) return true;
85126
int? level = Level;
86-
return level is not null && level.Value >= REQUIREMENT_LINK_FLOOR_LEVEL;
127+
if (level is null) return IsArchitectural();
128+
return level.Value >= REQUIREMENT_LINK_FLOOR_LEVEL;
87129
}
88130

89131
/// <summary>

‎server/python/src/metaobjects/meta/core/requirement/meta_requirement.py‎

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@
99

1010
from ...meta_data import MetaData
1111
from .requirement_constants import (
12+
REQUIREMENT_ATTR_DISPOSITION,
1213
REQUIREMENT_ATTR_IMPLEMENTED_BY,
1314
REQUIREMENT_ATTR_LEVEL,
1415
REQUIREMENT_ATTR_STATUS,
16+
REQUIREMENT_ATTR_TRACKED_BY,
1517
REQUIREMENT_ATTR_VERIFIED_BY,
1618
REQUIREMENT_LINK_FLOOR_LEVEL,
19+
REQUIREMENT_STATUS_PLANNED,
1720
REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES,
21+
REQUIREMENT_STATUSES_WITH_OUTSTANDING_WORK,
1822
REQUIREMENT_SUBTYPE_ARCHITECTURAL,
1923
REQUIREMENT_SUBTYPE_FUNCTIONAL,
2024
)
@@ -31,8 +35,9 @@ def is_architectural(self) -> bool:
3135

3236
def level(self) -> int | None:
3337
"""ADR-0039: resolving. 1 solution · 2 segment · 3 service · 4 object ·
34-
5 member. Architectural requirements carry none — they are
35-
object-independent by definition."""
38+
5 member. REQUIRED on functional; OPTIONAL on architectural, where absent
39+
means a flat object-independent policy and present opts the node into a
40+
levelled tree (e.g. a quality taxonomy over the non-functional set)."""
3641
v = self.get_meta_attr(REQUIREMENT_ATTR_LEVEL)
3742
return v if isinstance(v, int) and not isinstance(v, bool) else None
3843

@@ -53,15 +58,46 @@ def verified_by(self) -> list[str]:
5358
v = self.get_meta_attr(REQUIREMENT_ATTR_VERIFIED_BY)
5459
return [str(x) for x in v] if isinstance(v, list) else []
5560

61+
def disposition(self) -> str | None:
62+
"""ADR-0039: resolving. What was DECIDED about the outstanding work — a
63+
different question from whether the work is done, which is what
64+
``status`` answers. ``None`` means UNDECIDED, a real state and the one a
65+
review exists to find."""
66+
v = self.get_meta_attr(REQUIREMENT_ATTR_DISPOSITION)
67+
return v if isinstance(v, str) else None
68+
69+
def tracked_by(self) -> list[str]:
70+
"""ADR-0039: resolving. Issue/ticket references for outstanding work.
71+
Free-form and NEVER resolved — verify has no network, so unlike
72+
``verified_by`` nothing here is checked to exist."""
73+
v = self.get_meta_attr(REQUIREMENT_ATTR_TRACKED_BY)
74+
return [str(x) for x in v] if isinstance(v, list) else []
75+
76+
def is_planned(self) -> bool:
77+
"""Intended but not built. Its nodes may legitimately not exist yet, and
78+
it must NOT count toward object coverage — planning a capability cannot
79+
be allowed to silence the warning that nothing implements it."""
80+
return self.status() == REQUIREMENT_STATUS_PLANNED
81+
82+
def has_outstanding_work(self) -> bool:
83+
"""True when there is outstanding work, so a ``disposition`` says
84+
something."""
85+
s = self.status()
86+
return s is not None and s in REQUIREMENT_STATUSES_WITH_OUTSTANDING_WORK
87+
5688
def may_reference_model(self) -> bool:
5789
"""True when this requirement is permitted to reference the model at all.
58-
Architectural requirements always may (their claim set is the point);
59-
functional ones only at or below the link floor, so the organisational
60-
tiers stay organisational."""
61-
if self.is_architectural():
62-
return True
90+
91+
An UNLEVELLED architectural requirement always may — its claim set is the
92+
whole point, and that is the original flat form. Once a level is PRESENT
93+
the node has opted into a tree, and the link floor applies to it exactly
94+
as it does to a functional one, so an "ISO 25010 Security" grouping node
95+
cannot quietly start naming entities. Levelling is the opt-in; enforcing
96+
the floor unconditionally would have broken every existing flat policy."""
6397
lvl = self.level()
64-
return lvl is not None and lvl >= REQUIREMENT_LINK_FLOOR_LEVEL
98+
if lvl is None:
99+
return self.is_architectural()
100+
return lvl >= REQUIREMENT_LINK_FLOOR_LEVEL
65101

66102
def requires_live_nodes(self) -> bool:
67103
"""True when a dangling ``@implementedBy`` is an ERROR rather than

0 commit comments

Comments
 (0)