Skip to content

Commit ad6ea53

Browse files
dmealingclaude
andcommitted
fix(requirements): architectural nesting is the contract, and Java's link floor obeyed it
The full reactor caught a stale test the java-fast lane could not: it runs the whole metadata suite, where java-fast runs four named classes. RequirementTest.architecturalRequirement_doesNotNest asserted the PRE-0.23.0 rule -- that architectural declares no nested-requirement child rule, because an architectural requirement is object-independent and so has no tier to contain. That reasoning holds for a FLAT policy and stopped being the whole story when `@level` became optional: levelling is opt-in so a quality taxonomy can organise the non-functional set the way a capability taxonomy organises the functional one. Inverted to architecturalRequirement_nests. Added architecturalRequirement_levelledTreeLoads, pinning the whole shape the optional `@level` exists for -- L1 to L2 to L4, @disposition, @trackedBy, and the link floor on both tiers. IT IMMEDIATELY CAUGHT A BUG IN THE PORT IT WAS WRITTEN FOR. Java's mayReferenceModel still returned true for ANY architectural node -- the identical defect fixed in C# and Python two commits ago and missed here -- so a levelled L1 grouping node could name entities. Now matches every other port: an UNLEVELLED architectural node always may, a LEVELLED one obeys the floor. Then a sweep for the retired doctrine, because a rule that changes leaves assertions behind: - Java checkValidChild's javadoc still said the child rule is declared on `functional` only. - C#'s Status docstring listed four statuses, omitting `planned`. - Java ValidationPhase's @status comment did the same. - A TS test asserted the loader error contains "live, partial, abandoned, superseded" -- which PASSED BY SUBSTRING LUCK against the real five-value message, and would have stayed green if `planned` were dropped from the set entirely. Now asserts all five. java-reactor (install): green. metadata suite green, RequirementTest 7/7, TS cli suite 47 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5346c31 commit ad6ea53

5 files changed

Lines changed: 105 additions & 36 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ public class MetaRequirement(TypeId typeId, string name) : MetaData(typeId, name
2222

2323
/// <summary>
2424
/// 1 solution · 2 segment · 3 service · 4 object · 5 member. Architectural
25-
/// requirements carry none — they are object-independent by definition.
25+
/// requirements carry it OPTIONALLY: absent means a flat, object-independent policy,
26+
/// present opts the node into a levelled tree (e.g. a quality taxonomy).
2627
/// ADR-0039: resolving — @level may be inherited via extends.
2728
/// </summary>
2829
public int? Level
@@ -41,7 +42,7 @@ public int? Level
4142
}
4243

4344
/// <summary>
44-
/// The lifecycle status (a closed enum: live / partial / abandoned / superseded).
45+
/// The lifecycle status (a closed enum: planned / live / partial / abandoned / superseded).
4546
/// ADR-0039: resolving — inheritable via extends.
4647
/// </summary>
4748
public string? Status => Attr(REQUIREMENT_ATTR_STATUS) as string;

‎server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4406,7 +4406,7 @@ private static void validateObjectIndexLookupFields(MetaObject obj) {
44064406
// type/subtype, not the container's; same reason source @kind/@role and
44074407
// relationship @onDelete/@onUpdate use post-load passes).
44084408
//
4409-
// @status must be one of: live / partial / abandoned / superseded
4409+
// @status must be one of: planned / live / partial / abandoned / superseded
44104410
//
44114411
// This is the mechanism the capability ledger was made registered vocabulary
44124412
// FOR (requirements-as-metadata ruling, Amendment 3): a typo'd status is

‎server/java/metadata/src/main/java/com/metaobjects/requirement/MetaRequirement.java‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,10 @@ protected MetaRequirement(String subType, String name) {
181181
* be lifted here or the declared child rule is unreachable.</p>
182182
*
183183
* <p>WHICH subtypes may nest stays the registry's decision, not this method's: the
184-
* spec declares the nested-{@code requirement} child rule on {@code functional} only
185-
* ({@code architectural} is object-independent and carries none), and
186-
* {@code applyStrictStructuralChildren} is what puts that rule on the type
187-
* definition. Every other same-type combination still hits the base refusal.</p>
184+
* spec declares the nested-{@code requirement} child rule on BOTH subtypes -- a
185+
* functional tree nests by capability, an architectural one may nest under a quality
186+
* taxonomy -- and {@code applyStrictStructuralChildren} is what puts that rule on the
187+
* type definition. Every other same-type combination still hits the base refusal.</p>
188188
*/
189189
@Override
190190
protected void checkValidChild(MetaData data) {
@@ -293,16 +293,20 @@ public List<MetaRequirement> getChildRequirements() {
293293

294294
/**
295295
* True when this requirement is permitted to reference the model at all.
296-
* Architectural requirements always may (their claim set is the point); functional
297-
* ones only at or below the link floor, so the organisational tiers stay
298-
* organisational.
296+
*
297+
* <p>An UNLEVELLED architectural requirement always may -- its claim set is the whole
298+
* point, and that is the original flat form. Once a level is PRESENT the node has
299+
* opted into a tree, and the link floor applies to it exactly as it does to a
300+
* functional one, so an "ISO 25010 Security" grouping node cannot quietly start
301+
* naming entities. Levelling is the opt-in; enforcing the floor unconditionally would
302+
* have broken every existing flat policy.</p>
299303
*/
300304
public boolean mayReferenceModel() {
301-
if (isArchitectural()) {
302-
return true;
303-
}
304305
Integer level = getLevel();
305-
return level != null && level >= LINK_FLOOR_LEVEL;
306+
if (level == null) {
307+
return isArchitectural();
308+
}
309+
return level >= LINK_FLOOR_LEVEL;
306310
}
307311

308312
/**

‎server/java/metadata/src/test/java/com/metaobjects/requirement/RequirementTest.java‎

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -167,17 +167,29 @@ public void architecturalRequirement_basicLoad_succeeds() {
167167
assertNotNull("requirement.architectural child found", req);
168168
assertEquals(MetaRequirement.SUBTYPE_ARCHITECTURAL, req.getSubType());
169169
assertTrue("isArchitectural()", req.isArchitectural());
170-
assertEquals("architectural carries no level", null, req.getLevel());
170+
assertEquals("a FLAT architectural node carries no level", null, req.getLevel());
171171
assertEquals(List.of("acme::shop::Order", "acme::shop::Customer"), req.getImplementedBy());
172172
assertTrue("architectural may always reference the model", req.mayReferenceModel());
173173
}
174174

175175
// ---------------------------------------------------------------------------
176-
// (3b) requirement.architectural declares NO nested-requirement child rule
176+
// (3b) requirement.architectural DOES nest -- it declares the same
177+
// nested-requirement child rule as functional.
178+
//
179+
// This test previously asserted the opposite, on the reasoning that an
180+
// architectural requirement is object-independent and so has no tier to
181+
// contain. That reasoning holds for a FLAT policy and stopped being the
182+
// whole story when `@level` became optional on architectural: levelling
183+
// is opt-in so a quality taxonomy (an ISO/IEC 25010 characteristic at
184+
// L1, its sub-characteristic at L2) can organise the non-functional set
185+
// the same way a capability taxonomy organises the functional one.
186+
// Declaring the child rule on `functional` only made an architectural
187+
// node nestable under a FUNCTIONAL parent but never under another
188+
// architectural one -- an omission rather than a design.
177189
// ---------------------------------------------------------------------------
178190

179191
@Test
180-
public void architecturalRequirement_doesNotNest() {
192+
public void architecturalRequirement_nests() {
181193
String json = "{ \"metadata.root\": { \"package\": \"acme::test\", \"children\": ["
182194
+ " { \"requirement.architectural\": { \"name\": \"uuidPrimaryKeys\","
183195
+ " \"@status\": \"live\","
@@ -193,27 +205,77 @@ public void architecturalRequirement_doesNotNest() {
193205
+ "] } }";
194206

195207
MetaDataLoader loader = strictLoader("test-requirement-architectural-nesting");
196-
MetaDataException caught = null;
197-
try {
198-
load(loader, json);
199-
} catch (MetaDataException e) {
200-
caught = e;
201-
}
208+
MetaRoot root = load(loader, json);
202209

203210
String messages = loader.getErrors().stream()
204211
.map(Throwable::getMessage).reduce("", (a, b) -> a + " | " + b);
205-
if (caught != null) {
206-
messages += " | THROWN: " + caught.getClass().getName() + ": " + caught.getMessage();
207-
}
208-
boolean refused = caught != null || !loader.getErrors().isEmpty();
209-
assertTrue("architectural is object-independent by definition and declares no "
210-
+ "nested-requirement child rule in spec/metamodel/requirement.json; "
211-
+ "messages=" + messages, refused);
212-
// The refusal must come from the REGISTRY's per-subtype child rule (sourced from
213-
// the spec file), not from MetaData's blanket same-type ban — the latter is what
214-
// MetaRequirement.checkValidChild deliberately lifts.
215-
assertTrue("refused by the registered child rule; messages=" + messages,
216-
messages.contains("requirement.architectural does not accept child"));
212+
assertTrue("a nested requirement loads clean under architectural; messages=" + messages,
213+
loader.getErrors().isEmpty());
214+
215+
MetaRequirement req = firstRequirement(root);
216+
assertNotNull("requirement.architectural child found", req);
217+
assertEquals(1, req.getChildren().stream()
218+
.filter(c -> MetaRequirement.TYPE_REQUIREMENT.equals(c.getType())).count());
219+
}
220+
221+
// ---------------------------------------------------------------------------
222+
// (3c) A LEVELLED architectural tree -- the shape the optional @level exists
223+
// for, and the one that was inexpressible while only `functional`
224+
// declared the nested-requirement child rule.
225+
// ---------------------------------------------------------------------------
226+
227+
@Test
228+
public void architecturalRequirement_levelledTreeLoads() {
229+
String json = "{ \"metadata.root\": { \"package\": \"acme::test\", \"children\": ["
230+
+ " { \"requirement.architectural\": { \"name\": \"security\","
231+
+ " \"@level\": 1,"
232+
+ " \"@status\": \"live\","
233+
+ " \"@statement\": \"The system protects the data it holds.\","
234+
+ " \"@violation\": \"A record readable by someone with no claim to it.\","
235+
+ " \"children\": ["
236+
+ " { \"requirement.architectural\": { \"name\": \"integrity\","
237+
+ " \"@level\": 2,"
238+
+ " \"@status\": \"partial\","
239+
+ " \"@disposition\": \"deferred\","
240+
+ " \"@trackedBy\": [\"acme/platform#412\"],"
241+
+ " \"@statement\": \"A stored value changes only under control.\","
242+
+ " \"@violation\": \"A number that changed with nothing explaining it.\","
243+
+ " \"children\": ["
244+
+ " { \"requirement.architectural\": { \"name\": \"ordersAreAppendOnly\","
245+
+ " \"@level\": 4,"
246+
+ " \"@status\": \"live\","
247+
+ " \"@statement\": \"An order row is appended, never mutated.\","
248+
+ " \"@violation\": \"An order total edited in place.\","
249+
+ " \"@implementedBy\": [\"acme::shop::Order\"] } }"
250+
+ " ] } }"
251+
+ " ] } }"
252+
+ "] } }";
253+
254+
MetaDataLoader loader = strictLoader("test-requirement-architectural-levelled");
255+
MetaRoot root = load(loader, json);
256+
257+
String messages = loader.getErrors().stream()
258+
.map(Throwable::getMessage).reduce("", (a, b) -> a + " | " + b);
259+
assertTrue("a levelled architectural tree loads clean; messages=" + messages,
260+
loader.getErrors().isEmpty());
261+
262+
MetaRequirement l1 = firstRequirement(root);
263+
assertNotNull("L1 architectural found", l1);
264+
assertEquals(Integer.valueOf(1), l1.getLevel());
265+
// An organisational tier may not reference the model, on either subtype,
266+
// once it has opted into a tree by carrying a level.
267+
assertTrue("L1 is organisational", !l1.mayReferenceModel());
268+
269+
MetaRequirement l2 = (MetaRequirement) l1.getChildren().stream()
270+
.filter(c -> c instanceof MetaRequirement).findFirst().orElseThrow();
271+
assertEquals("deferred", l2.getDisposition());
272+
assertEquals(List.of("acme/platform#412"), l2.getTrackedBy());
273+
assertTrue("partial has outstanding work", l2.hasOutstandingWork());
274+
275+
MetaRequirement l4 = (MetaRequirement) l2.getChildren().stream()
276+
.filter(c -> c instanceof MetaRequirement).findFirst().orElseThrow();
277+
assertTrue("the link floor may reference the model", l4.mayReferenceModel());
278+
assertEquals(List.of("acme::shop::Order"), l4.getImplementedBy());
217279
}
218280

219281
// ---------------------------------------------------------------------------

‎server/typescript/packages/cli/test/unit/requirement-check.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ describe("requirement.* — it is metadata", () => {
219219
implementedBy: ["acme::shop::Order"]
220220
`), OTHER);
221221
expect(r.loadError ?? "").toContain("not one of the allowed values");
222-
expect(r.loadError ?? "").toContain("live, partial, abandoned, superseded");
222+
// The FULL set, `planned` included. Asserting the tail alone passed by
223+
// substring luck and would have stayed green if `planned` were dropped.
224+
expect(r.loadError ?? "").toContain("planned, live, partial, abandoned, superseded");
223225
});
224226

225227
test("THE LOADER rejects an undeclared attr on a requirement (ADR-0023)", async () => {

0 commit comments

Comments
 (0)