kibi-core 0.10.0 → 0.12.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.10.0",
3
+ "version": "0.12.0",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
@@ -8,7 +8,10 @@
8
8
  "node": ">=18"
9
9
  },
10
10
  "main": "./src/kb.pl",
11
- "files": ["src/**/*.pl", "schema/**/*.pl"],
11
+ "files": [
12
+ "src/**/*.pl",
13
+ "schema/**/*.pl"
14
+ ],
12
15
  "license": "AGPL-3.0-or-later",
13
16
  "author": "Piotr Franczyk",
14
17
  "repository": {
@@ -3,6 +3,7 @@
3
3
  :- module(kibi_entities, [entity_type/1, entity_property/3, required_property/2, optional_property/2]).
4
4
 
5
5
  % Entity types
6
+ % implements REQ-004
6
7
  entity_type(req).
7
8
  entity_type(scenario).
8
9
  entity_type(test).
@@ -36,6 +37,8 @@ entity_property(req, semantic_inventory_version, string).
36
37
  entity_property(req, semantic_source_field, string).
37
38
  entity_property(req, semantic_source_hash, string).
38
39
  entity_property(req, semantic_inventory, list_or_json).
40
+ entity_property(req, proof_exempt, boolean).
41
+ entity_property(req, proof_exempt_reason, string).
39
42
 
40
43
  % Typed fact fields - only valid for fact entities
41
44
  entity_property(fact, fact_kind, atom).
@@ -87,7 +90,9 @@ entity_property(symbol, sourceEndColumn, integer).
87
90
  % Typed test verification fields - only valid for test entities
88
91
  entity_property(test, verification_scope, atom).
89
92
  entity_property(test, verification_perspective, atom).
90
- entity_property(test, verification_receipts, string).
93
+ entity_property(test, proof_contract, list_or_json).
94
+ entity_property(test, proof_bindings, string).
95
+ entity_property(test, proof_receipts, string).
91
96
 
92
97
  % Required properties for all entity types
93
98
  required_property(Type, id) :- entity_type(Type).
@@ -111,9 +116,13 @@ optional_property(req, semantic_inventory_version).
111
116
  optional_property(req, semantic_source_field).
112
117
  optional_property(req, semantic_source_hash).
113
118
  optional_property(req, semantic_inventory).
119
+ optional_property(req, proof_exempt).
120
+ optional_property(req, proof_exempt_reason).
114
121
  optional_property(test, verification_scope).
115
122
  optional_property(test, verification_perspective).
116
- optional_property(test, verification_receipts).
123
+ optional_property(test, proof_contract).
124
+ optional_property(test, proof_bindings).
125
+ optional_property(test, proof_receipts).
117
126
 
118
127
  % Documentation helpers
119
128
  % list all entity types
@@ -3,6 +3,7 @@
3
3
  :- module(kibi_relationships, [relationship_type/1, valid_relationship/3, relationship_metadata/1]).
4
4
 
5
5
  % Relationship types
6
+ % implements REQ-005
6
7
  relationship_type(depends_on).
7
8
  relationship_type(executable_for).
8
9
  relationship_type(specified_by).
@@ -119,7 +119,9 @@ is_fact_only_field(claim_span_end).
119
119
  % is_test_only_field(+Key) - true if Key is a test-specific field
120
120
  is_test_only_field(verification_scope).
121
121
  is_test_only_field(verification_perspective).
122
- is_test_only_field(verification_receipts).
122
+ is_test_only_field(proof_contract).
123
+ is_test_only_field(proof_bindings).
124
+ is_test_only_field(proof_receipts).
123
125
 
124
126
  % validate_fact_shape(+Kind, +Props)
125
127
  validate_fact_shape(subject, Props) :-
package/src/checks.pl CHANGED
@@ -6,6 +6,7 @@
6
6
  :- module(checks, [
7
7
  check_all/1, % Returns all violations as a dict
8
8
  check_all_json/1, % Returns all violations as JSON string
9
+ check_selected_json/2, % Returns only selected rule violations as JSON
9
10
  check_must_priority_coverage/1, % Returns list of must-priority violations
10
11
  check_symbol_coverage/1, % Returns list of uncovered symbols
11
12
  check_symbol_traceability/2, % Returns list of symbols lacking requirement traceability (ReqAdr option)
@@ -14,6 +15,7 @@
14
15
  check_required_fields/1, % Returns list of missing required field violations
15
16
  check_deprecated_adrs/1, % Returns list of deprecated ADR violations
16
17
  check_domain_contradictions/1, % Returns list of contradiction violations
18
+ check_domain_contradictions_and_witnesses/2,
17
19
  check_domain_contradiction_witnesses/1,
18
20
  check_strict_fact_shape/1, % Returns list of malformed strict fact violations
19
21
  check_strict_req_fact_pairing/1,% Returns list of malformed strict req/fact pairing violations
@@ -23,6 +25,7 @@
23
25
  check_rule_safety/1,
24
26
  check_rule_verifiability/1,
25
27
  check_semantic_completeness/1,
28
+ check_req_status_vocabulary/1,
26
29
  run_checks_json/0, % Entry point for JSON output
27
30
  violation_id_text/2 % Extract text from entity ID term (exported for testing)
28
31
  ]).
@@ -33,6 +36,8 @@
33
36
  :- use_module('../schema/entities.pl', [entity_type/1, required_property/2]).
34
37
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
35
38
  :- 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]).
39
+ % GENERATED registry facts; single source is schema/rule-registry.json.
40
+ :- use_module('rule_registry.pl', [known_rule/1]).
36
41
 
37
42
  % Required fields for all entities
38
43
  required_fields([id, title, status, created_at, updated_at, source]).
@@ -64,6 +69,7 @@ check_all(ViolationsDict) :-
64
69
  check_rule_safety(RuleSafety),
65
70
  check_rule_verifiability(RuleVerifiability),
66
71
  check_semantic_completeness(SemanticCompleteness),
72
+ check_req_status_vocabulary(ReqStatusVocabulary),
67
73
  ViolationsDict = _{
68
74
  must_priority_coverage: MustPriority,
69
75
  symbol_coverage: SymbolCoverage,
@@ -80,7 +86,8 @@ check_all(ViolationsDict) :-
80
86
  logic_coverage: LogicCoverage,
81
87
  rule_safety: RuleSafety,
82
88
  rule_verifiability: RuleVerifiability,
83
- semantic_completeness: SemanticCompleteness
89
+ semantic_completeness: SemanticCompleteness,
90
+ req_status_vocabulary: ReqStatusVocabulary
84
91
  }.
85
92
 
86
93
  %% check_must_priority_coverage(-Violations)
@@ -361,6 +368,39 @@ deprecated_adr_violation(violation(
361
368
  ; Source = ""
362
369
  ).
363
370
 
371
+ %% check_req_status_vocabulary(-Violations)
372
+ % Rejects requirement statuses outside the canonical+legacy vocabulary.
373
+ % Requirement documents carrying ADR vocabulary (e.g. `status: accepted`)
374
+ % compile and pass schema validation, then silently fall out of
375
+ % current_req/1 — the proof ladder reports them not_applicable with no
376
+ % signal. This rule surfaces the vocabulary mismatch at check time instead.
377
+ canonical_req_statuses([open, in_progress, closed]).
378
+ legacy_req_statuses([active, approved]).
379
+
380
+ check_req_status_vocabulary(Violations) :-
381
+ findall(
382
+ Violation,
383
+ req_status_vocabulary_violation(Violation),
384
+ Violations
385
+ ).
386
+
387
+ req_status_vocabulary_violation(violation(
388
+ 'req-status-vocabulary',
389
+ ReqId,
390
+ Description,
391
+ Suggestion,
392
+ Source
393
+ )) :-
394
+ kb_entity(ReqId, req, Props),
395
+ memberchk(status=RawStatus, Props),
396
+ normalize_term_atom(RawStatus, StatusAtom),
397
+ canonical_req_statuses(Canonical),
398
+ legacy_req_statuses(Legacy),
399
+ \+ (memberchk(StatusAtom, Canonical) ; memberchk(StatusAtom, Legacy)),
400
+ format(string(Description), "Requirement status '~w' is not a current requirement status; it is silently excluded from the proof ladder", [StatusAtom]),
401
+ format(string(Suggestion), "Set status to one of open, in_progress, closed (legacy: active, approved). To park a current requirement out of E2E-proof scope, use proof_exempt: true with proof_exempt_reason instead of an ADR status", []),
402
+ violation_source(ReqId, req, Source).
403
+
364
404
  %% check_strict_fact_shape(-Violations)
365
405
  % Finds all strict facts (with fact_kind) that have malformed shape.
366
406
  % Only checks facts with fact_kind present; legacy facts without fact_kind are ignored.
@@ -591,6 +631,15 @@ check_domain_contradictions(Violations) :-
591
631
  check_domain_contradiction_witnesses(Witnesses),
592
632
  maplist(contradiction_witness_violation, Witnesses, Violations).
593
633
 
634
+ %% check_domain_contradictions_and_witnesses(-Violations, -Witnesses)
635
+ % Compute the contradiction witness graph once when a caller needs both the
636
+ % report violations and their exact evidence. The existing one-result
637
+ % predicates remain unchanged; this paired form avoids duplicate work in
638
+ % proof contexts without weakening any contradiction checks.
639
+ check_domain_contradictions_and_witnesses(Violations, Witnesses) :-
640
+ check_domain_contradiction_witnesses(Witnesses),
641
+ maplist(contradiction_witness_violation, Witnesses, Violations).
642
+
594
643
  %% check_domain_contradiction_witnesses(-Witnesses)
595
644
  % Exact, JSON-safe evidence for every strict-property, ground-predicate, and
596
645
  % safe-rule contradiction result. Rule overlap that cannot be proven or
@@ -989,9 +1038,11 @@ predicate_verifiability_kind_label(Kind, Label) :-
989
1038
 
990
1039
  %% check_logic_coverage(-Violations)
991
1040
  % Validates explicitly declared atomic requirement claim manifests. Legacy
992
- % requirements without logic_claims remain eligible for gradual backfill; once
993
- % a manifest exists, every key must be grounded by a linked strict-property or
994
- % predicate fact carrying the same claim_key.
1041
+ % requirements without logic_claims remain eligible for gradual backfill. A
1042
+ % modeled proposition must be grounded exactly once; ambiguous, ontology-gap,
1043
+ % and missing propositions remain explicit unresolved inventory states and are
1044
+ % not required to have a ground fact (missing is reported by semantic
1045
+ % completeness).
995
1046
  check_logic_coverage(Violations) :-
996
1047
  findall(Violation, logic_coverage_violation(Violation), Violations0),
997
1048
  sort(Violations0, Violations).
@@ -1006,6 +1057,7 @@ logic_coverage_violation(violation(
1006
1057
  kb:current_req(ReqId),
1007
1058
  requirement_logic_claims(ReqId, ClaimKeys),
1008
1059
  member(ClaimKey, ClaimKeys),
1060
+ claim_requires_grounding(ReqId, ClaimKey),
1009
1061
  \+ grounded_requirement_claim(ReqId, ClaimKey),
1010
1062
  format(
1011
1063
  string(Description),
@@ -1014,6 +1066,24 @@ logic_coverage_violation(violation(
1014
1066
  ),
1015
1067
  violation_source(ReqId, req, Source).
1016
1068
 
1069
+ logic_coverage_violation(violation(
1070
+ 'logic-coverage',
1071
+ ReqId,
1072
+ Description,
1073
+ "Keep ontology gaps, ambiguities, and missing interpretations unresolved; only modeled propositions may have logical ground facts",
1074
+ Source
1075
+ )) :-
1076
+ kb:current_req(ReqId),
1077
+ requirement_inventory_status(ReqId, ClaimKey, Status),
1078
+ memberchk(Status, [ambiguous, ontology_gap, missing]),
1079
+ grounded_requirement_claim(ReqId, ClaimKey),
1080
+ format(
1081
+ string(Description),
1082
+ "Requirement grounds unresolved ~w proposition ~w",
1083
+ [Status, ClaimKey]
1084
+ ),
1085
+ violation_source(ReqId, req, Source).
1086
+
1017
1087
  logic_coverage_violation(violation(
1018
1088
  'logic-coverage',
1019
1089
  ReqId,
@@ -1076,6 +1146,28 @@ requirement_logic_claims(ReqId, ClaimKeys) :-
1076
1146
  memberchk(logic_claims=RawClaimKeys, Props),
1077
1147
  kb:normalize_term_atom_list(RawClaimKeys, ClaimKeys).
1078
1148
 
1149
+ claim_requires_grounding(ReqId, ClaimKey) :-
1150
+ requirement_inventory_status(ReqId, ClaimKey, modeled),
1151
+ !.
1152
+ claim_requires_grounding(ReqId, ClaimKey) :-
1153
+ \+ requirement_inventory_status(ReqId, ClaimKey, _).
1154
+
1155
+ requirement_inventory_status(ReqId, ClaimKey, Status) :-
1156
+ kb_entity(ReqId, req, Props),
1157
+ memberchk(semantic_inventory=RawInventory, Props),
1158
+ inventory_entries(RawInventory, Entries),
1159
+ member(Entry, Entries),
1160
+ checks_inventory_entry_field(Entry, claim_key, RawClaimKey),
1161
+ kb:normalize_term_atom(RawClaimKey, ClaimKey),
1162
+ inventory_entry_status(Entry, Status).
1163
+
1164
+ checks_inventory_entry_field(Entry, Key, Value) :-
1165
+ is_dict(Entry),
1166
+ get_dict(Key, Entry, Value).
1167
+ checks_inventory_entry_field(Entry, Key, Value) :-
1168
+ is_list(Entry),
1169
+ memberchk(Key=Value, Entry).
1170
+
1079
1171
  grounded_requirement_claim(ReqId, ClaimKey) :-
1080
1172
  grounded_requirement_claim_fact(ReqId, ClaimKey, _FactId).
1081
1173
 
@@ -1222,13 +1314,15 @@ rule_verifiability_description(Props, FactId, Description) :-
1222
1314
  rule_verifiability_description(Props, FactId, Description) :-
1223
1315
  memberchk(fact_kind=RawKind, Props),
1224
1316
  normalize_term_atom(RawKind, rule),
1225
- memberchk(rule_schema_id=SchemaId, Props),
1317
+ memberchk(rule_schema_id=RawSchemaId, Props),
1318
+ normalize_term_atom(RawSchemaId, SchemaId),
1226
1319
  \+ kb_entity(SchemaId, fact, _),
1227
1320
  format(string(Description), "Rule fact ~w references missing rule schema ~w", [FactId, SchemaId]).
1228
1321
  rule_verifiability_description(Props, FactId, Description) :-
1229
1322
  memberchk(fact_kind=RawKind, Props),
1230
1323
  normalize_term_atom(RawKind, rule),
1231
- memberchk(rule_schema_id=SchemaId, Props),
1324
+ memberchk(rule_schema_id=RawSchemaId, Props),
1325
+ normalize_term_atom(RawSchemaId, SchemaId),
1232
1326
  kb_entity(SchemaId, fact, SchemaProps),
1233
1327
  \+ valid_rule_schema_props(SchemaProps),
1234
1328
  format(string(Description), "Rule fact ~w references ~w, which is not a rule_schema fact", [FactId, SchemaId]).
@@ -1286,6 +1380,10 @@ inventory_entries(Raw, Entries) :-
1286
1380
  catch(atom_json_dict(Raw, JsonEntries, [value_string_as(string)]), _, fail),
1287
1381
  is_list(JsonEntries)
1288
1382
  -> Entries = JsonEntries
1383
+ ; (atom(Raw) ; string(Raw)),
1384
+ catch(term_string(PrologEntries, Raw), _, fail),
1385
+ is_list(PrologEntries)
1386
+ -> Entries = PrologEntries
1289
1387
  ; Entries = []
1290
1388
  ).
1291
1389
 
@@ -1341,6 +1439,88 @@ check_all_json(JsonString) :-
1341
1439
  JsonString
1342
1440
  ).
1343
1441
 
1442
+ %% check_selected_json(+Rules, -JsonString)
1443
+ % Run only the named aggregated checks. This is intentionally separate from
1444
+ % check_all/1 so focused diagnostics cannot evaluate unrelated rule queries.
1445
+ check_selected_json(Rules, JsonString) :-
1446
+ check_selected(Rules, ViolationsDict),
1447
+ violations_dict_to_json(ViolationsDict, JsonDict),
1448
+ with_output_to_string(
1449
+ json_write_dict(current_output, JsonDict, [width(0)]),
1450
+ JsonString
1451
+ ).
1452
+
1453
+ %% check_selected(+Rules, -ViolationsDict)
1454
+ % Run only the named aggregated checks. This is intentionally separate from
1455
+ % check_all/1 so focused diagnostics cannot evaluate unrelated rule queries.
1456
+ % Unknown rule names fail loudly: a typo must never masquerade as a clean,
1457
+ % violation-free run. Rules implemented on the TypeScript side (recorded in
1458
+ % rule_registry.pl with implementation typescript) are accepted here and
1459
+ % simply produce no Prolog-side violations.
1460
+ check_selected(Rules, ViolationsDict) :-
1461
+ require_known_rules(Rules),
1462
+ check_selected_dispatch(Rules, ViolationsDict).
1463
+
1464
+ check_selected_dispatch(Rules, _{
1465
+ must_priority_coverage: MustPriority,
1466
+ symbol_coverage: SymbolCoverage,
1467
+ symbol_traceability: SymbolTraceability,
1468
+ no_dangling_refs: DanglingRefs,
1469
+ no_cycles: Cycles,
1470
+ required_fields: RequiredFields,
1471
+ deprecated_adr_no_successor: DeprecatedADRs,
1472
+ domain_contradictions: Contradictions,
1473
+ strict_fact_shape: StrictFactShape,
1474
+ strict_req_fact_pairing: StrictReqFactPairing,
1475
+ strict_readiness: StrictReadiness,
1476
+ predicate_verifiability: PredicateVerifiability,
1477
+ logic_coverage: LogicCoverage,
1478
+ rule_safety: RuleSafety,
1479
+ rule_verifiability: RuleVerifiability,
1480
+ semantic_completeness: SemanticCompleteness,
1481
+ req_status_vocabulary: ReqStatusVocabulary
1482
+ }) :-
1483
+ selected_rule(Rules, 'must-priority-coverage', check_must_priority_coverage, MustPriority),
1484
+ selected_rule(Rules, 'symbol-coverage', check_symbol_coverage, SymbolCoverage),
1485
+ selected_rule_with_options(Rules, 'symbol-traceability', check_symbol_traceability, SymbolTraceability),
1486
+ selected_rule(Rules, 'no-dangling-refs', check_no_dangling_refs, DanglingRefs),
1487
+ selected_rule(Rules, 'no-cycles', check_no_cycles, Cycles),
1488
+ selected_rule(Rules, 'required-fields', check_required_fields, RequiredFields),
1489
+ selected_rule(Rules, 'deprecated-adr-no-successor', check_deprecated_adrs, DeprecatedADRs),
1490
+ selected_rule(Rules, 'domain-contradictions', check_domain_contradictions, Contradictions),
1491
+ selected_rule(Rules, 'strict-fact-shape', check_strict_fact_shape, StrictFactShape),
1492
+ selected_rule(Rules, 'strict-req-fact-pairing', check_strict_req_fact_pairing, StrictReqFactPairing),
1493
+ selected_rule(Rules, 'strict-readiness', check_strict_readiness, StrictReadiness),
1494
+ selected_rule(Rules, 'predicate-verifiability', check_predicate_verifiability, PredicateVerifiability),
1495
+ selected_rule(Rules, 'logic-coverage', check_logic_coverage, LogicCoverage),
1496
+ selected_rule(Rules, 'rule-safety', check_rule_safety, RuleSafety),
1497
+ selected_rule(Rules, 'rule-verifiability', check_rule_verifiability, RuleVerifiability),
1498
+ selected_rule(Rules, 'semantic-completeness', check_semantic_completeness, SemanticCompleteness),
1499
+ selected_rule(Rules, 'req-status-vocabulary', check_req_status_vocabulary, ReqStatusVocabulary).
1500
+
1501
+ selected_rule(Rules, Name, Goal, Violations) :-
1502
+ ( memberchk(Name, Rules)
1503
+ -> call(Goal, Violations)
1504
+ ; Violations = []
1505
+ ).
1506
+
1507
+ selected_rule_with_options(Rules, Name, Goal, Violations) :-
1508
+ ( memberchk(Name, Rules)
1509
+ -> call(Goal, false, Violations)
1510
+ ; Violations = []
1511
+ ).
1512
+
1513
+ %% require_known_rules(+Rules)
1514
+ % Fail closed on names outside the generated registry so a typo or a
1515
+ % stale cross-version call surfaces as an error instead of an empty result.
1516
+ require_known_rules(Rules) :-
1517
+ exclude(rule_registry:known_rule, Rules, Unknown),
1518
+ ( Unknown == []
1519
+ -> true
1520
+ ; throw(error(domain_error(check_rule, Unknown),
1521
+ context(check_selected, 'Unknown check rules requested')))
1522
+ ).
1523
+
1344
1524
  %% check_all_json_with_options(-JsonString, +RequireAdr)
1345
1525
  % Returns all violations as JSON string with options.
1346
1526
  % RequireAdr: if true, symbol-traceability also requires ADR constraints.
@@ -1371,6 +1551,7 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1371
1551
  check_rule_safety(RuleSafety),
1372
1552
  check_rule_verifiability(RuleVerifiability),
1373
1553
  check_semantic_completeness(SemanticCompleteness),
1554
+ check_req_status_vocabulary(ReqStatusVocabulary),
1374
1555
  ViolationsDict = _{
1375
1556
  must_priority_coverage: MustPriority,
1376
1557
  symbol_coverage: SymbolCoverage,
@@ -1387,7 +1568,8 @@ check_all_with_options(ViolationsDict, RequireAdr) :-
1387
1568
  logic_coverage: LogicCoverage,
1388
1569
  rule_safety: RuleSafety,
1389
1570
  rule_verifiability: RuleVerifiability,
1390
- semantic_completeness: SemanticCompleteness
1571
+ semantic_completeness: SemanticCompleteness,
1572
+ req_status_vocabulary: ReqStatusVocabulary
1391
1573
  }.
1392
1574
 
1393
1575
  %% violations_dict_to_json(+ViolationsDict, -JsonDict)
package/src/discovery.pl CHANGED
@@ -5,13 +5,19 @@
5
5
  find_gaps_json/8,
6
6
  coverage_report_json/7,
7
7
  coverage_report_json/10,
8
+ coverage_report_json/11,
9
+ coverage_report_json/12,
10
+ coverage_evidence_json/5,
8
11
  graph_expand_json/8
9
12
  ]).
10
13
 
11
14
  :- use_module(library(http/json)).
12
15
  :- use_module(library(aggregate)).
16
+ :- use_module(library(apply)).
17
+ :- use_module(library(lists)).
18
+ :- use_module(library(pairs)).
13
19
  :- use_module('kb.pl').
14
- :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof/4]).
20
+ :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof_context/6, requirement_proof/4]).
15
21
  :- use_module('status.pl', [status_meta_dict/1]).
16
22
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
17
23
 
@@ -32,13 +38,105 @@ coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset,
32
38
  coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, unknown, '1970-01-01T00:00:00Z', 604800, JsonString).
33
39
 
34
40
  coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
35
- coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows0, Summary),
41
+ coverage_report_json(By, Tags, IncludePassing, strict_snapshot, _{}, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString).
42
+
43
+ % Slim per-requirement proof evidence for quality diagnostics (W1 push-down).
44
+ % Projects only the fields the quality diagnostics consume - proof status,
45
+ % the passing-e2e stage summary, and receipt-related gap codes - instead of
46
+ % the full coverage row set, keeping the response small on large KBs.
47
+ coverage_evidence_json(Tags, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
48
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, ProofContext),
49
+ findall(Evidence,
50
+ ( kb_entity(Id, req, Props),
51
+ matches_tags(Tags, Props),
52
+ requirement_proof(Id, Props, ProofContext, Proof),
53
+ coverage_evidence_row(Id, Proof, Evidence)
54
+ ),
55
+ Evidences),
56
+ Response = _{rows: Evidences},
57
+ dict_json_string(Response, JsonString).
58
+
59
+ coverage_evidence_row(Id, Proof, Evidence) :-
60
+ ( get_dict(proofStatus, Proof, ProofStatus)
61
+ -> true
62
+ ; ProofStatus = unknown
63
+ ),
64
+ ( get_dict(proofStages, Proof, Stages),
65
+ get_dict(passingE2e, Stages, PassingE2e),
66
+ get_dict(status, PassingE2e, PassingE2eStatus)
67
+ -> ( get_dict(tests, PassingE2e, PassingE2eTests)
68
+ -> true
69
+ ; PassingE2eTests = []
70
+ )
71
+ ; PassingE2eStatus = unknown,
72
+ PassingE2eTests = []
73
+ ),
74
+ ( get_dict(proofGaps, Proof, Gaps)
75
+ -> include(coverage_evidence_receipt_gap, Gaps, ReceiptGapCodes)
76
+ ; ReceiptGapCodes = []
77
+ ),
78
+ Evidence = _{
79
+ id: Id,
80
+ proofStatus: ProofStatus,
81
+ passingE2eStatus: PassingE2eStatus,
82
+ passingE2eTests: PassingE2eTests,
83
+ receiptGapCodes: ReceiptGapCodes
84
+ }.
85
+
86
+ coverage_evidence_receipt_gap(Gap) :-
87
+ ( atom(Gap)
88
+ -> Atom = Gap
89
+ ; format(atom(Atom), "~w", [Gap])
90
+ ),
91
+ ( sub_atom(Atom, _, _, _, 'proof_receipt')
92
+ ; sub_atom(Atom, _, _, _, 'proof_contract')
93
+ ; sub_atom(Atom, _, _, _, 'proof_snapshot')
94
+ ).
95
+
96
+ % Per-contract receipt binding (W2): BindingMode strict_snapshot | per_contract
97
+ % with TestBindings mapping TestId -> current binding hash.
98
+ coverage_report_json(By, Tags, IncludePassing, BindingMode, TestBindings, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
99
+ % Binding mode applies to requirement rows only; other By values keep
100
+ % their snapshot-era arity so type/symbol reports are unchanged.
101
+ ( By == req
102
+ -> coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Rows0, Summary)
103
+ ; coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows0, Summary)
104
+ ),
36
105
  sort_dict_rows(Rows0, SortedRows),
37
106
  paginate_rows(SortedRows, Offset, Limit, Rows),
38
107
  status_meta_dict(Meta),
39
108
  Response = _{summary: Summary, rows: Rows, meta: Meta},
40
109
  dict_json_string(Response, JsonString).
41
110
 
111
+ % implements REQ-kibi-coverage-status-filter
112
+ % Proof-status filtered report. The status filter selects requirement rows by
113
+ % proofStatus (proven, missing, unresolved, not_applicable) and replaces the
114
+ % default include-passing filtering, so callers can enumerate exactly one
115
+ % slice — e.g. every not_applicable row together with its typed applicability
116
+ % reason — without diffing full exports by hand. The summary is always
117
+ % computed over all rows so filtered responses keep whole-KB counts.
118
+ coverage_report_json(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, Limit, Offset, VerificationSnapshot, CheckedAt, MaxAgeSeconds, JsonString) :-
119
+ status_filtered_coverage_rows(By, Tags, IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, MatchedRows0, Summary),
120
+ sort_dict_rows(MatchedRows0, SortedRows),
121
+ paginate_rows(SortedRows, Offset, Limit, Rows),
122
+ status_meta_dict(Meta),
123
+ Response = _{summary: Summary, rows: Rows, meta: Meta},
124
+ dict_json_string(Response, JsonString).
125
+
126
+ status_filtered_coverage_rows(By, Tags, IncludePassing, [], IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
127
+ !,
128
+ coverage_rows(By, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary).
129
+ status_filtered_coverage_rows(req, Tags, _IncludePassing, StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, MatchedRows, Summary) :-
130
+ !,
131
+ coverage_rows(req, Tags, true, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows0, Summary),
132
+ include(req_row_proof_status_in(StatusFilter), Rows0, MatchedRows).
133
+ status_filtered_coverage_rows(By, Tags, _IncludePassing, _StatusFilter, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
134
+ coverage_rows(By, Tags, true, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary).
135
+
136
+ req_row_proof_status_in(StatusFilter, Row) :-
137
+ Status = Row.proofStatus,
138
+ memberchk(Status, StatusFilter).
139
+
42
140
  graph_expand_json(SeedIds, Relationships, Direction, Depth, EntityTypes, MaxNodes, MaxEdges, JsonString) :-
43
141
  sort(SeedIds, SeedSet),
44
142
  bfs_layers(SeedSet, SeedSet, [], Relationships, Direction, Depth, MaxNodes, MaxEdges, SeenNodes0, SeenEdges0, Truncated0),
@@ -100,9 +198,9 @@ relationship_count(Id, Relationship, Count) :-
100
198
  (kb_relationship(Relationship, Id, _); kb_relationship(Relationship, _, Id)),
101
199
  Count).
102
200
 
103
- coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
201
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, Rows, Summary) :-
104
202
  !,
105
- requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, ProofContext),
203
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, BindingMode, TestBindings, ProofContext),
106
204
  findall(Row,
107
205
  requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row),
108
206
  AllRows),
@@ -140,10 +238,14 @@ coverage_rows(type, _Tags, _IncludePassing, _IncludeTransitive, _VerificationSna
140
238
  length(Rows, Total),
141
239
  Summary = _{total: Total}.
142
240
 
241
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
242
+ !,
243
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, strict_snapshot, _{}, Rows, Summary).
244
+
143
245
  requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row) :-
144
246
  kb_entity(Id, req, Props),
145
247
  matches_tags(Tags, Props),
146
- entity_title_status_source(Props, Title, Status, _Source),
248
+ entity_title_status_source(Props, Title, Status, Source),
147
249
  entity_priority(Props, Priority),
148
250
  count_distinct_targets(specified_by, Id, source, ScenarioCount),
149
251
  requirement_test_count(Id, TestCount),
@@ -156,6 +258,7 @@ requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row) :-
156
258
  type: req,
157
259
  title: Title,
158
260
  status: Status,
261
+ source: Source,
159
262
  priority: Priority,
160
263
  scenarioCount: ScenarioCount,
161
264
  testCount: TestCount,
package/src/kb.pl CHANGED
@@ -71,6 +71,7 @@
71
71
  :- use_module(library(thread)).
72
72
  :- use_module(library(filesex)).
73
73
  :- use_module(library(readutil)).
74
+ :- use_module(library(http/json)).
74
75
  :- use_module(library(aggregate), [aggregate_all/3]).
75
76
  :- use_module(library(lists), [sum_list/2]).
76
77
  :- use_module(library(ordsets)).
@@ -156,8 +157,14 @@ kb_attach_journaled(Directory) :-
156
157
  ; make_directory_path(PersistencyDirectory)
157
158
  ),
158
159
  % rdf_persistency owns the database lock for the lifetime of the engine.
159
- rdf_attach_db(PersistencyDirectory,
160
- [access(read_write), silent(true), concurrency(4)]),
160
+ % A failed attach throws a structured store_locked term carrying the
161
+ % ownership journal (if any) so callers can surface the holder or take
162
+ % over when the holder is provably dead.
163
+ catch(rdf_attach_db(PersistencyDirectory,
164
+ [access(read_write), silent(true), concurrency(4)]),
165
+ error(permission_error(_, _, _), Original),
166
+ kb_throw_store_locked(PersistencyDirectory, Directory, Original)),
167
+ catch(kb_write_lock_owner(Directory), _, true),
161
168
  % Create RDF graph name from directory. Do not unload a graph here:
162
169
  % rdf_attach_db has already restored it from its snapshot/journal. A
163
170
  % staged migration can have been atomically moved after the graph was
@@ -229,6 +236,58 @@ kb_attach_legacy(Directory) :-
229
236
  kb_rebuild_indexes.
230
237
 
231
238
 
239
+ %% Store lock ownership journal.
240
+ % rdf_persistency holds the branch-store lock while an engine is attached, but
241
+ % records nothing about the holder. The journal bridges that gap: kibi writes
242
+ % who attached (pid, workspace, boot id, started at) next to the lock so a
243
+ % later store_locked failure can surface the holder and self-heal when the
244
+ % holder is provably dead (crash, kill, removed worktree).
245
+ % implements REQ-core-journaled-engine-persistence
246
+
247
+ kb_lock_owner_path(Directory, Path) :-
248
+ atom_concat(Directory, '/.kibi-lock-owner.json', Path).
249
+
250
+ kb_write_lock_owner(Directory) :-
251
+ kb_lock_owner_path(Directory, Path),
252
+ current_prolog_flag(pid, Pid),
253
+ ( exists_file('/proc/sys/kernel/random/boot_id')
254
+ -> catch(( read_file_to_string('/proc/sys/kernel/random/boot_id',
255
+ BootIdRaw, []),
256
+ split_string(BootIdRaw, "\r\n", "\r\n", [BootId|_])),
257
+ _,
258
+ BootId = "")
259
+ ; BootId = ""
260
+ ),
261
+ get_time(Now),
262
+ format_time(atom(StartedAt), '%FT%TZ', Now),
263
+ atom_concat(Path, '.tmp', TmpPath),
264
+ setup_call_cleanup(
265
+ open(TmpPath, write, Stream),
266
+ json_write_dict(Stream,
267
+ json([pid:Pid,
268
+ workspaceRoot:Directory,
269
+ bootId:BootId,
270
+ startedAt:StartedAt]),
271
+ []),
272
+ (close(Stream), rename_file(TmpPath, Path))).
273
+
274
+ kb_remove_lock_owner :-
275
+ ( kb_attached(Directory)
276
+ -> kb_lock_owner_path(Directory, Path),
277
+ catch(delete_file(Path), _, true)
278
+ ; true
279
+ ).
280
+
281
+ kb_throw_store_locked(PersistencyDirectory, Directory, Original) :-
282
+ kb_lock_owner_path(Directory, Path),
283
+ ( exists_file(Path)
284
+ -> catch(read_file_to_string(Path, OwnerJson, []), _, OwnerJson = "")
285
+ ; OwnerJson = ""
286
+ ),
287
+ term_string(Original, OriginalText),
288
+ throw(error(permission_error(attach, kb_store, Directory),
289
+ kb_store_locked(OwnerJson, PersistencyDirectory, OriginalText))).
290
+
232
291
  %% kb_detach
233
292
  % Safely detach from KB without persisting pending changes.
234
293
  % Call kb_save/0 explicitly before kb_detach/0 when durability is required.
@@ -238,7 +297,8 @@ kb_detach :-
238
297
  -> (
239
298
  ( kb_storage_mode(journaled)
240
299
  -> catch(rdf_flush_journals([min_size(16384)]), _, true),
241
- catch(rdf_detach_db, _, true)
300
+ catch(rdf_detach_db, _, true),
301
+ catch(kb_remove_lock_owner, _, true)
242
302
  ; true
243
303
  ),
244
304
  % Unload RDF graph from memory to prevent duplication on reattach
@@ -1836,7 +1896,13 @@ integer_property_key(Key) :-
1836
1896
  literal_to_value(semantic_inventory, Literal, Value) :-
1837
1897
  !,
1838
1898
  structured_literal_value(Literal, Value).
1839
- literal_to_value(verification_receipts, Literal, Value) :-
1899
+ literal_to_value(proof_receipts, Literal, Value) :-
1900
+ !,
1901
+ structured_literal_value(Literal, Value).
1902
+ literal_to_value(proof_contract, Literal, Value) :-
1903
+ !,
1904
+ structured_literal_value(Literal, Value).
1905
+ literal_to_value(proof_bindings, Literal, Value) :-
1840
1906
  !,
1841
1907
  structured_literal_value(Literal, Value).
1842
1908
  literal_to_value(_Key, Literal, Value) :-
package/src/logic_ir.pl CHANGED
@@ -530,7 +530,7 @@ rule_dependency_edge(
530
530
  logic_predicate_signature(atom(Namespace, Name, Args, _Polarity, _ClosedWorld), predicate(Namespace, Name, Arity)) :-
531
531
  length(Args, Arity).
532
532
 
533
- expression_dependency(atom(_Namespace, _Name, _Args, _Polarity, _ClosedWorld), Atom, positive) :-
533
+ expression_dependency(Atom, Atom, positive) :-
534
534
  Atom = atom(_, _, _, _, _).
535
535
  expression_dependency(not(Atom), Atom, negative).
536
536
  expression_dependency(all(Items), Atom, Sign) :- member(Item, Items), expression_dependency(Item, Atom, Sign).