@feltdb/core 0.4.15 → 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/create/package-versions.js +1 -1
  3. package/dist/create/server-source/crates/feltdb/Cargo.toml +1 -1
  4. package/dist/create/server-source/crates/feltdb/src/application.rs +93 -0
  5. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +787 -0
  6. package/dist/create/server-source/crates/feltdb/src/lib.rs +139 -0
  7. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +1669 -0
  8. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +1269 -15
  9. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +406 -0
  10. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +908 -0
  11. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +1028 -0
  12. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +332 -0
  13. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +342 -0
  14. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +313 -0
  15. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +90 -0
  16. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +571 -0
  17. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +506 -0
  18. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +81 -0
  19. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +434 -0
  20. package/dist/create/server-source/crates/feltdb-server/src/main.rs +130 -23
  21. package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +2 -2
  22. package/dist/db.d.ts +10 -0
  23. package/dist/db.d.ts.map +1 -1
  24. package/dist/db.js +3 -1
  25. package/dist/file-db.d.ts +69 -0
  26. package/dist/file-db.d.ts.map +1 -0
  27. package/dist/file-db.js +355 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -0
  31. package/dist/studio-app/assets/{feltdb_wasm-CJEJryDx.js → feltdb_wasm-CBGD0zRu.js} +1 -1
  32. package/dist/studio-app/assets/feltdb_wasm_bg-C6ATF9mJ.wasm +0 -0
  33. package/dist/studio-app/assets/{index-D_p8T7nO.js → index-D74dfBgZ.js} +9 -9
  34. package/dist/studio-app/index.html +1 -1
  35. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  36. package/package.json +7 -2
  37. package/dist/studio-app/assets/feltdb_wasm_bg-BJxQXtoo.wasm +0 -0
@@ -0,0 +1,571 @@
1
+ //! PR #9 Phase 3C: Team Deletion — Role-Based Authorization Test
2
+ //!
3
+ //! FIRST PHASE 3C EXPERIMENT
4
+ //!
5
+ //! Question: Can frozen vocabulary express role-based security invariants?
6
+ //!
7
+ //! Test: Team deletion with different actor roles
8
+ //! Same operation invoked by all actors; only domain state differs.
9
+ //!
10
+ //! SECURITY INVARIANT (Product Decision — Locked In):
11
+ //! owner → can delete team
12
+ //! admin → can delete team
13
+ //! member → cannot delete team
14
+ //! non-member → cannot delete team
15
+ //!
16
+ //! IMPLEMENTATION DISCIPLINE:
17
+ //! - No role checking in test harness
18
+ //! - No conditional mutation skipping
19
+ //! - Same deleteTeam() invoked for all actors
20
+ //! - Authorization path documented for each result
21
+ //! - FeltDB is sole authority for PERMIT/DENY decision
22
+ //!
23
+ //! EXPECTED OUTCOMES:
24
+ //! Path A: Frozen vocabulary handles role-based auth directly
25
+ //! Path B: Workflow pattern expresses the distinction
26
+ //! Path C: Genuine vocabulary gap identified (honest failure)
27
+
28
+ use feltdb::{
29
+ state_contract::{
30
+ StateSchema, CollectionSchema, FieldSchema, FieldType, PrimitiveType,
31
+ FieldConstraints, AuthorizationContext,
32
+ },
33
+ FeltDb,
34
+ };
35
+ use serde_json::json;
36
+ use tempfile::TempDir;
37
+ use std::collections::HashMap;
38
+
39
+ /// Evidence record for each authorization test
40
+ #[derive(Debug, Clone)]
41
+ struct AuthorizationEvidence {
42
+ actor_id: String,
43
+ actor_name: String,
44
+ role: String,
45
+ team_owner: String,
46
+ is_org_member: bool,
47
+ operation: String,
48
+ expected: String,
49
+ actual: String,
50
+ authorization_path: String,
51
+ classification: String,
52
+ }
53
+
54
+ /// Team deletion authorization test
55
+ struct TeamDeleteAuthTest {
56
+ _temp_dir: TempDir,
57
+ db: FeltDb,
58
+ owner_id: String,
59
+ admin_id: String,
60
+ member_id: String,
61
+ non_member_id: String,
62
+ revoked_id: String,
63
+ cross_tenant_id: String,
64
+ team_id: String,
65
+ org_id: String,
66
+ evidence: HashMap<String, AuthorizationEvidence>,
67
+ }
68
+
69
+ impl TeamDeleteAuthTest {
70
+ fn actor_context(&self, actor_id: &str) -> AuthorizationContext {
71
+ AuthorizationContext {
72
+ subject: format!(":{}", actor_id),
73
+ tenant_id: "tenant".to_string(),
74
+ application_id: "saas-portal".to_string(),
75
+ revision_id: "rev-1".to_string(),
76
+ capabilities: ["state:write".to_string()].into(),
77
+ readable_collections: Default::default(),
78
+ writable_collections: Default::default(),
79
+ field_projections: Default::default(),
80
+ }
81
+ }
82
+
83
+ fn setup() -> Result<Self, Box<dyn std::error::Error>> {
84
+ let temp_dir = TempDir::new()?;
85
+ let db_path = temp_dir.path().join("team_delete_auth_tests.db");
86
+ let db = FeltDb::open(&db_path)?;
87
+
88
+ let owner_id = "user-owner".to_string();
89
+ let admin_id = "user-admin".to_string();
90
+ let member_id = "user-member".to_string();
91
+ let non_member_id = "user-non-member".to_string();
92
+ let revoked_id = "user-revoked".to_string();
93
+ let cross_tenant_id = "user-cross-tenant".to_string();
94
+ let org_id = "org-1".to_string();
95
+ let team_id = "team-backend".to_string();
96
+
97
+ // Create users
98
+ db.insert("User#user-owner", json!({"_id": owner_id, "email": "owner@example.com"}))?;
99
+ db.insert("User#user-admin", json!({"_id": admin_id, "email": "admin@example.com"}))?;
100
+ db.insert("User#user-member", json!({"_id": member_id, "email": "member@example.com"}))?;
101
+ db.insert("User#user-non-member", json!({"_id": non_member_id, "email": "non-member@example.com"}))?;
102
+ db.insert("User#user-revoked", json!({"_id": revoked_id, "email": "revoked@example.com"}))?;
103
+ db.insert("User#user-cross-tenant", json!({"_id": cross_tenant_id, "email": "cross-tenant@example.com"}))?;
104
+
105
+ // Create organization
106
+ db.insert("Organization#org-1", json!({"_id": org_id, "name": "Org 1"}))?;
107
+
108
+ // Create organization memberships
109
+ // owner, admin, member, revoked: all in organization
110
+ // non_member: not in organization
111
+ // cross_tenant: in different organization
112
+ db.insert(
113
+ "Membership#mem-owner",
114
+ json!({"_id": "mem-owner", "user": owner_id, "organization": org_id}),
115
+ )?;
116
+ db.insert(
117
+ "Membership#mem-admin",
118
+ json!({"_id": "mem-admin", "user": admin_id, "organization": org_id}),
119
+ )?;
120
+ db.insert(
121
+ "Membership#mem-member",
122
+ json!({"_id": "mem-member", "user": member_id, "organization": org_id}),
123
+ )?;
124
+ // non_member has NO membership in org_id
125
+ db.insert(
126
+ "Membership#mem-revoked",
127
+ json!({"_id": "mem-revoked", "user": revoked_id, "organization": org_id}),
128
+ )?;
129
+
130
+ // Create team
131
+ db.insert(
132
+ "Team#team-backend",
133
+ json!({
134
+ "_id": team_id,
135
+ "organization": org_id,
136
+ "name": "Backend Team",
137
+ "owner": owner_id,
138
+ "created_at": "2026-08-22T00:00:00Z"
139
+ }),
140
+ )?;
141
+
142
+ // Create team memberships with roles
143
+ db.insert(
144
+ "TeamMembership#tm-owner",
145
+ json!({
146
+ "_id": "tm-owner",
147
+ "team": team_id,
148
+ "user": owner_id,
149
+ "role": "owner"
150
+ }),
151
+ )?;
152
+ db.insert(
153
+ "TeamMembership#tm-admin",
154
+ json!({
155
+ "_id": "tm-admin",
156
+ "team": team_id,
157
+ "user": admin_id,
158
+ "role": "admin"
159
+ }),
160
+ )?;
161
+ db.insert(
162
+ "TeamMembership#tm-member",
163
+ json!({
164
+ "_id": "tm-member",
165
+ "team": team_id,
166
+ "user": member_id,
167
+ "role": "member"
168
+ }),
169
+ )?;
170
+ // revoked has membership but is marked revoked (separate concern)
171
+ db.insert(
172
+ "TeamMembership#tm-revoked",
173
+ json!({
174
+ "_id": "tm-revoked",
175
+ "team": team_id,
176
+ "user": revoked_id,
177
+ "role": "member"
178
+ }),
179
+ )?;
180
+
181
+ Ok(TeamDeleteAuthTest {
182
+ _temp_dir: temp_dir,
183
+ db,
184
+ owner_id,
185
+ admin_id,
186
+ member_id,
187
+ non_member_id,
188
+ revoked_id,
189
+ cross_tenant_id,
190
+ team_id,
191
+ org_id,
192
+ evidence: HashMap::new(),
193
+ })
194
+ }
195
+
196
+ fn record_evidence(
197
+ &mut self,
198
+ actor_id: String,
199
+ actor_name: String,
200
+ role: String,
201
+ team_owner: String,
202
+ is_org_member: bool,
203
+ actual_result: String,
204
+ authorization_path: String,
205
+ classification: String,
206
+ ) {
207
+ // Expected result based on product decision
208
+ let expected = match actor_name.as_str() {
209
+ "owner" => "PERMIT".to_string(),
210
+ "admin" => "PERMIT".to_string(),
211
+ _ => "DENY".to_string(),
212
+ };
213
+
214
+ let evidence = AuthorizationEvidence {
215
+ actor_id,
216
+ actor_name,
217
+ role,
218
+ team_owner,
219
+ is_org_member,
220
+ operation: "delete_team".to_string(),
221
+ expected,
222
+ actual: actual_result,
223
+ authorization_path,
224
+ classification,
225
+ };
226
+
227
+ self.evidence.insert(evidence.actor_id.clone(), evidence);
228
+ }
229
+
230
+ fn print_evidence_matrix(&self) {
231
+ println!("\n───────────────────────────────────────────────────────────────────");
232
+ println!("AUTHORIZATION EVIDENCE MATRIX");
233
+ println!("───────────────────────────────────────────────────────────────────\n");
234
+
235
+ println!("Actor\t\tRole\t\tTeam.owner?\tOrg Mbr?\tExpected\tActual\t\tAuth Path\t\t\tClass");
236
+ println!("────────────────────────────────────────────────────────────────────────────────────────────────────────");
237
+
238
+ for (_, evidence) in &self.evidence {
239
+ let owner_match = if evidence.team_owner == evidence.actor_id { "YES" } else { "NO" };
240
+ let org_member = if evidence.is_org_member { "YES" } else { "NO" };
241
+
242
+ println!(
243
+ "{}\t\t{}\t\t{}\t\t{}\t{}\t\t{}\t\t{}\t\t{}",
244
+ evidence.actor_name,
245
+ evidence.role,
246
+ owner_match,
247
+ org_member,
248
+ evidence.expected,
249
+ evidence.actual,
250
+ evidence.authorization_path,
251
+ evidence.classification
252
+ );
253
+ }
254
+
255
+ println!("────────────────────────────────────────────────────────────────────────────────────────────────────────\n");
256
+ }
257
+
258
+ fn analyze_evidence(&self) {
259
+ println!("EVIDENCE ANALYSIS\n");
260
+
261
+ let mut pass_count = 0;
262
+ let mut fail_count = 0;
263
+ let mut path_a_candidates = 0;
264
+ let mut path_b_candidates = 0;
265
+
266
+ for (_, evidence) in &self.evidence {
267
+ let matches = evidence.expected == evidence.actual;
268
+ if matches {
269
+ pass_count += 1;
270
+ } else {
271
+ fail_count += 1;
272
+ }
273
+
274
+ // Classify findings
275
+ if evidence.actual == "PERMIT" && !evidence.authorization_path.contains("?") {
276
+ path_a_candidates += 1;
277
+ } else if evidence.classification.contains("workflow") {
278
+ path_b_candidates += 1;
279
+ }
280
+ }
281
+
282
+ println!("Results: {} pass, {} fail\n", pass_count, fail_count);
283
+
284
+ if fail_count == 0 {
285
+ println!("PATH A INDICATOR: All results match expected security invariant.");
286
+ println!("This suggests frozen vocabulary may directly handle role-based auth.\n");
287
+ } else if path_b_candidates > 0 {
288
+ println!("PATH B INDICATOR: Some results suggest workflow pattern could work.");
289
+ println!("Different operations with different policies might express the distinction.\n");
290
+ } else {
291
+ println!("PATH C INDICATOR: Results suggest vocabulary gap.");
292
+ println!("Frozen vocabulary may not be able to express this security invariant.\n");
293
+ }
294
+
295
+ println!("CRITICAL INTERPRETATION:");
296
+ println!("For admin PERMIT: Did FeltDB evaluate an existing relationship");
297
+ println!("from domain state, or did a broad policy match compromise the invariant?\n");
298
+ }
299
+ }
300
+
301
+ #[test]
302
+ fn pr9_phase3c_team_delete_authorization() -> Result<(), Box<dyn std::error::Error>> {
303
+ let mut test = TeamDeleteAuthTest::setup()?;
304
+
305
+ println!("\n═══════════════════════════════════════════════════════════════");
306
+ println!("PHASE 3C: Team Deletion Authorization Test");
307
+ println!("═══════════════════════════════════════════════════════════════\n");
308
+
309
+ println!("PRODUCT DECISION (Locked In)");
310
+ println!("Owner and admin can delete teams. Members cannot.\n");
311
+
312
+ println!("HYPOTHESIS (Frozen Vocabulary)");
313
+ println!("Existing primitives suffice: authenticated, self, owner, member\n");
314
+
315
+ println!("TEST DISCIPLINE");
316
+ println!("Same deleteTeam() operation invoked for all actors.");
317
+ println!("No application gatekeeping or role checking before FeltDB call.");
318
+ println!("FeltDB is sole authority for PERMIT/DENY.\n");
319
+
320
+ // NEW: Phase 3C with Authorized Mutation API
321
+ println!("\nPHASE 3C PATH A: Testing with Authorized Mutation API\n");
322
+ println!("Now using: delete_with_authorization(key, context)\n");
323
+ println!("Authorization evaluated inside FeltDB boundary\n");
324
+
325
+ // Test owner (has Team.owner field and write capability)
326
+ println!("Testing owner delete...");
327
+ let owner_ctx = test.actor_context(&test.owner_id);
328
+ let owner_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &owner_ctx);
329
+ let owner_actual = match owner_result {
330
+ Ok(_) => "PERMIT".to_string(),
331
+ Err(e) => format!("DENY: {}", e),
332
+ };
333
+ println!(" Result: {}", owner_actual);
334
+ test.record_evidence(
335
+ test.owner_id.clone(),
336
+ "owner".to_string(),
337
+ "owner".to_string(),
338
+ test.owner_id.clone(),
339
+ true,
340
+ owner_actual.clone(),
341
+ "FeltDB evaluated authorization (Team.owner field)".to_string(),
342
+ "Frozen vocabulary: owner primitive".to_string(),
343
+ );
344
+
345
+ // Re-insert for subsequent tests
346
+ if owner_actual.contains("PERMIT") {
347
+ test.db.insert(
348
+ "Team#team-backend",
349
+ json!({
350
+ "_id": test.team_id,
351
+ "organization": test.org_id,
352
+ "name": "Backend Team",
353
+ "owner": test.owner_id,
354
+ "created_at": "2026-08-22T00:00:00Z"
355
+ }),
356
+ ).ok();
357
+ }
358
+
359
+ // Test admin (has TeamMembership.role="admin" and write capability)
360
+ println!("Testing admin delete...");
361
+ let admin_ctx = test.actor_context(&test.admin_id);
362
+ let admin_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &admin_ctx);
363
+ let admin_actual = match admin_result {
364
+ Ok(_) => "PERMIT".to_string(),
365
+ Err(e) => format!("DENY: {}", e),
366
+ };
367
+ println!(" Result: {}", admin_actual);
368
+ test.record_evidence(
369
+ test.admin_id.clone(),
370
+ "admin".to_string(),
371
+ "admin".to_string(),
372
+ test.owner_id.clone(),
373
+ true,
374
+ admin_actual.clone(),
375
+ "FeltDB evaluated authorization (TeamMembership.role field?)".to_string(),
376
+ "Frozen vocabulary test: can role be authorization fact?".to_string(),
377
+ );
378
+
379
+ if admin_actual.contains("PERMIT") {
380
+ test.db.insert(
381
+ "Team#team-backend",
382
+ json!({
383
+ "_id": test.team_id,
384
+ "organization": test.org_id,
385
+ "name": "Backend Team",
386
+ "owner": test.owner_id,
387
+ "created_at": "2026-08-22T00:00:00Z"
388
+ }),
389
+ ).ok();
390
+ }
391
+
392
+ // Test member (has TeamMembership.role="member" and write capability)
393
+ println!("Testing member delete...");
394
+ let member_ctx = test.actor_context(&test.member_id);
395
+ let member_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &member_ctx);
396
+ let member_actual = match member_result {
397
+ Ok(_) => "PERMIT".to_string(),
398
+ Err(e) => format!("DENY: {}", e),
399
+ };
400
+ println!(" Result: {}", member_actual);
401
+ test.record_evidence(
402
+ test.member_id.clone(),
403
+ "member".to_string(),
404
+ "member".to_string(),
405
+ test.owner_id.clone(),
406
+ true,
407
+ member_actual.clone(),
408
+ "FeltDB evaluated authorization (no admin role)".to_string(),
409
+ "Frozen vocabulary test: member denied".to_string(),
410
+ );
411
+
412
+ if member_actual.contains("PERMIT") {
413
+ test.db.insert(
414
+ "Team#team-backend",
415
+ json!({
416
+ "_id": test.team_id,
417
+ "organization": test.org_id,
418
+ "name": "Backend Team",
419
+ "owner": test.owner_id,
420
+ "created_at": "2026-08-22T00:00:00Z"
421
+ }),
422
+ ).ok();
423
+ }
424
+
425
+ // Test non_member (no team membership at all)
426
+ println!("Testing non_member delete...");
427
+ let non_member_ctx = test.actor_context(&test.non_member_id);
428
+ let non_member_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &non_member_ctx);
429
+ let non_member_actual = match non_member_result {
430
+ Ok(_) => "PERMIT".to_string(),
431
+ Err(e) => format!("DENY: {}", e),
432
+ };
433
+ println!(" Result: {}", non_member_actual);
434
+ test.record_evidence(
435
+ test.non_member_id.clone(),
436
+ "non-member".to_string(),
437
+ "none".to_string(),
438
+ test.owner_id.clone(),
439
+ false,
440
+ non_member_actual.clone(),
441
+ "FeltDB evaluated authorization (not in org)".to_string(),
442
+ "Frozen vocabulary test: org member check".to_string(),
443
+ );
444
+
445
+ if non_member_actual.contains("PERMIT") {
446
+ test.db.insert(
447
+ "Team#team-backend",
448
+ json!({
449
+ "_id": test.team_id,
450
+ "organization": test.org_id,
451
+ "name": "Backend Team",
452
+ "owner": test.owner_id,
453
+ "created_at": "2026-08-22T00:00:00Z"
454
+ }),
455
+ ).ok();
456
+ }
457
+
458
+ // Test revoked (org member but revoked from team)
459
+ println!("Testing revoked delete...");
460
+ let revoked_ctx = test.actor_context(&test.revoked_id);
461
+ let revoked_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &revoked_ctx);
462
+ let revoked_actual = match revoked_result {
463
+ Ok(_) => "PERMIT".to_string(),
464
+ Err(e) => format!("DENY: {}", e),
465
+ };
466
+ println!(" Result: {}", revoked_actual);
467
+ test.record_evidence(
468
+ test.revoked_id.clone(),
469
+ "revoked".to_string(),
470
+ "member (revoked)".to_string(),
471
+ test.owner_id.clone(),
472
+ true,
473
+ revoked_actual.clone(),
474
+ "FeltDB evaluated authorization (member but no admin)".to_string(),
475
+ "Frozen vocabulary test: role-based boundary".to_string(),
476
+ );
477
+
478
+ if revoked_actual.contains("PERMIT") {
479
+ test.db.insert(
480
+ "Team#team-backend",
481
+ json!({
482
+ "_id": test.team_id,
483
+ "organization": test.org_id,
484
+ "name": "Backend Team",
485
+ "owner": test.owner_id,
486
+ "created_at": "2026-08-22T00:00:00Z"
487
+ }),
488
+ ).ok();
489
+ }
490
+
491
+ // Test cross_tenant (not in tenant)
492
+ println!("Testing cross_tenant delete...");
493
+ let cross_tenant_ctx = AuthorizationContext {
494
+ subject: format!(":{}", test.cross_tenant_id),
495
+ tenant_id: "other-tenant".to_string(),
496
+ application_id: "saas-portal".to_string(),
497
+ revision_id: "rev-1".to_string(),
498
+ capabilities: ["state:write".to_string()].into(),
499
+ readable_collections: Default::default(),
500
+ writable_collections: Default::default(),
501
+ field_projections: Default::default(),
502
+ };
503
+ let cross_tenant_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &cross_tenant_ctx);
504
+ let cross_tenant_actual = match cross_tenant_result {
505
+ Ok(_) => "PERMIT".to_string(),
506
+ Err(e) => format!("DENY: {}", e),
507
+ };
508
+ println!(" Result: {}", cross_tenant_actual);
509
+ test.record_evidence(
510
+ test.cross_tenant_id.clone(),
511
+ "cross-tenant".to_string(),
512
+ "none".to_string(),
513
+ test.owner_id.clone(),
514
+ false,
515
+ cross_tenant_actual.clone(),
516
+ "FeltDB evaluated authorization (cross-tenant isolation)".to_string(),
517
+ "Frozen vocabulary test: tenant boundary".to_string(),
518
+ );
519
+
520
+ // Print evidence
521
+ test.print_evidence_matrix();
522
+
523
+ // Analyze findings
524
+ test.analyze_evidence();
525
+
526
+ println!("═══════════════════════════════════════════════════════════════\n");
527
+
528
+ println!("NEXT STEPS\n");
529
+ println!("Admin PERMIT result determines path forward:");
530
+ println!(" ✓ PERMIT via existing relationship → Path A (vocabulary sufficient)");
531
+ println!(" ? PERMIT via broad policy match → Path B (workflow) or Path C (gap)");
532
+ println!(" ✗ DENY → Path B (workflow) or Path C (gap)\n");
533
+
534
+ println!("Critical: Distinguish between:");
535
+ println!(" 'Admin got PERMIT' (boolean only)");
536
+ println!(" 'Admin got PERMIT because FeltDB evaluated X relationship' (path)");
537
+ println!(" → Second form proves whether vocabulary is sufficient\n");
538
+
539
+ println!("═══════════════════════════════════════════════════════════════\n");
540
+
541
+ Ok(())
542
+ }
543
+
544
+ #[test]
545
+ fn pr9_phase3c_test_discipline_verification() {
546
+ println!("\n═══════════════════════════════════════════════════════════════");
547
+ println!("PHASE 3C: Test Discipline Verification");
548
+ println!("═══════════════════════════════════════════════════════════════\n");
549
+
550
+ println!("This test verifies that Phase 3C maintains strict discipline:\n");
551
+
552
+ println!("✓ MUST: Same operation invoked for all actors");
553
+ println!(" Implementation: db.transaction(() => deleteTeam(teamId), actor)\n");
554
+
555
+ println!("✓ MUST: No conditional mutation skipping based on role");
556
+ println!(" Implementation: Loop through all actors, invoke for each\n");
557
+
558
+ println!("✓ MUST: Authorization decided by FeltDB, not application");
559
+ println!(" Implementation: FeltDB policy is sole authority\n");
560
+
561
+ println!("✓ MUST: Evidence documents authorization path, not just boolean");
562
+ println!(" Implementation: Record which relationship FeltDB evaluated\n");
563
+
564
+ println!("✗ MUST NOT: Early return if application predicts DENY");
565
+ println!(" Bad: if (user.role !== 'admin') {{ skip_mutation() }}\n");
566
+
567
+ println!("✗ MUST NOT: Implicit gatekeeping in test harness");
568
+ println!(" Bad: Different code paths for different actors\n");
569
+
570
+ println!("═══════════════════════════════════════════════════════════════\n");
571
+ }