kibi-core 0.28.0 → 0.29.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
package/src/discovery.pl CHANGED
@@ -417,7 +417,7 @@ evidence_floor_json(BindingMode, TestBindings, VerificationSnapshot, CheckedAt,
417
417
  ( member(Id-Proof, Evaluated),
418
418
  Floor = Proof.evidenceFloor,
419
419
  Floor.status \== met,
420
- Row = _{
420
+ Row0 = _{
421
421
  id: Id,
422
422
  evidenceLevel: Proof.evidenceLevel,
423
423
  requiredEvidence: Floor.required,
@@ -427,7 +427,14 @@ evidence_floor_json(BindingMode, TestBindings, VerificationSnapshot, CheckedAt,
427
427
  proofGaps: Proof.proofGaps,
428
428
  gapDetails: Details
429
429
  },
430
- evidence_floor_gap_details(Proof, Details)
430
+ evidence_floor_gap_details(Proof, Details),
431
+ % implements REQ-kibi-claim-bound-proofs
432
+ % A claim its proof refuted: the test and what it read.
433
+ ( get_dict(claimBindingRejections, Proof, Refutations),
434
+ Refutations \== []
435
+ -> Row = Row0.put(refutations, Refutations)
436
+ ; Row = Row0
437
+ )
431
438
  ),
432
439
  Rows0),
433
440
  sort(id, @<, Rows0, Rows),
@@ -117,8 +117,9 @@ certificate_model(state_machine, Certificate, model(any(States), Table, machine(
117
117
  ; Initial = none
118
118
  ),
119
119
  machine_table(States, Edges, Initial, Table).
120
- certificate_model(source, Certificate, model(none, none, source(Members, Entities, Routes, Sequence))) :-
120
+ certificate_model(source, Certificate, model(none, none, source(Members, Entities, Routes, Sequence, Matching))) :-
121
121
  !,
122
+ source_matching(Certificate, Matching),
122
123
  required(Certificate, model, Model),
123
124
  required(Model, members, RawMembers),
124
125
  maplist(text_string, RawMembers, Members0),
@@ -306,6 +307,204 @@ relation_problem("excludes", Actual, Expected, What, Reason) :-
306
307
  atomic_list_concat(Shared, ', ', Text),
307
308
  format(string(Reason), "~w include ~w, which the fact excludes", [What, Text]).
308
309
 
310
+ % ---------------------------------------------------------------------------
311
+ % Reference members
312
+ %
313
+ % The references reader's members are dotted, slashed or ::-joined names
314
+ % (java.sql.Connection, github.com/labstack/echo/v4/middleware,
315
+ % std::env::var), a wildcard import (java.sql.*), or *.m for a call of m on a
316
+ % receiver the reader cannot name. For these members an expected item E
317
+ % covers more than its own spelling, and each operator reads it in the
318
+ % direction that can never prove a claim the code does not hold:
319
+ %
320
+ % excludes E fails on any member that is or may be E: E itself, a member
321
+ % under E at a segment boundary (java.sql covers
322
+ % java.sql.Connection), a member under the prefix of E* (java.sql.*
323
+ % covers java.sql.Connection), a wildcard import that may bring E
324
+ % in, a shorter name E ends with (File.delete may be
325
+ % java.io.File.delete), a longer name ending in E at a segment
326
+ % boundary (checkNotNull may be named by
327
+ % com.google.common.base.Preconditions.checkNotNull), and *.m
328
+ % when E ends in .m (it may be E).
329
+ % includes E holds only on a member that certainly is E: E itself, a
330
+ % member under the prefix of E*, or a member ending in .m for *.m.
331
+ % *.m never satisfies X.m.
332
+ % subset_of A member is allowed only when it certainly is an allowed item:
333
+ % the item itself, a member under the prefix of an item E*, or a
334
+ % member ending in .m for *.m. *.m is allowed only when *.m is.
335
+ % eq Compares the spellings exactly, as for every other reader.
336
+
337
+ source_matching(Certificate, references(Files)) :-
338
+ get_dict(source, Certificate, Source),
339
+ is_dict(Source),
340
+ get_dict(reader, Source, Reader),
341
+ text_string(Reader, "references"),
342
+ !,
343
+ ( get_dict(files, Source, RawFiles)
344
+ -> ( is_list(RawFiles) -> maplist(text_string, RawFiles, Files) ; text_string(RawFiles, File), Files = [File] )
345
+ ; Files = []
346
+ ).
347
+ source_matching(_, literal).
348
+
349
+ member_separator(".").
350
+ member_separator("/").
351
+ member_separator("::").
352
+
353
+ % extends_member(+Member, +Stem): Member is Stem or lies under it at a
354
+ % segment boundary. The empty stem (from the item "*") covers everything.
355
+ extends_member(_, "") :- !.
356
+ extends_member(Member, Stem) :- Member == Stem, !.
357
+ extends_member(Member, Stem) :-
358
+ member_separator(Separator),
359
+ string_concat(Stem, Separator, Prefix),
360
+ string_concat(Prefix, _, Member),
361
+ !.
362
+
363
+ % pattern_stem(+Item, -Stem): Item ends in *; Stem drops it and the separator
364
+ % before it (java.sql.* -> java.sql, * -> "").
365
+ pattern_stem(Item, Stem) :-
366
+ \+ unknown_receiver(Item, _),
367
+ string_concat(Base, "*", Item),
368
+ ( member_separator(Separator), string_concat(Stem0, Separator, Base)
369
+ -> Stem = Stem0
370
+ ; Stem = Base
371
+ ).
372
+
373
+ % unknown_receiver(+Member, -Method): Member is *.Method.
374
+ unknown_receiver(Member, Method) :-
375
+ string_concat("*.", Method, Member),
376
+ Method \== "".
377
+
378
+ % ends_with_method(+Member, +Method): Member's last segment is Method.
379
+ ends_with_method(Member, Method) :-
380
+ member_separator(Separator),
381
+ string_concat(Separator, Method, Suffix),
382
+ string_concat(_, Suffix, Member),
383
+ !.
384
+
385
+ % exclude_match(+Actual, +Expected, -How): Actual is (certain) or may be
386
+ % (possible) what Expected names.
387
+ exclude_match(Actual, Expected, certain) :- Actual == Expected, !.
388
+ exclude_match(Actual, Expected, certain) :-
389
+ unknown_receiver(Expected, Method),
390
+ \+ unknown_receiver(Actual, _),
391
+ ends_with_method(Actual, Method),
392
+ !.
393
+ exclude_match(Actual, Expected, possible) :-
394
+ unknown_receiver(Actual, Method),
395
+ !,
396
+ ( pattern_stem(Expected, _)
397
+ ; ends_with_method(Expected, Method)
398
+ ),
399
+ !.
400
+ exclude_match(Actual, Expected, How) :-
401
+ pattern_stem(Actual, ActualStem),
402
+ !,
403
+ ( pattern_stem(Expected, Target) -> true ; Target = Expected ),
404
+ ( extends_member(ActualStem, Target)
405
+ -> How = certain
406
+ ; extends_member(Target, ActualStem)
407
+ -> How = possible
408
+ ).
409
+ exclude_match(Actual, Expected, How) :-
410
+ ( pattern_stem(Expected, Stem)
411
+ -> extends_member(Actual, Stem),
412
+ How = certain
413
+ ; extends_member(Actual, Expected)
414
+ -> How = certain
415
+ ; ends_with_method(Expected, Actual)
416
+ -> How = possible
417
+ ; ends_with_method(Actual, Expected)
418
+ -> How = possible
419
+ ).
420
+
421
+ % certain_match(+Actual, +Expected): Actual certainly is what Expected names
422
+ % (includes, subset_of).
423
+ certain_match(Actual, Expected) :- Actual == Expected, !.
424
+ certain_match(_, "*") :- !.
425
+ certain_match(Actual, Expected) :-
426
+ \+ unknown_receiver(Actual, _),
427
+ ( pattern_stem(Expected, Stem)
428
+ -> ( pattern_stem(Actual, ActualStem)
429
+ -> extends_member(ActualStem, Stem)
430
+ ; extends_member(Actual, Stem)
431
+ )
432
+ ; unknown_receiver(Expected, Method)
433
+ -> \+ pattern_stem(Actual, _),
434
+ ends_with_method(Actual, Method)
435
+ ),
436
+ !.
437
+
438
+ files_text([], "the files read") :- !.
439
+ files_text(Files, Text) :-
440
+ length(Files, N),
441
+ ( N > 3
442
+ -> length(Shown, 3), append(Shown, _, Files),
443
+ atomic_list_concat(Shown, ', ', Listed),
444
+ Extra is N - 3,
445
+ format(string(Text), "~w and ~w more", [Listed, Extra])
446
+ ; atomic_list_concat(Files, ', ', Text)
447
+ ).
448
+
449
+ excluded_text(Files, Actual-Expected-How, Text) :-
450
+ ( How == certain, Actual == Expected
451
+ -> Text = Actual
452
+ ; How == certain
453
+ -> format(string(Text), "~w (within ~w)", [Actual, Expected])
454
+ ; unknown_receiver(Actual, Method)
455
+ -> files_text(Files, Where),
456
+ format(string(Text), "~w (a call of ~w on a receiver Kibi cannot name, in ~w; it may be ~w)", [Actual, Method, Where, Expected])
457
+ ; pattern_stem(Actual, _)
458
+ -> format(string(Text), "~w (a wildcard that may cover ~w)", [Actual, Expected])
459
+ ; ends_with_method(Actual, Expected)
460
+ -> format(string(Text), "~w (it may be the ~w the fact excludes)", [Actual, Expected])
461
+ ; format(string(Text), "~w (a shorter name that may be ~w)", [Actual, Expected])
462
+ ).
463
+
464
+ % reference_relation_problem(+Operator, +Actual, +Expected, +Files, +What,
465
+ % -Reason) fails when the relation holds under the reference matching above.
466
+ reference_relation_problem("excludes", Actual, Expected, Files, What, Reason) :-
467
+ !,
468
+ findall(A-E-How, (member(A, Actual), member(E, Expected), exclude_match(A, E, How)), Pairs0),
469
+ sort(Pairs0, Pairs),
470
+ Pairs \== [],
471
+ include(certain_pair, Pairs, Certain),
472
+ maplist(excluded_text(Files), Pairs, Texts),
473
+ atomic_list_concat(Texts, ', ', Text),
474
+ ( Certain \== []
475
+ -> format(string(Reason), "~w include ~w, which the fact excludes", [What, Text])
476
+ ; format(string(Reason), "~w include ~w, which the fact excludes; Kibi cannot decide the claim for those members", [What, Text])
477
+ ).
478
+ reference_relation_problem("includes", Actual, Expected, _, What, Reason) :-
479
+ !,
480
+ exclude(matched_by_some(Actual), Expected, Missing),
481
+ Missing \== [],
482
+ atomic_list_concat(Missing, ', ', Text),
483
+ format(string(Reason), "~w do not include ~w", [What, Text]).
484
+ reference_relation_problem("subset_of", Actual, Expected, _, What, Reason) :-
485
+ !,
486
+ exclude(allowed_by_some(Expected), Actual, Extra),
487
+ Extra \== [],
488
+ atomic_list_concat(Extra, ', ', Text),
489
+ ( member(Member, Extra), unknown_receiver(Member, _)
490
+ -> format(string(Reason), "~w include ~w, which the fact's set does not allow (a call on a receiver Kibi cannot name is allowed only when the set lists it as *.m)", [What, Text])
491
+ ; format(string(Reason), "~w include ~w, which the fact's set does not allow", [What, Text])
492
+ ).
493
+ reference_relation_problem(Operator, Actual, Expected, _, What, Reason) :-
494
+ relation_problem(Operator, Actual, Expected, What, Reason).
495
+
496
+ certain_pair(_-_-certain).
497
+
498
+ matched_by_some(Actual, Expected) :-
499
+ member(Member, Actual),
500
+ certain_match(Member, Expected),
501
+ !.
502
+
503
+ allowed_by_some(Expected, Member) :-
504
+ member(Item, Expected),
505
+ certain_match(Member, Item),
506
+ !.
507
+
309
508
  % ---------------------------------------------------------------------------
310
509
  % Obligations
311
510
 
@@ -397,14 +596,18 @@ obligation_result("machine_relation", Obligation, model(_, _, Machine), Result)
397
596
  ; Result = ok
398
597
  )
399
598
  ).
400
- obligation_result("set_relation", Obligation, model(_, _, source(Actual, _, _, _)), Result) :- !,
599
+ obligation_result("set_relation", Obligation, model(_, _, source(Actual, _, _, _, Matching)), Result) :- !,
401
600
  dict_text(Obligation, operator, Operator),
402
601
  obligation_strings(Obligation, Expected),
403
- ( relation_problem(Operator, Actual, Expected, "the members Kibi read from the source", Reason)
602
+ What = "the members Kibi read from the source",
603
+ ( ( Matching = references(Files)
604
+ -> reference_relation_problem(Operator, Actual, Expected, Files, What, Reason)
605
+ ; relation_problem(Operator, Actual, Expected, What, Reason)
606
+ )
404
607
  -> Result = problem(Reason)
405
608
  ; Result = ok
406
609
  ).
407
- obligation_result(FieldKind, Obligation, model(_, _, source(_, Entities, _, _)), Result) :-
610
+ obligation_result(FieldKind, Obligation, model(_, _, source(_, Entities, _, _, _)), Result) :-
408
611
  memberchk(FieldKind, ["required_fields", "nullable_fields"]),
409
612
  !,
410
613
  ( is_list(Entities), Entities \== []
@@ -417,7 +620,7 @@ obligation_result(FieldKind, Obligation, model(_, _, source(_, Entities, _, _)),
417
620
  -> Result = problem(Reason)
418
621
  ; Result = ok
419
622
  ).
420
- obligation_result("route_middleware", Obligation, model(_, _, source(_, _, Routes, _)), Result) :- !,
623
+ obligation_result("route_middleware", Obligation, model(_, _, source(_, _, Routes, _, _)), Result) :- !,
421
624
  ( is_list(Routes), Routes \== []
422
625
  -> true
423
626
  ; throw(model_error("the source has no routes; read a route table for a middleware claim"))
@@ -436,7 +639,7 @@ obligation_result("route_middleware", Obligation, model(_, _, source(_, _, Route
436
639
  ; Result = ok
437
640
  ).
438
641
  % implements REQ-kibi-source-proof-kinds
439
- obligation_result("sequence_precedes", Obligation, model(_, _, source(_, _, _, Sequence)), Result) :- !,
642
+ obligation_result("sequence_precedes", Obligation, model(_, _, source(_, _, _, Sequence, _)), Result) :- !,
440
643
  ( is_list(Sequence)
441
644
  -> true
442
645
  ; throw(model_error("the source has no order; read a list for a precedes claim"))
@@ -2402,15 +2402,15 @@ receipt_claim_fields(Receipt, Evidence0, Evidence) :-
2402
2402
  claim_evidence(ReqId, Props, Inventory, ProofStatus, ScenarioTestStage, Context,
2403
2403
  ClaimEvidence, ProvedTests, Rejections) :-
2404
2404
  ( get_dict(tests, ScenarioTestStage, Tests) -> true ; Tests = [] ),
2405
- ( ProofStatus == tested
2406
- -> findall(TestId-Evidence,
2407
- (member(TestId, Tests),
2408
- test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
2409
- get_dict(state, Evidence, passed),
2410
- get_dict(claimBindings, Evidence, _)),
2411
- BoundTests)
2412
- ; BoundTests = []
2413
- ),
2405
+ % The bindings are read whatever the proof status, so a claim that is not
2406
+ % proved because the requirement is not tested end to end still shows the
2407
+ % binding each receipt holds, and a rejected binding its reason.
2408
+ findall(TestId-Evidence,
2409
+ (member(TestId, Tests),
2410
+ test_receipt_evidence(Context, [unit, integration, end_to_end], TestId, Evidence),
2411
+ get_dict(state, Evidence, passed),
2412
+ get_dict(claimBindings, Evidence, _)),
2413
+ BoundTests),
2414
2414
  intent_claim_level(Props, IntentLevel),
2415
2415
  maplist(claim_entry(ReqId, ProofStatus, IntentLevel, BoundTests), Inventory, Entries0),
2416
2416
  exclude(==(none), Entries0, ClaimEvidence),
@@ -2458,17 +2458,25 @@ claim_entry(ReqId, ProofStatus, IntentLevel, BoundTests, InventoryEntry, Entry)
2458
2458
  ; Status \== modeled
2459
2459
  -> Entry = Base.put(_{applicable: true, level: IntentLevel, bindings: [],
2460
2460
  blocker: unresolved_semantic_proposition})
2461
- ; ProofStatus \== tested
2462
- -> Entry = Base.put(_{applicable: true, level: IntentLevel, bindings: []})
2463
2461
  ; findall(Binding, test_claim_binding(ReqId, Key, BoundTests, Binding), Bindings0),
2464
2462
  sort(Bindings0, Bindings),
2465
2463
  include(binding_is(rejected), Bindings, RejectedBindings),
2466
2464
  maplist(binding_rejection(Key), RejectedBindings, Rejections),
2467
- ( member(B, Bindings), get_dict(binding, B, derived), \+ get_dict(scope, B, intent)
2468
- -> Level = proved
2469
- ; Level = tested
2465
+ ( ProofStatus \== tested
2466
+ -> % A derived binding proves nothing until the requirement is
2467
+ % tested end to end; the claim keeps the intent level and
2468
+ % says why.
2469
+ Entry1 = Base.put(_{applicable: true, level: IntentLevel, bindings: Bindings}),
2470
+ ( member(B, Bindings), get_dict(binding, B, derived)
2471
+ -> Entry0 = Entry1.put(notProved, requirement_not_tested)
2472
+ ; Entry0 = Entry1
2473
+ )
2474
+ ; ( member(B, Bindings), get_dict(binding, B, derived), \+ get_dict(scope, B, intent)
2475
+ -> Level = proved
2476
+ ; Level = tested
2477
+ ),
2478
+ Entry0 = Base.put(_{applicable: true, level: Level, bindings: Bindings})
2470
2479
  ),
2471
- Entry0 = Base.put(_{applicable: true, level: Level, bindings: Bindings}),
2472
2480
  ( Rejections == []
2473
2481
  -> Entry = Entry0
2474
2482
  ; Entry = Entry0.put(rejections, Rejections)
@@ -18,6 +18,7 @@ known_rule('must-priority-coverage').
18
18
  known_rule('symbol-coverage').
19
19
  known_rule('symbol-traceability').
20
20
  known_rule('symbol-granularity').
21
+ known_rule('symbol-coordinates-missing').
21
22
  known_rule('no-dangling-refs').
22
23
  known_rule('source-relationship-parity').
23
24
  known_rule('branch-store-not-compiled').
@@ -77,6 +78,7 @@ rule_enforcement_class('must-priority-coverage', canonical).
77
78
  rule_enforcement_class('symbol-coverage', canonical).
78
79
  rule_enforcement_class('symbol-traceability', canonical).
79
80
  rule_enforcement_class('symbol-granularity', canonical).
81
+ rule_enforcement_class('symbol-coordinates-missing', advisory).
80
82
  rule_enforcement_class('no-dangling-refs', canonical).
81
83
  rule_enforcement_class('source-relationship-parity', canonical).
82
84
  rule_enforcement_class('branch-store-not-compiled', canonical).
@@ -136,6 +138,7 @@ rule_implementation('must-priority-coverage', prolog).
136
138
  rule_implementation('symbol-coverage', prolog).
137
139
  rule_implementation('symbol-traceability', prolog).
138
140
  rule_implementation('symbol-granularity', typescript).
141
+ rule_implementation('symbol-coordinates-missing', typescript).
139
142
  rule_implementation('no-dangling-refs', prolog).
140
143
  rule_implementation('source-relationship-parity', typescript).
141
144
  rule_implementation('branch-store-not-compiled', typescript).
@@ -271,6 +274,7 @@ rule_description('must-priority-coverage', 'Every current must-priority requirem
271
274
  rule_description('symbol-coverage', 'Production symbols need qualifying coverage via covered_by plus a canonical requirement/scenario test path').
272
275
  rule_description('symbol-traceability', 'Production symbols must directly implement requirements for ownership; covered_by is coverage only and executable_for is test identity only').
273
276
  rule_description('symbol-granularity', 'A symbol with traceability relationships must not link its source file coarsely while narrower behavioral symbols are available, unless it carries an allowed granularity_reason; the staged gate\'s symbol_granularity_violation evaluated KB-wide').
277
+ rule_description('symbol-coordinates-missing', 'Advisory: an active symbol names a source file that exists, but Kibi could not place it there (the declaration was not found, the file is unreadable to the analyzer, or a declared plugin or grammar pack is missing), so its link is checked against no code').
274
278
  rule_description('no-dangling-refs', 'All relationship targets must exist in the KB').
275
279
  rule_description('source-relationship-parity', 'Authored Markdown and relationship-shard edges must exactly match the compiled KB').
276
280
  rule_description('branch-store-not-compiled', 'The current branch\'s KB store must be compiled from the authored .kb/ sources; a missing or empty store is reported once, naming kibi sync, instead of every store-backed rule reading an empty KB').