@@ -83,6 +83,36 @@ public static void run(Checks c) {
8383 }
8484 }
8585
86+ c .section ("the T2/T3 membrane: no raw register crosses, and every array is a named breach" );
87+ // Doctrine: .claude/knowledge/membrane-tiers.md (T2/T3). A public consumer
88+ // surface may carry NAMES (handle, classid, field name, version), counts and
89+ // statuses — never a raw content register (byte[] = a [u8;12] rail/payload) and
90+ // never an un-named array population. `byte[]` is folded into FORBIDDEN above.
91+ // Array returns are the row-id/slot-index leak: allowed ONLY from a method whose
92+ // name announces the crossing (materialize* out, import* in) — the GraphHopTest
93+ // allowlist, enforced here on the COMPILED surface so a bridge/inherited return
94+ // cannot slip it past a source grep.
95+ List <String > unnamedArrays = new ArrayList <>();
96+ for (Class <?> type : types ) {
97+ for (Method m : type .getMethods ()) {
98+ if (!isPublicApi (m ) || m .getDeclaringClass () == Object .class ) {
99+ continue ;
100+ }
101+ if (m .getReturnType ().isArray () && !isNamedBreach (m .getName ())) {
102+ unnamedArrays .add (type .getSimpleName () + "." + m .getName ()
103+ + " returns " + m .getReturnType ().getSimpleName ()
104+ + " but its name does not start with materialize/import" );
105+ }
106+ }
107+ }
108+ if (unnamedArrays .isEmpty ()) {
109+ c .that ("every array-returning public method names its crossing (materialize*/import*)" , true );
110+ } else {
111+ for (String u : unnamedArrays ) {
112+ c .that ("UNNAMED-BREACH: " + u , false );
113+ }
114+ }
115+
86116 c .section ("the escape hatch is named, not incidental" );
87117 // Raw native access must require deliberately reaching into an internal package. It must
88118 // never be something ordinary composition hands you.
@@ -135,6 +165,7 @@ private static boolean isPublicApi(Executable e) {
135165
136166 private static void check (List <String > leaks , Class <?> owner , String where , Class <?> t ) {
137167 Class <?> component = t ;
168+ boolean isArray = t .isArray ();
138169 while (component .isArray ()) {
139170 component = component .getComponentType ();
140171 }
@@ -144,6 +175,19 @@ private static void check(List<String> leaks, Class<?> owner, String where, Clas
144175 leaks .add (owner .getSimpleName () + "." + where + " is " + name );
145176 }
146177 }
178+ // T2/T3 membrane: a byte[] in any public signature is a raw content register
179+ // (a [u8;12] rail array / payload bytes) crossing the wall — the substrate
180+ // wearing a collection. Names cross; registers never do.
181+ // (.claude/knowledge/membrane-tiers.md ledger L6.)
182+ if (isArray && component == byte .class ) {
183+ leaks .add (owner .getSimpleName () + "." + where
184+ + " is byte[] (a raw content register may not cross the consumer membrane)" );
185+ }
186+ }
187+
188+ /** A crossing whose method name announces it — the only sanctioned materialiser/importer. */
189+ private static boolean isNamedBreach (String methodName ) {
190+ return methodName .startsWith ("materialize" ) || methodName .startsWith ("import" );
147191 }
148192
149193 /** Enumerate public types by listing the compiled package directory on the classpath. */
0 commit comments