kibi-core 0.26.1 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,7 +47,9 @@
47
47
  % routes. set_relation compares the members with the
48
48
  % fact's set; required_fields and nullable_fields check
49
49
  % each selected table or object; route_middleware checks
50
- % each selected route's middleware.
50
+ % each selected route's middleware; sequence_precedes
51
+ % checks each "A -> B" member against the list in the
52
+ % order read (every A before every B).
51
53
  %
52
54
  % Rules with explicit exists quantifiers, count or temporal expressions, or
53
55
  % durations, timestamps and intervals are outside what Kibi enumerates and
@@ -115,14 +117,18 @@ certificate_model(state_machine, Certificate, model(any(States), Table, machine(
115
117
  ; Initial = none
116
118
  ),
117
119
  machine_table(States, Edges, Initial, Table).
118
- certificate_model(source, Certificate, model(none, none, source(Members, Entities, Routes))) :-
120
+ certificate_model(source, Certificate, model(none, none, source(Members, Entities, Routes, Sequence))) :-
119
121
  !,
120
122
  required(Certificate, model, Model),
121
123
  required(Model, members, RawMembers),
122
124
  maplist(text_string, RawMembers, Members0),
123
125
  sort(Members0, Members),
124
126
  ( get_dict(entities, Model, Entities) -> true ; Entities = none ),
125
- ( get_dict(routes, Model, Routes) -> true ; Routes = none ).
127
+ ( get_dict(routes, Model, Routes) -> true ; Routes = none ),
128
+ ( get_dict(sequence, Model, RawSequence)
129
+ -> maplist(text_string, RawSequence, Sequence)
130
+ ; Sequence = none
131
+ ).
126
132
  certificate_model(Kind, _, _) :-
127
133
  format(string(Reason), "a ~w certificate has no finite model to check", [Kind]),
128
134
  throw(model_error(Reason)).
@@ -391,14 +397,14 @@ obligation_result("machine_relation", Obligation, model(_, _, Machine), Result)
391
397
  ; Result = ok
392
398
  )
393
399
  ).
394
- obligation_result("set_relation", Obligation, model(_, _, source(Actual, _, _)), Result) :- !,
400
+ obligation_result("set_relation", Obligation, model(_, _, source(Actual, _, _, _)), Result) :- !,
395
401
  dict_text(Obligation, operator, Operator),
396
402
  obligation_strings(Obligation, Expected),
397
403
  ( relation_problem(Operator, Actual, Expected, "the members Kibi read from the source", Reason)
398
404
  -> Result = problem(Reason)
399
405
  ; Result = ok
400
406
  ).
401
- obligation_result(FieldKind, Obligation, model(_, _, source(_, Entities, _)), Result) :-
407
+ obligation_result(FieldKind, Obligation, model(_, _, source(_, Entities, _, _)), Result) :-
402
408
  memberchk(FieldKind, ["required_fields", "nullable_fields"]),
403
409
  !,
404
410
  ( is_list(Entities), Entities \== []
@@ -411,7 +417,7 @@ obligation_result(FieldKind, Obligation, model(_, _, source(_, Entities, _)), Re
411
417
  -> Result = problem(Reason)
412
418
  ; Result = ok
413
419
  ).
414
- obligation_result("route_middleware", Obligation, model(_, _, source(_, _, Routes)), Result) :- !,
420
+ obligation_result("route_middleware", Obligation, model(_, _, source(_, _, Routes, _)), Result) :- !,
415
421
  ( is_list(Routes), Routes \== []
416
422
  -> true
417
423
  ; throw(model_error("the source has no routes; read a route table for a middleware claim"))
@@ -429,9 +435,51 @@ obligation_result("route_middleware", Obligation, model(_, _, source(_, _, Route
429
435
  -> Result = problem(Reason)
430
436
  ; Result = ok
431
437
  ).
438
+ % implements REQ-kibi-source-proof-kinds
439
+ obligation_result("sequence_precedes", Obligation, model(_, _, source(_, _, _, Sequence)), Result) :- !,
440
+ ( is_list(Sequence)
441
+ -> true
442
+ ; throw(model_error("the source has no order; read a list for a precedes claim"))
443
+ ),
444
+ obligation_strings(Obligation, Pairs),
445
+ ( member(Pair, Pairs),
446
+ precedes_problem(Pair, Sequence, Reason)
447
+ -> Result = problem(Reason)
448
+ ; Result = ok
449
+ ).
432
450
  obligation_result(Kind, _, _, problem(Reason)) :-
433
451
  format(string(Reason), "a ~w obligation cannot be checked against a finite model", [Kind]).
434
452
 
453
+ % A precedes member "A -> B" holds when A and B are both in the list and
454
+ % every A comes before every B.
455
+ precedes_problem(Pair, Sequence, Reason) :-
456
+ ( precedes_pair(Pair, First, Second)
457
+ -> findall(I, nth1(I, Sequence, First), FirstAt),
458
+ findall(I, nth1(I, Sequence, Second), SecondAt),
459
+ ( FirstAt == []
460
+ -> format(string(Reason), "~w is not in the list Kibi read", [First])
461
+ ; SecondAt == []
462
+ -> format(string(Reason), "~w is not in the list Kibi read", [Second])
463
+ ; max_list(FirstAt, LastFirst),
464
+ min_list(SecondAt, FirstSecond),
465
+ LastFirst > FirstSecond
466
+ -> format(string(Reason), "~w comes before ~w in the list Kibi read (items ~w and ~w)",
467
+ [Second, First, FirstSecond, LastFirst])
468
+ )
469
+ ; format(string(Reason), "~w is not spelled \"A -> B\"", [Pair])
470
+ ).
471
+
472
+ precedes_pair(Pair, First, Second) :-
473
+ sub_string(Pair, Before, 4, After, " -> "),
474
+ sub_string(Pair, 0, Before, _, First0),
475
+ sub_string(Pair, _, After, 0, Second0),
476
+ \+ sub_string(Second0, _, _, _, " -> "),
477
+ normalize_space(string(First), First0),
478
+ normalize_space(string(Second), Second0),
479
+ First \== "",
480
+ Second \== "",
481
+ !.
482
+
435
483
  obligation_strings(Obligation, Members) :-
436
484
  get_dict(members, Obligation, Raw),
437
485
  maplist(text_string, Raw, Members0),
@@ -54,7 +54,13 @@ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds,
54
54
  (catch(parse_time(CheckedAtAtom, CheckedAtStamp), _, fail) -> true ; CheckedAtStamp = -1),
55
55
  normalize_integer(MaxAgeSeconds, MaxAge),
56
56
  normalize_atom(BindingMode, BindingModeAtom),
57
+ % Everything else in the context follows from the KB, so the proof memo
58
+ % key needs only the caller's parameters.
59
+ variant_sha1(params(Snapshot, CheckedAtAtom, MaxAge, BindingModeAtom, TestBindings), ParamsKey),
60
+ variant_sha1(findings(Contradictions, ContradictionWitnesses, UnsafeRules, UnverifiableRules), FindingsKey),
57
61
  Context = _{
62
+ proofMemoKey: ParamsKey,
63
+ ladderMemoKey: FindingsKey,
58
64
  contradictions: Contradictions,
59
65
  contradictionWitnesses: ContradictionWitnesses,
60
66
  unsafeRules: UnsafeRules,
@@ -68,28 +74,86 @@ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds,
68
74
  }.
69
75
 
70
76
  % implements REQ-kibi-conservative-requirement-proof
77
+ % A requirement's proof is kept per KB generation and proof parameters, so
78
+ % the evidence-floor rule, the coverage evidence and a coverage report in one
79
+ % check read each ladder once. Inside a transaction (what-if staging) the
80
+ % KB is not the generation's, so the proof is computed afresh.
81
+ requirement_proof(ReqId, ReqProps, Context, Proof) :-
82
+ ( \+ rdf_db:rdf_active_transaction(_),
83
+ get_dict(proofMemoKey, Context, ParamsKey),
84
+ kb:index_verification_key(requirement_proof, IndexKey)
85
+ -> Key = key(IndexKey, ParamsKey),
86
+ ( kb:kb_requirement_proof_memo(ReqId, Key, Proof0)
87
+ -> Proof = Proof0
88
+ ; compute_requirement_proof(ReqId, ReqProps, Context, Proof),
89
+ ( once(kb:kb_requirement_proof_memo(_, Stored, _)),
90
+ Stored \== Key
91
+ -> retractall(kb:kb_requirement_proof_memo(_, _, _))
92
+ ; true
93
+ ),
94
+ assertz(kb:kb_requirement_proof_memo(ReqId, Key, Proof))
95
+ )
96
+ ; compute_requirement_proof(ReqId, ReqProps, Context, Proof)
97
+ ).
98
+
99
+ % The stages before passing_e2e read the KB and the context's KB-derived
100
+ % findings (contradictions, unsafe and unverifiable rules) only, never the
101
+ % caller's snapshot, receipt bindings or checkedAt. A check passes a new
102
+ % checkedAt every run, so these stages are kept per KB generation and those
103
+ % findings apart from the full proof; inside a transaction they are computed
104
+ % afresh.
105
+ receipt_independent_stages(ReqId, ReqProps, Context, Ladder) :-
106
+ ( \+ rdf_db:rdf_active_transaction(_),
107
+ get_dict(ladderMemoKey, Context, FindingsKey),
108
+ kb:index_verification_key(requirement_ladder, IndexKey)
109
+ -> variant_sha1(ReqProps, PropsKey),
110
+ Key = key(IndexKey, FindingsKey, PropsKey),
111
+ ( kb:kb_requirement_ladder_memo(ReqId, Key, Ladder0)
112
+ -> Ladder = Ladder0
113
+ ; compute_receipt_independent_stages(ReqId, ReqProps, Context, Ladder),
114
+ ( once(kb:kb_requirement_ladder_memo(_, key(Stored, _, _), _)),
115
+ Stored \== IndexKey
116
+ -> retractall(kb:kb_requirement_ladder_memo(_, _, _))
117
+ ; true
118
+ ),
119
+ retractall(kb:kb_requirement_ladder_memo(ReqId, _, _)),
120
+ assertz(kb:kb_requirement_ladder_memo(ReqId, Key, Ladder))
121
+ )
122
+ ; compute_receipt_independent_stages(ReqId, ReqProps, Context, Ladder)
123
+ ).
124
+
125
+ compute_receipt_independent_stages(ReqId, ReqProps, Context,
126
+ ladder(SemanticStage, Inventory, LogicStage, ContradictionStage,
127
+ ScenarioStage, ScenarioTestStage, ScenarioTests, SymbolSets)) :-
128
+ semantic_inventory_stage(ReqProps, SemanticStage, Inventory),
129
+ logic_grounding_stage(ReqId, ReqProps, Inventory, Context, LogicStage),
130
+ contradiction_stage(ReqId, LogicStage.status, SemanticStage.status, Context, ContradictionStage),
131
+ scenario_stage(ReqId, ScenarioStage, ScenarioIds),
132
+ scenario_test_stage(ScenarioIds, ScenarioTestStage, ScenarioTests),
133
+ production_symbol_sets(ReqId, SymbolSets).
134
+
71
135
  % Exemption first: a current requirement explicitly marked proof_exempt with a
72
136
  % reason is intentionally outside E2E-proof scope and must never fall through
73
137
  % to the ladder (which would report misleading receipt gaps).
74
- requirement_proof(ReqId, ReqProps, _Context, Proof) :-
138
+ compute_requirement_proof(ReqId, ReqProps, _Context, Proof) :-
75
139
  kb:current_req(ReqId),
76
140
  proof_exempt(ReqProps, Reason),
77
141
  !,
78
142
  exempt_requirement_proof(Reason, ReqProps, Proof).
79
- requirement_proof(ReqId, ReqProps, _Context, Proof) :-
143
+ compute_requirement_proof(ReqId, ReqProps, _Context, Proof) :-
80
144
  \+ kb:current_req(ReqId),
81
145
  !,
82
146
  applicability_exclusion_reason(ReqId, Reason),
83
147
  exempt_requirement_proof(Reason, ReqProps, Proof).
84
- requirement_proof(ReqId, ReqProps, Context, Proof) :-
85
- semantic_inventory_stage(ReqProps, SemanticStage, Inventory),
86
- logic_grounding_stage(ReqId, ReqProps, Inventory, Context, LogicStage),
87
- contradiction_stage(ReqId, LogicStage.status, SemanticStage.status, Context, ContradictionStage),
88
- scenario_stage(ReqId, ScenarioStage, ScenarioIds),
89
- scenario_test_stage(ScenarioIds, ScenarioTestStage, ScenarioTests),
148
+ compute_requirement_proof(ReqId, ReqProps, Context, Proof) :-
149
+ receipt_independent_stages(ReqId, ReqProps, Context,
150
+ ladder(SemanticStage, Inventory, LogicStage, ContradictionStage,
151
+ ScenarioStage, ScenarioTestStage, ScenarioTests, SymbolSets)),
90
152
  passing_e2e_stage(ScenarioTestStage, Context, PassingE2eStage, PassingE2eTests),
91
153
  executable_symbol_stage(PassingE2eTests, ExecutableStage, ExecutableSymbols),
92
- production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, PassingE2eStage.receiptEvidence, Context, ProductionStage, ProductionSymbols),
154
+ production_symbol_stage_from_sets(ReqId, SymbolSets, PassingE2eTests, ScenarioTests,
155
+ PassingE2eStage.receiptEvidence, Context,
156
+ ProductionStage, ProductionSymbols),
93
157
  source_coordinate_stage(ReqProps, ExecutableSymbols, ProductionSymbols, CoordinateStage),
94
158
  Stages = _{
95
159
  semanticInventory: SemanticStage,
@@ -1211,6 +1275,11 @@ canonical_json_value(Value, Json) :-
1211
1275
  maplist(canonical_json_value, Value, Parts),
1212
1276
  atomics_to_string(Parts, ",", Body),
1213
1277
  format(string(Json), "[~s]", [Body]).
1278
+ % JSON true, false and null read back as atoms; json_write would quote them,
1279
+ % so the hash would differ from the one the CLI recorded at ingest.
1280
+ canonical_json_value(Value, Json) :-
1281
+ memberchk(Value, [true, false, null]), !,
1282
+ atom_string(Value, Json).
1214
1283
  canonical_json_value(Value, Json) :-
1215
1284
  with_output_to(string(Json), json_write(current_output, Value, [width(0)])).
1216
1285
 
@@ -1375,6 +1444,14 @@ production_symbol_stage(ReqId, PassingE2eTests, Stage, ProductionSymbols) :-
1375
1444
  production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, [], Context, Stage, ProductionSymbols).
1376
1445
 
1377
1446
  production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence, Context, Stage, ProductionSymbols) :-
1447
+ production_symbol_sets(ReqId, Sets),
1448
+ production_symbol_stage_from_sets(ReqId, Sets, PassingE2eTests, ScenarioTests,
1449
+ ReceiptEvidence, Context, Stage, ProductionSymbols).
1450
+
1451
+ % The implementing symbols of a requirement, split into structural
1452
+ % (type-shape with a unit-test contract) and production ones, with the
1453
+ % production symbols' coordinates: all read from the KB alone.
1454
+ production_symbol_sets(ReqId, symbol_sets(StructuralSymbols, ProductionSymbols, Coordinates)) :-
1378
1455
  findall(SymbolId,
1379
1456
  (kb_relationship(implements, SymbolId, ReqId),
1380
1457
  kb_entity(SymbolId, symbol, _),
@@ -1383,13 +1460,17 @@ production_symbol_stage(ReqId, PassingE2eTests, ScenarioTests, ReceiptEvidence,
1383
1460
  sort(Implementing0, ImplementingSymbols),
1384
1461
  include(type_shape_symbol_with_structural_contract, ImplementingSymbols, StructuralSymbols),
1385
1462
  exclude(type_shape_symbol_with_structural_contract, ImplementingSymbols, ProductionSymbols),
1463
+ maplist(symbol_coordinate_ref, ProductionSymbols, Coordinates).
1464
+
1465
+ production_symbol_stage_from_sets(ReqId, symbol_sets(StructuralSymbols, ProductionSymbols, Coordinates),
1466
+ PassingE2eTests, ScenarioTests, ReceiptEvidence, Context,
1467
+ Stage, ProductionSymbols) :-
1386
1468
  include(symbol_not_covered_by_tests(PassingE2eTests), ProductionSymbols, UncoveredProduction),
1387
1469
  structural_unit_receipts(StructuralSymbols, Context, UnitReceipts),
1388
1470
  exclude(structural_symbol_covered(PassingE2eTests, UnitReceipts), StructuralSymbols, UncoveredStructural),
1389
1471
  append(UncoveredProduction, UncoveredStructural, UncoveredSymbols),
1390
1472
  production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests,
1391
1473
  UncoveredProduction, UncoveredStructural, Status, StatusReason),
1392
- maplist(symbol_coordinate_ref, ProductionSymbols, Coordinates),
1393
1474
  production_coverage_explanations(
1394
1475
  ReqId, ProductionSymbols, StructuralSymbols, UncoveredSymbols,
1395
1476
  PassingE2eTests, ScenarioTests, ReceiptEvidence, UnitReceipts-Context, Explanations),
package/src/status.pl CHANGED
@@ -203,7 +203,23 @@ kb_snapshot_time(SnapshotTime) :-
203
203
  % made kb_status_json quadratic (paths x entities) and could wedge large KBs
204
204
  % for minutes whenever mtime churn (e.g. a git checkout) marked whole lanes
205
205
  % newer than the snapshot.
206
+ % The source -> entity id index reads every entity, so it is kept per KB
207
+ % generation (whose key names the attached store's graph): status runs it on
208
+ % each freshness probe while the KB is unchanged. Inside a transaction it is
209
+ % built afresh.
206
210
  entity_source_id_index(Index) :-
211
+ ( \+ rdf_db:rdf_active_transaction(_),
212
+ kb:index_verification_key(status_source_index, Key)
213
+ -> ( kb:kb_status_source_index_memo(Key, Index0)
214
+ -> Index = Index0
215
+ ; compute_entity_source_id_index(Index),
216
+ retractall(kb:kb_status_source_index_memo(_, _)),
217
+ assertz(kb:kb_status_source_index_memo(Key, Index))
218
+ )
219
+ ; compute_entity_source_id_index(Index)
220
+ ).
221
+
222
+ compute_entity_source_id_index(Index) :-
207
223
  findall(RelativeSource-Id,
208
224
  ( kb_entity(Id, _Type, Props),
209
225
  entity_source_property(Props, RawSource),
package/src/units.pl CHANGED
@@ -120,13 +120,11 @@ numeric_value_type(number).
120
120
 
121
121
  known_unit(Unit, BaseUnit, Factor) :-
122
122
  atom(Unit),
123
- Unit \== '',
124
123
  ( unit_symbol(Unit, BaseUnit, Factor)
125
124
  -> true
126
125
  ; downcase_atom(Unit, Lower),
127
126
  unit_word(Lower, BaseUnit, Factor)
128
- ),
129
- !.
127
+ ).
130
128
 
131
129
  % Integers (including big integers) stay exact. Floats have binary noise
132
130
  % rounded away (0.1 h * 3600 = 360.00000000000006) before deciding whether the