kibi-core 0.10.0 → 0.11.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.11.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).
@@ -87,6 +88,7 @@ entity_property(symbol, sourceEndColumn, integer).
87
88
  % Typed test verification fields - only valid for test entities
88
89
  entity_property(test, verification_scope, atom).
89
90
  entity_property(test, verification_perspective, atom).
91
+ entity_property(test, verification_contract, list_or_json).
90
92
  entity_property(test, verification_receipts, string).
91
93
 
92
94
  % Required properties for all entity types
@@ -113,6 +115,7 @@ optional_property(req, semantic_source_hash).
113
115
  optional_property(req, semantic_inventory).
114
116
  optional_property(test, verification_scope).
115
117
  optional_property(test, verification_perspective).
118
+ optional_property(test, verification_contract).
116
119
  optional_property(test, verification_receipts).
117
120
 
118
121
  % Documentation helpers
@@ -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,6 +119,7 @@ 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_contract).
122
123
  is_test_only_field(verification_receipts).
123
124
 
124
125
  % validate_fact_shape(+Kind, +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
@@ -591,6 +593,15 @@ check_domain_contradictions(Violations) :-
591
593
  check_domain_contradiction_witnesses(Witnesses),
592
594
  maplist(contradiction_witness_violation, Witnesses, Violations).
593
595
 
596
+ %% check_domain_contradictions_and_witnesses(-Violations, -Witnesses)
597
+ % Compute the contradiction witness graph once when a caller needs both the
598
+ % report violations and their exact evidence. The existing one-result
599
+ % predicates remain unchanged; this paired form avoids duplicate work in
600
+ % proof contexts without weakening any contradiction checks.
601
+ check_domain_contradictions_and_witnesses(Violations, Witnesses) :-
602
+ check_domain_contradiction_witnesses(Witnesses),
603
+ maplist(contradiction_witness_violation, Witnesses, Violations).
604
+
594
605
  %% check_domain_contradiction_witnesses(-Witnesses)
595
606
  % Exact, JSON-safe evidence for every strict-property, ground-predicate, and
596
607
  % safe-rule contradiction result. Rule overlap that cannot be proven or
@@ -989,9 +1000,11 @@ predicate_verifiability_kind_label(Kind, Label) :-
989
1000
 
990
1001
  %% check_logic_coverage(-Violations)
991
1002
  % 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.
1003
+ % requirements without logic_claims remain eligible for gradual backfill. A
1004
+ % modeled proposition must be grounded exactly once; ambiguous, ontology-gap,
1005
+ % and missing propositions remain explicit unresolved inventory states and are
1006
+ % not required to have a ground fact (missing is reported by semantic
1007
+ % completeness).
995
1008
  check_logic_coverage(Violations) :-
996
1009
  findall(Violation, logic_coverage_violation(Violation), Violations0),
997
1010
  sort(Violations0, Violations).
@@ -1006,6 +1019,7 @@ logic_coverage_violation(violation(
1006
1019
  kb:current_req(ReqId),
1007
1020
  requirement_logic_claims(ReqId, ClaimKeys),
1008
1021
  member(ClaimKey, ClaimKeys),
1022
+ claim_requires_grounding(ReqId, ClaimKey),
1009
1023
  \+ grounded_requirement_claim(ReqId, ClaimKey),
1010
1024
  format(
1011
1025
  string(Description),
@@ -1014,6 +1028,24 @@ logic_coverage_violation(violation(
1014
1028
  ),
1015
1029
  violation_source(ReqId, req, Source).
1016
1030
 
1031
+ logic_coverage_violation(violation(
1032
+ 'logic-coverage',
1033
+ ReqId,
1034
+ Description,
1035
+ "Keep ontology gaps, ambiguities, and missing interpretations unresolved; only modeled propositions may have logical ground facts",
1036
+ Source
1037
+ )) :-
1038
+ kb:current_req(ReqId),
1039
+ requirement_inventory_status(ReqId, ClaimKey, Status),
1040
+ memberchk(Status, [ambiguous, ontology_gap, missing]),
1041
+ grounded_requirement_claim(ReqId, ClaimKey),
1042
+ format(
1043
+ string(Description),
1044
+ "Requirement grounds unresolved ~w proposition ~w",
1045
+ [Status, ClaimKey]
1046
+ ),
1047
+ violation_source(ReqId, req, Source).
1048
+
1017
1049
  logic_coverage_violation(violation(
1018
1050
  'logic-coverage',
1019
1051
  ReqId,
@@ -1076,6 +1108,28 @@ requirement_logic_claims(ReqId, ClaimKeys) :-
1076
1108
  memberchk(logic_claims=RawClaimKeys, Props),
1077
1109
  kb:normalize_term_atom_list(RawClaimKeys, ClaimKeys).
1078
1110
 
1111
+ claim_requires_grounding(ReqId, ClaimKey) :-
1112
+ requirement_inventory_status(ReqId, ClaimKey, modeled),
1113
+ !.
1114
+ claim_requires_grounding(ReqId, ClaimKey) :-
1115
+ \+ requirement_inventory_status(ReqId, ClaimKey, _).
1116
+
1117
+ requirement_inventory_status(ReqId, ClaimKey, Status) :-
1118
+ kb_entity(ReqId, req, Props),
1119
+ memberchk(semantic_inventory=RawInventory, Props),
1120
+ inventory_entries(RawInventory, Entries),
1121
+ member(Entry, Entries),
1122
+ checks_inventory_entry_field(Entry, claim_key, RawClaimKey),
1123
+ kb:normalize_term_atom(RawClaimKey, ClaimKey),
1124
+ inventory_entry_status(Entry, Status).
1125
+
1126
+ checks_inventory_entry_field(Entry, Key, Value) :-
1127
+ is_dict(Entry),
1128
+ get_dict(Key, Entry, Value).
1129
+ checks_inventory_entry_field(Entry, Key, Value) :-
1130
+ is_list(Entry),
1131
+ memberchk(Key=Value, Entry).
1132
+
1079
1133
  grounded_requirement_claim(ReqId, ClaimKey) :-
1080
1134
  grounded_requirement_claim_fact(ReqId, ClaimKey, _FactId).
1081
1135
 
@@ -1222,13 +1276,15 @@ rule_verifiability_description(Props, FactId, Description) :-
1222
1276
  rule_verifiability_description(Props, FactId, Description) :-
1223
1277
  memberchk(fact_kind=RawKind, Props),
1224
1278
  normalize_term_atom(RawKind, rule),
1225
- memberchk(rule_schema_id=SchemaId, Props),
1279
+ memberchk(rule_schema_id=RawSchemaId, Props),
1280
+ normalize_term_atom(RawSchemaId, SchemaId),
1226
1281
  \+ kb_entity(SchemaId, fact, _),
1227
1282
  format(string(Description), "Rule fact ~w references missing rule schema ~w", [FactId, SchemaId]).
1228
1283
  rule_verifiability_description(Props, FactId, Description) :-
1229
1284
  memberchk(fact_kind=RawKind, Props),
1230
1285
  normalize_term_atom(RawKind, rule),
1231
- memberchk(rule_schema_id=SchemaId, Props),
1286
+ memberchk(rule_schema_id=RawSchemaId, Props),
1287
+ normalize_term_atom(RawSchemaId, SchemaId),
1232
1288
  kb_entity(SchemaId, fact, SchemaProps),
1233
1289
  \+ valid_rule_schema_props(SchemaProps),
1234
1290
  format(string(Description), "Rule fact ~w references ~w, which is not a rule_schema fact", [FactId, SchemaId]).
@@ -1286,6 +1342,10 @@ inventory_entries(Raw, Entries) :-
1286
1342
  catch(atom_json_dict(Raw, JsonEntries, [value_string_as(string)]), _, fail),
1287
1343
  is_list(JsonEntries)
1288
1344
  -> Entries = JsonEntries
1345
+ ; (atom(Raw) ; string(Raw)),
1346
+ catch(term_string(PrologEntries, Raw), _, fail),
1347
+ is_list(PrologEntries)
1348
+ -> Entries = PrologEntries
1289
1349
  ; Entries = []
1290
1350
  ).
1291
1351
 
@@ -1341,6 +1401,64 @@ check_all_json(JsonString) :-
1341
1401
  JsonString
1342
1402
  ).
1343
1403
 
1404
+ %% check_selected_json(+Rules, -JsonString)
1405
+ % Run only the named aggregated checks. This is intentionally separate from
1406
+ % check_all/1 so focused diagnostics cannot evaluate unrelated rule queries.
1407
+ check_selected_json(Rules, JsonString) :-
1408
+ check_selected(Rules, ViolationsDict),
1409
+ violations_dict_to_json(ViolationsDict, JsonDict),
1410
+ with_output_to_string(
1411
+ json_write_dict(current_output, JsonDict, [width(0)]),
1412
+ JsonString
1413
+ ).
1414
+
1415
+ check_selected(Rules, _{
1416
+ must_priority_coverage: MustPriority,
1417
+ symbol_coverage: SymbolCoverage,
1418
+ symbol_traceability: SymbolTraceability,
1419
+ no_dangling_refs: DanglingRefs,
1420
+ no_cycles: Cycles,
1421
+ required_fields: RequiredFields,
1422
+ deprecated_adr_no_successor: DeprecatedADRs,
1423
+ domain_contradictions: Contradictions,
1424
+ strict_fact_shape: StrictFactShape,
1425
+ strict_req_fact_pairing: StrictReqFactPairing,
1426
+ strict_readiness: StrictReadiness,
1427
+ predicate_verifiability: PredicateVerifiability,
1428
+ logic_coverage: LogicCoverage,
1429
+ rule_safety: RuleSafety,
1430
+ rule_verifiability: RuleVerifiability,
1431
+ semantic_completeness: SemanticCompleteness
1432
+ }) :-
1433
+ selected_rule(Rules, 'must-priority-coverage', check_must_priority_coverage, MustPriority),
1434
+ selected_rule(Rules, 'symbol-coverage', check_symbol_coverage, SymbolCoverage),
1435
+ selected_rule_with_options(Rules, 'symbol-traceability', check_symbol_traceability, SymbolTraceability),
1436
+ selected_rule(Rules, 'no-dangling-refs', check_no_dangling_refs, DanglingRefs),
1437
+ selected_rule(Rules, 'no-cycles', check_no_cycles, Cycles),
1438
+ selected_rule(Rules, 'required-fields', check_required_fields, RequiredFields),
1439
+ selected_rule(Rules, 'deprecated-adr-no-successor', check_deprecated_adrs, DeprecatedADRs),
1440
+ selected_rule(Rules, 'domain-contradictions', check_domain_contradictions, Contradictions),
1441
+ selected_rule(Rules, 'strict-fact-shape', check_strict_fact_shape, StrictFactShape),
1442
+ selected_rule(Rules, 'strict-req-fact-pairing', check_strict_req_fact_pairing, StrictReqFactPairing),
1443
+ selected_rule(Rules, 'strict-readiness', check_strict_readiness, StrictReadiness),
1444
+ selected_rule(Rules, 'predicate-verifiability', check_predicate_verifiability, PredicateVerifiability),
1445
+ selected_rule(Rules, 'logic-coverage', check_logic_coverage, LogicCoverage),
1446
+ selected_rule(Rules, 'rule-safety', check_rule_safety, RuleSafety),
1447
+ selected_rule(Rules, 'rule-verifiability', check_rule_verifiability, RuleVerifiability),
1448
+ selected_rule(Rules, 'semantic-completeness', check_semantic_completeness, SemanticCompleteness).
1449
+
1450
+ selected_rule(Rules, Name, Goal, Violations) :-
1451
+ ( memberchk(Name, Rules)
1452
+ -> call(Goal, Violations)
1453
+ ; Violations = []
1454
+ ).
1455
+
1456
+ selected_rule_with_options(Rules, Name, Goal, Violations) :-
1457
+ ( memberchk(Name, Rules)
1458
+ -> call(Goal, false, Violations)
1459
+ ; Violations = []
1460
+ ).
1461
+
1344
1462
  %% check_all_json_with_options(-JsonString, +RequireAdr)
1345
1463
  % Returns all violations as JSON string with options.
1346
1464
  % RequireAdr: if true, symbol-traceability also requires ADR constraints.
package/src/discovery.pl CHANGED
@@ -143,7 +143,7 @@ coverage_rows(type, _Tags, _IncludePassing, _IncludeTransitive, _VerificationSna
143
143
  requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row) :-
144
144
  kb_entity(Id, req, Props),
145
145
  matches_tags(Tags, Props),
146
- entity_title_status_source(Props, Title, Status, _Source),
146
+ entity_title_status_source(Props, Title, Status, Source),
147
147
  entity_priority(Props, Priority),
148
148
  count_distinct_targets(specified_by, Id, source, ScenarioCount),
149
149
  requirement_test_count(Id, TestCount),
@@ -156,6 +156,7 @@ requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row) :-
156
156
  type: req,
157
157
  title: Title,
158
158
  status: Status,
159
+ source: Source,
159
160
  priority: Priority,
160
161
  scenarioCount: ScenarioCount,
161
162
  testCount: TestCount,
package/src/kb.pl CHANGED
@@ -1839,6 +1839,9 @@ literal_to_value(semantic_inventory, Literal, Value) :-
1839
1839
  literal_to_value(verification_receipts, Literal, Value) :-
1840
1840
  !,
1841
1841
  structured_literal_value(Literal, Value).
1842
+ literal_to_value(verification_contract, Literal, Value) :-
1843
+ !,
1844
+ structured_literal_value(Literal, Value).
1842
1845
  literal_to_value(_Key, Literal, Value) :-
1843
1846
  ( % Handle ^^/2 functor (RDF typed literal shorthand)
1844
1847
  Literal = ^^(StrVal, 'http://www.w3.org/2001/XMLSchema#string')
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).
@@ -8,12 +8,12 @@
8
8
  ]).
9
9
 
10
10
  :- use_module(library(http/json)).
11
+ :- use_module(library(crypto)).
11
12
  :- use_module(library(date)).
12
13
  :- use_module(library(pcre)).
13
14
  :- use_module('kb.pl').
14
15
  :- use_module('checks.pl', [
15
- check_domain_contradictions/1,
16
- check_domain_contradiction_witnesses/1,
16
+ check_domain_contradictions_and_witnesses/2,
17
17
  check_rule_safety/1,
18
18
  check_rule_verifiability/1
19
19
  ]).
@@ -25,8 +25,7 @@ requirement_proof_context(Context) :-
25
25
  requirement_proof_context(unknown, '1970-01-01T00:00:00Z', 604800, Context).
26
26
 
27
27
  requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, Context) :-
28
- check_domain_contradictions(Contradictions),
29
- check_domain_contradiction_witnesses(ContradictionWitnesses),
28
+ check_domain_contradictions_and_witnesses(Contradictions, ContradictionWitnesses),
30
29
  check_rule_safety(UnsafeRules),
31
30
  check_rule_verifiability(UnverifiableRules),
32
31
  normalize_atom(VerificationSnapshot, Snapshot),
@@ -52,6 +51,7 @@ requirement_proof(ReqId, _ReqProps, _Context, Proof) :-
52
51
  proofVersion: Version,
53
52
  proofStatus: not_applicable,
54
53
  proofGaps: [],
54
+ proofAdvisories: [],
55
55
  proofRepairs: [],
56
56
  proofStages: _{applicability: _{status: not_applicable}}
57
57
  }.
@@ -76,7 +76,7 @@ requirement_proof(ReqId, ReqProps, Context, Proof) :-
76
76
  productionSymbols: ProductionStage,
77
77
  sourceCoordinates: CoordinateStage
78
78
  },
79
- proof_gaps(Stages, Gaps),
79
+ proof_issues(Stages, Gaps, Advisories),
80
80
  proof_repairs(Gaps, Repairs),
81
81
  proof_status(Stages, Status),
82
82
  proof_version(Version),
@@ -84,6 +84,7 @@ requirement_proof(ReqId, ReqProps, Context, Proof) :-
84
84
  proofVersion: Version,
85
85
  proofStatus: Status,
86
86
  proofGaps: Gaps,
87
+ proofAdvisories: Advisories,
87
88
  proofRepairs: Repairs,
88
89
  proofStages: Stages
89
90
  }.
@@ -224,6 +225,7 @@ logic_grounding_stage(ReqId, Props, Inventory, Context, Stage) :-
224
225
  InvalidRuleFactIds,
225
226
  Status
226
227
  ),
228
+ ground_source_refs(Evidence, GroundSources),
227
229
  Stage = _{
228
230
  status: Status,
229
231
  manifestClaims: ManifestKeys,
@@ -238,7 +240,8 @@ logic_grounding_stage(ReqId, Props, Inventory, Context, Stage) :-
238
240
  missingManifestClaims: MissingManifestKeys,
239
241
  extraManifestClaims: ExtraManifestKeys,
240
242
  undeclaredGroundClaims: UndeclaredGroundKeys,
241
- invalidRuleFacts: InvalidRuleFactIds
243
+ invalidRuleFacts: InvalidRuleFactIds,
244
+ sources: GroundSources
242
245
  }.
243
246
 
244
247
  requirement_claim_keys(Props, true, Keys) :-
@@ -436,7 +439,8 @@ scenario_stage(ReqId, Stage, ScenarioIds) :-
436
439
  ScenarioIds0),
437
440
  sort(ScenarioIds0, ScenarioIds),
438
441
  (ScenarioIds = [] -> Status = missing ; Status = passed),
439
- Stage = _{status: Status, scenarios: ScenarioIds}.
442
+ maplist(entity_source_ref, ScenarioIds, Sources),
443
+ Stage = _{status: Status, scenarios: ScenarioIds, sources: Sources}.
440
444
 
441
445
  scenario_test_stage(ScenarioIds, Stage, ScenarioTests) :-
442
446
  findall(TestId,
@@ -444,7 +448,8 @@ scenario_test_stage(ScenarioIds, Stage, ScenarioTests) :-
444
448
  ScenarioTests0),
445
449
  sort(ScenarioTests0, ScenarioTests),
446
450
  (ScenarioTests = [] -> Status = missing ; Status = passed),
447
- Stage = _{status: Status, tests: ScenarioTests}.
451
+ maplist(entity_source_ref, ScenarioTests, Sources),
452
+ Stage = _{status: Status, tests: ScenarioTests, sources: Sources}.
448
453
 
449
454
  scenario_test(ScenarioId, TestId) :-
450
455
  kb_relationship(verified_by, ScenarioId, TestId),
@@ -460,9 +465,10 @@ passing_e2e_stage(ScenarioTests, Context, Stage, PassingE2eTests) :-
460
465
  evidence_tests_with_state(Evidence, stale, StaleReceiptTests),
461
466
  evidence_tests_with_state(Evidence, failed, FailedReceiptTests),
462
467
  evidence_tests_with_state(Evidence, invalid, InvalidReceiptTests),
468
+ evidence_tests_with_state(Evidence, contract_mismatch, ContractMismatchReceiptTests),
463
469
  evidence_tests_with_state(Evidence, snapshot_unavailable, SnapshotUnavailableTests),
464
470
  evidence_tests_with_state(Evidence, not_end_to_end, NonEndToEndTests),
465
- passing_e2e_status(PassingE2eTests, InvalidReceiptTests, SnapshotUnavailableTests, Status),
471
+ passing_e2e_status(PassingE2eTests, InvalidReceiptTests, ContractMismatchReceiptTests, SnapshotUnavailableTests, Status),
466
472
  Stage = _{
467
473
  status: Status,
468
474
  tests: PassingE2eTests,
@@ -471,6 +477,7 @@ passing_e2e_stage(ScenarioTests, Context, Stage, PassingE2eTests) :-
471
477
  staleReceiptTests: StaleReceiptTests,
472
478
  failedReceiptTests: FailedReceiptTests,
473
479
  invalidReceiptTests: InvalidReceiptTests,
480
+ contractMismatchReceiptTests: ContractMismatchReceiptTests,
474
481
  snapshotUnavailableTests: SnapshotUnavailableTests,
475
482
  nonEndToEndTests: NonEndToEndTests,
476
483
  currentCodeSnapshot: Context.verificationSnapshot,
@@ -478,10 +485,11 @@ passing_e2e_stage(ScenarioTests, Context, Stage, PassingE2eTests) :-
478
485
  maxAgeSeconds: Context.verificationMaxAgeSeconds
479
486
  }.
480
487
 
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).
488
+ passing_e2e_status(Passing, _, _, _, passed) :- Passing \= [], !.
489
+ passing_e2e_status([], Invalid, _, _, unresolved) :- Invalid \= [], !.
490
+ passing_e2e_status([], _, ContractMismatch, _, unresolved) :- ContractMismatch \= [], !.
491
+ passing_e2e_status([], _, _, SnapshotUnavailable, unresolved) :- SnapshotUnavailable \= [], !.
492
+ passing_e2e_status([], _, _, _, missing).
485
493
 
486
494
  evidence_tests_with_state(Evidence, State, TestIds) :-
487
495
  findall(TestId,
@@ -497,26 +505,33 @@ test_receipt_evidence(Context, TestId, Evidence) :-
497
505
  ; Context.verificationSnapshot == unknown
498
506
  -> Evidence = _{testId: TestId, state: snapshot_unavailable, scope: Scope}
499
507
  ; verification_receipt_entries(Props, Receipts, ReceiptPropertyPresent),
500
- receipt_evidence_state(TestId, Scope, Receipts, ReceiptPropertyPresent, Context, Evidence)
508
+ receipt_evidence_state(TestId, Scope, Props, Receipts, ReceiptPropertyPresent, Context, Evidence)
501
509
  ).
502
510
 
503
- receipt_evidence_state(TestId, Scope, [], false, _Context,
511
+ receipt_evidence_state(TestId, Scope, _Props, [], false, _Context,
504
512
  _{testId: TestId, state: missing, scope: Scope, receiptCount: 0}) :- !.
505
- receipt_evidence_state(TestId, Scope, Receipts, _Present, _Context,
513
+ receipt_evidence_state(TestId, Scope, Props, Receipts, _Present, _Context,
506
514
  _{testId: TestId, state: invalid, scope: Scope, receiptCount: Count}) :-
507
515
  length(Receipts, Count),
508
- (Receipts == []
509
- ; member(Receipt, Receipts), \+ valid_receipt_shape(TestId, Scope, Receipt)
516
+ verification_contract_binding(Props, ContractBinding),
517
+ (ContractBinding == invalid
518
+ ; Receipts == []
519
+ ; member(Receipt, Receipts), \+ valid_receipt_shape(TestId, Receipt)
510
520
  ; \+ chronological_receipt_history(Receipts)),
511
521
  !.
512
- receipt_evidence_state(TestId, Scope, Receipts, _Present, Context, Evidence) :-
513
- include(receipt_for_snapshot(Context.verificationSnapshot), Receipts, CurrentReceipts),
514
- ( CurrentReceipts == []
522
+ receipt_evidence_state(TestId, Scope, Props, Receipts, _Present, Context, Evidence) :-
523
+ include(receipt_for_snapshot(Context.verificationSnapshot), Receipts, SnapshotReceipts),
524
+ ( SnapshotReceipts == []
515
525
  -> length(Receipts, Count),
516
526
  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)
527
+ ; verification_contract_binding(Props, ContractBinding),
528
+ include(receipt_matches_current_binding(Scope, ContractBinding), SnapshotReceipts, CurrentReceipts),
529
+ ( CurrentReceipts == []
530
+ -> receipt_contract_mismatch_evidence(TestId, Scope, SnapshotReceipts, ContractBinding, Evidence)
531
+ ; latest_receipt(CurrentReceipts, Latest, FinishedStamp),
532
+ receipt_runtime_state(Latest, FinishedStamp, Context, State, AgeSeconds),
533
+ receipt_evidence_dict(TestId, Scope, Latest, State, AgeSeconds, Evidence)
534
+ )
520
535
  ).
521
536
 
522
537
  verification_receipt_entries(Props, Receipts, Present) :-
@@ -527,9 +542,10 @@ verification_receipt_entries(Props, Receipts, Present) :-
527
542
  Receipts = []
528
543
  ).
529
544
 
530
- valid_receipt_shape(TestId, Scope, Receipt) :-
545
+ valid_receipt_shape(TestId, Receipt) :-
531
546
  inventory_entry_field(Receipt, version, RawVersion),
532
- normalize_receipt_atom(RawVersion, 'kibi.verification-receipt.v1'),
547
+ normalize_receipt_atom(RawVersion, Version),
548
+ memberchk(Version, ['kibi.verification-receipt.v1', 'kibi.verification-receipt.v2']),
533
549
  inventory_entry_field(Receipt, receipt_id, RawReceiptId),
534
550
  normalize_receipt_atom(RawReceiptId, ReceiptId), valid_receipt_id(ReceiptId),
535
551
  inventory_entry_field(Receipt, test_id, RawTestId),
@@ -539,10 +555,11 @@ valid_receipt_shape(TestId, Scope, Receipt) :-
539
555
  inventory_entry_field(Receipt, command, RawCommand),
540
556
  normalize_receipt_atom(RawCommand, Command), Command \= '',
541
557
  inventory_entry_field(Receipt, scope, RawScope),
542
- normalize_receipt_atom(RawScope, Scope),
558
+ normalize_receipt_atom(RawScope, ReceiptScope),
559
+ memberchk(ReceiptScope, [unit, integration, end_to_end]),
543
560
  inventory_entry_field(Receipt, outcome, RawOutcome),
544
561
  normalize_receipt_atom(RawOutcome, Outcome),
545
- memberchk(Outcome, [passed, failed, errored, cancelled, skipped]),
562
+ memberchk(Outcome, [passed, failed, errored, cancelled, skipped, timed_out, interrupted]),
546
563
  inventory_entry_field(Receipt, code_snapshot, RawSnapshot),
547
564
  normalize_receipt_atom(RawSnapshot, Snapshot), valid_sha256(Snapshot),
548
565
  inventory_entry_field(Receipt, environment_hash, RawEnvironmentHash),
@@ -551,7 +568,53 @@ valid_receipt_shape(TestId, Scope, Receipt) :-
551
568
  normalize_receipt_atom(RawArtifactDigest, ArtifactDigest), valid_sha256(ArtifactDigest),
552
569
  receipt_timestamp(Receipt, started_at, StartedStamp),
553
570
  receipt_timestamp(Receipt, finished_at, FinishedStamp),
554
- FinishedStamp >= StartedStamp.
571
+ FinishedStamp >= StartedStamp,
572
+ valid_receipt_version_fields(Version, Receipt).
573
+
574
+ valid_receipt_version_fields('kibi.verification-receipt.v1', _Receipt).
575
+ valid_receipt_version_fields('kibi.verification-receipt.v2', Receipt) :-
576
+ inventory_entry_field(Receipt, command_argv, RawCommandArgv),
577
+ is_list(RawCommandArgv),
578
+ RawCommandArgv \= [],
579
+ maplist(nonempty_receipt_atom, RawCommandArgv),
580
+ inventory_entry_field(Receipt, contract_hash, RawContractHash),
581
+ normalize_receipt_atom(RawContractHash, ContractHash),
582
+ valid_sha256(ContractHash),
583
+ inventory_entry_field(Receipt, case_results, RawCases),
584
+ is_list(RawCases),
585
+ RawCases \= [],
586
+ maplist(valid_receipt_case, RawCases),
587
+ findall(Key,
588
+ (member(Case, RawCases), receipt_case_key(Case, Key)), Keys),
589
+ sort(Keys, UniqueKeys),
590
+ length(Keys, KeyCount),
591
+ length(UniqueKeys, KeyCount).
592
+
593
+ nonempty_receipt_atom(Value) :-
594
+ normalize_receipt_atom(Value, Atom),
595
+ Atom \= ''.
596
+
597
+ valid_receipt_case(Case) :-
598
+ inventory_entry_field(Case, symbol_id, RawSymbolId),
599
+ nonempty_receipt_atom(RawSymbolId),
600
+ inventory_entry_field(Case, project, RawProject),
601
+ nonempty_receipt_atom(RawProject),
602
+ inventory_entry_field(Case, outcome, RawOutcome),
603
+ normalize_receipt_atom(RawOutcome, Outcome),
604
+ memberchk(Outcome, [passed, failed, timed_out, skipped, interrupted]),
605
+ inventory_entry_field(Case, retries, Retries),
606
+ normalize_integer(Retries, RetryCount),
607
+ RetryCount >= 0,
608
+ inventory_entry_field(Case, duration_ms, Duration),
609
+ normalize_integer(Duration, DurationMs),
610
+ DurationMs >= 0.
611
+
612
+ receipt_case_key(Case, Key) :-
613
+ inventory_entry_field(Case, project, Project),
614
+ inventory_entry_field(Case, symbol_id, SymbolId),
615
+ normalize_receipt_atom(Project, ProjectAtom),
616
+ normalize_receipt_atom(SymbolId, SymbolAtom),
617
+ atomic_list_concat([ProjectAtom, SymbolAtom], '/', Key).
555
618
 
556
619
  valid_sha256(Value) :-
557
620
  atom_length(Value, 64),
@@ -584,6 +647,82 @@ receipt_for_snapshot(Snapshot, Receipt) :-
584
647
  inventory_entry_field(Receipt, code_snapshot, RawSnapshot),
585
648
  normalize_receipt_atom(RawSnapshot, Snapshot).
586
649
 
650
+ receipt_matches_current_binding(Scope, ContractBinding, Receipt) :-
651
+ inventory_entry_field(Receipt, scope, RawScope),
652
+ normalize_receipt_atom(RawScope, Scope),
653
+ receipt_matches_current_contract(ContractBinding, Receipt).
654
+
655
+ receipt_matches_current_contract(none, _Receipt).
656
+ receipt_matches_current_contract(hash(ExpectedHash), Receipt) :-
657
+ inventory_entry_field(Receipt, version, RawVersion),
658
+ normalize_receipt_atom(RawVersion, 'kibi.verification-receipt.v2'),
659
+ inventory_entry_field(Receipt, contract_hash, RawContractHash),
660
+ normalize_receipt_atom(RawContractHash, ExpectedHash).
661
+
662
+ receipt_contract_mismatch_evidence(TestId, Scope, Receipts, ContractBinding, Evidence) :-
663
+ length(Receipts, Count),
664
+ findall(Hash,
665
+ (member(Receipt, Receipts),
666
+ inventory_entry_field(Receipt, contract_hash, RawHash),
667
+ normalize_receipt_atom(RawHash, Hash)),
668
+ Hashes0),
669
+ sort(Hashes0, Hashes),
670
+ current_contract_hash_value(ContractBinding, CurrentContractHash),
671
+ Evidence = _{
672
+ testId: TestId,
673
+ state: contract_mismatch,
674
+ scope: Scope,
675
+ receiptCount: Count,
676
+ currentContractHash: CurrentContractHash,
677
+ receiptContractHashes: Hashes
678
+ }.
679
+
680
+ current_contract_hash_value(hash(Hash), Hash).
681
+ current_contract_hash_value(none, none).
682
+
683
+ verification_contract_binding(Props, Binding) :-
684
+ ( memberchk(verification_contract=RawContract, Props)
685
+ -> (verification_contract_hash(RawContract, Hash) -> Binding = hash(Hash) ; Binding = invalid)
686
+ ; Binding = none
687
+ ).
688
+
689
+ verification_contract_hash(RawContract, Hash) :-
690
+ verification_contract_dict(RawContract, Contract),
691
+ canonical_json_value(Contract, Canonical),
692
+ crypto_data_hash(Canonical, Hash, [algorithm(sha256), encoding(utf8)]).
693
+
694
+ verification_contract_dict(^^(Value, _), Contract) :- !,
695
+ verification_contract_dict(Value, Contract).
696
+ verification_contract_dict(literal(type(_, Value)), Contract) :- !,
697
+ verification_contract_dict(Value, Contract).
698
+ verification_contract_dict(literal(Value), Contract) :- !,
699
+ verification_contract_dict(Value, Contract).
700
+ verification_contract_dict(Contract, Contract) :-
701
+ is_dict(Contract), !.
702
+ verification_contract_dict(Raw, Contract) :-
703
+ (atom(Raw) ; string(Raw)),
704
+ catch(atom_json_dict(Raw, Contract, [value_string_as(string)]), _, fail),
705
+ is_dict(Contract).
706
+
707
+ canonical_json_value(Value, Json) :-
708
+ is_dict(Value), !,
709
+ dict_pairs(Value, _, Pairs),
710
+ maplist(canonical_json_pair, Pairs, Parts),
711
+ atomics_to_string(Parts, ",", Body),
712
+ format(string(Json), "{~s}", [Body]).
713
+ canonical_json_value(Value, Json) :-
714
+ is_list(Value), !,
715
+ maplist(canonical_json_value, Value, Parts),
716
+ atomics_to_string(Parts, ",", Body),
717
+ format(string(Json), "[~s]", [Body]).
718
+ canonical_json_value(Value, Json) :-
719
+ with_output_to(string(Json), json_write(current_output, Value, [width(0)])).
720
+
721
+ canonical_json_pair(Key-Value, Json) :-
722
+ with_output_to(string(KeyJson), json_write(current_output, Key, [width(0)])),
723
+ canonical_json_value(Value, ValueJson),
724
+ format(string(Json), "~s:~s", [KeyJson, ValueJson]).
725
+
587
726
  latest_receipt(Receipts, Latest, FinishedStamp) :-
588
727
  findall(Stamp-Receipt,
589
728
  (member(Receipt, Receipts), receipt_timestamp(Receipt, finished_at, Stamp)),
@@ -662,7 +801,7 @@ test_scope(Props, end_to_end) :-
662
801
  !.
663
802
  test_scope(_, unknown).
664
803
 
665
- executable_symbol_stage([], _{status: blocked, symbols: [], missingTests: []}, []).
804
+ executable_symbol_stage([], _{status: blocked, symbols: [], missingTests: [], coordinates: []}, []).
666
805
  executable_symbol_stage(PassingE2eTests, Stage, Symbols) :-
667
806
  PassingE2eTests \= [],
668
807
  findall(SymbolId,
@@ -673,7 +812,8 @@ executable_symbol_stage(PassingE2eTests, Stage, Symbols) :-
673
812
  sort(Symbols0, Symbols),
674
813
  include(test_missing_executable_symbol, PassingE2eTests, MissingTests),
675
814
  (MissingTests = [] -> Status = passed ; Status = missing),
676
- Stage = _{status: Status, symbols: Symbols, missingTests: MissingTests}.
815
+ maplist(symbol_coordinate_ref, Symbols, Coordinates),
816
+ Stage = _{status: Status, symbols: Symbols, missingTests: MissingTests, coordinates: Coordinates}.
677
817
 
678
818
  test_missing_executable_symbol(TestId) :-
679
819
  \+ (kb_relationship(executable_for, SymbolId, TestId), kb_entity(SymbolId, symbol, _)).
@@ -683,16 +823,36 @@ production_symbol_stage(ReqId, PassingE2eTests, Stage, ProductionSymbols) :-
683
823
  (kb_relationship(implements, SymbolId, ReqId),
684
824
  kb_entity(SymbolId, symbol, _),
685
825
  \+ kb:executable_test_symbol(SymbolId)),
686
- Production0),
687
- sort(Production0, ProductionSymbols),
826
+ Implementing0),
827
+ sort(Implementing0, ImplementingSymbols),
828
+ include(type_shape_symbol_with_structural_contract, ImplementingSymbols, StructuralSymbols),
829
+ exclude(type_shape_symbol_with_structural_contract, ImplementingSymbols, ProductionSymbols),
688
830
  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}.
831
+ production_stage_status(ProductionSymbols, StructuralSymbols, PassingE2eTests, UncoveredSymbols, Status),
832
+ maplist(symbol_coordinate_ref, ProductionSymbols, Coordinates),
833
+ Stage = _{
834
+ status: Status,
835
+ symbols: ProductionSymbols,
836
+ structuralSymbols: StructuralSymbols,
837
+ uncoveredSymbols: UncoveredSymbols,
838
+ coordinates: Coordinates
839
+ }.
691
840
 
692
- production_stage_status([], _, _, missing) :- !.
693
- production_stage_status(_, [], _, blocked) :- !.
694
- production_stage_status(_, _, [], passed) :- !.
695
- production_stage_status(_, _, _, missing).
841
+ type_shape_symbol_with_structural_contract(SymbolId) :-
842
+ kb_entity(SymbolId, symbol, SymbolProps),
843
+ memberchk(symbol_role=RawRole, SymbolProps),
844
+ normalize_atom(RawRole, 'type-shape'),
845
+ kb_relationship(covered_by, SymbolId, TestId),
846
+ kb_entity(TestId, test, TestProps),
847
+ test_scope(TestProps, unit),
848
+ memberchk(status=RawStatus, TestProps),
849
+ normalize_atom(RawStatus, Status),
850
+ memberchk(Status, [active, passing]).
851
+
852
+ production_stage_status([], [], _, _, missing) :- !.
853
+ production_stage_status(_, _, [], _, blocked) :- !.
854
+ production_stage_status(_, _, _, [], passed) :- !.
855
+ production_stage_status(_, _, _, _, missing).
696
856
 
697
857
  symbol_not_covered_by_tests(Tests, SymbolId) :-
698
858
  \+ (member(TestId, Tests), kb_relationship(covered_by, SymbolId, TestId)).
@@ -707,11 +867,15 @@ source_coordinate_stage(ReqProps, ExecutableSymbols, ProductionSymbols, Stage) :
707
867
  ; RequirementSource = present,
708
868
  coordinate_stage_status(Symbols, MissingSymbols, Status)
709
869
  ),
870
+ requirement_source_path(ReqProps, RequirementPath),
871
+ maplist(symbol_coordinate_ref, Symbols, Coordinates),
710
872
  Stage = _{
711
873
  status: Status,
712
874
  requirementSource: RequirementSource,
875
+ requirementPath: RequirementPath,
713
876
  symbols: Symbols,
714
- missingSymbols: MissingSymbols
877
+ missingSymbols: MissingSymbols,
878
+ coordinates: Coordinates
715
879
  }.
716
880
 
717
881
  coordinate_stage_status([], _, blocked) :- !.
@@ -724,7 +888,7 @@ symbol_missing_coordinates(SymbolId) :-
724
888
 
725
889
  valid_symbol_coordinates(Props) :-
726
890
  memberchk(sourceFile=RawSourceFile, Props),
727
- normalize_atom(RawSourceFile, SourceFile),
891
+ source_path_atom(RawSourceFile, SourceFile),
728
892
  SourceFile \= '',
729
893
  memberchk(sourceLine=RawLine, Props),
730
894
  memberchk(sourceColumn=RawColumn, Props),
@@ -739,10 +903,66 @@ valid_symbol_coordinates(Props) :-
739
903
  EndLine >= Line,
740
904
  EndColumn >= 0.
741
905
 
742
- nonempty_source(Props) :-
906
+ % Prefer sourceFile, then source, without URI-basename stripping. Paths must
907
+ % remain inspectable coordinates for report linking.
908
+ proof_source_property(Props, RawSource) :-
909
+ memberchk(sourceFile=RawSource, Props),
910
+ source_path_atom(RawSource, _),
911
+ !.
912
+ proof_source_property(Props, RawSource) :-
743
913
  memberchk(source=RawSource, Props),
744
- normalize_atom(RawSource, Source),
745
- Source \= ''.
914
+ source_path_atom(RawSource, _).
915
+
916
+ nonempty_source(Props) :-
917
+ proof_source_property(Props, _).
918
+
919
+ requirement_source_path(Props, Path) :-
920
+ proof_source_property(Props, RawSource),
921
+ source_path_atom(RawSource, Path),
922
+ !.
923
+ requirement_source_path(_, '').
924
+
925
+ entity_source_path(Id, Path) :-
926
+ kb_entity(Id, _, Props),
927
+ requirement_source_path(Props, Path).
928
+
929
+ entity_source_ref(Id, _{id: Id, path: Path}) :-
930
+ entity_source_path(Id, Path).
931
+
932
+ ground_source_refs(Evidence, Refs) :-
933
+ findall(Ref,
934
+ (member(Item, Evidence),
935
+ entity_source_ref(Item.factId, Ref),
936
+ Ref.path \= ''),
937
+ Refs0),
938
+ sort(Refs0, Refs).
939
+
940
+ symbol_coordinate_ref(SymbolId, Ref) :-
941
+ kb_entity(SymbolId, symbol, Props),
942
+ memberchk(sourceFile=RawFile, Props),
943
+ source_path_atom(RawFile, File),
944
+ File \= '',
945
+ !,
946
+ ( valid_symbol_coordinates(Props)
947
+ -> memberchk(sourceLine=RawLine, Props),
948
+ memberchk(sourceColumn=RawColumn, Props),
949
+ memberchk(sourceEndLine=RawEndLine, Props),
950
+ memberchk(sourceEndColumn=RawEndColumn, Props),
951
+ normalize_integer(RawLine, Line),
952
+ normalize_integer(RawColumn, Column),
953
+ normalize_integer(RawEndLine, EndLine),
954
+ normalize_integer(RawEndColumn, EndColumn),
955
+ Ref = _{
956
+ id: SymbolId,
957
+ path: File,
958
+ line: Line,
959
+ column: Column,
960
+ endLine: EndLine,
961
+ endColumn: EndColumn
962
+ }
963
+ ; Ref = _{id: SymbolId, path: File}
964
+ ).
965
+ symbol_coordinate_ref(SymbolId, _{id: SymbolId, path: ''}).
746
966
 
747
967
  proof_status(Stages, unresolved) :-
748
968
  Stages.contradictions.status == blocked,
@@ -762,7 +982,36 @@ stage_dict(Stages, Stage) :-
762
982
  member(_-Stage, Pairs).
763
983
 
764
984
  proof_gaps(Stages, Gaps) :-
765
- findall(Gap, (gap_definition(Gap, _, _, _), proof_gap_present(Gap, Stages)), Gaps).
985
+ proof_issues(Stages, Gaps, _).
986
+
987
+ proof_advisories(Stages, Advisories) :-
988
+ proof_issues(Stages, _, Advisories).
989
+
990
+ % implements REQ-kibi-conservative-requirement-proof
991
+ % proofGaps are blocking only. Receipt-completeness codes become
992
+ % proofAdvisories when passingE2e already supplies strict proof.
993
+ % Invariant: proofStatus == proven implies proofGaps == [].
994
+ proof_issues(Stages, Gaps, Advisories) :-
995
+ findall(Gap,
996
+ (gap_definition(Gap, _, _, _),
997
+ proof_gap_present(Gap, Stages),
998
+ \+ proof_issue_advisory(Gap, Stages)),
999
+ Gaps),
1000
+ findall(Gap,
1001
+ (gap_definition(Gap, _, _, _),
1002
+ proof_gap_present(Gap, Stages),
1003
+ proof_issue_advisory(Gap, Stages)),
1004
+ Advisories).
1005
+
1006
+ receipt_completeness_issue(missing_verification_receipt).
1007
+ receipt_completeness_issue(stale_verification_receipt).
1008
+ receipt_completeness_issue(failed_verification_receipt).
1009
+ receipt_completeness_issue(invalid_verification_receipt).
1010
+ receipt_completeness_issue(verification_contract_mismatch).
1011
+
1012
+ proof_issue_advisory(Gap, Stages) :-
1013
+ receipt_completeness_issue(Gap),
1014
+ Stages.passingE2e.status == passed.
766
1015
 
767
1016
  proof_gap_present(missing_semantic_inventory, Stages) :- Stages.semanticInventory.propositionCount =:= 0.
768
1017
  proof_gap_present(incomplete_semantic_inventory, Stages) :- Stages.semanticInventory.missingCount > 0.
@@ -783,9 +1032,12 @@ proof_gap_present(missing_verification_receipt, Stages) :- Stages.passingE2e.mis
783
1032
  proof_gap_present(stale_verification_receipt, Stages) :- Stages.passingE2e.staleReceiptTests \= [].
784
1033
  proof_gap_present(failed_verification_receipt, Stages) :- Stages.passingE2e.failedReceiptTests \= [].
785
1034
  proof_gap_present(invalid_verification_receipt, Stages) :- Stages.passingE2e.invalidReceiptTests \= [].
1035
+ proof_gap_present(verification_contract_mismatch, Stages) :- Stages.passingE2e.contractMismatchReceiptTests \= [].
786
1036
  proof_gap_present(verification_snapshot_unavailable, Stages) :- Stages.passingE2e.snapshotUnavailableTests \= [].
787
1037
  proof_gap_present(missing_executable_test_symbol, Stages) :- Stages.executableSymbols.status == missing.
788
- proof_gap_present(missing_production_symbol, Stages) :- Stages.productionSymbols.symbols == [].
1038
+ proof_gap_present(missing_production_symbol, Stages) :-
1039
+ Stages.productionSymbols.symbols == [],
1040
+ Stages.productionSymbols.structuralSymbols == [].
789
1041
  proof_gap_present(missing_production_symbol_coverage, Stages) :- Stages.productionSymbols.uncoveredSymbols \= [].
790
1042
  proof_gap_present(missing_symbol_coordinates, Stages) :- Stages.sourceCoordinates.missingSymbols \= [].
791
1043
  proof_gap_present(missing_requirement_source, Stages) :- Stages.sourceCoordinates.requirementSource == missing.
@@ -803,14 +1055,15 @@ gap_definition(contradiction_check_incomplete, 41, contradictions, "Complete log
803
1055
  gap_definition(missing_scenario, 50, scenarios, "Add a specified_by scenario for the requirement.").
804
1056
  gap_definition(missing_scenario_test, 51, scenario_tests, "Link the scenario to a test with verified_by or validates.").
805
1057
  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.").
1058
+ gap_definition(missing_verification_receipt, 53, passing_e2e, "Run the exact current verification contract through kibi verify and append its kibi.verification-receipt.v2 result for the scenario-backed E2E test.").
807
1059
  gap_definition(stale_verification_receipt, 54, passing_e2e, "Re-run the E2E test against the current code snapshot and append its receipt.").
808
1060
  gap_definition(failed_verification_receipt, 55, passing_e2e, "Repair the failing E2E behavior and append a newer passing receipt for the same snapshot.").
809
1061
  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.").
1062
+ gap_definition(verification_contract_mismatch, 57, passing_e2e, "Run the current verification contract and append its receipt without rewriting historical evidence.").
1063
+ gap_definition(verification_snapshot_unavailable, 58, passing_e2e, "Run coverage through a CLI or MCP runtime that exposes the deterministic workspace snapshot.").
811
1064
  gap_definition(missing_executable_test_symbol, 60, executable_symbols, "Link executable test code to every qualifying E2E test with executable_for.").
812
1065
  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.").
1066
+ gap_definition(missing_production_symbol_coverage, 71, production_symbols, "Link every runtime implementing production symbol to a qualifying E2E test with covered_by; type-shape symbols retain structural unit contracts instead.").
814
1067
  gap_definition(missing_symbol_coordinates, 80, source_coordinates, "Refresh and persist exact coordinates for every proof-bearing symbol.").
815
1068
  gap_definition(missing_requirement_source, 81, source_coordinates, "Bind the requirement to its current source document.").
816
1069
 
@@ -822,6 +1075,10 @@ proof_repairs(Gaps, Repairs) :-
822
1075
  normalize_atom(Value, Atom) :-
823
1076
  kb:normalize_term_atom(Value, Atom).
824
1077
 
1078
+ source_path_atom(Value, Atom) :-
1079
+ kb:source_value_atom(Value, Atom),
1080
+ Atom \= ''.
1081
+
825
1082
  normalize_atom_list(Value, Atoms) :-
826
1083
  kb:normalize_term_atom_list(Value, Atoms).
827
1084
 
package/src/status.pl CHANGED
@@ -19,12 +19,16 @@ status_meta_dict(StatusDict) :-
19
19
  snapshot_id(SnapshotId),
20
20
  synced_at(DataFile, SyncedAt),
21
21
  freshness_state(DataFile, Dirty, SyncState),
22
+ stale_reasons(StaleReasons, StaleReasonCount, StaleReasonsTruncated),
22
23
  StatusDict = _{
23
24
  branch: Branch,
24
25
  snapshotId: SnapshotId,
25
26
  syncedAt: SyncedAt,
26
27
  dirty: Dirty,
27
28
  syncState: SyncState,
29
+ staleReasons: StaleReasons,
30
+ staleReasonCount: StaleReasonCount,
31
+ staleReasonsTruncated: StaleReasonsTruncated,
28
32
  kbPath: KbPath,
29
33
  lastSyncSource: persisted
30
34
  }.
@@ -37,6 +41,9 @@ status_meta_dict(StatusDict) :-
37
41
  syncedAt: null,
38
42
  dirty: false,
39
43
  syncState: unknown,
44
+ staleReasons: [],
45
+ staleReasonCount: 0,
46
+ staleReasonsTruncated: false,
40
47
  kbPath: KbPath,
41
48
  lastSyncSource: unknown
42
49
  }.
@@ -104,9 +111,90 @@ freshness_state(DataFile, false, fresh) :-
104
111
  !.
105
112
  freshness_state(_, true, unknown).
106
113
 
114
+ stale_reasons(Reasons, Count, Truncated) :-
115
+ findall(Reason, stale_indexed_source_reason(Reason), IndexedReasons),
116
+ findall(Reason, stale_knowledge_lane_reason(Reason), KnowledgeReasons),
117
+ findall(Reason, stale_documentation_reason(Reason), DocumentationReasons),
118
+ append(IndexedReasons, KnowledgeReasons, IndexedAndKnowledge),
119
+ append(IndexedAndKnowledge, DocumentationReasons, Reasons0),
120
+ sort(Reasons0, Sorted),
121
+ length(Sorted, Count),
122
+ ( Count > 200
123
+ -> length(Reasons, 200), append(Reasons, _, Sorted), Truncated = true
124
+ ; Reasons = Sorted, Truncated = false
125
+ ).
126
+
127
+ stale_indexed_source_reason(Reason) :-
128
+ attached_workspace_root(WorkspaceRoot),
129
+ kb:kb_indexed_sources(Sources),
130
+ member(SourceAtom, Sources),
131
+ repo_relative_source(SourceAtom, RelativeSource),
132
+ directory_file_path(WorkspaceRoot, RelativeSource, SourcePath),
133
+ ( exists_file(SourcePath)
134
+ -> time_file(SourcePath, FileTime),
135
+ kb_snapshot_time(SnapshotTime),
136
+ FileTime > SnapshotTime,
137
+ Code = indexed_source_newer
138
+ ; exists_directory(SourcePath)
139
+ -> fail
140
+ ; Code = indexed_source_missing
141
+ ),
142
+ entity_ids_for_source(RelativeSource, EntityIds),
143
+ Reason = _{code: Code, path: RelativeSource, entityIds: EntityIds}.
144
+
145
+ stale_knowledge_lane_reason(Reason) :-
146
+ attached_workspace_root(WorkspaceRoot),
147
+ knowledge_lane(Lane),
148
+ directory_file_path(WorkspaceRoot, '.kb', KbRoot),
149
+ directory_file_path(KbRoot, Lane, LaneRoot),
150
+ exists_directory(LaneRoot),
151
+ kb_snapshot_time(SnapshotTime),
152
+ directory_tree_newer_path(LaneRoot, SnapshotTime, Path),
153
+ workspace_relative_path(WorkspaceRoot, Path, RelativePath),
154
+ entity_ids_for_source(RelativePath, EntityIds),
155
+ Reason = _{code: knowledge_source_newer, path: RelativePath, entityIds: EntityIds}.
156
+
157
+ stale_documentation_reason(Reason) :-
158
+ attached_workspace_root(WorkspaceRoot),
159
+ directory_file_path(WorkspaceRoot, 'documentation', DocumentationRoot),
160
+ exists_directory(DocumentationRoot),
161
+ kb_snapshot_time(SnapshotTime),
162
+ directory_tree_newer_path(DocumentationRoot, SnapshotTime, Path),
163
+ workspace_relative_path(WorkspaceRoot, Path, RelativePath),
164
+ entity_ids_for_source(RelativePath, EntityIds),
165
+ Reason = _{code: documentation_source_newer, path: RelativePath, entityIds: EntityIds}.
166
+
167
+ kb_snapshot_time(SnapshotTime) :-
168
+ kb:kb_attached(KbPath),
169
+ ( directory_file_path(KbPath, 'CURRENT', DataFile), exists_file(DataFile)
170
+ -> time_file(DataFile, SnapshotTime)
171
+ ; directory_file_path(KbPath, 'kb.rdf', DataFile), exists_file(DataFile)
172
+ -> time_file(DataFile, SnapshotTime)
173
+ ; SnapshotTime = 0
174
+ ).
175
+
176
+ entity_ids_for_source(RelativeSource, EntityIds) :-
177
+ findall(Id,
178
+ (kb_entity(Id, _Type, Props),
179
+ entity_source_property(Props, RawSource),
180
+ source_value_atom(RawSource, SourceAtom),
181
+ repo_relative_source(SourceAtom, RelativeSource),
182
+ Id \= '') ,
183
+ Ids0),
184
+ sort(Ids0, EntityIds).
185
+
186
+ entity_source_property(Props, RawSource) :-
187
+ memberchk(sourceFile=RawSource, Props),
188
+ !.
189
+ entity_source_property(Props, RawSource) :-
190
+ memberchk(source=RawSource, Props).
191
+
107
192
  workspace_state_changed(SnapshotTime) :-
108
193
  workspace_source_changed(SnapshotTime),
109
194
  !.
195
+ workspace_state_changed(SnapshotTime) :-
196
+ knowledge_lane_tree_changed(SnapshotTime),
197
+ !.
110
198
  workspace_state_changed(SnapshotTime) :-
111
199
  documentation_tree_changed(SnapshotTime),
112
200
  !.
@@ -126,6 +214,15 @@ workspace_source_changed(SnapshotTime) :-
126
214
  ),
127
215
  !.
128
216
 
217
+ knowledge_lane_tree_changed(SnapshotTime) :-
218
+ attached_workspace_root(WorkspaceRoot),
219
+ knowledge_lane(Lane),
220
+ directory_file_path(WorkspaceRoot, '.kb', KbRoot),
221
+ directory_file_path(KbRoot, Lane, LaneRoot),
222
+ exists_directory(LaneRoot),
223
+ directory_tree_newer(LaneRoot, SnapshotTime),
224
+ !.
225
+
129
226
  documentation_tree_changed(SnapshotTime) :-
130
227
  attached_workspace_root(WorkspaceRoot),
131
228
  directory_file_path(WorkspaceRoot, 'documentation', DocumentationRoot),
@@ -140,6 +237,7 @@ directory_tree_newer(Path, SnapshotTime) :-
140
237
  time_file(Path, EntryTime),
141
238
  EntryTime > SnapshotTime,
142
239
  !.
240
+
143
241
  directory_tree_newer(Path, SnapshotTime) :-
144
242
  exists_directory(Path),
145
243
  directory_files(Path, Entries),
@@ -150,6 +248,21 @@ directory_tree_newer(Path, SnapshotTime) :-
150
248
  directory_tree_newer(ChildPath, SnapshotTime),
151
249
  !.
152
250
 
251
+ directory_tree_newer_path(Path, SnapshotTime, Path) :-
252
+ exists_file(Path),
253
+ entity_documentation_file(Path),
254
+ \+ ignored_documentation_file(Path),
255
+ time_file(Path, EntryTime),
256
+ EntryTime > SnapshotTime.
257
+ directory_tree_newer_path(Path, SnapshotTime, ChildPath) :-
258
+ exists_directory(Path),
259
+ directory_files(Path, Entries),
260
+ member(Entry, Entries),
261
+ Entry \= '.',
262
+ Entry \= '..',
263
+ directory_file_path(Path, Entry, Candidate),
264
+ directory_tree_newer_path(Candidate, SnapshotTime, ChildPath).
265
+
153
266
  attached_workspace_root(WorkspaceRoot) :-
154
267
  kb:kb_attached(KbPath),
155
268
  branch_workspace_from_kb_path(KbPath, _Branch, WorkspaceRoot).
@@ -219,8 +332,20 @@ source_value_atom(Val, Atom) :-
219
332
  term_string(Val, Str),
220
333
  atom_string(Atom, Str).
221
334
 
335
+ knowledge_lane(requirements).
336
+ knowledge_lane(scenarios).
337
+ knowledge_lane(tests).
338
+ knowledge_lane(facts).
339
+ knowledge_lane(adr).
340
+ knowledge_lane(flags).
341
+ knowledge_lane(events).
342
+
222
343
  ignored_documentation_file(Path) :-
223
344
  file_base_name(Path, 'README.md').
345
+ ignored_documentation_file(Path) :-
346
+ sub_atom(Path, _, _, _, '/tests/e2e/').
347
+ ignored_documentation_file(Path) :-
348
+ sub_atom(Path, _, _, _, '/tests/benchmarks/').
224
349
 
225
350
  entity_documentation_file(Path) :-
226
351
  read_file_to_string(Path, Content, []),