Skip to content

Commit f5913ce

Browse files
dmealingclaude
andcommitted
test(metamodel): pin the @mutability tightening order in the three ports that never had one
`@mutability` is ranked by `indexOf` over a per-port `MUTABILITY_MODES` list — declaration order IS the tightening order — and only TypeScript pinned that order. The shared corpus could not stand in for the missing pins: its inheritance fixtures pair `readOnly` with `readWrite`, the two ENDPOINTS, so a full reversal is caught while a reorder that moves ONLY `writeOnce` is invisible. Every port's declaration is currently correct; this is a missing gate, not a live bug. Closes the behavioural half cross-port with a corpus fixture whose downgrade runs through the middle mode, and the structural half with an order pin in each port that reads the array. Each new gate was proved by breaking the order and watching it go red — under the break the EXISTING fixture still passed while this one failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
1 parent 435bed9 commit f5913ce

6 files changed

Lines changed: 230 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"errors": [
3+
{
4+
"code": "ERR_MUTABILITY_DOWNGRADE",
5+
"source": {
6+
"format": "json",
7+
"files": [
8+
"meta.demo.json"
9+
],
10+
"jsonPath": "$['metadata.root'].children[1]['object.entity'].children[2]['field.string']"
11+
}
12+
}
13+
],
14+
"warnings": []
15+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"metadata.root": {
3+
"package": "demo",
4+
"children": [
5+
{
6+
"object.entity": {
7+
"name": "BaseEntity",
8+
"abstract": true,
9+
"children": [
10+
{
11+
"field.string": {
12+
"name": "issuedCurrency",
13+
"@mutability": "readOnly"
14+
}
15+
}
16+
]
17+
}
18+
},
19+
{
20+
"object.entity": {
21+
"name": "Loosener",
22+
"extends": "demo::BaseEntity",
23+
"children": [
24+
{
25+
"source.rdb": {
26+
"@table": "looseners"
27+
}
28+
},
29+
{
30+
"field.long": {
31+
"name": "id"
32+
}
33+
},
34+
{
35+
"field.string": {
36+
"name": "issuedCurrency",
37+
"@mutability": "writeOnce"
38+
}
39+
},
40+
{
41+
"identity.primary": {
42+
"name": "id",
43+
"@fields": "id"
44+
}
45+
}
46+
]
47+
}
48+
}
49+
]
50+
}
51+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["metaobjects-core-types","metaobjects-db"]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// FR-037 R1 — the @mutability tightening order is load-bearing, so it is pinned.
2+
//
3+
// ValidationPasses.MutabilityRank() is an INDEX comparison over
4+
// FieldConstants.MUTABILITY_MODES ("declaration order IS the tightening order", as the
5+
// constant's own doc comment says), so reordering that array silently inverts "may only
6+
// tighten" with nothing to catch it.
7+
//
8+
// The shared conformance corpus cannot stand in for this. Its inheritance fixtures pair
9+
// only readOnly with readWrite — the two ENDPOINTS — so a full reversal is caught but a
10+
// reorder that moves ONLY writeOnce is not. The corpus fixture
11+
// error-field-mutability-downgrade-writeonce closes the behavioural half cross-port; this
12+
// closes the structural half in the port whose rank function reads the array.
13+
//
14+
// Mirrors the TypeScript pin (metadata/test/fr037-field-mutability.test.ts), which was the
15+
// only such pin in any port until now.
16+
17+
using MetaObjects.Core.Field;
18+
using Xunit;
19+
20+
namespace MetaObjects.Conformance.Tests;
21+
22+
public class Fr037MutabilityOrderTests
23+
{
24+
[Fact]
25+
public void DeclarationOrderIsTheTighteningOrder()
26+
{
27+
// Loosest first. The downgrade rule is rank(child) >= rank(parent), so this
28+
// order IS the rule.
29+
Assert.Equal(
30+
new[] { "readWrite", "writeOnce", "readOnly" },
31+
FieldConstants.MUTABILITY_MODES);
32+
}
33+
34+
[Fact]
35+
public void ModeSpellings()
36+
{
37+
// The wire spellings travel cross-port; a typo here is a silent divergence.
38+
Assert.Equal("readWrite", FieldConstants.MUTABILITY_READ_WRITE);
39+
Assert.Equal("writeOnce", FieldConstants.MUTABILITY_WRITE_ONCE);
40+
Assert.Equal("readOnly", FieldConstants.MUTABILITY_READ_ONLY);
41+
}
42+
43+
/// <summary>
44+
/// The specific relationship the corpus never exercises. Stated as an explicit rank
45+
/// comparison rather than inferred from the array above, so a future change that keeps
46+
/// the array's CONTENTS but alters how rank is derived still fails here.
47+
/// </summary>
48+
[Fact]
49+
public void WriteOnceRanksBetweenTheTwoEndpoints()
50+
{
51+
int readWrite = System.Array.IndexOf(FieldConstants.MUTABILITY_MODES, FieldConstants.MUTABILITY_READ_WRITE);
52+
int writeOnce = System.Array.IndexOf(FieldConstants.MUTABILITY_MODES, FieldConstants.MUTABILITY_WRITE_ONCE);
53+
int readOnly = System.Array.IndexOf(FieldConstants.MUTABILITY_MODES, FieldConstants.MUTABILITY_READ_ONLY);
54+
Assert.True(readWrite < writeOnce, "readWrite must rank looser than writeOnce");
55+
Assert.True(writeOnce < readOnly, "writeOnce must rank looser than readOnly");
56+
}
57+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.metaobjects.field;
2+
3+
import org.junit.Test;
4+
5+
import java.util.Arrays;
6+
7+
import static org.junit.Assert.assertEquals;
8+
import static org.junit.Assert.assertTrue;
9+
10+
/**
11+
* FR-037 R1 — the {@code @mutability} tightening order is load-bearing, so it is pinned.
12+
*
13+
* <p>{@code ValidationPhase.mutabilityRank()} is an INDEX comparison over
14+
* {@link MetaField#MUTABILITY_MODES} ("declaration order IS the tightening order", as the
15+
* constant's own javadoc says), so reordering that list silently inverts "may only tighten"
16+
* with nothing to catch it. Kotlin inherits this same constant through the JVM loader, so
17+
* this pin covers both JVM ports.
18+
*
19+
* <p>The shared conformance corpus cannot stand in for this. Its inheritance fixtures pair
20+
* only {@code readOnly} with {@code readWrite} — the two ENDPOINTS — so a full reversal is
21+
* caught but a reorder that moves ONLY {@code writeOnce} is not.
22+
* {@code error-field-mutability-downgrade-writeonce} closes the behavioural half cross-port;
23+
* this closes the structural half in the port whose rank function reads the list.
24+
*
25+
* <p>Mirrors the TypeScript pin (metadata/test/fr037-field-mutability.test.ts), which was the
26+
* only such pin in any port until now.
27+
*/
28+
public class FieldMutabilityOrderTest {
29+
30+
@Test
31+
public void declarationOrderIsTheTighteningOrder() {
32+
// Loosest first. The downgrade rule is rank(child) >= rank(parent), so this
33+
// order IS the rule.
34+
assertEquals(
35+
Arrays.asList("readWrite", "writeOnce", "readOnly"),
36+
MetaField.MUTABILITY_MODES);
37+
}
38+
39+
@Test
40+
public void modeSpellings() {
41+
// The wire spellings travel cross-port; a typo here is a silent divergence.
42+
assertEquals("readWrite", MetaField.MUTABILITY_READ_WRITE);
43+
assertEquals("writeOnce", MetaField.MUTABILITY_WRITE_ONCE);
44+
assertEquals("readOnly", MetaField.MUTABILITY_READ_ONLY);
45+
}
46+
47+
/**
48+
* The specific relationship the corpus never exercises. Stated as an explicit rank
49+
* comparison rather than inferred from the list above, so a future change that keeps the
50+
* list's CONTENTS but alters how rank is derived still fails here.
51+
*/
52+
@Test
53+
public void writeOnceRanksBetweenTheTwoEndpoints() {
54+
int readWrite = MetaField.MUTABILITY_MODES.indexOf(MetaField.MUTABILITY_READ_WRITE);
55+
int writeOnce = MetaField.MUTABILITY_MODES.indexOf(MetaField.MUTABILITY_WRITE_ONCE);
56+
int readOnly = MetaField.MUTABILITY_MODES.indexOf(MetaField.MUTABILITY_READ_ONLY);
57+
assertTrue("readWrite must rank looser than writeOnce", readWrite < writeOnce);
58+
assertTrue("writeOnce must rank looser than readOnly", writeOnce < readOnly);
59+
}
60+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""FR-037 R1 — the ``@mutability`` tightening order is load-bearing, so it is pinned.
2+
3+
``_rank()`` in ``loader/validate_field_mutability.py`` is an INDEX comparison over
4+
``MUTABILITY_MODES`` ("declaration order IS the order", as its own docstring says), so
5+
reordering that tuple silently inverts "may only tighten" with nothing to catch it.
6+
7+
The shared conformance corpus cannot stand in for this pin. Its inheritance fixtures pair
8+
only ``readOnly`` with ``readWrite`` — the two ENDPOINTS — so a full reversal is caught but
9+
a reorder that moves ONLY ``writeOnce`` is not. ``error-field-mutability-downgrade-writeonce``
10+
closes the behavioural half cross-port; this closes the structural half in the port whose
11+
rank function reads the tuple.
12+
13+
Mirrors the TypeScript pin (``metadata/test/fr037-field-mutability.test.ts`` — "declaration
14+
order IS the tightening order"), which was the only such pin in any port until now.
15+
"""
16+
17+
from metaobjects.meta.core.field.field_constants import (
18+
MUTABILITY_MODES,
19+
MUTABILITY_READ_ONLY,
20+
MUTABILITY_READ_WRITE,
21+
MUTABILITY_WRITE_ONCE,
22+
)
23+
24+
25+
def test_mutability_modes_declaration_order_is_the_tightening_order() -> None:
26+
# Loosest first. The downgrade rule is `rank(child) >= rank(parent)`, so this
27+
# order is the rule.
28+
assert tuple(MUTABILITY_MODES) == ("readWrite", "writeOnce", "readOnly")
29+
30+
31+
def test_mutability_mode_spellings() -> None:
32+
# The wire spellings travel cross-port; a typo here is a silent divergence.
33+
assert MUTABILITY_READ_WRITE == "readWrite"
34+
assert MUTABILITY_WRITE_ONCE == "writeOnce"
35+
assert MUTABILITY_READ_ONLY == "readOnly"
36+
37+
38+
def test_write_once_ranks_between_the_two_endpoints() -> None:
39+
"""The specific relationship the corpus never exercises.
40+
41+
Stated as an explicit rank comparison rather than inferred from the tuple above, so
42+
that a future change which keeps the tuple's CONTENTS but alters how rank is derived
43+
still fails here.
44+
"""
45+
rank = list(MUTABILITY_MODES).index
46+
assert rank(MUTABILITY_READ_WRITE) < rank(MUTABILITY_WRITE_ONCE) < rank(MUTABILITY_READ_ONLY)

0 commit comments

Comments
 (0)