kibi-core 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,837 @@
1
+ % Module: requirement_proof
2
+ % Conservative, inspectable proof outcomes for requirement coverage reports.
3
+
4
+ :- module(requirement_proof, [
5
+ requirement_proof_context/1,
6
+ requirement_proof_context/4,
7
+ requirement_proof/4
8
+ ]).
9
+
10
+ :- use_module(library(http/json)).
11
+ :- use_module(library(date)).
12
+ :- use_module(library(pcre)).
13
+ :- use_module('kb.pl').
14
+ :- use_module('checks.pl', [
15
+ check_domain_contradictions/1,
16
+ check_domain_contradiction_witnesses/1,
17
+ check_rule_safety/1,
18
+ check_rule_verifiability/1
19
+ ]).
20
+ :- use_module('logic_ir.pl', [logic_rule_safety/2]).
21
+
22
+ proof_version('kibi.requirement-proof.v2').
23
+
24
+ requirement_proof_context(Context) :-
25
+ requirement_proof_context(unknown, '1970-01-01T00:00:00Z', 604800, Context).
26
+
27
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, Context) :-
28
+ check_domain_contradictions(Contradictions),
29
+ check_domain_contradiction_witnesses(ContradictionWitnesses),
30
+ check_rule_safety(UnsafeRules),
31
+ check_rule_verifiability(UnverifiableRules),
32
+ normalize_atom(VerificationSnapshot, Snapshot),
33
+ normalize_atom(CheckedAt, CheckedAtAtom),
34
+ (catch(parse_time(CheckedAtAtom, CheckedAtStamp), _, fail) -> true ; CheckedAtStamp = -1),
35
+ normalize_integer(MaxAgeSeconds, MaxAge),
36
+ Context = _{
37
+ contradictions: Contradictions,
38
+ contradictionWitnesses: ContradictionWitnesses,
39
+ unsafeRules: UnsafeRules,
40
+ unverifiableRules: UnverifiableRules,
41
+ verificationSnapshot: Snapshot,
42
+ verificationCheckedAt: CheckedAtAtom,
43
+ verificationCheckedAtStamp: CheckedAtStamp,
44
+ verificationMaxAgeSeconds: MaxAge
45
+ }.
46
+
47
+ requirement_proof(ReqId, _ReqProps, _Context, Proof) :-
48
+ \+ kb:current_req(ReqId),
49
+ !,
50
+ proof_version(Version),
51
+ Proof = _{
52
+ proofVersion: Version,
53
+ proofStatus: not_applicable,
54
+ proofGaps: [],
55
+ proofRepairs: [],
56
+ proofStages: _{applicability: _{status: not_applicable}}
57
+ }.
58
+ requirement_proof(ReqId, ReqProps, Context, Proof) :-
59
+ semantic_inventory_stage(ReqProps, SemanticStage, Inventory),
60
+ logic_grounding_stage(ReqId, ReqProps, Inventory, Context, LogicStage),
61
+ contradiction_stage(ReqId, LogicStage.status, Context, ContradictionStage),
62
+ scenario_stage(ReqId, ScenarioStage, ScenarioIds),
63
+ scenario_test_stage(ScenarioIds, ScenarioTestStage, ScenarioTests),
64
+ passing_e2e_stage(ScenarioTests, Context, PassingE2eStage, PassingE2eTests),
65
+ executable_symbol_stage(PassingE2eTests, ExecutableStage, ExecutableSymbols),
66
+ production_symbol_stage(ReqId, PassingE2eTests, ProductionStage, ProductionSymbols),
67
+ source_coordinate_stage(ReqProps, ExecutableSymbols, ProductionSymbols, CoordinateStage),
68
+ Stages = _{
69
+ semanticInventory: SemanticStage,
70
+ logicGrounding: LogicStage,
71
+ contradictions: ContradictionStage,
72
+ scenarios: ScenarioStage,
73
+ scenarioTests: ScenarioTestStage,
74
+ passingE2e: PassingE2eStage,
75
+ executableSymbols: ExecutableStage,
76
+ productionSymbols: ProductionStage,
77
+ sourceCoordinates: CoordinateStage
78
+ },
79
+ proof_gaps(Stages, Gaps),
80
+ proof_repairs(Gaps, Repairs),
81
+ proof_status(Stages, Status),
82
+ proof_version(Version),
83
+ Proof = _{
84
+ proofVersion: Version,
85
+ proofStatus: Status,
86
+ proofGaps: Gaps,
87
+ proofRepairs: Repairs,
88
+ proofStages: Stages
89
+ }.
90
+
91
+ semantic_inventory_stage(Props, Stage, Entries) :-
92
+ ( memberchk(semantic_inventory=RawInventory, Props),
93
+ inventory_entries(RawInventory, Entries0),
94
+ Entries0 \= []
95
+ -> Entries = Entries0,
96
+ inventory_counts(Entries, Modeled, Unresolved, Missing, Nonlogical, Malformed),
97
+ semantic_inventory_status(Unresolved, Missing, Malformed, Status),
98
+ length(Entries, Count),
99
+ Stage = _{
100
+ status: Status,
101
+ propositionCount: Count,
102
+ modeledCount: Modeled,
103
+ unresolvedCount: Unresolved,
104
+ missingCount: Missing,
105
+ nonlogicalCount: Nonlogical,
106
+ malformedCount: Malformed
107
+ }
108
+ ; Entries = [],
109
+ Stage = _{
110
+ status: missing,
111
+ propositionCount: 0,
112
+ modeledCount: 0,
113
+ unresolvedCount: 0,
114
+ missingCount: 0,
115
+ nonlogicalCount: 0,
116
+ malformedCount: 0
117
+ }
118
+ ).
119
+
120
+ inventory_counts(Entries, Modeled, Unresolved, Missing, Nonlogical, Malformed) :-
121
+ include(inventory_entry_status_is(modeled), Entries, ModeledEntries),
122
+ include(inventory_entry_unresolved, Entries, UnresolvedEntries),
123
+ include(inventory_entry_status_is(missing), Entries, MissingEntries),
124
+ include(inventory_entry_status_is(nonlogical), Entries, NonlogicalEntries),
125
+ exclude(valid_inventory_entry, Entries, MalformedEntries),
126
+ length(ModeledEntries, Modeled),
127
+ length(UnresolvedEntries, Unresolved),
128
+ length(MissingEntries, Missing),
129
+ length(NonlogicalEntries, Nonlogical),
130
+ length(MalformedEntries, Malformed).
131
+
132
+ semantic_inventory_status(_Unresolved, Missing, _Malformed, missing) :-
133
+ Missing > 0,
134
+ !.
135
+ semantic_inventory_status(_Unresolved, _Missing, Malformed, missing) :-
136
+ Malformed > 0,
137
+ !.
138
+ semantic_inventory_status(Unresolved, _Missing, _Malformed, unresolved) :-
139
+ Unresolved > 0,
140
+ !.
141
+ semantic_inventory_status(_, _, _, passed).
142
+
143
+ valid_inventory_entry(Entry) :-
144
+ inventory_entry_field(Entry, claim_key, ClaimKey),
145
+ normalize_atom(ClaimKey, ClaimKeyAtom),
146
+ ClaimKeyAtom \= '',
147
+ inventory_entry_field(Entry, claim_text, ClaimText),
148
+ normalize_atom(ClaimText, ClaimTextAtom),
149
+ ClaimTextAtom \= '',
150
+ inventory_entry_field(Entry, role, _),
151
+ inventory_entry_status(Entry, Status),
152
+ memberchk(Status, [modeled, ambiguous, ontology_gap, nonlogical, missing]),
153
+ inventory_entry_field(Entry, span, Span),
154
+ valid_inventory_span(Span).
155
+
156
+ valid_inventory_span(Span) :-
157
+ inventory_entry_field(Span, start, StartRaw),
158
+ inventory_entry_field(Span, end, EndRaw),
159
+ normalize_integer(StartRaw, Start),
160
+ normalize_integer(EndRaw, End),
161
+ Start >= 0,
162
+ End > Start.
163
+
164
+ inventory_entry_status_is(Expected, Entry) :-
165
+ inventory_entry_status(Entry, Expected).
166
+
167
+ inventory_entry_unresolved(Entry) :-
168
+ inventory_entry_status(Entry, Status),
169
+ memberchk(Status, [ambiguous, ontology_gap]).
170
+
171
+ inventory_entry_status(Entry, Status) :-
172
+ inventory_entry_field(Entry, status, RawStatus),
173
+ normalize_atom(RawStatus, Status).
174
+
175
+ inventory_entry_field(Dict, Key, Value) :-
176
+ is_dict(Dict),
177
+ get_dict(Key, Dict, Value).
178
+ inventory_entry_field(List, Key, Value) :-
179
+ is_list(List),
180
+ memberchk(Key=Value, List).
181
+
182
+ inventory_entries(Raw, Entries) :-
183
+ ( is_list(Raw)
184
+ -> Entries = Raw
185
+ ; Raw = ^^(Value, _)
186
+ -> inventory_entries(Value, Entries)
187
+ ; (atom(Raw) ; string(Raw)),
188
+ catch(atom_json_dict(Raw, JsonEntries, [value_string_as(string)]), _, fail),
189
+ is_list(JsonEntries)
190
+ -> Entries = JsonEntries
191
+ ; (atom(Raw) ; string(Raw)),
192
+ catch(term_string(PrologEntries, Raw), _, fail),
193
+ is_list(PrologEntries)
194
+ -> Entries = PrologEntries
195
+ ; Entries = []
196
+ ).
197
+
198
+ logic_grounding_stage(ReqId, Props, Inventory, Context, Stage) :-
199
+ requirement_claim_keys(Props, ManifestPresent, ManifestKeys),
200
+ inventory_assertive_keys(Inventory, InventoryKeys),
201
+ inventory_modeled_keys(Inventory, ModeledKeys),
202
+ findall(Evidence, valid_ground_evidence(ReqId, Evidence), Evidence0),
203
+ sort(Evidence0, Evidence),
204
+ grounded_claim_keys(Evidence, GroundedKeys),
205
+ missing_keys(ModeledKeys, GroundedKeys, MissingGroundKeys),
206
+ duplicate_ground_keys(Evidence, DuplicateGroundKeys),
207
+ inventory_ground_text_mismatches(Inventory, Evidence, ClaimTextMismatchKeys),
208
+ invalid_linked_claim_keys(ReqId, InvalidGroundKeys),
209
+ missing_keys(InventoryKeys, ManifestKeys, MissingManifestKeys),
210
+ missing_keys(ManifestKeys, InventoryKeys, ExtraManifestKeys),
211
+ linked_ground_keys(ReqId, LinkedGroundKeys),
212
+ missing_keys(LinkedGroundKeys, ManifestKeys, UndeclaredGroundKeys),
213
+ linked_rule_fact_ids(ReqId, RuleFactIds),
214
+ invalid_rule_fact_ids(RuleFactIds, Context, InvalidRuleFactIds),
215
+ logic_grounding_status(
216
+ ManifestPresent,
217
+ MissingGroundKeys,
218
+ MissingManifestKeys,
219
+ ExtraManifestKeys,
220
+ DuplicateGroundKeys,
221
+ ClaimTextMismatchKeys,
222
+ InvalidGroundKeys,
223
+ UndeclaredGroundKeys,
224
+ InvalidRuleFactIds,
225
+ Status
226
+ ),
227
+ Stage = _{
228
+ status: Status,
229
+ manifestClaims: ManifestKeys,
230
+ inventoryClaims: InventoryKeys,
231
+ modeledClaims: ModeledKeys,
232
+ groundedClaims: GroundedKeys,
233
+ evidence: Evidence,
234
+ missingGroundClaims: MissingGroundKeys,
235
+ duplicateGroundClaims: DuplicateGroundKeys,
236
+ claimTextMismatchClaims: ClaimTextMismatchKeys,
237
+ invalidGroundClaims: InvalidGroundKeys,
238
+ missingManifestClaims: MissingManifestKeys,
239
+ extraManifestClaims: ExtraManifestKeys,
240
+ undeclaredGroundClaims: UndeclaredGroundKeys,
241
+ invalidRuleFacts: InvalidRuleFactIds
242
+ }.
243
+
244
+ requirement_claim_keys(Props, true, Keys) :-
245
+ memberchk(logic_claims=RawKeys, Props),
246
+ kb:normalize_term_atom_list(RawKeys, Keys0),
247
+ sort(Keys0, Keys),
248
+ Keys \= [],
249
+ !.
250
+ requirement_claim_keys(_, false, []).
251
+
252
+ inventory_assertive_keys(Entries, Keys) :-
253
+ findall(Key,
254
+ (member(Entry, Entries),
255
+ inventory_entry_status(Entry, Status),
256
+ Status \= nonlogical,
257
+ inventory_entry_field(Entry, claim_key, RawKey),
258
+ normalize_atom(RawKey, Key)),
259
+ Keys0),
260
+ sort(Keys0, Keys).
261
+
262
+ inventory_modeled_keys(Entries, Keys) :-
263
+ findall(Key,
264
+ (member(Entry, Entries),
265
+ inventory_entry_status(Entry, modeled),
266
+ inventory_entry_field(Entry, claim_key, RawKey),
267
+ normalize_atom(RawKey, Key)),
268
+ Keys0),
269
+ sort(Keys0, Keys).
270
+
271
+ valid_ground_evidence(ReqId, _{claimKey: ClaimKey, claimText: ClaimText, factId: FactId, lane: property}) :-
272
+ kb_relationship(requires_property, ReqId, FactId),
273
+ ground_claim_provenance(FactId, property_value, ClaimKey, ClaimText),
274
+ kb:fact_property_tuple(FactId, SubjectKey, _, _, _, _, _, _, _),
275
+ kb_relationship(constrains, ReqId, SubjectFactId),
276
+ kb:fact_subject_key(SubjectFactId, SubjectKey).
277
+ valid_ground_evidence(ReqId, _{claimKey: ClaimKey, claimText: ClaimText, factId: FactId, lane: predicate}) :-
278
+ kb_relationship(requires_predicate, ReqId, FactId),
279
+ ground_claim_provenance(FactId, predicate, ClaimKey, ClaimText),
280
+ kb:predicate_fact(FactId, _, _, _, _).
281
+ valid_ground_evidence(ReqId, _{claimKey: ClaimKey, claimText: ClaimText, factId: FactId, lane: rule}) :-
282
+ kb_relationship(requires_rule, ReqId, FactId),
283
+ ground_claim_provenance(FactId, rule, ClaimKey, ClaimText),
284
+ valid_rule_fact(FactId).
285
+
286
+ ground_claim_provenance(FactId, ExpectedKind, ClaimKey, ClaimText) :-
287
+ kb_entity(FactId, fact, Props),
288
+ memberchk(fact_kind=RawKind, Props),
289
+ normalize_atom(RawKind, ExpectedKind),
290
+ memberchk(claim_key=RawClaimKey, Props),
291
+ normalize_atom(RawClaimKey, ClaimKey),
292
+ memberchk(claim_text=RawClaimText, Props),
293
+ normalize_atom(RawClaimText, ClaimText),
294
+ ClaimText \= ''.
295
+
296
+ valid_rule_fact(FactId) :-
297
+ kb_entity(FactId, fact, Props),
298
+ logic_rule_safety(Props, []),
299
+ memberchk(rule_schema_id=RawSchemaId, Props),
300
+ normalize_atom(RawSchemaId, SchemaId),
301
+ kb_entity(SchemaId, fact, SchemaProps),
302
+ valid_rule_schema_props(SchemaProps).
303
+
304
+ valid_rule_schema_props(Props) :-
305
+ memberchk(fact_kind=RawKind, Props),
306
+ normalize_atom(RawKind, rule_schema),
307
+ memberchk(rule_name=RawName, Props),
308
+ normalize_atom(RawName, Name),
309
+ Name \= '',
310
+ memberchk(argument_names=RawNames, Props),
311
+ memberchk(argument_types=RawTypes, Props),
312
+ normalize_atom_list(RawNames, Names),
313
+ normalize_atom_list(RawTypes, Types),
314
+ same_length(Names, Types).
315
+
316
+ grounded_claim_keys(Evidence, Keys) :-
317
+ findall(Key, (member(Item, Evidence), Key = Item.claimKey), Keys0),
318
+ sort(Keys0, Keys).
319
+
320
+ duplicate_ground_keys(Evidence, Keys) :-
321
+ grounded_claim_keys(Evidence, UniqueKeys),
322
+ include(has_duplicate_ground(Evidence), UniqueKeys, Keys).
323
+
324
+ has_duplicate_ground(Evidence, Key) :-
325
+ include(evidence_for_key(Key), Evidence, Matches),
326
+ length(Matches, Count),
327
+ Count > 1.
328
+
329
+ evidence_for_key(Key, Evidence) :-
330
+ Evidence.claimKey == Key.
331
+
332
+ inventory_ground_text_mismatches(Inventory, Evidence, Keys) :-
333
+ findall(Key,
334
+ (member(Item, Evidence),
335
+ Key = Item.claimKey,
336
+ member(Entry, Inventory),
337
+ inventory_entry_field(Entry, claim_key, RawKey),
338
+ normalize_atom(RawKey, Key),
339
+ inventory_entry_field(Entry, claim_text, RawInventoryText),
340
+ normalize_atom(RawInventoryText, InventoryText),
341
+ InventoryText \== Item.claimText),
342
+ Keys0),
343
+ sort(Keys0, Keys).
344
+
345
+ linked_ground_keys(ReqId, Keys) :-
346
+ findall(Key, linked_ground_claim_key(ReqId, Key), Keys0),
347
+ sort(Keys0, Keys).
348
+
349
+ linked_ground_claim_key(ReqId, ClaimKey) :-
350
+ member(Relationship-Kind, [requires_property-property_value, requires_predicate-predicate, requires_rule-rule]),
351
+ kb_relationship(Relationship, ReqId, FactId),
352
+ ground_claim_provenance(FactId, Kind, ClaimKey, _).
353
+
354
+ invalid_linked_claim_keys(ReqId, Keys) :-
355
+ findall(ClaimKey,
356
+ (linked_ground_claim_key(ReqId, ClaimKey),
357
+ \+ valid_ground_evidence_for_key(ReqId, ClaimKey)),
358
+ Keys0),
359
+ sort(Keys0, Keys).
360
+
361
+ valid_ground_evidence_for_key(ReqId, ClaimKey) :-
362
+ valid_ground_evidence(ReqId, Evidence),
363
+ Evidence.claimKey == ClaimKey.
364
+
365
+ linked_rule_fact_ids(ReqId, FactIds) :-
366
+ findall(FactId, kb_relationship(requires_rule, ReqId, FactId), FactIds0),
367
+ sort(FactIds0, FactIds).
368
+
369
+ invalid_rule_fact_ids(RuleFactIds, Context, InvalidFactIds) :-
370
+ findall(FactId,
371
+ (member(FactId, RuleFactIds),
372
+ (violation_for_entity(Context.unsafeRules, FactId)
373
+ ; violation_for_entity(Context.unverifiableRules, FactId))),
374
+ Invalid0),
375
+ sort(Invalid0, InvalidFactIds).
376
+
377
+ logic_grounding_status(false, _, _, _, _, _, _, _, _, missing) :- !.
378
+ logic_grounding_status(true, MissingGround, MissingManifest, _, _, _, _, _, _, missing) :-
379
+ (MissingGround \= [] ; MissingManifest \= []),
380
+ !.
381
+ logic_grounding_status(true, _, _, ExtraManifest, Duplicate, ClaimTextMismatch, Invalid, Undeclared, InvalidRules, unresolved) :-
382
+ (ExtraManifest \= [] ; Duplicate \= [] ; ClaimTextMismatch \= [] ; Invalid \= [] ; Undeclared \= [] ; InvalidRules \= []),
383
+ !.
384
+ logic_grounding_status(true, _, _, _, _, _, _, _, _, passed).
385
+
386
+ missing_keys(Expected, Actual, Missing) :-
387
+ findall(Key, (member(Key, Expected), \+ memberchk(Key, Actual)), Missing).
388
+
389
+ contradiction_stage(ReqId, _LogicStatus, Context, Stage) :-
390
+ requirement_contradiction_witnesses(ReqId, Context.contradictionWitnesses, Conflicts),
391
+ member(Witness, Conflicts),
392
+ Witness.status == contradiction,
393
+ Conflicts \= [],
394
+ !,
395
+ Stage = _{status: blocked, outcome: conflict_found, conflicts: Conflicts}.
396
+ contradiction_stage(ReqId, _LogicStatus, Context, Stage) :-
397
+ requirement_contradiction_witnesses(ReqId, Context.contradictionWitnesses, Conflicts),
398
+ Conflicts \= [],
399
+ !,
400
+ Stage = _{status: unresolved, outcome: analysis_incomplete, conflicts: Conflicts}.
401
+ contradiction_stage(ReqId, _LogicStatus, Context, Stage) :-
402
+ requirement_contradictions(ReqId, Context.contradictions, Conflicts),
403
+ Conflicts \= [],
404
+ !,
405
+ Stage = _{status: blocked, outcome: conflict_found, conflicts: Conflicts}.
406
+ contradiction_stage(_ReqId, LogicStatus, _Context, Stage) :-
407
+ LogicStatus \= passed,
408
+ !,
409
+ Stage = _{status: unresolved, outcome: incomplete_grounding, conflicts: []}.
410
+ contradiction_stage(_ReqId, passed, _Context, _{status: passed, outcome: no_conflict_found, conflicts: []}).
411
+
412
+ requirement_contradictions(ReqId, Violations, Conflicts) :-
413
+ include(violation_mentions_requirement(ReqId), Violations, Conflicts0),
414
+ maplist(violation_description, Conflicts0, Conflicts).
415
+
416
+ requirement_contradiction_witnesses(ReqId, Witnesses, Conflicts) :-
417
+ include(witness_mentions_requirement(ReqId), Witnesses, Conflicts).
418
+
419
+ witness_mentions_requirement(ReqId, Witness) :-
420
+ memberchk(ReqId, Witness.requirements).
421
+
422
+ violation_mentions_requirement(ReqId, violation(_, EntityId, _, _, _)) :-
423
+ normalize_atom(EntityId, Pair),
424
+ atomic_list_concat(Ids, '/', Pair),
425
+ memberchk(ReqId, Ids).
426
+
427
+ violation_description(violation(_, EntityId, Description, _, _), _{requirements: EntityId, reason: Description}).
428
+
429
+ violation_for_entity(Violations, EntityId) :-
430
+ member(violation(_, RawId, _, _, _), Violations),
431
+ normalize_atom(RawId, EntityId).
432
+
433
+ scenario_stage(ReqId, Stage, ScenarioIds) :-
434
+ findall(ScenarioId,
435
+ (kb_relationship(specified_by, ReqId, ScenarioId), kb_entity(ScenarioId, scenario, _)),
436
+ ScenarioIds0),
437
+ sort(ScenarioIds0, ScenarioIds),
438
+ (ScenarioIds = [] -> Status = missing ; Status = passed),
439
+ Stage = _{status: Status, scenarios: ScenarioIds}.
440
+
441
+ scenario_test_stage(ScenarioIds, Stage, ScenarioTests) :-
442
+ findall(TestId,
443
+ (member(ScenarioId, ScenarioIds), scenario_test(ScenarioId, TestId)),
444
+ ScenarioTests0),
445
+ sort(ScenarioTests0, ScenarioTests),
446
+ (ScenarioTests = [] -> Status = missing ; Status = passed),
447
+ Stage = _{status: Status, tests: ScenarioTests}.
448
+
449
+ scenario_test(ScenarioId, TestId) :-
450
+ kb_relationship(verified_by, ScenarioId, TestId),
451
+ kb_entity(TestId, test, _).
452
+ scenario_test(ScenarioId, TestId) :-
453
+ kb_relationship(validates, TestId, ScenarioId),
454
+ kb_entity(TestId, test, _).
455
+
456
+ passing_e2e_stage(ScenarioTests, Context, Stage, PassingE2eTests) :-
457
+ maplist(test_receipt_evidence(Context), ScenarioTests, Evidence),
458
+ evidence_tests_with_state(Evidence, passed, PassingE2eTests),
459
+ evidence_tests_with_state(Evidence, missing, MissingReceiptTests),
460
+ evidence_tests_with_state(Evidence, stale, StaleReceiptTests),
461
+ evidence_tests_with_state(Evidence, failed, FailedReceiptTests),
462
+ evidence_tests_with_state(Evidence, invalid, InvalidReceiptTests),
463
+ evidence_tests_with_state(Evidence, snapshot_unavailable, SnapshotUnavailableTests),
464
+ evidence_tests_with_state(Evidence, not_end_to_end, NonEndToEndTests),
465
+ passing_e2e_status(PassingE2eTests, InvalidReceiptTests, SnapshotUnavailableTests, Status),
466
+ Stage = _{
467
+ status: Status,
468
+ tests: PassingE2eTests,
469
+ receiptEvidence: Evidence,
470
+ missingReceiptTests: MissingReceiptTests,
471
+ staleReceiptTests: StaleReceiptTests,
472
+ failedReceiptTests: FailedReceiptTests,
473
+ invalidReceiptTests: InvalidReceiptTests,
474
+ snapshotUnavailableTests: SnapshotUnavailableTests,
475
+ nonEndToEndTests: NonEndToEndTests,
476
+ currentCodeSnapshot: Context.verificationSnapshot,
477
+ checkedAt: Context.verificationCheckedAt,
478
+ maxAgeSeconds: Context.verificationMaxAgeSeconds
479
+ }.
480
+
481
+ passing_e2e_status(Passing, _, _, passed) :- Passing \= [], !.
482
+ passing_e2e_status([], Invalid, _, unresolved) :- Invalid \= [], !.
483
+ passing_e2e_status([], _, SnapshotUnavailable, unresolved) :- SnapshotUnavailable \= [], !.
484
+ passing_e2e_status([], _, _, missing).
485
+
486
+ evidence_tests_with_state(Evidence, State, TestIds) :-
487
+ findall(TestId,
488
+ (member(Item, Evidence), Item.state == State, TestId = Item.testId),
489
+ TestIds0),
490
+ sort(TestIds0, TestIds).
491
+
492
+ test_receipt_evidence(Context, TestId, Evidence) :-
493
+ kb_entity(TestId, test, Props),
494
+ test_scope(Props, Scope),
495
+ ( Scope \= end_to_end
496
+ -> Evidence = _{testId: TestId, state: not_end_to_end, scope: Scope}
497
+ ; Context.verificationSnapshot == unknown
498
+ -> Evidence = _{testId: TestId, state: snapshot_unavailable, scope: Scope}
499
+ ; verification_receipt_entries(Props, Receipts, ReceiptPropertyPresent),
500
+ receipt_evidence_state(TestId, Scope, Receipts, ReceiptPropertyPresent, Context, Evidence)
501
+ ).
502
+
503
+ receipt_evidence_state(TestId, Scope, [], false, _Context,
504
+ _{testId: TestId, state: missing, scope: Scope, receiptCount: 0}) :- !.
505
+ receipt_evidence_state(TestId, Scope, Receipts, _Present, _Context,
506
+ _{testId: TestId, state: invalid, scope: Scope, receiptCount: Count}) :-
507
+ length(Receipts, Count),
508
+ (Receipts == []
509
+ ; member(Receipt, Receipts), \+ valid_receipt_shape(TestId, Scope, Receipt)
510
+ ; \+ chronological_receipt_history(Receipts)),
511
+ !.
512
+ receipt_evidence_state(TestId, Scope, Receipts, _Present, Context, Evidence) :-
513
+ include(receipt_for_snapshot(Context.verificationSnapshot), Receipts, CurrentReceipts),
514
+ ( CurrentReceipts == []
515
+ -> length(Receipts, Count),
516
+ Evidence = _{testId: TestId, state: stale, scope: Scope, receiptCount: Count}
517
+ ; latest_receipt(CurrentReceipts, Latest, FinishedStamp),
518
+ receipt_runtime_state(Latest, FinishedStamp, Context, State, AgeSeconds),
519
+ receipt_evidence_dict(TestId, Scope, Latest, State, AgeSeconds, Evidence)
520
+ ).
521
+
522
+ verification_receipt_entries(Props, Receipts, Present) :-
523
+ ( memberchk(verification_receipts=Raw, Props)
524
+ -> Present = true,
525
+ inventory_entries(Raw, Receipts)
526
+ ; Present = false,
527
+ Receipts = []
528
+ ).
529
+
530
+ valid_receipt_shape(TestId, Scope, Receipt) :-
531
+ inventory_entry_field(Receipt, version, RawVersion),
532
+ normalize_receipt_atom(RawVersion, 'kibi.verification-receipt.v1'),
533
+ inventory_entry_field(Receipt, receipt_id, RawReceiptId),
534
+ normalize_receipt_atom(RawReceiptId, ReceiptId), valid_receipt_id(ReceiptId),
535
+ inventory_entry_field(Receipt, test_id, RawTestId),
536
+ normalize_receipt_atom(RawTestId, TestId),
537
+ inventory_entry_field(Receipt, runner, RawRunner),
538
+ normalize_receipt_atom(RawRunner, Runner), Runner \= '',
539
+ inventory_entry_field(Receipt, command, RawCommand),
540
+ normalize_receipt_atom(RawCommand, Command), Command \= '',
541
+ inventory_entry_field(Receipt, scope, RawScope),
542
+ normalize_receipt_atom(RawScope, Scope),
543
+ inventory_entry_field(Receipt, outcome, RawOutcome),
544
+ normalize_receipt_atom(RawOutcome, Outcome),
545
+ memberchk(Outcome, [passed, failed, errored, cancelled, skipped]),
546
+ inventory_entry_field(Receipt, code_snapshot, RawSnapshot),
547
+ normalize_receipt_atom(RawSnapshot, Snapshot), valid_sha256(Snapshot),
548
+ inventory_entry_field(Receipt, environment_hash, RawEnvironmentHash),
549
+ normalize_receipt_atom(RawEnvironmentHash, EnvironmentHash), valid_sha256(EnvironmentHash),
550
+ inventory_entry_field(Receipt, artifact_digest, RawArtifactDigest),
551
+ normalize_receipt_atom(RawArtifactDigest, ArtifactDigest), valid_sha256(ArtifactDigest),
552
+ receipt_timestamp(Receipt, started_at, StartedStamp),
553
+ receipt_timestamp(Receipt, finished_at, FinishedStamp),
554
+ FinishedStamp >= StartedStamp.
555
+
556
+ valid_sha256(Value) :-
557
+ atom_length(Value, 64),
558
+ atom_chars(Value, Chars),
559
+ forall(member(Char, Chars), memberchk(Char, ['0','1','2','3','4','5','6','7','8','9',a,b,c,d,e,f])).
560
+
561
+ valid_receipt_id(Value) :-
562
+ re_match('^VR-[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$', Value).
563
+
564
+ receipt_timestamp(Receipt, Field, Stamp) :-
565
+ inventory_entry_field(Receipt, Field, RawValue),
566
+ normalize_receipt_atom(RawValue, Value),
567
+ re_match('^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:[.][0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})$', Value),
568
+ catch(parse_time(Value, Stamp), _, fail).
569
+
570
+ chronological_receipt_history(Receipts) :-
571
+ maplist(receipt_finished_stamp, Receipts, FinishedStamps),
572
+ strictly_increasing(FinishedStamps).
573
+
574
+ receipt_finished_stamp(Receipt, Stamp) :-
575
+ receipt_timestamp(Receipt, finished_at, Stamp).
576
+
577
+ strictly_increasing([]).
578
+ strictly_increasing([_]).
579
+ strictly_increasing([Left, Right|Rest]) :-
580
+ Right > Left,
581
+ strictly_increasing([Right|Rest]).
582
+
583
+ receipt_for_snapshot(Snapshot, Receipt) :-
584
+ inventory_entry_field(Receipt, code_snapshot, RawSnapshot),
585
+ normalize_receipt_atom(RawSnapshot, Snapshot).
586
+
587
+ latest_receipt(Receipts, Latest, FinishedStamp) :-
588
+ findall(Stamp-Receipt,
589
+ (member(Receipt, Receipts), receipt_timestamp(Receipt, finished_at, Stamp)),
590
+ Pairs),
591
+ keysort(Pairs, Sorted),
592
+ last(Sorted, FinishedStamp-Latest).
593
+
594
+ receipt_runtime_state(_Receipt, FinishedStamp, Context, invalid, AgeSeconds) :-
595
+ AgeSeconds is Context.verificationCheckedAtStamp - FinishedStamp,
596
+ AgeSeconds < -300,
597
+ !.
598
+ receipt_runtime_state(_Receipt, FinishedStamp, Context, stale, AgeSeconds) :-
599
+ AgeSeconds is Context.verificationCheckedAtStamp - FinishedStamp,
600
+ AgeSeconds > Context.verificationMaxAgeSeconds,
601
+ !.
602
+ receipt_runtime_state(Receipt, FinishedStamp, Context, State, AgeSeconds) :-
603
+ AgeSeconds is Context.verificationCheckedAtStamp - FinishedStamp,
604
+ inventory_entry_field(Receipt, outcome, RawOutcome),
605
+ normalize_receipt_atom(RawOutcome, Outcome),
606
+ (Outcome == passed -> State = passed ; State = failed).
607
+
608
+ receipt_evidence_dict(TestId, Scope, Receipt, State, AgeSeconds, Evidence) :-
609
+ receipt_string_field(Receipt, receipt_id, ReceiptId),
610
+ receipt_string_field(Receipt, runner, Runner),
611
+ receipt_string_field(Receipt, command, Command),
612
+ receipt_string_field(Receipt, outcome, Outcome),
613
+ receipt_string_field(Receipt, code_snapshot, CodeSnapshot),
614
+ receipt_string_field(Receipt, environment_hash, EnvironmentHash),
615
+ receipt_string_field(Receipt, started_at, StartedAt),
616
+ receipt_string_field(Receipt, finished_at, FinishedAt),
617
+ receipt_string_field(Receipt, artifact_digest, ArtifactDigest),
618
+ Evidence = _{
619
+ testId: TestId,
620
+ state: State,
621
+ scope: Scope,
622
+ receiptId: ReceiptId,
623
+ runner: Runner,
624
+ command: Command,
625
+ outcome: Outcome,
626
+ codeSnapshot: CodeSnapshot,
627
+ environmentHash: EnvironmentHash,
628
+ startedAt: StartedAt,
629
+ finishedAt: FinishedAt,
630
+ artifactDigest: ArtifactDigest,
631
+ ageSeconds: AgeSeconds
632
+ }.
633
+
634
+ receipt_string_field(Receipt, Field, Value) :-
635
+ inventory_entry_field(Receipt, Field, RawValue),
636
+ normalize_receipt_atom(RawValue, Value).
637
+
638
+ normalize_receipt_atom(^^(Value, _Type), Atom) :-
639
+ !,
640
+ normalize_receipt_atom(Value, Atom).
641
+ normalize_receipt_atom(literal(type(_, Value)), Atom) :-
642
+ !,
643
+ normalize_receipt_atom(Value, Atom).
644
+ normalize_receipt_atom(Value, Atom) :-
645
+ string(Value),
646
+ !,
647
+ atom_string(Atom, Value).
648
+ normalize_receipt_atom(Value, Value) :-
649
+ atom(Value),
650
+ !.
651
+ normalize_receipt_atom(Value, Atom) :-
652
+ term_string(Value, String),
653
+ atom_string(Atom, String).
654
+
655
+ test_scope(Props, Scope) :-
656
+ memberchk(verification_scope=RawScope, Props),
657
+ !,
658
+ normalize_atom(RawScope, Scope).
659
+ test_scope(Props, end_to_end) :-
660
+ (memberchk(tags=Tags, Props), member(RawTag, Tags), normalize_atom(RawTag, Tag), downcase_atom(Tag, e2e)
661
+ ; memberchk(source=RawSource, Props), normalize_atom(RawSource, Source), downcase_atom(Source, Lower), sub_atom(Lower, _, _, _, e2e)),
662
+ !.
663
+ test_scope(_, unknown).
664
+
665
+ executable_symbol_stage([], _{status: blocked, symbols: [], missingTests: []}, []).
666
+ executable_symbol_stage(PassingE2eTests, Stage, Symbols) :-
667
+ PassingE2eTests \= [],
668
+ findall(SymbolId,
669
+ (member(TestId, PassingE2eTests),
670
+ kb_relationship(executable_for, SymbolId, TestId),
671
+ kb_entity(SymbolId, symbol, _)),
672
+ Symbols0),
673
+ sort(Symbols0, Symbols),
674
+ include(test_missing_executable_symbol, PassingE2eTests, MissingTests),
675
+ (MissingTests = [] -> Status = passed ; Status = missing),
676
+ Stage = _{status: Status, symbols: Symbols, missingTests: MissingTests}.
677
+
678
+ test_missing_executable_symbol(TestId) :-
679
+ \+ (kb_relationship(executable_for, SymbolId, TestId), kb_entity(SymbolId, symbol, _)).
680
+
681
+ production_symbol_stage(ReqId, PassingE2eTests, Stage, ProductionSymbols) :-
682
+ findall(SymbolId,
683
+ (kb_relationship(implements, SymbolId, ReqId),
684
+ kb_entity(SymbolId, symbol, _),
685
+ \+ kb:executable_test_symbol(SymbolId)),
686
+ Production0),
687
+ sort(Production0, ProductionSymbols),
688
+ include(symbol_not_covered_by_tests(PassingE2eTests), ProductionSymbols, UncoveredSymbols),
689
+ production_stage_status(ProductionSymbols, PassingE2eTests, UncoveredSymbols, Status),
690
+ Stage = _{status: Status, symbols: ProductionSymbols, uncoveredSymbols: UncoveredSymbols}.
691
+
692
+ production_stage_status([], _, _, missing) :- !.
693
+ production_stage_status(_, [], _, blocked) :- !.
694
+ production_stage_status(_, _, [], passed) :- !.
695
+ production_stage_status(_, _, _, missing).
696
+
697
+ symbol_not_covered_by_tests(Tests, SymbolId) :-
698
+ \+ (member(TestId, Tests), kb_relationship(covered_by, SymbolId, TestId)).
699
+
700
+ source_coordinate_stage(ReqProps, ExecutableSymbols, ProductionSymbols, Stage) :-
701
+ append(ExecutableSymbols, ProductionSymbols, Symbols0),
702
+ sort(Symbols0, Symbols),
703
+ include(symbol_missing_coordinates, Symbols, MissingSymbols),
704
+ ( \+ nonempty_source(ReqProps)
705
+ -> Status = missing,
706
+ RequirementSource = missing
707
+ ; RequirementSource = present,
708
+ coordinate_stage_status(Symbols, MissingSymbols, Status)
709
+ ),
710
+ Stage = _{
711
+ status: Status,
712
+ requirementSource: RequirementSource,
713
+ symbols: Symbols,
714
+ missingSymbols: MissingSymbols
715
+ }.
716
+
717
+ coordinate_stage_status([], _, blocked) :- !.
718
+ coordinate_stage_status(_, [], passed) :- !.
719
+ coordinate_stage_status(_, _, missing).
720
+
721
+ symbol_missing_coordinates(SymbolId) :-
722
+ kb_entity(SymbolId, symbol, Props),
723
+ \+ valid_symbol_coordinates(Props).
724
+
725
+ valid_symbol_coordinates(Props) :-
726
+ memberchk(sourceFile=RawSourceFile, Props),
727
+ normalize_atom(RawSourceFile, SourceFile),
728
+ SourceFile \= '',
729
+ memberchk(sourceLine=RawLine, Props),
730
+ memberchk(sourceColumn=RawColumn, Props),
731
+ memberchk(sourceEndLine=RawEndLine, Props),
732
+ memberchk(sourceEndColumn=RawEndColumn, Props),
733
+ normalize_integer(RawLine, Line),
734
+ normalize_integer(RawColumn, Column),
735
+ normalize_integer(RawEndLine, EndLine),
736
+ normalize_integer(RawEndColumn, EndColumn),
737
+ Line >= 1,
738
+ Column >= 0,
739
+ EndLine >= Line,
740
+ EndColumn >= 0.
741
+
742
+ nonempty_source(Props) :-
743
+ memberchk(source=RawSource, Props),
744
+ normalize_atom(RawSource, Source),
745
+ Source \= ''.
746
+
747
+ proof_status(Stages, unresolved) :-
748
+ Stages.contradictions.status == blocked,
749
+ !.
750
+ proof_status(Stages, missing) :-
751
+ stage_dict(Stages, Stage),
752
+ Stage.status == missing,
753
+ !.
754
+ proof_status(Stages, unresolved) :-
755
+ stage_dict(Stages, Stage),
756
+ memberchk(Stage.status, [unresolved, blocked]),
757
+ !.
758
+ proof_status(_, proven).
759
+
760
+ stage_dict(Stages, Stage) :-
761
+ dict_pairs(Stages, _, Pairs),
762
+ member(_-Stage, Pairs).
763
+
764
+ proof_gaps(Stages, Gaps) :-
765
+ findall(Gap, (gap_definition(Gap, _, _, _), proof_gap_present(Gap, Stages)), Gaps).
766
+
767
+ proof_gap_present(missing_semantic_inventory, Stages) :- Stages.semanticInventory.propositionCount =:= 0.
768
+ proof_gap_present(incomplete_semantic_inventory, Stages) :- Stages.semanticInventory.missingCount > 0.
769
+ proof_gap_present(malformed_semantic_inventory, Stages) :- Stages.semanticInventory.malformedCount > 0.
770
+ proof_gap_present(unresolved_semantic_proposition, Stages) :- Stages.semanticInventory.unresolvedCount > 0.
771
+ proof_gap_present(missing_logic_claims, Stages) :- Stages.logicGrounding.manifestClaims == [].
772
+ proof_gap_present(logic_manifest_mismatch, Stages) :-
773
+ (Stages.logicGrounding.missingManifestClaims \= [] ; Stages.logicGrounding.extraManifestClaims \= [] ; Stages.logicGrounding.undeclaredGroundClaims \= []).
774
+ proof_gap_present(missing_logic_grounding, Stages) :- Stages.logicGrounding.missingGroundClaims \= [].
775
+ proof_gap_present(ambiguous_logic_grounding, Stages) :-
776
+ (Stages.logicGrounding.duplicateGroundClaims \= [] ; Stages.logicGrounding.claimTextMismatchClaims \= [] ; Stages.logicGrounding.invalidGroundClaims \= [] ; Stages.logicGrounding.invalidRuleFacts \= []).
777
+ proof_gap_present(blocking_contradiction, Stages) :- Stages.contradictions.status == blocked.
778
+ proof_gap_present(contradiction_check_incomplete, Stages) :- Stages.contradictions.status == unresolved.
779
+ proof_gap_present(missing_scenario, Stages) :- Stages.scenarios.status == missing.
780
+ proof_gap_present(missing_scenario_test, Stages) :- Stages.scenarios.status == passed, Stages.scenarioTests.status == missing.
781
+ proof_gap_present(missing_passing_e2e, Stages) :- Stages.passingE2e.status == missing.
782
+ proof_gap_present(missing_verification_receipt, Stages) :- Stages.passingE2e.missingReceiptTests \= [].
783
+ proof_gap_present(stale_verification_receipt, Stages) :- Stages.passingE2e.staleReceiptTests \= [].
784
+ proof_gap_present(failed_verification_receipt, Stages) :- Stages.passingE2e.failedReceiptTests \= [].
785
+ proof_gap_present(invalid_verification_receipt, Stages) :- Stages.passingE2e.invalidReceiptTests \= [].
786
+ proof_gap_present(verification_snapshot_unavailable, Stages) :- Stages.passingE2e.snapshotUnavailableTests \= [].
787
+ proof_gap_present(missing_executable_test_symbol, Stages) :- Stages.executableSymbols.status == missing.
788
+ proof_gap_present(missing_production_symbol, Stages) :- Stages.productionSymbols.symbols == [].
789
+ proof_gap_present(missing_production_symbol_coverage, Stages) :- Stages.productionSymbols.uncoveredSymbols \= [].
790
+ proof_gap_present(missing_symbol_coordinates, Stages) :- Stages.sourceCoordinates.missingSymbols \= [].
791
+ proof_gap_present(missing_requirement_source, Stages) :- Stages.sourceCoordinates.requirementSource == missing.
792
+
793
+ gap_definition(missing_semantic_inventory, 10, semantic_inventory, "Run kb_semantic_advisor and persist its complete proposition ledger.").
794
+ gap_definition(incomplete_semantic_inventory, 11, semantic_inventory, "Classify every missing assertive span as modeled or explicitly unresolved.").
795
+ gap_definition(malformed_semantic_inventory, 12, semantic_inventory, "Restore claim provenance and valid UTF-8 spans for every proposition.").
796
+ gap_definition(unresolved_semantic_proposition, 20, semantic_inventory, "Resolve each ambiguity or ontology gap before claiming proof.").
797
+ gap_definition(missing_logic_claims, 30, logic_grounding, "Persist the stable claim-key manifest returned by semantic analysis.").
798
+ gap_definition(logic_manifest_mismatch, 31, logic_grounding, "Align the proposition ledger, logic_claims, and linked ground facts one-to-one.").
799
+ gap_definition(missing_logic_grounding, 32, logic_grounding, "Ground every modeled claim with one strict property, predicate, or safe rule fact.").
800
+ gap_definition(ambiguous_logic_grounding, 33, logic_grounding, "Remove duplicate or invalid ground representations and validate linked rules.").
801
+ gap_definition(blocking_contradiction, 40, contradictions, "Supersede or reconcile the conflicting normative requirement.").
802
+ gap_definition(contradiction_check_incomplete, 41, contradictions, "Complete logical grounding before interpreting absence of a conflict as evidence.").
803
+ gap_definition(missing_scenario, 50, scenarios, "Add a specified_by scenario for the requirement.").
804
+ gap_definition(missing_scenario_test, 51, scenario_tests, "Link the scenario to a test with verified_by or validates.").
805
+ gap_definition(missing_passing_e2e, 52, passing_e2e, "Record fresh passing end-to-end evidence on a scenario-backed test.").
806
+ gap_definition(missing_verification_receipt, 53, passing_e2e, "Record a kibi.verification-receipt.v1 result for the scenario-backed E2E test.").
807
+ gap_definition(stale_verification_receipt, 54, passing_e2e, "Re-run the E2E test against the current code snapshot and append its receipt.").
808
+ gap_definition(failed_verification_receipt, 55, passing_e2e, "Repair the failing E2E behavior and append a newer passing receipt for the same snapshot.").
809
+ gap_definition(invalid_verification_receipt, 56, passing_e2e, "Repair malformed, future-dated, mismatched, or otherwise uncheckable receipt evidence.").
810
+ gap_definition(verification_snapshot_unavailable, 57, passing_e2e, "Run coverage through a CLI or MCP runtime that exposes the deterministic workspace snapshot.").
811
+ gap_definition(missing_executable_test_symbol, 60, executable_symbols, "Link executable test code to every qualifying E2E test with executable_for.").
812
+ gap_definition(missing_production_symbol, 70, production_symbols, "Link at least one production symbol to the requirement with implements.").
813
+ gap_definition(missing_production_symbol_coverage, 71, production_symbols, "Link every implementing production symbol to a qualifying E2E test with covered_by.").
814
+ gap_definition(missing_symbol_coordinates, 80, source_coordinates, "Refresh and persist exact coordinates for every proof-bearing symbol.").
815
+ gap_definition(missing_requirement_source, 81, source_coordinates, "Bind the requirement to its current source document.").
816
+
817
+ proof_repairs(Gaps, Repairs) :-
818
+ findall(_{gap: Gap, priority: Priority, stage: Stage, action: Action},
819
+ (member(Gap, Gaps), gap_definition(Gap, Priority, Stage, Action)),
820
+ Repairs).
821
+
822
+ normalize_atom(Value, Atom) :-
823
+ kb:normalize_term_atom(Value, Atom).
824
+
825
+ normalize_atom_list(Value, Atoms) :-
826
+ kb:normalize_term_atom_list(Value, Atoms).
827
+
828
+ normalize_integer(Value, Integer) :-
829
+ ( Value = ^^(Inner, _)
830
+ -> normalize_integer(Inner, Integer)
831
+ ; integer(Value)
832
+ -> Integer = Value
833
+ ; atom(Value)
834
+ -> atom_number(Value, Integer)
835
+ ; string(Value)
836
+ -> number_string(Integer, Value)
837
+ ).