@feltdb/core 0.8.6 → 0.8.8
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/collection.d.ts.map +1 -1
- package/dist/collection.js +26 -1
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +465 -107
- package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +96 -0
- package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +191 -0
- package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +1153 -0
- package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +771 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +3 -3
- package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1 -1
- package/dist/http-db.d.ts +11 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +11 -3
- package/dist/studio-app/assets/{index-Bbos1m2U.js → index-ByHX4xDq.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1153 @@
|
|
|
1
|
+
//! The FeltDB production certification matrix.
|
|
2
|
+
//!
|
|
3
|
+
//! This file is the **authoritative** statement of what FeltDB can and cannot
|
|
4
|
+
//! claim. Every count in `docs/architecture/production-certification.md` is
|
|
5
|
+
//! derived from the table below; none is maintained by hand.
|
|
6
|
+
//!
|
|
7
|
+
//! # The certification principle
|
|
8
|
+
//!
|
|
9
|
+
//! > For every claim: **what exact evidence proves this today, and what does
|
|
10
|
+
//! > that evidence not prove?**
|
|
11
|
+
//!
|
|
12
|
+
//! A claim is not `Proven` because an implementation exists, because another
|
|
13
|
+
//! claim implies it, because an earlier change said so, because a related test
|
|
14
|
+
//! passes, or because the wording sounds reasonable. Every `Proven` claim names
|
|
15
|
+
//! a test that exists and runs now.
|
|
16
|
+
//!
|
|
17
|
+
//! # What this audit changed
|
|
18
|
+
//!
|
|
19
|
+
//! Recalculating rather than inheriting **downgraded two claims** that earlier
|
|
20
|
+
//! work recorded as `Proven` (see `DOWNGRADES`), corrected two dangling
|
|
21
|
+
//! evidence citations, and added the managed-incident remediation, which
|
|
22
|
+
//! had shipped with tests but no matrix entry at all. Discovering that an
|
|
23
|
+
//! earlier conclusion no longer holds is the point of the exercise, not a
|
|
24
|
+
//! failure of it.
|
|
25
|
+
|
|
26
|
+
use std::collections::BTreeMap;
|
|
27
|
+
|
|
28
|
+
mod common;
|
|
29
|
+
|
|
30
|
+
/// The status labels that withhold rather than grant. A table row carrying one
|
|
31
|
+
/// of these is recording a gap, and is read as such by the prose tripwire.
|
|
32
|
+
const WITHHELD_LABELS: &[&str] = &["Partially Proven", "Unproven", "Blocked", "Not Applicable"];
|
|
33
|
+
|
|
34
|
+
/// The only statuses this certification recognises.
|
|
35
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
36
|
+
enum Status {
|
|
37
|
+
Proven,
|
|
38
|
+
PartiallyProven,
|
|
39
|
+
Unproven,
|
|
40
|
+
Blocked,
|
|
41
|
+
NotApplicable,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl Status {
|
|
45
|
+
/// Every status this certification recognises. Counts are derived over this
|
|
46
|
+
/// list, so a bucket that happens to be empty is still published as zero.
|
|
47
|
+
const ALL: [Status; 5] = [
|
|
48
|
+
Status::Proven,
|
|
49
|
+
Status::PartiallyProven,
|
|
50
|
+
Status::Unproven,
|
|
51
|
+
Status::Blocked,
|
|
52
|
+
Status::NotApplicable,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/// The enum variant's own name, which is how earlier ledgers spell it.
|
|
56
|
+
fn variant(self) -> &'static str {
|
|
57
|
+
match self {
|
|
58
|
+
Status::Proven => "Proven",
|
|
59
|
+
Status::PartiallyProven => "PartiallyProven",
|
|
60
|
+
Status::Unproven => "Unproven",
|
|
61
|
+
Status::Blocked => "Blocked",
|
|
62
|
+
Status::NotApplicable => "NotApplicable",
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// How much a status claims. Only used to assert that a divergence from an
|
|
67
|
+
/// earlier ledger is a *weakening*; a strengthening needs new evidence and
|
|
68
|
+
/// must not slip through as a declared downgrade.
|
|
69
|
+
fn strength(self) -> u8 {
|
|
70
|
+
match self {
|
|
71
|
+
Status::Proven => 3,
|
|
72
|
+
Status::PartiallyProven => 2,
|
|
73
|
+
Status::Blocked => 1,
|
|
74
|
+
Status::Unproven | Status::NotApplicable => 0,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn label(self) -> &'static str {
|
|
79
|
+
match self {
|
|
80
|
+
Status::Proven => "Proven",
|
|
81
|
+
Status::PartiallyProven => "Partially Proven",
|
|
82
|
+
Status::Unproven => "Unproven",
|
|
83
|
+
Status::Blocked => "Blocked",
|
|
84
|
+
Status::NotApplicable => "Not Applicable",
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// How a claim is established. The distinction matters: implementation
|
|
90
|
+
/// inspection alone may not carry a *behavioural* guarantee.
|
|
91
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
92
|
+
enum Evidence {
|
|
93
|
+
/// A test that exercises the behaviour and asserts the outcome.
|
|
94
|
+
ExecutableContract,
|
|
95
|
+
/// A test that damages, truncates or kills something and asserts recovery.
|
|
96
|
+
FailureInjection,
|
|
97
|
+
/// A test that runs a fixed experiment and asserts a relationship.
|
|
98
|
+
DeterministicExperiment,
|
|
99
|
+
/// A measurement under a frozen workload.
|
|
100
|
+
Benchmark,
|
|
101
|
+
/// Reading the code. Sufficient only for structural claims.
|
|
102
|
+
ImplementationInspection,
|
|
103
|
+
/// No evidence exists.
|
|
104
|
+
None,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
impl Evidence {
|
|
108
|
+
/// How the published document names this evidence type.
|
|
109
|
+
fn label(self) -> &'static str {
|
|
110
|
+
match self {
|
|
111
|
+
Evidence::ExecutableContract => "Executable contract",
|
|
112
|
+
Evidence::FailureInjection => "Failure injection",
|
|
113
|
+
Evidence::DeterministicExperiment => "Deterministic experiment",
|
|
114
|
+
Evidence::Benchmark => "Benchmark",
|
|
115
|
+
Evidence::ImplementationInspection => "Implementation inspection",
|
|
116
|
+
Evidence::None => "None",
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
struct Claim {
|
|
122
|
+
id: &'static str,
|
|
123
|
+
domain: &'static str,
|
|
124
|
+
claim: &'static str,
|
|
125
|
+
status: Status,
|
|
126
|
+
/// The test function that establishes this claim, or "" when there is none.
|
|
127
|
+
evidence: &'static str,
|
|
128
|
+
kind: Evidence,
|
|
129
|
+
/// What the evidence does **not** establish. Required for every claim that
|
|
130
|
+
/// is not fully `Proven`, and used freely on `Proven` ones.
|
|
131
|
+
limitation: &'static str,
|
|
132
|
+
source: &'static str,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Where this audit disagrees with the ledger of the pull request that first
|
|
136
|
+
/// recorded a claim.
|
|
137
|
+
///
|
|
138
|
+
/// The earlier ledgers stay as they are: they are the record of what #307, #313
|
|
139
|
+
/// and #314 concluded on the evidence they had, and rewriting them would erase
|
|
140
|
+
/// the fact that the conclusion changed. But a disagreement must never be
|
|
141
|
+
/// silent, so every one of them is declared here and checked in both
|
|
142
|
+
/// directions — the earlier ledger must still say what this table claims it
|
|
143
|
+
/// says, and the certification must still say the weaker thing.
|
|
144
|
+
///
|
|
145
|
+
/// `(claim, what the earlier ledger records, what this audit records, why)`
|
|
146
|
+
const DOWNGRADES: &[(&str, &str, &str, &str)] = &[
|
|
147
|
+
(
|
|
148
|
+
"E6",
|
|
149
|
+
"Proven",
|
|
150
|
+
"PartiallyProven",
|
|
151
|
+
"the frozen workload is asserted by a test; the environment is only in prose",
|
|
152
|
+
),
|
|
153
|
+
(
|
|
154
|
+
"O2",
|
|
155
|
+
"Proven",
|
|
156
|
+
"PartiallyProven",
|
|
157
|
+
"the library value and the status derivation are tested; the HTTP handler is not",
|
|
158
|
+
),
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
/// The certification matrix.
|
|
162
|
+
///
|
|
163
|
+
/// Laid out as a table on purpose: a claim is meant to be read across its row,
|
|
164
|
+
/// and the fields line up so a missing limitation or an empty evidence cell is
|
|
165
|
+
/// visible at a glance. `rustfmt` would expand each entry to nine lines and
|
|
166
|
+
/// lose that.
|
|
167
|
+
#[rustfmt::skip]
|
|
168
|
+
const CERTIFICATION: &[Claim] = &[
|
|
169
|
+
// ---------------------------------------------------------------- D
|
|
170
|
+
Claim { id: "D1", domain: "Durability", claim: "committed mutations survive a clean restart",
|
|
171
|
+
status: Status::Proven, evidence: "a_clean_restart_preserves_state_history_and_retention",
|
|
172
|
+
kind: Evidence::ExecutableContract,
|
|
173
|
+
limitation: "A clean, cooperative shutdown. Says nothing about an abrupt one.",
|
|
174
|
+
source: "#307" },
|
|
175
|
+
Claim { id: "D2", domain: "Durability", claim: "revision history survives a clean restart",
|
|
176
|
+
status: Status::Proven, evidence: "a_clean_restart_preserves_state_history_and_retention",
|
|
177
|
+
kind: Evidence::ExecutableContract, limitation: "As D1.", source: "#307" },
|
|
178
|
+
Claim { id: "D3", domain: "Durability", claim: "retention policy and horizon survive a restart",
|
|
179
|
+
status: Status::Proven, evidence: "a_clean_restart_preserves_state_history_and_retention",
|
|
180
|
+
kind: Evidence::ExecutableContract, limitation: "As D1.", source: "#306" },
|
|
181
|
+
Claim { id: "D4", domain: "Durability", claim: "a reopened database continues history rather than restarting it",
|
|
182
|
+
status: Status::Proven, evidence: "a_clean_restart_preserves_state_history_and_retention",
|
|
183
|
+
kind: Evidence::ExecutableContract, limitation: "As D1.", source: "#306" },
|
|
184
|
+
Claim { id: "D5", domain: "Durability", claim: "a single write survives operating-system or power loss",
|
|
185
|
+
status: Status::Unproven, evidence: "", kind: Evidence::None,
|
|
186
|
+
limitation: "NOT PROVEN AND NOT PROVABLE HERE. The default mode flushes to the \
|
|
187
|
+
operating system without fsync. `Synced` mode issues a barrier per write, \
|
|
188
|
+
but whether the device honoured it is a property of the storage stack that \
|
|
189
|
+
no test in this repository can observe. Settling this needs an experiment \
|
|
190
|
+
that cuts power to real hardware.",
|
|
191
|
+
source: "#312, #314" },
|
|
192
|
+
Claim { id: "D6", domain: "Durability", claim: "every crash point leaves a clean prefix of the mutation sequence",
|
|
193
|
+
status: Status::Proven, evidence: "every_crash_point_leaves_a_clean_prefix",
|
|
194
|
+
kind: Evidence::FailureInjection,
|
|
195
|
+
limitation: "Truncation models the durable bytes at every byte boundary of a \
|
|
196
|
+
three-write log. It does not model a filesystem reordering writes or a \
|
|
197
|
+
device returning stale data.",
|
|
198
|
+
source: "#312" },
|
|
199
|
+
Claim { id: "D7", domain: "Durability", claim: "a crash never produces a revision for a record that was not written",
|
|
200
|
+
status: Status::Proven, evidence: "no_crash_point_produces_a_phantom_revision",
|
|
201
|
+
kind: Evidence::FailureInjection,
|
|
202
|
+
limitation: "Follows from record-before-revision ordering; verified over one workload.",
|
|
203
|
+
source: "#312" },
|
|
204
|
+
Claim { id: "D8", domain: "Durability", claim: "a committed write survives the process dying",
|
|
205
|
+
status: Status::Proven, evidence: "a_committed_write_survives_process_death",
|
|
206
|
+
kind: Evidence::FailureInjection,
|
|
207
|
+
limitation: "A real SIGABRT. The process, not the machine.", source: "#312" },
|
|
208
|
+
Claim { id: "D9", domain: "Durability", claim: "a transaction is all-or-nothing across a crash",
|
|
209
|
+
status: Status::Proven, evidence: "a_transaction_is_all_or_nothing_at_every_crash_point",
|
|
210
|
+
kind: Evidence::FailureInjection,
|
|
211
|
+
limitation: "Every byte offset of one three-row transaction.", source: "#312" },
|
|
212
|
+
Claim { id: "D10", domain: "Durability", claim: "a crash-recovered database replicates identically",
|
|
213
|
+
status: Status::Proven, evidence: "a_crash_recovered_database_replicates_identically",
|
|
214
|
+
kind: Evidence::FailureInjection,
|
|
215
|
+
limitation: "Crash points sampled every 37 bytes, not exhaustively.", source: "#312" },
|
|
216
|
+
// ---------------------------------------------------------------- A
|
|
217
|
+
Claim { id: "A1", domain: "Atomicity", claim: "a stale fenced write is rejected and mints nothing",
|
|
218
|
+
status: Status::Proven, evidence: "a_stale_fenced_write_is_rejected_and_mints_nothing",
|
|
219
|
+
kind: Evidence::ExecutableContract, limitation: "Version conflict only.", source: "#307" },
|
|
220
|
+
Claim { id: "A2", domain: "Atomicity", claim: "concurrent writes to distinct resources keep distinct chains",
|
|
221
|
+
status: Status::Proven, evidence: "concurrent_writes_produce_correct_chains",
|
|
222
|
+
kind: Evidence::ExecutableContract,
|
|
223
|
+
limitation: "Four threads, twenty-five writes each, on one machine.", source: "#307" },
|
|
224
|
+
Claim { id: "A3", domain: "Atomicity", claim: "concurrent writes to one resource form one correct chain",
|
|
225
|
+
status: Status::Proven, evidence: "concurrent_writes_produce_correct_chains",
|
|
226
|
+
kind: Evidence::ExecutableContract,
|
|
227
|
+
limitation: "One hundred writes producing one hundred unique sequences. Not a \
|
|
228
|
+
stress test.", source: "#307" },
|
|
229
|
+
Claim { id: "A4", domain: "Atomicity", claim: "a duplicate remote operation is idempotent",
|
|
230
|
+
status: Status::Proven, evidence: "replayed_and_gapped_remote_operations_are_handled_explicitly",
|
|
231
|
+
kind: Evidence::ExecutableContract, limitation: "Whole-stream replay.", source: "#307" },
|
|
232
|
+
Claim { id: "A5", domain: "Atomicity", claim: "a remote operation with a sequence gap is rejected",
|
|
233
|
+
status: Status::Proven, evidence: "replayed_and_gapped_remote_operations_are_handled_explicitly",
|
|
234
|
+
kind: Evidence::ExecutableContract, limitation: "One gap shape.", source: "#307" },
|
|
235
|
+
Claim { id: "A6", domain: "Atomicity", claim: "a crash part-way through a mutation leaves consistent state",
|
|
236
|
+
status: Status::Proven, evidence: "every_crash_point_leaves_a_clean_prefix",
|
|
237
|
+
kind: Evidence::FailureInjection, limitation: "As D6.", source: "#312" },
|
|
238
|
+
// ---------------------------------------------------------------- R
|
|
239
|
+
Claim { id: "R1", domain: "Recovery", claim: "an incomplete final append is recovered and reported, not silently dropped",
|
|
240
|
+
status: Status::Proven, evidence: "an_unterminated_final_record_is_recovered_and_reported",
|
|
241
|
+
kind: Evidence::FailureInjection,
|
|
242
|
+
limitation: "A KNOWN INCOMPLETE APPEND ONLY. This is not general recovery from \
|
|
243
|
+
storage failure.", source: "#309" },
|
|
244
|
+
Claim { id: "R2", domain: "Recovery", claim: "a complete but invalid record refuses the open, wherever it sits",
|
|
245
|
+
status: Status::Proven, evidence: "a_complete_but_invalid_final_record_refuses_the_open",
|
|
246
|
+
kind: Evidence::FailureInjection,
|
|
247
|
+
limitation: "Detection and refusal. Damaged records are never repaired.", source: "#309" },
|
|
248
|
+
Claim { id: "R3", domain: "Recovery", claim: "damaged ancestry is distinguishable from expired ancestry",
|
|
249
|
+
status: Status::Proven, evidence: "damaged_ancestry_is_distinguishable_from_expired_ancestry",
|
|
250
|
+
kind: Evidence::ExecutableContract,
|
|
251
|
+
limitation: "On inspection of a named revision. See R4.", source: "#306, #307" },
|
|
252
|
+
Claim { id: "R4", domain: "Recovery", claim: "ancestry damage is surfaced without being asked for",
|
|
253
|
+
status: Status::Unproven, evidence: "", kind: Evidence::None,
|
|
254
|
+
limitation: "`parent_of` distinguishes Expired from Missing when a caller asks about \
|
|
255
|
+
a specific revision. NOTHING SCANS PROACTIVELY. Detection on inspection is \
|
|
256
|
+
not monitoring.", source: "#307" },
|
|
257
|
+
// ---------------------------------------------------------------- H
|
|
258
|
+
Claim { id: "H1", domain: "History", claim: "history is bounded under a configured policy",
|
|
259
|
+
status: Status::Proven, evidence: "repeated_mutation_has_bounded_history",
|
|
260
|
+
kind: Evidence::ExecutableContract, limitation: "Count-based policy only.", source: "#306" },
|
|
261
|
+
Claim { id: "H2", domain: "History", claim: "history is unbounded without one, by design",
|
|
262
|
+
status: Status::Proven, evidence: "the_retention_envelope_behaves_in_both_directions",
|
|
263
|
+
kind: Evidence::ExecutableContract,
|
|
264
|
+
limitation: "A stated contract, not an omission.", source: "#306" },
|
|
265
|
+
Claim { id: "H3", domain: "History", claim: "a policy change takes effect in both directions",
|
|
266
|
+
status: Status::Proven, evidence: "the_retention_envelope_behaves_in_both_directions",
|
|
267
|
+
kind: Evidence::ExecutableContract,
|
|
268
|
+
limitation: "Loosening does not restore what was expired.", source: "#306" },
|
|
269
|
+
Claim { id: "H4", domain: "History", claim: "maintenance never rewrites history",
|
|
270
|
+
status: Status::Proven, evidence: "maintenance_never_rewrites_history_and_forks_survive",
|
|
271
|
+
kind: Evidence::ExecutableContract,
|
|
272
|
+
limitation: "Compaction, an unrelated write and a retention pass.", source: "#306" },
|
|
273
|
+
Claim { id: "H5", domain: "History", claim: "forked history is preserved",
|
|
274
|
+
status: Status::Proven, evidence: "maintenance_never_rewrites_history_and_forks_survive",
|
|
275
|
+
kind: Evidence::ExecutableContract, limitation: "Two heads from one base.", source: "#306" },
|
|
276
|
+
// ---------------------------------------------------------------- B
|
|
277
|
+
Claim { id: "B1", domain: "Backup", claim: "a durable backup artifact is self-contained and independently verifiable",
|
|
278
|
+
status: Status::Proven, evidence: "a_backup_is_self_contained_and_outlives_its_source",
|
|
279
|
+
kind: Evidence::ExecutableContract,
|
|
280
|
+
limitation: "Verified with the source database deleted.", source: "#310" },
|
|
281
|
+
Claim { id: "B2", domain: "Backup", claim: "a restore proves the result means the same thing, not merely that it parsed",
|
|
282
|
+
status: Status::Proven, evidence: "a_restore_preserves_state_history_forks_retention_and_provenance",
|
|
283
|
+
kind: Evidence::ExecutableContract,
|
|
284
|
+
limitation: "The meaning digest is recomputed from the restored live database.",
|
|
285
|
+
source: "#310" },
|
|
286
|
+
Claim { id: "B3", domain: "Backup", claim: "an altered or truncated backup is refused rather than restored",
|
|
287
|
+
status: Status::Proven, evidence: "a_single_altered_byte_is_refused",
|
|
288
|
+
kind: Evidence::FailureInjection,
|
|
289
|
+
limitation: "One altered byte inside a value: still valid JSON, still a valid record.",
|
|
290
|
+
source: "#310" },
|
|
291
|
+
Claim { id: "B4", domain: "Backup", claim: "a restore will not overwrite an existing database",
|
|
292
|
+
status: Status::Proven, evidence: "a_restore_will_not_overwrite_an_existing_database",
|
|
293
|
+
kind: Evidence::ExecutableContract,
|
|
294
|
+
limitation: "The occupied target is byte-identical afterwards.", source: "#310" },
|
|
295
|
+
Claim { id: "B5", domain: "Backup", claim: "backup and restore are exposed as an operator command-line workflow",
|
|
296
|
+
status: Status::Unproven, evidence: "", kind: Evidence::None,
|
|
297
|
+
limitation: "THE PRIMITIVE WORKING IS NOT A WORKFLOW. `write_backup`, `verify_backup` \
|
|
298
|
+
and `restore_backup` are library calls. There is no command, schedule, or \
|
|
299
|
+
documented operator procedure. An operator without a Rust toolchain has no \
|
|
300
|
+
backup.", source: "#310" },
|
|
301
|
+
// ---------------------------------------------------------------- S
|
|
302
|
+
Claim { id: "S1", domain: "Replication", claim: "current state replicates to a peer",
|
|
303
|
+
status: Status::Proven, evidence: "replication_preserves_revision_history_and_ancestry",
|
|
304
|
+
kind: Evidence::ExecutableContract,
|
|
305
|
+
limitation: "STATE CONVERGENCE IS NOT HISTORY CONVERGENCE. See S4.", source: "#311" },
|
|
306
|
+
Claim { id: "S2", domain: "Replication", claim: "revision history replicates to a peer with identical identities",
|
|
307
|
+
status: Status::Proven, evidence: "replication_preserves_revision_history_and_ancestry",
|
|
308
|
+
kind: Evidence::ExecutableContract,
|
|
309
|
+
limitation: "Reconstructed from operation provenance, not re-minted. Requires the peer \
|
|
310
|
+
to send provenance; see S5.", source: "#311" },
|
|
311
|
+
Claim { id: "S3", domain: "Replication", claim: "replicated history preserves parents, sequences, forks and provenance",
|
|
312
|
+
status: Status::Proven, evidence: "concurrent_authorities_produce_a_fork_that_survives_replication",
|
|
313
|
+
kind: Evidence::ExecutableContract,
|
|
314
|
+
limitation: "Two authorities, one fork.", source: "#311" },
|
|
315
|
+
Claim { id: "S4", domain: "Replication", claim: "state equivalence and historical equivalence are checked separately",
|
|
316
|
+
status: Status::Proven, evidence: "same_state_with_different_history_is_detected",
|
|
317
|
+
kind: Evidence::ExecutableContract,
|
|
318
|
+
limitation: "THE NEGATIVE CONTROL. Two databases with an identical state digest and a \
|
|
319
|
+
different history digest. Without this, every other S claim could be \
|
|
320
|
+
satisfied by values alone.", source: "#311" },
|
|
321
|
+
Claim { id: "S5", domain: "Replication", claim: "a peer that sends no revision provenance contributes no history",
|
|
322
|
+
status: Status::Proven, evidence: "an_operation_without_provenance_replicates_state_only",
|
|
323
|
+
kind: Evidence::ExecutableContract,
|
|
324
|
+
limitation: "A bounded, stated consequence of the field being additive.", source: "#311" },
|
|
325
|
+
// ---------------------------------------------------------------- E
|
|
326
|
+
Claim { id: "E1", domain: "Envelope", claim: "retention cost per write grows with the retention window",
|
|
327
|
+
status: Status::Proven, evidence: "the_retention_envelope_has_the_measured_shape",
|
|
328
|
+
kind: Evidence::DeterministicExperiment, limitation: "An ordering, not a number.", source: "#314" },
|
|
329
|
+
Claim { id: "E2", domain: "Envelope", claim: "retention increases log size before compaction",
|
|
330
|
+
status: Status::Proven, evidence: "the_retention_envelope_has_the_measured_shape",
|
|
331
|
+
kind: Evidence::DeterministicExperiment, limitation: "Expiry tombstones. Before compaction only.",
|
|
332
|
+
source: "#314" },
|
|
333
|
+
Claim { id: "E3", domain: "Envelope", claim: "durability is a selectable contract, and each mode states its guarantee",
|
|
334
|
+
status: Status::Proven, evidence: "every_durability_mode_states_its_own_guarantee",
|
|
335
|
+
kind: Evidence::ExecutableContract,
|
|
336
|
+
limitation: "The wording is asserted not to overstate. It does not make the guarantee true.",
|
|
337
|
+
source: "#314" },
|
|
338
|
+
Claim { id: "E4", domain: "Envelope", claim: "a stable-storage barrier per write has a measured cost",
|
|
339
|
+
status: Status::Proven, evidence: "a_barrier_per_write_costs_more_than_none",
|
|
340
|
+
kind: Evidence::DeterministicExperiment,
|
|
341
|
+
limitation: "3-4x on the recorded environment. THE COST OF ASKING, NOT PROOF OF LANDING.",
|
|
342
|
+
source: "#314" },
|
|
343
|
+
Claim { id: "E5", domain: "Envelope", claim: "the durability mode changes the contract and not the stored database",
|
|
344
|
+
status: Status::Proven, evidence: "the_durability_mode_does_not_change_what_is_stored",
|
|
345
|
+
kind: Evidence::ExecutableContract,
|
|
346
|
+
limitation: "Compared on one path, because authority identity is path-derived.",
|
|
347
|
+
source: "#314" },
|
|
348
|
+
Claim { id: "E6", domain: "Envelope", claim: "the workload envelope is recorded against a frozen workload and a named environment",
|
|
349
|
+
status: Status::PartiallyProven, evidence: "a_measurement_carries_the_workload_that_produced_it",
|
|
350
|
+
kind: Evidence::Benchmark,
|
|
351
|
+
limitation: "DOWNGRADED BY THIS AUDIT. The test asserts the frozen workload half only. \
|
|
352
|
+
The environment is recorded in prose in the envelope document and no test \
|
|
353
|
+
asserts that a measurement carries it.", source: "#314" },
|
|
354
|
+
Claim { id: "E7", domain: "Envelope", claim: "grouped durability is measurably cheaper than a barrier per write",
|
|
355
|
+
status: Status::Unproven, evidence: "", kind: Evidence::None,
|
|
356
|
+
limitation: "Grouped ran SLOWER than per-write fsync on one of four workloads and faster \
|
|
357
|
+
on three. The proven property is that grouping reduces barrier frequency to \
|
|
358
|
+
the configured interval, which is a contract, not a speed.", source: "#314" },
|
|
359
|
+
// ---------------------------------------------------------------- U
|
|
360
|
+
Claim { id: "U1", domain: "Format", claim: "an incompatible durable format is detected before any record is interpreted",
|
|
361
|
+
status: Status::Proven, evidence: "pre_306_database_does_not_open_as_empty_history",
|
|
362
|
+
kind: Evidence::FailureInjection,
|
|
363
|
+
limitation: "A read-only probe runs before the open.", source: "#308" },
|
|
364
|
+
Claim { id: "U2", domain: "Format", claim: "a pre-model database's revision history is losslessly migratable",
|
|
365
|
+
status: Status::NotApplicable, evidence: "migration_is_refused_because_the_resource_was_never_recorded",
|
|
366
|
+
kind: Evidence::ExecutableContract,
|
|
367
|
+
limitation: "IMPOSSIBLE, NOT UNIMPLEMENTED. A legacy revision never recorded its \
|
|
368
|
+
resource, so which history it belongs to is not in the data. Migration \
|
|
369
|
+
would have to invent it.", source: "#308" },
|
|
370
|
+
Claim { id: "U3", domain: "Format", claim: "a persisted durable format version exists and is checked on open",
|
|
371
|
+
status: Status::Proven, evidence: "a_new_database_declares_its_format",
|
|
372
|
+
kind: Evidence::ExecutableContract,
|
|
373
|
+
limitation: "New logs and compaction headers. An unversioned database with no legacy \
|
|
374
|
+
revisions opens by design.", source: "#308" },
|
|
375
|
+
Claim { id: "U4", domain: "Format", claim: "a refused open leaves the database byte-identical",
|
|
376
|
+
status: Status::Proven, evidence: "pre_306_database_does_not_open_as_empty_history",
|
|
377
|
+
kind: Evidence::FailureInjection,
|
|
378
|
+
limitation: "Bytes compared before and after. Made unconditional by making the log \
|
|
379
|
+
reader read-only until replay succeeds.", source: "#308, #309" },
|
|
380
|
+
Claim { id: "U5", domain: "Format", claim: "a durable format from a newer build is refused",
|
|
381
|
+
status: Status::Proven, evidence: "a_newer_durable_format_is_refused",
|
|
382
|
+
kind: Evidence::FailureInjection,
|
|
383
|
+
limitation: "The rollback direction, exercised by a synthesised future version.",
|
|
384
|
+
source: "#308" },
|
|
385
|
+
Claim { id: "U6", domain: "Format", claim: "a snapshot from another format is refused",
|
|
386
|
+
status: Status::Proven, evidence: "a_snapshot_from_another_format_is_refused",
|
|
387
|
+
kind: Evidence::ExecutableContract,
|
|
388
|
+
limitation: "Snapshots are a second durable ingress and carry their own version.",
|
|
389
|
+
source: "#308" },
|
|
390
|
+
// ---------------------------------------------------------------- O
|
|
391
|
+
Claim { id: "O1", domain: "Observability", claim: "an operator can inspect history and retention state",
|
|
392
|
+
status: Status::PartiallyProven, evidence: "revision_retention_bounds_a_resources_history",
|
|
393
|
+
kind: Evidence::ExecutableContract,
|
|
394
|
+
limitation: "Per resource only. Nothing aggregates, so \"is any ancestry anywhere \
|
|
395
|
+
damaged?\" means walking every revision.", source: "#313" },
|
|
396
|
+
Claim { id: "O2", domain: "Observability", claim: "reported health reflects actual storage state",
|
|
397
|
+
status: Status::PartiallyProven, evidence: "a_clean_open_reports_clean_storage",
|
|
398
|
+
kind: Evidence::ExecutableContract,
|
|
399
|
+
limitation: "DOWNGRADED BY THIS AUDIT. The library value and the server's status \
|
|
400
|
+
derivation are each tested, but no test exercises the HTTP endpoint. That \
|
|
401
|
+
the handler composes them is established by inspection.", source: "#313" },
|
|
402
|
+
Claim { id: "O3", domain: "Observability", claim: "a recovered open is distinguishable from a clean one in health",
|
|
403
|
+
status: Status::Proven, evidence: "a_recovered_open_does_not_report_the_same_health_as_a_clean_one",
|
|
404
|
+
kind: Evidence::ExecutableContract,
|
|
405
|
+
limitation: "At the library boundary.", source: "#313" },
|
|
406
|
+
Claim { id: "O4", domain: "Observability", claim: "health reports only conditions that were observed",
|
|
407
|
+
status: Status::Proven, evidence: "health_reports_only_conditions_that_were_observed",
|
|
408
|
+
kind: Evidence::ExecutableContract,
|
|
409
|
+
limitation: "Asserts no storage label says `durable` or `healthy`.", source: "#313" },
|
|
410
|
+
Claim { id: "O5", domain: "Observability", claim: "the remaining health fields are compile-time constants",
|
|
411
|
+
status: Status::PartiallyProven, evidence: "crates/feltdb-server/src/main.rs:HealthResponse",
|
|
412
|
+
kind: Evidence::ImplementationInspection,
|
|
413
|
+
limitation: "`runtime`, `fabric`, `workflows` and `agents` are still constants. \
|
|
414
|
+
Recorded, not fixed: each needs an operational contract first.",
|
|
415
|
+
source: "#313" },
|
|
416
|
+
// ---------------------------------------------------------------- M
|
|
417
|
+
Claim { id: "M1", domain: "Managed incident", claim: "a timer-driven compaction defers the log rewrite",
|
|
418
|
+
status: Status::Proven, evidence: "the_default_policy_does_not_rewrite_on_a_handful_of_operations",
|
|
419
|
+
kind: Evidence::ExecutableContract,
|
|
420
|
+
limitation: "Ten timer ticks over a small database cause zero rewrites. THIS DOES NOT \
|
|
421
|
+
MAKE COMPACTION NON-BLOCKING; see M6.", source: "6b550ea" },
|
|
422
|
+
Claim { id: "M2", domain: "Managed incident", claim: "an explicit compaction still rewrites immediately",
|
|
423
|
+
status: Status::Proven, evidence: "an_explicit_compaction_still_rewrites_now",
|
|
424
|
+
kind: Evidence::ExecutableContract,
|
|
425
|
+
limitation: "The policy governs the timer, not a caller. Every prior call site keeps \
|
|
426
|
+
its behaviour.", source: "6b550ea" },
|
|
427
|
+
Claim { id: "M3", domain: "Managed incident", claim: "deferring the rewrite preserves state and history",
|
|
428
|
+
status: Status::Proven, evidence: "deferring_the_rewrite_preserves_state_and_history",
|
|
429
|
+
kind: Evidence::ExecutableContract,
|
|
430
|
+
limitation: "State and history digests across a reopen. The log stays longer than \
|
|
431
|
+
needed, which is the accepted cost.", source: "6b550ea" },
|
|
432
|
+
Claim { id: "M4", domain: "Managed incident", claim: "an overload is refused with a status, a code and a retry hint",
|
|
433
|
+
status: Status::Proven, evidence: "an_overload_is_an_explicit_refusal_with_a_retry_hint",
|
|
434
|
+
kind: Evidence::ExecutableContract,
|
|
435
|
+
limitation: "The refusal's shape is asserted. THAT THE BOUND AND DEADLINE FIRE UNDER \
|
|
436
|
+
REAL LOAD IS NOT TESTED; there is no load harness.", source: "6b550ea" },
|
|
437
|
+
Claim { id: "M5", domain: "Managed incident", claim: "a bounded search visits only as many rows as its limit",
|
|
438
|
+
status: Status::Proven, evidence: "a_bounded_search_stops_at_the_limit_instead_of_scanning",
|
|
439
|
+
kind: Evidence::ExecutableContract,
|
|
440
|
+
limitation: "500 rows, limit 10, exactly 10 predicate visits. THIS BOUNDS THE SEARCH \
|
|
441
|
+
AND PROVENANCE PATHS ONLY, not every read in the database.",
|
|
442
|
+
source: "6b550ea" },
|
|
443
|
+
Claim { id: "M6", domain: "Managed incident", claim: "compaction no longer blocks the database",
|
|
444
|
+
status: Status::Unproven, evidence: "", kind: Evidence::None,
|
|
445
|
+
limitation: "EXPLICITLY NOT CLAIMED. A rewrite still holds the global lock for its \
|
|
446
|
+
duration; it merely happens rarely and off the runtime. A large enough \
|
|
447
|
+
database will still stall while one runs. `the_rewrite_cost_grows_with_the_\
|
|
448
|
+
database` measures that it grows.", source: "6b550ea" },
|
|
449
|
+
Claim { id: "M7", domain: "Managed incident", claim: "readiness performs a real scoped storage probe",
|
|
450
|
+
status: Status::PartiallyProven, evidence: "a_recovered_open_does_not_report_the_same_health_as_a_clean_one",
|
|
451
|
+
kind: Evidence::ImplementationInspection,
|
|
452
|
+
limitation: "The storage conditions it reports are tested at the library boundary. \
|
|
453
|
+
THE `/health/ready` HANDLER ITSELF IS NOT EXERCISED BY A TEST, including \
|
|
454
|
+
the STORAGE_STALLED path; there is no HTTP-level harness.",
|
|
455
|
+
source: "6b550ea" },
|
|
456
|
+
Claim { id: "M8", domain: "Managed incident", claim: "scoped reads stay correct and bounded while a compaction runs",
|
|
457
|
+
status: Status::Proven, evidence: "scoped_reads_stay_correct_and_bounded_while_compaction_runs",
|
|
458
|
+
kind: Evidence::ExecutableContract,
|
|
459
|
+
limitation: "The composed shape, not the isolated fixes. NO LATENCY IS MEASURED, so \
|
|
460
|
+
this is not evidence that the incident cannot recur; see M6.",
|
|
461
|
+
source: "#315" },
|
|
462
|
+
Claim { id: "M9", domain: "Managed incident", claim: "a compaction concurrent with traffic leaves the database intact",
|
|
463
|
+
status: Status::Proven, evidence: "a_concurrent_compaction_leaves_the_database_intact",
|
|
464
|
+
kind: Evidence::ExecutableContract,
|
|
465
|
+
limitation: "Correctness under concurrency, including a forced rewrite and a reopen. \
|
|
466
|
+
Says nothing about how long anything waited.",
|
|
467
|
+
source: "#315" },
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
fn matrix() -> BTreeMap<&'static str, &'static Claim> {
|
|
471
|
+
CERTIFICATION.iter().map(|c| (c.id, c)).collect()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/// Every test named as evidence, gathered from the test sources on disk.
|
|
475
|
+
/// Resolve a claim's `evidence` field to something that actually exists.
|
|
476
|
+
///
|
|
477
|
+
/// Two citation forms are permitted, and the permitted form depends on the
|
|
478
|
+
/// evidence *kind*:
|
|
479
|
+
///
|
|
480
|
+
/// - a **test function name**, which must be a `fn` in this repository's test
|
|
481
|
+
/// suites (or in the server crate, which holds its tests inline);
|
|
482
|
+
/// - a **source location** of the form `path/to/file.rs:Symbol`, permitted only
|
|
483
|
+
/// for `ImplementationInspection`, where there is by definition no test. The
|
|
484
|
+
/// file must exist and must contain the named symbol.
|
|
485
|
+
///
|
|
486
|
+
/// The restriction is the point: a claim may not dodge the test requirement by
|
|
487
|
+
/// citing a source file. Only a claim that has already been marked as resting
|
|
488
|
+
/// on inspection — and which `inspection_alone_never_proves_a_behavioural_claim`
|
|
489
|
+
/// therefore forbids from being `Proven` — may cite one.
|
|
490
|
+
fn evidence_resolves(claim: &Claim, known: &[String]) -> bool {
|
|
491
|
+
match claim.evidence.split_once(".rs:") {
|
|
492
|
+
Some((file, symbol)) if claim.kind == Evidence::ImplementationInspection => {
|
|
493
|
+
let path = std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
|
|
494
|
+
.join(format!("{file}.rs"));
|
|
495
|
+
match std::fs::read_to_string(path) {
|
|
496
|
+
Ok(source) => source.contains(symbol),
|
|
497
|
+
Err(_) => false,
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
Some(_) => false,
|
|
501
|
+
None => known.iter().any(|f| f == claim.evidence),
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/// A tripwire that cannot fire is decoration, so the mention/assertion rule has
|
|
506
|
+
/// its own negative control.
|
|
507
|
+
#[test]
|
|
508
|
+
fn the_forbidden_phrase_rule_separates_assertion_from_mention() {
|
|
509
|
+
// Asserted: flagged.
|
|
510
|
+
assert!(common::asserts_phrase(
|
|
511
|
+
"feltdb is power-loss safe.",
|
|
512
|
+
"power-loss safe"
|
|
513
|
+
));
|
|
514
|
+
assert!(common::asserts_phrase(
|
|
515
|
+
"a mention of \"power-loss safe\" here, but the next line is not.\n\
|
|
516
|
+
and feltdb is power-loss safe.",
|
|
517
|
+
"power-loss safe"
|
|
518
|
+
));
|
|
519
|
+
|
|
520
|
+
// Merely named: not flagged.
|
|
521
|
+
assert!(!common::asserts_phrase(
|
|
522
|
+
"a test asserts no mode's wording contains \"power-loss safe\".",
|
|
523
|
+
"power-loss safe"
|
|
524
|
+
));
|
|
525
|
+
assert!(!common::asserts_phrase(
|
|
526
|
+
"the phrase `power-loss safe` appears nowhere in the api.",
|
|
527
|
+
"power-loss safe"
|
|
528
|
+
));
|
|
529
|
+
|
|
530
|
+
// An unbalanced delimiter must not silence the line it opens.
|
|
531
|
+
assert!(common::asserts_phrase(
|
|
532
|
+
"the \"mode is power-loss safe",
|
|
533
|
+
"power-loss safe"
|
|
534
|
+
));
|
|
535
|
+
|
|
536
|
+
// Absent: not flagged.
|
|
537
|
+
assert!(!common::asserts_phrase(
|
|
538
|
+
"flushed is not synced.",
|
|
539
|
+
"power-loss safe"
|
|
540
|
+
));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/// The published document, read from disk.
|
|
544
|
+
fn published_document() -> String {
|
|
545
|
+
let path = concat!(
|
|
546
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
547
|
+
"/../../docs/architecture/production-certification.md"
|
|
548
|
+
);
|
|
549
|
+
std::fs::read_to_string(path).expect("the certification document exists and is readable")
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/// The status counts the document publishes, read out of its summary table.
|
|
553
|
+
fn published_counts() -> BTreeMap<String, usize> {
|
|
554
|
+
let mut counts = BTreeMap::new();
|
|
555
|
+
for line in published_document().lines() {
|
|
556
|
+
let Some(cells) = common::table_cells(line) else {
|
|
557
|
+
continue;
|
|
558
|
+
};
|
|
559
|
+
if cells.len() != 2 {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
let label = cells[0].clone();
|
|
563
|
+
if !Status::ALL.iter().any(|status| status.label() == label) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
let Ok(count) = cells[1].parse::<usize>() else {
|
|
567
|
+
continue;
|
|
568
|
+
};
|
|
569
|
+
assert!(
|
|
570
|
+
counts.insert(label.clone(), count).is_none(),
|
|
571
|
+
"the document publishes the count for `{label}` more than once"
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
counts
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/// One claim row as the document renders it.
|
|
578
|
+
struct PublishedRow {
|
|
579
|
+
status: String,
|
|
580
|
+
evidence: String,
|
|
581
|
+
kind: String,
|
|
582
|
+
source: String,
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/// Every claim row in the document, keyed by claim id.
|
|
586
|
+
fn published_rows() -> BTreeMap<String, PublishedRow> {
|
|
587
|
+
let mut rows = BTreeMap::new();
|
|
588
|
+
for line in published_document().lines() {
|
|
589
|
+
let Some(cells) = common::table_cells(line) else {
|
|
590
|
+
continue;
|
|
591
|
+
};
|
|
592
|
+
// id | domain | claim | status | evidence | kind | limitation | source
|
|
593
|
+
if cells.len() != 8 {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
let id = cells[0].clone();
|
|
597
|
+
let looks_like_a_claim_id = id.len() >= 2
|
|
598
|
+
&& id.starts_with(|c: char| c.is_ascii_uppercase())
|
|
599
|
+
&& id[1..].chars().all(|c| c.is_ascii_digit());
|
|
600
|
+
if !looks_like_a_claim_id {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
assert!(
|
|
604
|
+
rows.insert(
|
|
605
|
+
id.clone(),
|
|
606
|
+
PublishedRow {
|
|
607
|
+
status: cells[3].clone(),
|
|
608
|
+
// An em dash is the document's rendering of "no evidence".
|
|
609
|
+
evidence: if cells[4] == "—" {
|
|
610
|
+
String::new()
|
|
611
|
+
} else {
|
|
612
|
+
cells[4].clone()
|
|
613
|
+
},
|
|
614
|
+
kind: cells[5].clone(),
|
|
615
|
+
source: cells[7].clone(),
|
|
616
|
+
},
|
|
617
|
+
)
|
|
618
|
+
.is_none(),
|
|
619
|
+
"the document publishes claim `{id}` more than once"
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
rows
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
fn known_test_functions() -> Vec<String> {
|
|
626
|
+
let mut found = Vec::new();
|
|
627
|
+
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/tests");
|
|
628
|
+
for entry in std::fs::read_dir(root).expect("the tests directory is readable") {
|
|
629
|
+
let path = entry.expect("a readable entry").path();
|
|
630
|
+
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
let source = std::fs::read_to_string(&path).expect("a readable test file");
|
|
634
|
+
for line in source.lines() {
|
|
635
|
+
if let Some(rest) = line.trim().strip_prefix("fn ") {
|
|
636
|
+
if let Some(name) = rest.split('(').next() {
|
|
637
|
+
found.push(name.to_string());
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// The server crate holds the admission and health-status tests.
|
|
643
|
+
let server = concat!(env!("CARGO_MANIFEST_DIR"), "/../feltdb-server/src/main.rs");
|
|
644
|
+
if let Ok(source) = std::fs::read_to_string(server) {
|
|
645
|
+
for line in source.lines() {
|
|
646
|
+
if let Some(rest) = line.trim().strip_prefix("fn ") {
|
|
647
|
+
if let Some(name) = rest.split('(').next() {
|
|
648
|
+
found.push(name.to_string());
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
found
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ===========================================================================
|
|
657
|
+
// Matrix integrity
|
|
658
|
+
// ===========================================================================
|
|
659
|
+
|
|
660
|
+
/// The statuses an earlier ledger records for the same claims.
|
|
661
|
+
///
|
|
662
|
+
/// Parsed rather than duplicated, so this cannot drift from the file it reads.
|
|
663
|
+
fn readiness_ledger() -> BTreeMap<String, String> {
|
|
664
|
+
let readiness = include_str!("production_readiness_contract.rs");
|
|
665
|
+
let start = readiness
|
|
666
|
+
.find("const CLAIMS")
|
|
667
|
+
.expect("the readiness contract declares CLAIMS");
|
|
668
|
+
let end = start
|
|
669
|
+
+ readiness[start..]
|
|
670
|
+
.find("];")
|
|
671
|
+
.expect("the CLAIMS table terminates");
|
|
672
|
+
let table = &readiness[start..end];
|
|
673
|
+
|
|
674
|
+
let mut ledger = BTreeMap::new();
|
|
675
|
+
let mut pending: Option<String> = None;
|
|
676
|
+
for line in table.lines() {
|
|
677
|
+
let trimmed = line.trim().trim_start_matches('(').trim();
|
|
678
|
+
if let Some(rest) = trimmed.strip_prefix('"') {
|
|
679
|
+
if let Some(id) = rest.split(' ').next() {
|
|
680
|
+
if id.len() >= 2 {
|
|
681
|
+
pending = Some(id.to_string());
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if let Some(index) = line.find("Status::") {
|
|
686
|
+
let status: String = line[index + "Status::".len()..]
|
|
687
|
+
.chars()
|
|
688
|
+
.take_while(|character| character.is_alphanumeric())
|
|
689
|
+
.collect();
|
|
690
|
+
if let Some(id) = pending.take() {
|
|
691
|
+
ledger.insert(id, status);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
ledger
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/// **Every disagreement with an earlier ledger is declared.**
|
|
699
|
+
///
|
|
700
|
+
/// This is the tripwire that makes recalculation safe. Re-deriving a claim from
|
|
701
|
+
/// current evidence is only honest if a changed answer is visible; otherwise two
|
|
702
|
+
/// files quietly assert different things and the reader believes whichever they
|
|
703
|
+
/// opened first.
|
|
704
|
+
///
|
|
705
|
+
/// It fails three ways. A claim whose status differs and is not in `DOWNGRADES`
|
|
706
|
+
/// fails. A declared downgrade whose earlier ledger no longer records the prior
|
|
707
|
+
/// status fails — so closing the gap for real forces this table to be updated
|
|
708
|
+
/// rather than left as a stale accusation. And a declared change that makes a
|
|
709
|
+
/// claim *stronger* fails, because new evidence is not a downgrade and must not
|
|
710
|
+
/// be recorded as one.
|
|
711
|
+
#[test]
|
|
712
|
+
fn every_disagreement_with_an_earlier_ledger_is_declared() {
|
|
713
|
+
let ledger = readiness_ledger();
|
|
714
|
+
let certified = matrix();
|
|
715
|
+
assert!(
|
|
716
|
+
ledger.len() > 40,
|
|
717
|
+
"the readiness ledger did not parse; it yielded {} entries",
|
|
718
|
+
ledger.len()
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
let mut problems = Vec::new();
|
|
722
|
+
|
|
723
|
+
for (id, earlier) in &ledger {
|
|
724
|
+
let Some(claim) = certified.get(id.as_str()) else {
|
|
725
|
+
continue;
|
|
726
|
+
};
|
|
727
|
+
let now = claim.status.variant();
|
|
728
|
+
if now == earlier {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
match DOWNGRADES.iter().find(|(claim_id, ..)| claim_id == id) {
|
|
732
|
+
None => problems.push(format!(
|
|
733
|
+
"{id}: the readiness ledger records `{earlier}`, this audit records `{now}`, \
|
|
734
|
+
and the disagreement is not declared"
|
|
735
|
+
)),
|
|
736
|
+
Some((_, from, to, _)) => {
|
|
737
|
+
if from != earlier {
|
|
738
|
+
problems.push(format!(
|
|
739
|
+
"{id}: declared as a downgrade from `{from}`, but the readiness ledger \
|
|
740
|
+
now records `{earlier}`"
|
|
741
|
+
));
|
|
742
|
+
}
|
|
743
|
+
if to != &now {
|
|
744
|
+
problems.push(format!(
|
|
745
|
+
"{id}: declared as a downgrade to `{to}`, but this audit records `{now}`"
|
|
746
|
+
));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
for (id, from, to, why) in DOWNGRADES {
|
|
753
|
+
let claim = certified
|
|
754
|
+
.get(id)
|
|
755
|
+
.unwrap_or_else(|| panic!("{id} is declared as a downgrade and is not in the matrix"));
|
|
756
|
+
assert_eq!(
|
|
757
|
+
claim.status.variant(),
|
|
758
|
+
*to,
|
|
759
|
+
"{id} is declared as a downgrade to `{to}`"
|
|
760
|
+
);
|
|
761
|
+
assert_eq!(
|
|
762
|
+
ledger.get(*id).map(String::as_str),
|
|
763
|
+
Some(*from),
|
|
764
|
+
"{id} is declared as a downgrade from `{from}`; the earlier ledger disagrees"
|
|
765
|
+
);
|
|
766
|
+
assert!(
|
|
767
|
+
!why.trim().is_empty(),
|
|
768
|
+
"{id} is declared as a downgrade with no reason"
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
let stronger = Status::ALL
|
|
772
|
+
.iter()
|
|
773
|
+
.find(|status| status.variant() == *from)
|
|
774
|
+
.expect("a declared prior status is a real status");
|
|
775
|
+
assert!(
|
|
776
|
+
claim.status.strength() < stronger.strength(),
|
|
777
|
+
"{id} is declared as a downgrade but `{to}` is not weaker than `{from}`"
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/// Claim identifiers are unique.
|
|
785
|
+
#[test]
|
|
786
|
+
fn every_claim_has_a_unique_identifier() {
|
|
787
|
+
assert_eq!(
|
|
788
|
+
matrix().len(),
|
|
789
|
+
CERTIFICATION.len(),
|
|
790
|
+
"a claim identifier is used twice"
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/// **Every `Proven` claim names a test that exists.**
|
|
795
|
+
///
|
|
796
|
+
/// The central tripwire. A claim cannot be `Proven` on the strength of an
|
|
797
|
+
/// implementation existing, of another claim, or of a test that was renamed or
|
|
798
|
+
/// deleted.
|
|
799
|
+
#[test]
|
|
800
|
+
fn every_proven_claim_names_a_test_that_exists() {
|
|
801
|
+
let known = known_test_functions();
|
|
802
|
+
let mut problems = Vec::new();
|
|
803
|
+
|
|
804
|
+
for claim in CERTIFICATION {
|
|
805
|
+
let proven = claim.status == Status::Proven;
|
|
806
|
+
if proven && claim.evidence.is_empty() {
|
|
807
|
+
problems.push(format!("{}: Proven with no named evidence", claim.id));
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if !claim.evidence.is_empty() && !evidence_resolves(claim, &known) {
|
|
811
|
+
problems.push(format!(
|
|
812
|
+
"{}: names `{}`, which resolves to nothing in this repository",
|
|
813
|
+
claim.id, claim.evidence
|
|
814
|
+
));
|
|
815
|
+
}
|
|
816
|
+
if proven && claim.kind == Evidence::None {
|
|
817
|
+
problems.push(format!("{}: Proven with evidence kind None", claim.id));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/// **Implementation inspection alone never carries a `Proven` behavioural
|
|
825
|
+
/// claim.**
|
|
826
|
+
///
|
|
827
|
+
/// Reading the code establishes what the code says, not what it does under a
|
|
828
|
+
/// condition nobody has run.
|
|
829
|
+
#[test]
|
|
830
|
+
fn inspection_alone_never_proves_a_behavioural_claim() {
|
|
831
|
+
for claim in CERTIFICATION {
|
|
832
|
+
if claim.kind == Evidence::ImplementationInspection {
|
|
833
|
+
assert_ne!(
|
|
834
|
+
claim.status,
|
|
835
|
+
Status::Proven,
|
|
836
|
+
"{} is Proven on implementation inspection alone: {}",
|
|
837
|
+
claim.id,
|
|
838
|
+
claim.claim
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/// Every claim that is not fully `Proven` says what is missing, and every
|
|
845
|
+
/// claim with no evidence has no evidence kind.
|
|
846
|
+
#[test]
|
|
847
|
+
fn every_claim_states_its_limitation() {
|
|
848
|
+
for claim in CERTIFICATION {
|
|
849
|
+
assert!(
|
|
850
|
+
!claim.limitation.trim().is_empty(),
|
|
851
|
+
"{} has no stated limitation",
|
|
852
|
+
claim.id
|
|
853
|
+
);
|
|
854
|
+
if claim.evidence.is_empty() {
|
|
855
|
+
assert_eq!(
|
|
856
|
+
claim.kind,
|
|
857
|
+
Evidence::None,
|
|
858
|
+
"{} names no evidence but claims a kind",
|
|
859
|
+
claim.id
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/// The certification covers every claim the readiness contract declares.
|
|
866
|
+
///
|
|
867
|
+
/// The two files are kept in step mechanically rather than by memory: a claim
|
|
868
|
+
/// added to one and forgotten in the other fails here.
|
|
869
|
+
#[test]
|
|
870
|
+
fn the_certification_covers_the_readiness_contract() {
|
|
871
|
+
let readiness = include_str!("production_readiness_contract.rs");
|
|
872
|
+
let start = readiness
|
|
873
|
+
.find("const CLAIMS")
|
|
874
|
+
.expect("the readiness contract declares CLAIMS");
|
|
875
|
+
let end = readiness[start..]
|
|
876
|
+
.find("];")
|
|
877
|
+
.map(|offset| start + offset)
|
|
878
|
+
.expect("the CLAIMS table terminates");
|
|
879
|
+
let table = &readiness[start..end];
|
|
880
|
+
|
|
881
|
+
let certified = matrix();
|
|
882
|
+
let mut missing = Vec::new();
|
|
883
|
+
for line in table.lines() {
|
|
884
|
+
let trimmed = line.trim();
|
|
885
|
+
let Some(rest) = trimmed.strip_prefix('"') else {
|
|
886
|
+
continue;
|
|
887
|
+
};
|
|
888
|
+
let Some(id) = rest.split(' ').next() else {
|
|
889
|
+
continue;
|
|
890
|
+
};
|
|
891
|
+
if id.len() >= 2 && !certified.contains_key(id) {
|
|
892
|
+
missing.push(id.to_string());
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
missing.sort();
|
|
896
|
+
missing.dedup();
|
|
897
|
+
assert!(
|
|
898
|
+
missing.is_empty(),
|
|
899
|
+
"declared in the readiness contract and absent from the certification: {missing:?}"
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// ===========================================================================
|
|
904
|
+
// Claim-inflation tripwires
|
|
905
|
+
// ===========================================================================
|
|
906
|
+
|
|
907
|
+
/// **The substitutions this certification refuses to make.**
|
|
908
|
+
///
|
|
909
|
+
/// Each pair is a real temptation, and each has been available at some point in
|
|
910
|
+
/// this work. Encoding them means a future change cannot make the stronger
|
|
911
|
+
/// claim by editing a status.
|
|
912
|
+
#[test]
|
|
913
|
+
fn no_claim_is_inflated_into_a_stronger_one() {
|
|
914
|
+
let certified = matrix();
|
|
915
|
+
let must_not_be_proven = [
|
|
916
|
+
(
|
|
917
|
+
"D5",
|
|
918
|
+
"a barrier being issued is not the hardware preserving data",
|
|
919
|
+
),
|
|
920
|
+
(
|
|
921
|
+
"E7",
|
|
922
|
+
"a configured grouping interval is not a measured speed advantage",
|
|
923
|
+
),
|
|
924
|
+
(
|
|
925
|
+
"B5",
|
|
926
|
+
"a working library primitive is not an operator workflow",
|
|
927
|
+
),
|
|
928
|
+
("R4", "detection on inspection is not proactive monitoring"),
|
|
929
|
+
("M6", "a deferred rewrite is not a non-blocking compaction"),
|
|
930
|
+
];
|
|
931
|
+
|
|
932
|
+
for (id, why) in must_not_be_proven {
|
|
933
|
+
let claim = certified
|
|
934
|
+
.get(id)
|
|
935
|
+
.unwrap_or_else(|| panic!("{id} is missing"));
|
|
936
|
+
assert_ne!(
|
|
937
|
+
claim.status,
|
|
938
|
+
Status::Proven,
|
|
939
|
+
"{id} must not be Proven: {why}"
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// State convergence must never stand in for history convergence: the claim
|
|
944
|
+
// that separates them has to remain Proven and independently evidenced.
|
|
945
|
+
let separation = certified.get("S4").expect("S4 exists");
|
|
946
|
+
assert_eq!(separation.status, Status::Proven);
|
|
947
|
+
assert_eq!(
|
|
948
|
+
separation.evidence, "same_state_with_different_history_is_detected",
|
|
949
|
+
"the negative control is what keeps every other replication claim honest"
|
|
950
|
+
);
|
|
951
|
+
assert_ne!(
|
|
952
|
+
certified.get("S1").unwrap().evidence,
|
|
953
|
+
separation.evidence,
|
|
954
|
+
"state convergence and history convergence must not rest on one test"
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/// **No document claims a guarantee the matrix does not permit.**
|
|
959
|
+
///
|
|
960
|
+
/// Documentation drifts faster than tests. These phrases would each assert
|
|
961
|
+
/// something the evidence does not support, so their absence is checked rather
|
|
962
|
+
/// than trusted.
|
|
963
|
+
#[test]
|
|
964
|
+
fn no_document_asserts_a_guarantee_the_matrix_withholds() {
|
|
965
|
+
let docs = concat!(env!("CARGO_MANIFEST_DIR"), "/../../docs/architecture");
|
|
966
|
+
let forbidden = [
|
|
967
|
+
"power-loss safe",
|
|
968
|
+
"power loss safe",
|
|
969
|
+
"guaranteed durable",
|
|
970
|
+
"compaction is non-blocking",
|
|
971
|
+
"compaction no longer blocks",
|
|
972
|
+
"all reads are bounded",
|
|
973
|
+
"supported capacity",
|
|
974
|
+
];
|
|
975
|
+
|
|
976
|
+
let mut violations = Vec::new();
|
|
977
|
+
for entry in std::fs::read_dir(docs).expect("the architecture docs are readable") {
|
|
978
|
+
let path = entry.expect("a readable entry").path();
|
|
979
|
+
if path.extension().and_then(|e| e.to_str()) != Some("md") {
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
let document = std::fs::read_to_string(&path).expect("a readable document");
|
|
983
|
+
let prose = common::prose_excluding_withheld_rows(&document, WITHHELD_LABELS);
|
|
984
|
+
for phrase in forbidden {
|
|
985
|
+
if common::asserts_phrase(&prose, phrase) {
|
|
986
|
+
violations.push(format!(
|
|
987
|
+
"{}: asserts \"{phrase}\"",
|
|
988
|
+
path.file_name().unwrap().to_string_lossy()
|
|
989
|
+
));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
assert!(violations.is_empty(), "{}", violations.join("\n"));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// ===========================================================================
|
|
997
|
+
// Derived counts
|
|
998
|
+
// ===========================================================================
|
|
999
|
+
|
|
1000
|
+
/// Counts are computed from the matrix, never maintained by hand.
|
|
1001
|
+
///
|
|
1002
|
+
/// The numbers below are the certification's published totals. They are
|
|
1003
|
+
/// asserted so the document and the table cannot disagree.
|
|
1004
|
+
#[test]
|
|
1005
|
+
fn the_published_counts_are_derived_from_the_matrix() {
|
|
1006
|
+
let mut derived: BTreeMap<&str, usize> = BTreeMap::new();
|
|
1007
|
+
for status in Status::ALL {
|
|
1008
|
+
derived.insert(status.label(), 0);
|
|
1009
|
+
}
|
|
1010
|
+
for claim in CERTIFICATION {
|
|
1011
|
+
*derived.entry(claim.status.label()).or_default() += 1;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
let total: usize = derived.values().sum();
|
|
1015
|
+
assert_eq!(
|
|
1016
|
+
total,
|
|
1017
|
+
CERTIFICATION.len(),
|
|
1018
|
+
"every claim has exactly one status"
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
// The document may not carry a total of its own. Each count it publishes is
|
|
1022
|
+
// checked against the count computed here, and every status must appear —
|
|
1023
|
+
// so a bucket cannot be quietly dropped from the published summary because
|
|
1024
|
+
// it is inconvenient.
|
|
1025
|
+
let published = published_counts();
|
|
1026
|
+
for (label, count) in &derived {
|
|
1027
|
+
let shown = published.get(*label).copied().unwrap_or_else(|| {
|
|
1028
|
+
panic!("the certification document publishes no count for `{label}`")
|
|
1029
|
+
});
|
|
1030
|
+
assert_eq!(
|
|
1031
|
+
shown, *count,
|
|
1032
|
+
"the document publishes {shown} `{label}` claims; the matrix holds {count}"
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
assert_eq!(
|
|
1036
|
+
published.len(),
|
|
1037
|
+
derived.len(),
|
|
1038
|
+
"the document publishes a status the matrix does not define"
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/// The published matrix is a rendering of the executable one, not a second
|
|
1043
|
+
/// record of the same facts.
|
|
1044
|
+
///
|
|
1045
|
+
/// Every claim must appear in the document with the same status, the same
|
|
1046
|
+
/// evidence and the same source. A downgrade that lands in the test file and
|
|
1047
|
+
/// not in the document — or the reverse — fails here.
|
|
1048
|
+
#[test]
|
|
1049
|
+
fn the_published_matrix_matches_the_executable_matrix() {
|
|
1050
|
+
let rows = published_rows();
|
|
1051
|
+
let mut problems = Vec::new();
|
|
1052
|
+
|
|
1053
|
+
for claim in CERTIFICATION {
|
|
1054
|
+
match rows.get(claim.id) {
|
|
1055
|
+
None => problems.push(format!("{}: absent from the document", claim.id)),
|
|
1056
|
+
Some(row) => {
|
|
1057
|
+
if row.status != claim.status.label() {
|
|
1058
|
+
problems.push(format!(
|
|
1059
|
+
"{}: the document says `{}`, the matrix says `{}`",
|
|
1060
|
+
claim.id,
|
|
1061
|
+
row.status,
|
|
1062
|
+
claim.status.label()
|
|
1063
|
+
));
|
|
1064
|
+
}
|
|
1065
|
+
if row.evidence != claim.evidence {
|
|
1066
|
+
problems.push(format!(
|
|
1067
|
+
"{}: the document cites `{}`, the matrix cites `{}`",
|
|
1068
|
+
claim.id, row.evidence, claim.evidence
|
|
1069
|
+
));
|
|
1070
|
+
}
|
|
1071
|
+
if row.kind != claim.kind.label() {
|
|
1072
|
+
problems.push(format!(
|
|
1073
|
+
"{}: the document types the evidence `{}`, the matrix types it `{}`",
|
|
1074
|
+
claim.id,
|
|
1075
|
+
row.kind,
|
|
1076
|
+
claim.kind.label()
|
|
1077
|
+
));
|
|
1078
|
+
}
|
|
1079
|
+
if row.source != claim.source {
|
|
1080
|
+
problems.push(format!(
|
|
1081
|
+
"{}: the document sources `{}`, the matrix sources `{}`",
|
|
1082
|
+
claim.id, row.source, claim.source
|
|
1083
|
+
));
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
for id in rows.keys() {
|
|
1090
|
+
if !CERTIFICATION.iter().any(|claim| &claim.id == id) {
|
|
1091
|
+
problems.push(format!("{id}: in the document, absent from the matrix"));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/// Every domain the certification covers is represented, and the managed
|
|
1099
|
+
/// incident is among them.
|
|
1100
|
+
#[test]
|
|
1101
|
+
fn every_domain_is_represented_including_the_managed_incident() {
|
|
1102
|
+
let mut domains: BTreeMap<&str, usize> = BTreeMap::new();
|
|
1103
|
+
for claim in CERTIFICATION {
|
|
1104
|
+
*domains.entry(claim.domain).or_default() += 1;
|
|
1105
|
+
}
|
|
1106
|
+
for domain in [
|
|
1107
|
+
"Durability",
|
|
1108
|
+
"Atomicity",
|
|
1109
|
+
"Recovery",
|
|
1110
|
+
"History",
|
|
1111
|
+
"Backup",
|
|
1112
|
+
"Replication",
|
|
1113
|
+
"Envelope",
|
|
1114
|
+
"Format",
|
|
1115
|
+
"Observability",
|
|
1116
|
+
"Managed incident",
|
|
1117
|
+
] {
|
|
1118
|
+
assert!(
|
|
1119
|
+
domains.contains_key(domain),
|
|
1120
|
+
"the certification has no {domain} claims"
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
assert_eq!(
|
|
1124
|
+
domains.get("Managed incident").copied().unwrap_or(0),
|
|
1125
|
+
9,
|
|
1126
|
+
"the incident remediation shipped with tests and no matrix entry; it has one now, \
|
|
1127
|
+
plus the two composed-shape claims this audit added"
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/// The intentional gaps are still gaps.
|
|
1132
|
+
///
|
|
1133
|
+
/// A certification whose Unproven claims quietly disappear is worse than one
|
|
1134
|
+
/// that keeps them. Each of these must remain present and unproven until real
|
|
1135
|
+
/// evidence exists.
|
|
1136
|
+
#[test]
|
|
1137
|
+
fn the_known_gaps_have_not_disappeared() {
|
|
1138
|
+
let certified = matrix();
|
|
1139
|
+
for id in ["D5", "E7", "B5", "R4", "M6"] {
|
|
1140
|
+
let claim = certified
|
|
1141
|
+
.get(id)
|
|
1142
|
+
.unwrap_or_else(|| panic!("{id} was removed from the certification"));
|
|
1143
|
+
assert_eq!(
|
|
1144
|
+
claim.status,
|
|
1145
|
+
Status::Unproven,
|
|
1146
|
+
"{id} changed status without new evidence"
|
|
1147
|
+
);
|
|
1148
|
+
assert!(
|
|
1149
|
+
claim.evidence.is_empty(),
|
|
1150
|
+
"{id} names evidence but is still Unproven"
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
}
|