@knolo/cli 6.2.0 → 6.2.2
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 +82 -3
- package/bin/flight.mjs +367 -0
- package/bin/knolo.mjs +52 -0
- package/bin/setup.mjs +260 -0
- package/bin/skill.mjs +1308 -0
- package/package.json +2 -2
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knolo/cli",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"knolo": "bin/knolo.mjs"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "node -e \"console.log('cli is executable via bin/knolo.mjs')\"",
|
|
31
|
-
"test": "npm run build --prefix ../core && node --test test/cli.test.mjs test/kar.test.mjs test/kar-graph.test.mjs test/kar-produce.test.mjs test/hub.integration.test.mjs test/compose.test.mjs test/cortex.test.mjs",
|
|
31
|
+
"test": "npm run build --prefix ../core && node --test test/cli.test.mjs test/kar.test.mjs test/kar-graph.test.mjs test/kar-produce.test.mjs test/hub.integration.test.mjs test/compose.test.mjs test/cortex.test.mjs test/flight.test.mjs test/skill.test.mjs test/setup.test.mjs",
|
|
32
32
|
"test:hub": "node --test test/hub.integration.test.mjs"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
//! Breadth-first closure over (node, frontier) states.
|
|
2
|
+
//! The arriving relation replaces the frontier. The first reach is the canonical witness.
|
|
3
|
+
|
|
4
|
+
use crate::kar::json::{digest_json, utf16_cmp, Json};
|
|
5
|
+
use crate::kar::model::{
|
|
6
|
+
build_indexes, empty_frontiers, frontier_list_mut, frontiers_json, group, Binding, Graph,
|
|
7
|
+
Indexes, Plan,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub struct PathStep {
|
|
12
|
+
pub node: String,
|
|
13
|
+
pub relation: Option<String>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[derive(Debug, Clone)]
|
|
17
|
+
pub struct Witness {
|
|
18
|
+
pub evidence_id: String,
|
|
19
|
+
pub frontier: String,
|
|
20
|
+
pub anchor_id: Option<String>,
|
|
21
|
+
pub path: Vec<PathStep>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[derive(Debug, Clone)]
|
|
25
|
+
pub struct Admission {
|
|
26
|
+
pub evidence_id: String,
|
|
27
|
+
pub frontier: String,
|
|
28
|
+
pub applicable: bool,
|
|
29
|
+
pub requirements: Vec<String>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Debug, Clone)]
|
|
33
|
+
pub struct ClosureSuccess {
|
|
34
|
+
pub frontiers: Vec<(String, Vec<String>)>,
|
|
35
|
+
pub witnesses: Vec<Witness>,
|
|
36
|
+
pub admissions: Vec<Admission>,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct State {
|
|
40
|
+
node_id: String,
|
|
41
|
+
frontier: String,
|
|
42
|
+
depth: i64,
|
|
43
|
+
anchor_id: String,
|
|
44
|
+
parent: Option<usize>,
|
|
45
|
+
arriving_relation_id: Option<String>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn close_frontiers(
|
|
49
|
+
graph: &Graph,
|
|
50
|
+
plan: &Plan,
|
|
51
|
+
anchors: &[String],
|
|
52
|
+
indexes: Option<&Indexes>,
|
|
53
|
+
) -> Result<ClosureSuccess, ()> {
|
|
54
|
+
let bounds = &plan.bounds;
|
|
55
|
+
if graph.bindings.len() as i64 > bounds.max_evidence_bindings
|
|
56
|
+
|| anchors.len() as i64 > bounds.max_anchor_nodes
|
|
57
|
+
|| anchors.len() as i64 > bounds.max_closure_nodes
|
|
58
|
+
{
|
|
59
|
+
return Err(());
|
|
60
|
+
}
|
|
61
|
+
let owned;
|
|
62
|
+
let resolved = if let Some(indexes) = indexes {
|
|
63
|
+
indexes
|
|
64
|
+
} else {
|
|
65
|
+
owned = build_indexes(graph);
|
|
66
|
+
&owned
|
|
67
|
+
};
|
|
68
|
+
let mut frontiers = empty_frontiers();
|
|
69
|
+
let mut members: Vec<(String, Vec<String>)> = empty_frontiers();
|
|
70
|
+
let mut witnesses = Vec::new();
|
|
71
|
+
let mut witness_keys = Vec::new();
|
|
72
|
+
let mut admissions = Vec::new();
|
|
73
|
+
let mut states = Vec::new();
|
|
74
|
+
let mut seen_states = Vec::new();
|
|
75
|
+
let mut seen_nodes = Vec::new();
|
|
76
|
+
let mut edges: i64 = 0;
|
|
77
|
+
let mut current = Vec::new();
|
|
78
|
+
for node_id in anchors {
|
|
79
|
+
let index = states.len();
|
|
80
|
+
seen_states.push(format!("{node_id}\0"));
|
|
81
|
+
if !seen_nodes.iter().any(|id| id == node_id) {
|
|
82
|
+
seen_nodes.push(node_id.clone());
|
|
83
|
+
}
|
|
84
|
+
states.push(State {
|
|
85
|
+
node_id: node_id.clone(),
|
|
86
|
+
frontier: String::new(),
|
|
87
|
+
depth: 0,
|
|
88
|
+
anchor_id: node_id.clone(),
|
|
89
|
+
parent: None,
|
|
90
|
+
arriving_relation_id: None,
|
|
91
|
+
});
|
|
92
|
+
current.push(index);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
loop {
|
|
96
|
+
if current.is_empty() {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
let mut next = Vec::new();
|
|
100
|
+
for state_index in ¤t {
|
|
101
|
+
let state_depth = states[*state_index].depth;
|
|
102
|
+
let state_node = states[*state_index].node_id.clone();
|
|
103
|
+
let state_anchor = states[*state_index].anchor_id.clone();
|
|
104
|
+
if state_depth >= plan.depth {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for relation in group(&resolved.relations_by_from, &state_node) {
|
|
108
|
+
let Some(label) = plan
|
|
109
|
+
.frontier_map
|
|
110
|
+
.iter()
|
|
111
|
+
.find(|(symbol, _)| symbol == &relation.relation)
|
|
112
|
+
.map(|(_, label)| label.clone())
|
|
113
|
+
else {
|
|
114
|
+
continue;
|
|
115
|
+
};
|
|
116
|
+
if edges + 1 > bounds.max_closure_edges {
|
|
117
|
+
return Err(());
|
|
118
|
+
}
|
|
119
|
+
edges += 1;
|
|
120
|
+
let key = format!("{}\0{label}", relation.to);
|
|
121
|
+
if seen_states.iter().any(|seen| seen == &key) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if !seen_nodes.iter().any(|id| id == &relation.to) {
|
|
125
|
+
if seen_nodes.len() as i64 + 1 > bounds.max_closure_nodes {
|
|
126
|
+
return Err(());
|
|
127
|
+
}
|
|
128
|
+
seen_nodes.push(relation.to.clone());
|
|
129
|
+
}
|
|
130
|
+
seen_states.push(key);
|
|
131
|
+
let index = states.len();
|
|
132
|
+
states.push(State {
|
|
133
|
+
node_id: relation.to.clone(),
|
|
134
|
+
frontier: label,
|
|
135
|
+
depth: state_depth + 1,
|
|
136
|
+
anchor_id: state_anchor.clone(),
|
|
137
|
+
parent: Some(*state_index),
|
|
138
|
+
arriving_relation_id: Some(relation.id.clone()),
|
|
139
|
+
});
|
|
140
|
+
next.push(index);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
next.sort_by(|left, right| {
|
|
144
|
+
let frontier_order = utf16_cmp(&states[*left].frontier, &states[*right].frontier);
|
|
145
|
+
if frontier_order != std::cmp::Ordering::Equal {
|
|
146
|
+
return frontier_order;
|
|
147
|
+
}
|
|
148
|
+
utf16_cmp(&states[*left].node_id, &states[*right].node_id)
|
|
149
|
+
});
|
|
150
|
+
for state_index in &next {
|
|
151
|
+
if admit(
|
|
152
|
+
*state_index,
|
|
153
|
+
&states,
|
|
154
|
+
resolved,
|
|
155
|
+
plan,
|
|
156
|
+
&mut frontiers,
|
|
157
|
+
&mut members,
|
|
158
|
+
&mut witnesses,
|
|
159
|
+
&mut witness_keys,
|
|
160
|
+
&mut admissions,
|
|
161
|
+
)
|
|
162
|
+
.is_err()
|
|
163
|
+
{
|
|
164
|
+
return Err(());
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
current = next;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if let Some(lexical) = &plan.lexical {
|
|
171
|
+
let mut ids = Vec::new();
|
|
172
|
+
for evidence_id in &lexical.evidence_ids {
|
|
173
|
+
if !ids.iter().any(|id| id == evidence_id) {
|
|
174
|
+
ids.push(evidence_id.clone());
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
ids.sort_by(|left, right| utf16_cmp(left, right));
|
|
178
|
+
for evidence_id in ids {
|
|
179
|
+
let bucket = members
|
|
180
|
+
.iter()
|
|
181
|
+
.find(|(frontier, _)| frontier == &lexical.frontier)
|
|
182
|
+
.map(|(_, ids)| ids.clone())
|
|
183
|
+
.unwrap_or_default();
|
|
184
|
+
if bucket.iter().any(|id| id == &evidence_id) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if bucket.len() as i64 + 1 > bounds.max_frontier_evidence {
|
|
188
|
+
return Err(());
|
|
189
|
+
}
|
|
190
|
+
if let Some(list) = members
|
|
191
|
+
.iter_mut()
|
|
192
|
+
.find(|(frontier, _)| frontier == &lexical.frontier)
|
|
193
|
+
{
|
|
194
|
+
list.1.push(evidence_id.clone());
|
|
195
|
+
}
|
|
196
|
+
if let Some(list) = frontier_list_mut(&mut frontiers, &lexical.frontier) {
|
|
197
|
+
list.push(evidence_id.clone());
|
|
198
|
+
}
|
|
199
|
+
witnesses.push(Witness {
|
|
200
|
+
evidence_id: evidence_id.clone(),
|
|
201
|
+
frontier: lexical.frontier.clone(),
|
|
202
|
+
anchor_id: None,
|
|
203
|
+
path: Vec::new(),
|
|
204
|
+
});
|
|
205
|
+
let related = group(&resolved.bindings_by_evidence, &evidence_id);
|
|
206
|
+
if related.is_empty() {
|
|
207
|
+
admissions.push(Admission {
|
|
208
|
+
evidence_id,
|
|
209
|
+
frontier: lexical.frontier.clone(),
|
|
210
|
+
applicable: true,
|
|
211
|
+
requirements: Vec::new(),
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
for binding in related {
|
|
215
|
+
let ok = applicable(binding, plan);
|
|
216
|
+
admissions.push(Admission {
|
|
217
|
+
evidence_id: evidence_id.clone(),
|
|
218
|
+
frontier: lexical.frontier.clone(),
|
|
219
|
+
applicable: ok,
|
|
220
|
+
requirements: if ok {
|
|
221
|
+
binding.requirements.clone()
|
|
222
|
+
} else {
|
|
223
|
+
Vec::new()
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (_, ids) in &mut frontiers {
|
|
232
|
+
ids.sort_by(|left, right| utf16_cmp(left, right));
|
|
233
|
+
}
|
|
234
|
+
witnesses.sort_by(witness_order);
|
|
235
|
+
Ok(ClosureSuccess {
|
|
236
|
+
frontiers,
|
|
237
|
+
witnesses,
|
|
238
|
+
admissions,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fn admit(
|
|
243
|
+
state_index: usize,
|
|
244
|
+
states: &[State],
|
|
245
|
+
indexes: &Indexes,
|
|
246
|
+
plan: &Plan,
|
|
247
|
+
frontiers: &mut Vec<(String, Vec<String>)>,
|
|
248
|
+
members: &mut Vec<(String, Vec<String>)>,
|
|
249
|
+
witnesses: &mut Vec<Witness>,
|
|
250
|
+
witness_keys: &mut Vec<String>,
|
|
251
|
+
admissions: &mut Vec<Admission>,
|
|
252
|
+
) -> Result<(), ()> {
|
|
253
|
+
let frontier = states[state_index].frontier.clone();
|
|
254
|
+
if frontier.is_empty() {
|
|
255
|
+
return Ok(());
|
|
256
|
+
}
|
|
257
|
+
let node_id = states[state_index].node_id.clone();
|
|
258
|
+
for binding in group(&indexes.bindings_by_node, &node_id) {
|
|
259
|
+
let bucket_len = members
|
|
260
|
+
.iter()
|
|
261
|
+
.find(|(name, _)| name == &frontier)
|
|
262
|
+
.map(|(_, ids)| ids.len())
|
|
263
|
+
.unwrap_or(0);
|
|
264
|
+
let already = members.iter().any(|(name, ids)| {
|
|
265
|
+
name == &frontier && ids.iter().any(|id| id == &binding.evidence_id)
|
|
266
|
+
});
|
|
267
|
+
if !already {
|
|
268
|
+
if bucket_len as i64 + 1 > plan.bounds.max_frontier_evidence {
|
|
269
|
+
return Err(());
|
|
270
|
+
}
|
|
271
|
+
if let Some(list) = members.iter_mut().find(|(name, _)| name == &frontier) {
|
|
272
|
+
list.1.push(binding.evidence_id.clone());
|
|
273
|
+
}
|
|
274
|
+
if let Some(list) = frontier_list_mut(frontiers, &frontier) {
|
|
275
|
+
list.push(binding.evidence_id.clone());
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
let key = format!("{frontier}\0{}", binding.evidence_id);
|
|
279
|
+
if !witness_keys.iter().any(|existing| existing == &key) {
|
|
280
|
+
witness_keys.push(key);
|
|
281
|
+
witnesses.push(Witness {
|
|
282
|
+
evidence_id: binding.evidence_id.clone(),
|
|
283
|
+
frontier: frontier.clone(),
|
|
284
|
+
anchor_id: Some(states[state_index].anchor_id.clone()),
|
|
285
|
+
path: materialize_path(states, state_index),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
let ok = applicable(binding, plan);
|
|
289
|
+
admissions.push(Admission {
|
|
290
|
+
evidence_id: binding.evidence_id.clone(),
|
|
291
|
+
frontier: frontier.clone(),
|
|
292
|
+
applicable: ok,
|
|
293
|
+
requirements: if ok {
|
|
294
|
+
binding.requirements.clone()
|
|
295
|
+
} else {
|
|
296
|
+
Vec::new()
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
Ok(())
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn materialize_path(states: &[State], state_index: usize) -> Vec<PathStep> {
|
|
304
|
+
let mut chain = Vec::new();
|
|
305
|
+
let mut cursor = Some(state_index);
|
|
306
|
+
while let Some(index) = cursor {
|
|
307
|
+
chain.push(index);
|
|
308
|
+
cursor = states[index].parent;
|
|
309
|
+
}
|
|
310
|
+
chain.reverse();
|
|
311
|
+
let mut path = vec![PathStep {
|
|
312
|
+
node: states[chain[0]].node_id.clone(),
|
|
313
|
+
relation: None,
|
|
314
|
+
}];
|
|
315
|
+
for index in chain.into_iter().skip(1) {
|
|
316
|
+
path.push(PathStep {
|
|
317
|
+
node: states[index].node_id.clone(),
|
|
318
|
+
relation: states[index].arriving_relation_id.clone(),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
path
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn applicable(binding: &Binding, plan: &Plan) -> bool {
|
|
325
|
+
if binding.unauthorized {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
if let Some(min_authority) = plan.min_authority {
|
|
329
|
+
match binding.authority {
|
|
330
|
+
Some(authority) if authority >= min_authority => {}
|
|
331
|
+
_ => return false,
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if let Some(valid_from) = &binding.valid_from {
|
|
335
|
+
if utf16_cmp(&plan.as_of, valid_from) == std::cmp::Ordering::Less {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if let Some(valid_until) = &binding.valid_until {
|
|
340
|
+
if utf16_cmp(&plan.as_of, valid_until) != std::cmp::Ordering::Less {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
true
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fn witness_order(left: &Witness, right: &Witness) -> std::cmp::Ordering {
|
|
348
|
+
let frontier = utf16_cmp(&left.frontier, &right.frontier);
|
|
349
|
+
if frontier != std::cmp::Ordering::Equal {
|
|
350
|
+
return frontier;
|
|
351
|
+
}
|
|
352
|
+
let evidence = utf16_cmp(&left.evidence_id, &right.evidence_id);
|
|
353
|
+
if evidence != std::cmp::Ordering::Equal {
|
|
354
|
+
return evidence;
|
|
355
|
+
}
|
|
356
|
+
utf16_cmp(
|
|
357
|
+
left.anchor_id.as_deref().unwrap_or(""),
|
|
358
|
+
right.anchor_id.as_deref().unwrap_or(""),
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
pub fn frontier_root(frontiers: &[(String, Vec<String>)]) -> String {
|
|
363
|
+
digest_json(&frontiers_json(frontiers))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
pub fn frontier_witness_root(witnesses: &[Witness]) -> String {
|
|
367
|
+
digest_json(&Json::Array(witnesses.iter().map(witness_json).collect()))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
pub fn witness_json(witness: &Witness) -> Json {
|
|
371
|
+
Json::object(vec![
|
|
372
|
+
(
|
|
373
|
+
"anchorId",
|
|
374
|
+
witness
|
|
375
|
+
.anchor_id
|
|
376
|
+
.clone()
|
|
377
|
+
.map(Json::String)
|
|
378
|
+
.unwrap_or(Json::Null),
|
|
379
|
+
),
|
|
380
|
+
("evidenceId", Json::String(witness.evidence_id.clone())),
|
|
381
|
+
("frontier", Json::String(witness.frontier.clone())),
|
|
382
|
+
(
|
|
383
|
+
"path",
|
|
384
|
+
Json::Array(witness.path.iter().map(path_json).collect()),
|
|
385
|
+
),
|
|
386
|
+
])
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
fn path_json(step: &PathStep) -> Json {
|
|
390
|
+
let mut pairs = vec![("node".to_string(), Json::String(step.node.clone()))];
|
|
391
|
+
if let Some(relation) = &step.relation {
|
|
392
|
+
pairs.push(("relation".to_string(), Json::String(relation.clone())));
|
|
393
|
+
}
|
|
394
|
+
Json::Object(pairs)
|
|
395
|
+
}
|