kibi-core 0.7.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
@@ -29,6 +29,7 @@ entity_property(_, severity, atom).
29
29
  entity_property(_, links, list).
30
30
  entity_property(_, text_ref, uri).
31
31
  entity_property(_, sourceFile, uri).
32
+ entity_property(req, logic_claims, list).
32
33
 
33
34
  % Typed fact fields - only valid for fact entities
34
35
  entity_property(fact, fact_kind, atom).
@@ -47,6 +48,8 @@ entity_property(fact, closed_world, boolean).
47
48
  entity_property(fact, valid_from, datetime).
48
49
  entity_property(fact, valid_to, datetime).
49
50
  entity_property(fact, canonical_key, string).
51
+ entity_property(fact, claim_key, string).
52
+ entity_property(fact, claim_text, string).
50
53
  entity_property(fact, predicate_name, string).
51
54
  entity_property(fact, predicate_namespace, string).
52
55
  entity_property(fact, predicate_arity, integer).
@@ -80,6 +83,7 @@ optional_property(Type, priority) :- entity_type(Type).
80
83
  optional_property(Type, severity) :- entity_type(Type).
81
84
  optional_property(Type, links) :- entity_type(Type).
82
85
  optional_property(Type, text_ref) :- entity_type(Type).
86
+ optional_property(req, logic_claims).
83
87
  optional_property(test, verification_scope).
84
88
  optional_property(test, verification_perspective).
85
89
 
@@ -59,17 +59,24 @@ validate_entity_shape(fact, Props) :-
59
59
  !,
60
60
  valid_optional_fact_enums(Props),
61
61
  valid_polarity_in_props(Props),
62
+ valid_claim_provenance(Props),
62
63
  ( memberchk(fact_kind=RawKind, Props) -> validate_fact_shape(RawKind, Props) ; true ).
63
64
  validate_entity_shape(test, Props) :-
64
65
  !,
65
66
  valid_optional_test_enums(Props),
66
67
  forall(member(Key=_, Props), \+ is_fact_only_field(Key)).
68
+ validate_entity_shape(req, Props) :-
69
+ !,
70
+ forall(member(Key=_, Props), \+ is_fact_only_field(Key)),
71
+ forall(member(Key=_, Props), \+ is_test_only_field(Key)).
67
72
  validate_entity_shape(Type, Props) :-
68
73
  Type \= fact,
69
74
  Type \= test,
75
+ Type \= req,
70
76
  % Non-fact/non-test entities cannot have type-specific fields
71
77
  forall(member(Key=_, Props), \+ is_fact_only_field(Key)),
72
- forall(member(Key=_, Props), \+ is_test_only_field(Key)).
78
+ forall(member(Key=_, Props), \+ is_test_only_field(Key)),
79
+ forall(member(Key=_, Props), Key \= logic_claims).
73
80
 
74
81
  % is_fact_only_field(+Key) - true if Key is a fact-specific field
75
82
  is_fact_only_field(fact_kind).
@@ -88,6 +95,8 @@ is_fact_only_field(closed_world).
88
95
  is_fact_only_field(valid_from).
89
96
  is_fact_only_field(valid_to).
90
97
  is_fact_only_field(canonical_key).
98
+ is_fact_only_field(claim_key).
99
+ is_fact_only_field(claim_text).
91
100
  is_fact_only_field(predicate_name).
92
101
  is_fact_only_field(predicate_namespace).
93
102
  is_fact_only_field(predicate_arity).
@@ -172,6 +181,18 @@ valid_strict_polarity_in_props(Props) :-
172
181
  valid_predicate_polarity_in_props(Props) :-
173
182
  ( memberchk(polarity=P, Props) -> valid_predicate_polarity(P) ; true ).
174
183
 
184
+ % claim_key and claim_text form one auditable provenance pair.
185
+ valid_claim_provenance(Props) :-
186
+ ( memberchk(claim_key=Key, Props)
187
+ -> memberchk(claim_text=Text, Props),
188
+ nonempty_claim_value(Key),
189
+ nonempty_claim_value(Text)
190
+ ; \+ memberchk(claim_text=_, Props)
191
+ ).
192
+
193
+ nonempty_claim_value(Value) :- string(Value), Value \= "".
194
+ nonempty_claim_value(Value) :- atom(Value), Value \= ''.
195
+
175
196
  % valid_optional_fact_enums(+Props)
176
197
  % Validates enum-typed fact fields whenever they are present
177
198
  valid_optional_fact_enums(Props) :-
package/src/checks.pl CHANGED
@@ -18,6 +18,7 @@
18
18
  check_strict_req_fact_pairing/1,% Returns list of malformed strict req/fact pairing violations
19
19
  check_strict_readiness/1, % Returns list of strict readiness audit violations
20
20
  check_predicate_verifiability/1,% Returns list of unverifiable predicate-lane requirement links
21
+ check_logic_coverage/1, % Returns list of incomplete declared logical claim manifests
21
22
  run_checks_json/0, % Entry point for JSON output
22
23
  violation_id_text/2 % Extract text from entity ID term (exported for testing)
23
24
  ]).
@@ -54,6 +55,7 @@ check_all(ViolationsDict) :-
54
55
  check_strict_req_fact_pairing(StrictReqFactPairing),
55
56
  check_strict_readiness(StrictReadiness),
56
57
  check_predicate_verifiability(PredicateVerifiability),
58
+ check_logic_coverage(LogicCoverage),
57
59
  ViolationsDict = _{
58
60
  must_priority_coverage: MustPriority,
59
61
  symbol_coverage: SymbolCoverage,
@@ -66,7 +68,8 @@ check_all(ViolationsDict) :-
66
68
  strict_fact_shape: StrictFactShape,
67
69
  strict_req_fact_pairing: StrictReqFactPairing,
68
70
  strict_readiness: StrictReadiness,
69
- predicate_verifiability: PredicateVerifiability
71
+ predicate_verifiability: PredicateVerifiability,
72
+ logic_coverage: LogicCoverage
70
73
  }.
71
74
 
72
75
  %% check_must_priority_coverage(-Violations)
@@ -800,6 +803,133 @@ predicate_verifiability_kind_label(legacy, "a legacy fact without fact_kind").
800
803
  predicate_verifiability_kind_label(Kind, Label) :-
801
804
  format(string(Label), "fact_kind=~w", [Kind]).
802
805
 
806
+ %% check_logic_coverage(-Violations)
807
+ % Validates explicitly declared atomic requirement claim manifests. Legacy
808
+ % requirements without logic_claims remain eligible for gradual backfill; once
809
+ % a manifest exists, every key must be grounded by a linked strict-property or
810
+ % predicate fact carrying the same claim_key.
811
+ check_logic_coverage(Violations) :-
812
+ findall(Violation, logic_coverage_violation(Violation), Violations0),
813
+ sort(Violations0, Violations).
814
+
815
+ logic_coverage_violation(violation(
816
+ 'logic-coverage',
817
+ ReqId,
818
+ 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",
820
+ Source
821
+ )) :-
822
+ kb:current_req(ReqId),
823
+ requirement_logic_claims(ReqId, ClaimKeys),
824
+ member(ClaimKey, ClaimKeys),
825
+ \+ grounded_requirement_claim(ReqId, ClaimKey),
826
+ format(
827
+ string(Description),
828
+ "Requirement declares logical claim ~w but has no matching ground fact",
829
+ [ClaimKey]
830
+ ),
831
+ violation_source(ReqId, req, Source).
832
+
833
+ logic_coverage_violation(violation(
834
+ 'logic-coverage',
835
+ ReqId,
836
+ Description,
837
+ "Keep a one-to-one mapping between each atomic claim key and its linked ground fact; split compound clauses before grounding",
838
+ Source
839
+ )) :-
840
+ kb:current_req(ReqId),
841
+ grounded_requirement_claim_fact(ReqId, ClaimKey, FactA),
842
+ grounded_requirement_claim_fact(ReqId, ClaimKey, FactB),
843
+ FactA @< FactB,
844
+ format(
845
+ string(Description),
846
+ "Requirement grounds logical claim ~w more than once through ~w and ~w",
847
+ [ClaimKey, FactA, FactB]
848
+ ),
849
+ violation_source(ReqId, req, Source).
850
+
851
+ logic_coverage_violation(violation(
852
+ 'logic-coverage',
853
+ ReqId,
854
+ Description,
855
+ "Remove punctuation or wording variants from the atomic inventory and keep one claim key for each distinct logical term",
856
+ Source
857
+ )) :-
858
+ kb:current_req(ReqId),
859
+ grounded_requirement_claim_fact(ReqId, ClaimA, FactA),
860
+ grounded_requirement_claim_fact(ReqId, ClaimB, FactB),
861
+ ClaimA \= ClaimB,
862
+ FactA @< FactB,
863
+ logical_ground_signature(FactA, Signature),
864
+ logical_ground_signature(FactB, Signature),
865
+ format(
866
+ string(Description),
867
+ "Requirement declares duplicate logical ground term ~w through claim keys ~w and ~w",
868
+ [Signature, ClaimA, ClaimB]
869
+ ),
870
+ violation_source(ReqId, req, Source).
871
+
872
+ logic_coverage_violation(violation(
873
+ 'logic-coverage',
874
+ ReqId,
875
+ Description,
876
+ "Append the linked fact claim_key to the requirement logic_claims manifest, or remove the stale logical link",
877
+ Source
878
+ )) :-
879
+ kb:current_req(ReqId),
880
+ requirement_logic_claims(ReqId, ClaimKeys),
881
+ grounded_requirement_claim(ReqId, ClaimKey),
882
+ \+ memberchk(ClaimKey, ClaimKeys),
883
+ format(
884
+ string(Description),
885
+ "Requirement links ground logical claim ~w but omits it from logic_claims",
886
+ [ClaimKey]
887
+ ),
888
+ violation_source(ReqId, req, Source).
889
+
890
+ requirement_logic_claims(ReqId, ClaimKeys) :-
891
+ kb_entity(ReqId, req, Props),
892
+ memberchk(logic_claims=RawClaimKeys, Props),
893
+ kb:normalize_term_atom_list(RawClaimKeys, ClaimKeys).
894
+
895
+ grounded_requirement_claim(ReqId, ClaimKey) :-
896
+ grounded_requirement_claim_fact(ReqId, ClaimKey, _FactId).
897
+
898
+ grounded_requirement_claim_fact(ReqId, ClaimKey, FactId) :-
899
+ kb_relationship(requires_property, ReqId, FactId),
900
+ ground_fact_claim_key(FactId, property_value, ClaimKey).
901
+ grounded_requirement_claim_fact(ReqId, ClaimKey, FactId) :-
902
+ kb_relationship(requires_predicate, ReqId, FactId),
903
+ ground_fact_claim_key(FactId, predicate, ClaimKey).
904
+
905
+ logical_ground_signature(FactId, predicate(Namespace, Name, Args, Polarity)) :-
906
+ kb:predicate_fact(FactId, Namespace, Name, Args, Polarity).
907
+ logical_ground_signature(
908
+ FactId,
909
+ property(Subject, Property, Operator, ValueType, Value, Unit, Scope, Polarity)
910
+ ) :-
911
+ kb:fact_property_tuple(
912
+ FactId,
913
+ Subject,
914
+ Property,
915
+ Operator,
916
+ ValueType,
917
+ Value,
918
+ Unit,
919
+ Scope,
920
+ Polarity
921
+ ).
922
+
923
+ ground_fact_claim_key(FactId, ExpectedKind, ClaimKey) :-
924
+ kb_entity(FactId, fact, Props),
925
+ memberchk(fact_kind=RawKind, Props),
926
+ normalize_term_atom(RawKind, ExpectedKind),
927
+ memberchk(claim_key=RawClaimKey, Props),
928
+ normalize_term_atom(RawClaimKey, ClaimKey),
929
+ memberchk(claim_text=RawClaimText, Props),
930
+ normalize_term_atom(RawClaimText, ClaimText),
931
+ ClaimText \= ''.
932
+
803
933
  strict_readiness_has_fact_link(ReqId) :-
804
934
  kb_relationship(constrains, ReqId, FactId),
805
935
  kb_entity(FactId, fact, _),
@@ -873,6 +1003,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
873
1003
  check_strict_req_fact_pairing(StrictReqFactPairing),
874
1004
  check_strict_readiness(StrictReadiness),
875
1005
  check_predicate_verifiability(PredicateVerifiability),
1006
+ check_logic_coverage(LogicCoverage),
876
1007
  ViolationsDict = _{
877
1008
  must_priority_coverage: MustPriority,
878
1009
  symbol_coverage: SymbolCoverage,
@@ -885,7 +1016,8 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
885
1016
  strict_fact_shape: StrictFactShape,
886
1017
  strict_req_fact_pairing: StrictReqFactPairing,
887
1018
  strict_readiness: StrictReadiness,
888
- predicate_verifiability: PredicateVerifiability
1019
+ predicate_verifiability: PredicateVerifiability,
1020
+ logic_coverage: LogicCoverage
889
1021
  }.
890
1022
 
891
1023
  %% violations_dict_to_json(+ViolationsDict, -JsonDict)
package/src/kb.pl CHANGED
@@ -45,6 +45,7 @@
45
45
  contradicting_reqs/3,
46
46
  check_req_contradiction/1,
47
47
  normalize_term_atom/2,
48
+ normalize_term_atom_list/2,
48
49
  changeset/4, % Export for testing
49
50
  kb_uri/1
50
51
  ]).
@@ -636,7 +637,7 @@ value_to_literal(Key, Value, Literal) :-
636
637
  ; string(Value)
637
638
  -> Literal = Value^^'http://www.w3.org/2001/XMLSchema#string'
638
639
  ; is_list(Value)
639
- -> format(atom(ListStr), '~w', [Value]),
640
+ -> with_output_to(atom(ListStr), write_term(Value, [quoted(true)])),
640
641
  Literal = ListStr^^'http://www.w3.org/2001/XMLSchema#string'
641
642
  ; format(atom(Str), '~w', [Value]),
642
643
  Literal = Str^^'http://www.w3.org/2001/XMLSchema#string'
@@ -1005,6 +1006,26 @@ req_conflict(ReqA, ReqB, Reason) :-
1005
1006
  ; property_conflict(SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, PolarityA,
1006
1007
  OpB, ValTypeB, ValB, UnitB, PolarityB, Reason)
1007
1008
  ).
1009
+ req_conflict(ReqA, ReqB, Reason) :-
1010
+ effective_req_predicate(ReqA, FactA, Namespace, Name, Args, PolarityA),
1011
+ effective_req_predicate(ReqB, FactB, Namespace, Name, Args, PolarityB),
1012
+ FactA \= FactB,
1013
+ opposite_predicate_polarity(PolarityA, PolarityB),
1014
+ atomic_list_concat(Args, ',', ArgsText),
1015
+ format(
1016
+ string(Reason),
1017
+ "Predicate conflict on ~w:~w(~w): ~w asserts ~w while ~w asserts ~w",
1018
+ [Namespace, Name, ArgsText, ReqA, PolarityA, ReqB, PolarityB]
1019
+ ).
1020
+
1021
+ %% effective_req_predicate(+ReqId, -FactId, -Namespace, -Name, -Args, -Polarity)
1022
+ % Ground predicate claim required by a requirement.
1023
+ effective_req_predicate(ReqId, FactId, Namespace, Name, Args, Polarity) :-
1024
+ kb_relationship(requires_predicate, ReqId, FactId),
1025
+ predicate_fact(FactId, Namespace, Name, Args, Polarity).
1026
+
1027
+ opposite_predicate_polarity(assert, deny).
1028
+ opposite_predicate_polarity(deny, assert).
1008
1029
 
1009
1030
  %% fact_subject_key(+FactId, -SubjectKey)
1010
1031
  % Extract the normalized subject key for strict subject facts.
@@ -44,7 +44,10 @@ validate_remote_endpoint(Endpoint) :-
44
44
 
45
45
  validate_select_query(Query) :-
46
46
  normalize_space(atom(Normalized), Query),
47
- downcase_atom(Normalized, Lower),
47
+ split_string(Normalized, "\n", " \t\r", RawLines),
48
+ strip_sparql_prelude(RawLines, RemainingLines),
49
+ atomic_list_concat(RemainingLines, ' ', Remainder),
50
+ downcase_atom(Remainder, Lower),
48
51
  sub_atom(Lower, 0, 6, After, 'select'),
49
52
  ( After =:= 0
50
53
  ; sub_atom(Lower, 6, 1, _, ' ')
@@ -54,6 +57,25 @@ validate_select_query(Query) :-
54
57
  validate_select_query(Query) :-
55
58
  throw(error(domain_error(sparql_select_query, Query), _)).
56
59
 
60
+ strip_sparql_prelude([], []).
61
+ strip_sparql_prelude([Line | Rest], Result) :-
62
+ normalize_space(atom(Trimmed), Line),
63
+ ( Trimmed == ''
64
+ -> strip_sparql_prelude(Rest, Result)
65
+ ; is_prelude_line(Trimmed)
66
+ -> strip_sparql_prelude(Rest, Result)
67
+ ; Result = [Trimmed | Rest]
68
+ ).
69
+
70
+ is_prelude_line(Line) :-
71
+ downcase_atom(Line, Lower),
72
+ split_string(Lower, " ", " \t", Tokens0),
73
+ exclude(=("") , Tokens0, Tokens),
74
+ Tokens = [FirstToken | _],
75
+ ( FirstToken == "prefix"
76
+ ; FirstToken == "base"
77
+ ).
78
+
57
79
  endpoint_host(Endpoint, Host) :-
58
80
  strip_endpoint_scheme(Endpoint, WithoutScheme),
59
81
  segment_before(WithoutScheme, '/', WithoutPath),
package/src/status.pl CHANGED
@@ -117,6 +117,7 @@ documentation_markdown_untracked(WorkspaceRoot) :-
117
117
  documentation_markdown_file(Path, Path) :-
118
118
  exists_file(Path),
119
119
  file_name_extension(_, md, Path),
120
+ entity_documentation_file(Path),
120
121
  \+ ignored_documentation_file(Path),
121
122
  !.
122
123
  documentation_markdown_file(Path, FilePath) :-
@@ -140,6 +141,7 @@ known_source_path(RelativePath) :-
140
141
 
141
142
  directory_tree_newer(Path, SnapshotTime) :-
142
143
  exists_file(Path),
144
+ entity_documentation_file(Path),
143
145
  \+ ignored_documentation_file(Path),
144
146
  time_file(Path, EntryTime),
145
147
  EntryTime > SnapshotTime,
@@ -218,5 +220,12 @@ source_value_atom(Val, Atom) :-
218
220
  ignored_documentation_file(Path) :-
219
221
  file_base_name(Path, 'README.md').
220
222
 
223
+ entity_documentation_file(Path) :-
224
+ read_file_to_string(Path, Content, []),
225
+ sub_string(Content, 0, 3, _, "---"),
226
+ sub_string(Content, _, _, _, "id:"),
227
+ sub_string(Content, _, _, _, "title:"),
228
+ sub_string(Content, _, _, _, "status:").
229
+
221
230
  dict_json_string(Dict, JsonString) :-
222
231
  with_output_to(string(JsonString), json_write_dict(current_output, Dict, [])).