kibi-core 0.19.0 → 0.21.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.19.0",
3
+ "version": "0.21.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
@@ -12,7 +12,11 @@
12
12
  requirement_proof_json/7,
13
13
  symbol_proof_json/7,
14
14
  graph_expand_json/8,
15
- search_answer_verdicts_json/2
15
+ search_answer_verdicts_json/2,
16
+ ask_predicate_json/9,
17
+ ask_property_json/6,
18
+ ask_schemas_json/5,
19
+ ask_fact_kind_json/4
16
20
  ]).
17
21
 
18
22
  % Source contains non-ASCII text; do not depend on the host locale.
@@ -471,6 +475,450 @@ graph_expand_json(SeedIds, Relationships, Direction, Depth, EntityTypes, MaxNode
471
475
  Response = _{nodes: Nodes, edges: Edges, truncated: Truncated, meta: Meta},
472
476
  dict_json_string(Response, JsonString).
473
477
 
478
+ %% ask_predicate_json(+Namespace, +Name, +ArgSpec, +PolarityFilter, +Scope, +DistinctArg, +Limit, +Offset, -JsonString)
479
+ % Typed query over ontology predicate facts (fact_kind: predicate). Namespace
480
+ % `none` matches any namespace. ArgSpec is either a positional list of
481
+ % constants where the atom '$any' is a wildcard ([] matches any arguments),
482
+ % or where(Pairs) with Name=Value entries resolved through the predicate's
483
+ % declared argument names (schema or built-in signature). PolarityFilter is
484
+ % `none`, assert or deny; Scope `current_requirements` keeps only facts that a
485
+ % current requirement requires; DistinctArg names a declared argument whose
486
+ % distinct values are counted. Rows carry the effective allow/deny decision
487
+ % when the predicate is a decision predicate. An empty answer lists, per
488
+ % bound argument, the values that do exist (suggestions). Read-only.
489
+ % implements REQ-kibi-query-fact-filters
490
+ ask_predicate_json(Namespace, Name, ArgSpec, PolarityFilter, Scope, DistinctArg, Limit, Offset, JsonString) :-
491
+ ask_namespace(Namespace, NamespaceMatch),
492
+ % The schema lookup may bind a namespace; an unnamed namespace must keep
493
+ % matching facts in every namespace, so the schema sees a copy.
494
+ copy_term(NamespaceMatch, SchemaNamespace),
495
+ ask_schema_dict(SchemaNamespace, Name, SchemaDict, ArgumentNames),
496
+ ask_arg_pattern(ArgSpec, ArgumentNames, ArgPattern, PatternWarnings),
497
+ ( PatternWarnings == []
498
+ -> findall(Row,
499
+ ask_predicate_row(NamespaceMatch, Name, ArgPattern, PolarityFilter, Scope, ArgumentNames, Row),
500
+ Rows0)
501
+ ; Rows0 = []
502
+ ),
503
+ sort_dict_rows(Rows0, SortedRows),
504
+ length(SortedRows, Count),
505
+ paginate_rows(SortedRows, Offset, Limit, Rows),
506
+ ask_distinct(DistinctArg, ArgumentNames, SortedRows, Distinct, DistinctWarnings),
507
+ append(PatternWarnings, DistinctWarnings, Warnings),
508
+ ask_suggestions(Count, NamespaceMatch, Name, ArgSpec, ArgumentNames, Suggestions),
509
+ ask_spec_json(ArgSpec, ArgPattern, SpecJson),
510
+ ask_optional_atom(Namespace, NamespaceJson),
511
+ status_meta_dict(Meta),
512
+ Response = _{
513
+ mode: predicate,
514
+ predicate: _{namespace: NamespaceJson, name: Name, args: SpecJson},
515
+ schema: SchemaDict,
516
+ count: Count,
517
+ rows: Rows,
518
+ distinct: Distinct,
519
+ suggestions: Suggestions,
520
+ warnings: Warnings,
521
+ meta: Meta
522
+ },
523
+ dict_json_string(Response, JsonString).
524
+
525
+ %% ask_arg_pattern(+ArgSpec, +ArgumentNames, -ArgPattern, -Warnings)
526
+ % A positional spec is used as is. A where(Pairs) spec becomes a positional
527
+ % pattern over the declared argument names; an undeclared name yields a
528
+ % warning that lists the declared names and no rows.
529
+ ask_arg_pattern(where(Pairs), ArgumentNames, ArgPattern, Warnings) :-
530
+ !,
531
+ ( is_list(ArgumentNames)
532
+ -> findall(Warning,
533
+ ( member(Key=_, Pairs),
534
+ \+ memberchk(Key, ArgumentNames),
535
+ atomic_list_concat(ArgumentNames, ', ', Declared),
536
+ format(atom(Warning), "'~w' is not an argument of this predicate; declared arguments: ~w", [Key, Declared])
537
+ ),
538
+ Warnings),
539
+ findall(Entry,
540
+ ( member(ArgName, ArgumentNames),
541
+ ( memberchk(ArgName=Value, Pairs)
542
+ -> Entry = Value
543
+ ; Entry = '$any'
544
+ )
545
+ ),
546
+ ArgPattern)
547
+ ; Pairs == []
548
+ -> ArgPattern = [],
549
+ Warnings = []
550
+ ; ArgPattern = [],
551
+ Warnings = ["named arguments need a predicate schema; none declares this predicate, so pass positional args instead"]
552
+ ).
553
+ ask_arg_pattern(ArgPattern, ArgumentNames, ArgPattern, Warnings) :-
554
+ ( is_list(ArgumentNames),
555
+ ArgPattern \== [],
556
+ length(ArgPattern, Given),
557
+ length(ArgumentNames, Arity),
558
+ Given =\= Arity
559
+ -> format(atom(Warning), "positional args need one entry per argument (~w declared, ~w given); use null for any value or name them with where", [Arity, Given]),
560
+ Warnings = [Warning]
561
+ ; Warnings = []
562
+ ).
563
+
564
+ ask_spec_json(where(Pairs), _ArgPattern, Json) :-
565
+ !,
566
+ findall(Key-Value, member(Key=Value, Pairs), KeyValues),
567
+ dict_pairs(Json, _, KeyValues).
568
+ ask_spec_json(_ArgSpec, ArgPattern, Json) :-
569
+ ask_pattern_json(ArgPattern, Json).
570
+
571
+ %% ask_suggestions(+Count, +Namespace, +Name, +ArgSpec, +ArgumentNames, -Suggestions)
572
+ % When nothing matched a where(...) spec, list for each bound argument the
573
+ % values that do exist for this predicate so the caller can correct the
574
+ % question instead of reading "no results".
575
+ ask_suggestions(0, Namespace, Name, where(Pairs), ArgumentNames, Suggestions) :-
576
+ is_list(ArgumentNames),
577
+ Pairs \== [],
578
+ findall(Key-Values,
579
+ ( member(Key=_, Pairs),
580
+ nth1(Position, ArgumentNames, Key),
581
+ findall(Value,
582
+ ( kb:predicate_fact(_, Namespace, Name, Args, _),
583
+ nth1(Position, Args, Value)
584
+ ),
585
+ Values0),
586
+ sort(Values0, Values)
587
+ ),
588
+ Entries),
589
+ Entries \== [],
590
+ !,
591
+ dict_pairs(Suggestions, _, Entries).
592
+ ask_suggestions(_Count, _Namespace, _Name, _ArgSpec, _ArgumentNames, null).
593
+
594
+ ask_namespace(none, _) :- !.
595
+ ask_namespace(Namespace, Namespace).
596
+
597
+ ask_optional_atom(none, null) :- !.
598
+ ask_optional_atom(Atom, Atom).
599
+
600
+ ask_pattern_json([], []) :- !.
601
+ ask_pattern_json(Pattern, Json) :-
602
+ maplist(ask_pattern_entry_json, Pattern, Json).
603
+
604
+ ask_pattern_entry_json('$any', null) :- !.
605
+ ask_pattern_entry_json(Value, Value).
606
+
607
+ % Arguments of a stored fact unify with the requested pattern; '$any' entries
608
+ % become fresh variables and an empty pattern accepts any arity.
609
+ ask_pattern_args([], _Args) :- !.
610
+ ask_pattern_args(Pattern, Args) :-
611
+ maplist(ask_pattern_arg, Pattern, Args).
612
+
613
+ ask_pattern_arg('$any', _) :- !.
614
+ ask_pattern_arg(Value, Arg) :-
615
+ kb:normalize_term_atom(Value, Arg).
616
+
617
+ ask_polarity_match(none, _) :- !.
618
+ ask_polarity_match(Polarity, Polarity).
619
+
620
+ % implements REQ-kibi-query-fact-filters
621
+ ask_predicate_row(Namespace, Name, ArgPattern, PolarityFilter, Scope, ArgumentNames, Row) :-
622
+ kb:predicate_fact(FactId, Namespace, Name, Args, Polarity),
623
+ ask_pattern_args(ArgPattern, Args),
624
+ ask_polarity_match(PolarityFilter, Polarity),
625
+ ask_fact_requirements(FactId, requires_predicate, Requirements, HasCurrent),
626
+ ask_scope_match(Scope, HasCurrent),
627
+ ask_fact_text(FactId, Title, Status, ClaimText, CanonicalKey),
628
+ ask_named_arguments(ArgumentNames, Args, NamedArguments),
629
+ ask_effect(Namespace, Name, Args, Polarity, Effect),
630
+ Row = _{
631
+ id: FactId,
632
+ type: fact,
633
+ title: Title,
634
+ status: Status,
635
+ namespace: Namespace,
636
+ name: Name,
637
+ args: Args,
638
+ arguments: NamedArguments,
639
+ polarity: Polarity,
640
+ effect: Effect,
641
+ canonicalKey: CanonicalKey,
642
+ claimText: ClaimText,
643
+ requirements: Requirements
644
+ }.
645
+
646
+ ask_scope_match(current_requirements, true) :- !.
647
+ ask_scope_match(current_requirements, _) :- !, fail.
648
+ ask_scope_match(_Scope, _HasCurrent).
649
+
650
+ %% ask_fact_requirements(+FactId, +Relationship, -Requirements, -HasCurrent)
651
+ % Requirements linked to the fact through Relationship, each with whether it
652
+ % is current (kb:current_req/1); HasCurrent is true when at least one is.
653
+ ask_fact_requirements(FactId, Relationship, Requirements, HasCurrent) :-
654
+ findall(ReqId, kb_relationship(Relationship, ReqId, FactId), ReqIds0),
655
+ sort(ReqIds0, ReqIds),
656
+ maplist(ask_requirement_dict, ReqIds, Requirements),
657
+ ( member(Dict, Requirements), Dict.current == true
658
+ -> HasCurrent = true
659
+ ; HasCurrent = false
660
+ ).
661
+
662
+ ask_requirement_dict(ReqId, _{id: ReqId, status: Status, current: Current}) :-
663
+ ( kb_entity(ReqId, req, Props), memberchk(status=StatusRaw, Props)
664
+ -> source_value_atom(StatusRaw, Status)
665
+ ; Status = ''
666
+ ),
667
+ ( kb:current_req(ReqId) -> Current = true ; Current = false ).
668
+
669
+ ask_fact_text(FactId, Title, Status, ClaimText, CanonicalKey) :-
670
+ kb_entity(FactId, fact, Props),
671
+ ( memberchk(title=TitleRaw, Props) -> source_value_atom(TitleRaw, Title) ; Title = '' ),
672
+ ( memberchk(status=StatusRaw, Props) -> source_value_atom(StatusRaw, Status) ; Status = '' ),
673
+ ( memberchk(claim_text=ClaimRaw, Props) -> source_value_atom(ClaimRaw, ClaimText) ; ClaimText = '' ),
674
+ ( memberchk(canonical_key=KeyRaw, Props) -> source_value_atom(KeyRaw, CanonicalKey) ; CanonicalKey = '' ).
675
+
676
+ % Named arguments follow the schema when it exists and has the same arity;
677
+ % otherwise the row carries only the positional list.
678
+ ask_named_arguments(ArgumentNames, Args, Named) :-
679
+ is_list(ArgumentNames),
680
+ same_length(ArgumentNames, Args),
681
+ !,
682
+ pairs_keys_values(Pairs, ArgumentNames, Args),
683
+ dict_pairs(Named, _, Pairs).
684
+ ask_named_arguments(_ArgumentNames, _Args, null).
685
+
686
+ % The effective allow/deny decision of a decision predicate (a schema with
687
+ % key_arguments and a decision argument, or the built-in permission_rule).
688
+ ask_effect(Namespace, Name, Args, Polarity, Effect) :-
689
+ length(Args, Arity),
690
+ kb:decision_predicate(Namespace, Name, Arity, _KeyPositions, DecisionPosition),
691
+ nth1(DecisionPosition, Args, Decision),
692
+ kb:effective_decision(Decision, Polarity, Effect0),
693
+ !,
694
+ Effect = Effect0.
695
+ ask_effect(_Namespace, _Name, _Args, _Polarity, null).
696
+
697
+ %% ask_schema_dict(?Namespace, +Name, -SchemaDict, -ArgumentNames)
698
+ % The project predicate_schema for the predicate, the built-in decision
699
+ % signature, or null when neither exists.
700
+ ask_schema_dict(Namespace, Name, SchemaDict, ArgumentNames) :-
701
+ kb:predicate_schema(SchemaId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes),
702
+ !,
703
+ ask_schema_row(SchemaId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes, SchemaDict).
704
+ ask_schema_dict(Namespace, Name, SchemaDict, ArgumentNames) :-
705
+ kb:builtin_decision_predicate(Namespace, Name, Arity, KeyPositions, DecisionPosition),
706
+ !,
707
+ ask_builtin_schema_row(Namespace, Name, Arity, KeyPositions, DecisionPosition, SchemaDict),
708
+ ArgumentNames = SchemaDict.argumentNames.
709
+ ask_schema_dict(_Namespace, _Name, null, none).
710
+
711
+ ask_builtin_schema_row(Namespace, Name, Arity, KeyPositions, DecisionPosition, Row) :-
712
+ builtin_argument_names(Name, Names),
713
+ findall(Key, (member(P, KeyPositions), nth1(P, Names, Key)), KeyArguments),
714
+ nth1(DecisionPosition, Names, DecisionArgument),
715
+ aggregate_all(count, kb:predicate_fact(_, Namespace, Name, _, _), FactCount),
716
+ Row = _{
717
+ id: null,
718
+ builtin: true,
719
+ namespace: Namespace,
720
+ name: Name,
721
+ arity: Arity,
722
+ argumentNames: Names,
723
+ argumentTypes: Names,
724
+ keyArguments: KeyArguments,
725
+ decisionArgument: DecisionArgument,
726
+ argumentConstants: null,
727
+ examples: [],
728
+ factCount: FactCount
729
+ }.
730
+
731
+ % Keep in step with builtin_decision_predicate/5 in kb.pl.
732
+ builtin_argument_names(permission_rule, [actor, action, resource, decision]).
733
+
734
+ ask_schema_row(SchemaId, Namespace, Name, Arity, ArgumentNames, ArgumentTypes, Row) :-
735
+ kb_entity(SchemaId, fact, Props),
736
+ ( memberchk(key_arguments=KeysRaw, Props)
737
+ -> kb:normalize_term_atom_list(KeysRaw, KeyArguments)
738
+ ; KeyArguments = []
739
+ ),
740
+ ( kb:decision_predicate(Namespace, Name, Arity, _, DecisionPosition),
741
+ nth1(DecisionPosition, ArgumentNames, DecisionArgument0)
742
+ -> DecisionArgument = DecisionArgument0
743
+ ; DecisionArgument = null
744
+ ),
745
+ ( memberchk(argument_constants=ConstantsRaw, Props),
746
+ ask_json_value(ConstantsRaw, Constants0)
747
+ -> Constants = Constants0
748
+ ; Constants = null
749
+ ),
750
+ ( memberchk(examples=ExamplesRaw, Props), is_list(ExamplesRaw)
751
+ -> maplist(source_value_atom, ExamplesRaw, Examples)
752
+ ; Examples = []
753
+ ),
754
+ aggregate_all(count, kb:predicate_fact(_, Namespace, Name, _, _), FactCount),
755
+ Row = _{
756
+ id: SchemaId,
757
+ builtin: false,
758
+ namespace: Namespace,
759
+ name: Name,
760
+ arity: Arity,
761
+ argumentNames: ArgumentNames,
762
+ argumentTypes: ArgumentTypes,
763
+ keyArguments: KeyArguments,
764
+ decisionArgument: DecisionArgument,
765
+ argumentConstants: Constants,
766
+ examples: Examples,
767
+ factCount: FactCount
768
+ }.
769
+
770
+ ask_json_value(^^(Value, _Type), Parsed) :- !, ask_json_value(Value, Parsed).
771
+ ask_json_value(literal(Value), Parsed) :- !, ask_json_value(Value, Parsed).
772
+ ask_json_value(Dict, Dict) :- is_dict(Dict), !.
773
+ ask_json_value(Raw, Parsed) :-
774
+ (atom(Raw) ; string(Raw)),
775
+ catch(atom_json_dict(Raw, Parsed, [value_string_as(string)]), _, fail).
776
+
777
+ %% ask_distinct(+DistinctArg, +ArgumentNames, +Rows, -Distinct, -Warnings)
778
+ ask_distinct(none, _ArgumentNames, _Rows, null, []) :- !.
779
+ ask_distinct(DistinctArg, ArgumentNames, Rows, Distinct, []) :-
780
+ is_list(ArgumentNames),
781
+ nth1(Position, ArgumentNames, DistinctArg),
782
+ !,
783
+ findall(Value, (member(Row, Rows), nth1(Position, Row.args, Value)), Values0),
784
+ msort(Values0, Values),
785
+ clumped(Values, Clumps),
786
+ findall(_{value: Value, count: Count}, member(Value-Count, Clumps), Entries),
787
+ Distinct = _{argument: DistinctArg, values: Entries}.
788
+ ask_distinct(DistinctArg, _ArgumentNames, _Rows, null, [Warning]) :-
789
+ format(atom(Warning), "distinct argument '~w' is not a declared argument of this predicate; no schema names it", [DistinctArg]).
790
+
791
+ %% ask_property_json(+SubjectKey, +PropertyKey, +Scope, +Limit, +Offset, -JsonString)
792
+ % Typed query over strict property_value facts of one subject key.
793
+ % PropertyKey `none` matches every property of the subject.
794
+ % implements REQ-kibi-query-fact-filters
795
+ ask_property_json(SubjectKey, PropertyKey, Scope, Limit, Offset, JsonString) :-
796
+ kb:normalize_term_atom(SubjectKey, Subject),
797
+ ask_namespace(PropertyKey, PropertyMatch),
798
+ findall(Row, ask_property_row(Subject, PropertyMatch, Scope, Row), Rows0),
799
+ sort_dict_rows(Rows0, SortedRows),
800
+ length(SortedRows, Count),
801
+ paginate_rows(SortedRows, Offset, Limit, Rows),
802
+ findall(SubjectId, kb:fact_subject_key(SubjectId, Subject), SubjectIds0),
803
+ sort(SubjectIds0, SubjectIds),
804
+ ask_optional_atom(PropertyKey, PropertyJson),
805
+ status_meta_dict(Meta),
806
+ Response = _{
807
+ mode: property,
808
+ property: _{subjectKey: Subject, propertyKey: PropertyJson},
809
+ subjectFacts: SubjectIds,
810
+ count: Count,
811
+ rows: Rows,
812
+ warnings: [],
813
+ meta: Meta
814
+ },
815
+ dict_json_string(Response, JsonString).
816
+
817
+ ask_property_row(Subject, PropertyMatch, Scope, Row) :-
818
+ kb:fact_property_tuple(FactId, Subject, Property, Op, ValType, Value, Unit, PropScope, Polarity),
819
+ ( PropertyMatch == Property -> true ; var(PropertyMatch) ),
820
+ ask_fact_requirements(FactId, requires_property, Requirements, HasCurrent),
821
+ ask_scope_match(Scope, HasCurrent),
822
+ ask_fact_text(FactId, Title, Status, ClaimText, CanonicalKey),
823
+ ask_property_value_json(Value, ValueJson),
824
+ Row = _{
825
+ id: FactId,
826
+ type: fact,
827
+ title: Title,
828
+ status: Status,
829
+ subjectKey: Subject,
830
+ propertyKey: Property,
831
+ operator: Op,
832
+ valueType: ValType,
833
+ value: ValueJson,
834
+ unit: Unit,
835
+ scope: PropScope,
836
+ polarity: Polarity,
837
+ canonicalKey: CanonicalKey,
838
+ claimText: ClaimText,
839
+ requirements: Requirements
840
+ }.
841
+
842
+ ask_property_value_json(Value, Value) :- number(Value), !.
843
+ ask_property_value_json(Value, Json) :- source_value_atom(Value, Json).
844
+
845
+ %% ask_schemas_json(+Namespace, +Name, +Limit, +Offset, -JsonString)
846
+ % Catalog of the predicates an agent can ask about: every project
847
+ % predicate_schema fact plus built-in decision predicates without a project
848
+ % schema, with argument names, constants, key arguments and fact counts.
849
+ % implements REQ-kibi-query-fact-filters
850
+ ask_schemas_json(Namespace, Name, Limit, Offset, JsonString) :-
851
+ ask_namespace(Namespace, NamespaceMatch),
852
+ ask_namespace(Name, NameMatch),
853
+ findall(Row,
854
+ ( kb:predicate_schema(SchemaId, NamespaceMatch, NameMatch, Arity, ArgumentNames, ArgumentTypes),
855
+ ask_schema_row(SchemaId, NamespaceMatch, NameMatch, Arity, ArgumentNames, ArgumentTypes, Row)
856
+ ),
857
+ SchemaRows),
858
+ findall(Row,
859
+ ( kb:builtin_decision_predicate(NamespaceMatch, NameMatch, Arity, KeyPositions, DecisionPosition),
860
+ \+ kb:predicate_schema(_, NamespaceMatch, NameMatch, Arity, _, _),
861
+ ask_builtin_schema_row(NamespaceMatch, NameMatch, Arity, KeyPositions, DecisionPosition, Row)
862
+ ),
863
+ BuiltinRows),
864
+ append(SchemaRows, BuiltinRows, Rows0),
865
+ map_list_to_pairs(ask_schema_sort_key, Rows0, Pairs),
866
+ keysort(Pairs, SortedPairs),
867
+ pairs_values(SortedPairs, SortedRows),
868
+ length(SortedRows, Count),
869
+ paginate_rows(SortedRows, Offset, Limit, Rows),
870
+ status_meta_dict(Meta),
871
+ Response = _{mode: schemas, count: Count, rows: Rows, warnings: [], meta: Meta},
872
+ dict_json_string(Response, JsonString).
873
+
874
+ ask_schema_sort_key(Row, Key) :-
875
+ format(atom(Key), '~w:~w/~w', [Row.namespace, Row.name, Row.arity]).
876
+
877
+ %% ask_fact_kind_json(+Kind, +Limit, +Offset, -JsonString)
878
+ % Facts of one fact_kind (subject, property_value, predicate, observation,
879
+ % meta, rule, rule_schema) with their identifying typed fields only, so a
880
+ % caller can see what is modeled without paging full entities.
881
+ % implements REQ-kibi-query-fact-filters
882
+ ask_fact_kind_json(Kind, Limit, Offset, JsonString) :-
883
+ findall(Row, ask_fact_kind_row(Kind, Row), Rows0),
884
+ sort_dict_rows(Rows0, SortedRows),
885
+ length(SortedRows, Count),
886
+ paginate_rows(SortedRows, Offset, Limit, Rows),
887
+ status_meta_dict(Meta),
888
+ Response = _{mode: fact_kind, factKind: Kind, count: Count, rows: Rows, warnings: [], meta: Meta},
889
+ dict_json_string(Response, JsonString).
890
+
891
+ ask_fact_kind_row(Kind, Row) :-
892
+ kb_entity(FactId, fact, Props),
893
+ memberchk(fact_kind=KindRaw, Props),
894
+ kb:normalize_term_atom(KindRaw, Kind),
895
+ ask_fact_text(FactId, Title, Status, ClaimText, CanonicalKey),
896
+ findall(Key-Value,
897
+ ( member(Key, [subject_key, property_key, operator, value_type, unit, predicate_namespace, predicate_name, predicate_args, polarity]),
898
+ memberchk(Key=Raw, Props),
899
+ ask_typed_value(Raw, Value)
900
+ ),
901
+ Typed),
902
+ ( Kind == property_value
903
+ -> ( memberchk(value_type=ValTypeRaw, Props)
904
+ -> kb:normalize_term_atom(ValTypeRaw, ValType)
905
+ ; ValType = string
906
+ ),
907
+ kb:value_from_props(Props, ValType, Value0),
908
+ ask_property_value_json(Value0, PropertyValue),
909
+ ValuePairs = [value-PropertyValue]
910
+ ; ValuePairs = []
911
+ ),
912
+ append(Typed, ValuePairs, Fields),
913
+ dict_pairs(Row, _, [id-FactId, type-fact, title-Title, status-Status, factKind-Kind, canonicalKey-CanonicalKey, claimText-ClaimText | Fields]).
914
+
915
+ ask_typed_value(Raw, Value) :-
916
+ is_list(Raw),
917
+ !,
918
+ kb:normalize_term_atom_list(Raw, Value).
919
+ ask_typed_value(Raw, Value) :-
920
+ source_value_atom(Raw, Value).
921
+
474
922
  matching_gap_row(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Row) :-
475
923
  matching_gap_candidate(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Row, Stub),
476
924
  gap_row_visible(Tags, Stub).
package/src/kb.pl CHANGED
@@ -38,6 +38,7 @@
38
38
  kb_log_relationship_upsert/4,
39
39
  kb_relationship/3,
40
40
  symbol_owns_requirement/2,
41
+ kb_domain_contradiction_pairs_examined/1,
41
42
  scenario_verified_by_test/2,
42
43
  requirement_test_fallback_allowed/1,
43
44
  test_satisfies_requirement_semantics/2,
@@ -2717,6 +2718,43 @@ req_conflict_witness(ReqA, ReqB, Witness) :-
2717
2718
  right: Right
2718
2719
  }.
2719
2720
 
2721
+ %% kb_domain_contradiction_pairs_examined(-Count)
2722
+ % implements REQ-search-answer-hygiene
2723
+ % How many distinct pairs of requirement facts the domain-contradictions
2724
+ % rule compared on the current KB: strict property facts of two current
2725
+ % requirements constraining the same subject and property, predicate facts
2726
+ % of two current requirements with the same namespace, name and ordered
2727
+ % arguments, and decision-predicate facts agreeing on every key argument.
2728
+ % A check that found no contradiction over zero pairs evaluated nothing, and
2729
+ % kb_check says so; the count never implies the pairs are consistent.
2730
+ kb_domain_contradiction_pairs_examined(Count) :-
2731
+ findall(FactA-FactB, domain_contradiction_candidate_pair(FactA, FactB), Pairs0),
2732
+ sort(Pairs0, Pairs),
2733
+ length(Pairs, Count).
2734
+
2735
+ domain_contradiction_candidate_pair(FactA, FactB) :-
2736
+ current_req(ReqA),
2737
+ current_req(ReqB),
2738
+ ReqA @< ReqB,
2739
+ kb_relationship(constrains, ReqA, SubjectFactA),
2740
+ kb_relationship(constrains, ReqB, SubjectFactB),
2741
+ fact_subject_key(SubjectFactA, SubjectKey),
2742
+ fact_subject_key(SubjectFactB, SubjectKey),
2743
+ effective_req_property_fact(ReqA, SubjectKey, FactA, PropertyKey, _, _, _, _, _, _, _, _),
2744
+ effective_req_property_fact(ReqB, SubjectKey, FactB, PropertyKey, _, _, _, _, _, _, _, _),
2745
+ FactA \= FactB.
2746
+ domain_contradiction_candidate_pair(FactA, FactB) :-
2747
+ current_req(ReqA),
2748
+ current_req(ReqB),
2749
+ ReqA @< ReqB,
2750
+ effective_req_predicate(ReqA, FactA, Namespace, Name, Args, _),
2751
+ effective_req_predicate(ReqB, FactB, Namespace, Name, Args, _),
2752
+ FactA \= FactB.
2753
+ domain_contradiction_candidate_pair(FactA, FactB) :-
2754
+ decision_signatures(Signatures),
2755
+ Signatures \== [],
2756
+ decision_conflict_pair(Signatures, _, _, FactA, FactB, _, _, _, _, _, _, _, _).
2757
+
2720
2758
  %% decision_conflict_pair(+Signatures, ?ReqA, ?ReqB, -FactA, -FactB, -Namespace, -Name, -ArgsA, -ArgsB, -PolarityA, -PolarityB, -KeyPositions, -DecisionPosition)
2721
2759
  % Two decision-predicate facts of current requirements ReqA @< ReqB that
2722
2760
  % agree on every key argument. Starts from the bound requirement.