@factoredui/core 0.4.1 → 0.5.1

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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/index.cjs +127 -275
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +298 -201
  5. package/dist/index.d.ts +298 -201
  6. package/dist/index.js +123 -275
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -11
  9. package/dist/cli/factoredui.cjs +0 -229
  10. package/supabase/migrations/20250101000001_schema_and_extensions.sql +0 -52
  11. package/supabase/migrations/20250101000002_sessions.sql +0 -20
  12. package/supabase/migrations/20250101000003_events.sql +0 -30
  13. package/supabase/migrations/20250101000004_factors.sql +0 -52
  14. package/supabase/migrations/20250101000005_vectors_and_clusters.sql +0 -32
  15. package/supabase/migrations/20250101000006_experiments.sql +0 -85
  16. package/supabase/migrations/20250101000007_variant_configs.sql +0 -19
  17. package/supabase/migrations/20250101000008_thresholds.sql +0 -22
  18. package/supabase/migrations/20250101000009_materialized_views.sql +0 -272
  19. package/supabase/migrations/20250101000010_rls_policies.sql +0 -69
  20. package/supabase/migrations/20250101000011_cron_jobs.sql +0 -99
  21. package/supabase/migrations/20250101000012_assignment_insert_grant.sql +0 -7
  22. package/supabase/migrations/20250101000013_assignment_variant_fk.sql +0 -7
  23. package/supabase/migrations/20250101000014_refresh_factor_views_fn.sql +0 -24
  24. package/supabase/migrations/20250101000015_structural_factors.sql +0 -123
  25. package/supabase/migrations/20250101000016_fix_cron_refresh.sql +0 -11
  26. package/supabase/migrations/20250101000017_targeting_rules.sql +0 -9
  27. package/supabase/migrations/20250101000018_governance_view.sql +0 -39
  28. package/supabase/migrations/20250101000019_bulk_factor_deltas.sql +0 -60
  29. package/supabase/migrations/20250101000020_governance_log.sql +0 -45
  30. package/supabase/migrations/20250101000021_governance_cron.sql +0 -170
  31. package/supabase/migrations/20250101000022_governance_trigger.sql +0 -217
  32. package/supabase/migrations/20250101000023_realtime_publication.sql +0 -9
  33. package/supabase/migrations/20250101000024_platform_dimension.sql +0 -18
  34. package/supabase/migrations/20250101000025_ui_specs.sql +0 -55
  35. package/supabase/migrations/20250101000026_rls_ui_specs.sql +0 -9
  36. package/supabase/migrations/20250101000027_devices.sql +0 -44
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Capture user interactions, compute behavioral factors, run experiments, and render server-driven UI. Pure TypeScript — no framework dependencies.
4
4
 
5
- Part of the [FactoredUI](https://github.com/jimjrasche/factored-ui) monorepo.
5
+ Part of the [FactoredUI](https://github.com/jjrasche/factoredui) monorepo.
6
6
 
7
7
  ## Install
8
8
 
package/dist/index.cjs CHANGED
@@ -27,7 +27,9 @@ __export(src_exports, {
27
27
  createDataSourceCache: () => createDataSourceCache,
28
28
  createEd25519Signer: () => createEd25519Signer,
29
29
  createEd25519Verifier: () => createEd25519Verifier,
30
+ createEventWriter: () => createEventWriter,
30
31
  createExperiment: () => createExperiment,
32
+ createSessionManager: () => createSessionManager,
31
33
  createSpecStorage: () => createSpecStorage,
32
34
  createWebAdapter: () => createWebAdapter,
33
35
  devSignatureVerifier: () => devSignatureVerifier,
@@ -47,10 +49,12 @@ __export(src_exports, {
47
49
  queryComponentFactors: () => queryComponentFactors,
48
50
  queryExperimentResults: () => queryExperimentResults,
49
51
  queryExperimentSummaries: () => queryExperimentSummaries,
52
+ queryExperimentSummary: () => queryExperimentSummary,
50
53
  queryFactorDelta: () => queryFactorDelta,
51
54
  queryFactorHistory: () => queryFactorHistory,
52
55
  queryFactors: () => queryFactors,
53
56
  queryGovernanceLog: () => queryGovernanceLog,
57
+ queryGovernanceLogByVerdict: () => queryGovernanceLogByVerdict,
54
58
  queryRecentGovernanceLog: () => queryRecentGovernanceLog,
55
59
  queryUserCluster: () => queryUserCluster,
56
60
  resolveAllSources: () => resolveAllSources,
@@ -66,7 +70,7 @@ module.exports = __toCommonJS(src_exports);
66
70
 
67
71
  // src/capture/session.ts
68
72
  var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
69
- function createSessionManager(supabase, adapter, platform, timeoutMs = DEFAULT_TIMEOUT_MS) {
73
+ function createSessionManager(store, adapter, platform, timeoutMs = DEFAULT_TIMEOUT_MS) {
70
74
  let currentSessionId = adapter.loadSessionId();
71
75
  let lastActivityAt = Date.now();
72
76
  function isSessionExpired() {
@@ -77,9 +81,8 @@ function createSessionManager(supabase, adapter, platform, timeoutMs = DEFAULT_T
77
81
  ...adapter.collectSessionMetadata(),
78
82
  platform
79
83
  };
80
- const { data, error } = await supabase.from("sessions").insert({ user_id: userId, metadata }).select("id").single();
81
- if (error) throw new Error(`Failed to create session: ${error.message}`);
82
- return data.id;
84
+ const { id } = await store.insertSession(userId, metadata);
85
+ return id;
83
86
  }
84
87
  async function ensureSession(userId) {
85
88
  lastActivityAt = Date.now();
@@ -92,7 +95,7 @@ function createSessionManager(supabase, adapter, platform, timeoutMs = DEFAULT_T
92
95
  }
93
96
  async function endSession() {
94
97
  if (!currentSessionId) return;
95
- await supabase.from("sessions").update({ ended_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", currentSessionId);
98
+ await store.endSession(currentSessionId);
96
99
  currentSessionId = null;
97
100
  adapter.clearSessionId();
98
101
  }
@@ -105,7 +108,7 @@ function createSessionManager(supabase, adapter, platform, timeoutMs = DEFAULT_T
105
108
  // src/capture/writer.ts
106
109
  var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
107
110
  var DEFAULT_FLUSH_BATCH_SIZE = 50;
108
- function createEventWriter(supabase, flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS, flushBatchSize = DEFAULT_FLUSH_BATCH_SIZE, adapter) {
111
+ function createEventWriter(store, flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS, flushBatchSize = DEFAULT_FLUSH_BATCH_SIZE, adapter) {
109
112
  let queue = [];
110
113
  let flushTimer = null;
111
114
  let isFlushing = false;
@@ -126,12 +129,7 @@ function createEventWriter(supabase, flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS
126
129
  isFlushing = true;
127
130
  const batch = queue.splice(0, flushBatchSize);
128
131
  try {
129
- const { error } = await supabase.from("events").insert(batch);
130
- if (error) {
131
- queue.unshift(...batch);
132
- persistQueueToAdapter();
133
- console.error("factoredui: flush failed:", error.message);
134
- }
132
+ await store.insertEvents(batch);
135
133
  } catch (err) {
136
134
  queue.unshift(...batch);
137
135
  persistQueueToAdapter();
@@ -513,18 +511,18 @@ function initCapture(config) {
513
511
  const adapter = config.adapter ?? createWebAdapter();
514
512
  const platform = config.platform ?? "web";
515
513
  const sessionManager = createSessionManager(
516
- config.supabase,
514
+ config.store,
517
515
  adapter,
518
516
  platform,
519
517
  config.sessionTimeoutMs
520
518
  );
521
519
  const writer = createEventWriter(
522
- config.supabase,
520
+ config.store,
523
521
  config.flushIntervalMs,
524
522
  config.flushBatchSize
525
523
  );
526
524
  const enqueueEvent = createEventEnqueuer(
527
- config.supabase,
525
+ config.store,
528
526
  sessionManager,
529
527
  writer
530
528
  );
@@ -553,10 +551,10 @@ function initCapture(config) {
553
551
  })
554
552
  };
555
553
  }
556
- function createEventEnqueuer(supabase, sessionManager, writer) {
554
+ function createEventEnqueuer(store, sessionManager, writer) {
557
555
  let cachedUserId = null;
558
556
  return (event) => {
559
- resolveUserId(supabase, cachedUserId).then((userId) => {
557
+ resolveUserId(store, cachedUserId).then((userId) => {
560
558
  cachedUserId = userId;
561
559
  return sessionManager.ensureSession(userId).then((sessionId) => {
562
560
  writer.enqueue(sessionId, userId, event);
@@ -566,43 +564,33 @@ function createEventEnqueuer(supabase, sessionManager, writer) {
566
564
  });
567
565
  };
568
566
  }
569
- async function resolveUserId(supabase, cachedUserId) {
567
+ async function resolveUserId(store, cachedUserId) {
570
568
  if (cachedUserId) return cachedUserId;
571
- const { data: { user } } = await supabase.auth.getUser();
572
- if (!user) throw new Error("factoredui: user not authenticated");
573
- return user.id;
569
+ const userId = await store.getCurrentUserId();
570
+ if (!userId) throw new Error("factoredui: user not authenticated");
571
+ return userId;
574
572
  }
575
573
 
576
574
  // src/factors/query.ts
577
- async function queryFactors(client, userId, componentPath) {
578
- const { data, error } = await client.from("v_factors_current").select("user_id, component_path, factor_name, factor_tier, value, computed_at").eq("user_id", userId).eq("component_path", componentPath);
579
- if (error) throw new Error(`queryFactors failed: ${error.message}`);
580
- return data;
581
- }
582
- async function queryComponentFactors(client, componentPath) {
583
- const { data, error } = await client.from("v_component_factors_agg").select(
584
- "component_path, factor_name, factor_tier, user_count, avg_value, median_value, p95_value, min_value, max_value, stddev_value"
585
- ).eq("component_path", componentPath);
586
- if (error) throw new Error(`queryComponentFactors failed: ${error.message}`);
587
- return data;
575
+ async function queryFactors(store, userId, componentPath) {
576
+ return store.queryFactors(userId, componentPath);
577
+ }
578
+ async function queryComponentFactors(store, componentPath) {
579
+ return store.queryComponentFactors(componentPath);
588
580
  }
589
581
 
590
582
  // src/factors/snapshots.ts
591
- async function queryFactorHistory(client, userId, componentPath, factorName, since) {
592
- const { data, error } = await client.from("factor_snapshots").select("factor_name, factor_tier, value, snapshot_at").eq("user_id", userId).eq("component_path", componentPath).eq("factor_name", factorName).gte("snapshot_at", since.toISOString()).order("snapshot_at", { ascending: true });
593
- if (error) throw new Error(`queryFactorHistory failed: ${error.message}`);
594
- return data;
595
- }
596
- async function queryFactorDelta(client, userId, componentPath, factorName, before, after) {
597
- const beforeSnapshot = await findClosestSnapshot(
598
- client,
583
+ async function queryFactorHistory(store, userId, componentPath, factorName, since) {
584
+ return store.queryFactorHistory(userId, componentPath, factorName, since);
585
+ }
586
+ async function queryFactorDelta(store, userId, componentPath, factorName, before, after) {
587
+ const beforeSnapshot = await store.findClosestSnapshot(
599
588
  userId,
600
589
  componentPath,
601
590
  factorName,
602
591
  before
603
592
  );
604
- const afterSnapshot = await findClosestSnapshot(
605
- client,
593
+ const afterSnapshot = await store.findClosestSnapshot(
606
594
  userId,
607
595
  componentPath,
608
596
  factorName,
@@ -616,28 +604,23 @@ async function queryFactorDelta(client, userId, componentPath, factorName, befor
616
604
  delta: afterSnapshot.value - beforeSnapshot.value
617
605
  };
618
606
  }
619
- async function findClosestSnapshot(client, userId, componentPath, factorName, targetDate) {
620
- const { data, error } = await client.from("factor_snapshots").select("factor_name, factor_tier, value, snapshot_at").eq("user_id", userId).eq("component_path", componentPath).eq("factor_name", factorName).lte("snapshot_at", targetDate.toISOString()).order("snapshot_at", { ascending: false }).limit(1).maybeSingle();
621
- if (error) throw new Error(`findClosestSnapshot failed: ${error.message}`);
622
- return data;
623
- }
624
607
 
625
608
  // src/factors/data-sources.ts
626
- function factorSource(supabase, userId, componentPath) {
609
+ function factorSource(store, userId, componentPath) {
627
610
  return {
628
- fetch: () => queryFactors(supabase, userId, componentPath),
611
+ fetch: () => queryFactors(store, userId, componentPath),
629
612
  cache: "local"
630
613
  };
631
614
  }
632
- function componentFactorSource(supabase, componentPath) {
615
+ function componentFactorSource(store, componentPath) {
633
616
  return {
634
- fetch: () => queryComponentFactors(supabase, componentPath),
617
+ fetch: () => queryComponentFactors(store, componentPath),
635
618
  cache: "local"
636
619
  };
637
620
  }
638
- function factorHistorySource(supabase, userId, componentPath, factorName, since) {
621
+ function factorHistorySource(store, userId, componentPath, factorName, since) {
639
622
  return {
640
- fetch: () => queryFactorHistory(supabase, userId, componentPath, factorName, since),
623
+ fetch: () => queryFactorHistory(store, userId, componentPath, factorName, since),
641
624
  cache: "local"
642
625
  };
643
626
  }
@@ -765,15 +748,11 @@ function buildStatColumn(id, label, valueRef) {
765
748
  }
766
749
 
767
750
  // src/factors/clustering.ts
768
- async function queryUserCluster(client, userId) {
769
- const { data, error } = await client.from("user_clusters").select("user_id, cluster_id, assigned_at").eq("user_id", userId).maybeSingle();
770
- if (error) throw new Error(`queryUserCluster failed: ${error.message}`);
771
- return data;
751
+ async function queryUserCluster(store, userId) {
752
+ return store.queryUserCluster(userId);
772
753
  }
773
- async function queryClusterMembers(client, clusterId) {
774
- const { data, error } = await client.from("user_clusters").select("user_id, cluster_id, assigned_at").eq("cluster_id", clusterId);
775
- if (error) throw new Error(`queryClusterMembers failed: ${error.message}`);
776
- return data ?? [];
754
+ async function queryClusterMembers(store, clusterId) {
755
+ return store.queryClusterMembers(clusterId);
777
756
  }
778
757
  var MAX_ITERATIONS = 50;
779
758
  var CONVERGENCE_THRESHOLD = 1e-6;
@@ -944,91 +923,54 @@ function compareString(value, operator, target) {
944
923
  }
945
924
 
946
925
  // src/experiment/flags.ts
947
- async function evaluateFlag(supabase, experimentName, platform, deviceMetadata) {
948
- const userId = await resolveUserId2(supabase);
926
+ async function evaluateFlag(store, experimentName, platform, deviceMetadata) {
927
+ const userId = await store.getCurrentUserId();
949
928
  if (!userId) return null;
950
- const existingAssignment = await fetchAssignment(supabase, userId, experimentName);
929
+ const existingAssignment = await store.getAssignment(userId, experimentName);
951
930
  if (existingAssignment) {
952
- await recordExposure(supabase, userId, existingAssignment);
931
+ await store.recordExposure(userId, existingAssignment.experiment_id, existingAssignment.variant_key);
953
932
  return existingAssignment;
954
933
  }
955
- const newAssignment = await assignToExperiment(supabase, userId, experimentName, platform, deviceMetadata);
934
+ const newAssignment = await assignToExperiment(store, userId, experimentName, platform, deviceMetadata);
956
935
  if (newAssignment) {
957
- await recordExposure(supabase, userId, newAssignment);
936
+ await store.recordExposure(userId, newAssignment.experiment_id, newAssignment.variant_key);
958
937
  }
959
938
  return newAssignment;
960
939
  }
961
- async function resolveUserId2(supabase) {
962
- const { data: { user } } = await supabase.auth.getUser();
963
- return user?.id ?? null;
964
- }
965
- async function fetchAssignment(supabase, userId, experimentName) {
966
- const { data, error } = await supabase.from("experiment_assignments").select(`
967
- experiment_id,
968
- variant_key,
969
- experiments!inner ( name, status ),
970
- experiment_variants!inner ( config )
971
- `).eq("user_id", userId).eq("experiments.name", experimentName).eq("experiments.status", "running").maybeSingle();
972
- if (error || !data) return null;
973
- const row = data;
974
- return {
975
- experiment_id: row.experiment_id,
976
- variant_key: row.variant_key,
977
- config: row.experiment_variants?.config ?? {}
978
- };
979
- }
980
- async function assignToExperiment(supabase, userId, experimentName, platform, deviceMetadata) {
981
- const experiment = await fetchRunningExperiment(supabase, experimentName, platform);
940
+ async function assignToExperiment(store, userId, experimentName, platform, deviceMetadata) {
941
+ const experiment = await store.getRunningExperiment(experimentName);
982
942
  if (!experiment) return null;
983
- const hasConflict = await hasConflictingAssignment(
984
- supabase,
943
+ if (platform && experiment.platforms.length > 0 && !experiment.platforms.includes(platform)) {
944
+ return null;
945
+ }
946
+ const hasConflict = await store.hasConflictingAssignment(
985
947
  userId,
986
948
  experiment.component_path,
987
949
  experiment.id
988
950
  );
989
951
  if (hasConflict) return null;
990
- const isTargeted = await checkTargeting(supabase, userId, experiment, deviceMetadata);
952
+ const isTargeted = await checkTargeting(store, userId, experiment, deviceMetadata);
991
953
  if (!isTargeted) return null;
992
- const variants = await fetchExperimentVariants(supabase, experiment.id);
954
+ const variants = await store.getVariants(experiment.id);
993
955
  if (variants.length === 0) return null;
994
956
  const selectedVariant = selectVariantByHash(userId, experiment.id, variants);
995
957
  if (!selectedVariant) return null;
996
- const { error } = await supabase.from("experiment_assignments").insert({
997
- user_id: userId,
998
- experiment_id: experiment.id,
999
- variant_key: selectedVariant.variant_key
1000
- });
1001
- if (error) return null;
958
+ try {
959
+ await store.writeAssignment(userId, experiment.id, selectedVariant.variant_key);
960
+ } catch {
961
+ return null;
962
+ }
1002
963
  return {
1003
964
  experiment_id: experiment.id,
1004
965
  variant_key: selectedVariant.variant_key,
1005
966
  config: selectedVariant.config
1006
967
  };
1007
968
  }
1008
- async function hasConflictingAssignment(supabase, userId, componentPath, currentExperimentId) {
1009
- const { data, error } = await supabase.from("experiment_assignments").select("experiment_id, experiments!inner ( id, status, component_path )").eq("user_id", userId).eq("experiments.status", "running").eq("experiments.component_path", componentPath).neq("experiment_id", currentExperimentId).limit(1);
1010
- if (error) return false;
1011
- return (data?.length ?? 0) > 0;
1012
- }
1013
- async function fetchRunningExperiment(supabase, experimentName, platform) {
1014
- const { data, error } = await supabase.from("experiments").select("id, name, component_path, targeting_rules, platforms").eq("name", experimentName).eq("status", "running").maybeSingle();
1015
- if (error || !data) return null;
1016
- const experiment = data;
1017
- if (platform && experiment.platforms.length > 0 && !experiment.platforms.includes(platform)) {
1018
- return null;
1019
- }
1020
- return experiment;
1021
- }
1022
- async function checkTargeting(supabase, userId, experiment, deviceMetadata) {
969
+ async function checkTargeting(store, userId, experiment, deviceMetadata) {
1023
970
  if (!experiment.targeting_rules || experiment.targeting_rules.length === 0) return true;
1024
- const factors = await queryFactors(supabase, userId, experiment.component_path);
971
+ const factors = await queryFactors(store, userId, experiment.component_path);
1025
972
  return evaluateTargeting(factors, experiment.targeting_rules, deviceMetadata);
1026
973
  }
1027
- async function fetchExperimentVariants(supabase, experimentId) {
1028
- const { data, error } = await supabase.from("experiment_variants").select("variant_key, config, traffic_percentage").eq("experiment_id", experimentId).order("variant_key");
1029
- if (error || !data) return [];
1030
- return data;
1031
- }
1032
974
  function selectVariantByHash(userId, experimentId, variants) {
1033
975
  const hashValue = simpleHash(`${userId}:${experimentId}`);
1034
976
  const bucket = hashValue % 100;
@@ -1048,27 +990,22 @@ function simpleHash(input) {
1048
990
  }
1049
991
  return hash;
1050
992
  }
1051
- async function recordExposure(supabase, userId, assignment) {
1052
- await supabase.from("experiment_exposures").insert({
1053
- user_id: userId,
1054
- experiment_id: assignment.experiment_id,
1055
- variant_key: assignment.variant_key
1056
- });
1057
- }
1058
993
 
1059
994
  // src/experiment/lifecycle.ts
1060
- async function createExperiment(client, definition) {
995
+ async function createExperiment(store, definition) {
1061
996
  validateDefinition(definition);
1062
- const experiment = await insertExperiment(client, definition);
1063
- await insertVariants(client, experiment.id, definition.variants);
997
+ const experiment = await store.insertExperiment({
998
+ name: definition.name,
999
+ description: definition.description ?? null,
1000
+ component_path: definition.component_path,
1001
+ targeting_rules: definition.targeting_rules ?? [],
1002
+ platforms: definition.platforms ?? []
1003
+ });
1004
+ await store.insertVariants(experiment.id, definition.variants);
1064
1005
  return experiment;
1065
1006
  }
1066
- async function startExperiment(client, experimentId) {
1067
- const { data, error } = await client.from("experiments").update({ status: "running" }).eq("id", experimentId).eq("status", "draft").select("id");
1068
- if (error) throw new Error(`startExperiment failed: ${error.message}`);
1069
- if (!data || data.length === 0) {
1070
- throw new Error(`startExperiment: experiment ${experimentId} not found or not in draft status`);
1071
- }
1007
+ async function startExperiment(store, experimentId) {
1008
+ await store.startExperiment(experimentId);
1072
1009
  }
1073
1010
  function validateDefinition(definition) {
1074
1011
  if (definition.variants.length < 2) {
@@ -1083,41 +1020,20 @@ function validateDefinition(definition) {
1083
1020
  throw new Error(`Traffic percentages must sum to 100, got ${totalTraffic}`);
1084
1021
  }
1085
1022
  }
1086
- async function insertExperiment(client, definition) {
1087
- const { data, error } = await client.from("experiments").insert({
1088
- name: definition.name,
1089
- description: definition.description ?? null,
1090
- component_path: definition.component_path,
1091
- targeting_rules: definition.targeting_rules ?? [],
1092
- platforms: definition.platforms ?? []
1093
- }).select("id, name, status, component_path").single();
1094
- if (error) throw new Error(`insertExperiment failed: ${error.message}`);
1095
- return data;
1096
- }
1097
- async function insertVariants(client, experimentId, variants) {
1098
- const rows = variants.map((v) => ({
1099
- experiment_id: experimentId,
1100
- variant_key: v.variant_key,
1101
- config: v.config,
1102
- traffic_percentage: v.traffic_percentage
1103
- }));
1104
- const { error } = await client.from("experiment_variants").insert(rows);
1105
- if (error) throw new Error(`insertVariants failed: ${error.message}`);
1106
- }
1107
1023
 
1108
1024
  // src/experiment/results.ts
1109
- async function queryExperimentResults(client, experimentId, factorNames) {
1110
- const experiment = await fetchExperiment(client, experimentId);
1111
- if (!experiment) return [];
1112
- const variantGroups = await fetchAssignmentsByVariant(client, experimentId);
1025
+ async function queryExperimentResults(store, experimentId, factorNames) {
1026
+ const meta = await store.getExperimentMeta(experimentId);
1027
+ if (!meta) return [];
1028
+ const variantGroups = await store.getAssignmentsByVariant(experimentId);
1113
1029
  const results = [];
1114
1030
  for (const [variantKey, userIds] of variantGroups) {
1115
1031
  const factorDeltas = await computeVariantDeltas(
1116
- client,
1032
+ store,
1117
1033
  userIds,
1118
- experiment.component_path,
1034
+ meta.component_path,
1119
1035
  factorNames,
1120
- experiment.created_at
1036
+ meta.created_at
1121
1037
  );
1122
1038
  results.push({
1123
1039
  variant_key: variantKey,
@@ -1127,35 +1043,17 @@ async function queryExperimentResults(client, experimentId, factorNames) {
1127
1043
  }
1128
1044
  return results;
1129
1045
  }
1130
- async function fetchExperiment(client, experimentId) {
1131
- const { data, error } = await client.from("experiments").select("component_path, created_at").eq("id", experimentId).maybeSingle();
1132
- if (error || !data) return null;
1133
- return data;
1134
- }
1135
- async function fetchAssignmentsByVariant(client, experimentId) {
1136
- const { data, error } = await client.from("experiment_assignments").select("variant_key, user_id").eq("experiment_id", experimentId);
1137
- if (error || !data) return /* @__PURE__ */ new Map();
1138
- const groups = /* @__PURE__ */ new Map();
1139
- for (const row of data) {
1140
- const existing = groups.get(row.variant_key) ?? [];
1141
- existing.push(row.user_id);
1142
- groups.set(row.variant_key, existing);
1143
- }
1144
- return groups;
1145
- }
1146
- async function computeVariantDeltas(client, userIds, componentPath, factorNames, experimentCreatedAt) {
1046
+ async function computeVariantDeltas(store, userIds, componentPath, factorNames, experimentCreatedAt) {
1147
1047
  const beforeDate = new Date(experimentCreatedAt);
1148
1048
  const afterDate = /* @__PURE__ */ new Date();
1149
- const { data, error } = await client.rpc("bulk_factor_deltas", {
1150
- p_user_ids: userIds,
1151
- p_component: componentPath,
1152
- p_factor_names: factorNames,
1153
- p_before: beforeDate.toISOString(),
1154
- p_after: afterDate.toISOString()
1155
- });
1156
- if (error) throw new Error(`bulk_factor_deltas failed: ${error.message}`);
1157
- if (!data) return [];
1158
- return data.map((row) => ({
1049
+ const rows = await store.bulkFactorDeltas(
1050
+ userIds,
1051
+ componentPath,
1052
+ factorNames,
1053
+ beforeDate.toISOString(),
1054
+ afterDate.toISOString()
1055
+ );
1056
+ return rows.map((row) => ({
1159
1057
  factor_name: row.factor_name,
1160
1058
  before: row.avg_before,
1161
1059
  after: row.avg_after,
@@ -1164,30 +1062,15 @@ async function computeVariantDeltas(client, userIds, componentPath, factorNames,
1164
1062
  }
1165
1063
 
1166
1064
  // src/experiment/governance.ts
1167
- async function evaluateExperimentThresholds(client, experimentId, factorNames) {
1168
- const componentPath = await fetchExperimentComponentPath(client, experimentId);
1169
- if (!componentPath) return buildEmptyVerdict();
1170
- const results = await queryExperimentResults(client, experimentId, factorNames);
1171
- const thresholds = await fetchThresholds(client, factorNames, componentPath);
1065
+ async function evaluateExperimentThresholds(store, experimentId, factorNames) {
1066
+ const meta = await store.getExperimentMeta(experimentId);
1067
+ if (!meta) return buildEmptyVerdict();
1068
+ const results = await queryExperimentResults(store, experimentId, factorNames);
1069
+ const thresholds = await store.queryThresholds(factorNames, meta.component_path);
1172
1070
  return computeGovernanceVerdict(results, thresholds);
1173
1071
  }
1174
- async function concludeExperiment(client, experimentId, winningVariant) {
1175
- const { error } = await client.from("experiments").update({
1176
- status: "concluded",
1177
- concluded_at: (/* @__PURE__ */ new Date()).toISOString(),
1178
- winning_variant: winningVariant
1179
- }).eq("id", experimentId).eq("status", "running");
1180
- if (error) throw new Error(`concludeExperiment failed: ${error.message}`);
1181
- }
1182
- async function fetchExperimentComponentPath(client, experimentId) {
1183
- const { data, error } = await client.from("experiments").select("component_path").eq("id", experimentId).maybeSingle();
1184
- if (error || !data) return null;
1185
- return data.component_path;
1186
- }
1187
- async function fetchThresholds(client, factorNames, componentPath) {
1188
- const { data, error } = await client.from("thresholds").select("id, factor_name, component_path, operator, value, action").in("factor_name", factorNames).or(`component_path.eq.${componentPath},component_path.is.null`);
1189
- if (error) throw new Error(`fetchThresholds failed: ${error.message}`);
1190
- return data ?? [];
1072
+ async function concludeExperiment(store, experimentId, winningVariant) {
1073
+ await store.concludeExperiment(experimentId, winningVariant);
1191
1074
  }
1192
1075
  function computeGovernanceVerdict(results, thresholds) {
1193
1076
  if (results.length < 2 || thresholds.length === 0) return buildEmptyVerdict();
@@ -1268,63 +1151,43 @@ function buildEmptyVerdict() {
1268
1151
  }
1269
1152
 
1270
1153
  // src/experiment/governance-check.ts
1271
- async function logGovernanceVerdict(client, experimentId, verdict) {
1272
- const { error } = await client.from("governance_log").insert({
1273
- experiment_id: experimentId,
1274
- verdict: verdict.action,
1275
- winning_variant: verdict.winning_variant,
1276
- factor_verdicts: verdict.factor_verdicts
1277
- });
1278
- if (error) throw new Error(`logGovernanceVerdict failed: ${error.message}`);
1154
+ async function logGovernanceVerdict(store, experimentId, verdict) {
1155
+ await store.insertGovernanceVerdict(
1156
+ experimentId,
1157
+ verdict.action,
1158
+ verdict.winning_variant,
1159
+ verdict.factor_verdicts
1160
+ );
1279
1161
  }
1280
- async function runGovernanceCheck(client, experimentId, factorNames) {
1281
- const verdict = await evaluateExperimentThresholds(client, experimentId, factorNames);
1282
- await logGovernanceVerdict(client, experimentId, verdict);
1283
- await concludeIfWinner(client, experimentId, verdict);
1162
+ async function runGovernanceCheck(store, experimentId, factorNames) {
1163
+ const verdict = await evaluateExperimentThresholds(store, experimentId, factorNames);
1164
+ await logGovernanceVerdict(store, experimentId, verdict);
1165
+ await concludeIfWinner(store, experimentId, verdict);
1284
1166
  return verdict;
1285
1167
  }
1286
- async function concludeIfWinner(client, experimentId, verdict) {
1168
+ async function concludeIfWinner(store, experimentId, verdict) {
1287
1169
  if (verdict.action === "conclude" && verdict.winning_variant) {
1288
- await concludeExperiment(client, experimentId, verdict.winning_variant);
1170
+ await concludeExperiment(store, experimentId, verdict.winning_variant);
1289
1171
  }
1290
1172
  }
1291
1173
 
1292
1174
  // src/experiment/dashboard.ts
1293
- async function queryExperimentSummaries(client, filters) {
1294
- let query = client.from("v_experiment_summary").select("*");
1295
- query = applyFilters(query, filters);
1296
- const { data, error } = await query;
1297
- if (error) throw new Error(`queryExperimentSummaries failed: ${error.message}`);
1298
- return data ?? [];
1175
+ async function queryExperimentSummaries(store, filters) {
1176
+ return store.queryExperimentSummaries(filters);
1299
1177
  }
1300
- function applyFilters(query, filters) {
1301
- if (!filters) return query;
1302
- if (filters.status) {
1303
- query = query.eq("status", filters.status);
1304
- }
1305
- if (filters.component_path) {
1306
- query = query.eq("component_path", filters.component_path);
1307
- }
1308
- if (filters.created_after) {
1309
- query = query.gte("created_at", filters.created_after);
1310
- }
1311
- if (filters.created_before) {
1312
- query = query.lte("created_at", filters.created_before);
1313
- }
1314
- return query;
1178
+ async function queryExperimentSummary(store, experimentId) {
1179
+ return store.queryExperimentSummary(experimentId);
1315
1180
  }
1316
1181
 
1317
1182
  // src/experiment/governance-log.ts
1318
- var GOVERNANCE_LOG_COLUMNS = "id, experiment_id, verdict, winning_variant, factor_verdicts, evaluated_at";
1319
- async function queryGovernanceLog(client, experimentId) {
1320
- const { data, error } = await client.from("governance_log").select(GOVERNANCE_LOG_COLUMNS).eq("experiment_id", experimentId).order("evaluated_at", { ascending: false });
1321
- if (error) throw new Error(`queryGovernanceLog failed: ${error.message}`);
1322
- return data ?? [];
1183
+ async function queryGovernanceLog(store, experimentId) {
1184
+ return store.queryGovernanceLog(experimentId);
1323
1185
  }
1324
- async function queryRecentGovernanceLog(client, limit = 50) {
1325
- const { data, error } = await client.from("governance_log").select(GOVERNANCE_LOG_COLUMNS).order("evaluated_at", { ascending: false }).limit(limit);
1326
- if (error) throw new Error(`queryRecentGovernanceLog failed: ${error.message}`);
1327
- return data ?? [];
1186
+ async function queryRecentGovernanceLog(store, limit = 50) {
1187
+ return store.queryRecentGovernanceLog(limit);
1188
+ }
1189
+ async function queryGovernanceLogByVerdict(store, verdict) {
1190
+ return store.queryGovernanceLogByVerdict(verdict);
1328
1191
  }
1329
1192
 
1330
1193
  // src/sdui/spec-validator.ts
@@ -1551,8 +1414,8 @@ function dispatchAction(actionRef, registry, context) {
1551
1414
  }
1552
1415
 
1553
1416
  // src/sdui/spec-loader.ts
1554
- async function loadSpec(supabase, platform, baseline, storage, verifier) {
1555
- const remote = await fetchRemoteSpec(supabase, platform);
1417
+ async function loadSpec(store, platform, baseline, storage, verifier) {
1418
+ const remote = await fetchRemoteSpec(store, platform);
1556
1419
  if (remote) {
1557
1420
  const validated = await validateSignedSpec(remote, verifier);
1558
1421
  if (validated) {
@@ -1582,28 +1445,13 @@ async function verifySpecHash(signed, verifier) {
1582
1445
  const computedHash = await verifier.computeHash(signed.spec);
1583
1446
  return computedHash === signed.spec_hash;
1584
1447
  }
1585
- async function fetchRemoteSpec(supabase, platform) {
1448
+ async function fetchRemoteSpec(store, platform) {
1586
1449
  try {
1587
- const { data, error } = await supabase.from("ui_active").select("spec_id, ui_specs(*)").eq("platform", platform).single();
1588
- if (error || !data) return null;
1589
- return mapRowToSignedSpec(data);
1450
+ return await store.loadActiveSpec(platform);
1590
1451
  } catch {
1591
1452
  return null;
1592
1453
  }
1593
1454
  }
1594
- function mapRowToSignedSpec(data) {
1595
- const row = data;
1596
- return {
1597
- spec: {
1598
- spec_version: row.ui_specs.spec_version,
1599
- renderer_min: row.ui_specs.renderer_min,
1600
- root: row.ui_specs.component_tree
1601
- },
1602
- signature: row.ui_specs.signature,
1603
- signed_at: "",
1604
- spec_hash: row.ui_specs.spec_hash
1605
- };
1606
- }
1607
1455
  async function loadActiveSpec(storage) {
1608
1456
  try {
1609
1457
  return await storage.loadActive();
@@ -1754,7 +1602,9 @@ function bytesToHex(bytes) {
1754
1602
  createDataSourceCache,
1755
1603
  createEd25519Signer,
1756
1604
  createEd25519Verifier,
1605
+ createEventWriter,
1757
1606
  createExperiment,
1607
+ createSessionManager,
1758
1608
  createSpecStorage,
1759
1609
  createWebAdapter,
1760
1610
  devSignatureVerifier,
@@ -1774,10 +1624,12 @@ function bytesToHex(bytes) {
1774
1624
  queryComponentFactors,
1775
1625
  queryExperimentResults,
1776
1626
  queryExperimentSummaries,
1627
+ queryExperimentSummary,
1777
1628
  queryFactorDelta,
1778
1629
  queryFactorHistory,
1779
1630
  queryFactors,
1780
1631
  queryGovernanceLog,
1632
+ queryGovernanceLogByVerdict,
1781
1633
  queryRecentGovernanceLog,
1782
1634
  queryUserCluster,
1783
1635
  resolveAllSources,