@@ -23,6 +23,9 @@ import {
2323 ExplainDecisionSchema ,
2424 AccessMatrixEntrySchema ,
2525 AccessMatrixSchema ,
26+ AuthzPostureSchema ,
27+ ExplainMatchedRuleSchema ,
28+ ExplainRecordAttributionSchema ,
2629} from './explain.zod' ;
2730
2831describe ( 'ExplainOperationSchema — the operation vocabulary is fixed' , ( ) => {
@@ -36,15 +39,18 @@ describe('ExplainOperationSchema — the operation vocabulary is fixed', () => {
3639 } ) ;
3740} ) ;
3841
39- describe ( 'ExplainLayerSchema — the nine -layer pipeline + contributor shape' , ( ) => {
42+ describe ( 'ExplainLayerSchema — the ten -layer pipeline + contributor shape' , ( ) => {
4043 const LAYERS = [
44+ // [ADR-0095 D1] Layer 0 — the always-first tenant wall, ahead of principal.
45+ 'tenant_isolation' ,
4146 'principal' , 'required_permissions' , 'object_crud' , 'fls' ,
4247 'owd_baseline' , 'depth' , 'sharing' , 'vama_bypass' , 'rls' ,
4348 ] ;
44- it ( 'locks the nine layer ids, in order ' , ( ) => {
49+ it ( 'locks the ten layer ids, including the ADR-0095 tenant_isolation Layer 0 ' , ( ) => {
4550 for ( const layer of LAYERS ) {
4651 expect ( ExplainLayerSchema . parse ( { layer, verdict : 'neutral' , detail : 'x' } ) . layer ) . toBe ( layer ) ;
4752 }
53+ // A near-miss ('tenant') is still rejected — only the full id is a member.
4854 expect ( ( ) => ExplainLayerSchema . parse ( { layer : 'tenant' , verdict : 'neutral' , detail : 'x' } ) ) . toThrow ( ) ;
4955 } ) ;
5056
@@ -86,6 +92,76 @@ describe('ExplainLayerSchema — the nine-layer pipeline + contributor shape', (
8692 contributors : [ { kind : 'position' , name : 'x' , state : 'suspended' } ] ,
8793 } ) ) . toThrow ( ) ;
8894 } ) ;
95+
96+ it ( '[C2] kernelTier + record are optional — object-level layers omit them' , ( ) => {
97+ const bare = ExplainLayerSchema . parse ( { layer : 'sharing' , verdict : 'not_applicable' , detail : 'x' } ) ;
98+ expect ( bare . kernelTier ) . toBeUndefined ( ) ;
99+ expect ( bare . record ) . toBeUndefined ( ) ;
100+ } ) ;
101+
102+ it ( '[ADR-0095 D1] kernelTier tags Layer 0 vs Layer 1; rejects other values' , ( ) => {
103+ expect ( ExplainLayerSchema . parse ( {
104+ layer : 'tenant_isolation' , verdict : 'narrows' , detail : 'x' , kernelTier : 'layer_0_tenant' ,
105+ } ) . kernelTier ) . toBe ( 'layer_0_tenant' ) ;
106+ expect ( ExplainLayerSchema . parse ( {
107+ layer : 'rls' , verdict : 'narrows' , detail : 'x' , kernelTier : 'layer_1_business' ,
108+ } ) . kernelTier ) . toBe ( 'layer_1_business' ) ;
109+ expect ( ( ) => ExplainLayerSchema . parse ( {
110+ layer : 'rls' , verdict : 'narrows' , detail : 'x' , kernelTier : 'layer_2' ,
111+ } ) ) . toThrow ( ) ;
112+ } ) ;
113+
114+ it ( '[C2] a record-grained layer carries per-record attribution' , ( ) => {
115+ const layer = ExplainLayerSchema . parse ( {
116+ layer : 'sharing' , verdict : 'widens' , detail : 'x' , kernelTier : 'layer_1_business' ,
117+ record : {
118+ outcome : 'admitted' ,
119+ rowFilter : { owner : 'u2' } ,
120+ matchesRecord : true ,
121+ rules : [
122+ { kind : 'record_share' , name : 'share_row_42' , grants : 'read' , via : 'group:sales_team' , effect : 'admits' } ,
123+ { kind : 'ownership' , name : 'owner-check' , via : 'owner' , effect : 'neutral' } ,
124+ ] ,
125+ detail : 'admitted: an explicit share targets this row' ,
126+ } ,
127+ } ) ;
128+ expect ( layer . record ?. outcome ) . toBe ( 'admitted' ) ;
129+ expect ( layer . record ?. matchesRecord ) . toBe ( true ) ;
130+ expect ( layer . record ?. rules . map ( ( r ) => r . kind ) ) . toEqual ( [ 'record_share' , 'ownership' ] ) ;
131+ expect ( layer . record ?. rules [ 0 ] . grants ) . toBe ( 'read' ) ;
132+ } ) ;
133+ } ) ;
134+
135+ describe ( '[ADR-0095 D2] AuthzPostureSchema — the monotonic posture ladder' , ( ) => {
136+ it ( 'locks the four rungs' , ( ) => {
137+ for ( const rung of [ 'PLATFORM_ADMIN' , 'TENANT_ADMIN' , 'MEMBER' , 'EXTERNAL' ] ) {
138+ expect ( AuthzPostureSchema . parse ( rung ) ) . toBe ( rung ) ;
139+ }
140+ expect ( ( ) => AuthzPostureSchema . parse ( 'SUPERUSER' ) ) . toThrow ( ) ;
141+ } ) ;
142+ } ) ;
143+
144+ describe ( '[C2] ExplainMatchedRule / ExplainRecordAttribution — row-level attribution' , ( ) => {
145+ it ( 'matched rule locks its kinds, effect, and optional grants/via/predicate' , ( ) => {
146+ for ( const kind of [ 'tenant_filter' , 'owd_baseline' , 'ownership' , 'record_share' , 'sharing_rule' , 'team' , 'territory' , 'rls_policy' ] ) {
147+ expect ( ExplainMatchedRuleSchema . parse ( { kind, name : 'r' , effect : 'admits' } ) . kind ) . toBe ( kind ) ;
148+ }
149+ expect ( ( ) => ExplainMatchedRuleSchema . parse ( { kind : 'guess' , name : 'r' , effect : 'admits' } ) ) . toThrow ( ) ;
150+ expect ( ( ) => ExplainMatchedRuleSchema . parse ( { kind : 'ownership' , name : 'r' , effect : 'maybe' } ) ) . toThrow ( ) ;
151+ const full = ExplainMatchedRuleSchema . parse ( {
152+ kind : 'sharing_rule' , name : 'open_leads_to_sales' , grants : 'edit' ,
153+ via : 'criteria: status == open' , predicate : { status : 'open' } , effect : 'admits' ,
154+ } ) ;
155+ expect ( full . grants ) . toBe ( 'edit' ) ;
156+ expect ( full . predicate ) . toEqual ( { status : 'open' } ) ;
157+ } ) ;
158+
159+ it ( 'record attribution defaults rules to [] and locks outcome' , ( ) => {
160+ const bare = ExplainRecordAttributionSchema . parse ( { outcome : 'excluded' } ) ;
161+ expect ( bare . rules ) . toEqual ( [ ] ) ;
162+ expect ( bare . matchesRecord ) . toBeUndefined ( ) ;
163+ expect ( ( ) => ExplainRecordAttributionSchema . parse ( { outcome : 'hidden' } ) ) . toThrow ( ) ;
164+ } ) ;
89165} ) ;
90166
91167describe ( 'ExplainRequestSchema — the request contract' , ( ) => {
@@ -96,6 +172,15 @@ describe('ExplainRequestSchema — the request contract', () => {
96172 expect ( ExplainRequestSchema . parse ( { object : 'x' , operation : 'update' , userId : 'u2' } ) . userId ) . toBe ( 'u2' ) ;
97173 expect ( ( ) => ExplainRequestSchema . parse ( { operation : 'read' } ) ) . toThrow ( ) ;
98174 } ) ;
175+
176+ it ( '[C2] recordId is optional and round-trips; object-level requests stay backward-compatible' , ( ) => {
177+ // Backward compat: the pre-C2 object-level request still parses, recordId absent.
178+ const objectLevel = ExplainRequestSchema . parse ( { object : 'leave_request' , operation : 'read' } ) ;
179+ expect ( objectLevel . recordId ) . toBeUndefined ( ) ;
180+ // Record-grained request round-trips the recordId.
181+ const recordLevel = ExplainRequestSchema . parse ( { object : 'leave_request' , operation : 'update' , recordId : 'lr_42' } ) ;
182+ expect ( recordLevel . recordId ) . toBe ( 'lr_42' ) ;
183+ } ) ;
99184} ) ;
100185
101186describe ( 'ExplainDecisionSchema — the full decision report L3 consumes' , ( ) => {
@@ -131,6 +216,68 @@ describe('ExplainDecisionSchema — the full decision report L3 consumes', () =>
131216 expect ( parsed . readFilter ) . toEqual ( { owner : 'u2' } ) ;
132217 } ) ;
133218
219+ it ( '[C2] round-trips a record-grained decision with posture + per-record trace' , ( ) => {
220+ const decision = {
221+ allowed : false ,
222+ object : 'leave_request' ,
223+ operation : 'update' ,
224+ principal : {
225+ userId : 'u2' ,
226+ positions : [ 'approver' , 'everyone' ] ,
227+ permissionSets : [ 'approve_set' , 'member_default' ] ,
228+ principalKind : 'human' ,
229+ posture : 'MEMBER' ,
230+ } ,
231+ layers : [
232+ {
233+ layer : 'tenant_isolation' , verdict : 'narrows' , detail : '…' , kernelTier : 'layer_0_tenant' ,
234+ record : { outcome : 'admitted' , matchesRecord : true , rules : [ { kind : 'tenant_filter' , name : 'org_wall' , effect : 'admits' } ] } ,
235+ } ,
236+ {
237+ layer : 'sharing' , verdict : 'not_applicable' , detail : '…' , kernelTier : 'layer_1_business' ,
238+ record : {
239+ outcome : 'excluded' ,
240+ rowFilter : { owner : 'u2' } ,
241+ matchesRecord : false ,
242+ rules : [ { kind : 'ownership' , name : 'owner-check' , via : 'owner' , effect : 'excludes' } ] ,
243+ detail : 'excluded: you are not the owner and no share targets this row' ,
244+ } ,
245+ } ,
246+ ] ,
247+ record : { recordId : 'lr_42' , visible : false , decidedBy : 'sharing' } ,
248+ } ;
249+ const parsed = ExplainDecisionSchema . parse ( decision ) ;
250+ expect ( parsed . principal . posture ) . toBe ( 'MEMBER' ) ;
251+ expect ( parsed . record ?. recordId ) . toBe ( 'lr_42' ) ;
252+ expect ( parsed . record ?. visible ) . toBe ( false ) ;
253+ expect ( parsed . record ?. decidedBy ) . toBe ( 'sharing' ) ;
254+ expect ( parsed . layers [ 0 ] . kernelTier ) . toBe ( 'layer_0_tenant' ) ;
255+ expect ( parsed . layers [ 1 ] . record ?. outcome ) . toBe ( 'excluded' ) ;
256+ } ) ;
257+
258+ it ( '[C2] object-level decisions omit record + posture (backward-compatible)' , ( ) => {
259+ const parsed = ExplainDecisionSchema . parse ( {
260+ allowed : true , object : 'x' , operation : 'read' ,
261+ principal : { userId : 'u2' } ,
262+ layers : [ { layer : 'rls' , verdict : 'narrows' , detail : 'x' , contributors : [ ] } ] ,
263+ readFilter : { owner : 'u2' } ,
264+ } ) ;
265+ expect ( parsed . record ) . toBeUndefined ( ) ;
266+ expect ( parsed . principal . posture ) . toBeUndefined ( ) ;
267+ } ) ;
268+
269+ it ( '[ADR-0095] rejects an unknown posture or decidedBy layer' , ( ) => {
270+ expect ( ( ) => ExplainDecisionSchema . parse ( {
271+ allowed : true , object : 'x' , operation : 'read' ,
272+ principal : { userId : 'u' , posture : 'GOD_MODE' } , layers : [ ] ,
273+ } ) ) . toThrow ( ) ;
274+ expect ( ( ) => ExplainDecisionSchema . parse ( {
275+ allowed : true , object : 'x' , operation : 'read' ,
276+ principal : { userId : 'u' } , layers : [ ] ,
277+ record : { recordId : 'r1' , visible : true , decidedBy : 'quantum' } ,
278+ } ) ) . toThrow ( ) ;
279+ } ) ;
280+
134281 it ( 'principal.userId is nullable (anonymous), positions/permissionSets default to []' , ( ) => {
135282 const parsed = ExplainDecisionSchema . parse ( {
136283 allowed : false , object : 'x' , operation : 'read' ,
0 commit comments