@feltdb/core 0.8.4 → 0.8.6
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.
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +165 -0
- package/dist/create/server-source/Cargo.toml +9 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
- package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
- package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
- package/dist/create/server-source/crates/feltdb/src/lib.rs +1648 -46
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
- package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
- package/dist/create/server-source/crates/feltdb/src/operation.rs +39 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
- package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
- package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1565 -536
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +9 -3
- package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
- package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
- package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +132 -0
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +272 -0
- package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +467 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +309 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +445 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +518 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
- package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +278 -0
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1365 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
- package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +417 -0
- package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
- package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
- package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
- package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
- package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
- package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +442 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +16 -1
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +695 -47
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +21 -0
- package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-C1VhI-U5.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C8HXbAXb.wasm +0 -0
- package/dist/studio-app/assets/{index-DwgNAIIX.js → index-Bbos1m2U.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workload.d.ts +2 -1
- package/dist/workload.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
//! The revision model decision experiment.
|
|
2
|
+
//!
|
|
3
|
+
//! This file **implements nothing in the product**. It builds three tiny probes
|
|
4
|
+
//! of what a `StateRevision` could *be*, runs the same seven scenarios against
|
|
5
|
+
//! each, and fills in a decision matrix by execution rather than by preference.
|
|
6
|
+
//!
|
|
7
|
+
//! It is the executable half of `docs/architecture/revision-model-decision.md`.
|
|
8
|
+
//!
|
|
9
|
+
//! # The question
|
|
10
|
+
//!
|
|
11
|
+
//! The [identity and ancestry contract](../../../docs/architecture/revision-identity-and-ancestry.md)
|
|
12
|
+
//! established that content-only identity cannot survive automatic minting, and
|
|
13
|
+
//! that `base` as "the previous state of resource X" is unanswerable because a
|
|
14
|
+
//! `StateRevision` has no resource. So the decision is not which fix to apply.
|
|
15
|
+
//! It is:
|
|
16
|
+
//!
|
|
17
|
+
//! > **What is a `StateRevision` a revision *of*?**
|
|
18
|
+
//!
|
|
19
|
+
//! Three answers are consistent with the evidence:
|
|
20
|
+
//!
|
|
21
|
+
//! | model | a revision is | identity |
|
|
22
|
+
//! | --- | --- | --- |
|
|
23
|
+
//! | **A — State** | a durable content | content alone; ancestry lives outside the revision |
|
|
24
|
+
//! | **B — Historical transition** | a particular occurrence in history | content + parent + a discriminator |
|
|
25
|
+
//! | **C — Resource transition** | a transition belonging to a resource | resource + content + parent + a discriminator |
|
|
26
|
+
//!
|
|
27
|
+
//! # Two invariants, asserted against every model
|
|
28
|
+
//!
|
|
29
|
+
//! These are not model choices. They are the boundary the T5 and T6 findings
|
|
30
|
+
//! draw, and a model that cannot hold them is not a revision model:
|
|
31
|
+
//!
|
|
32
|
+
//! 1. **Once committed, a revision's identity and ancestry are immutable.** A
|
|
33
|
+
//! record whose historical parent can change after commit is not a durable
|
|
34
|
+
//! historical object; it is a mutable record that happens to contain
|
|
35
|
+
//! historical fields.
|
|
36
|
+
//! 2. **A `parent_id` is either a reference to an authoritative revision, or it
|
|
37
|
+
//! is not a parent.** Parent existence is part of what makes a revision
|
|
38
|
+
//! valid, not a lookup applied afterwards.
|
|
39
|
+
//!
|
|
40
|
+
//! The probes are deliberately small and their identities are readable strings
|
|
41
|
+
//! rather than hashes: what is under test is the *structure* of identity, not
|
|
42
|
+
//! its encoding.
|
|
43
|
+
//!
|
|
44
|
+
//! Storage cost, compaction economics and copy-on-write are **deliberately
|
|
45
|
+
//! absent**. Measuring them before the object is defined would optimise
|
|
46
|
+
//! something that does not yet exist.
|
|
47
|
+
|
|
48
|
+
use feltdb::state_model::{StateRevision, StateStore};
|
|
49
|
+
use feltdb::FeltDb;
|
|
50
|
+
use std::collections::BTreeMap;
|
|
51
|
+
use std::sync::Arc;
|
|
52
|
+
use tempfile::TempDir;
|
|
53
|
+
|
|
54
|
+
type Rid = String;
|
|
55
|
+
|
|
56
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
57
|
+
enum MintError {
|
|
58
|
+
/// Invariant 2: the named parent is not an authoritative revision.
|
|
59
|
+
UnknownParent,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// The model has no way to express the question, as opposed to answering "no".
|
|
63
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
64
|
+
struct Unsupported(&'static str);
|
|
65
|
+
|
|
66
|
+
/// What every candidate model must be able to do.
|
|
67
|
+
trait Model {
|
|
68
|
+
fn name(&self) -> &'static str;
|
|
69
|
+
|
|
70
|
+
/// Mint a revision of `content` for `resource`, descending from `parent`.
|
|
71
|
+
fn mint(
|
|
72
|
+
&mut self,
|
|
73
|
+
resource: &str,
|
|
74
|
+
content: &str,
|
|
75
|
+
parent: Option<&Rid>,
|
|
76
|
+
) -> Result<Rid, MintError>;
|
|
77
|
+
|
|
78
|
+
/// The parent of `id` as seen from `resource`.
|
|
79
|
+
fn parent_of(&self, resource: &str, id: &Rid) -> Option<Rid>;
|
|
80
|
+
|
|
81
|
+
/// The revisions of one resource, oldest first.
|
|
82
|
+
fn history_of(&self, resource: &str) -> Result<Vec<Rid>, Unsupported>;
|
|
83
|
+
|
|
84
|
+
/// Retain only the newest `keep` revisions of one resource.
|
|
85
|
+
fn retain_last(&mut self, resource: &str, keep: usize) -> Result<usize, Unsupported>;
|
|
86
|
+
|
|
87
|
+
/// Every durable record, for immutability checking.
|
|
88
|
+
fn records(&self) -> BTreeMap<Rid, String>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Model A — a revision is a durable state
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
/// Identity is content. Ancestry is **not a field of the revision** — it is a
|
|
96
|
+
/// separate edge set, so the same state recurring in history is one object
|
|
97
|
+
/// referenced twice rather than one object rewritten twice.
|
|
98
|
+
#[derive(Default)]
|
|
99
|
+
struct StateModel {
|
|
100
|
+
contents: BTreeMap<Rid, String>,
|
|
101
|
+
edges: Vec<Edge>,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
struct Edge {
|
|
105
|
+
resource: String,
|
|
106
|
+
from: Option<Rid>,
|
|
107
|
+
to: Rid,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl Model for StateModel {
|
|
111
|
+
fn name(&self) -> &'static str {
|
|
112
|
+
"A state"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn mint(
|
|
116
|
+
&mut self,
|
|
117
|
+
resource: &str,
|
|
118
|
+
content: &str,
|
|
119
|
+
parent: Option<&Rid>,
|
|
120
|
+
) -> Result<Rid, MintError> {
|
|
121
|
+
if let Some(parent) = parent {
|
|
122
|
+
if !self.contents.contains_key(parent) {
|
|
123
|
+
return Err(MintError::UnknownParent);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let id = format!("state:{content}");
|
|
127
|
+
// Idempotent by construction: the same content is the same bytes.
|
|
128
|
+
self.contents.insert(id.clone(), content.to_string());
|
|
129
|
+
self.edges.push(Edge {
|
|
130
|
+
resource: resource.to_string(),
|
|
131
|
+
from: parent.cloned(),
|
|
132
|
+
to: id.clone(),
|
|
133
|
+
});
|
|
134
|
+
Ok(id)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn parent_of(&self, resource: &str, id: &Rid) -> Option<Rid> {
|
|
138
|
+
// The last time this state was reached on this resource.
|
|
139
|
+
self.edges
|
|
140
|
+
.iter()
|
|
141
|
+
.rev()
|
|
142
|
+
.find(|edge| edge.resource == resource && &edge.to == id)
|
|
143
|
+
.and_then(|edge| edge.from.clone())
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn history_of(&self, resource: &str) -> Result<Vec<Rid>, Unsupported> {
|
|
147
|
+
Ok(self
|
|
148
|
+
.edges
|
|
149
|
+
.iter()
|
|
150
|
+
.filter(|edge| edge.resource == resource)
|
|
151
|
+
.map(|edge| edge.to.clone())
|
|
152
|
+
.collect())
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn retain_last(&mut self, resource: &str, keep: usize) -> Result<usize, Unsupported> {
|
|
156
|
+
let mine: Vec<usize> = self
|
|
157
|
+
.edges
|
|
158
|
+
.iter()
|
|
159
|
+
.enumerate()
|
|
160
|
+
.filter(|(_, edge)| edge.resource == resource)
|
|
161
|
+
.map(|(index, _)| index)
|
|
162
|
+
.collect();
|
|
163
|
+
let drop_count = mine.len().saturating_sub(keep);
|
|
164
|
+
let dropped: Vec<usize> = mine.into_iter().take(drop_count).collect();
|
|
165
|
+
self.edges = std::mem::take(&mut self.edges)
|
|
166
|
+
.into_iter()
|
|
167
|
+
.enumerate()
|
|
168
|
+
.filter(|(index, _)| !dropped.contains(index))
|
|
169
|
+
.map(|(_, edge)| edge)
|
|
170
|
+
.collect();
|
|
171
|
+
// Contents stay: they are shared, and another resource may reference them.
|
|
172
|
+
Ok(drop_count)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn records(&self) -> BTreeMap<Rid, String> {
|
|
176
|
+
self.contents.clone()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Model B — a revision is a historical transition
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/// Identity is content plus parent plus a monotonic discriminator, so the same
|
|
185
|
+
/// content occurring twice is two revisions. There is **no resource**: a
|
|
186
|
+
/// revision is an occurrence in history, and history is not divided up.
|
|
187
|
+
#[derive(Default)]
|
|
188
|
+
struct TransitionModel {
|
|
189
|
+
revisions: BTreeMap<Rid, (String, Option<Rid>)>,
|
|
190
|
+
sequence: u64,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
impl Model for TransitionModel {
|
|
194
|
+
fn name(&self) -> &'static str {
|
|
195
|
+
"B transition"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn mint(
|
|
199
|
+
&mut self,
|
|
200
|
+
_resource: &str,
|
|
201
|
+
content: &str,
|
|
202
|
+
parent: Option<&Rid>,
|
|
203
|
+
) -> Result<Rid, MintError> {
|
|
204
|
+
if let Some(parent) = parent {
|
|
205
|
+
if !self.revisions.contains_key(parent) {
|
|
206
|
+
return Err(MintError::UnknownParent);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
self.sequence += 1;
|
|
210
|
+
let id = format!(
|
|
211
|
+
"occurrence:{}:{}:{content}",
|
|
212
|
+
self.sequence,
|
|
213
|
+
parent.map(String::as_str).unwrap_or("root")
|
|
214
|
+
);
|
|
215
|
+
self.revisions
|
|
216
|
+
.insert(id.clone(), (content.to_string(), parent.cloned()));
|
|
217
|
+
Ok(id)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn parent_of(&self, _resource: &str, id: &Rid) -> Option<Rid> {
|
|
221
|
+
self.revisions
|
|
222
|
+
.get(id)
|
|
223
|
+
.and_then(|(_, parent)| parent.clone())
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn history_of(&self, _resource: &str) -> Result<Vec<Rid>, Unsupported> {
|
|
227
|
+
Err(Unsupported(
|
|
228
|
+
"a transition belongs to no resource, so a resource has no history",
|
|
229
|
+
))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fn retain_last(&mut self, _resource: &str, _keep: usize) -> Result<usize, Unsupported> {
|
|
233
|
+
Err(Unsupported(
|
|
234
|
+
"retention cannot be scoped to a resource the model cannot name",
|
|
235
|
+
))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fn records(&self) -> BTreeMap<Rid, String> {
|
|
239
|
+
self.revisions
|
|
240
|
+
.iter()
|
|
241
|
+
.map(|(id, (content, parent))| (id.clone(), format!("{content}|{parent:?}")))
|
|
242
|
+
.collect()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Model C — a revision is a transition of a resource
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
/// Identity is resource plus content plus parent plus a discriminator. A
|
|
251
|
+
/// revision knows what it is a revision *of*.
|
|
252
|
+
#[derive(Default)]
|
|
253
|
+
struct ResourceTransitionModel {
|
|
254
|
+
revisions: BTreeMap<Rid, (String, String, Option<Rid>)>,
|
|
255
|
+
sequence: u64,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
impl Model for ResourceTransitionModel {
|
|
259
|
+
fn name(&self) -> &'static str {
|
|
260
|
+
"C resource transition"
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn mint(
|
|
264
|
+
&mut self,
|
|
265
|
+
resource: &str,
|
|
266
|
+
content: &str,
|
|
267
|
+
parent: Option<&Rid>,
|
|
268
|
+
) -> Result<Rid, MintError> {
|
|
269
|
+
if let Some(parent) = parent {
|
|
270
|
+
if !self.revisions.contains_key(parent) {
|
|
271
|
+
return Err(MintError::UnknownParent);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
self.sequence += 1;
|
|
275
|
+
let id = format!(
|
|
276
|
+
"{resource}@{}:{}:{content}",
|
|
277
|
+
self.sequence,
|
|
278
|
+
parent.map(String::as_str).unwrap_or("root")
|
|
279
|
+
);
|
|
280
|
+
self.revisions.insert(
|
|
281
|
+
id.clone(),
|
|
282
|
+
(resource.to_string(), content.to_string(), parent.cloned()),
|
|
283
|
+
);
|
|
284
|
+
Ok(id)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn parent_of(&self, _resource: &str, id: &Rid) -> Option<Rid> {
|
|
288
|
+
self.revisions
|
|
289
|
+
.get(id)
|
|
290
|
+
.and_then(|(_, _, parent)| parent.clone())
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
fn history_of(&self, resource: &str) -> Result<Vec<Rid>, Unsupported> {
|
|
294
|
+
Ok(self
|
|
295
|
+
.revisions
|
|
296
|
+
.iter()
|
|
297
|
+
.filter(|(_, (owner, _, _))| owner == resource)
|
|
298
|
+
.map(|(id, _)| id.clone())
|
|
299
|
+
.collect())
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fn retain_last(&mut self, resource: &str, keep: usize) -> Result<usize, Unsupported> {
|
|
303
|
+
let mine = self.history_of(resource)?;
|
|
304
|
+
let drop_count = mine.len().saturating_sub(keep);
|
|
305
|
+
for id in mine.into_iter().take(drop_count) {
|
|
306
|
+
self.revisions.remove(&id);
|
|
307
|
+
}
|
|
308
|
+
Ok(drop_count)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
fn records(&self) -> BTreeMap<Rid, String> {
|
|
312
|
+
self.revisions
|
|
313
|
+
.iter()
|
|
314
|
+
.map(|(id, (owner, content, parent))| {
|
|
315
|
+
(id.clone(), format!("{owner}|{content}|{parent:?}"))
|
|
316
|
+
})
|
|
317
|
+
.collect()
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fn models() -> Vec<Box<dyn Model>> {
|
|
322
|
+
vec![
|
|
323
|
+
Box::<StateModel>::default(),
|
|
324
|
+
Box::<TransitionModel>::default(),
|
|
325
|
+
Box::<ResourceTransitionModel>::default(),
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const X: &str = r#"{"title":"X"}"#;
|
|
330
|
+
const Y: &str = r#"{"title":"Y"}"#;
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// The two invariants, asserted against every model
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
/// **Invariant 1 — a committed revision's identity and ancestry are immutable.**
|
|
337
|
+
///
|
|
338
|
+
/// Run every scenario that rewrote history under today's implementation, and
|
|
339
|
+
/// assert that no already-stored record changed. All three models hold, by
|
|
340
|
+
/// different means: A because a revision has no ancestry to rewrite, B and C
|
|
341
|
+
/// because the rewrite would have a different identity.
|
|
342
|
+
#[test]
|
|
343
|
+
fn every_model_holds_committed_revisions_immutable() {
|
|
344
|
+
for mut model in models() {
|
|
345
|
+
let name = model.name();
|
|
346
|
+
let first = model.mint("tasks:1", X, None).unwrap();
|
|
347
|
+
let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
|
|
348
|
+
let before = model.records();
|
|
349
|
+
|
|
350
|
+
// The three sequences that corrupt history today.
|
|
351
|
+
model.mint("tasks:1", X, Some(&second)).unwrap(); // revert
|
|
352
|
+
model.mint("tasks:1", Y, None).unwrap(); // re-mint a child as a root
|
|
353
|
+
model.mint("tasks:2", X, None).unwrap(); // same content, other resource
|
|
354
|
+
|
|
355
|
+
let after = model.records();
|
|
356
|
+
for (id, record) in &before {
|
|
357
|
+
assert_eq!(
|
|
358
|
+
after.get(id),
|
|
359
|
+
Some(record),
|
|
360
|
+
"{name}: the record for {id} changed after commit"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// **Invariant 2 — a `parent_id` references an authoritative revision, or it is
|
|
367
|
+
/// not a parent.**
|
|
368
|
+
///
|
|
369
|
+
/// Model-independent: all three can enforce it at the mint, and it costs a
|
|
370
|
+
/// containment check. This is the clause T5 should be discharged by — a rule in
|
|
371
|
+
/// the definition of a valid revision, not a lookup bolted on afterwards.
|
|
372
|
+
#[test]
|
|
373
|
+
fn every_model_rejects_a_parent_it_does_not_hold() {
|
|
374
|
+
for mut model in models() {
|
|
375
|
+
let name = model.name();
|
|
376
|
+
let foreign = "state:{\"never\":\"stored\"}".to_string();
|
|
377
|
+
assert_eq!(
|
|
378
|
+
model.mint("tasks:1", X, Some(&foreign)),
|
|
379
|
+
Err(MintError::UnknownParent),
|
|
380
|
+
"{name}: accepted a parent it does not hold"
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/// **The real store now holds both — this is what the experiment decided.**
|
|
386
|
+
///
|
|
387
|
+
/// This test asserted the opposite when the experiment was run: `StateStore`
|
|
388
|
+
/// accepted an unheld parent, and re-minting a child's content as a root erased
|
|
389
|
+
/// the parent that child had. Model C was adopted and implemented, and the same
|
|
390
|
+
/// two sequences are now refused at the mint.
|
|
391
|
+
///
|
|
392
|
+
/// It is kept in the experiment file rather than moved because the experiment's
|
|
393
|
+
/// claim was that both invariants are *model-independent and cheap*. This is
|
|
394
|
+
/// that claim, checked against the real store instead of a probe.
|
|
395
|
+
#[test]
|
|
396
|
+
fn the_real_store_now_holds_both_invariants() {
|
|
397
|
+
let directory = TempDir::new().unwrap();
|
|
398
|
+
let db = Arc::new(FeltDb::open(directory.path().join("today.log")).unwrap());
|
|
399
|
+
let store = StateStore::with_feltdb(db).unwrap();
|
|
400
|
+
|
|
401
|
+
let first = store
|
|
402
|
+
.create("tasks:1", X.into(), "decision".into())
|
|
403
|
+
.unwrap();
|
|
404
|
+
let second = store.commit(Y.into(), &first, "decision".into()).unwrap();
|
|
405
|
+
assert_eq!(
|
|
406
|
+
store.get(&second.id).unwrap().parent_id,
|
|
407
|
+
Some(first.id.clone())
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// Invariant 1: beginning the resource again is refused, and a further
|
|
411
|
+
// commit of the same content is a new revision rather than a rewrite.
|
|
412
|
+
assert!(
|
|
413
|
+
store
|
|
414
|
+
.create("tasks:1", Y.into(), "decision".into())
|
|
415
|
+
.is_err(),
|
|
416
|
+
"a resource has one beginning"
|
|
417
|
+
);
|
|
418
|
+
let third = store.commit(Y.into(), &second, "decision".into()).unwrap();
|
|
419
|
+
assert_ne!(
|
|
420
|
+
third.id, second.id,
|
|
421
|
+
"the same state again is a new revision"
|
|
422
|
+
);
|
|
423
|
+
assert_eq!(
|
|
424
|
+
store.get(&second.id).unwrap().parent_id,
|
|
425
|
+
Some(first.id),
|
|
426
|
+
"the committed revision kept the parent it was committed with"
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
// Invariant 2: a parent the store does not hold is refused.
|
|
430
|
+
let unheld = StateRevision::at(
|
|
431
|
+
"tasks:1".into(),
|
|
432
|
+
r#"{"never":"stored"}"#.into(),
|
|
433
|
+
None,
|
|
434
|
+
0,
|
|
435
|
+
"decision".into(),
|
|
436
|
+
);
|
|
437
|
+
assert!(
|
|
438
|
+
store.commit(X.into(), &unheld, "decision".into()).is_err(),
|
|
439
|
+
"a parent must resolve to a held revision"
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
// The seven scenarios
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
/// Whether the two mints produce one identity or two.
|
|
448
|
+
fn same_content_twice(model: &mut dyn Model) -> bool {
|
|
449
|
+
let first = model.mint("tasks:1", X, None).unwrap();
|
|
450
|
+
let second = model.mint("tasks:1", X, None).unwrap();
|
|
451
|
+
first == second
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/// Whether a revert lands back on the identity it started from.
|
|
455
|
+
fn revert_reuses_the_original_identity(model: &mut dyn Model) -> bool {
|
|
456
|
+
let first = model.mint("tasks:1", X, None).unwrap();
|
|
457
|
+
let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
|
|
458
|
+
let third = model.mint("tasks:1", X, Some(&second)).unwrap();
|
|
459
|
+
first == third
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/// Whether two resources holding the same content share one identity.
|
|
463
|
+
fn two_resources_share_an_identity(model: &mut dyn Model) -> bool {
|
|
464
|
+
let one = model.mint("tasks:1", X, None).unwrap();
|
|
465
|
+
let two = model.mint("tasks:2", X, None).unwrap();
|
|
466
|
+
one == two
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/// Whether re-minting a child's content as a root can disturb the child.
|
|
470
|
+
fn re_minting_a_child_as_a_root_disturbs_it(model: &mut dyn Model) -> bool {
|
|
471
|
+
let first = model.mint("tasks:1", X, None).unwrap();
|
|
472
|
+
let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
|
|
473
|
+
let before = model.parent_of("tasks:1", &second);
|
|
474
|
+
model.mint("tasks:1", Y, None).unwrap();
|
|
475
|
+
model.parent_of("tasks:1", &second) != before
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/// Whether a foreign parent is refused.
|
|
479
|
+
fn foreign_parent_refused(model: &mut dyn Model) -> bool {
|
|
480
|
+
model.mint("tasks:1", X, Some(&"nothing:here".to_string())) == Err(MintError::UnknownParent)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/// Whether the history of one resource can be listed.
|
|
484
|
+
fn resource_history_addressable(model: &mut dyn Model) -> bool {
|
|
485
|
+
model.mint("tasks:1", X, None).unwrap();
|
|
486
|
+
model.mint("tasks:2", Y, None).unwrap();
|
|
487
|
+
matches!(model.history_of("tasks:1"), Ok(history) if history.len() == 1)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/// Whether a retention policy can be *stated* — "keep the last k of this
|
|
491
|
+
/// resource" — as opposed to being applied to everything at once.
|
|
492
|
+
fn retention_expressible(model: &mut dyn Model) -> bool {
|
|
493
|
+
let mut previous = model.mint("tasks:1", X, None).unwrap();
|
|
494
|
+
for n in 0..4 {
|
|
495
|
+
previous = model
|
|
496
|
+
.mint("tasks:1", &format!(r#"{{"n":{n}}}"#), Some(&previous))
|
|
497
|
+
.unwrap();
|
|
498
|
+
}
|
|
499
|
+
model.retain_last("tasks:1", 2).is_ok()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/// Whether following parents from the newest revision terminates.
|
|
503
|
+
fn topology_terminates(model: &mut dyn Model) -> bool {
|
|
504
|
+
let first = model.mint("tasks:1", X, None).unwrap();
|
|
505
|
+
let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
|
|
506
|
+
let third = model.mint("tasks:1", X, Some(&second)).unwrap();
|
|
507
|
+
|
|
508
|
+
let mut seen: Vec<Rid> = Vec::new();
|
|
509
|
+
let mut current = third;
|
|
510
|
+
for _ in 0..32 {
|
|
511
|
+
if seen.contains(¤t) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
seen.push(current.clone());
|
|
515
|
+
match model.parent_of("tasks:1", ¤t) {
|
|
516
|
+
Some(parent) => current = parent,
|
|
517
|
+
None => return true,
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
false
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// The decision matrix
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
/// One row of the matrix: a requirement, and what each model does about it.
|
|
528
|
+
struct Row {
|
|
529
|
+
requirement: &'static str,
|
|
530
|
+
/// Evaluated per model, in the order A, B, C.
|
|
531
|
+
evaluate: fn(&mut dyn Model) -> bool,
|
|
532
|
+
expected: [bool; 3],
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/// The matrix, filled by execution.
|
|
536
|
+
///
|
|
537
|
+
/// `expected` is what the probes actually do. Changing a probe without changing
|
|
538
|
+
/// this table fails the suite.
|
|
539
|
+
const MATRIX: &[Row] = &[
|
|
540
|
+
Row {
|
|
541
|
+
requirement: "the same content can recur without reusing an identity",
|
|
542
|
+
evaluate: |m| !same_content_twice(m),
|
|
543
|
+
expected: [false, true, true],
|
|
544
|
+
},
|
|
545
|
+
Row {
|
|
546
|
+
requirement: "a revert is distinguishable from the state it returns to",
|
|
547
|
+
evaluate: |m| !revert_reuses_the_original_identity(m),
|
|
548
|
+
expected: [false, true, true],
|
|
549
|
+
},
|
|
550
|
+
Row {
|
|
551
|
+
requirement: "two resources at the same value are distinguishable",
|
|
552
|
+
evaluate: |m| !two_resources_share_an_identity(m),
|
|
553
|
+
expected: [false, true, true],
|
|
554
|
+
},
|
|
555
|
+
Row {
|
|
556
|
+
requirement: "a committed revision's parent is undisturbed by later mints",
|
|
557
|
+
evaluate: |m| !re_minting_a_child_as_a_root_disturbs_it(m),
|
|
558
|
+
// A fails this for the same reason it deduplicates: parentage is not a
|
|
559
|
+
// property of a revision there, so "the parent of R" has no stable
|
|
560
|
+
// answer. See `model_a_is_sound_only_when_history_is_walked_over_edges`.
|
|
561
|
+
expected: [false, true, true],
|
|
562
|
+
},
|
|
563
|
+
Row {
|
|
564
|
+
requirement: "a parent must resolve to a held revision",
|
|
565
|
+
evaluate: foreign_parent_refused,
|
|
566
|
+
expected: [true, true, true],
|
|
567
|
+
},
|
|
568
|
+
Row {
|
|
569
|
+
requirement: "the history of one resource is addressable",
|
|
570
|
+
evaluate: resource_history_addressable,
|
|
571
|
+
expected: [true, false, true],
|
|
572
|
+
},
|
|
573
|
+
Row {
|
|
574
|
+
requirement: "a per-resource retention policy is expressible",
|
|
575
|
+
evaluate: retention_expressible,
|
|
576
|
+
expected: [true, false, true],
|
|
577
|
+
},
|
|
578
|
+
Row {
|
|
579
|
+
requirement: "following parents terminates",
|
|
580
|
+
evaluate: topology_terminates,
|
|
581
|
+
// A cycles exactly as today's implementation does, and for the same
|
|
582
|
+
// reason: resolving a parent by *identity* collapses the two occasions
|
|
583
|
+
// one state was reached.
|
|
584
|
+
expected: [false, true, true],
|
|
585
|
+
},
|
|
586
|
+
];
|
|
587
|
+
|
|
588
|
+
#[test]
|
|
589
|
+
fn the_decision_matrix_is_exactly_this() {
|
|
590
|
+
let mut drifted = Vec::new();
|
|
591
|
+
for row in MATRIX {
|
|
592
|
+
for (index, mut model) in models().into_iter().enumerate() {
|
|
593
|
+
let actual = (row.evaluate)(model.as_mut());
|
|
594
|
+
if actual != row.expected[index] {
|
|
595
|
+
drifted.push(format!(
|
|
596
|
+
" {} / {}: expected {}, observed {}",
|
|
597
|
+
row.requirement,
|
|
598
|
+
model.name(),
|
|
599
|
+
row.expected[index],
|
|
600
|
+
actual
|
|
601
|
+
));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
assert!(
|
|
606
|
+
drifted.is_empty(),
|
|
607
|
+
"the decision matrix has drifted from the probes:\n{}\n\n\
|
|
608
|
+
Update docs/architecture/revision-model-decision.md and this table together.",
|
|
609
|
+
drifted.join("\n")
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/// **What the matrix forces.**
|
|
614
|
+
///
|
|
615
|
+
/// A passes 3 of 8, B passes 6 of 8, C passes 8 of 8.
|
|
616
|
+
///
|
|
617
|
+
/// B fails exactly the two rows that need a resource, and no discriminator added
|
|
618
|
+
/// to its identity can fix that: those rows are unanswerable because the model
|
|
619
|
+
/// has no resource to name, not because its identity is too coarse. Giving B a
|
|
620
|
+
/// resource dimension **is** model C.
|
|
621
|
+
///
|
|
622
|
+
/// A fails the three identity rows by design — a recurring state is one object
|
|
623
|
+
/// referenced twice — and then fails two more as a consequence, because once
|
|
624
|
+
/// identity is shared, "the parent of this revision" stops having one answer.
|
|
625
|
+
/// That is not a defect in the probe; it is what moving ancestry out of the
|
|
626
|
+
/// revision costs, and it is only recoverable under the discipline the next
|
|
627
|
+
/// test describes.
|
|
628
|
+
#[test]
|
|
629
|
+
fn the_matrix_scores_a_three_b_six_c_eight() {
|
|
630
|
+
let mut scores = [0usize; 3];
|
|
631
|
+
for row in MATRIX {
|
|
632
|
+
for (index, mut model) in models().into_iter().enumerate() {
|
|
633
|
+
if (row.evaluate)(model.as_mut()) {
|
|
634
|
+
scores[index] += 1;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
assert_eq!(scores, [3, 6, 8], "A, B, C");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/// **B's failures are the two rows that need a resource, and only those.**
|
|
642
|
+
#[test]
|
|
643
|
+
fn model_b_fails_exactly_the_resource_rows() {
|
|
644
|
+
let failed: Vec<&str> = MATRIX
|
|
645
|
+
.iter()
|
|
646
|
+
.filter(|row| !row.expected[1])
|
|
647
|
+
.map(|row| row.requirement)
|
|
648
|
+
.collect();
|
|
649
|
+
assert_eq!(
|
|
650
|
+
failed,
|
|
651
|
+
vec![
|
|
652
|
+
"the history of one resource is addressable",
|
|
653
|
+
"a per-resource retention policy is expressible",
|
|
654
|
+
]
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/// **Model A is sound — but only if the addressable unit of history is the
|
|
659
|
+
/// edge, not the revision.**
|
|
660
|
+
///
|
|
661
|
+
/// The matrix shows A cycling and losing parentage when ancestry is resolved by
|
|
662
|
+
/// revision identity. That is the tempting reading of "ancestry lives outside
|
|
663
|
+
/// the revision", and it does not work: two occasions of one state are one
|
|
664
|
+
/// identity, so the question has two answers and the walk closes a loop.
|
|
665
|
+
///
|
|
666
|
+
/// Walked over the *edge list* instead — where each edge is a distinct,
|
|
667
|
+
/// ordered occurrence — A is well behaved: the walk strictly decreases in index
|
|
668
|
+
/// and therefore terminates, and no edge is ever rewritten.
|
|
669
|
+
///
|
|
670
|
+
/// So A is not disqualified. It is conditional, and the condition is a real
|
|
671
|
+
/// constraint on every consumer: `base` in model A is an edge lookup, and
|
|
672
|
+
/// `revision.parent` does not exist.
|
|
673
|
+
#[test]
|
|
674
|
+
fn model_a_is_sound_only_when_history_is_walked_over_edges() {
|
|
675
|
+
let mut model = StateModel::default();
|
|
676
|
+
let first = model.mint("tasks:1", X, None).unwrap();
|
|
677
|
+
let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
|
|
678
|
+
let third = model.mint("tasks:1", X, Some(&second)).unwrap();
|
|
679
|
+
assert_eq!(first, third, "one object, reached twice");
|
|
680
|
+
|
|
681
|
+
// Walk backwards over the edges of this resource, by position.
|
|
682
|
+
let positions: Vec<usize> = model
|
|
683
|
+
.edges
|
|
684
|
+
.iter()
|
|
685
|
+
.enumerate()
|
|
686
|
+
.filter(|(_, edge)| edge.resource == "tasks:1")
|
|
687
|
+
.map(|(index, _)| index)
|
|
688
|
+
.collect();
|
|
689
|
+
|
|
690
|
+
let mut walked = Vec::new();
|
|
691
|
+
let mut cursor = Some(*positions.last().unwrap());
|
|
692
|
+
while let Some(position) = cursor {
|
|
693
|
+
walked.push(model.edges[position].to.clone());
|
|
694
|
+
let from = model.edges[position].from.clone();
|
|
695
|
+
cursor = match from {
|
|
696
|
+
None => None,
|
|
697
|
+
Some(parent) => positions
|
|
698
|
+
.iter()
|
|
699
|
+
.rev()
|
|
700
|
+
.find(|candidate| **candidate < position && model.edges[**candidate].to == parent)
|
|
701
|
+
.copied(),
|
|
702
|
+
};
|
|
703
|
+
assert!(walked.len() <= 8, "the edge walk must terminate");
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
assert_eq!(
|
|
707
|
+
walked,
|
|
708
|
+
vec![third, second, first],
|
|
709
|
+
"three occasions, in order, from one deduplicated pair of states"
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/// **Why A behaves that way**, stated where it cannot be missed.
|
|
714
|
+
///
|
|
715
|
+
/// A keeps deduplication — the only thing currently bounding storage growth —
|
|
716
|
+
/// but it pays for it by moving ancestry out of the revision. A revision can no
|
|
717
|
+
/// longer be asked for its parent: the question only has an answer relative to a
|
|
718
|
+
/// resource, and if a state recurs on one resource it has more than one answer.
|
|
719
|
+
#[test]
|
|
720
|
+
fn model_a_has_no_well_formed_parent_for_a_revision() {
|
|
721
|
+
let mut model = StateModel::default();
|
|
722
|
+
let first = model.mint("tasks:1", X, None).unwrap();
|
|
723
|
+
let second = model.mint("tasks:1", Y, Some(&first)).unwrap();
|
|
724
|
+
let third = model.mint("tasks:1", X, Some(&second)).unwrap();
|
|
725
|
+
assert_eq!(first, third, "one object, reached twice");
|
|
726
|
+
|
|
727
|
+
// The same identity has two parents on one resource, depending on when.
|
|
728
|
+
let parents: Vec<Option<Rid>> = model
|
|
729
|
+
.edges
|
|
730
|
+
.iter()
|
|
731
|
+
.filter(|edge| edge.to == first)
|
|
732
|
+
.map(|edge| edge.from.clone())
|
|
733
|
+
.collect();
|
|
734
|
+
assert_eq!(
|
|
735
|
+
parents,
|
|
736
|
+
vec![None, Some(second)],
|
|
737
|
+
"'the parent of this revision' is not a well-formed question in model A"
|
|
738
|
+
);
|
|
739
|
+
}
|