create-feltdb 0.9.1 → 0.11.0

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 (30) hide show
  1. package/dist/package-versions.js +1 -1
  2. package/dist/server-source/crates/feltdb/src/acceptance_tests.rs +8 -1
  3. package/dist/server-source/crates/feltdb/src/application.rs +16 -0
  4. package/dist/server-source/crates/feltdb/src/bin/feltdb_node.rs +21 -22
  5. package/dist/server-source/crates/feltdb/src/lib.rs +556 -45
  6. package/dist/server-source/crates/feltdb/src/state_contract.rs +48 -3
  7. package/dist/server-source/crates/feltdb/src/state_model.rs +569 -4
  8. package/dist/server-source/crates/feltdb/src/state_trigger.rs +1 -0
  9. package/dist/server-source/crates/feltdb/src/versioned_object_substrate.rs +708 -0
  10. package/dist/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
  11. package/dist/server-source/crates/feltdb/src/workload.rs +480 -44
  12. package/dist/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +123 -0
  13. package/dist/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +281 -0
  14. package/dist/server-source/crates/feltdb/tests/local_process_ownership.rs +69 -0
  15. package/dist/server-source/crates/feltdb/tests/production_readiness_contract.rs +15 -3
  16. package/dist/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +49 -24
  17. package/dist/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +328 -0
  18. package/dist/server-source/crates/feltdb/tests/workload_envelope_contract.rs +20 -3
  19. package/dist/server-source/crates/feltdb-server/src/app_state.rs +14 -4
  20. package/dist/server-source/crates/feltdb-server/src/backup.rs +0 -4
  21. package/dist/server-source/crates/feltdb-server/src/canonical_principals.rs +1093 -0
  22. package/dist/server-source/crates/feltdb-server/src/causal.rs +905 -144
  23. package/dist/server-source/crates/feltdb-server/src/connections.rs +9 -6
  24. package/dist/server-source/crates/feltdb-server/src/control_plane.rs +90 -0
  25. package/dist/server-source/crates/feltdb-server/src/lib.rs +1 -0
  26. package/dist/server-source/crates/feltdb-server/src/main.rs +418 -82
  27. package/dist/server-source/crates/feltdb-server/src/principals.rs +9 -501
  28. package/dist/server-source/crates/feltdb-server/src/tenancy.rs +22 -0
  29. package/dist/server-source/crates/feltdb-server/tests/principal_tenant_migration.rs +247 -0
  30. package/package.json +1 -1
@@ -0,0 +1,123 @@
1
+ //! Evidence for generalized version-control semantics over data representations.
2
+ //!
3
+ //! This test suite adds no new capability. It checks that the same revision
4
+ //! machinery behaves the same for source-shaped, artifact-shaped, and
5
+ //! application-shaped state.
6
+
7
+ use feltdb::{
8
+ reconcile, ConflictClass, ConflictClassification, ReconciliationPlan, Relationship, SemanticDiff,
9
+ StateStore, StateTopology,
10
+ };
11
+ use serde_json::{json, Value};
12
+
13
+ struct Scenario {
14
+ resource: &'static str,
15
+ base: Value,
16
+ left: Value,
17
+ right: Value,
18
+ }
19
+
20
+ #[derive(Debug, PartialEq, Eq)]
21
+ struct Outcome {
22
+ relationship: Relationship,
23
+ has_lca: bool,
24
+ left_changes: usize,
25
+ right_changes: usize,
26
+ overall: ConflictClass,
27
+ path_conflicts: usize,
28
+ }
29
+
30
+ fn run_scenario(scenario: Scenario) -> Outcome {
31
+ let store = StateStore::new_volatile();
32
+ let base = store
33
+ .create(scenario.resource, scenario.base.to_string(), "audit".to_string())
34
+ .expect("base");
35
+ let left = store
36
+ .commit(scenario.left.to_string(), &base, "audit".to_string())
37
+ .expect("left");
38
+ let right = store
39
+ .commit(scenario.right.to_string(), &base, "audit".to_string())
40
+ .expect("right");
41
+
42
+ let mut topology = StateTopology::new();
43
+ for revision in [&base, &left, &right] {
44
+ topology.add_revision(revision.clone());
45
+ }
46
+
47
+ let left_diff = SemanticDiff::compute(&scenario.base, &scenario.left);
48
+ let right_diff = SemanticDiff::compute(&scenario.base, &scenario.right);
49
+ let classification = ConflictClassification::classify(&base, &left, &right);
50
+
51
+ let plan = ReconciliationPlan::new(left.id.clone(), right.id.clone(), base.id.clone(), true);
52
+ let merged = reconcile(&base, &left, &right, &plan, "reconciler".to_string()).expect("merge");
53
+ assert_eq!(
54
+ merged.materialized_state.parent_id.as_ref(),
55
+ Some(&left.id),
56
+ "the selected parent is honored independent of representation"
57
+ );
58
+
59
+ Outcome {
60
+ relationship: topology.relationship(&left.id, &right.id),
61
+ has_lca: topology.common_ancestor(&left.id, &right.id).is_some(),
62
+ left_changes: left_diff.changes.len(),
63
+ right_changes: right_diff.changes.len(),
64
+ overall: classification.overall,
65
+ path_conflicts: classification.path_conflicts.len(),
66
+ }
67
+ }
68
+
69
+ #[test]
70
+ fn source_artifact_and_application_states_share_the_same_version_control_behavior() {
71
+ let source = run_scenario(Scenario {
72
+ resource: "source:src/main.rs",
73
+ base: json!({"body":"fn main() {}","digest":"h1"}),
74
+ left: json!({"body":"fn main() { println!(\"ok\"); }","digest":"h1"}),
75
+ right: json!({"body":"fn main() {}","digest":"h2"}),
76
+ });
77
+
78
+ let artifact = run_scenario(Scenario {
79
+ resource: "artifact:dist/app.wasm",
80
+ base: json!({"payload":"AA==","digest":"h1"}),
81
+ left: json!({"payload":"AQ==","digest":"h1"}),
82
+ right: json!({"payload":"AA==","digest":"h2"}),
83
+ });
84
+
85
+ let application = run_scenario(Scenario {
86
+ resource: "application:tasks/1",
87
+ base: json!({"state":"open","digest":"h1"}),
88
+ left: json!({"state":"closed","digest":"h1"}),
89
+ right: json!({"state":"open","digest":"h2"}),
90
+ });
91
+
92
+ let expected = Outcome {
93
+ relationship: Relationship::Diverged,
94
+ has_lca: true,
95
+ left_changes: 1,
96
+ right_changes: 1,
97
+ overall: ConflictClass::Independent,
98
+ path_conflicts: 2,
99
+ };
100
+
101
+ assert_eq!(source, expected);
102
+ assert_eq!(artifact, expected);
103
+ assert_eq!(application, expected);
104
+ }
105
+
106
+ #[test]
107
+ fn mutating_resource_identity_changes_ids_not_version_control_semantics() {
108
+ let source_namespace = run_scenario(Scenario {
109
+ resource: "source:module/alpha",
110
+ base: json!({"value":1,"tag":"stable"}),
111
+ left: json!({"value":2,"tag":"stable"}),
112
+ right: json!({"value":1,"tag":"candidate"}),
113
+ });
114
+
115
+ let artifact_namespace = run_scenario(Scenario {
116
+ resource: "artifact:module/alpha",
117
+ base: json!({"value":1,"tag":"stable"}),
118
+ left: json!({"value":2,"tag":"stable"}),
119
+ right: json!({"value":1,"tag":"candidate"}),
120
+ });
121
+
122
+ assert_eq!(source_namespace, artifact_namespace);
123
+ }
@@ -0,0 +1,281 @@
1
+ use feltdb::{
2
+ MaterializationError, MaterializationProjection, PathComponent, RefError, StateId, StateStore,
3
+ };
4
+ use feltdb::state_model::GarbageCollectionReport;
5
+ use serde_json::json;
6
+ use std::collections::BTreeMap;
7
+ use std::sync::Arc;
8
+ use tempfile::TempDir;
9
+
10
+ fn durable_store(name: &str) -> (TempDir, StateStore) {
11
+ let directory = TempDir::new().unwrap();
12
+ let db = Arc::new(feltdb::FeltDb::open(directory.path().join(name)).unwrap());
13
+ let store = StateStore::with_feltdb(db).unwrap();
14
+ (directory, store)
15
+ }
16
+
17
+ fn projection(paths: Vec<Vec<PathComponent>>) -> MaterializationProjection {
18
+ MaterializationProjection::select(paths)
19
+ }
20
+
21
+ #[test]
22
+ fn refs_cas_enforcement_rejects_stale_moves() {
23
+ let store = StateStore::new_volatile();
24
+ let a = store
25
+ .create("docs:1", json!({"n": 1}).to_string(), "auth".into())
26
+ .unwrap();
27
+ let b = store
28
+ .commit(json!({"n": 2}).to_string(), &a, "auth".into())
29
+ .unwrap();
30
+ store
31
+ .create_ref("refs", "main", a.id.clone(), "auth".into(), BTreeMap::new())
32
+ .unwrap();
33
+
34
+ store
35
+ .compare_and_swap_ref("refs", "main", 1, b.id.clone(), "auth".into(), None)
36
+ .unwrap();
37
+ let stale = store.compare_and_swap_ref("refs", "main", 1, a.id.clone(), "auth".into(), None);
38
+ assert!(matches!(
39
+ stale,
40
+ Err(RefError::VersionConflict {
41
+ expected: 1,
42
+ current: 2
43
+ })
44
+ ));
45
+ }
46
+
47
+ #[test]
48
+ fn refs_persist_target_and_versions_across_restart() {
49
+ let (directory, store) = durable_store("refs.log");
50
+ let a = store
51
+ .create("docs:1", json!({"n": 1}).to_string(), "auth".into())
52
+ .unwrap();
53
+ let b = store
54
+ .commit(json!({"n": 2}).to_string(), &a, "auth".into())
55
+ .unwrap();
56
+
57
+ store
58
+ .create_ref("refs", "main", a.id.clone(), "auth".into(), BTreeMap::new())
59
+ .unwrap();
60
+ let moved = store.move_ref("refs", "main", b.id.clone(), "auth2".into()).unwrap();
61
+ assert_eq!(moved.target, b.id);
62
+ assert_eq!(moved.version, 2);
63
+
64
+ let reopened = StateStore::with_feltdb(Arc::new(
65
+ feltdb::FeltDb::open(directory.path().join("refs.log")).unwrap(),
66
+ ))
67
+ .unwrap();
68
+ let persisted = reopened.get_ref("refs", "main").unwrap();
69
+ assert_eq!(persisted.target, moved.target);
70
+ assert_eq!(persisted.version, moved.version);
71
+ }
72
+
73
+ #[test]
74
+ fn refs_namespace_isolation_deletion_and_enumeration() {
75
+ let store = StateStore::new_volatile();
76
+ let revision = store
77
+ .create("docs:1", json!({"n": 1}).to_string(), "auth".into())
78
+ .unwrap();
79
+
80
+ store
81
+ .create_ref("refs", "main", revision.id.clone(), "auth".into(), BTreeMap::new())
82
+ .unwrap();
83
+ store
84
+ .create_ref("tags", "main", revision.id.clone(), "auth".into(), BTreeMap::new())
85
+ .unwrap();
86
+
87
+ let refs_all = store.list_refs(None);
88
+ assert_eq!(refs_all.len(), 2);
89
+ assert_eq!(store.list_refs(Some("refs")).len(), 1);
90
+ assert_eq!(store.list_refs(Some("tags")).len(), 1);
91
+
92
+ store.delete_ref("tags", "main", Some(1)).unwrap();
93
+ assert!(store.get_ref("tags", "main").is_none());
94
+ assert!(store.get_ref("refs", "main").is_some());
95
+ }
96
+
97
+ #[test]
98
+ fn materialization_projection_filters_revision_selection_and_nested_paths() {
99
+ let store = StateStore::new_volatile();
100
+ let base = store
101
+ .create(
102
+ "source:file",
103
+ json!({"tree":{"src":{"lib.rs":"v1","main.rs":"m1"}},"meta":{"lang":"rust"}})
104
+ .to_string(),
105
+ "auth".into(),
106
+ )
107
+ .unwrap();
108
+ let head = store
109
+ .commit(
110
+ json!({"tree":{"src":{"lib.rs":"v2","main.rs":"m1"}},"meta":{"lang":"rust"}})
111
+ .to_string(),
112
+ &base,
113
+ "auth".into(),
114
+ )
115
+ .unwrap();
116
+
117
+ let view = store
118
+ .materialize(
119
+ &head.id,
120
+ &projection(vec![
121
+ vec![PathComponent::Key("tree".into()), PathComponent::Key("src".into()), PathComponent::Key("lib.rs".into())],
122
+ ]),
123
+ )
124
+ .unwrap();
125
+ assert_eq!(view, json!({"tree":{"src":{"lib.rs":"v2"}}}));
126
+
127
+ let base_view = store
128
+ .materialize(
129
+ &base.id,
130
+ &projection(vec![
131
+ vec![PathComponent::Key("tree".into()), PathComponent::Key("src".into()), PathComponent::Key("lib.rs".into())],
132
+ ]),
133
+ )
134
+ .unwrap();
135
+ assert_eq!(base_view, json!({"tree":{"src":{"lib.rs":"v1"}}}));
136
+ }
137
+
138
+ #[test]
139
+ fn materialization_missing_paths_and_deterministic_output_are_enforced() {
140
+ let store = StateStore::new_volatile();
141
+ let revision = store
142
+ .create(
143
+ "artifact:item",
144
+ json!({"manifest":{"digest":"abc","size":7},"chunks":[{"id":1},{"id":2}]}).to_string(),
145
+ "auth".into(),
146
+ )
147
+ .unwrap();
148
+
149
+ let missing = store.materialize(
150
+ &revision.id,
151
+ &projection(vec![vec![
152
+ PathComponent::Key("manifest".into()),
153
+ PathComponent::Key("missing".into()),
154
+ ]]),
155
+ );
156
+ assert!(matches!(missing, Err(MaterializationError::MissingPath(_))));
157
+
158
+ let first = store
159
+ .materialize(
160
+ &revision.id,
161
+ &projection(vec![
162
+ vec![PathComponent::Key("manifest".into()), PathComponent::Key("size".into())],
163
+ vec![PathComponent::Key("chunks".into()), PathComponent::Index(1)],
164
+ ]),
165
+ )
166
+ .unwrap();
167
+ let second = store
168
+ .materialize(
169
+ &revision.id,
170
+ &projection(vec![
171
+ vec![PathComponent::Key("chunks".into()), PathComponent::Index(1)],
172
+ vec![PathComponent::Key("manifest".into()), PathComponent::Key("size".into())],
173
+ ]),
174
+ )
175
+ .unwrap();
176
+ assert_eq!(first, second);
177
+ }
178
+
179
+ fn setup_gc_graph(store: &StateStore) -> (StateId, StateId, StateId, StateId) {
180
+ let a = store
181
+ .create("graph:r", json!({"node":"A"}).to_string(), "auth".into())
182
+ .unwrap();
183
+ let b = store
184
+ .commit(json!({"node":"B"}).to_string(), &a, "auth".into())
185
+ .unwrap();
186
+ let c = store
187
+ .commit(json!({"node":"C"}).to_string(), &b, "auth".into())
188
+ .unwrap();
189
+ let d = store
190
+ .commit(json!({"node":"D"}).to_string(), &b, "auth".into())
191
+ .unwrap();
192
+ (a.id, b.id, c.id, d.id)
193
+ }
194
+
195
+ fn collect(store: &StateStore) -> GarbageCollectionReport {
196
+ store.collect_unreachable().unwrap()
197
+ }
198
+
199
+ #[test]
200
+ fn gc_root_discovery_and_unreachable_collection() {
201
+ let store = StateStore::new_volatile();
202
+ let (_a, _b, _c, _d) = setup_gc_graph(&store);
203
+ let report = collect(&store);
204
+ assert_eq!(report.roots.len(), 0);
205
+ assert_eq!(report.reachable_revisions.len(), 0);
206
+ assert_eq!(report.reachable_objects.len(), 0);
207
+ assert_eq!(report.collected_revisions.len(), 4);
208
+ assert_eq!(report.collected_objects.len(), 4);
209
+ }
210
+
211
+ #[test]
212
+ fn gc_parent_and_content_traversal_preserves_shared_history() {
213
+ let store = StateStore::new_volatile();
214
+ let (a, b, c, d) = setup_gc_graph(&store);
215
+ store
216
+ .create_ref("refs", "c", c.clone(), "auth".into(), BTreeMap::new())
217
+ .unwrap();
218
+ store
219
+ .create_ref("refs", "d", d.clone(), "auth".into(), BTreeMap::new())
220
+ .unwrap();
221
+
222
+ let keep_all = collect(&store);
223
+ assert!(keep_all.collected_revisions.is_empty());
224
+
225
+ store.delete_ref("refs", "d", Some(1)).unwrap();
226
+ let after_delete = collect(&store);
227
+ assert!(after_delete.collected_revisions.contains(&d));
228
+ assert!(!after_delete.collected_revisions.contains(&b));
229
+ assert!(!after_delete.collected_revisions.contains(&c));
230
+ assert!(!after_delete.collected_revisions.contains(&a));
231
+ assert_eq!(after_delete.collected_revisions.len(), 1);
232
+ assert_eq!(after_delete.collected_objects.len(), 1);
233
+ }
234
+
235
+ #[test]
236
+ fn substrate_is_domain_neutral_for_refs_materialization_and_gc() {
237
+ let store = StateStore::new_volatile();
238
+
239
+ let scenarios = vec![
240
+ (
241
+ "source:module",
242
+ json!({"tree":{"src":{"lib.rs":"v1","main.rs":"v1"}}}),
243
+ vec![PathComponent::Key("tree".into()), PathComponent::Key("src".into()), PathComponent::Key("lib.rs".into())],
244
+ ),
245
+ (
246
+ "artifact:bundle",
247
+ json!({"manifest":{"digest":"h1"},"payload":{"chunks":["a","b"]}}),
248
+ vec![PathComponent::Key("manifest".into()), PathComponent::Key("digest".into())],
249
+ ),
250
+ (
251
+ "application:state",
252
+ json!({"entities":{"tasks":{"1":{"status":"open"}}},"meta":{"v":1}}),
253
+ vec![PathComponent::Key("entities".into()), PathComponent::Key("tasks".into()), PathComponent::Key("1".into()), PathComponent::Key("status".into())],
254
+ ),
255
+ ];
256
+
257
+ for (resource, content, path) in scenarios {
258
+ let rev = store
259
+ .create(resource, content.to_string(), "auth".into())
260
+ .unwrap();
261
+ store
262
+ .create_ref("heads", resource, rev.id.clone(), "auth".into(), BTreeMap::new())
263
+ .unwrap();
264
+ let view = store
265
+ .materialize(&rev.id, &projection(vec![path]))
266
+ .unwrap();
267
+ assert!(view.is_object());
268
+ }
269
+
270
+ let keep = collect(&store);
271
+ assert_eq!(keep.collected_revisions.len(), 0);
272
+
273
+ for reference in store.list_refs(Some("heads")) {
274
+ store
275
+ .delete_ref("heads", &reference.name, Some(reference.version))
276
+ .unwrap();
277
+ }
278
+ let collected = collect(&store);
279
+ assert_eq!(collected.collected_revisions.len(), 3);
280
+ assert_eq!(collected.collected_objects.len(), 3);
281
+ }
@@ -0,0 +1,69 @@
1
+ use std::process::Command;
2
+
3
+ use feltdb::{AtomicMutation, FeltDb};
4
+ use serde_json::json;
5
+ use tempfile::TempDir;
6
+
7
+ const CHILD_ENV: &str = "FELTDB_OWNERSHIP_CHILD";
8
+ const PATH_ENV: &str = "FELTDB_OWNERSHIP_PATH";
9
+ const EXPECT_LOCKED_ENV: &str = "FELTDB_EXPECT_LOCKED";
10
+
11
+ #[test]
12
+ fn ownership_child() {
13
+ if std::env::var_os(CHILD_ENV).is_none() {
14
+ return;
15
+ }
16
+ let path = std::env::var(PATH_ENV).unwrap();
17
+ let expect_locked = std::env::var_os(EXPECT_LOCKED_ENV).is_some();
18
+ match FeltDb::open(path) {
19
+ Ok(database) => {
20
+ assert!(!expect_locked, "a second owner opened the database");
21
+ let mutation = AtomicMutation {
22
+ capability: "probe".into(),
23
+ key: "probe:child".into(),
24
+ value: Some(json!({ "owner": "child" })),
25
+ };
26
+ database
27
+ .apply_atomic_transaction("child", None, &[], &[mutation], None)
28
+ .unwrap();
29
+ }
30
+ Err(error) => {
31
+ assert!(expect_locked, "an unowned database failed to open: {error}");
32
+ assert!(error.to_string().contains("already owned"));
33
+ }
34
+ }
35
+ }
36
+
37
+ fn child(path: &std::path::Path, expect_locked: bool) -> std::process::ExitStatus {
38
+ let mut command = Command::new(std::env::current_exe().unwrap());
39
+ command
40
+ .arg("--exact")
41
+ .arg("ownership_child")
42
+ .arg("--nocapture")
43
+ .env(CHILD_ENV, "1")
44
+ .env(PATH_ENV, path);
45
+ if expect_locked {
46
+ command.env(EXPECT_LOCKED_ENV, "1");
47
+ }
48
+ command.status().unwrap()
49
+ }
50
+
51
+ #[test]
52
+ fn another_process_cannot_read_write_or_transact_while_owned() {
53
+ let directory = TempDir::new().unwrap();
54
+ let path = directory.path().join("shared.db");
55
+ let _owner = FeltDb::open(&path).unwrap();
56
+ assert!(child(&path, true).success());
57
+ }
58
+
59
+ #[test]
60
+ fn ownership_is_released_when_the_process_exits() {
61
+ let directory = TempDir::new().unwrap();
62
+ let path = directory.path().join("shared.db");
63
+ assert!(child(&path, false).success());
64
+ let database = FeltDb::open(path).unwrap();
65
+ assert_eq!(
66
+ database.get::<serde_json::Value>("probe:child").unwrap(),
67
+ Some(json!({ "owner": "child" }))
68
+ );
69
+ }
@@ -1018,9 +1018,21 @@ fn retention_cost_grows_with_the_window_and_inflates_the_log() {
1018
1018
  (start.elapsed(), std::fs::metadata(&path).unwrap().len())
1019
1019
  }
1020
1020
 
1021
- let (none_time, none_bytes) = run(None);
1022
- let (small_time, small_bytes) = run(Some(5));
1023
- let (large_time, _) = run(Some(50));
1021
+ // Compare the best of repeated independent runs. The workspace executes
1022
+ // other crash/soak processes concurrently, so a single wall-clock sample
1023
+ // can measure scheduler delay rather than retention work. Taking the
1024
+ // minimum preserves the envelope claim while filtering that one-sided
1025
+ // noise; every sample still performs the complete durable workload.
1026
+ fn sample(run_once: impl Fn() -> (std::time::Duration, u64)) -> (std::time::Duration, u64) {
1027
+ (0..3)
1028
+ .map(|_| run_once())
1029
+ .min_by_key(|(elapsed, _)| *elapsed)
1030
+ .unwrap()
1031
+ }
1032
+
1033
+ let (none_time, none_bytes) = sample(|| run(None));
1034
+ let (small_time, small_bytes) = sample(|| run(Some(5)));
1035
+ let (large_time, _) = sample(|| run(Some(50)));
1024
1036
 
1025
1037
  assert!(
1026
1038
  large_time > small_time,
@@ -74,7 +74,17 @@ fn compacted_log_after_revisions(count: usize, pad: usize) -> String {
74
74
 
75
75
  fn revision_lines(log: &str) -> usize {
76
76
  log.lines()
77
- .filter(|line| line.contains("state:revision:"))
77
+ .filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
78
+ .filter(|record| {
79
+ record
80
+ .get("capability")
81
+ .and_then(serde_json::Value::as_str)
82
+ == Some("state")
83
+ && record
84
+ .get("key")
85
+ .and_then(serde_json::Value::as_str)
86
+ .is_some_and(|key| key.starts_with("state:revision:"))
87
+ })
78
88
  .count()
79
89
  }
80
90
 
@@ -188,13 +198,12 @@ fn each_compaction_rewrites_the_whole_revision_history() {
188
198
  // Cost: what a revision is worth in bytes
189
199
  // ---------------------------------------------------------------------------
190
200
 
191
- /// **A revision costs a fixed overhead of roughly half a kilobyte, plus one
192
- /// copy of its own state.**
201
+ /// **A revision and its canonical transaction attribution have linear cost.**
193
202
  ///
194
203
  /// The compacted log is linear in both the revision count and the state size:
195
204
  ///
196
205
  /// ```text
197
- /// bytes = header + count × (fixed_overhead + |content|)
206
+ /// bytes ≈ header + count × (fixed_overhead + 2 × |content|)
198
207
  /// ```
199
208
  ///
200
209
  /// The fixed part is not incidental — it is a 64-character hex identity, a
@@ -203,43 +212,59 @@ fn each_compaction_rewrites_the_whole_revision_history() {
203
212
  /// key and the fully-qualified Rust type name. It dominates until the state
204
213
  /// being versioned is itself several hundred bytes.
205
214
  ///
206
- /// The state is stored exactly once per revision, not twice. But it is stored
207
- /// *escaped*, inside a JSON string field of the record — so every quote in the
208
- /// versioned document costs a second byte.
215
+ /// The state is stored once in the revision and once in the canonical
216
+ /// transaction attribution retained for replay and provenance. The latter is
217
+ /// history, rather than a duplicate authority, and survives compaction by
218
+ /// design.
209
219
  #[test]
210
220
  fn a_revision_costs_a_fixed_overhead_plus_one_copy_of_its_state() {
211
- let small_ten = compacted_log_after_revisions(10, 0).len();
221
+ let small_ten_log = compacted_log_after_revisions(10, 0);
222
+ let small_ten = small_ten_log.len();
212
223
  let small_twenty = compacted_log_after_revisions(20, 0).len();
213
- let padded_hundred = compacted_log_after_revisions(10, 100).len();
214
- let padded_thousand = compacted_log_after_revisions(10, 1000).len();
224
+ let padded_hundred_log = compacted_log_after_revisions(10, 100);
225
+ let padded_thousand_log = compacted_log_after_revisions(10, 1000);
215
226
 
216
227
  // Ten more revisions of a 16-byte state cost ten more per-revision records.
217
228
  // Indices 10..19 are one digit wider than 0..9, hence the ten-byte term.
218
229
  let ten_more = small_twenty - small_ten - 10;
219
230
  let per_revision = ten_more / 10;
220
231
  // The shape is unchanged by the resource-scoped model; the constant is
221
- // not. A revision now also carries its resource, a second identity and a
222
- // sequence, so the floor is higher than the 492 bytes measured before the
223
- // model was adopted. Re-measuring the economics properly is a separate
224
- // step; this assertion exists to catch the shape changing, not to be the
225
- // measurement.
232
+ // not. A revision now carries its resource, identity and sequence, while
233
+ // the snapshot header also retains the canonical transaction attribution
234
+ // required for replay and provenance. This assertion catches a change in
235
+ // the combined linear shape rather than pretending that history is free.
226
236
  assert!(
227
- (550..=750).contains(&per_revision),
237
+ (1_800..=2_300).contains(&per_revision),
228
238
  "a revision of a 16-byte state costs {per_revision} bytes; \
229
239
  the overhead is the record, not the state"
230
240
  );
231
241
 
232
- // Content is stored exactly once: growing each state by N bytes grows the
233
- // log by exactly count × N.
242
+ // Content is represented in both the revision and its canonical
243
+ // transaction attribution. Count those copies directly instead of
244
+ // requiring unrelated encoded metadata to have byte-for-byte identical
245
+ // size across platforms and runs.
234
246
  assert_eq!(
235
- padded_hundred - small_ten,
236
- 10 * 100,
237
- "a hundred more bytes of state costs a hundred more bytes per revision"
247
+ padded_hundred_log.matches("x".repeat(100).as_str()).count(),
248
+ 10 * 2,
249
+ "each state appears in its revision and canonical transaction attribution"
238
250
  );
239
251
  assert_eq!(
240
- padded_thousand - small_ten,
241
- 10 * 1000,
242
- "the relationship stays exactly linear at a kilobyte of state"
252
+ padded_thousand_log
253
+ .matches("x".repeat(1000).as_str())
254
+ .count(),
255
+ 10 * 2,
256
+ "the two-copy relationship remains direct at a kilobyte of state"
257
+ );
258
+
259
+ let hundred_growth = padded_hundred_log.len() - small_ten;
260
+ let thousand_growth = padded_thousand_log.len() - small_ten;
261
+ assert!(
262
+ hundred_growth.abs_diff(10 * 2 * 100) <= 100,
263
+ "the measured 100-byte content term was {hundred_growth} bytes"
264
+ );
265
+ assert!(
266
+ thousand_growth.abs_diff(10 * 2 * 1000) <= 1_000,
267
+ "the measured 1,000-byte content term was {thousand_growth} bytes"
243
268
  );
244
269
  }
245
270