@knolo/cli 6.1.0 → 6.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -4
- package/bin/cortex.mjs +125 -0
- package/bin/flight.mjs +367 -0
- package/bin/knolo.mjs +36 -0
- package/bin/skill.mjs +698 -0
- package/package.json +3 -3
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/closure.rs +395 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/cover.rs +692 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/evaluate.rs +343 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/json.rs +486 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/mod.rs +14 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/model.rs +1068 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/kar/session.rs +1156 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/lib.rs +7 -1
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
//! Plan, graph, and anchor validation for the frozen `kar-1-research-1` evaluator.
|
|
2
|
+
//! Indexes are execution-only. They are not hashed.
|
|
3
|
+
|
|
4
|
+
use crate::kar::json::{digest_json, utf16_cmp, Json};
|
|
5
|
+
|
|
6
|
+
pub const KAR_VERSION: &str = "kar-1-research-1";
|
|
7
|
+
pub const KAR_API: &str = "experimental";
|
|
8
|
+
pub const FRONTIERS: [&str; 5] = ["F_S", "F_O", "F_Q", "F_T", "F_A"];
|
|
9
|
+
const PROFILES: [&str; 3] = [
|
|
10
|
+
"minimum-cover",
|
|
11
|
+
"minimum-cover-redundancy-v1",
|
|
12
|
+
"exp1-lexicographic",
|
|
13
|
+
];
|
|
14
|
+
const ROOT: &str = "sha256-";
|
|
15
|
+
/// `Number.MAX_SAFE_INTEGER`. `Number.isSafeInteger` rejects the next value.
|
|
16
|
+
const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Clone)]
|
|
19
|
+
pub struct Bounds {
|
|
20
|
+
pub max_anchor_nodes: i64,
|
|
21
|
+
pub max_closure_nodes: i64,
|
|
22
|
+
pub max_closure_edges: i64,
|
|
23
|
+
pub max_frontier_evidence: i64,
|
|
24
|
+
pub max_requirements_per_frontier: i64,
|
|
25
|
+
pub max_evidence_bindings: i64,
|
|
26
|
+
pub max_cover_visits: i64,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[derive(Debug, Clone)]
|
|
30
|
+
pub struct WitnessRecord {
|
|
31
|
+
pub node_id: String,
|
|
32
|
+
pub query_term: Option<String>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[derive(Debug, Clone)]
|
|
36
|
+
pub enum AnchorSpec {
|
|
37
|
+
Recompute { procedure: String },
|
|
38
|
+
Supplied { witness: Vec<WitnessRecord> },
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[derive(Debug, Clone)]
|
|
42
|
+
pub struct Lexical {
|
|
43
|
+
pub frontier: String,
|
|
44
|
+
pub evidence_ids: Vec<String>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[derive(Debug, Clone)]
|
|
48
|
+
pub struct Plan {
|
|
49
|
+
pub anchor: AnchorSpec,
|
|
50
|
+
pub frontier_map: Vec<(String, String)>,
|
|
51
|
+
pub depth: i64,
|
|
52
|
+
pub cardinality_bound: i64,
|
|
53
|
+
pub coverage_mode: String,
|
|
54
|
+
pub requirements: Vec<(String, Vec<String>)>,
|
|
55
|
+
pub floors: Vec<(String, String)>,
|
|
56
|
+
pub profile: String,
|
|
57
|
+
pub as_of: String,
|
|
58
|
+
pub min_authority: Option<i64>,
|
|
59
|
+
pub bounds: Bounds,
|
|
60
|
+
pub lexical: Option<Lexical>,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[derive(Debug, Clone)]
|
|
64
|
+
pub struct Relation {
|
|
65
|
+
pub id: String,
|
|
66
|
+
pub from: String,
|
|
67
|
+
pub relation: String,
|
|
68
|
+
pub to: String,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[derive(Debug, Clone)]
|
|
72
|
+
pub struct Binding {
|
|
73
|
+
pub id: String,
|
|
74
|
+
pub node_id: String,
|
|
75
|
+
pub evidence_id: String,
|
|
76
|
+
pub requirements: Vec<String>,
|
|
77
|
+
pub authority: Option<i64>,
|
|
78
|
+
pub unauthorized: bool,
|
|
79
|
+
pub valid_from: Option<String>,
|
|
80
|
+
pub valid_until: Option<String>,
|
|
81
|
+
pub provenance: Option<String>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[derive(Debug, Clone)]
|
|
85
|
+
pub struct Graph {
|
|
86
|
+
pub knowledge_root: String,
|
|
87
|
+
pub producer: String,
|
|
88
|
+
pub note: Option<String>,
|
|
89
|
+
pub nodes: Vec<String>,
|
|
90
|
+
pub relations: Vec<Relation>,
|
|
91
|
+
pub bindings: Vec<Binding>,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[derive(Debug, Clone)]
|
|
95
|
+
pub struct PreparedImage {
|
|
96
|
+
pub knowledge_root: String,
|
|
97
|
+
pub texts: Vec<(String, String)>,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[derive(Debug, Clone)]
|
|
101
|
+
pub struct Indexes {
|
|
102
|
+
pub relations_by_from: Vec<(String, Vec<Relation>)>,
|
|
103
|
+
pub bindings_by_node: Vec<(String, Vec<Binding>)>,
|
|
104
|
+
pub bindings_by_evidence: Vec<(String, Vec<Binding>)>,
|
|
105
|
+
pub node_ids: Vec<String>,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[derive(Debug, Clone)]
|
|
109
|
+
pub struct AnchorCommitment {
|
|
110
|
+
pub mode: Option<String>,
|
|
111
|
+
pub procedure: Option<String>,
|
|
112
|
+
pub witness: Option<Vec<WitnessRecord>>,
|
|
113
|
+
pub nodes: Vec<String>,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
impl AnchorCommitment {
|
|
117
|
+
pub fn empty() -> Self {
|
|
118
|
+
Self {
|
|
119
|
+
mode: None,
|
|
120
|
+
procedure: None,
|
|
121
|
+
witness: None,
|
|
122
|
+
nodes: Vec::new(),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pub fn to_json(&self) -> Json {
|
|
127
|
+
let witness = match &self.witness {
|
|
128
|
+
None => Json::Null,
|
|
129
|
+
Some(rows) => Json::Array(rows.iter().map(WitnessRecord::to_json).collect()),
|
|
130
|
+
};
|
|
131
|
+
Json::object(vec![
|
|
132
|
+
("mode", opt_string(&self.mode)),
|
|
133
|
+
(
|
|
134
|
+
"nodes",
|
|
135
|
+
Json::Array(self.nodes.iter().cloned().map(Json::String).collect()),
|
|
136
|
+
),
|
|
137
|
+
("procedure", opt_string(&self.procedure)),
|
|
138
|
+
("witness", witness),
|
|
139
|
+
])
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
impl WitnessRecord {
|
|
144
|
+
fn to_json(&self) -> Json {
|
|
145
|
+
let mut pairs = vec![("nodeId", Json::String(self.node_id.clone()))];
|
|
146
|
+
if let Some(term) = &self.query_term {
|
|
147
|
+
pairs.push(("queryTerm", Json::String(term.clone())));
|
|
148
|
+
}
|
|
149
|
+
Json::Object(
|
|
150
|
+
pairs
|
|
151
|
+
.into_iter()
|
|
152
|
+
.map(|(key, value)| (key.to_string(), value))
|
|
153
|
+
.collect(),
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn opt_string(value: &Option<String>) -> Json {
|
|
159
|
+
match value {
|
|
160
|
+
Some(text) => Json::String(text.clone()),
|
|
161
|
+
None => Json::Null,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pub fn is_root(value: &str) -> bool {
|
|
166
|
+
value.starts_with(ROOT)
|
|
167
|
+
&& value.len() == ROOT.len() + 64
|
|
168
|
+
&& value.as_bytes()[ROOT.len()..]
|
|
169
|
+
.iter()
|
|
170
|
+
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pub fn is_frontier(value: &str) -> bool {
|
|
174
|
+
FRONTIERS.contains(&value)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
pub fn prepare_image(evidence: Vec<(String, String)>) -> PreparedImage {
|
|
178
|
+
let mut rows = evidence;
|
|
179
|
+
rows.sort_by(|left, right| utf16_cmp(&left.0, &right.0));
|
|
180
|
+
let image = Json::object(vec![
|
|
181
|
+
(
|
|
182
|
+
"evidence",
|
|
183
|
+
Json::Array(
|
|
184
|
+
rows.iter()
|
|
185
|
+
.map(|(id, text)| {
|
|
186
|
+
Json::object(vec![
|
|
187
|
+
("id", Json::String(id.clone())),
|
|
188
|
+
("text", Json::String(text.clone())),
|
|
189
|
+
])
|
|
190
|
+
})
|
|
191
|
+
.collect(),
|
|
192
|
+
),
|
|
193
|
+
),
|
|
194
|
+
("version", Json::Int(1)),
|
|
195
|
+
]);
|
|
196
|
+
PreparedImage {
|
|
197
|
+
knowledge_root: digest_json(&image),
|
|
198
|
+
texts: rows,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
pub fn prepare_graph(input: &Json) -> Option<Graph> {
|
|
203
|
+
let raw = input.as_object()?;
|
|
204
|
+
if !only_keys(
|
|
205
|
+
raw,
|
|
206
|
+
&[
|
|
207
|
+
"version",
|
|
208
|
+
"knowledgeRoot",
|
|
209
|
+
"provenance",
|
|
210
|
+
"nodes",
|
|
211
|
+
"relations",
|
|
212
|
+
"bindings",
|
|
213
|
+
],
|
|
214
|
+
) {
|
|
215
|
+
return None;
|
|
216
|
+
}
|
|
217
|
+
if input.get("version")?.as_int() != Some(1) {
|
|
218
|
+
return None;
|
|
219
|
+
}
|
|
220
|
+
let knowledge_root = input.get("knowledgeRoot")?.as_str()?;
|
|
221
|
+
if !is_root(knowledge_root) {
|
|
222
|
+
return None;
|
|
223
|
+
}
|
|
224
|
+
let provenance_raw = input.get("provenance")?.as_object()?;
|
|
225
|
+
if !only_keys(provenance_raw, &["producer", "note"]) {
|
|
226
|
+
return None;
|
|
227
|
+
}
|
|
228
|
+
let producer = input.get("provenance")?.get("producer")?.as_str()?;
|
|
229
|
+
if producer.is_empty() {
|
|
230
|
+
return None;
|
|
231
|
+
}
|
|
232
|
+
let note = match input.get("provenance")?.get("note") {
|
|
233
|
+
None => None,
|
|
234
|
+
Some(Json::String(value)) => Some(value.clone()),
|
|
235
|
+
Some(_) => return None,
|
|
236
|
+
};
|
|
237
|
+
let nodes_json = match input.get("nodes")? {
|
|
238
|
+
Json::Array(items) => items,
|
|
239
|
+
_ => return None,
|
|
240
|
+
};
|
|
241
|
+
let mut nodes = Vec::new();
|
|
242
|
+
for item in nodes_json {
|
|
243
|
+
let row = item.as_object()?;
|
|
244
|
+
if !only_keys(row, &["id"]) {
|
|
245
|
+
return None;
|
|
246
|
+
}
|
|
247
|
+
let id = item.get("id")?.as_str()?;
|
|
248
|
+
if id.is_empty() || nodes.iter().any(|existing| existing == id) {
|
|
249
|
+
return None;
|
|
250
|
+
}
|
|
251
|
+
nodes.push(id.to_string());
|
|
252
|
+
}
|
|
253
|
+
let node_set = nodes.clone();
|
|
254
|
+
let relations_json = match input.get("relations")? {
|
|
255
|
+
Json::Array(items) => items,
|
|
256
|
+
_ => return None,
|
|
257
|
+
};
|
|
258
|
+
let mut relations = Vec::new();
|
|
259
|
+
for item in relations_json {
|
|
260
|
+
let row = item.as_object()?;
|
|
261
|
+
if !only_keys(row, &["id", "from", "relation", "to"]) {
|
|
262
|
+
return None;
|
|
263
|
+
}
|
|
264
|
+
let id = item.get("id")?.as_str()?;
|
|
265
|
+
let from = item.get("from")?.as_str()?;
|
|
266
|
+
let relation = item.get("relation")?.as_str()?;
|
|
267
|
+
let to = item.get("to")?.as_str()?;
|
|
268
|
+
if id.is_empty()
|
|
269
|
+
|| relation.is_empty()
|
|
270
|
+
|| relations
|
|
271
|
+
.iter()
|
|
272
|
+
.any(|existing: &Relation| existing.id == id)
|
|
273
|
+
{
|
|
274
|
+
return None;
|
|
275
|
+
}
|
|
276
|
+
if !node_set.iter().any(|node| node == from) || !node_set.iter().any(|node| node == to) {
|
|
277
|
+
return None;
|
|
278
|
+
}
|
|
279
|
+
relations.push(Relation {
|
|
280
|
+
id: id.to_string(),
|
|
281
|
+
from: from.to_string(),
|
|
282
|
+
relation: relation.to_string(),
|
|
283
|
+
to: to.to_string(),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
let bindings_json = match input.get("bindings")? {
|
|
287
|
+
Json::Array(items) => items,
|
|
288
|
+
_ => return None,
|
|
289
|
+
};
|
|
290
|
+
let mut bindings = Vec::new();
|
|
291
|
+
for item in bindings_json {
|
|
292
|
+
bindings.push(normalize_binding(item, &node_set, &bindings)?);
|
|
293
|
+
}
|
|
294
|
+
nodes.sort_by(|left, right| utf16_cmp(left, right));
|
|
295
|
+
relations.sort_by(|left, right| utf16_cmp(&left.id, &right.id));
|
|
296
|
+
bindings.sort_by(|left, right| utf16_cmp(&left.id, &right.id));
|
|
297
|
+
Some(Graph {
|
|
298
|
+
knowledge_root: knowledge_root.to_string(),
|
|
299
|
+
producer: producer.to_string(),
|
|
300
|
+
note,
|
|
301
|
+
nodes,
|
|
302
|
+
relations,
|
|
303
|
+
bindings,
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
fn normalize_binding(item: &Json, nodes: &[String], bindings: &[Binding]) -> Option<Binding> {
|
|
308
|
+
let row = item.as_object()?;
|
|
309
|
+
if !only_keys(
|
|
310
|
+
row,
|
|
311
|
+
&[
|
|
312
|
+
"id",
|
|
313
|
+
"nodeId",
|
|
314
|
+
"evidenceId",
|
|
315
|
+
"requirements",
|
|
316
|
+
"authority",
|
|
317
|
+
"unauthorized",
|
|
318
|
+
"validFrom",
|
|
319
|
+
"validUntil",
|
|
320
|
+
"provenance",
|
|
321
|
+
],
|
|
322
|
+
) {
|
|
323
|
+
return None;
|
|
324
|
+
}
|
|
325
|
+
let id = item.get("id")?.as_str()?;
|
|
326
|
+
let node_id = item.get("nodeId")?.as_str()?;
|
|
327
|
+
let evidence_id = item.get("evidenceId")?.as_str()?;
|
|
328
|
+
if id.is_empty()
|
|
329
|
+
|| evidence_id.is_empty()
|
|
330
|
+
|| bindings.iter().any(|existing| existing.id == id)
|
|
331
|
+
|| !nodes.iter().any(|node| node == node_id)
|
|
332
|
+
{
|
|
333
|
+
return None;
|
|
334
|
+
}
|
|
335
|
+
let requirements_json = match item.get("requirements")? {
|
|
336
|
+
Json::Array(items) => items,
|
|
337
|
+
_ => return None,
|
|
338
|
+
};
|
|
339
|
+
let mut requirements = Vec::new();
|
|
340
|
+
for requirement in requirements_json {
|
|
341
|
+
let text = requirement.as_str()?;
|
|
342
|
+
if text.is_empty() || requirements.iter().any(|existing| existing == text) {
|
|
343
|
+
return None;
|
|
344
|
+
}
|
|
345
|
+
requirements.push(text.to_string());
|
|
346
|
+
}
|
|
347
|
+
requirements.sort_by(|left, right| utf16_cmp(left, right));
|
|
348
|
+
let authority = match item.get("authority") {
|
|
349
|
+
None => None,
|
|
350
|
+
Some(Json::Int(value)) => Some(*value),
|
|
351
|
+
Some(_) => return None,
|
|
352
|
+
};
|
|
353
|
+
let unauthorized = match item.get("unauthorized") {
|
|
354
|
+
None => false,
|
|
355
|
+
Some(Json::Bool(true)) => true,
|
|
356
|
+
Some(Json::Bool(false)) => false,
|
|
357
|
+
Some(_) => return None,
|
|
358
|
+
};
|
|
359
|
+
let valid_from = optional_date(item.get("validFrom"))?;
|
|
360
|
+
let valid_until = optional_date(item.get("validUntil"))?;
|
|
361
|
+
let provenance = match item.get("provenance") {
|
|
362
|
+
None => None,
|
|
363
|
+
Some(Json::String(value)) if !value.is_empty() => Some(value.clone()),
|
|
364
|
+
Some(_) => return None,
|
|
365
|
+
};
|
|
366
|
+
Some(Binding {
|
|
367
|
+
id: id.to_string(),
|
|
368
|
+
node_id: node_id.to_string(),
|
|
369
|
+
evidence_id: evidence_id.to_string(),
|
|
370
|
+
requirements,
|
|
371
|
+
authority,
|
|
372
|
+
unauthorized,
|
|
373
|
+
valid_from,
|
|
374
|
+
valid_until,
|
|
375
|
+
provenance,
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fn optional_date(value: Option<&Json>) -> Option<Option<String>> {
|
|
380
|
+
match value {
|
|
381
|
+
None => Some(None),
|
|
382
|
+
Some(Json::String(text)) if is_date(text) => Some(Some(text.clone())),
|
|
383
|
+
Some(_) => None,
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
fn is_date(value: &str) -> bool {
|
|
388
|
+
let bytes = value.as_bytes();
|
|
389
|
+
bytes.len() == 10
|
|
390
|
+
&& bytes[4] == b'-'
|
|
391
|
+
&& bytes[7] == b'-'
|
|
392
|
+
&& bytes
|
|
393
|
+
.iter()
|
|
394
|
+
.enumerate()
|
|
395
|
+
.all(|(index, byte)| index == 4 || index == 7 || byte.is_ascii_digit())
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
pub fn graph_json(graph: &Graph) -> Json {
|
|
399
|
+
let mut provenance = vec![("producer".to_string(), Json::String(graph.producer.clone()))];
|
|
400
|
+
if let Some(note) = &graph.note {
|
|
401
|
+
provenance.push(("note".to_string(), Json::String(note.clone())));
|
|
402
|
+
}
|
|
403
|
+
Json::object(vec![
|
|
404
|
+
(
|
|
405
|
+
"bindings",
|
|
406
|
+
Json::Array(graph.bindings.iter().map(binding_json).collect()),
|
|
407
|
+
),
|
|
408
|
+
("knowledgeRoot", Json::String(graph.knowledge_root.clone())),
|
|
409
|
+
(
|
|
410
|
+
"nodes",
|
|
411
|
+
Json::Array(
|
|
412
|
+
graph
|
|
413
|
+
.nodes
|
|
414
|
+
.iter()
|
|
415
|
+
.cloned()
|
|
416
|
+
.map(|id| Json::object(vec![("id", Json::String(id))]))
|
|
417
|
+
.collect(),
|
|
418
|
+
),
|
|
419
|
+
),
|
|
420
|
+
("provenance", Json::Object(provenance)),
|
|
421
|
+
(
|
|
422
|
+
"relations",
|
|
423
|
+
Json::Array(graph.relations.iter().map(relation_json).collect()),
|
|
424
|
+
),
|
|
425
|
+
("version", Json::Int(1)),
|
|
426
|
+
])
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
fn relation_json(relation: &Relation) -> Json {
|
|
430
|
+
Json::object(vec![
|
|
431
|
+
("from", Json::String(relation.from.clone())),
|
|
432
|
+
("id", Json::String(relation.id.clone())),
|
|
433
|
+
("relation", Json::String(relation.relation.clone())),
|
|
434
|
+
("to", Json::String(relation.to.clone())),
|
|
435
|
+
])
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
fn binding_json(binding: &Binding) -> Json {
|
|
439
|
+
let mut pairs = vec![
|
|
440
|
+
(
|
|
441
|
+
"evidenceId".to_string(),
|
|
442
|
+
Json::String(binding.evidence_id.clone()),
|
|
443
|
+
),
|
|
444
|
+
("id".to_string(), Json::String(binding.id.clone())),
|
|
445
|
+
("nodeId".to_string(), Json::String(binding.node_id.clone())),
|
|
446
|
+
(
|
|
447
|
+
"requirements".to_string(),
|
|
448
|
+
Json::Array(
|
|
449
|
+
binding
|
|
450
|
+
.requirements
|
|
451
|
+
.iter()
|
|
452
|
+
.cloned()
|
|
453
|
+
.map(Json::String)
|
|
454
|
+
.collect(),
|
|
455
|
+
),
|
|
456
|
+
),
|
|
457
|
+
];
|
|
458
|
+
if let Some(authority) = binding.authority {
|
|
459
|
+
pairs.push(("authority".to_string(), Json::Int(authority)));
|
|
460
|
+
}
|
|
461
|
+
if binding.unauthorized {
|
|
462
|
+
pairs.push(("unauthorized".to_string(), Json::Bool(true)));
|
|
463
|
+
}
|
|
464
|
+
if let Some(valid_from) = &binding.valid_from {
|
|
465
|
+
pairs.push(("validFrom".to_string(), Json::String(valid_from.clone())));
|
|
466
|
+
}
|
|
467
|
+
if let Some(valid_until) = &binding.valid_until {
|
|
468
|
+
pairs.push(("validUntil".to_string(), Json::String(valid_until.clone())));
|
|
469
|
+
}
|
|
470
|
+
if let Some(provenance) = &binding.provenance {
|
|
471
|
+
pairs.push(("provenance".to_string(), Json::String(provenance.clone())));
|
|
472
|
+
}
|
|
473
|
+
Json::Object(pairs)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
pub fn semantic_root_of(graph: &Graph) -> String {
|
|
477
|
+
digest_json(&Json::object(vec![
|
|
478
|
+
("graph", graph_json(graph)),
|
|
479
|
+
("knowledgeRoot", Json::String(graph.knowledge_root.clone())),
|
|
480
|
+
]))
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
pub fn unresolved_evidence(graph: &Graph, texts: &[(String, String)]) -> bool {
|
|
484
|
+
graph
|
|
485
|
+
.bindings
|
|
486
|
+
.iter()
|
|
487
|
+
.any(|binding| texts.iter().all(|(id, _)| id != &binding.evidence_id))
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
pub fn build_indexes(graph: &Graph) -> Indexes {
|
|
491
|
+
let mut relations_by_from: Vec<(String, Vec<Relation>)> = Vec::new();
|
|
492
|
+
for relation in &graph.relations {
|
|
493
|
+
push_group(&mut relations_by_from, &relation.from, relation.clone());
|
|
494
|
+
}
|
|
495
|
+
for (_, list) in &mut relations_by_from {
|
|
496
|
+
list.sort_by(|left, right| utf16_cmp(&left.id, &right.id));
|
|
497
|
+
}
|
|
498
|
+
let mut bindings_by_node: Vec<(String, Vec<Binding>)> = Vec::new();
|
|
499
|
+
let mut bindings_by_evidence: Vec<(String, Vec<Binding>)> = Vec::new();
|
|
500
|
+
for binding in &graph.bindings {
|
|
501
|
+
push_group(&mut bindings_by_node, &binding.node_id, binding.clone());
|
|
502
|
+
push_group(
|
|
503
|
+
&mut bindings_by_evidence,
|
|
504
|
+
&binding.evidence_id,
|
|
505
|
+
binding.clone(),
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
Indexes {
|
|
509
|
+
relations_by_from,
|
|
510
|
+
bindings_by_node,
|
|
511
|
+
bindings_by_evidence,
|
|
512
|
+
node_ids: graph.nodes.clone(),
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
fn push_group<T>(groups: &mut Vec<(String, Vec<T>)>, key: &str, value: T) {
|
|
517
|
+
if let Some((_, list)) = groups.iter_mut().find(|(name, _)| name == key) {
|
|
518
|
+
list.push(value);
|
|
519
|
+
} else {
|
|
520
|
+
groups.push((key.to_string(), vec![value]));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
pub fn group<'a, T>(groups: &'a [(String, Vec<T>)], key: &str) -> &'a [T] {
|
|
525
|
+
groups
|
|
526
|
+
.iter()
|
|
527
|
+
.find(|(name, _)| name == key)
|
|
528
|
+
.map(|(_, list)| list.as_slice())
|
|
529
|
+
.unwrap_or(&[])
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
pub fn validate_plan(input: &Json) -> Option<Plan> {
|
|
533
|
+
let raw = input.as_object()?;
|
|
534
|
+
if !only_keys(
|
|
535
|
+
raw,
|
|
536
|
+
&[
|
|
537
|
+
"version",
|
|
538
|
+
"anchor",
|
|
539
|
+
"frontierMap",
|
|
540
|
+
"depth",
|
|
541
|
+
"cardinalityBound",
|
|
542
|
+
"coverageMode",
|
|
543
|
+
"requirements",
|
|
544
|
+
"floors",
|
|
545
|
+
"profile",
|
|
546
|
+
"asOf",
|
|
547
|
+
"minAuthority",
|
|
548
|
+
"bounds",
|
|
549
|
+
"lexical",
|
|
550
|
+
],
|
|
551
|
+
) {
|
|
552
|
+
return None;
|
|
553
|
+
}
|
|
554
|
+
if input.get("version")?.as_int() != Some(1) {
|
|
555
|
+
return None;
|
|
556
|
+
}
|
|
557
|
+
let depth = non_negative(input.get("depth")?)?;
|
|
558
|
+
let cardinality_bound = non_negative(input.get("cardinalityBound")?)?;
|
|
559
|
+
let coverage_mode = input.get("coverageMode")?.as_str()?;
|
|
560
|
+
if coverage_mode != "requirements" && coverage_mode != "nonempty" {
|
|
561
|
+
return None;
|
|
562
|
+
}
|
|
563
|
+
let profile = input.get("profile")?.as_str()?;
|
|
564
|
+
if !PROFILES.contains(&profile) {
|
|
565
|
+
return None;
|
|
566
|
+
}
|
|
567
|
+
let as_of = input.get("asOf")?.as_str()?;
|
|
568
|
+
if !is_date(as_of) {
|
|
569
|
+
return None;
|
|
570
|
+
}
|
|
571
|
+
let min_authority = match input.get("minAuthority")? {
|
|
572
|
+
Json::Null => None,
|
|
573
|
+
Json::Int(value) if (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(value) => Some(*value),
|
|
574
|
+
_ => return None,
|
|
575
|
+
};
|
|
576
|
+
let anchor = validate_anchor(input.get("anchor")?)?;
|
|
577
|
+
let frontier_map = validate_frontier_map(input.get("frontierMap")?)?;
|
|
578
|
+
let requirements = validate_frontier_lists(input.get("requirements")?)?;
|
|
579
|
+
let floors = validate_floors(input.get("floors")?)?;
|
|
580
|
+
let bounds = validate_bounds(input.get("bounds")?)?;
|
|
581
|
+
let lexical = validate_lexical(input.get("lexical")?)?;
|
|
582
|
+
if requirements
|
|
583
|
+
.iter()
|
|
584
|
+
.any(|(_, list)| list.len() > bounds.max_requirements_per_frontier as usize)
|
|
585
|
+
{
|
|
586
|
+
return None;
|
|
587
|
+
}
|
|
588
|
+
Some(Plan {
|
|
589
|
+
anchor,
|
|
590
|
+
frontier_map,
|
|
591
|
+
depth,
|
|
592
|
+
cardinality_bound,
|
|
593
|
+
coverage_mode: coverage_mode.to_string(),
|
|
594
|
+
requirements,
|
|
595
|
+
floors,
|
|
596
|
+
profile: profile.to_string(),
|
|
597
|
+
as_of: as_of.to_string(),
|
|
598
|
+
min_authority,
|
|
599
|
+
bounds,
|
|
600
|
+
lexical,
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
pub fn plan_json(plan: &Plan) -> Json {
|
|
605
|
+
let anchor = match &plan.anchor {
|
|
606
|
+
AnchorSpec::Recompute { procedure } => Json::object(vec![
|
|
607
|
+
("mode", Json::String("recompute".into())),
|
|
608
|
+
("procedure", Json::String(procedure.clone())),
|
|
609
|
+
]),
|
|
610
|
+
AnchorSpec::Supplied { witness } => Json::object(vec![
|
|
611
|
+
("mode", Json::String("supplied".into())),
|
|
612
|
+
(
|
|
613
|
+
"witness",
|
|
614
|
+
Json::Array(witness.iter().map(WitnessRecord::to_json).collect()),
|
|
615
|
+
),
|
|
616
|
+
]),
|
|
617
|
+
};
|
|
618
|
+
let lexical = match &plan.lexical {
|
|
619
|
+
None => Json::Null,
|
|
620
|
+
Some(lexical) => Json::object(vec![
|
|
621
|
+
(
|
|
622
|
+
"evidenceIds",
|
|
623
|
+
Json::Array(
|
|
624
|
+
lexical
|
|
625
|
+
.evidence_ids
|
|
626
|
+
.iter()
|
|
627
|
+
.cloned()
|
|
628
|
+
.map(Json::String)
|
|
629
|
+
.collect(),
|
|
630
|
+
),
|
|
631
|
+
),
|
|
632
|
+
("frontier", Json::String(lexical.frontier.clone())),
|
|
633
|
+
]),
|
|
634
|
+
};
|
|
635
|
+
Json::object(vec![
|
|
636
|
+
("anchor", anchor),
|
|
637
|
+
("asOf", Json::String(plan.as_of.clone())),
|
|
638
|
+
(
|
|
639
|
+
"bounds",
|
|
640
|
+
Json::object(vec![
|
|
641
|
+
("maxAnchorNodes", Json::Int(plan.bounds.max_anchor_nodes)),
|
|
642
|
+
("maxClosureEdges", Json::Int(plan.bounds.max_closure_edges)),
|
|
643
|
+
("maxClosureNodes", Json::Int(plan.bounds.max_closure_nodes)),
|
|
644
|
+
("maxCoverVisits", Json::Int(plan.bounds.max_cover_visits)),
|
|
645
|
+
(
|
|
646
|
+
"maxEvidenceBindings",
|
|
647
|
+
Json::Int(plan.bounds.max_evidence_bindings),
|
|
648
|
+
),
|
|
649
|
+
(
|
|
650
|
+
"maxFrontierEvidence",
|
|
651
|
+
Json::Int(plan.bounds.max_frontier_evidence),
|
|
652
|
+
),
|
|
653
|
+
(
|
|
654
|
+
"maxRequirementsPerFrontier",
|
|
655
|
+
Json::Int(plan.bounds.max_requirements_per_frontier),
|
|
656
|
+
),
|
|
657
|
+
]),
|
|
658
|
+
),
|
|
659
|
+
("cardinalityBound", Json::Int(plan.cardinality_bound)),
|
|
660
|
+
("coverageMode", Json::String(plan.coverage_mode.clone())),
|
|
661
|
+
("depth", Json::Int(plan.depth)),
|
|
662
|
+
(
|
|
663
|
+
"floors",
|
|
664
|
+
Json::Object(
|
|
665
|
+
plan.floors
|
|
666
|
+
.iter()
|
|
667
|
+
.map(|(frontier, floor)| (frontier.clone(), Json::String(floor.clone())))
|
|
668
|
+
.collect(),
|
|
669
|
+
),
|
|
670
|
+
),
|
|
671
|
+
(
|
|
672
|
+
"frontierMap",
|
|
673
|
+
Json::Object(
|
|
674
|
+
plan.frontier_map
|
|
675
|
+
.iter()
|
|
676
|
+
.map(|(symbol, frontier)| (symbol.clone(), Json::String(frontier.clone())))
|
|
677
|
+
.collect(),
|
|
678
|
+
),
|
|
679
|
+
),
|
|
680
|
+
("lexical", lexical),
|
|
681
|
+
(
|
|
682
|
+
"minAuthority",
|
|
683
|
+
plan.min_authority.map(Json::Int).unwrap_or(Json::Null),
|
|
684
|
+
),
|
|
685
|
+
("profile", Json::String(plan.profile.clone())),
|
|
686
|
+
(
|
|
687
|
+
"requirements",
|
|
688
|
+
Json::Object(
|
|
689
|
+
plan.requirements
|
|
690
|
+
.iter()
|
|
691
|
+
.map(|(frontier, ids)| {
|
|
692
|
+
(
|
|
693
|
+
frontier.clone(),
|
|
694
|
+
Json::Array(ids.iter().cloned().map(Json::String).collect()),
|
|
695
|
+
)
|
|
696
|
+
})
|
|
697
|
+
.collect(),
|
|
698
|
+
),
|
|
699
|
+
),
|
|
700
|
+
("version", Json::Int(1)),
|
|
701
|
+
])
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
fn validate_anchor(input: &Json) -> Option<AnchorSpec> {
|
|
705
|
+
let raw = input.as_object()?;
|
|
706
|
+
match input.get("mode")?.as_str()? {
|
|
707
|
+
"recompute" => {
|
|
708
|
+
if !only_keys(raw, &["mode", "procedure"])
|
|
709
|
+
|| input.get("procedure")?.as_str()? != "member-id-v1"
|
|
710
|
+
{
|
|
711
|
+
return None;
|
|
712
|
+
}
|
|
713
|
+
Some(AnchorSpec::Recompute {
|
|
714
|
+
procedure: "member-id-v1".into(),
|
|
715
|
+
})
|
|
716
|
+
}
|
|
717
|
+
"supplied" => {
|
|
718
|
+
if !only_keys(raw, &["mode", "witness"]) {
|
|
719
|
+
return None;
|
|
720
|
+
}
|
|
721
|
+
let rows = match input.get("witness")? {
|
|
722
|
+
Json::Array(items) => items,
|
|
723
|
+
_ => return None,
|
|
724
|
+
};
|
|
725
|
+
let mut witness = Vec::new();
|
|
726
|
+
for item in rows {
|
|
727
|
+
let record = item.as_object()?;
|
|
728
|
+
if !only_keys(record, &["nodeId", "queryTerm"]) {
|
|
729
|
+
return None;
|
|
730
|
+
}
|
|
731
|
+
let node_id = item.get("nodeId")?.as_str()?;
|
|
732
|
+
if node_id.is_empty() {
|
|
733
|
+
return None;
|
|
734
|
+
}
|
|
735
|
+
let query_term = match item.get("queryTerm") {
|
|
736
|
+
None => None,
|
|
737
|
+
Some(Json::String(value)) => Some(value.clone()),
|
|
738
|
+
Some(_) => return None,
|
|
739
|
+
};
|
|
740
|
+
witness.push(WitnessRecord {
|
|
741
|
+
node_id: node_id.to_string(),
|
|
742
|
+
query_term,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
Some(AnchorSpec::Supplied { witness })
|
|
746
|
+
}
|
|
747
|
+
_ => None,
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
fn validate_frontier_map(input: &Json) -> Option<Vec<(String, String)>> {
|
|
752
|
+
let mut map = Vec::new();
|
|
753
|
+
for (symbol, label) in input.as_object()? {
|
|
754
|
+
let frontier = label.as_str()?;
|
|
755
|
+
if symbol.is_empty() || !is_frontier(frontier) {
|
|
756
|
+
return None;
|
|
757
|
+
}
|
|
758
|
+
map.push((symbol.clone(), frontier.to_string()));
|
|
759
|
+
}
|
|
760
|
+
Some(map)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
fn validate_frontier_lists(input: &Json) -> Option<Vec<(String, Vec<String>)>> {
|
|
764
|
+
let raw = input.as_object()?;
|
|
765
|
+
if !only_keys(raw, &FRONTIERS) {
|
|
766
|
+
return None;
|
|
767
|
+
}
|
|
768
|
+
let mut out = Vec::new();
|
|
769
|
+
for frontier in FRONTIERS {
|
|
770
|
+
out.push((frontier.to_string(), string_list(input.get(frontier)?)?));
|
|
771
|
+
}
|
|
772
|
+
Some(out)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
fn validate_floors(input: &Json) -> Option<Vec<(String, String)>> {
|
|
776
|
+
let raw = input.as_object()?;
|
|
777
|
+
if !only_keys(raw, &FRONTIERS) {
|
|
778
|
+
return None;
|
|
779
|
+
}
|
|
780
|
+
let mut out = Vec::new();
|
|
781
|
+
for frontier in FRONTIERS {
|
|
782
|
+
let floor = input.get(frontier)?.as_str()?;
|
|
783
|
+
if floor_micros(floor).is_none() {
|
|
784
|
+
return None;
|
|
785
|
+
}
|
|
786
|
+
out.push((frontier.to_string(), floor.to_string()));
|
|
787
|
+
}
|
|
788
|
+
Some(out)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
pub fn floor_micros(floor: &str) -> Option<i64> {
|
|
792
|
+
let mut parts = floor.split('.');
|
|
793
|
+
let whole = parts.next()?;
|
|
794
|
+
let frac = parts.next();
|
|
795
|
+
if parts.next().is_some()
|
|
796
|
+
|| whole.is_empty()
|
|
797
|
+
|| !whole.bytes().all(|byte| byte.is_ascii_digit())
|
|
798
|
+
{
|
|
799
|
+
return None;
|
|
800
|
+
}
|
|
801
|
+
let fraction = match frac {
|
|
802
|
+
None => String::new(),
|
|
803
|
+
Some(text)
|
|
804
|
+
if (1..=6).contains(&text.len()) && text.bytes().all(|byte| byte.is_ascii_digit()) =>
|
|
805
|
+
{
|
|
806
|
+
text.to_string()
|
|
807
|
+
}
|
|
808
|
+
Some(_) => return None,
|
|
809
|
+
};
|
|
810
|
+
let whole_number = whole.parse::<i64>().ok()?;
|
|
811
|
+
if whole_number > MAX_SAFE_INTEGER {
|
|
812
|
+
return None;
|
|
813
|
+
}
|
|
814
|
+
let padded = format!("{fraction:0<6}");
|
|
815
|
+
let micros = padded.parse::<i64>().ok()?;
|
|
816
|
+
whole_number.checked_mul(1_000_000)?.checked_add(micros)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
fn validate_bounds(input: &Json) -> Option<Bounds> {
|
|
820
|
+
let raw = input.as_object()?;
|
|
821
|
+
let names = [
|
|
822
|
+
"maxAnchorNodes",
|
|
823
|
+
"maxClosureNodes",
|
|
824
|
+
"maxClosureEdges",
|
|
825
|
+
"maxFrontierEvidence",
|
|
826
|
+
"maxRequirementsPerFrontier",
|
|
827
|
+
"maxEvidenceBindings",
|
|
828
|
+
"maxCoverVisits",
|
|
829
|
+
];
|
|
830
|
+
if !only_keys(raw, &names) {
|
|
831
|
+
return None;
|
|
832
|
+
}
|
|
833
|
+
Some(Bounds {
|
|
834
|
+
max_anchor_nodes: non_negative(input.get("maxAnchorNodes")?)?,
|
|
835
|
+
max_closure_nodes: non_negative(input.get("maxClosureNodes")?)?,
|
|
836
|
+
max_closure_edges: non_negative(input.get("maxClosureEdges")?)?,
|
|
837
|
+
max_frontier_evidence: non_negative(input.get("maxFrontierEvidence")?)?,
|
|
838
|
+
max_requirements_per_frontier: non_negative(input.get("maxRequirementsPerFrontier")?)?,
|
|
839
|
+
max_evidence_bindings: non_negative(input.get("maxEvidenceBindings")?)?,
|
|
840
|
+
max_cover_visits: non_negative(input.get("maxCoverVisits")?)?,
|
|
841
|
+
})
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
fn validate_lexical(input: &Json) -> Option<Option<Lexical>> {
|
|
845
|
+
if matches!(input, Json::Null) {
|
|
846
|
+
return Some(None);
|
|
847
|
+
}
|
|
848
|
+
let raw = input.as_object()?;
|
|
849
|
+
if !only_keys(raw, &["frontier", "evidenceIds"]) {
|
|
850
|
+
return None;
|
|
851
|
+
}
|
|
852
|
+
let frontier = input.get("frontier")?.as_str()?;
|
|
853
|
+
if !is_frontier(frontier) {
|
|
854
|
+
return None;
|
|
855
|
+
}
|
|
856
|
+
let ids = match input.get("evidenceIds")? {
|
|
857
|
+
Json::Array(items) => items,
|
|
858
|
+
_ => return None,
|
|
859
|
+
};
|
|
860
|
+
let mut evidence_ids = Vec::new();
|
|
861
|
+
for id in ids {
|
|
862
|
+
let text = id.as_str()?;
|
|
863
|
+
if text.is_empty() {
|
|
864
|
+
return None;
|
|
865
|
+
}
|
|
866
|
+
evidence_ids.push(text.to_string());
|
|
867
|
+
}
|
|
868
|
+
Some(Some(Lexical {
|
|
869
|
+
frontier: frontier.to_string(),
|
|
870
|
+
evidence_ids,
|
|
871
|
+
}))
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
fn string_list(input: &Json) -> Option<Vec<String>> {
|
|
875
|
+
let items = match input {
|
|
876
|
+
Json::Array(items) => items,
|
|
877
|
+
_ => return None,
|
|
878
|
+
};
|
|
879
|
+
let mut out = Vec::new();
|
|
880
|
+
for item in items {
|
|
881
|
+
let text = item.as_str()?;
|
|
882
|
+
if text.is_empty() || out.iter().any(|existing| existing == text) {
|
|
883
|
+
return None;
|
|
884
|
+
}
|
|
885
|
+
out.push(text.to_string());
|
|
886
|
+
}
|
|
887
|
+
Some(out)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
fn non_negative(input: &Json) -> Option<i64> {
|
|
891
|
+
match input.as_int() {
|
|
892
|
+
Some(value) if (0..=MAX_SAFE_INTEGER).contains(&value) => Some(value),
|
|
893
|
+
_ => None,
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
pub fn only_keys(pairs: &[(String, Json)], allowed: &[&str]) -> bool {
|
|
898
|
+
pairs
|
|
899
|
+
.iter()
|
|
900
|
+
.all(|(key, _)| allowed.iter().any(|name| name == key))
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
pub fn resolve_anchor(
|
|
904
|
+
query: &str,
|
|
905
|
+
node_ids: &[String],
|
|
906
|
+
anchor: &AnchorSpec,
|
|
907
|
+
) -> (AnchorCommitment, bool) {
|
|
908
|
+
match anchor {
|
|
909
|
+
AnchorSpec::Recompute { procedure } => {
|
|
910
|
+
let nodes = if procedure == "member-id-v1" {
|
|
911
|
+
member_id_anchors(query, node_ids)
|
|
912
|
+
} else {
|
|
913
|
+
Vec::new()
|
|
914
|
+
};
|
|
915
|
+
(
|
|
916
|
+
AnchorCommitment {
|
|
917
|
+
mode: Some("recompute".into()),
|
|
918
|
+
procedure: Some(procedure.clone()),
|
|
919
|
+
witness: None,
|
|
920
|
+
nodes,
|
|
921
|
+
},
|
|
922
|
+
false,
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
AnchorSpec::Supplied { witness } => {
|
|
926
|
+
let mut rows = witness.clone();
|
|
927
|
+
rows.sort_by(|left, right| {
|
|
928
|
+
let node_order = utf16_cmp(&left.node_id, &right.node_id);
|
|
929
|
+
if node_order != std::cmp::Ordering::Equal {
|
|
930
|
+
return node_order;
|
|
931
|
+
}
|
|
932
|
+
utf16_cmp(
|
|
933
|
+
left.query_term.as_deref().unwrap_or(""),
|
|
934
|
+
right.query_term.as_deref().unwrap_or(""),
|
|
935
|
+
)
|
|
936
|
+
});
|
|
937
|
+
let mut nodes = Vec::new();
|
|
938
|
+
for row in &rows {
|
|
939
|
+
if !nodes.iter().any(|id| id == &row.node_id) {
|
|
940
|
+
nodes.push(row.node_id.clone());
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
nodes.sort_by(|left, right| utf16_cmp(left, right));
|
|
944
|
+
let rejected = nodes
|
|
945
|
+
.iter()
|
|
946
|
+
.any(|id| !node_ids.iter().any(|node| node == id));
|
|
947
|
+
(
|
|
948
|
+
AnchorCommitment {
|
|
949
|
+
mode: Some("supplied".into()),
|
|
950
|
+
procedure: None,
|
|
951
|
+
witness: Some(rows),
|
|
952
|
+
nodes,
|
|
953
|
+
},
|
|
954
|
+
rejected,
|
|
955
|
+
)
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
fn member_id_anchors(query: &str, node_ids: &[String]) -> Vec<String> {
|
|
961
|
+
let mut found = Vec::new();
|
|
962
|
+
for token in
|
|
963
|
+
query.split(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_' || ch == ':' || ch == '-'))
|
|
964
|
+
{
|
|
965
|
+
if !token.is_empty()
|
|
966
|
+
&& node_ids.iter().any(|id| id == token)
|
|
967
|
+
&& !found.iter().any(|id| id == token)
|
|
968
|
+
{
|
|
969
|
+
found.push(token.to_string());
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
found.sort_by(|left, right| utf16_cmp(left, right));
|
|
973
|
+
found
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
pub fn anchor_root(commitment: &AnchorCommitment) -> String {
|
|
977
|
+
digest_json(&commitment.to_json())
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
pub fn empty_frontiers() -> Vec<(String, Vec<String>)> {
|
|
981
|
+
FRONTIERS
|
|
982
|
+
.iter()
|
|
983
|
+
.map(|frontier| ((*frontier).to_string(), Vec::new()))
|
|
984
|
+
.collect()
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
pub fn frontier_list_mut<'a>(
|
|
988
|
+
frontiers: &'a mut [(String, Vec<String>)],
|
|
989
|
+
name: &str,
|
|
990
|
+
) -> Option<&'a mut Vec<String>> {
|
|
991
|
+
frontiers
|
|
992
|
+
.iter_mut()
|
|
993
|
+
.find(|(frontier, _)| frontier == name)
|
|
994
|
+
.map(|(_, ids)| ids)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
pub fn frontiers_json(frontiers: &[(String, Vec<String>)]) -> Json {
|
|
998
|
+
Json::Object(
|
|
999
|
+
frontiers
|
|
1000
|
+
.iter()
|
|
1001
|
+
.map(|(frontier, ids)| {
|
|
1002
|
+
(
|
|
1003
|
+
frontier.clone(),
|
|
1004
|
+
Json::Array(ids.iter().cloned().map(Json::String).collect()),
|
|
1005
|
+
)
|
|
1006
|
+
})
|
|
1007
|
+
.collect(),
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
pub fn lookup_requirement<'a>(plan: &'a Plan, frontier: &str) -> &'a [String] {
|
|
1012
|
+
plan.requirements
|
|
1013
|
+
.iter()
|
|
1014
|
+
.find(|(name, _)| name == frontier)
|
|
1015
|
+
.map(|(_, ids)| ids.as_slice())
|
|
1016
|
+
.unwrap_or(&[])
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
pub fn lookup_floor<'a>(plan: &'a Plan, frontier: &str) -> &'a str {
|
|
1020
|
+
plan.floors
|
|
1021
|
+
.iter()
|
|
1022
|
+
.find(|(name, _)| name == frontier)
|
|
1023
|
+
.map(|(_, floor)| floor.as_str())
|
|
1024
|
+
.unwrap_or("0")
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
#[cfg(test)]
|
|
1028
|
+
mod tests {
|
|
1029
|
+
use super::*;
|
|
1030
|
+
use crate::kar::json::parse_json;
|
|
1031
|
+
|
|
1032
|
+
fn plan_with(max_cover_visits: i64, min_authority: &str) -> Json {
|
|
1033
|
+
parse_json(&format!(
|
|
1034
|
+
r#"{{
|
|
1035
|
+
"version": 1,
|
|
1036
|
+
"anchor": {{"mode": "recompute", "procedure": "member-id-v1"}},
|
|
1037
|
+
"frontierMap": {{}},
|
|
1038
|
+
"depth": 2,
|
|
1039
|
+
"cardinalityBound": 8,
|
|
1040
|
+
"coverageMode": "requirements",
|
|
1041
|
+
"requirements": {{"F_S": [], "F_O": [], "F_Q": [], "F_T": [], "F_A": []}},
|
|
1042
|
+
"floors": {{"F_S": "0", "F_O": "0", "F_Q": "0", "F_T": "0", "F_A": "0"}},
|
|
1043
|
+
"profile": "minimum-cover",
|
|
1044
|
+
"asOf": "2026-06-01",
|
|
1045
|
+
"minAuthority": {min_authority},
|
|
1046
|
+
"bounds": {{
|
|
1047
|
+
"maxAnchorNodes": 8,
|
|
1048
|
+
"maxClosureNodes": 32,
|
|
1049
|
+
"maxClosureEdges": 32,
|
|
1050
|
+
"maxFrontierEvidence": 8,
|
|
1051
|
+
"maxRequirementsPerFrontier": 4,
|
|
1052
|
+
"maxEvidenceBindings": 8,
|
|
1053
|
+
"maxCoverVisits": {max_cover_visits}
|
|
1054
|
+
}},
|
|
1055
|
+
"lexical": null
|
|
1056
|
+
}}"#
|
|
1057
|
+
))
|
|
1058
|
+
.expect("plan json")
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
#[test]
|
|
1062
|
+
fn plan_bounds_reject_unsafe_integers() {
|
|
1063
|
+
assert!(validate_plan(&plan_with(MAX_SAFE_INTEGER, "null")).is_some());
|
|
1064
|
+
assert!(validate_plan(&plan_with(MAX_SAFE_INTEGER + 1, "null")).is_none());
|
|
1065
|
+
assert!(validate_plan(&plan_with(8, "-1")).is_some());
|
|
1066
|
+
assert!(validate_plan(&plan_with(8, "-9007199254740992")).is_none());
|
|
1067
|
+
}
|
|
1068
|
+
}
|