kibi-core 0.4.0 → 0.5.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.4.0",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
@@ -9,10 +9,7 @@
9
9
  "bun": ">=1.0"
10
10
  },
11
11
  "main": "./src/kb.pl",
12
- "files": [
13
- "src/**/*.pl",
14
- "schema/**/*.pl"
15
- ],
12
+ "files": ["src/**/*.pl", "schema/**/*.pl"],
16
13
  "license": "AGPL-3.0-or-later",
17
14
  "author": "Piotr Franczyk",
18
15
  "repository": {
@@ -28,6 +28,7 @@ entity_property(_, priority, atom).
28
28
  entity_property(_, severity, atom).
29
29
  entity_property(_, links, list).
30
30
  entity_property(_, text_ref, uri).
31
+ entity_property(_, sourceFile, uri).
31
32
 
32
33
  % Typed fact fields - only valid for fact entities
33
34
  entity_property(fact, fact_kind, atom).
@@ -47,6 +48,10 @@ entity_property(fact, valid_from, datetime).
47
48
  entity_property(fact, valid_to, datetime).
48
49
  entity_property(fact, canonical_key, string).
49
50
 
51
+ % Typed test verification fields - only valid for test entities
52
+ entity_property(test, verification_scope, atom).
53
+ entity_property(test, verification_perspective, atom).
54
+
50
55
  % Required properties for all entity types
51
56
  required_property(Type, id) :- entity_type(Type).
52
57
  required_property(Type, title) :- entity_type(Type).
@@ -62,6 +67,8 @@ optional_property(Type, priority) :- entity_type(Type).
62
67
  optional_property(Type, severity) :- entity_type(Type).
63
68
  optional_property(Type, links) :- entity_type(Type).
64
69
  optional_property(Type, text_ref) :- entity_type(Type).
70
+ optional_property(test, verification_scope).
71
+ optional_property(test, verification_perspective).
65
72
 
66
73
  % Documentation helpers
67
74
  % list all entity types
@@ -4,6 +4,7 @@
4
4
 
5
5
  % Relationship types
6
6
  relationship_type(depends_on).
7
+ relationship_type(executable_for).
7
8
  relationship_type(specified_by).
8
9
  relationship_type(verified_by).
9
10
  relationship_type(validates).
@@ -20,9 +21,12 @@ relationship_type(requires_property).
20
21
 
21
22
  % valid_relationship(RelType, FromType, ToType).
22
23
  valid_relationship(depends_on, req, req).
24
+ valid_relationship(executable_for, symbol, test).
23
25
  valid_relationship(specified_by, req, scenario).
24
26
  valid_relationship(verified_by, req, test).
27
+ valid_relationship(verified_by, scenario, test).
25
28
  valid_relationship(validates, test, req).
29
+ valid_relationship(validates, test, scenario).
26
30
  valid_relationship(implements, symbol, req).
27
31
  valid_relationship(covered_by, symbol, test).
28
32
  valid_relationship(constrained_by, symbol, adr).
@@ -60,10 +60,16 @@ validate_entity_shape(fact, Props) :-
60
60
  valid_optional_fact_enums(Props),
61
61
  valid_polarity_in_props(Props),
62
62
  ( memberchk(fact_kind=RawKind, Props) -> validate_fact_shape(RawKind, Props) ; true ).
63
+ validate_entity_shape(test, Props) :-
64
+ !,
65
+ valid_optional_test_enums(Props),
66
+ forall(member(Key=_, Props), \+ is_fact_only_field(Key)).
63
67
  validate_entity_shape(Type, Props) :-
64
68
  Type \= fact,
65
- % Non-fact entities cannot have fact-only fields
66
- forall(member(Key=_Val, Props), \+ is_fact_only_field(Key)).
69
+ Type \= test,
70
+ % Non-fact/non-test entities cannot have type-specific fields
71
+ forall(member(Key=_, Props), \+ is_fact_only_field(Key)),
72
+ forall(member(Key=_, Props), \+ is_test_only_field(Key)).
67
73
 
68
74
  % is_fact_only_field(+Key) - true if Key is a fact-specific field
69
75
  is_fact_only_field(fact_kind).
@@ -83,6 +89,10 @@ is_fact_only_field(valid_from).
83
89
  is_fact_only_field(valid_to).
84
90
  is_fact_only_field(canonical_key).
85
91
 
92
+ % is_test_only_field(+Key) - true if Key is a test-specific field
93
+ is_test_only_field(verification_scope).
94
+ is_test_only_field(verification_perspective).
95
+
86
96
  % validate_fact_shape(+Kind, +Props)
87
97
  validate_fact_shape(subject, Props) :-
88
98
  memberchk(subject_key=_Val, Props),
@@ -136,10 +146,25 @@ valid_optional_fact_enums(Props) :-
136
146
  ( memberchk(operator=Op, Props) -> valid_operator(Op) ; true ),
137
147
  ( memberchk(value_type=VT, Props) -> valid_value_type(VT) ; true ).
138
148
 
149
+ % valid_optional_test_enums(+Props)
150
+ % Validates enum-typed test fields whenever they are present
151
+ valid_optional_test_enums(Props) :-
152
+ ( memberchk(verification_scope=Scope, Props) -> valid_verification_scope(Scope) ; true ),
153
+ ( memberchk(verification_perspective=Perspective, Props) -> valid_verification_perspective(Perspective) ; true ).
154
+
139
155
  % valid_polarity(+P)
140
156
  valid_polarity(require).
141
157
  valid_polarity(forbid).
142
158
 
159
+ % valid_verification_scope(+Scope)
160
+ valid_verification_scope(unit).
161
+ valid_verification_scope(integration).
162
+ valid_verification_scope(end_to_end).
163
+
164
+ % valid_verification_perspective(+Perspective)
165
+ valid_verification_perspective(internal).
166
+ valid_verification_perspective(consumer).
167
+
143
168
  % exactly_one_value_field(+Props)
144
169
  exactly_one_value_field(Props) :-
145
170
  findall(F, (member(F=_, Props), is_value_field(F)), Fields),
package/src/checks.pl CHANGED
@@ -90,7 +90,7 @@ coverage_gap_suggestion(missing_scenario, "Create scenario that specifies this r
90
90
  coverage_gap_suggestion(missing_scenario_and_test, "Create scenario that specifies and test that validates this requirement").
91
91
 
92
92
  %% check_symbol_coverage(-Violations)
93
- % Finds all symbols not traceable to any functional requirement.
93
+ % Finds all production symbols lacking qualifying production coverage.
94
94
  check_symbol_coverage(Violations) :-
95
95
  findall(SymbolId, symbol_no_req_coverage(SymbolId, _), SymbolIds0),
96
96
  sort(SymbolIds0, SymbolIds),
@@ -99,15 +99,15 @@ check_symbol_coverage(Violations) :-
99
99
  symbol_coverage_violation(SymbolId, violation(
100
100
  'symbol-coverage',
101
101
  SymbolId,
102
- "Code symbol is not traceable to any functional requirement.",
103
- "Update symbols.yaml to link this symbol to a related requirement.",
102
+ "Production symbol lacks qualifying requirement coverage.",
103
+ "Add 'covered_by: TEST-xxx' for production coverage, and ensure that test reaches the requirement through a scenario->test path or direct req->test fallback when no scenario exists.",
104
104
  Source
105
105
  )) :-
106
106
  violation_source(SymbolId, symbol, Source).
107
107
 
108
108
  %% check_symbol_traceability(+RequireAdr, -Violations)
109
- % Finds all symbols lacking supported requirement traceability:
110
- % - Every symbol must have at least one supported requirement traceability path
109
+ % Finds all symbols lacking direct requirement ownership:
110
+ % - Every symbol must have at least one direct implements ownership path
111
111
  % - If RequireAdr=true, the symbol must also have at least one 'constrained_by' relationship to an ADR
112
112
  check_symbol_traceability(RequireAdr, Violations) :-
113
113
  findall(
@@ -125,8 +125,9 @@ symbol_traceability_violation(RequireAdr, violation(
125
125
  Source
126
126
  )) :-
127
127
  kb_entity(SymbolId, symbol, _),
128
- % Check if symbol has a supported requirement traceability path
129
- ( transitively_implements(SymbolId, ReqId),
128
+ \+ executable_test_symbol(SymbolId),
129
+ % Check if symbol has direct requirement ownership
130
+ ( symbol_owns_requirement(SymbolId, ReqId),
130
131
  kb_entity(ReqId, req, _)
131
132
  -> HasReq = true
132
133
  ; HasReq = false
@@ -142,11 +143,11 @@ symbol_traceability_violation(RequireAdr, violation(
142
143
  ),
143
144
  % Determine what is missing
144
145
  ( HasReq = false, HasAdr = false, RequireAdr = true ->
145
- Description = "Symbol has no supported requirement traceability path and no ADR constraint.",
146
- Suggestion = "Add a direct 'implements: REQ-xxx' link or a test-backed 'covered_by' + 'validates'/'verified_by' path, and add 'constrained_by: ADR-xxx' in symbols.yaml."
146
+ Description = "Symbol has no direct requirement ownership and no ADR constraint.",
147
+ Suggestion = "Add a direct 'implements: REQ-xxx' link for ownership, use 'covered_by' only for production coverage, use 'executable_for' only for executable test code, and add 'constrained_by: ADR-xxx' in symbols.yaml."
147
148
  ; HasReq = false ->
148
- Description = "Symbol has no supported requirement traceability path.",
149
- Suggestion = "Add a direct 'implements: REQ-xxx' link or a test-backed 'covered_by' + 'validates'/'verified_by' path."
149
+ Description = "Symbol has no direct requirement ownership.",
150
+ Suggestion = "Add a direct 'implements: REQ-xxx' link for ownership. Use 'covered_by' for production coverage and 'executable_for' for executable test code identity."
150
151
  ; HasAdr = false ->
151
152
  Description = "Symbol has no ADR constraint.",
152
153
  Suggestion = "Add 'constrained_by: ADR-xxx' in symbols.yaml."
@@ -177,7 +178,7 @@ dangling_ref_violation(Type, violation(
177
178
  "Remove relationship or create missing entity",
178
179
  ""
179
180
  )) :-
180
- kb_relationship(Type, FromId, ToId),
181
+ kb_relationship(Type, FromId, _ToId),
181
182
  \+ kb_entity(FromId, _, _), % From doesn't exist
182
183
  format(string(Description), "Relationship references non-existent entity: ~w", [FromId]).
183
184
 
@@ -297,7 +298,7 @@ missing_required_field(Required, violation(
297
298
  Suggestion,
298
299
  Source
299
300
  )) :-
300
- kb_entity(EntityId, Type, Props),
301
+ kb_entity(EntityId, _Type, Props),
301
302
  member(Field, Required),
302
303
  \+ memberchk(Field=_, Props),
303
304
 
package/src/kb.pl CHANGED
@@ -15,6 +15,14 @@
15
15
  kb_assert_relationship_no_audit/4,
16
16
  kb_log_relationship_upsert/4,
17
17
  kb_relationship/3,
18
+ symbol_owns_requirement/2,
19
+ scenario_verified_by_test/2,
20
+ requirement_test_fallback_allowed/1,
21
+ test_satisfies_requirement_semantics/2,
22
+ production_symbol_covered_for_requirement/2,
23
+ production_symbol_untested/1,
24
+ executable_test_symbol/1,
25
+ mixed_role_symbol/1,
18
26
  transitively_implements/2,
19
27
  transitively_depends/2,
20
28
  impacted_by_change/2,
@@ -382,6 +390,7 @@ kb_assert_relationship_no_audit(RelType, FromId, ToId, _Metadata) :-
382
390
  once(kb_entity(FromId, FromType, _)),
383
391
  once(kb_entity(ToId, ToType, _)),
384
392
  validate_relationship(RelType, FromType, ToType),
393
+ validate_symbol_role_compatibility(RelType, FromId, ToId),
385
394
  % NOTE: Strict-lane fact_kind pairing is validated at the MCP layer
386
395
  % via validateStrictLanePairing() before the transaction begins.
387
396
  % Prolog-level validation is deferred to avoid potential issues with
@@ -400,6 +409,25 @@ kb_assert_relationship_no_audit(RelType, FromId, ToId, _Metadata) :-
400
409
  rdf_assert(FromURI, RelURI, ToURI, Graph)
401
410
  )).
402
411
 
412
+ validate_symbol_role_compatibility(RelType, FromId, _ToId) :-
413
+ memberchk(RelType, [implements, covered_by, executable_for]),
414
+ !,
415
+ ( mixed_symbol_role(RelType, FromId)
416
+ -> format(atom(Msg), 'symbol ~w cannot mix executable_for with production ownership/coverage relationships', [FromId]),
417
+ throw(error(validation_error(Msg), Msg))
418
+ ; true
419
+ ).
420
+ validate_symbol_role_compatibility(_, _, _).
421
+
422
+ mixed_symbol_role(executable_for, SymbolId) :-
423
+ ( kb_relationship(implements, SymbolId, _)
424
+ ; kb_relationship(covered_by, SymbolId, _)
425
+ ).
426
+ mixed_symbol_role(implements, SymbolId) :-
427
+ kb_relationship(executable_for, SymbolId, _).
428
+ mixed_symbol_role(covered_by, SymbolId) :-
429
+ kb_relationship(executable_for, SymbolId, _).
430
+
403
431
  %% kb_log_relationship_upsert(+Type, +From, +To, +Metadata)
404
432
  % Append the audit entry for a successfully committed relationship upsert.
405
433
  kb_log_relationship_upsert(RelType, FromId, ToId, _Metadata) :-
@@ -567,18 +595,85 @@ uri_to_key(URI, Key) :-
567
595
  %% Inference predicates (Phase 1)
568
596
  %% ------------------------------------------------------------------
569
597
 
570
- %% transitively_implements(+Symbol, +Req)
571
- % A symbol transitively implements a requirement if it directly implements it,
572
- % or if it is covered by a test that validates/verifies the requirement.
573
- transitively_implements(Symbol, Req) :-
598
+ %% symbol_owns_requirement(+Symbol, +Req)
599
+ % Direct requirement ownership for production code.
600
+ symbol_owns_requirement(Symbol, Req) :-
574
601
  kb_relationship(implements, Symbol, Req).
602
+
603
+ %% transitively_implements(+Symbol, +Req)
604
+ % Ownership is direct only; coverage/test traceability is handled separately.
575
605
  transitively_implements(Symbol, Req) :-
576
- kb_relationship(covered_by, Symbol, Test),
606
+ symbol_owns_requirement(Symbol, Req).
607
+
608
+ %% scenario_verified_by_test(+Scenario, +Test)
609
+ % Canonical scenario-to-test verification path.
610
+ scenario_verified_by_test(Scenario, Test) :-
611
+ kb_relationship(validates, Test, Scenario).
612
+ scenario_verified_by_test(Scenario, Test) :-
613
+ kb_relationship(verified_by, Scenario, Test).
614
+
615
+ %% requirement_test_fallback_allowed(+Req)
616
+ % Direct requirement-to-test fallback is only allowed when no scenario exists.
617
+ requirement_test_fallback_allowed(Req) :-
618
+ \+ has_scenario(Req).
619
+
620
+ %% executable_test_symbol(+Symbol)
621
+ % Symbol represents executable test code rather than production code.
622
+ executable_test_symbol(Symbol) :-
623
+ kb_relationship(executable_for, Symbol, _).
624
+
625
+ %% mixed_role_symbol(+Symbol)
626
+ % Invalid symbol carrying both executable test identity and production semantics.
627
+ mixed_role_symbol(Symbol) :-
628
+ executable_test_symbol(Symbol),
629
+ ( kb_relationship(implements, Symbol, _)
630
+ ; kb_relationship(covered_by, Symbol, _)
631
+ ).
632
+
633
+ %% production_symbol(+Symbol)
634
+ % Internal helper for production-only symbol checks.
635
+ production_symbol(Symbol) :-
636
+ kb_entity(Symbol, symbol, _),
637
+ \+ executable_test_symbol(Symbol).
638
+
639
+ %% requirement_verified_by_test(+Req, +Test)
640
+ % Direct req<->test compatibility path.
641
+ requirement_verified_by_test(Req, Test) :-
577
642
  kb_relationship(validates, Test, Req).
578
- transitively_implements(Symbol, Req) :-
579
- kb_relationship(covered_by, Symbol, Test),
643
+ requirement_verified_by_test(Req, Test) :-
580
644
  kb_relationship(verified_by, Req, Test).
581
645
 
646
+ %% test_satisfies_requirement_semantics(+Test, +Req)
647
+ % Matches requirement verification facts against typed test fields when present.
648
+ % If the requirement declares no verification semantics, compatibility passes.
649
+ test_satisfies_requirement_semantics(Test, Req) :-
650
+ findall(Key-Expected,
651
+ required_test_semantic(Req, Key, Expected),
652
+ RequiredPairs0),
653
+ sort(RequiredPairs0, RequiredPairs),
654
+ ( RequiredPairs = []
655
+ -> true
656
+ ; kb_entity(Test, test, TestProps),
657
+ forall(member(Key-Expected, RequiredPairs),
658
+ test_matches_required_semantic(TestProps, Key, Expected))
659
+ ).
660
+
661
+ required_test_semantic(Req, Key, Expected) :-
662
+ memberchk(Key, [verification_scope, verification_perspective]),
663
+ verification_subject_key(Req, SubjectKey),
664
+ effective_req_property(Req, SubjectKey, Key, Operator, _ValueType, Value, _Unit, _Scope, Polarity),
665
+ Operator == eq,
666
+ Polarity == require,
667
+ normalize_term_atom(Value, Expected).
668
+
669
+ verification_subject_key(Req, SubjectKey) :-
670
+ format(atom(SubjectKey), 'requirement.~w.verification', [Req]).
671
+
672
+ test_matches_required_semantic(TestProps, Key, Expected) :-
673
+ memberchk(Key=ActualRaw, TestProps),
674
+ normalize_term_atom(ActualRaw, Actual),
675
+ Actual == Expected.
676
+
582
677
  %% transitively_depends(+Req1, +Req2)
583
678
  % Req1 transitively depends on Req2 through depends_on chains.
584
679
  transitively_depends(Req1, Req2) :-
@@ -658,14 +753,17 @@ has_test(Req) :-
658
753
  once(kb_relationship(validates, _, Req)).
659
754
  has_test(Req) :-
660
755
  once(kb_relationship(verified_by, Req, _)).
756
+ has_test(Req) :-
757
+ kb_relationship(specified_by, Req, Scenario),
758
+ once(kb_relationship(validates, _, Scenario)).
759
+ has_test(Req) :-
760
+ kb_relationship(specified_by, Req, Scenario),
761
+ once(kb_relationship(verified_by, Scenario, _)).
661
762
 
662
763
  %% untested_symbols(-Symbols)
663
- % Returns symbols with no test coverage relationship.
764
+ % Returns production symbols with no test coverage relationship.
664
765
  untested_symbols(Symbols) :-
665
- setof(Symbol,
666
- (kb_entity(Symbol, symbol, _),
667
- \+ kb_relationship(covered_by, Symbol, _)),
668
- Symbols),
766
+ setof(Symbol, production_symbol_untested(Symbol), Symbols),
669
767
  !.
670
768
  untested_symbols([]).
671
769
 
@@ -683,9 +781,9 @@ stale(Entity, MaxAgeDays) :-
683
781
  AgeDays > MaxAgeDays.
684
782
 
685
783
  %% orphaned(+Symbol)
686
- % Symbol is orphaned if it has no core traceability links.
784
+ % Production symbol is orphaned if it has no core traceability links.
687
785
  orphaned(Symbol) :-
688
- kb_entity(Symbol, symbol, _),
786
+ production_symbol(Symbol),
689
787
  \+ kb_relationship(implements, Symbol, _),
690
788
  \+ kb_relationship(covered_by, Symbol, _),
691
789
  \+ kb_relationship(constrained_by, Symbol, _).
@@ -1001,15 +1099,40 @@ normalize_uri_atom(Value, Atom) :-
1001
1099
  ).
1002
1100
 
1003
1101
  %% symbol_no_req_coverage(+Symbol, -Reason)
1004
- % Find symbols that are not traceable to any functional requirement.
1005
- symbol_no_req_coverage(Symbol, no_path_to_req) :-
1006
- kb_entity(Symbol, symbol, _),
1007
- \+ transitively_implements(Symbol, _).
1102
+ % Find symbols that lack canonical production requirement coverage.
1103
+ symbol_no_req_coverage(Symbol, no_qualifying_production_coverage) :-
1104
+ production_symbol(Symbol),
1105
+ \+ production_symbol_covered_for_requirement(Symbol, _).
1106
+
1107
+ %% production_symbol_covered_for_requirement(+Symbol, +Req)
1108
+ % Production coverage requires a covered_by edge and a canonical requirement/test path.
1109
+ production_symbol_covered_for_requirement(Symbol, Req) :-
1110
+ production_symbol(Symbol),
1111
+ kb_relationship(covered_by, Symbol, Test),
1112
+ test_covers_requirement(Test, Req).
1113
+
1114
+ symbol_has_req_coverage(Symbol, Req) :-
1115
+ production_symbol_covered_for_requirement(Symbol, Req).
1116
+
1117
+ test_covers_requirement(Test, Req) :-
1118
+ requirement_test_fallback_allowed(Req),
1119
+ requirement_verified_by_test(Req, Test),
1120
+ test_satisfies_requirement_semantics(Test, Req).
1121
+ test_covers_requirement(Test, Req) :-
1122
+ kb_relationship(specified_by, Req, Scenario),
1123
+ scenario_verified_by_test(Scenario, Test),
1124
+ test_satisfies_requirement_semantics(Test, Req).
1125
+
1126
+ %% production_symbol_untested(+Symbol)
1127
+ % Production symbol with no covered_by test evidence at all.
1128
+ production_symbol_untested(Symbol) :-
1129
+ production_symbol(Symbol),
1130
+ \+ kb_relationship(covered_by, Symbol, _).
1008
1131
 
1009
1132
  % Helper predicate for readability - symbols with no traceability
1010
1133
  symbol_uncovered(Symbol) :-
1011
- kb_entity(Symbol, symbol, _),
1012
- \+ transitively_implements(Symbol, _).
1134
+ production_symbol(Symbol),
1135
+ \+ production_symbol_covered_for_requirement(Symbol, _).
1013
1136
 
1014
1137
 
1015
1138
  coerce_timestamp_atom(Val^^_Type, Atom) :-
@@ -1045,7 +1168,8 @@ coerce_timestamp_atom(Val, Atom) :-
1045
1168
  % counted so that `// implements: REQ-001` can satisfy the gate.
1046
1169
  changed_symbol_missing_req(Symbol, MinLinks, Count) :-
1047
1170
  changed_symbol(Symbol),
1048
- ( setof(Req, transitively_implements(Symbol, Req), KbReqs)
1171
+ \+ executable_test_symbol(Symbol),
1172
+ ( setof(Req, symbol_owns_requirement(Symbol, Req), KbReqs)
1049
1173
  -> true
1050
1174
  ; KbReqs = []
1051
1175
  ),