kibi-core 0.1.10 → 0.3.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/src/status.pl ADDED
@@ -0,0 +1,215 @@
1
+ % Module: status
2
+ % Curated KB status and freshness reporting for MCP and CLI surfaces.
3
+
4
+ :- module(status, [
5
+ kb_status_json/1,
6
+ status_meta_dict/1
7
+ ]).
8
+
9
+ :- use_module(library(http/json)).
10
+ :- use_module('kb.pl').
11
+
12
+ kb_status_json(JsonString) :-
13
+ status_meta_dict(StatusDict),
14
+ dict_json_string(StatusDict, JsonString).
15
+
16
+ status_meta_dict(StatusDict) :-
17
+ attached_kb_info(Branch, KbPath, DataFile),
18
+ !,
19
+ snapshot_id(SnapshotId),
20
+ synced_at(DataFile, SyncedAt),
21
+ freshness_state(DataFile, Dirty, SyncState),
22
+ StatusDict = _{
23
+ branch: Branch,
24
+ snapshotId: SnapshotId,
25
+ syncedAt: SyncedAt,
26
+ dirty: Dirty,
27
+ syncState: SyncState,
28
+ kbPath: KbPath,
29
+ lastSyncSource: persisted
30
+ }.
31
+ status_meta_dict(StatusDict) :-
32
+ % Fallback for non-standard KB paths (e.g. temp dirs in tests)
33
+ ( kb:kb_attached(KbPath) -> true ; KbPath = unknown ),
34
+ StatusDict = _{
35
+ branch: unknown,
36
+ snapshotId: unknown,
37
+ syncedAt: null,
38
+ dirty: false,
39
+ syncState: unknown,
40
+ kbPath: KbPath,
41
+ lastSyncSource: unknown
42
+ }.
43
+
44
+ attached_kb_info(Branch, KbPath, DataFile) :-
45
+ kb:kb_attached(KbPath),
46
+ branch_workspace_from_kb_path(KbPath, Branch, _WorkspaceRoot),
47
+ directory_file_path(KbPath, 'kb.rdf', DataFile).
48
+
49
+ snapshot_id(SnapshotId) :-
50
+ kb:kb_attached_snapshot(stamp(MTime, Size)),
51
+ !,
52
+ format(atom(SnapshotId), 'stamp:~16f:~w', [MTime, Size]).
53
+ snapshot_id(SnapshotId) :-
54
+ kb:kb_attached_snapshot(missing),
55
+ !,
56
+ SnapshotId = missing.
57
+ snapshot_id(unknown).
58
+
59
+ synced_at(DataFile, SyncedAt) :-
60
+ exists_file(DataFile),
61
+ !,
62
+ time_file(DataFile, Timestamp),
63
+ format_time(atom(SyncedAt), '%FT%TZ', Timestamp).
64
+ synced_at(_, @(null)).
65
+
66
+ freshness_state(DataFile, true, stale) :-
67
+ exists_file(DataFile),
68
+ time_file(DataFile, SnapshotTime),
69
+ workspace_state_changed(SnapshotTime),
70
+ !.
71
+ freshness_state(DataFile, false, fresh) :-
72
+ exists_file(DataFile),
73
+ !.
74
+ freshness_state(_, true, unknown).
75
+
76
+ workspace_state_changed(SnapshotTime) :-
77
+ workspace_source_changed(SnapshotTime),
78
+ !.
79
+ workspace_state_changed(SnapshotTime) :-
80
+ documentation_tree_changed(SnapshotTime),
81
+ !.
82
+
83
+ workspace_source_changed(SnapshotTime) :-
84
+ attached_workspace_root(WorkspaceRoot),
85
+ kb_entity(_, _, Props),
86
+ memberchk(source=SourceValue, Props),
87
+ source_value_atom(SourceValue, SourceAtom),
88
+ repo_relative_source(SourceAtom, RelativeSource),
89
+ directory_file_path(WorkspaceRoot, RelativeSource, SourcePath),
90
+ ( exists_file(SourcePath)
91
+ -> time_file(SourcePath, FileTime),
92
+ FileTime > SnapshotTime
93
+ ; true
94
+ ),
95
+ !.
96
+
97
+ documentation_tree_changed(_SnapshotTime) :-
98
+ attached_workspace_root(WorkspaceRoot),
99
+ documentation_markdown_untracked(WorkspaceRoot),
100
+ !.
101
+ documentation_tree_changed(SnapshotTime) :-
102
+ attached_workspace_root(WorkspaceRoot),
103
+ directory_file_path(WorkspaceRoot, 'documentation', DocumentationRoot),
104
+ exists_directory(DocumentationRoot),
105
+ directory_tree_newer(DocumentationRoot, SnapshotTime),
106
+ !.
107
+
108
+ documentation_markdown_untracked(WorkspaceRoot) :-
109
+ directory_file_path(WorkspaceRoot, 'documentation', DocumentationRoot),
110
+ exists_directory(DocumentationRoot),
111
+ documentation_markdown_file(DocumentationRoot, FilePath),
112
+ path_relative_to_workspace(WorkspaceRoot, FilePath, RelativePath),
113
+ \+ known_source_path(RelativePath),
114
+ !.
115
+
116
+ documentation_markdown_file(Path, Path) :-
117
+ exists_file(Path),
118
+ file_name_extension(_, md, Path),
119
+ !.
120
+ documentation_markdown_file(Path, FilePath) :-
121
+ exists_directory(Path),
122
+ directory_files(Path, Entries),
123
+ member(Entry, Entries),
124
+ Entry \= '.',
125
+ Entry \= '..',
126
+ directory_file_path(Path, Entry, ChildPath),
127
+ documentation_markdown_file(ChildPath, FilePath).
128
+
129
+ path_relative_to_workspace(WorkspaceRoot, FilePath, RelativePath) :-
130
+ atom_concat(WorkspaceRoot, '/', Prefix),
131
+ atom_concat(Prefix, RelativePath, FilePath).
132
+
133
+ known_source_path(RelativePath) :-
134
+ kb_entity(_, _, Props),
135
+ memberchk(source=SourceValue, Props),
136
+ source_value_atom(SourceValue, SourceAtom),
137
+ repo_relative_source(SourceAtom, RelativePath).
138
+
139
+ directory_tree_newer(Path, SnapshotTime) :-
140
+ time_file(Path, EntryTime),
141
+ EntryTime > SnapshotTime,
142
+ !.
143
+ directory_tree_newer(Path, SnapshotTime) :-
144
+ exists_directory(Path),
145
+ directory_files(Path, Entries),
146
+ member(Entry, Entries),
147
+ Entry \= '.',
148
+ Entry \= '..',
149
+ directory_file_path(Path, Entry, ChildPath),
150
+ directory_tree_newer(ChildPath, SnapshotTime),
151
+ !.
152
+
153
+ attached_workspace_root(WorkspaceRoot) :-
154
+ kb:kb_attached(KbPath),
155
+ branch_workspace_from_kb_path(KbPath, _Branch, WorkspaceRoot).
156
+
157
+ branch_workspace_from_kb_path(KbPath, Branch, WorkspaceRoot) :-
158
+ branch_path_segments(KbPath, BranchesDir, Segments),
159
+ file_directory_name(BranchesDir, KbRoot),
160
+ file_directory_name(KbRoot, WorkspaceRoot),
161
+ atomic_list_concat(Segments, '/', Branch).
162
+
163
+ branch_path_segments(KbPath, BranchesDir, [Base]) :-
164
+ file_directory_name(KbPath, BranchesDir),
165
+ file_base_name(BranchesDir, branches),
166
+ file_base_name(KbPath, Base).
167
+ branch_path_segments(KbPath, BranchesDir, Segments) :-
168
+ file_directory_name(KbPath, Parent),
169
+ Parent \= KbPath,
170
+ branch_path_segments(Parent, BranchesDir, ParentSegments),
171
+ file_base_name(KbPath, Base),
172
+ append(ParentSegments, [Base], Segments).
173
+
174
+ repo_relative_source(SourceAtom, RelativeSource) :-
175
+ strip_fragment(SourceAtom, NoFragment),
176
+ \+ sub_atom(NoFragment, _, _, _, '://'),
177
+ ( attached_workspace_root(WorkspaceRoot),
178
+ workspace_relative_path(WorkspaceRoot, NoFragment, RelativePath)
179
+ -> RelativeSource = RelativePath
180
+ ; RelativeSource = NoFragment
181
+ ).
182
+
183
+ workspace_relative_path(WorkspaceRoot, SourcePath, RelativePath) :-
184
+ atom_concat(WorkspaceRoot, '/', Prefix),
185
+ atom_concat(Prefix, RelativePath, SourcePath).
186
+
187
+ strip_fragment(SourceAtom, NoFragment) :-
188
+ ( sub_atom(SourceAtom, Before, _, _, '#')
189
+ -> sub_atom(SourceAtom, 0, Before, _, NoFragment)
190
+ ; NoFragment = SourceAtom
191
+ ).
192
+
193
+ source_value_atom(Val, Atom) :-
194
+ nonvar(Val),
195
+ compound(Val),
196
+ Val =.. ['^^', Inner, _Type],
197
+ !,
198
+ source_value_atom(Inner, Atom).
199
+ source_value_atom(literal(type(_, Val)), Atom) :-
200
+ !,
201
+ source_value_atom(Val, Atom).
202
+ source_value_atom(Val, Atom) :-
203
+ string(Val),
204
+ !,
205
+ atom_string(Atom, Val).
206
+ source_value_atom(Val, Atom) :-
207
+ atom(Val),
208
+ !,
209
+ Atom = Val.
210
+ source_value_atom(Val, Atom) :-
211
+ term_string(Val, Str),
212
+ atom_string(Atom, Str).
213
+
214
+ dict_json_string(Dict, JsonString) :-
215
+ with_output_to(string(JsonString), json_write_dict(current_output, Dict, [])).