kibi-core 0.7.1 → 0.9.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/src/checks.pl CHANGED
@@ -14,11 +14,15 @@
14
14
  check_required_fields/1, % Returns list of missing required field violations
15
15
  check_deprecated_adrs/1, % Returns list of deprecated ADR violations
16
16
  check_domain_contradictions/1, % Returns list of contradiction violations
17
+ check_domain_contradiction_witnesses/1,
17
18
  check_strict_fact_shape/1, % Returns list of malformed strict fact violations
18
19
  check_strict_req_fact_pairing/1,% Returns list of malformed strict req/fact pairing violations
19
20
  check_strict_readiness/1, % Returns list of strict readiness audit violations
20
21
  check_predicate_verifiability/1,% Returns list of unverifiable predicate-lane requirement links
21
22
  check_logic_coverage/1, % Returns list of incomplete declared logical claim manifests
23
+ check_rule_safety/1,
24
+ check_rule_verifiability/1,
25
+ check_semantic_completeness/1,
22
26
  run_checks_json/0, % Entry point for JSON output
23
27
  violation_id_text/2 % Extract text from entity ID term (exported for testing)
24
28
  ]).
@@ -28,6 +32,7 @@
28
32
  :- use_module('kb.pl').
29
33
  :- use_module('../schema/entities.pl', [entity_type/1, required_property/2]).
30
34
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
35
+ :- use_module('logic_ir.pl', [logic_rule_safety/2, logic_rule_from_props/2, logic_rule_conflict/3, logic_rule_conflict_witness/3, logic_rules_stratified/1]).
31
36
 
32
37
  % Required fields for all entities
33
38
  required_fields([id, title, status, created_at, updated_at, source]).
@@ -35,7 +40,7 @@ required_fields([id, title, status, created_at, updated_at, source]).
35
40
  % Relationship types to check for dangling references
36
41
  all_relationship_types([
37
42
  depends_on, verified_by, validates, specified_by,
38
- constrains, requires_property, requires_predicate, supersedes, relates_to
43
+ constrains, requires_property, requires_predicate, requires_rule, supersedes, relates_to
39
44
  ]).
40
45
 
41
46
  %% check_all(-ViolationsDict)
@@ -56,6 +61,9 @@ check_all(ViolationsDict) :-
56
61
  check_strict_readiness(StrictReadiness),
57
62
  check_predicate_verifiability(PredicateVerifiability),
58
63
  check_logic_coverage(LogicCoverage),
64
+ check_rule_safety(RuleSafety),
65
+ check_rule_verifiability(RuleVerifiability),
66
+ check_semantic_completeness(SemanticCompleteness),
59
67
  ViolationsDict = _{
60
68
  must_priority_coverage: MustPriority,
61
69
  symbol_coverage: SymbolCoverage,
@@ -69,7 +77,10 @@ check_all(ViolationsDict) :-
69
77
  strict_req_fact_pairing: StrictReqFactPairing,
70
78
  strict_readiness: StrictReadiness,
71
79
  predicate_verifiability: PredicateVerifiability,
72
- logic_coverage: LogicCoverage
80
+ logic_coverage: LogicCoverage,
81
+ rule_safety: RuleSafety,
82
+ rule_verifiability: RuleVerifiability,
83
+ semantic_completeness: SemanticCompleteness
73
84
  }.
74
85
 
75
86
  %% check_must_priority_coverage(-Violations)
@@ -408,9 +419,25 @@ strict_fact_shape_violation(violation(
408
419
  Description = First,
409
420
  Suggestion = "Ensure predicate facts have predicate_name, non-empty predicate_args, canonical_key, and assert/deny polarity when present"
410
421
  )
422
+ ; Kind = rule_schema
423
+ -> findall(Msg, rule_schema_shape_error(Props, Msg), Errors),
424
+ ( Errors = []
425
+ -> fail
426
+ ; Errors = [First|_],
427
+ Description = First,
428
+ Suggestion = "Ensure rule_schema facts declare rule_name plus same-length argument_names and argument_types"
429
+ )
430
+ ; Kind = rule
431
+ -> findall(Msg, rule_shape_error(Props, Msg), Errors),
432
+ ( Errors = []
433
+ -> fail
434
+ ; Errors = [First|_],
435
+ Description = First,
436
+ Suggestion = "Ensure rule facts contain a safe kibi.logic.v1 rule_ir, full rule_hash, rule_schema_id, rule_name, and semantic_key"
437
+ )
411
438
  ; % Unknown fact_kind - report as malformed
412
439
  format(string(Description), "Unknown fact_kind: ~w", [Kind]),
413
- Suggestion = "Use one of: subject, property_value, observation, meta, predicate_schema, predicate"
440
+ Suggestion = "Use one of: subject, property_value, observation, meta, predicate_schema, predicate, rule_schema, rule"
414
441
  ),
415
442
 
416
443
  ( memberchk(source=Source0, Props)
@@ -503,6 +530,42 @@ predicate_shape_error(Props, "Predicate fact polarity must be assert or deny") :
503
530
  normalize_term_atom(RawPolarity, Polarity),
504
531
  \+ memberchk(Polarity, [assert, deny]).
505
532
 
533
+ rule_schema_shape_error(Props, "Rule schema fact missing required field: rule_name") :-
534
+ \+ memberchk(rule_name=_, Props).
535
+ rule_schema_shape_error(Props, "Rule schema fact missing required field: argument_names") :-
536
+ memberchk(rule_name=_, Props),
537
+ \+ memberchk(argument_names=_, Props).
538
+ rule_schema_shape_error(Props, "Rule schema fact missing required field: argument_types") :-
539
+ memberchk(argument_names=_, Props),
540
+ \+ memberchk(argument_types=_, Props).
541
+ rule_schema_shape_error(Props, "Rule schema fact argument_names and argument_types must have equal lengths") :-
542
+ memberchk(argument_names=RawNames, Props),
543
+ memberchk(argument_types=RawTypes, Props),
544
+ checks_normalize_atom_list(RawNames, Names),
545
+ checks_normalize_atom_list(RawTypes, Types),
546
+ length(Names, NameCount),
547
+ length(Types, TypeCount),
548
+ NameCount =\= TypeCount.
549
+
550
+ rule_shape_error(Props, "Rule fact missing required field: rule_ir") :-
551
+ \+ memberchk(rule_ir=_, Props).
552
+ rule_shape_error(Props, "Rule fact missing required field: rule_hash") :-
553
+ memberchk(rule_ir=_, Props),
554
+ \+ memberchk(rule_hash=_, Props).
555
+ rule_shape_error(Props, "Rule fact missing required field: rule_schema_id") :-
556
+ memberchk(rule_hash=_, Props),
557
+ \+ memberchk(rule_schema_id=_, Props).
558
+ rule_shape_error(Props, "Rule fact missing required field: rule_name") :-
559
+ memberchk(rule_schema_id=_, Props),
560
+ \+ memberchk(rule_name=_, Props).
561
+ rule_shape_error(Props, "Rule fact missing required field: semantic_key") :-
562
+ memberchk(rule_name=_, Props),
563
+ \+ memberchk(semantic_key=_, Props).
564
+ rule_shape_error(Props, "Rule fact rule_ir failed kibi.logic.v1 safety validation") :-
565
+ memberchk(rule_ir=_, Props),
566
+ logic_rule_safety(Props, Errors),
567
+ Errors \= [].
568
+
506
569
  checks_normalize_integer(Raw, Integer) :-
507
570
  ( Raw = ^^(Value, _Type)
508
571
  -> checks_normalize_integer(Value, Integer)
@@ -525,19 +588,140 @@ checks_normalize_atom_list(Raw, Atoms) :-
525
588
  %% check_domain_contradictions(-Violations)
526
589
  % Finds all pairs of requirements with contradicting required properties.
527
590
  check_domain_contradictions(Violations) :-
528
- findall(
529
- violation(
530
- 'domain-contradictions',
531
- EntityId,
532
- Description,
533
- "Supersede one requirement or align both to the same required property",
534
- ""
535
- ),
536
- ( contradicting_reqs(ReqA, ReqB, Reason),
537
- format(string(EntityId), "~w/~w", [ReqA, ReqB]),
538
- format(string(Description), "~w [strict-readiness: contradiction-ready]", [Reason])
539
- ),
540
- Violations
591
+ check_domain_contradiction_witnesses(Witnesses),
592
+ maplist(contradiction_witness_violation, Witnesses, Violations).
593
+
594
+ %% check_domain_contradiction_witnesses(-Witnesses)
595
+ % Exact, JSON-safe evidence for every strict-property, ground-predicate, and
596
+ % safe-rule contradiction result. Rule overlap that cannot be proven or
597
+ % disproven remains status=unresolved rather than becoming consistency.
598
+ check_domain_contradiction_witnesses(Witnesses) :-
599
+ findall(Witness, req_conflict_witness(_ReqA, _ReqB, Witness), GroundWitnesses),
600
+ findall(Witness, rule_contradiction_witness(Witness), RuleWitnesses),
601
+ append(GroundWitnesses, RuleWitnesses, Witnesses0),
602
+ sort(Witnesses0, Witnesses).
603
+
604
+ contradiction_witness_violation(Witness, violation(
605
+ 'domain-contradictions',
606
+ EntityId,
607
+ Description,
608
+ Suggestion,
609
+ ""
610
+ )) :-
611
+ Witness.requirements = [ReqA, ReqB],
612
+ format(string(EntityId), "~w/~w", [ReqA, ReqB]),
613
+ ( Witness.kind == rule
614
+ -> Description = Witness.reason,
615
+ Suggestion = "Align the opposing rule heads, separate their scopes, or add an explicit supersedes relationship"
616
+ ; format(string(Description), "~w [strict-readiness: contradiction-ready]", [Witness.reason]),
617
+ Suggestion = "Supersede one requirement or align both to the same canonical logical term"
618
+ ).
619
+
620
+ opposing_rule_requirements(ReqA, ReqB, RuleA, RuleB) :-
621
+ opposing_rule_requirement_facts(ReqA, ReqB, _FactA, _FactB, RuleA, RuleB).
622
+
623
+ opposing_rule_requirement_facts(ReqA, ReqB, FactA, FactB, RuleA, RuleB) :-
624
+ kb:current_req(ReqA),
625
+ kb:current_req(ReqB),
626
+ ReqA @< ReqB,
627
+ kb_relationship(requires_rule, ReqA, FactA),
628
+ kb_relationship(requires_rule, ReqB, FactB),
629
+ FactA @< FactB,
630
+ kb_entity(FactA, fact, PropsA),
631
+ kb_entity(FactB, fact, PropsB),
632
+ memberchk(fact_kind=KindA, PropsA),
633
+ memberchk(fact_kind=KindB, PropsB),
634
+ normalize_term_atom(KindA, rule),
635
+ normalize_term_atom(KindB, rule),
636
+ logic_rule_from_props(PropsA, RuleA),
637
+ logic_rule_from_props(PropsB, RuleB).
638
+
639
+ rule_contradiction_witness(Witness) :-
640
+ opposing_rule_requirement_facts(ReqA, ReqB, FactA, FactB, RuleA, RuleB),
641
+ logic_rule_conflict_witness(RuleA, RuleB, InternalWitness),
642
+ InternalWitness.status \= disjoint,
643
+ rule_conflict_side(ReqA, FactA, Left),
644
+ rule_conflict_side(ReqB, FactB, Right),
645
+ rule_comparison_evidence(InternalWitness, Comparison),
646
+ format(string(Reason), "Rule conflict (~w) between ~w and ~w", [InternalWitness.status, ReqA, ReqB]),
647
+ Witness = _{
648
+ kind: rule,
649
+ status: InternalWitness.status,
650
+ requirements: [ReqA, ReqB],
651
+ reason: Reason,
652
+ left: Left,
653
+ right: Right,
654
+ comparison: Comparison
655
+ }.
656
+
657
+ rule_conflict_side(ReqId, FactId, Side) :-
658
+ evidence_entity_source(ReqId, req, RequirementSource),
659
+ kb_entity(FactId, fact, Props),
660
+ evidence_property_text(Props, source, FactSource),
661
+ evidence_property_text(Props, claim_key, ClaimKey),
662
+ evidence_property_text(Props, claim_text, ClaimText),
663
+ evidence_property_text(Props, rule_hash, RuleHash),
664
+ evidence_property_text(Props, semantic_key, SemanticKey),
665
+ evidence_property_text(Props, rule_ir, RuleIr),
666
+ evidence_property_integer(Props, claim_span_start, ClaimSpanStart),
667
+ evidence_property_integer(Props, claim_span_end, ClaimSpanEnd),
668
+ Side = _{
669
+ requirementId: ReqId,
670
+ requirementSource: RequirementSource,
671
+ factId: FactId,
672
+ factSource: FactSource,
673
+ claimKey: ClaimKey,
674
+ claimText: ClaimText,
675
+ claimSpan: _{start: ClaimSpanStart, end: ClaimSpanEnd},
676
+ ruleHash: RuleHash,
677
+ semanticKey: SemanticKey,
678
+ ruleIr: RuleIr
679
+ }.
680
+
681
+ rule_comparison_evidence(Internal, Evidence) :-
682
+ term_string(Internal.head_a, HeadA, [quoted(true), max_depth(0)]),
683
+ term_string(Internal.head_b, HeadB, [quoted(true), max_depth(0)]),
684
+ term_string(Internal.body_a, BodyA, [quoted(true), max_depth(0)]),
685
+ term_string(Internal.body_b, BodyB, [quoted(true), max_depth(0)]),
686
+ term_string(Internal.scope_a, ScopeA, [quoted(true), max_depth(0)]),
687
+ term_string(Internal.scope_b, ScopeB, [quoted(true), max_depth(0)]),
688
+ Evidence = _{
689
+ modalityA: Internal.modality_a,
690
+ modalityB: Internal.modality_b,
691
+ headA: HeadA,
692
+ headB: HeadB,
693
+ bodyA: BodyA,
694
+ bodyB: BodyB,
695
+ scopeA: ScopeA,
696
+ scopeB: ScopeB,
697
+ validFromA: Internal.valid_from_a,
698
+ validToA: Internal.valid_to_a,
699
+ validFromB: Internal.valid_from_b,
700
+ validToB: Internal.valid_to_b,
701
+ ruleSchemaA: Internal.rule_schema_a,
702
+ ruleSchemaB: Internal.rule_schema_b
703
+ }.
704
+
705
+ evidence_entity_source(EntityId, Type, Source) :-
706
+ kb_entity(EntityId, Type, Props),
707
+ evidence_property_text(Props, source, Source).
708
+
709
+ evidence_property_text(Props, Key, Text) :-
710
+ (memberchk(Key=Raw, Props) -> evidence_term_text(Raw, Text) ; Text = '').
711
+
712
+ evidence_property_integer(Props, Key, Integer) :-
713
+ (memberchk(Key=Raw, Props), catch(checks_normalize_integer(Raw, Integer), _, fail) -> true ; Integer = -1).
714
+
715
+ evidence_term_text(Raw, Text) :-
716
+ ( Raw = ^^(Value, _Type)
717
+ -> evidence_term_text(Value, Text)
718
+ ; Raw = literal(type(_, Value))
719
+ -> evidence_term_text(Value, Text)
720
+ ; atom(Raw)
721
+ -> Text = Raw
722
+ ; string(Raw)
723
+ -> atom_string(Text, Raw)
724
+ ; term_string(Raw, String), atom_string(Text, String)
541
725
  ).
542
726
 
543
727
  %% check_strict_req_fact_pairing(-Violations)
@@ -816,7 +1000,7 @@ logic_coverage_violation(violation(
816
1000
  'logic-coverage',
817
1001
  ReqId,
818
1002
  Description,
819
- "Ground every declared claim with a property_value fact via requires_property or a predicate fact via requires_predicate, preserving the same claim_key",
1003
+ "Ground every declared claim with a property_value, predicate, or rule fact via its typed relationship, preserving the same claim_key",
820
1004
  Source
821
1005
  )) :-
822
1006
  kb:current_req(ReqId),
@@ -901,6 +1085,9 @@ grounded_requirement_claim_fact(ReqId, ClaimKey, FactId) :-
901
1085
  grounded_requirement_claim_fact(ReqId, ClaimKey, FactId) :-
902
1086
  kb_relationship(requires_predicate, ReqId, FactId),
903
1087
  ground_fact_claim_key(FactId, predicate, ClaimKey).
1088
+ grounded_requirement_claim_fact(ReqId, ClaimKey, FactId) :-
1089
+ kb_relationship(requires_rule, ReqId, FactId),
1090
+ ground_fact_claim_key(FactId, rule, ClaimKey).
904
1091
 
905
1092
  logical_ground_signature(FactId, predicate(Namespace, Name, Args, Polarity)) :-
906
1093
  kb:predicate_fact(FactId, Namespace, Name, Args, Polarity).
@@ -919,6 +1106,14 @@ logical_ground_signature(
919
1106
  Scope,
920
1107
  Polarity
921
1108
  ).
1109
+ logical_ground_signature(FactId, rule(SemanticKey)) :-
1110
+ kb_entity(FactId, fact, Props),
1111
+ memberchk(fact_kind=RawKind, Props),
1112
+ normalize_term_atom(RawKind, rule),
1113
+ ( memberchk(semantic_key=RawKey, Props)
1114
+ -> normalize_term_atom(RawKey, SemanticKey)
1115
+ ; memberchk(rule_hash=RawKey, Props), normalize_term_atom(RawKey, SemanticKey)
1116
+ ).
922
1117
 
923
1118
  ground_fact_claim_key(FactId, ExpectedKind, ClaimKey) :-
924
1119
  kb_entity(FactId, fact, Props),
@@ -930,6 +1125,175 @@ ground_fact_claim_key(FactId, ExpectedKind, ClaimKey) :-
930
1125
  normalize_term_atom(RawClaimText, ClaimText),
931
1126
  ClaimText \= ''.
932
1127
 
1128
+ %% check_rule_safety(-Violations)
1129
+ % Every stored rule must decode through the closed Logic IR vocabulary and
1130
+ % pass the independent Prolog-side safety checks.
1131
+ check_rule_safety(Violations) :-
1132
+ findall(Violation, rule_safety_violation(Violation), Raw),
1133
+ sort(Raw, Violations).
1134
+
1135
+ rule_safety_violation(violation(
1136
+ 'rule-safety',
1137
+ FactId,
1138
+ Description,
1139
+ "Use kibi.logic.v1 typed IR; remove raw Prolog, unsafe variables, or unstratified negation",
1140
+ Source
1141
+ )) :-
1142
+ kb_entity(FactId, fact, Props),
1143
+ memberchk(fact_kind=RawKind, Props),
1144
+ normalize_term_atom(RawKind, rule),
1145
+ logic_rule_safety(Props, Errors),
1146
+ Errors \= [],
1147
+ format(string(Description), "Rule fact ~w failed safety validation: ~w", [FactId, Errors]),
1148
+ violation_source(FactId, fact, Source).
1149
+
1150
+ % Local rule checks cannot detect a negation cycle spanning multiple facts.
1151
+ % Run the finite dependency-graph check over the complete stored rule set and
1152
+ % report each participating negated rule as infrastructure-grade unsafe IR.
1153
+ rule_safety_violation(violation(
1154
+ 'rule-safety',
1155
+ FactId,
1156
+ "Stored rules contain an unstratified negation dependency cycle",
1157
+ "Separate the negated dependency into strata or replace it with an explicit closed-world fact",
1158
+ Source
1159
+ )) :-
1160
+ findall(Rule, stored_rule_term(Rule), Rules),
1161
+ Rules \= [],
1162
+ \+ logic_rules_stratified(Rules),
1163
+ stored_rule_fact(FactId, _Props, Rule),
1164
+ rule_contains_negation(Rule),
1165
+ violation_source(FactId, fact, Source).
1166
+
1167
+ stored_rule_term(Rule) :-
1168
+ kb_entity(_FactId, fact, Props),
1169
+ memberchk(fact_kind=RawKind, Props),
1170
+ normalize_term_atom(RawKind, rule),
1171
+ logic_rule_from_props(Props, Rule).
1172
+
1173
+ stored_rule_fact(FactId, Props, Rule) :-
1174
+ kb_entity(FactId, fact, Props),
1175
+ memberchk(fact_kind=RawKind, Props),
1176
+ normalize_term_atom(RawKind, rule),
1177
+ logic_rule_from_props(Props, Rule).
1178
+
1179
+ rule_contains_negation(rule(_, _, _Head, Body, Exceptions, _Scope, _From, _To, _Schema, _Variables)) :-
1180
+ ( expression_contains_negation(Body)
1181
+ ; member(Exception, Exceptions), expression_contains_negation(Exception)
1182
+ ),
1183
+ !.
1184
+
1185
+ expression_contains_negation(not(_)).
1186
+ expression_contains_negation(all(Items)) :- member(Item, Items), expression_contains_negation(Item).
1187
+ expression_contains_negation(any(Items)) :- member(Item, Items), expression_contains_negation(Item).
1188
+ expression_contains_negation(count(Atom, _Operator, _Value)) :- expression_contains_negation(Atom).
1189
+
1190
+ %% check_rule_verifiability(-Violations)
1191
+ % A requires_rule edge is meaningful only when it reaches a rule fact whose
1192
+ % schema endpoint exists and whose rule IR is safe.
1193
+ check_rule_verifiability(Violations) :-
1194
+ findall(Violation, rule_verifiability_violation(Violation), Raw),
1195
+ sort(Raw, Violations).
1196
+
1197
+ rule_verifiability_violation(violation(
1198
+ 'rule-verifiability',
1199
+ ReqId,
1200
+ Description,
1201
+ "Link requires_rule to fact_kind=rule with a valid rule_schema_id and safe kibi.logic.v1 IR",
1202
+ Source
1203
+ )) :-
1204
+ kb:current_req(ReqId),
1205
+ kb_relationship(requires_rule, ReqId, FactId),
1206
+ kb_entity(FactId, fact, Props),
1207
+ rule_verifiability_description(Props, FactId, Description),
1208
+ violation_source(ReqId, req, Source).
1209
+
1210
+ rule_verifiability_description(Props, FactId, Description) :-
1211
+ ( memberchk(fact_kind=RawKind, Props)
1212
+ -> normalize_term_atom(RawKind, Kind)
1213
+ ; Kind = legacy
1214
+ ),
1215
+ Kind \= rule,
1216
+ format(string(Description), "Requirement uses requires_rule ~w but target is fact_kind=~w", [FactId, Kind]).
1217
+ rule_verifiability_description(Props, FactId, Description) :-
1218
+ memberchk(fact_kind=RawKind, Props),
1219
+ normalize_term_atom(RawKind, rule),
1220
+ \+ memberchk(rule_schema_id=_, Props),
1221
+ format(string(Description), "Rule fact ~w has no rule_schema_id", [FactId]).
1222
+ rule_verifiability_description(Props, FactId, Description) :-
1223
+ memberchk(fact_kind=RawKind, Props),
1224
+ normalize_term_atom(RawKind, rule),
1225
+ memberchk(rule_schema_id=SchemaId, Props),
1226
+ \+ kb_entity(SchemaId, fact, _),
1227
+ format(string(Description), "Rule fact ~w references missing rule schema ~w", [FactId, SchemaId]).
1228
+ rule_verifiability_description(Props, FactId, Description) :-
1229
+ memberchk(fact_kind=RawKind, Props),
1230
+ normalize_term_atom(RawKind, rule),
1231
+ memberchk(rule_schema_id=SchemaId, Props),
1232
+ kb_entity(SchemaId, fact, SchemaProps),
1233
+ \+ valid_rule_schema_props(SchemaProps),
1234
+ format(string(Description), "Rule fact ~w references ~w, which is not a rule_schema fact", [FactId, SchemaId]).
1235
+ rule_verifiability_description(Props, FactId, Description) :-
1236
+ memberchk(fact_kind=RawKind, Props),
1237
+ normalize_term_atom(RawKind, rule),
1238
+ logic_rule_safety(Props, Errors),
1239
+ Errors \= [],
1240
+ format(string(Description), "Rule fact ~w is not safe: ~w", [FactId, Errors]).
1241
+
1242
+ valid_rule_schema_props(Props) :-
1243
+ memberchk(fact_kind=RawKind, Props),
1244
+ normalize_term_atom(RawKind, rule_schema),
1245
+ memberchk(rule_name=Name, Props),
1246
+ nonempty_normalized(Name),
1247
+ memberchk(argument_names=RawNames, Props),
1248
+ memberchk(argument_types=RawTypes, Props),
1249
+ checks_normalize_atom_list(RawNames, Names),
1250
+ checks_normalize_atom_list(RawTypes, Types),
1251
+ same_length(Names, Types).
1252
+
1253
+ nonempty_normalized(Value) :-
1254
+ normalize_term_atom(Value, Atom),
1255
+ Atom \= ''.
1256
+
1257
+ %% check_semantic_completeness(-Violations)
1258
+ % The proposition ledger is additive and optional during migration. When it is
1259
+ % present, every assertive proposition must either be modeled or explicitly
1260
+ % recorded as an unresolved review state; silently missing entries are errors.
1261
+ check_semantic_completeness(Violations) :-
1262
+ findall(Violation, semantic_completeness_violation(Violation), Raw),
1263
+ sort(Raw, Violations).
1264
+
1265
+ semantic_completeness_violation(violation(
1266
+ 'semantic-completeness',
1267
+ ReqId,
1268
+ Description,
1269
+ "Run kb_semantic_advisor again, preserve every proposition span, and model or explicitly classify each assertive proposition",
1270
+ Source
1271
+ )) :-
1272
+ kb:current_req(ReqId),
1273
+ kb_entity(ReqId, req, Props),
1274
+ memberchk(semantic_inventory=RawInventory, Props),
1275
+ inventory_entries(RawInventory, Entries),
1276
+ member(Entry, Entries),
1277
+ inventory_entry_status(Entry, Status),
1278
+ memberchk(Status, [missing]),
1279
+ format(string(Description), "Requirement proposition ledger contains an unclassified assertive span (~w)", [Entry]),
1280
+ violation_source(ReqId, req, Source).
1281
+
1282
+ inventory_entries(Raw, Entries) :-
1283
+ ( is_list(Raw) -> Entries = Raw
1284
+ ; Raw = ^^(Value, _) -> inventory_entries(Value, Entries)
1285
+ ; (atom(Raw) ; string(Raw)),
1286
+ catch(atom_json_dict(Raw, JsonEntries, [value_string_as(string)]), _, fail),
1287
+ is_list(JsonEntries)
1288
+ -> Entries = JsonEntries
1289
+ ; Entries = []
1290
+ ).
1291
+
1292
+ inventory_entry_status(Entry, Status) :-
1293
+ is_dict(Entry), get_dict(status, Entry, Raw), normalize_term_atom(Raw, Status).
1294
+ inventory_entry_status(Entry, Status) :-
1295
+ is_list(Entry), memberchk(status=Raw, Entry), normalize_term_atom(Raw, Status).
1296
+
933
1297
  strict_readiness_has_fact_link(ReqId) :-
934
1298
  kb_relationship(constrains, ReqId, FactId),
935
1299
  kb_entity(FactId, fact, _),
@@ -1004,6 +1368,9 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1004
1368
  check_strict_readiness(StrictReadiness),
1005
1369
  check_predicate_verifiability(PredicateVerifiability),
1006
1370
  check_logic_coverage(LogicCoverage),
1371
+ check_rule_safety(RuleSafety),
1372
+ check_rule_verifiability(RuleVerifiability),
1373
+ check_semantic_completeness(SemanticCompleteness),
1007
1374
  ViolationsDict = _{
1008
1375
  must_priority_coverage: MustPriority,
1009
1376
  symbol_coverage: SymbolCoverage,
@@ -1017,7 +1384,10 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1017
1384
  strict_req_fact_pairing: StrictReqFactPairing,
1018
1385
  strict_readiness: StrictReadiness,
1019
1386
  predicate_verifiability: PredicateVerifiability,
1020
- logic_coverage: LogicCoverage
1387
+ logic_coverage: LogicCoverage,
1388
+ rule_safety: RuleSafety,
1389
+ rule_verifiability: RuleVerifiability,
1390
+ semantic_completeness: SemanticCompleteness
1021
1391
  }.
1022
1392
 
1023
1393
  %% violations_dict_to_json(+ViolationsDict, -JsonDict)
@@ -1038,7 +1408,28 @@ pairs_to_json_pairs([Key-Violations|Rest], [Key-JsonViolations|JsonRest]) :-
1038
1408
  % Converts a violation(Rule, EntityId, Description, Suggestion, Source) term
1039
1409
  % to a JSON-compatible dict.
1040
1410
  violation_to_json(Violation, JsonDict) :-
1041
- violation_term_to_dict(Violation, JsonDict).
1411
+ violation_term_to_dict(Violation, BaseDict),
1412
+ ( contradiction_violation_witnesses(Violation, Witnesses),
1413
+ Witnesses \= []
1414
+ -> put_dict(evidence, BaseDict, _{witnesses: Witnesses}, JsonDict)
1415
+ ; JsonDict = BaseDict
1416
+ ).
1417
+
1418
+ contradiction_violation_witnesses(
1419
+ violation('domain-contradictions', EntityId, Description, _Suggestion, _Source),
1420
+ Witnesses
1421
+ ) :-
1422
+ violation_id_text(EntityId, EntityIdText),
1423
+ violation_text(Description, DescriptionText),
1424
+ check_domain_contradiction_witnesses(AllWitnesses),
1425
+ include(witness_matches_violation(EntityIdText, DescriptionText), AllWitnesses, Witnesses).
1426
+
1427
+ witness_matches_violation(EntityIdText, DescriptionText, Witness) :-
1428
+ Witness.requirements = [ReqA, ReqB],
1429
+ format(string(ExpectedEntityId), "~w/~w", [ReqA, ReqB]),
1430
+ ExpectedEntityId == EntityIdText,
1431
+ violation_text(Witness.reason, ReasonText),
1432
+ sub_string(DescriptionText, 0, _, _, ReasonText).
1042
1433
 
1043
1434
  violation_term_to_dict(violation(Rule, EntityId, Description, Suggestion, Source), JsonDict) :-
1044
1435
  violation_text(Rule, RuleText),