kibi-core 0.17.3 → 0.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-core",
3
- "version": "0.17.3",
3
+ "version": "0.18.1",
4
4
  "private": false,
5
5
  "description": "Core Prolog modules and RDF graph logic for Kibi",
6
6
  "type": "module",
package/src/discovery.pl CHANGED
@@ -32,18 +32,32 @@
32
32
  :- use_module('../schema/relationships.pl', [relationship_type/1]).
33
33
 
34
34
  find_gaps_json(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Limit, Offset, JsonString) :-
35
+ % One pass over the entities: every candidate carries whether it is a
36
+ % provenance stub, so the rows and the stub count share the enumeration.
35
37
  findall(
36
- Row,
37
- matching_gap_row(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Row),
38
- Rows0
38
+ Row-Stub,
39
+ matching_gap_candidate(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Row, Stub),
40
+ Candidates
39
41
  ),
42
+ findall(Row, (member(Row-Stub, Candidates), gap_row_visible(Tags, Stub)), Rows0),
40
43
  sort_dict_rows(Rows0, SortedRows),
41
44
  paginate_rows(SortedRows, Offset, Limit, Rows),
42
45
  length(SortedRows, Count),
46
+ % Provenance stubs that match the same filters are counted apart
47
+ % (summary.provenanceStubs) whether or not the requested tags let them
48
+ % into the rows, so a caller can tell "no gaps" from "only stubs".
49
+ % implements REQ-bootstrap-provenance-stubs
50
+ aggregate_all(count, member(_-true, Candidates), StubCount),
51
+ Summary = _{total: Count, provenanceStubs: StubCount},
43
52
  status_meta_dict(Meta),
44
- Response = _{rows: Rows, count: Count, meta: Meta},
53
+ Response = _{rows: Rows, count: Count, summary: Summary, meta: Meta},
45
54
  dict_json_string(Response, JsonString).
46
55
 
56
+ % A stub enters the rows only when the caller asked for its tag.
57
+ gap_row_visible(_Tags, false).
58
+ gap_row_visible(Tags, true) :-
59
+ memberchk('bootstrap:provenance-stub', Tags).
60
+
47
61
  coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, JsonString) :-
48
62
  coverage_report_json(By, Tags, IncludePassing, IncludeTransitive, Limit, Offset, unknown, '1970-01-01T00:00:00Z', 604800, JsonString).
49
63
 
@@ -458,10 +472,15 @@ graph_expand_json(SeedIds, Relationships, Direction, Depth, EntityTypes, MaxNode
458
472
  dict_json_string(Response, JsonString).
459
473
 
460
474
  matching_gap_row(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Row) :-
475
+ matching_gap_candidate(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Row, Stub),
476
+ gap_row_visible(Tags, Stub).
477
+
478
+ % implements REQ-bootstrap-provenance-stubs
479
+ matching_gap_candidate(TypeFilter, MissingRelationships, PresentRelationships, Tags, SourceFilter, Row, Stub) :-
461
480
  kb_entity(Id, Type, Props),
462
481
  matches_type(TypeFilter, Type),
463
482
  matches_tags(Tags, Props),
464
- \+ hidden_provenance_stub(Tags, Type, Props),
483
+ (provenance_stub_props(Type, Props) -> Stub = true ; Stub = false),
465
484
  matches_source(SourceFilter, Props),
466
485
  relationships_missing(Id, MissingRelationships),
467
486
  relationships_present(Id, PresentRelationships),
package/src/kb.pl CHANGED
@@ -15,6 +15,7 @@
15
15
  kb_assert_entity_no_audit/2,
16
16
  kb_commit_upsert/5,
17
17
  kb_commit_upsert_batch/2,
18
+ kb_preview_upsert_contradiction/3,
18
19
  kb_log_entity_upsert/3,
19
20
  kb_retract_entity/1,
20
21
  kb_retract_entity/3,
@@ -1136,6 +1137,36 @@ kb_stage_upsert(Type, Props, Relationships, SkipContradiction, ChangeKind) :-
1136
1137
  kb_log_entity_upsert(ChangeKind, Type, Props),
1137
1138
  kb_commit_relationship_audits(Relationships).
1138
1139
 
1140
+ %% kb_preview_upsert_contradiction(+Type, +Properties, +Relationships)
1141
+ % implements REQ-kibi-operation-interface-parity
1142
+ % Stage one upsert in a transaction that is always rolled back and run the
1143
+ % commit-time requirement contradiction check against the staged KB, so a
1144
+ % kb_upsert dry run refuses exactly what the commit would refuse. Nothing is
1145
+ % written and nothing marks the branch dirty: the no-audit assertions only
1146
+ % touch the RDF store, which the transaction rolls back, and no audit entry,
1147
+ % snapshot save or journal flush follows. The entity index is not
1148
+ % transactional, so the staged id is re-indexed from the restored store.
1149
+ kb_preview_upsert_contradiction(Type, Props, Relationships) :-
1150
+ memberchk(id=Id, Props),
1151
+ with_kb_mutex(
1152
+ call_cleanup(
1153
+ catch(
1154
+ rdf_transaction((
1155
+ kb_assert_entity_no_audit(Type, Props),
1156
+ kb_commit_relationships_no_audit(Relationships),
1157
+ kb_maybe_check_req_contradiction(Type, Id, false),
1158
+ throw(kibi_preview_rolled_back)
1159
+ )),
1160
+ Caught,
1161
+ ( Caught == kibi_preview_rolled_back
1162
+ -> true
1163
+ ; throw(Caught)
1164
+ )
1165
+ ),
1166
+ kb_refresh_entity_index(Id)
1167
+ )
1168
+ ).
1169
+
1139
1170
  kb_stage_upserts([], []).
1140
1171
  kb_stage_upserts([
1141
1172
  upsert(Type, Props, Relationships, SkipContradiction)|Rest
@@ -705,13 +705,22 @@ unique_evidence_by_test(Evidence0, Evidence) :-
705
705
  sort(TestIds0, TestIds),
706
706
  maplist(first_evidence_for_test(Evidence0), TestIds, Evidence).
707
707
 
708
+ % A test shared by scenarios with different accepted scopes keeps its receipt
709
+ % state from any scenario that accepts its scope, independent of scenario order.
710
+ % implements REQ-ui-pattern-component-proof
711
+ first_evidence_for_test(Evidence, TestId, Item) :-
712
+ member(Item, Evidence),
713
+ Item.testId == TestId,
714
+ Item.state \== not_end_to_end,
715
+ !.
708
716
  first_evidence_for_test(Evidence, TestId, Item) :-
709
717
  member(Item, Evidence),
710
718
  Item.testId == TestId,
711
719
  !.
712
720
 
713
721
  scenario_passing_e2e_obligation(Context, TestObligation, Obligation) :-
714
- maplist(test_receipt_evidence(Context), TestObligation.tests, Evidence),
722
+ scenario_accepted_scopes(TestObligation.scenarioId, AcceptedScopes),
723
+ maplist(test_receipt_evidence(Context, AcceptedScopes), TestObligation.tests, Evidence),
715
724
  include(e2e_evidence, Evidence, E2eEvidence),
716
725
  include(non_e2e_evidence, Evidence, NonEndToEndEvidence),
717
726
  evidence_tests_with_state(E2eEvidence, passed, PassingE2eTests),
@@ -734,6 +743,7 @@ scenario_passing_e2e_obligation(Context, TestObligation, Obligation) :-
734
743
  scenarioTestTargets: TestObligation.scenarioTestTargets,
735
744
  invalidScenarioTestTargets: TestObligation.invalidScenarioTestTargets,
736
745
  tests: TestObligation.tests,
746
+ acceptedScopes: AcceptedScopes,
737
747
  e2eTests: E2eTests,
738
748
  nonEndToEndTests: NonEndToEndTests,
739
749
  evidence: Evidence,
@@ -747,6 +757,42 @@ scenario_passing_e2e_obligation(Context, TestObligation, Obligation) :-
747
757
  snapshotUnavailableTests: SnapshotUnavailableTests
748
758
  }.
749
759
 
760
+ %% scenario_accepted_scopes(+ScenarioId, -Scopes)
761
+ % End-to-end evidence proves every scenario. A scenario whose specifying
762
+ % requirements are all grounded only in component-scope UI pattern predicates
763
+ % may also be proven by a unit or integration test that renders the component
764
+ % in isolation, with the same receipt rules (valid, fresh, passing).
765
+ % implements REQ-ui-pattern-component-proof
766
+ scenario_accepted_scopes(ScenarioId, [unit, integration, end_to_end]) :-
767
+ component_scope_scenario(ScenarioId),
768
+ !.
769
+ scenario_accepted_scopes(_ScenarioId, [end_to_end]).
770
+
771
+ %% component_scope_predicate(?Name)
772
+ % Built-in UI pattern predicates a component rendered in isolation can prove:
773
+ % the pattern, its markers and variant parity are visible in the rendered
774
+ % markup. Layout claims (ui_container, visual_layout_rule) depend on real
775
+ % rendering and keep the end-to-end requirement.
776
+ component_scope_predicate(ui_pattern).
777
+ component_scope_predicate(same_pattern).
778
+ component_scope_predicate(pattern_marker).
779
+
780
+ component_scope_scenario(ScenarioId) :-
781
+ findall(ReqId, kb_relationship(specified_by, ReqId, ScenarioId), ReqIds0),
782
+ sort(ReqIds0, ReqIds),
783
+ ReqIds \= [],
784
+ forall(member(ReqId, ReqIds), component_scope_requirement(ReqId)).
785
+
786
+ component_scope_requirement(ReqId) :-
787
+ findall(FactId, kb_relationship(requires_predicate, ReqId, FactId), FactIds),
788
+ FactIds \= [],
789
+ forall(member(FactId, FactIds),
790
+ ( predicate_fact(FactId, _Namespace, Name, _Args, _Polarity),
791
+ component_scope_predicate(Name)
792
+ )),
793
+ \+ kb_relationship(requires_property, ReqId, _),
794
+ \+ kb_relationship(requires_rule, ReqId, _).
795
+
750
796
  e2e_evidence(Item) :- Item.state \= not_end_to_end.
751
797
  non_e2e_evidence(Item) :- Item.state == not_end_to_end.
752
798
 
@@ -799,9 +845,15 @@ evidence_tests_with_state(Evidence, State, TestIds) :-
799
845
  sort(TestIds0, TestIds).
800
846
 
801
847
  test_receipt_evidence(Context, TestId, Evidence) :-
848
+ test_receipt_evidence(Context, [end_to_end], TestId, Evidence).
849
+
850
+ % implements REQ-ui-pattern-component-proof
851
+ % AcceptedScopes lists the verification scopes whose receipts count for the
852
+ % scenario. Tests outside them keep the nonblocking not_end_to_end state.
853
+ test_receipt_evidence(Context, AcceptedScopes, TestId, Evidence) :-
802
854
  kb_entity(TestId, test, Props),
803
855
  test_scope(Props, Scope),
804
- ( Scope \= end_to_end
856
+ ( \+ memberchk(Scope, AcceptedScopes)
805
857
  -> Evidence = _{testId: TestId, state: not_end_to_end, scope: Scope}
806
858
  ; Context.proofSnapshot == unknown
807
859
  -> Evidence = _{testId: TestId, state: snapshot_unavailable, scope: Scope}
@@ -1216,7 +1268,7 @@ type_shape_symbol_with_structural_contract(SymbolId) :-
1216
1268
  memberchk(Status, [active, passing]).
1217
1269
 
1218
1270
  % Stage status plus a typed reason, from a single total clause so status and
1219
- % reason can never disagree (the Align stale-receipts case reported `blocked`
1271
+ % reason can never disagree (a test project's stale-receipts case reported `blocked`
1220
1272
  % with no indication that fresh receipts were the unblock). Clause order keeps
1221
1273
  % the historical precedence: no production symbols at all is `missing` even
1222
1274
  % when E2E evidence is also absent; only then does absent E2E evidence read
@@ -19,6 +19,7 @@ known_rule('symbol-coverage').
19
19
  known_rule('symbol-traceability').
20
20
  known_rule('no-dangling-refs').
21
21
  known_rule('source-relationship-parity').
22
+ known_rule('branch-store-not-compiled').
22
23
  known_rule('source-path-dangling').
23
24
  known_rule('no-cycles').
24
25
  known_rule('required-fields').
@@ -58,12 +59,15 @@ known_rule('adr-proposed').
58
59
  known_rule('predicate-schema-conformance').
59
60
  known_rule('entity-context-missing').
60
61
  known_rule('entity-context-acknowledged').
62
+ known_rule('policy-ownership').
63
+ known_rule('policy-markers').
61
64
 
62
65
  rule_enforcement_class('must-priority-coverage', canonical).
63
66
  rule_enforcement_class('symbol-coverage', canonical).
64
67
  rule_enforcement_class('symbol-traceability', canonical).
65
68
  rule_enforcement_class('no-dangling-refs', canonical).
66
69
  rule_enforcement_class('source-relationship-parity', canonical).
70
+ rule_enforcement_class('branch-store-not-compiled', canonical).
67
71
  rule_enforcement_class('source-path-dangling', canonical).
68
72
  rule_enforcement_class('no-cycles', canonical).
69
73
  rule_enforcement_class('required-fields', canonical).
@@ -103,12 +107,15 @@ rule_enforcement_class('adr-proposed', advisory).
103
107
  rule_enforcement_class('predicate-schema-conformance', advisory).
104
108
  rule_enforcement_class('entity-context-missing', canonical).
105
109
  rule_enforcement_class('entity-context-acknowledged', advisory).
110
+ rule_enforcement_class('policy-ownership', canonical).
111
+ rule_enforcement_class('policy-markers', canonical).
106
112
 
107
113
  rule_implementation('must-priority-coverage', prolog).
108
114
  rule_implementation('symbol-coverage', prolog).
109
115
  rule_implementation('symbol-traceability', prolog).
110
116
  rule_implementation('no-dangling-refs', prolog).
111
117
  rule_implementation('source-relationship-parity', typescript).
118
+ rule_implementation('branch-store-not-compiled', typescript).
112
119
  rule_implementation('source-path-dangling', typescript).
113
120
  rule_implementation('no-cycles', prolog).
114
121
  rule_implementation('required-fields', prolog).
@@ -148,6 +155,8 @@ rule_implementation('adr-proposed', prolog).
148
155
  rule_implementation('predicate-schema-conformance', typescript).
149
156
  rule_implementation('entity-context-missing', typescript).
150
157
  rule_implementation('entity-context-acknowledged', typescript).
158
+ rule_implementation('policy-ownership', typescript).
159
+ rule_implementation('policy-markers', typescript).
151
160
 
152
161
  rule_predicate('must-priority-coverage', check_must_priority_coverage).
153
162
  rule_predicate('symbol-coverage', check_symbol_coverage).
@@ -224,6 +233,7 @@ rule_description('symbol-coverage', 'Production symbols need qualifying coverage
224
233
  rule_description('symbol-traceability', 'Production symbols must directly implement requirements for ownership; covered_by is coverage only and executable_for is test identity only').
225
234
  rule_description('no-dangling-refs', 'All relationship targets must exist in the KB').
226
235
  rule_description('source-relationship-parity', 'Authored Markdown and relationship-shard edges must exactly match the compiled KB').
236
+ rule_description('branch-store-not-compiled', 'The current branch\'s KB store must be compiled from the authored .kb/ sources; a missing or empty store is reported once, naming kibi sync, instead of every store-backed rule reading an empty KB').
227
237
  rule_description('source-path-dangling', 'An authored entity\'s source field must name an existing workspace path (a #anchor suffix is ignored), an existing entity id, or an http(s) URL; a missing source is allowed').
228
238
  rule_description('no-cycles', 'No circular dependency chains in requirements').
229
239
  rule_description('required-fields', 'All entities must have required fields').
@@ -263,3 +273,5 @@ rule_description('adr-proposed', 'Informational: an ADR that is still proposed a
263
273
  rule_description('predicate-schema-conformance', 'Predicate facts match a predicate_schema (project-local, or the built-in catalog in the default namespace) and use its declared argument constants').
264
274
  rule_description('entity-context-missing', 'A current requirement, scenario, test, ADR or observation/meta fact must carry body context: at least 12 words of prose (requirements: under a Context, Rationale, Why, Background, Source, Notes or Evidence heading) that do not restate the title or semantic_text; entities tagged review:context-missing are acknowledged legacy and exempt').
265
275
  rule_description('entity-context-acknowledged', 'Informational: how many current entities carry the review:context-missing tag, so the legacy context backlog stays visible; one summary finding per check').
276
+ rule_description('policy-ownership', 'When a plugin activated for kibi.check-policy.v1 declares ownership rules, every production symbol in a rule\'s file set must implement a current requirement grounded (requires_predicate) in one of the rule\'s predicates; projects without an activated check policy are unaffected').
277
+ rule_description('policy-markers', 'When a plugin activated for kibi.check-policy.v1 declares marker rules, every file implementing a requirement grounded in the rule\'s pattern predicate (with its declared sibling files) must contain each marker the KB declares for that pattern').
@@ -363,7 +363,10 @@ implication_witness_violation(Witness, violation(
363
363
  % was minted as several facts so requirements about it never meet: more than
364
364
  % one active subject fact for the same subject_key, and more than one active
365
365
  % property_value fact stating the same claim: the same subject_key,
366
- % property_key, operator, typed value and unit. Facts that differ in operator
366
+ % property_key, operator, polarity (absent = require), typed value and unit.
367
+ % A `forbid` fact states the opposite of its `require` twin, so the pair is
368
+ % never one claim. Facts linked only by superseded or deprecated requirements
369
+ % are history, not live duplicates. Facts that differ in operator
367
370
  % (the two bounds of a range) or in value (two requirements bounding the same
368
371
  % property differently) are distinct claims, which domain-contradictions and
369
372
  % domain-implication compare. Each group is reported once, on its first fact
@@ -390,7 +393,8 @@ duplicate_subject_fact_violations(Violations) :-
390
393
  findall(
391
394
  SubjectKey-FactId,
392
395
  ( fact_subject_key_of_kind(FactId, subject, SubjectKey),
393
- active_fact(FactId)
396
+ active_fact(FactId),
397
+ identity_live_fact(FactId)
394
398
  ),
395
399
  Pairs0
396
400
  ),
@@ -423,15 +427,17 @@ duplicate_subject_fact_violations(Violations) :-
423
427
  %% duplicate_property_fact_violations(-Violations)
424
428
  duplicate_property_fact_violations(Violations) :-
425
429
  findall(
426
- key(SubjectKey, PropertyKey, Operator, Value)-FactId,
430
+ key(SubjectKey, PropertyKey, Operator, Polarity, Value)-FactId,
427
431
  ( fact_subject_key_of_kind(FactId, property_value, SubjectKey),
428
432
  kb_entity(FactId, fact, Props),
429
433
  memberchk(property_key=RawProperty, Props),
430
434
  normalize_term_atom(RawProperty, PropertyKey),
431
435
  memberchk(operator=RawOperator, Props),
432
436
  normalize_term_atom(RawOperator, Operator),
437
+ property_value_polarity(Props, Polarity),
433
438
  property_value_signature(Props, Value),
434
- active_fact(FactId)
439
+ active_fact(FactId),
440
+ identity_live_fact(FactId)
435
441
  ),
436
442
  Pairs0
437
443
  ),
@@ -444,20 +450,55 @@ duplicate_property_fact_violations(Violations) :-
444
450
  Description,
445
451
  "Link every requirement to one shared fact and remove the duplicates (kb_delete) once nothing links them",
446
452
  Source,
447
- _{subjectKey: SubjectKey, propertyKey: PropertyKey, operator: Operator, facts: FactIds}
453
+ _{subjectKey: SubjectKey, propertyKey: PropertyKey, operator: Operator, polarity: Polarity, facts: FactIds}
448
454
  ),
449
- ( member(key(SubjectKey, PropertyKey, Operator, _)-FactIds, Groups),
455
+ ( member(key(SubjectKey, PropertyKey, Operator, Polarity, _)-FactIds, Groups),
450
456
  FactIds = [FirstId, _|_],
451
457
  length(FactIds, Count),
452
458
  atomic_list_concat(FactIds, ', ', FactText),
453
459
  format(string(Description),
454
- "~w active property_value facts state the same claim on ~w ~w (operator ~w, same value) (~w), so one claim is minted as several facts",
455
- [Count, SubjectKey, PropertyKey, Operator, FactText]),
460
+ "~w active property_value facts state the same claim on ~w ~w (operator ~w, polarity ~w, same value) (~w), so one claim is minted as several facts",
461
+ [Count, SubjectKey, PropertyKey, Operator, Polarity, FactText]),
456
462
  entity_text(FirstId, fact, source, Source)
457
463
  ),
458
464
  Violations
459
465
  ).
460
466
 
467
+ %% property_value_polarity(+Props, -Polarity)
468
+ % The polarity of a property_value fact; an absent polarity means require.
469
+ % `forbid` states the opposite claim of `require` on the same tuple, so the
470
+ % two never count as one claim minted twice.
471
+ % implements REQ-kibi-subject-vocabulary
472
+ property_value_polarity(Props, Polarity) :-
473
+ ( memberchk(polarity=Raw, Props),
474
+ normalize_term_atom(Raw, Polarity0),
475
+ Polarity0 \== ''
476
+ -> Polarity = Polarity0
477
+ ; Polarity = require
478
+ ).
479
+
480
+ %% identity_live_fact(+FactId)
481
+ % A fact still carried by the current requirement set: no requirement links
482
+ % it (an orphan duplicate stays visible), or at least one current requirement
483
+ % links it. A fact linked only by superseded or deprecated requirements is
484
+ % history kept for the append-only supersedes chain, not a live duplicate.
485
+ % `closed` means done, not retired, so a closed requirement still counts.
486
+ % implements REQ-kibi-subject-vocabulary
487
+ identity_live_fact(FactId) :-
488
+ findall(
489
+ ReqId,
490
+ ( member(LinkType, [constrains, requires_property, requires_predicate, requires_rule]),
491
+ kb_relationship(LinkType, ReqId, FactId)
492
+ ),
493
+ Linkers
494
+ ),
495
+ ( Linkers == []
496
+ -> true
497
+ ; member(ReqId, Linkers),
498
+ kb:current_req(ReqId)
499
+ -> true
500
+ ).
501
+
461
502
  %% property_value_signature(+Props, -Signature)
462
503
  % The typed value and unit of a property_value fact, as Field-Value pairs in a
463
504
  % fixed field order, so two facts compare equal only when they store the same