kibi-core 0.7.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.
package/src/discovery.pl CHANGED
@@ -4,12 +4,14 @@
4
4
  :- module(discovery, [
5
5
  find_gaps_json/8,
6
6
  coverage_report_json/7,
7
+ coverage_report_json/10,
7
8
  graph_expand_json/8
8
9
  ]).
9
10
 
10
11
  :- use_module(library(http/json)).
11
12
  :- use_module(library(aggregate)).
12
13
  :- use_module('kb.pl').
14
+ :- use_module('requirement_proof.pl', [requirement_proof_context/1, requirement_proof_context/4, requirement_proof/4]).
13
15
  :- use_module('status.pl', [status_meta_dict/1]).
14
16
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
15
17
 
@@ -27,7 +29,10 @@ find_gaps_json(TypeFilter, MissingRelationships, PresentRelationships, Tags, Sou
27
29
  dict_json_string(Response, JsonString).
28
30
 
29
31
  coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, JsonString) :-
30
- coverage_rows(By, Tags, IncludePassing, IncludeTransitive, Rows0, Summary),
32
+ coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, unknown, '1970-01-01T00:00:00Z', 604800, JsonString).
33
+
34
+ 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),
31
36
  sort_dict_rows(Rows0, SortedRows),
32
37
  paginate_rows(SortedRows, Offset, Limit, Rows),
33
38
  status_meta_dict(Meta),
@@ -95,14 +100,15 @@ relationship_count(Id, Relationship, Count) :-
95
100
  (kb_relationship(Relationship, Id, _); kb_relationship(Relationship, _, Id)),
96
101
  Count).
97
102
 
98
- coverage_rows(req, Tags, IncludePassing, IncludeTransitive, Rows, Summary) :-
103
+ coverage_rows(req, Tags, IncludePassing, IncludeTransitive, VerificationSnapshot, CheckedAt, MaxAgeSeconds, Rows, Summary) :-
99
104
  !,
105
+ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds, ProofContext),
100
106
  findall(Row,
101
- requirement_coverage_row(Tags, IncludeTransitive, Row),
107
+ requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row),
102
108
  AllRows),
103
109
  filter_req_coverage_rows(IncludePassing, AllRows, Rows),
104
110
  coverage_summary(AllRows, Summary).
105
- coverage_rows(symbol, Tags, IncludePassing, _IncludeTransitive, Rows, Summary) :-
111
+ coverage_rows(symbol, Tags, IncludePassing, _IncludeTransitive, _VerificationSnapshot, _CheckedAt, _MaxAgeSeconds, Rows, Summary) :-
106
112
  !,
107
113
  findall(Row,
108
114
  symbol_coverage_row(Tags, Row),
@@ -110,10 +116,23 @@ coverage_rows(symbol, Tags, IncludePassing, _IncludeTransitive, Rows, Summary) :
110
116
  filter_symbol_coverage_rows(IncludePassing, AllRows, Rows),
111
117
  length(AllRows, Total),
112
118
  include(symbol_row_fully_covered, AllRows, CoveredRows),
119
+ include(symbol_row_uncovered, AllRows, UncoveredRows),
120
+ include(symbol_row_not_applicable, AllRows, NotApplicableRows),
121
+ include(symbol_row_mixed_role, AllRows, MixedRoleRows),
113
122
  length(CoveredRows, Covered),
114
- Uncovered is Total - Covered,
115
- Summary = _{total: Total, fullyCovered: Covered, uncovered: Uncovered}.
116
- coverage_rows(type, _Tags, _IncludePassing, _IncludeTransitive, Rows, Summary) :-
123
+ length(UncoveredRows, Uncovered),
124
+ length(NotApplicableRows, NotApplicable),
125
+ length(MixedRoleRows, MixedRole),
126
+ Applicable is Total - NotApplicable,
127
+ Summary = _{
128
+ total: Total,
129
+ applicable: Applicable,
130
+ fullyCovered: Covered,
131
+ uncovered: Uncovered,
132
+ notApplicable: NotApplicable,
133
+ mixedRole: MixedRole
134
+ }.
135
+ coverage_rows(type, _Tags, _IncludePassing, _IncludeTransitive, _VerificationSnapshot, _CheckedAt, _MaxAgeSeconds, Rows, Summary) :-
117
136
  findall(Type-Count,
118
137
  type_entity_count(Type, Count),
119
138
  Pairs),
@@ -121,7 +140,7 @@ coverage_rows(type, _Tags, _IncludePassing, _IncludeTransitive, Rows, Summary) :
121
140
  length(Rows, Total),
122
141
  Summary = _{total: Total}.
123
142
 
124
- requirement_coverage_row(Tags, IncludeTransitive, Row) :-
143
+ requirement_coverage_row(Tags, IncludeTransitive, ProofContext, Row) :-
125
144
  kb_entity(Id, req, Props),
126
145
  matches_tags(Tags, Props),
127
146
  entity_title_status_source(Props, Title, Status, _Source),
@@ -132,7 +151,7 @@ requirement_coverage_row(Tags, IncludeTransitive, Row) :-
132
151
  count_transitive_symbols(Id, IncludeTransitive, TransitiveSymbolCount),
133
152
  requirement_coverage_state(Id, Props, Gaps, Evaluated, CoverageStatus),
134
153
  requirement_coverage_depth(Id, ScenarioCount, CoverageDepth, CoverageEvidence),
135
- Row = _{
154
+ BaseRow = _{
136
155
  id: Id,
137
156
  type: req,
138
157
  title: Title,
@@ -151,7 +170,9 @@ requirement_coverage_row(Tags, IncludeTransitive, Row) :-
151
170
  scenarioTests: CoverageEvidence.scenarioTests,
152
171
  testStatuses: CoverageEvidence.testStatuses,
153
172
  verificationScopes: CoverageEvidence.verificationScopes
154
- }.
173
+ },
174
+ requirement_proof(Id, Props, ProofContext, Proof),
175
+ put_dict(Proof, BaseRow, Row).
155
176
 
156
177
  symbol_coverage_row(Tags, Row) :-
157
178
  kb_entity(Id, symbol, Props),
@@ -159,19 +180,32 @@ symbol_coverage_row(Tags, Row) :-
159
180
  entity_title_status_source(Props, Title, Status, _Source),
160
181
  count_direct_requirements(Id, DirectRequirementCount),
161
182
  count_direct_tests(Id, TestCount),
162
- ( symbol_no_req_coverage(Id, _)
163
- -> Gaps = [missing_requirement],
183
+ count_executable_tests(Id, ExecutableTestCount),
184
+ ( mixed_role_symbol(Id)
185
+ -> TraceabilityRole = mixed,
186
+ Gaps = [invalid_mixed_symbol_role],
164
187
  CoverageStatus = uncovered
165
- ; Gaps = [],
166
- CoverageStatus = fully_covered
188
+ ; executable_test_symbol(Id)
189
+ -> TraceabilityRole = executable_test,
190
+ Gaps = [],
191
+ CoverageStatus = not_applicable
192
+ ; TraceabilityRole = production,
193
+ ( symbol_no_req_coverage(Id, _)
194
+ -> Gaps = [missing_requirement],
195
+ CoverageStatus = uncovered
196
+ ; Gaps = [],
197
+ CoverageStatus = fully_covered
198
+ )
167
199
  ),
168
200
  Row = _{
169
201
  id: Id,
170
202
  type: symbol,
171
203
  title: Title,
172
204
  status: Status,
205
+ traceabilityRole: TraceabilityRole,
173
206
  directRequirementCount: DirectRequirementCount,
174
207
  testCount: TestCount,
208
+ executableTestCount: ExecutableTestCount,
175
209
  gaps: Gaps,
176
210
  coverageStatus: CoverageStatus
177
211
  }.
@@ -185,6 +219,10 @@ coverage_summary(Rows, Summary) :-
185
219
  include(req_row_missing_scenario, Rows, MissingScenarioRows),
186
220
  include(req_row_missing_test, Rows, MissingTestRows),
187
221
  include(req_row_missing_both, Rows, MissingBothRows),
222
+ include(req_row_proof_proven, Rows, ProofProvenRows),
223
+ include(req_row_proof_unresolved, Rows, ProofUnresolvedRows),
224
+ include(req_row_proof_missing, Rows, ProofMissingRows),
225
+ include(req_row_proof_not_applicable, Rows, ProofNotApplicableRows),
188
226
  length(EvaluatedRows, Evaluated),
189
227
  length(FullyCoveredRows, FullyCovered),
190
228
  length(UncoveredRows, Uncovered),
@@ -192,6 +230,10 @@ coverage_summary(Rows, Summary) :-
192
230
  length(MissingScenarioRows, MissingScenario),
193
231
  length(MissingTestRows, MissingTest),
194
232
  length(MissingBothRows, MissingScenarioAndTest),
233
+ length(ProofProvenRows, ProofProven),
234
+ length(ProofUnresolvedRows, ProofUnresolved),
235
+ length(ProofMissingRows, ProofMissing),
236
+ length(ProofNotApplicableRows, ProofNotApplicable),
195
237
  Summary = _{
196
238
  total: Total,
197
239
  evaluated: Evaluated,
@@ -200,7 +242,11 @@ coverage_summary(Rows, Summary) :-
200
242
  notApplicable: NotApplicable,
201
243
  missingScenario: MissingScenario,
202
244
  missingTest: MissingTest,
203
- missingScenarioAndTest: MissingScenarioAndTest
245
+ missingScenarioAndTest: MissingScenarioAndTest,
246
+ proofProven: ProofProven,
247
+ proofUnresolved: ProofUnresolved,
248
+ proofMissing: ProofMissing,
249
+ proofNotApplicable: ProofNotApplicable
204
250
  }.
205
251
 
206
252
  req_row_fully_covered(Row) :-
@@ -221,10 +267,24 @@ req_row_missing_test(Row) :-
221
267
  Row.gaps = [missing_test].
222
268
  req_row_missing_both(Row) :-
223
269
  Row.gaps = [missing_scenario_and_test].
270
+ req_row_proof_proven(Row) :-
271
+ Row.proofStatus == proven.
272
+ req_row_proof_unresolved(Row) :-
273
+ Row.proofStatus == unresolved.
274
+ req_row_proof_missing(Row) :-
275
+ Row.proofStatus == missing.
276
+ req_row_proof_not_applicable(Row) :-
277
+ Row.proofStatus == not_applicable.
224
278
 
225
279
  symbol_row_fully_covered(Row) :-
226
280
  CoverageStatus = Row.get(coverageStatus),
227
281
  CoverageStatus = fully_covered.
282
+ symbol_row_uncovered(Row) :-
283
+ Row.coverageStatus == uncovered.
284
+ symbol_row_not_applicable(Row) :-
285
+ Row.coverageStatus == not_applicable.
286
+ symbol_row_mixed_role(Row) :-
287
+ Row.traceabilityRole == mixed.
228
288
 
229
289
  count_distinct_targets(Relationship, Id, source, Count) :-
230
290
  !,
@@ -235,7 +295,8 @@ count_distinct_targets(Relationship, Id, source, Count) :-
235
295
  requirement_test_count(Id, Count) :-
236
296
  findall(TestId, kb_relationship(verified_by, Id, TestId), Verified0),
237
297
  findall(TestId, kb_relationship(validates, TestId, Id), Validates0),
238
- append(Verified0, Validates0, Combined0),
298
+ findall(TestId, scenario_requirement_test(Id, TestId), ScenarioTests0),
299
+ append([Verified0, Validates0, ScenarioTests0], Combined0),
239
300
  sort(Combined0, Combined),
240
301
  length(Combined, Count).
241
302
 
@@ -381,6 +442,11 @@ count_direct_tests(Id, Count) :-
381
442
  sort(TestIds0, TestIds),
382
443
  length(TestIds, Count).
383
444
 
445
+ count_executable_tests(Id, Count) :-
446
+ findall(TestId, kb_relationship(executable_for, Id, TestId), TestIds0),
447
+ sort(TestIds0, TestIds),
448
+ length(TestIds, Count).
449
+
384
450
  requirement_gap_list(Id, [Reason]) :-
385
451
  coverage_gap(Id, Reason),
386
452
  !.
@@ -399,11 +465,15 @@ requirement_coverage_state(_Id, _Props, [], false, not_applicable).
399
465
 
400
466
  filter_req_coverage_rows(true, Rows, Rows).
401
467
  filter_req_coverage_rows(false, Rows, Filtered) :-
402
- exclude(req_row_fully_covered, Rows, Filtered).
468
+ exclude(req_row_proof_passing, Rows, Filtered).
469
+
470
+ req_row_proof_passing(Row) :-
471
+ Status = Row.proofStatus,
472
+ memberchk(Status, [proven, not_applicable]).
403
473
 
404
474
  filter_symbol_coverage_rows(true, Rows, Rows).
405
475
  filter_symbol_coverage_rows(false, Rows, Filtered) :-
406
- exclude(symbol_row_fully_covered, Rows, Filtered).
476
+ include(symbol_row_uncovered, Rows, Filtered).
407
477
 
408
478
  entity_priority(Props, Priority) :-
409
479
  ( memberchk(priority=PriorityValue, Props)
package/src/kb.pl CHANGED
@@ -7,6 +7,7 @@
7
7
  with_kb_mutex/1,
8
8
  kb_assert_entity/2,
9
9
  kb_assert_entity_no_audit/2,
10
+ kb_commit_upsert/5,
10
11
  kb_log_entity_upsert/3,
11
12
  kb_retract_entity/1,
12
13
  kb_retract_entity/3,
@@ -43,6 +44,7 @@
43
44
  predicate_schema/6,
44
45
  predicate_fact/5,
45
46
  contradicting_reqs/3,
47
+ req_conflict_witness/3,
46
48
  check_req_contradiction/1,
47
49
  normalize_term_atom/2,
48
50
  normalize_term_atom_list/2,
@@ -111,7 +113,10 @@ kb_attach(Directory) :-
111
113
  atom_concat(Directory, '/audit.log', AuditLog),
112
114
  ( db_attached(AuditLog)
113
115
  -> true % Already attached
114
- ; db_attach(AuditLog, [])
116
+ ; % Close the journal stream after each write. The default `flush`
117
+ % mode retains an exclusive write lock for the lifetime of a
118
+ % long-running MCP process and blocks writers in other runtimes.
119
+ db_attach(AuditLog, [sync(close)])
115
120
  ),
116
121
  % Track attachment state
117
122
  assert(kb_attached(Directory)),
@@ -171,12 +176,18 @@ kb_save_locked(Directory) :-
171
176
  catch(
172
177
  (
173
178
  ensure_snapshot_current(DataFile),
179
+ kb_stage(snapshot_save),
174
180
  save_rdf_snapshot(TempFile),
181
+ kb_stage(audit_sync),
182
+ sync_audit_log,
183
+ % Keep the durable RDF rename after audit synchronization. The
184
+ % temporary snapshot can be discarded if an audit-stage failure
185
+ % occurs, so a failed commit cannot publish new RDF state.
186
+ kb_stage(snapshot_save),
175
187
  rename_file(TempFile, DataFile),
176
188
  current_data_stamp(DataFile, UpdatedStamp),
177
189
  retractall(kb_attached_snapshot(_)),
178
- assert(kb_attached_snapshot(UpdatedStamp)),
179
- sync_audit_log
190
+ assert(kb_attached_snapshot(UpdatedStamp))
180
191
  ),
181
192
  Error,
182
193
  (
@@ -304,6 +315,109 @@ kb_assert_entity_no_audit(Type, Props) :-
304
315
  )
305
316
  )).
306
317
 
318
+ %% kb_commit_upsert(+Type, +Properties, +Relationships, +SkipContradiction, -ChangeKind)
319
+ % Commit an entity upsert, its relationships, audit entries, and RDF snapshot
320
+ % as one bounded operation. The branch lock is held before any RDF mutation,
321
+ % so concurrent runtimes cannot observe or publish an intermediate upsert.
322
+ % Relationships use rel(Type, FromId, ToId, Metadata) terms. Metadata is
323
+ % retained for the caller's serialization contract; the audit schema records
324
+ % only the relationship endpoints and type, as before.
325
+ kb_commit_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
326
+ memberchk(id=Id, Props),
327
+ memberchk(SkipContradiction, [true, false]),
328
+ kb_attached(Directory),
329
+ atom_concat(Directory, '/kb.rdf', DataFile),
330
+ kb_stage(runtime),
331
+ kb_runtime_diagnostic,
332
+ % Emit the lock marker before waiting so a timeout identifies a branch
333
+ % lock held by another runtime instead of reporting only the prior stage.
334
+ kb_stage(lock),
335
+ with_kb_file_lock(Directory, (
336
+ audit_store_writable,
337
+ ensure_snapshot_current(DataFile),
338
+ upsert_change_kind(Id, ChangeKind),
339
+ rdf_transaction((
340
+ kb_stage(rdf_mutation),
341
+ kb_assert_entity_no_audit(Type, Props),
342
+ kb_commit_relationships_no_audit(Relationships),
343
+ kb_maybe_check_req_contradiction(Type, Id, SkipContradiction),
344
+ kb_stage(entity_audit),
345
+ kb_log_entity_upsert(ChangeKind, Type, Props),
346
+ kb_stage(relationship_audit),
347
+ kb_commit_relationship_audits(Relationships),
348
+ kb_save_locked(Directory)
349
+ ))
350
+ )).
351
+
352
+ upsert_change_kind(Id, updated) :-
353
+ once(kb_entity(Id, _, _)),
354
+ !.
355
+ upsert_change_kind(_, created).
356
+
357
+ kb_commit_relationships_no_audit([]).
358
+ kb_commit_relationships_no_audit([
359
+ rel(RelType, FromId, ToId, Metadata)|Rest
360
+ ]) :-
361
+ kb_assert_relationship_no_audit(RelType, FromId, ToId, Metadata),
362
+ kb_commit_relationships_no_audit(Rest).
363
+
364
+ kb_maybe_check_req_contradiction(req, Id, false) :-
365
+ !,
366
+ kb_stage(contradiction_check),
367
+ check_req_contradiction(Id).
368
+ kb_maybe_check_req_contradiction(_, _, _).
369
+
370
+ kb_commit_relationship_audits([]).
371
+ kb_commit_relationship_audits([
372
+ rel(RelType, FromId, ToId, _Metadata)|Rest
373
+ ]) :-
374
+ kb_log_relationship_upsert(RelType, FromId, ToId, []),
375
+ kb_commit_relationship_audits(Rest).
376
+
377
+ % Probe the existing journal without waiting. A process running code from
378
+ % before the sync(close) fix may still retain the journal's write lock; fail
379
+ % before touching RDF so callers receive a safe, retryable diagnostic.
380
+ audit_store_writable :-
381
+ kb_audit_db(AuditLog),
382
+ ( exists_file(AuditLog)
383
+ -> catch(
384
+ setup_call_cleanup(
385
+ open(AuditLog, append, Stream,
386
+ [lock(write), wait(false), encoding(utf8)]),
387
+ true,
388
+ close(Stream)
389
+ ),
390
+ Error,
391
+ throw(error(permission_error(lock, audit_log, AuditLog),
392
+ context(kb_commit_upsert/5, Error)))
393
+ )
394
+ ; true
395
+ ).
396
+
397
+ % Reserved stderr markers are consumed by PrologProcess diagnostics and never
398
+ % form part of the JSON result. They make timeout errors stage-specific even
399
+ % when the child is blocked inside a filesystem primitive.
400
+ kb_stage(Stage) :-
401
+ format(user_error, '__KIBI_STAGE__:~w~n', [Stage]),
402
+ flush_output(user_error).
403
+
404
+ kb_runtime_diagnostic :-
405
+ ( current_prolog_flag(pid, Pid) -> true ; Pid = unknown ),
406
+ ( kb_audit_db(AuditLog) -> true ; AuditLog = unknown ),
407
+ kb_environment_value('KIBI_RUNTIME_NAME', RuntimeName),
408
+ kb_environment_value('KIBI_RUNTIME_VERSION', RuntimeVersion),
409
+ kb_environment_value('KIBI_PACKAGE_VERSIONS', PackageVersions),
410
+ format(user_error,
411
+ '__KIBI_RUNTIME__:pid=~w;audit=~w;runtime=~w@~w;packages=~w~n',
412
+ [Pid, AuditLog, RuntimeName, RuntimeVersion, PackageVersions]),
413
+ flush_output(user_error).
414
+
415
+ kb_environment_value(Name, Value) :-
416
+ ( getenv(Name, Value)
417
+ -> true
418
+ ; Value = unknown
419
+ ).
420
+
307
421
  %% kb_retract_entity_relationships(+Id)
308
422
  % Remove all relationship triples for an entity, preserving property triples.
309
423
  % Used by projectStagedEntities to clear stale relationships before re-asserting.
@@ -358,7 +472,7 @@ kb_retract_entity(Id, Type, AuditProps) :-
358
472
 
359
473
  entity_delete_audit_props(Id, Props, AuditProps) :-
360
474
  findall(Key=Value,
361
- ( member(Key, [title, source, text_ref]),
475
+ ( member(Key, [title, source, text_ref, semantic_text]),
362
476
  memberchk(Key=RawValue, Props),
363
477
  audit_property_value(RawValue, Value)
364
478
  ),
@@ -398,7 +512,7 @@ kb_entity(Id, Type, Props) :-
398
512
  atom_concat(BaseURI, type, TypeURI),
399
513
  PropURI \= TypeURI,
400
514
  uri_to_key(PropURI, Key),
401
- literal_to_value(ValueLiteral, Value)
515
+ literal_to_value(Key, ValueLiteral, Value)
402
516
  ), Props).
403
517
 
404
518
  % Fallback: read from legacy entity/4 facts loaded from .pl files
@@ -625,7 +739,7 @@ store_property(EntityURI, Key, Value, Graph) :-
625
739
  % Key-aware: typed fact fields get specific XSD datatypes.
626
740
  value_to_literal(Key, Value, Literal) :-
627
741
  % Typed fact fields with specific XSD datatypes
628
- ( Key == value_int, integer(Value)
742
+ ( integer_property_key(Key), integer(Value)
629
743
  -> Literal = Value^^'http://www.w3.org/2001/XMLSchema#integer'
630
744
  ; Key == value_number, number(Value)
631
745
  -> Literal = Value^^'http://www.w3.org/2001/XMLSchema#decimal'
@@ -643,9 +757,29 @@ value_to_literal(Key, Value, Literal) :-
643
757
  Literal = Str^^'http://www.w3.org/2001/XMLSchema#string'
644
758
  ).
645
759
 
646
- %% literal_to_value(+Literal, -Value)
647
- % Extract value from RDF literal, parse list syntax back to Prolog lists.
648
- literal_to_value(Literal, Value) :-
760
+ integer_property_key(Key) :-
761
+ memberchk(Key, [
762
+ value_int,
763
+ predicate_arity,
764
+ claim_span_start,
765
+ claim_span_end,
766
+ sourceLine,
767
+ sourceColumn,
768
+ sourceEndLine,
769
+ sourceEndColumn
770
+ ]).
771
+
772
+ %% literal_to_value(+Key, +Literal, -Value)
773
+ % Extract a property value from an RDF literal. JSON-backed fields must remain
774
+ % intact: JSON arrays are also valid Prolog list syntax, but parsing them as
775
+ % Prolog terms corrupts their object entries before the JSON decoder sees them.
776
+ literal_to_value(semantic_inventory, Literal, Value) :-
777
+ !,
778
+ structured_literal_value(Literal, Value).
779
+ literal_to_value(verification_receipts, Literal, Value) :-
780
+ !,
781
+ structured_literal_value(Literal, Value).
782
+ literal_to_value(_Key, Literal, Value) :-
649
783
  ( % Handle ^^/2 functor (RDF typed literal shorthand)
650
784
  Literal = ^^(StrVal, 'http://www.w3.org/2001/XMLSchema#string')
651
785
  -> ( % Preserve RDF typed literal functor for string values so callers
@@ -678,6 +812,12 @@ literal_to_value(Literal, Value) :-
678
812
  ; Value = Literal
679
813
  ).
680
814
 
815
+ structured_literal_value(^^(Value, _), Value) :- !.
816
+ structured_literal_value(literal(type(_, Value)), Value) :- !.
817
+ structured_literal_value(literal(lang(_, Value)), Value) :- !.
818
+ structured_literal_value(literal(Value), Value) :- !.
819
+ structured_literal_value(Value, Value).
820
+
681
821
  %% literal_to_atom(+Literal, -Atom)
682
822
  % Convert RDF literal to atom (for type field).
683
823
  literal_to_atom(Literal, Atom) :-
@@ -979,19 +1119,21 @@ current_req(Id) :-
979
1119
  % semantic conflicts (same subject/property but incompatible values or polarities).
980
1120
  % Checks in order of specificity: polarity conflicts, then value conflicts.
981
1121
  contradicting_reqs(ReqA, ReqB, Reason) :-
982
- current_req(ReqA),
983
- current_req(ReqB),
984
- ReqA @< ReqB,
985
- req_conflict(ReqA, ReqB, Reason).
1122
+ req_conflict_witness(ReqA, ReqB, Witness),
1123
+ Reason = Witness.reason.
986
1124
 
987
1125
  %% ------------------------------------------------------------------
988
1126
  %% Semantic Contradiction Helpers (Task 4)
989
1127
  %% ------------------------------------------------------------------
990
1128
 
991
- %% req_conflict(+ReqA, +ReqB, -Reason)
992
- % Detects conflicts between requirements via shared subject facts and
993
- % incompatible property_value facts linked through requires_property.
994
- req_conflict(ReqA, ReqB, Reason) :-
1129
+ %% req_conflict_witness(+ReqA, +ReqB, -Witness)
1130
+ % Return exact, source-bound evidence for one proven strict or predicate
1131
+ % contradiction. This remains deliberately syntactic: differently shaped
1132
+ % terms are never treated as equivalent merely because their prose is similar.
1133
+ req_conflict_witness(ReqA, ReqB, Witness) :-
1134
+ current_req(ReqA),
1135
+ current_req(ReqB),
1136
+ ReqA @< ReqB,
995
1137
  kb_relationship(constrains, ReqA, SubjectFactA),
996
1138
  kb_relationship(constrains, ReqB, SubjectFactB),
997
1139
  fact_subject_key(SubjectFactA, SubjectKey),
@@ -1005,8 +1147,23 @@ req_conflict(ReqA, ReqB, Reason) :-
1005
1147
  OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, Reason)
1006
1148
  ; property_conflict(SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, PolarityA,
1007
1149
  OpB, ValTypeB, ValB, UnitB, PolarityB, Reason)
1008
- ).
1009
- req_conflict(ReqA, ReqB, Reason) :-
1150
+ ),
1151
+ property_conflict_side(ReqA, FactA, SubjectKey, PropertyKey, OpA, ValTypeA, ValA, UnitA, ScopeA, PolarityA, ValidFromA, ValidToA, Left),
1152
+ property_conflict_side(ReqB, FactB, SubjectKey, PropertyKey, OpB, ValTypeB, ValB, UnitB, ScopeB, PolarityB, ValidFromB, ValidToB, Right),
1153
+ Witness = _{
1154
+ kind: strict_property,
1155
+ status: contradiction,
1156
+ requirements: [ReqA, ReqB],
1157
+ reason: Reason,
1158
+ subjectKey: SubjectKey,
1159
+ propertyKey: PropertyKey,
1160
+ left: Left,
1161
+ right: Right
1162
+ }.
1163
+ req_conflict_witness(ReqA, ReqB, Witness) :-
1164
+ current_req(ReqA),
1165
+ current_req(ReqB),
1166
+ ReqA @< ReqB,
1010
1167
  effective_req_predicate(ReqA, FactA, Namespace, Name, Args, PolarityA),
1011
1168
  effective_req_predicate(ReqB, FactB, Namespace, Name, Args, PolarityB),
1012
1169
  FactA \= FactB,
@@ -1016,6 +1173,86 @@ req_conflict(ReqA, ReqB, Reason) :-
1016
1173
  string(Reason),
1017
1174
  "Predicate conflict on ~w:~w(~w): ~w asserts ~w while ~w asserts ~w",
1018
1175
  [Namespace, Name, ArgsText, ReqA, PolarityA, ReqB, PolarityB]
1176
+ ),
1177
+ predicate_conflict_side(ReqA, FactA, Namespace, Name, Args, PolarityA, Left),
1178
+ predicate_conflict_side(ReqB, FactB, Namespace, Name, Args, PolarityB, Right),
1179
+ length(Args, Arity),
1180
+ findall(SchemaId, predicate_schema(SchemaId, Namespace, Name, Arity, _ArgumentNames, _ArgumentTypes), SchemaIds0),
1181
+ sort(SchemaIds0, SchemaIds),
1182
+ Witness = _{
1183
+ kind: predicate,
1184
+ status: contradiction,
1185
+ requirements: [ReqA, ReqB],
1186
+ reason: Reason,
1187
+ predicateNamespace: Namespace,
1188
+ predicateName: Name,
1189
+ predicateArgs: Args,
1190
+ schemaIds: SchemaIds,
1191
+ left: Left,
1192
+ right: Right
1193
+ }.
1194
+
1195
+ property_conflict_side(ReqId, FactId, SubjectKey, PropertyKey, Operator, ValueType, Value, Unit, Scope, Polarity, ValidFrom, ValidTo, Side) :-
1196
+ entity_evidence_source(ReqId, req, RequirementSource),
1197
+ fact_evidence_fields(FactId, FactSource, ClaimKey, ClaimText, _CanonicalKey),
1198
+ Side = _{
1199
+ requirementId: ReqId,
1200
+ requirementSource: RequirementSource,
1201
+ factId: FactId,
1202
+ factSource: FactSource,
1203
+ claimKey: ClaimKey,
1204
+ claimText: ClaimText,
1205
+ term: _{
1206
+ subjectKey: SubjectKey,
1207
+ propertyKey: PropertyKey,
1208
+ operator: Operator,
1209
+ valueType: ValueType,
1210
+ value: Value,
1211
+ unit: Unit,
1212
+ scope: Scope,
1213
+ polarity: Polarity,
1214
+ validFrom: ValidFrom,
1215
+ validTo: ValidTo
1216
+ }
1217
+ }.
1218
+
1219
+ predicate_conflict_side(ReqId, FactId, Namespace, Name, Args, Polarity, Side) :-
1220
+ entity_evidence_source(ReqId, req, RequirementSource),
1221
+ fact_evidence_fields(FactId, FactSource, ClaimKey, ClaimText, CanonicalKey),
1222
+ Side = _{
1223
+ requirementId: ReqId,
1224
+ requirementSource: RequirementSource,
1225
+ factId: FactId,
1226
+ factSource: FactSource,
1227
+ claimKey: ClaimKey,
1228
+ claimText: ClaimText,
1229
+ term: _{
1230
+ namespace: Namespace,
1231
+ name: Name,
1232
+ args: Args,
1233
+ polarity: Polarity,
1234
+ canonicalKey: CanonicalKey
1235
+ }
1236
+ }.
1237
+
1238
+ entity_evidence_source(EntityId, Type, Source) :-
1239
+ kb_entity(EntityId, Type, Props),
1240
+ (memberchk(source=RawSource, Props) -> evidence_term_atom(RawSource, Source) ; Source = '').
1241
+
1242
+ fact_evidence_fields(FactId, Source, ClaimKey, ClaimText, CanonicalKey) :-
1243
+ kb_entity(FactId, fact, Props),
1244
+ (memberchk(source=RawSource, Props) -> evidence_term_atom(RawSource, Source) ; Source = ''),
1245
+ (memberchk(claim_key=RawClaimKey, Props) -> evidence_term_atom(RawClaimKey, ClaimKey) ; ClaimKey = ''),
1246
+ (memberchk(claim_text=RawClaimText, Props) -> evidence_term_atom(RawClaimText, ClaimText) ; ClaimText = ''),
1247
+ (memberchk(canonical_key=RawCanonicalKey, Props) -> evidence_term_atom(RawCanonicalKey, CanonicalKey) ; CanonicalKey = '').
1248
+
1249
+ evidence_term_atom(Raw, Atom) :-
1250
+ unwrap_rdf_value(Raw, Value),
1251
+ ( atom(Value)
1252
+ -> Atom = Value
1253
+ ; string(Value)
1254
+ -> atom_string(Atom, Value)
1255
+ ; term_string(Value, String), atom_string(Atom, String)
1019
1256
  ).
1020
1257
 
1021
1258
  %% effective_req_predicate(+ReqId, -FactId, -Namespace, -Name, -Args, -Polarity)