kibi-core 0.14.0 → 0.15.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.
@@ -18,6 +18,8 @@
18
18
  :- use_module('kb.pl').
19
19
  :- use_module('checks.pl', [
20
20
  check_domain_contradictions_and_witnesses/2,
21
+ infeasible_scenario/5,
22
+ scenario_feasibility_outcome/2,
21
23
  check_rule_safety/1,
22
24
  check_rule_verifiability/1
23
25
  ]).
@@ -61,7 +63,7 @@ requirement_proof_context(VerificationSnapshot, CheckedAt, MaxAgeSeconds,
61
63
  proofTestBindings: TestBindings
62
64
  }.
63
65
 
64
- % implements REQ-kibi-proof-applicability-reason
66
+ % implements REQ-kibi-conservative-requirement-proof
65
67
  % Exemption first: a current requirement explicitly marked proof_exempt with a
66
68
  % reason is intentionally outside E2E-proof scope and must never fall through
67
69
  % to the ladder (which would report misleading receipt gaps).
@@ -78,7 +80,7 @@ requirement_proof(ReqId, _ReqProps, _Context, Proof) :-
78
80
  requirement_proof(ReqId, ReqProps, Context, Proof) :-
79
81
  semantic_inventory_stage(ReqProps, SemanticStage, Inventory),
80
82
  logic_grounding_stage(ReqId, ReqProps, Inventory, Context, LogicStage),
81
- contradiction_stage(ReqId, LogicStage.status, Context, ContradictionStage),
83
+ contradiction_stage(ReqId, LogicStage.status, SemanticStage.status, Context, ContradictionStage),
82
84
  scenario_stage(ReqId, ScenarioStage, ScenarioIds),
83
85
  scenario_test_stage(ScenarioIds, ScenarioTestStage, ScenarioTests),
84
86
  passing_e2e_stage(ScenarioTestStage, Context, PassingE2eStage, PassingE2eTests),
@@ -512,6 +514,21 @@ contradiction_stage(_ReqId, LogicStatus, _Context, Stage) :-
512
514
  Stage = _{status: unresolved, outcome: incomplete_grounding, conflicts: []}.
513
515
  contradiction_stage(_ReqId, passed, _Context, _{status: passed, outcome: no_conflict_found, conflicts: []}).
514
516
 
517
+ %% contradiction_stage(+ReqId, +LogicStatus, +InventoryStatus, +Context, -Stage)
518
+ % implements REQ-kibi-truthful-consistency
519
+ % Absence of a conflict is evidence only when every normative proposition was
520
+ % modeled. Propositions still marked ambiguous or ontology_gap were never
521
+ % encoded, so the checker cannot have looked at them: report the analysis as
522
+ % incomplete instead of no_conflict_found. Found conflicts still win.
523
+ contradiction_stage(ReqId, LogicStatus, InventoryStatus, Context, Stage) :-
524
+ contradiction_stage(ReqId, LogicStatus, Context, Stage0),
525
+ ( Stage0.outcome == no_conflict_found,
526
+ InventoryStatus == unresolved
527
+ -> Stage = _{status: unresolved, outcome: analysis_incomplete, conflicts: [],
528
+ reason: "unresolved_propositions"}
529
+ ; Stage = Stage0
530
+ ).
531
+
515
532
  requirement_contradictions(ReqId, Violations, Conflicts) :-
516
533
  include(violation_mentions_requirement(ReqId), Violations, Conflicts0),
517
534
  maplist(violation_description, Conflicts0, Conflicts).
@@ -542,16 +559,46 @@ scenario_stage(ReqId, Stage, ScenarioIds) :-
542
559
  findall(ScenarioId,
543
560
  (member(ScenarioId, ScenarioTargets), \+ existing_scenario(ScenarioId)),
544
561
  InvalidScenarioTargets),
545
- ((ScenarioIds = [] ; InvalidScenarioTargets \= []) -> Status = missing ; Status = passed),
562
+ % implements REQ-kibi-scenario-feasibility-v2
563
+ % A scenario that expects success but whose assumptions cannot hold with
564
+ % what current requirements require (pairwise or jointly) can never pass,
565
+ % so the requirement cannot be proven through it.
566
+ findall(ScenarioId,
567
+ (member(ScenarioId, ScenarioIds), once(infeasible_scenario(ScenarioId, _, _, _, _))),
568
+ InfeasibleScenarios),
569
+ % A success scenario whose feasibility cannot be decided is reported as a
570
+ % distinct, non-blocking note instead of being counted as feasible.
571
+ findall(_{scenario: ScenarioId, reason: ReasonText},
572
+ ( member(ScenarioId, ScenarioIds),
573
+ once(scenario_feasibility_outcome(ScenarioId, unknown(Reason))),
574
+ unknown_feasibility_reason(Reason, ReasonText)
575
+ ),
576
+ UnknownFeasibility),
577
+ ( (ScenarioIds = [] ; InvalidScenarioTargets \= [])
578
+ -> Status = missing
579
+ ; InfeasibleScenarios \= []
580
+ -> Status = blocked
581
+ ; Status = passed
582
+ ),
546
583
  maplist(entity_source_ref, ScenarioIds, Sources),
547
584
  Stage = _{
548
585
  status: Status,
549
586
  scenarios: ScenarioIds,
550
587
  scenarioTargets: ScenarioTargets,
551
588
  invalidScenarioTargets: InvalidScenarioTargets,
589
+ infeasibleScenarios: InfeasibleScenarios,
590
+ unknownFeasibility: UnknownFeasibility,
552
591
  sources: Sources
553
592
  }.
554
593
 
594
+ unknown_feasibility_reason(no_assumptions, no_assumptions).
595
+ unknown_feasibility_reason(contradictory_assumptions(_), contradictory_assumptions).
596
+ unknown_feasibility_reason(unmatched_assumption(_), unmatched_assumption).
597
+ unknown_feasibility_reason(incomparable_assumption(_), incomparable_assumption).
598
+ unknown_feasibility_reason(conflicting_requirements(_), conflicting_requirements).
599
+ unknown_feasibility_reason(undecided_rule(_), undecided_rule).
600
+ unknown_feasibility_reason(undetermined_validity(_), undetermined_validity).
601
+
555
602
  existing_scenario(ScenarioId) :-
556
603
  kb_entity(ScenarioId, scenario, _).
557
604
 
@@ -939,10 +986,10 @@ receipt_for_current_mode(TestId, Context, Receipts, SnapshotReceipts) :-
939
986
  ( Context.proofBindingMode == per_contract,
940
987
  get_dict(TestId, Context.proofTestBindings, CurrentBinding)
941
988
  -> include(receipt_with_binding_hash, Receipts, BoundReceipts),
942
- include(receipt_for_binding(CurrentBinding), BoundReceipts, SnapshotReceipts),
943
- ( SnapshotReceipts == []
989
+ include(receipt_for_binding(CurrentBinding), BoundReceipts, BindingReceipts),
990
+ ( BindingReceipts == []
944
991
  -> include(receipt_for_snapshot(Context.proofSnapshot), Receipts, SnapshotReceipts)
945
- ; true
992
+ ; SnapshotReceipts = BindingReceipts
946
993
  )
947
994
  ; include(receipt_for_snapshot(Context.proofSnapshot), Receipts, SnapshotReceipts)
948
995
  ).
@@ -1610,8 +1657,9 @@ receipt_completeness_issue(failed_proof_receipt).
1610
1657
  receipt_completeness_issue(invalid_proof_receipt).
1611
1658
  receipt_completeness_issue(proof_contract_mismatch).
1612
1659
 
1613
- proof_issue_advisory(_Gap, _Stages) :-
1614
- fail.
1660
+ % Unknown scenario feasibility never blocks proof: it is surfaced as an
1661
+ % advisory so it is not mistaken for a passed feasibility check.
1662
+ proof_issue_advisory(unknown_scenario_feasibility, _Stages).
1615
1663
 
1616
1664
  proof_gap_present(missing_semantic_inventory, Stages) :- Stages.semanticInventory.propositionCount =:= 0.
1617
1665
  proof_gap_present(incomplete_semantic_inventory, Stages) :- Stages.semanticInventory.missingCount > 0.
@@ -1626,7 +1674,11 @@ proof_gap_present(ambiguous_logic_grounding, Stages) :-
1626
1674
  proof_gap_present(blocking_contradiction, Stages) :- Stages.contradictions.status == blocked.
1627
1675
  proof_gap_present(contradiction_check_incomplete, Stages) :- Stages.contradictions.status == unresolved.
1628
1676
  proof_gap_present(missing_scenario, Stages) :- Stages.scenarios.status == missing.
1629
- proof_gap_present(missing_scenario_test, Stages) :- Stages.scenarios.status == passed, Stages.scenarioTests.status == missing.
1677
+ proof_gap_present(infeasible_scenario, Stages) :- Stages.scenarios.infeasibleScenarios \= [].
1678
+ proof_gap_present(unknown_scenario_feasibility, Stages) :-
1679
+ get_dict(unknownFeasibility, Stages.scenarios, Unknown),
1680
+ Unknown \= [].
1681
+ proof_gap_present(missing_scenario_test, Stages) :- memberchk(Stages.scenarios.status, [passed, blocked]), Stages.scenarioTests.status == missing.
1630
1682
  proof_gap_present(missing_passing_e2e, Stages) :- Stages.passingE2e.status == missing.
1631
1683
  proof_gap_present(missing_proof_receipt, Stages) :- Stages.passingE2e.missingReceiptTests \= [].
1632
1684
  proof_gap_present(stale_proof_receipt, Stages) :- Stages.passingE2e.staleReceiptTests \= [].
@@ -1652,7 +1704,9 @@ gap_definition(missing_logic_grounding, 32, logic_grounding, "Ground every model
1652
1704
  gap_definition(ambiguous_logic_grounding, 33, logic_grounding, "Remove duplicate or invalid ground representations and validate linked rules.").
1653
1705
  gap_definition(blocking_contradiction, 40, contradictions, "Supersede or reconcile the conflicting normative requirement.").
1654
1706
  gap_definition(contradiction_check_incomplete, 41, contradictions, "Complete logical grounding before interpreting absence of a conflict as evidence.").
1707
+ gap_definition(unknown_scenario_feasibility, 49, scenarios, "Link the property values the success scenario relies on with assumes facts that a current requirement constrains, so its feasibility can be decided.").
1655
1708
  gap_definition(missing_scenario, 50, scenarios, "Add a specified_by scenario for the requirement.").
1709
+ gap_definition(infeasible_scenario, 49, scenarios, "Set expects: rejection on the scenario, correct its assumes fact, or record a human-approved exception requirement that exempts the forbidding requirement.").
1656
1710
  gap_definition(missing_scenario_test, 51, scenario_tests, "Link the scenario to a test with verified_by or validates.").
1657
1711
  gap_definition(missing_passing_e2e, 52, passing_e2e, "Record fresh passing end-to-end evidence on a scenario-backed test.").
1658
1712
  gap_definition(missing_proof_receipt, 53, passing_e2e, "Run the configured proof integration through kibi prove and append its kibi.proof-receipt.v1 result for the scenario-backed proof-bearing test.").
@@ -19,9 +19,16 @@ 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('source-path-dangling').
22
23
  known_rule('no-cycles').
23
24
  known_rule('required-fields').
24
25
  known_rule('deprecated-adr-no-successor').
26
+ known_rule('superseded-requirement-open').
27
+ known_rule('scenario-feasibility').
28
+ known_rule('scenario-feasibility-unknown').
29
+ known_rule('exception-claim-keys').
30
+ known_rule('numeric-string-value').
31
+ known_rule('rule-key-arguments-missing').
25
32
  known_rule('domain-contradictions').
26
33
  known_rule('logic-coverage').
27
34
  known_rule('rule-safety').
@@ -40,6 +47,13 @@ known_rule('domain-implication').
40
47
  known_rule('subject-key-identity').
41
48
  known_rule('subject-key-shape').
42
49
  known_rule('ontology-quality').
50
+ known_rule('exception-unapproved').
51
+ known_rule('exception-approval-self-attested').
52
+ known_rule('agent-requirement-unapproved').
53
+ known_rule('requirement-rationale-missing').
54
+ known_rule('symbol-owner-superseded').
55
+ known_rule('adr-unlinked').
56
+ known_rule('adr-proposed').
43
57
  known_rule('predicate-schema-conformance').
44
58
 
45
59
  rule_enforcement_class('must-priority-coverage', canonical).
@@ -47,16 +61,23 @@ rule_enforcement_class('symbol-coverage', canonical).
47
61
  rule_enforcement_class('symbol-traceability', canonical).
48
62
  rule_enforcement_class('no-dangling-refs', canonical).
49
63
  rule_enforcement_class('source-relationship-parity', canonical).
64
+ rule_enforcement_class('source-path-dangling', canonical).
50
65
  rule_enforcement_class('no-cycles', canonical).
51
66
  rule_enforcement_class('required-fields', canonical).
52
67
  rule_enforcement_class('deprecated-adr-no-successor', canonical).
68
+ rule_enforcement_class('superseded-requirement-open', canonical).
69
+ rule_enforcement_class('scenario-feasibility', canonical).
70
+ rule_enforcement_class('scenario-feasibility-unknown', advisory).
71
+ rule_enforcement_class('exception-claim-keys', canonical).
72
+ rule_enforcement_class('numeric-string-value', advisory).
73
+ rule_enforcement_class('rule-key-arguments-missing', advisory).
53
74
  rule_enforcement_class('domain-contradictions', canonical).
54
75
  rule_enforcement_class('logic-coverage', canonical).
55
76
  rule_enforcement_class('rule-safety', canonical).
56
77
  rule_enforcement_class('rule-verifiability', canonical).
57
78
  rule_enforcement_class('query-plan-safety', canonical).
58
79
  rule_enforcement_class('req-status-vocabulary', canonical).
59
- rule_enforcement_class('strict-fact-shape', advisory).
80
+ rule_enforcement_class('strict-fact-shape', canonical).
60
81
  rule_enforcement_class('strict-req-fact-pairing', advisory).
61
82
  rule_enforcement_class('predicate-verifiability', advisory).
62
83
  rule_enforcement_class('strict-readiness', migration).
@@ -68,6 +89,13 @@ rule_enforcement_class('domain-implication', advisory).
68
89
  rule_enforcement_class('subject-key-identity', advisory).
69
90
  rule_enforcement_class('subject-key-shape', advisory).
70
91
  rule_enforcement_class('ontology-quality', advisory).
92
+ rule_enforcement_class('exception-unapproved', advisory).
93
+ rule_enforcement_class('exception-approval-self-attested', advisory).
94
+ rule_enforcement_class('agent-requirement-unapproved', advisory).
95
+ rule_enforcement_class('requirement-rationale-missing', advisory).
96
+ rule_enforcement_class('symbol-owner-superseded', advisory).
97
+ rule_enforcement_class('adr-unlinked', advisory).
98
+ rule_enforcement_class('adr-proposed', advisory).
71
99
  rule_enforcement_class('predicate-schema-conformance', advisory).
72
100
 
73
101
  rule_implementation('must-priority-coverage', prolog).
@@ -75,9 +103,16 @@ rule_implementation('symbol-coverage', prolog).
75
103
  rule_implementation('symbol-traceability', prolog).
76
104
  rule_implementation('no-dangling-refs', prolog).
77
105
  rule_implementation('source-relationship-parity', typescript).
106
+ rule_implementation('source-path-dangling', typescript).
78
107
  rule_implementation('no-cycles', prolog).
79
108
  rule_implementation('required-fields', prolog).
80
109
  rule_implementation('deprecated-adr-no-successor', prolog).
110
+ rule_implementation('superseded-requirement-open', prolog).
111
+ rule_implementation('scenario-feasibility', prolog).
112
+ rule_implementation('scenario-feasibility-unknown', prolog).
113
+ rule_implementation('exception-claim-keys', prolog).
114
+ rule_implementation('numeric-string-value', prolog).
115
+ rule_implementation('rule-key-arguments-missing', prolog).
81
116
  rule_implementation('domain-contradictions', prolog).
82
117
  rule_implementation('logic-coverage', prolog).
83
118
  rule_implementation('rule-safety', prolog).
@@ -96,6 +131,13 @@ rule_implementation('domain-implication', prolog).
96
131
  rule_implementation('subject-key-identity', prolog).
97
132
  rule_implementation('subject-key-shape', prolog).
98
133
  rule_implementation('ontology-quality', prolog).
134
+ rule_implementation('exception-unapproved', typescript).
135
+ rule_implementation('exception-approval-self-attested', typescript).
136
+ rule_implementation('agent-requirement-unapproved', typescript).
137
+ rule_implementation('requirement-rationale-missing', typescript).
138
+ rule_implementation('symbol-owner-superseded', prolog).
139
+ rule_implementation('adr-unlinked', prolog).
140
+ rule_implementation('adr-proposed', prolog).
99
141
  rule_implementation('predicate-schema-conformance', typescript).
100
142
 
101
143
  rule_predicate('must-priority-coverage', check_must_priority_coverage).
@@ -105,6 +147,12 @@ rule_predicate('no-dangling-refs', check_no_dangling_refs).
105
147
  rule_predicate('no-cycles', check_no_cycles).
106
148
  rule_predicate('required-fields', check_required_fields).
107
149
  rule_predicate('deprecated-adr-no-successor', check_deprecated_adrs).
150
+ rule_predicate('superseded-requirement-open', check_superseded_requirement_open).
151
+ rule_predicate('scenario-feasibility', check_scenario_feasibility).
152
+ rule_predicate('scenario-feasibility-unknown', check_scenario_feasibility_unknown).
153
+ rule_predicate('exception-claim-keys', check_exception_claim_keys).
154
+ rule_predicate('numeric-string-value', check_numeric_string_values).
155
+ rule_predicate('rule-key-arguments-missing', check_rule_key_arguments_missing).
108
156
  rule_predicate('domain-contradictions', check_domain_contradictions).
109
157
  rule_predicate('logic-coverage', check_logic_coverage).
110
158
  rule_predicate('rule-safety', check_rule_safety).
@@ -122,6 +170,9 @@ rule_predicate('domain-implication', check_domain_implication).
122
170
  rule_predicate('subject-key-identity', check_subject_key_identity).
123
171
  rule_predicate('subject-key-shape', check_subject_key_shape).
124
172
  rule_predicate('ontology-quality', check_ontology_quality).
173
+ rule_predicate('symbol-owner-superseded', check_symbol_owner_superseded).
174
+ rule_predicate('adr-unlinked', check_adr_unlinked).
175
+ rule_predicate('adr-proposed', check_adr_proposed).
125
176
 
126
177
  rule_predicate_arity('must-priority-coverage', 1).
127
178
  rule_predicate_arity('symbol-coverage', 1).
@@ -130,6 +181,12 @@ rule_predicate_arity('no-dangling-refs', 1).
130
181
  rule_predicate_arity('no-cycles', 1).
131
182
  rule_predicate_arity('required-fields', 1).
132
183
  rule_predicate_arity('deprecated-adr-no-successor', 1).
184
+ rule_predicate_arity('superseded-requirement-open', 1).
185
+ rule_predicate_arity('scenario-feasibility', 1).
186
+ rule_predicate_arity('scenario-feasibility-unknown', 1).
187
+ rule_predicate_arity('exception-claim-keys', 1).
188
+ rule_predicate_arity('numeric-string-value', 1).
189
+ rule_predicate_arity('rule-key-arguments-missing', 1).
133
190
  rule_predicate_arity('domain-contradictions', 1).
134
191
  rule_predicate_arity('logic-coverage', 1).
135
192
  rule_predicate_arity('rule-safety', 1).
@@ -147,15 +204,25 @@ rule_predicate_arity('domain-implication', 1).
147
204
  rule_predicate_arity('subject-key-identity', 1).
148
205
  rule_predicate_arity('subject-key-shape', 1).
149
206
  rule_predicate_arity('ontology-quality', 1).
207
+ rule_predicate_arity('symbol-owner-superseded', 1).
208
+ rule_predicate_arity('adr-unlinked', 1).
209
+ rule_predicate_arity('adr-proposed', 1).
150
210
 
151
211
  rule_description('must-priority-coverage', 'Every must-priority requirement must have a scenario and a test').
152
212
  rule_description('symbol-coverage', 'Production symbols need qualifying coverage via covered_by plus a canonical requirement/scenario test path').
153
213
  rule_description('symbol-traceability', 'Production symbols must directly implement requirements for ownership; covered_by is coverage only and executable_for is test identity only').
154
214
  rule_description('no-dangling-refs', 'All relationship targets must exist in the KB').
155
215
  rule_description('source-relationship-parity', 'Authored Markdown and relationship-shard edges must exactly match the compiled KB').
216
+ 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').
156
217
  rule_description('no-cycles', 'No circular dependency chains in requirements').
157
218
  rule_description('required-fields', 'All entities must have required fields').
158
219
  rule_description('deprecated-adr-no-successor', 'Deprecated ADRs must have a successor ADR that supersedes them').
220
+ rule_description('superseded-requirement-open', 'A requirement that another requirement supersedes must have status closed, and supersedes links between requirements must not form a cycle').
221
+ rule_description('scenario-feasibility', 'A scenario that expects success must not assume property values that cannot hold, alone or together, with what current requirements require through typed property values or rules, unless an approved exception requirement waives that requirement or the listed clauses').
222
+ rule_description('scenario-feasibility-unknown', 'Advisory: a scenario that expects success assumes nothing, assumes values that contradict each other, assumes a property value no governing requirement constrains comparably (type, unit or operator mismatch, or conflicting requirements), leaves a governing rule\'s conditions undecided, or conflicts only with constraints whose validity window may not cover it, so its feasibility cannot be decided').
223
+ rule_description('exception-claim-keys', 'An exception requirement\'s exempts_claims must name claim keys of a requirement it exempts, and requires an exempts link').
224
+ rule_description('numeric-string-value', 'Advisory: a property value stores a numeric-looking string where the comparison is numeric (ordering operator, or an int/number fact on the same property); the finding names the fact and the typed value, without converting it').
225
+ rule_description('rule-key-arguments-missing', 'Advisory: an opposing rule pair stays unresolved only because a body predicate declares no key_arguments; the finding names the predicate and the key positions that would decide the pair').
159
226
  rule_description('domain-contradictions', 'Detect contradictions between requirements constraining the same fact').
160
227
  rule_description('logic-coverage', 'Require every explicitly declared atomic requirement claim to be grounded by a linked strict-property or predicate fact').
161
228
  rule_description('rule-safety', 'Stored Logic IR rules use only the typed, bounded, stratified vocabulary').
@@ -171,7 +238,14 @@ rule_description('proof-contract-symbols', 'Detect unresolved required_proofs.sy
171
238
  rule_description('entity-id-style', 'Entity IDs name the governed behavior and match their filename stem; new purely numeric IDs are reported at creation boundaries').
172
239
  rule_description('domain-redundancy', 'Two current requirements must not ground the identical logical term or share a ground fact unless linked by supersedes or restates').
173
240
  rule_description('domain-implication', 'Informational: one requirement\'s numeric bound strictly implies another requirement\'s bound on the same subject and property').
174
- rule_description('subject-key-identity', 'Subject keys must name a shared component, not be derived from a requirement ID').
175
- rule_description('subject-key-shape', 'Subject keys follow dotted component.aspect[.sub] with lowercase snake segments').
241
+ rule_description('subject-key-identity', 'Subject keys must name a shared component, not be derived from a requirement ID, and one subject or claim must not be minted as several active subject facts or identical property_value facts').
242
+ rule_description('subject-key-shape', 'Subject keys follow dotted component.aspect[.sub] with lowercase snake segments, and property keys name a property rather than a numbered clause (clause_NN)').
176
243
  rule_description('ontology-quality', 'Informational: predicate schemas whose argument values mostly occur in only one fact are carrying prose instead of a shared vocabulary').
244
+ rule_description('exception-unapproved', 'Advisory: a current exception requirement exempts another requirement but has no approved_by, so it exempts nothing until a human approves it').
245
+ rule_description('exception-approval-self-attested', 'Advisory: an agent-authored exception records approved_by but no human corroborated it (origin.approved_by or approval_ref is missing), so a human should confirm the approval').
246
+ rule_description('agent-requirement-unapproved', 'Informational: current requirements authored by an agent (origin.kind agent) that no human has approved (origin.approved_by), listed up to 25 per check').
247
+ rule_description('requirement-rationale-missing', 'Advisory: a current requirement authored by a human or an agent (origin.kind human or agent) states no rationale: no rationale field, no Rationale or Why section in its body, and no link to an ADR; listed up to 25 per check plus one summary finding').
248
+ rule_description('symbol-owner-superseded', 'Advisory: a symbol whose every implements target is superseded or deprecated has no current owning requirement; listed up to 25 per check plus one summary finding').
249
+ rule_description('adr-unlinked', 'Advisory: an accepted ADR that no requirement or other ADR is linked with (relates_to, supersedes or any typed edge, in either direction)').
250
+ rule_description('adr-proposed', 'Informational: an ADR that is still proposed and not superseded, so nobody accepted or withdrew the decision').
177
251
  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').
@@ -8,8 +8,10 @@
8
8
  % entity-id-style filename stem must equal the frontmatter id
9
9
  % domain-redundancy two current requirements ground the same term
10
10
  % domain-implication one numeric bound implies another (informational)
11
- % subject-key-identity subject keys derived from requirement IDs
12
- % subject-key-shape subject keys outside component.aspect[.sub]
11
+ % subject-key-identity subject keys derived from requirement IDs, and one
12
+ % subject or claim minted as several facts
13
+ % subject-key-shape subject keys outside component.aspect[.sub], and
14
+ % property keys that number a clause
13
15
  % ontology-quality predicate schemas whose arguments are mostly
14
16
  % one-off atoms (prose smuggled into atoms)
15
17
  %
@@ -356,6 +358,16 @@ implication_witness_violation(Witness, violation(
356
358
  %
357
359
  % Subject facts are reported; property_value facts are reported only when no
358
360
  % subject fact carries the same key (otherwise fixing the subject fixes them).
361
+ %
362
+ % The rule also reports vocabulary fragmentation, where one subject or claim
363
+ % was minted as several facts so requirements about it never meet: more than
364
+ % one active subject fact for the same subject_key, and more than one active
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
367
+ % (the two bounds of a range) or in value (two requirements bounding the same
368
+ % property differently) are distinct claims, which domain-contradictions and
369
+ % domain-implication compare. Each group is reported once, on its first fact
370
+ % id.
359
371
  % implements REQ-kibi-subject-vocabulary
360
372
  check_subject_key_identity(Violations) :-
361
373
  findall(Segment-ReqId, requirement_subject_segment(ReqId, Segment), SegmentPairs0),
@@ -368,7 +380,116 @@ check_subject_key_identity(Violations) :-
368
380
  subject_key_identity_violation(Segments, SubjectKeys, Violation),
369
381
  Violations0
370
382
  ),
371
- sort(Violations0, Violations).
383
+ duplicate_subject_fact_violations(SubjectDuplicates),
384
+ duplicate_property_fact_violations(PropertyDuplicates),
385
+ append([Violations0, SubjectDuplicates, PropertyDuplicates], Violations1),
386
+ sort(Violations1, Violations).
387
+
388
+ %% duplicate_subject_fact_violations(-Violations)
389
+ duplicate_subject_fact_violations(Violations) :-
390
+ findall(
391
+ SubjectKey-FactId,
392
+ ( fact_subject_key_of_kind(FactId, subject, SubjectKey),
393
+ active_fact(FactId)
394
+ ),
395
+ Pairs0
396
+ ),
397
+ sort(Pairs0, Pairs),
398
+ group_pairs_by_key(Pairs, Groups),
399
+ findall(
400
+ violation(
401
+ 'subject-key-identity',
402
+ FirstId,
403
+ Description,
404
+ Suggestion,
405
+ Source,
406
+ _{subjectKey: SubjectKey, facts: FactIds}
407
+ ),
408
+ ( member(SubjectKey-FactIds, Groups),
409
+ FactIds = [FirstId, _|_],
410
+ length(FactIds, Count),
411
+ atomic_list_concat(FactIds, ', ', FactText),
412
+ format(string(Description),
413
+ "~w active subject facts share subject_key ~w (~w), so requirements about this subject are split across facts that never meet",
414
+ [Count, SubjectKey, FactText]),
415
+ format(string(Suggestion),
416
+ "Keep one subject fact for ~w, point every requirement's constrains link at it, and remove the others (kb_delete) once nothing links them",
417
+ [SubjectKey]),
418
+ entity_text(FirstId, fact, source, Source)
419
+ ),
420
+ Violations
421
+ ).
422
+
423
+ %% duplicate_property_fact_violations(-Violations)
424
+ duplicate_property_fact_violations(Violations) :-
425
+ findall(
426
+ key(SubjectKey, PropertyKey, Operator, Value)-FactId,
427
+ ( fact_subject_key_of_kind(FactId, property_value, SubjectKey),
428
+ kb_entity(FactId, fact, Props),
429
+ memberchk(property_key=RawProperty, Props),
430
+ normalize_term_atom(RawProperty, PropertyKey),
431
+ memberchk(operator=RawOperator, Props),
432
+ normalize_term_atom(RawOperator, Operator),
433
+ property_value_signature(Props, Value),
434
+ active_fact(FactId)
435
+ ),
436
+ Pairs0
437
+ ),
438
+ sort(Pairs0, Pairs),
439
+ group_pairs_by_key(Pairs, Groups),
440
+ findall(
441
+ violation(
442
+ 'subject-key-identity',
443
+ FirstId,
444
+ Description,
445
+ "Link every requirement to one shared fact and remove the duplicates (kb_delete) once nothing links them",
446
+ Source,
447
+ _{subjectKey: SubjectKey, propertyKey: PropertyKey, operator: Operator, facts: FactIds}
448
+ ),
449
+ ( member(key(SubjectKey, PropertyKey, Operator, _)-FactIds, Groups),
450
+ FactIds = [FirstId, _|_],
451
+ length(FactIds, Count),
452
+ atomic_list_concat(FactIds, ', ', FactText),
453
+ 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]),
456
+ entity_text(FirstId, fact, source, Source)
457
+ ),
458
+ Violations
459
+ ).
460
+
461
+ %% property_value_signature(+Props, -Signature)
462
+ % The typed value and unit of a property_value fact, as Field-Value pairs in a
463
+ % fixed field order, so two facts compare equal only when they store the same
464
+ % value of the same type in the same unit.
465
+ property_value_signature(Props, Signature) :-
466
+ findall(
467
+ Field-Value,
468
+ ( member(Field, [value_type, value_string, value_int, value_number, value_bool, unit]),
469
+ memberchk(Field=Raw, Props),
470
+ signature_value(Raw, Value),
471
+ Value \== ''
472
+ ),
473
+ Signature
474
+ ).
475
+
476
+ signature_value('^^'(Raw, _), Value) :- !, signature_value(Raw, Value).
477
+ signature_value(literal(type(_, Raw)), Value) :- !, signature_value(Raw, Value).
478
+ signature_value(Raw, Value) :- string(Raw), !, atom_string(Value, Raw).
479
+ signature_value(Raw, Raw).
480
+
481
+ %% active_fact(+FactId)
482
+ % A fact whose status does not retire it.
483
+ active_fact(FactId) :-
484
+ entity_text(FactId, fact, status, Status),
485
+ retired_fact_status(Status, Retired),
486
+ Retired == false.
487
+
488
+ retired_fact_status(Status, Retired) :-
489
+ ( memberchk(Status, ["superseded", "deprecated", "closed", "removed", "retired", "inactive", "archived"])
490
+ -> Retired = true
491
+ ; Retired = false
492
+ ).
372
493
 
373
494
  subject_key_identity_violation(Segments, SubjectKeys, violation(
374
495
  'subject-key-identity',
@@ -471,6 +592,33 @@ subject_key_shape_violation(violation(
471
592
  \+ valid_subject_key(SubjectKey),
472
593
  format(string(Description), "Subject key ~w does not follow the component.aspect[.sub] convention", [SubjectKey]),
473
594
  entity_text(FactId, fact, source, Source).
595
+ % A property key that numbers a clause (`clause_03_must_refresh_...`,
596
+ % `contract_clause_2`) names a position in one requirement's prose, not a
597
+ % property other requirements can share, so equal claims never meet.
598
+ subject_key_shape_violation(violation(
599
+ 'subject-key-shape',
600
+ FactId,
601
+ Description,
602
+ "Rename the property key to the property the clause constrains (for example refresh_before_serving), reusing an existing property_key for the same subject when one fits",
603
+ Source
604
+ )) :-
605
+ kb_entity(FactId, fact, Props),
606
+ memberchk(property_key=RawKey, Props),
607
+ normalize_term_atom(RawKey, PropertyKey),
608
+ clause_numbered_property_key(PropertyKey),
609
+ format(string(Description), "Property key ~w numbers a clause instead of naming a property", [PropertyKey]),
610
+ entity_text(FactId, fact, source, Source).
611
+
612
+ %% clause_numbered_property_key(+PropertyKey)
613
+ % True when the key contains the word `clause` followed by a number.
614
+ clause_numbered_property_key(PropertyKey) :-
615
+ atom(PropertyKey),
616
+ atomic_list_concat(Words, '_', PropertyKey),
617
+ append(_, [clause, Number|_], Words),
618
+ atom_codes(Number, Codes),
619
+ Codes \== [],
620
+ forall(member(Code, Codes), code_type(Code, digit)),
621
+ !.
474
622
 
475
623
  %% valid_subject_key(+SubjectKey)
476
624
  valid_subject_key(SubjectKey) :-
package/src/status.pl CHANGED
@@ -3,7 +3,8 @@
3
3
 
4
4
  :- module(status, [
5
5
  kb_status_json/1,
6
- status_meta_dict/1
6
+ status_meta_dict/1,
7
+ status_scope_dict/1
7
8
  ]).
8
9
 
9
10
  :- use_module(library(http/json)).
@@ -57,6 +58,19 @@ status_meta_dict(StatusDict) :-
57
58
  lastSyncSource: unknown
58
59
  }.
59
60
 
61
+ %% status_scope_dict(-Scope)
62
+ % implements REQ-kibi-search-answer-layer-v2
63
+ % The snapshot an answer is computed from: the attached snapshot id and when
64
+ % it was synced. Unlike status_meta_dict/1 it skips the freshness scan, so a
65
+ % discovery answer can name its snapshot at no extra cost; kb_status remains
66
+ % the place to ask whether that snapshot is stale.
67
+ status_scope_dict(_{snapshotId: SnapshotId, syncedAt: SyncedAt}) :-
68
+ attached_kb_info(_Branch, _KbPath, DataFile),
69
+ !,
70
+ snapshot_id(SnapshotId),
71
+ synced_at(DataFile, SyncedAt).
72
+ status_scope_dict(_{snapshotId: unknown, syncedAt: null}).
73
+
60
74
  attached_kb_info(Branch, KbPath, DataFile) :-
61
75
  kb:kb_attached(KbPath),
62
76
  branch_workspace_from_kb_path(KbPath, Branch, _WorkspaceRoot),
@@ -99,7 +113,8 @@ synced_at(DataFile, SyncedAt) :-
99
113
  exists_file(DataFile),
100
114
  !,
101
115
  time_file(DataFile, Timestamp),
102
- format_time(atom(SyncedAt), '%FT%TZ', Timestamp).
116
+ stamp_date_time(Timestamp, TimestampUTC, 'UTC'),
117
+ format_time(atom(SyncedAt), '%FT%TZ', TimestampUTC).
103
118
  % Before the first successful sync there is no kb.rdf, so the public JSON contract must expose syncedAt: null.
104
119
  synced_at(_, null).
105
120
 
@@ -263,6 +278,7 @@ directory_tree_newer(Path, SnapshotTime) :-
263
278
  !.
264
279
 
265
280
  directory_tree_newer(Path, SnapshotTime) :-
281
+ \+ ignored_documentation_file(Path),
266
282
  exists_directory(Path),
267
283
  directory_files(Path, Entries),
268
284
  member(Entry, Entries),
@@ -275,6 +291,7 @@ directory_tree_newer(Path, SnapshotTime) :-
275
291
  directory_tree_newer_path(Path, SnapshotTime, Path) :-
276
292
  newer_entity_documentation_file(Path, SnapshotTime).
277
293
  directory_tree_newer_path(Path, SnapshotTime, ChildPath) :-
294
+ \+ ignored_documentation_file(Path),
278
295
  exists_directory(Path),
279
296
  directory_files(Path, Entries),
280
297
  member(Entry, Entries),
@@ -377,8 +394,12 @@ ignored_documentation_file(Path) :-
377
394
  file_base_name(Path, 'README.md').
378
395
  ignored_documentation_file(Path) :-
379
396
  sub_atom(Path, _, _, _, '/tests/e2e/').
397
+ ignored_documentation_file(Path) :-
398
+ sub_atom(Path, _, _, 0, '/tests/e2e').
380
399
  ignored_documentation_file(Path) :-
381
400
  sub_atom(Path, _, _, _, '/tests/benchmarks/').
401
+ ignored_documentation_file(Path) :-
402
+ sub_atom(Path, _, _, 0, '/tests/benchmarks').
382
403
 
383
404
  %% newer_entity_documentation_file(+Path, +SnapshotTime) is semidet.
384
405
  % Cheap path and mtime tests run before the file is read, and the content
@@ -395,6 +416,8 @@ newer_entity_documentation_file(Path, SnapshotTime) :-
395
416
  entity_documentation_file(Path) :-
396
417
  read_file_to_string(Path, Content, []),
397
418
  sub_string(Content, 0, 3, _, "---"),
419
+ % These are existence checks, not generators. Repeated metadata examples
420
+ % must not multiply freshness reasons or cause Cartesian backtracking.
398
421
  once(sub_string(Content, _, _, _, "id:")),
399
422
  once(sub_string(Content, _, _, _, "title:")),
400
423
  once(sub_string(Content, _, _, _, "status:")).