kibi-opencode 0.15.0 → 0.15.2
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/README.md +4 -12
- package/dist/config.d.ts +0 -3
- package/dist/config.js +0 -9
- package/dist/e2e-coverage-signals.js +1 -3
- package/dist/enforcement-policy.d.ts +1 -1
- package/dist/enforcement-policy.js +9 -4
- package/dist/enforcement-scope.js +1 -3
- package/dist/file-entity-links.js +2 -6
- package/dist/file-operation-reminders.d.ts +4 -4
- package/dist/file-operation-state.js +4 -2
- package/dist/init-kibi-alias.js +0 -1
- package/dist/init-kibi-capability.js +1 -5
- package/dist/kb-freshness-state.js +6 -2
- package/dist/kibi-checkpoint-runner.d.ts +1 -1
- package/dist/kibi-checkpoint-runner.js +6 -3
- package/dist/knowledge-classifier.js +2 -1
- package/dist/meaningful-change-classifier.d.ts +1 -1
- package/dist/meaningful-change-classifier.js +1 -2
- package/dist/plugin-startup.js +0 -1
- package/dist/plugin.js +45 -395
- package/dist/prompt.d.ts +2 -6
- package/dist/prompt.js +21 -84
- package/dist/reconcile-engine.d.ts +15 -1
- package/dist/reconcile-engine.js +1 -3
- package/dist/scheduler.js +16 -4
- package/dist/session-edit-state.js +2 -1
- package/dist/source-linked-guidance.js +3 -1
- package/dist/tui.js +1 -403
- package/dist/tui.jsx +1 -113
- package/dist/version-metadata.json +1 -1
- package/package.json +3 -5
- package/dist/brief-delivery-reasons.d.ts +0 -12
- package/dist/brief-delivery-reasons.js +0 -132
- package/dist/brief-intent.d.ts +0 -41
- package/dist/brief-intent.js +0 -142
- package/dist/briefing-runtime.d.ts +0 -24
- package/dist/briefing-runtime.js +0 -277
- package/dist/graph-narrator.d.ts +0 -25
- package/dist/graph-narrator.js +0 -408
- package/dist/idle-brief-audit.d.ts +0 -36
- package/dist/idle-brief-audit.js +0 -186
- package/dist/idle-brief-paths.d.ts +0 -6
- package/dist/idle-brief-paths.js +0 -120
- package/dist/idle-brief-reader.d.ts +0 -37
- package/dist/idle-brief-reader.js +0 -163
- package/dist/idle-brief-runtime.d.ts +0 -55
- package/dist/idle-brief-runtime.js +0 -496
- package/dist/idle-brief-store.d.ts +0 -113
- package/dist/idle-brief-store.js +0 -262
- package/dist/session-fingerprint.d.ts +0 -11
- package/dist/session-fingerprint.js +0 -21
- package/dist/tui-brief-delivery.d.ts +0 -67
- package/dist/tui-brief-delivery.js +0 -279
- package/dist/tui-brief-view-model.d.ts +0 -63
- package/dist/tui-brief-view-model.js +0 -209
- package/dist/utils/brief-marker.d.ts +0 -19
- package/dist/utils/brief-marker.js +0 -101
package/dist/graph-narrator.js
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
// implements REQ-opencode-kibi-briefing-v6
|
|
2
|
-
function asRecord(value) {
|
|
3
|
-
return typeof value === "object" && value !== null
|
|
4
|
-
? value
|
|
5
|
-
: null;
|
|
6
|
-
}
|
|
7
|
-
function asString(value) {
|
|
8
|
-
return typeof value === "string" ? value : "";
|
|
9
|
-
}
|
|
10
|
-
function asStringArray(value) {
|
|
11
|
-
return Array.isArray(value)
|
|
12
|
-
? value.filter((entry) => typeof entry === "string")
|
|
13
|
-
: [];
|
|
14
|
-
}
|
|
15
|
-
function getSessionApi(client) {
|
|
16
|
-
const root = asRecord(client);
|
|
17
|
-
const session = asRecord(root?.session);
|
|
18
|
-
if (!session) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
const create = session.create;
|
|
22
|
-
const prompt = session.prompt;
|
|
23
|
-
if (typeof create !== "function" || typeof prompt !== "function") {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
create: create,
|
|
28
|
-
prompt: prompt,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function extractSessionId(response) {
|
|
32
|
-
const root = asRecord(response);
|
|
33
|
-
if (!root) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
const directId = asString(root.id).trim();
|
|
37
|
-
if (directId) {
|
|
38
|
-
return directId;
|
|
39
|
-
}
|
|
40
|
-
const data = asRecord(root.data);
|
|
41
|
-
return asString(data?.id).trim() || null;
|
|
42
|
-
}
|
|
43
|
-
function extractPromptResponseJson(response) {
|
|
44
|
-
const root = asRecord(response);
|
|
45
|
-
if (!root)
|
|
46
|
-
return null;
|
|
47
|
-
const data = asRecord(root.data);
|
|
48
|
-
const parts = Array.isArray(data?.parts)
|
|
49
|
-
? data.parts
|
|
50
|
-
: Array.isArray(root.parts)
|
|
51
|
-
? root.parts
|
|
52
|
-
: null;
|
|
53
|
-
if (!parts)
|
|
54
|
-
return null;
|
|
55
|
-
for (const part of parts) {
|
|
56
|
-
const record = asRecord(part);
|
|
57
|
-
if (record?.type !== "text") {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const text = asString(record.text);
|
|
61
|
-
if (!text) {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(text);
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
async function invokeTool(sessionApi, sessionID, tool, args) {
|
|
74
|
-
const response = await sessionApi.prompt({
|
|
75
|
-
sessionID,
|
|
76
|
-
parts: [{ type: "text", text: JSON.stringify({ tool, args }) }],
|
|
77
|
-
tools: { [tool]: true },
|
|
78
|
-
format: {
|
|
79
|
-
type: "json_schema",
|
|
80
|
-
schema: {
|
|
81
|
-
type: ["object", "array"],
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
return extractPromptResponseJson(response);
|
|
86
|
-
}
|
|
87
|
-
function inferTypeFromId(id) {
|
|
88
|
-
const prefix = id.split("-")[0]?.toLowerCase() ?? "entity";
|
|
89
|
-
switch (prefix) {
|
|
90
|
-
case "req":
|
|
91
|
-
case "scenario":
|
|
92
|
-
case "test":
|
|
93
|
-
case "adr":
|
|
94
|
-
case "flag":
|
|
95
|
-
case "event":
|
|
96
|
-
case "symbol":
|
|
97
|
-
case "fact":
|
|
98
|
-
return prefix;
|
|
99
|
-
case "scen":
|
|
100
|
-
return "scenario";
|
|
101
|
-
case "sym":
|
|
102
|
-
return "symbol";
|
|
103
|
-
default:
|
|
104
|
-
return prefix;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function humanizeType(type, count = 1) {
|
|
108
|
-
const singular = {
|
|
109
|
-
req: "requirement",
|
|
110
|
-
scenario: "scenario",
|
|
111
|
-
test: "test",
|
|
112
|
-
adr: "ADR",
|
|
113
|
-
flag: "flag",
|
|
114
|
-
event: "event",
|
|
115
|
-
symbol: "symbol",
|
|
116
|
-
fact: "fact",
|
|
117
|
-
}[type] ?? type;
|
|
118
|
-
if (count === 1) {
|
|
119
|
-
return singular;
|
|
120
|
-
}
|
|
121
|
-
if (singular === "ADR") {
|
|
122
|
-
return "ADRs";
|
|
123
|
-
}
|
|
124
|
-
return singular.endsWith("s") ? singular : `${singular}s`;
|
|
125
|
-
}
|
|
126
|
-
function humanizeDomain(domain) {
|
|
127
|
-
const normalized = domain.trim().toLowerCase();
|
|
128
|
-
switch (normalized) {
|
|
129
|
-
case "opencode":
|
|
130
|
-
return "OpenCode";
|
|
131
|
-
case "mcp":
|
|
132
|
-
return "MCP";
|
|
133
|
-
case "cli":
|
|
134
|
-
return "CLI";
|
|
135
|
-
case "vscode":
|
|
136
|
-
return "VSCode";
|
|
137
|
-
case "core":
|
|
138
|
-
return "Core";
|
|
139
|
-
case "requirements":
|
|
140
|
-
return "Requirements";
|
|
141
|
-
default:
|
|
142
|
-
return normalized ? normalized.charAt(0).toUpperCase() + normalized.slice(1) : "Changes";
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function formatList(items) {
|
|
146
|
-
if (items.length === 0)
|
|
147
|
-
return "";
|
|
148
|
-
if (items.length === 1)
|
|
149
|
-
return items[0] ?? "";
|
|
150
|
-
if (items.length === 2)
|
|
151
|
-
return `${items[0]} and ${items[1]}`;
|
|
152
|
-
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
|
|
153
|
-
}
|
|
154
|
-
function normalizeEntity(record, idFallback) {
|
|
155
|
-
const properties = asRecord(record.properties) ?? {};
|
|
156
|
-
const id = asString(record.id || properties.id).trim() || idFallback;
|
|
157
|
-
const type = asString(record.type).trim() ||
|
|
158
|
-
asString(record.entityType).trim() ||
|
|
159
|
-
inferTypeFromId(id);
|
|
160
|
-
const title = asString(record.title).trim() ||
|
|
161
|
-
asString(properties.title).trim() ||
|
|
162
|
-
id;
|
|
163
|
-
const status = asString(record.status).trim() ||
|
|
164
|
-
asString(properties.status).trim() ||
|
|
165
|
-
asString(properties.change_kind).trim();
|
|
166
|
-
const source = asString(record.source).trim() ||
|
|
167
|
-
asString(properties.source).trim();
|
|
168
|
-
const tags = [
|
|
169
|
-
...asStringArray(record.tags),
|
|
170
|
-
...asStringArray(properties.tags),
|
|
171
|
-
];
|
|
172
|
-
const factKind = asString(record.fact_kind).trim() ||
|
|
173
|
-
asString(properties.fact_kind).trim();
|
|
174
|
-
return {
|
|
175
|
-
id,
|
|
176
|
-
type,
|
|
177
|
-
title,
|
|
178
|
-
status,
|
|
179
|
-
source,
|
|
180
|
-
tags,
|
|
181
|
-
factKind,
|
|
182
|
-
removed: false,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
function normalizeGraph(value) {
|
|
186
|
-
const root = asRecord(value);
|
|
187
|
-
if (!root) {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
const edges = Array.isArray(root.edges)
|
|
191
|
-
? root.edges
|
|
192
|
-
.map((edge) => {
|
|
193
|
-
const record = asRecord(edge);
|
|
194
|
-
if (!record)
|
|
195
|
-
return null;
|
|
196
|
-
const from = asString(record.from).trim();
|
|
197
|
-
const to = asString(record.to).trim();
|
|
198
|
-
const type = asString(record.type).trim();
|
|
199
|
-
return from && to && type ? { from, to, type } : null;
|
|
200
|
-
})
|
|
201
|
-
.filter((edge) => edge !== null)
|
|
202
|
-
: [];
|
|
203
|
-
return { edges };
|
|
204
|
-
}
|
|
205
|
-
function describeEntity(entity, id) {
|
|
206
|
-
if (!entity) {
|
|
207
|
-
return id;
|
|
208
|
-
}
|
|
209
|
-
return `${entity.title} (${entity.id})`;
|
|
210
|
-
}
|
|
211
|
-
function inferDomain(entity) {
|
|
212
|
-
const source = entity.source.replaceAll("\\", "/");
|
|
213
|
-
const packageMatch = source.match(/packages\/([^/]+)\//);
|
|
214
|
-
if (packageMatch?.[1]) {
|
|
215
|
-
return humanizeDomain(packageMatch[1]);
|
|
216
|
-
}
|
|
217
|
-
const tagDomain = entity.tags.find((tag) => ["cli", "mcp", "vscode", "opencode", "core"].includes(tag.toLowerCase()));
|
|
218
|
-
if (tagDomain) {
|
|
219
|
-
return humanizeDomain(tagDomain);
|
|
220
|
-
}
|
|
221
|
-
if (["req", "scenario", "test"].includes(entity.type)) {
|
|
222
|
-
return "Requirements";
|
|
223
|
-
}
|
|
224
|
-
return humanizeDomain(entity.type);
|
|
225
|
-
}
|
|
226
|
-
function classifyRelationship(relationship, entities) {
|
|
227
|
-
const fromEntity = entities.get(relationship.from);
|
|
228
|
-
const toEntity = entities.get(relationship.to);
|
|
229
|
-
const fromText = describeEntity(fromEntity, relationship.from);
|
|
230
|
-
const toText = describeEntity(toEntity, relationship.to);
|
|
231
|
-
switch (relationship.type) {
|
|
232
|
-
case "supersedes":
|
|
233
|
-
return `${toText} was superseded by ${fromText}`;
|
|
234
|
-
case "implements":
|
|
235
|
-
return `${fromText} now implements ${toText}`;
|
|
236
|
-
case "covered_by":
|
|
237
|
-
return `${fromText} gained test coverage via ${toText}`;
|
|
238
|
-
case "verified_by":
|
|
239
|
-
return `${fromText} is verified by ${toText}`;
|
|
240
|
-
case "specified_by":
|
|
241
|
-
return `${fromText} is specified by ${toText}`;
|
|
242
|
-
case "requires_property":
|
|
243
|
-
return `${fromText} constrains property ${toText}`;
|
|
244
|
-
case "constrains":
|
|
245
|
-
if (toEntity?.type === "fact" && toEntity.factKind === "subject") {
|
|
246
|
-
return `${fromText} is linked to fact ${toText}`;
|
|
247
|
-
}
|
|
248
|
-
return `${fromText} is linked to ${toText}`;
|
|
249
|
-
default:
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
function uniqueStrings(items) {
|
|
254
|
-
return [...new Set(items.filter(Boolean))];
|
|
255
|
-
}
|
|
256
|
-
function buildEntityChange(entity, relationships, graph) {
|
|
257
|
-
const label = describeEntity(entity, entity.id);
|
|
258
|
-
if (entity.removed) {
|
|
259
|
-
return `${label} was removed`;
|
|
260
|
-
}
|
|
261
|
-
if (entity.status === "superseded") {
|
|
262
|
-
return `${label} was marked superseded`;
|
|
263
|
-
}
|
|
264
|
-
const hasRelationshipChange = relationships.some((relationship) => relationship.from === entity.id || relationship.to === entity.id);
|
|
265
|
-
const hasGraphEdges = (graph?.edges.length ?? 0) > 0;
|
|
266
|
-
if (!hasRelationshipChange && !hasGraphEdges) {
|
|
267
|
-
return `${label} was created`;
|
|
268
|
-
}
|
|
269
|
-
return `${label} was updated`;
|
|
270
|
-
}
|
|
271
|
-
function summarizeTypes(entities) {
|
|
272
|
-
const counts = new Map();
|
|
273
|
-
for (const entity of entities) {
|
|
274
|
-
counts.set(entity.type, (counts.get(entity.type) ?? 0) + 1);
|
|
275
|
-
}
|
|
276
|
-
return formatList([...counts.entries()].map(([type, count]) => `${count} ${humanizeType(type, count)}`));
|
|
277
|
-
}
|
|
278
|
-
function summarizeTopChanges(domainChanges, relationshipChanges) {
|
|
279
|
-
const focus = relationshipChanges.length > 0 ? relationshipChanges : domainChanges;
|
|
280
|
-
const selected = focus.slice(0, 2);
|
|
281
|
-
return selected.length > 0 ? `Key changes: ${selected.join("; ")}.` : "";
|
|
282
|
-
}
|
|
283
|
-
function validationStatusText(checkResult) {
|
|
284
|
-
if (checkResult.count <= 0) {
|
|
285
|
-
return "All checks pass";
|
|
286
|
-
}
|
|
287
|
-
return `${checkResult.count} validation issue${checkResult.count === 1 ? "" : "s"}`;
|
|
288
|
-
}
|
|
289
|
-
async function loadEntity(sessionApi, sessionID, id) {
|
|
290
|
-
const response = await invokeTool(sessionApi, sessionID, "kb_query", { id, limit: 1 });
|
|
291
|
-
if (!Array.isArray(response)) {
|
|
292
|
-
throw new Error(`kb_query returned unsupported payload for ${id}`);
|
|
293
|
-
}
|
|
294
|
-
if (response.length === 0) {
|
|
295
|
-
return {
|
|
296
|
-
id,
|
|
297
|
-
type: inferTypeFromId(id),
|
|
298
|
-
title: id,
|
|
299
|
-
status: "removed",
|
|
300
|
-
source: "",
|
|
301
|
-
tags: [],
|
|
302
|
-
factKind: "",
|
|
303
|
-
removed: true,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
const record = asRecord(response[0]);
|
|
307
|
-
if (!record) {
|
|
308
|
-
throw new Error(`kb_query returned invalid entity for ${id}`);
|
|
309
|
-
}
|
|
310
|
-
return normalizeEntity(record, id);
|
|
311
|
-
}
|
|
312
|
-
async function loadGraph(sessionApi, sessionID, id) {
|
|
313
|
-
const response = await invokeTool(sessionApi, sessionID, "kb_graph", {
|
|
314
|
-
seedIds: [id],
|
|
315
|
-
direction: "both",
|
|
316
|
-
depth: 2,
|
|
317
|
-
maxNodes: 100,
|
|
318
|
-
maxEdges: 200,
|
|
319
|
-
});
|
|
320
|
-
return normalizeGraph(response);
|
|
321
|
-
}
|
|
322
|
-
// implements REQ-opencode-kibi-briefing-v2
|
|
323
|
-
export async function generateGraphNarrative(client, workspaceCtx, changedEntityIds, changedRelationships, checkResult) {
|
|
324
|
-
const uniqueIds = uniqueStrings(changedEntityIds);
|
|
325
|
-
if (uniqueIds.length === 0) {
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
const sessionApi = getSessionApi(client);
|
|
329
|
-
if (!sessionApi) {
|
|
330
|
-
return null;
|
|
331
|
-
}
|
|
332
|
-
let sessionID = null;
|
|
333
|
-
try {
|
|
334
|
-
const worker = await sessionApi.create({
|
|
335
|
-
directory: workspaceCtx.workspaceRoot,
|
|
336
|
-
title: `Kibi Graph Narrator (${workspaceCtx.branch})`,
|
|
337
|
-
});
|
|
338
|
-
sessionID = extractSessionId(worker);
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
if (!sessionID) {
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
346
|
-
const entities = new Map();
|
|
347
|
-
const graphs = new Map();
|
|
348
|
-
for (const id of uniqueIds) {
|
|
349
|
-
let entity;
|
|
350
|
-
try {
|
|
351
|
-
entity = await loadEntity(sessionApi, sessionID, id);
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
if (!entity) {
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
entities.set(id, entity);
|
|
360
|
-
if (entity.removed) {
|
|
361
|
-
graphs.set(id, null);
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
try {
|
|
365
|
-
graphs.set(id, await loadGraph(sessionApi, sessionID, id));
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
graphs.set(id, null);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
const entityList = [...entities.values()];
|
|
372
|
-
if (entityList.length === 0) {
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
const relationshipChanges = uniqueStrings(changedRelationships
|
|
376
|
-
.map((relationship) => classifyRelationship(relationship, entities))
|
|
377
|
-
.filter((sentence) => !!sentence));
|
|
378
|
-
const domainMap = new Map();
|
|
379
|
-
for (const entity of entityList) {
|
|
380
|
-
const domain = inferDomain(entity);
|
|
381
|
-
const changes = domainMap.get(domain) ?? [];
|
|
382
|
-
changes.push(buildEntityChange(entity, changedRelationships, graphs.get(entity.id) ?? null));
|
|
383
|
-
domainMap.set(domain, changes);
|
|
384
|
-
}
|
|
385
|
-
const domains = [...domainMap.entries()]
|
|
386
|
-
.map(([name, changes]) => ({ name, changes: uniqueStrings(changes) }))
|
|
387
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
388
|
-
const domainNames = domains.map((domain) => domain.name);
|
|
389
|
-
const headline = `${summarizeTypes(entityList)} changed${domainNames.length > 1 ? ` across ${formatList(domainNames)} domains` : ""}.`;
|
|
390
|
-
const allDomainChanges = domains.flatMap((domain) => domain.changes);
|
|
391
|
-
const validationStatus = validationStatusText(checkResult);
|
|
392
|
-
const tldr = [
|
|
393
|
-
headline,
|
|
394
|
-
summarizeTopChanges(allDomainChanges, relationshipChanges),
|
|
395
|
-
validationStatus === "All checks pass"
|
|
396
|
-
? "Validation remains clean."
|
|
397
|
-
: `${validationStatus}.`,
|
|
398
|
-
]
|
|
399
|
-
.filter(Boolean)
|
|
400
|
-
.join(" ");
|
|
401
|
-
return {
|
|
402
|
-
headline,
|
|
403
|
-
tldr,
|
|
404
|
-
domains,
|
|
405
|
-
relationshipChanges,
|
|
406
|
-
validationStatus,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export interface AuditCursor {
|
|
2
|
-
lastTimestamp: string;
|
|
3
|
-
lastOperation: string;
|
|
4
|
-
entryCount: number;
|
|
5
|
-
fileSize: number;
|
|
6
|
-
}
|
|
7
|
-
export interface AuditDelta {
|
|
8
|
-
hasChanges: boolean;
|
|
9
|
-
entries: AuditEntry[];
|
|
10
|
-
newCursor: AuditCursor;
|
|
11
|
-
contentHash: string;
|
|
12
|
-
}
|
|
13
|
-
export interface AuditEntry {
|
|
14
|
-
timestamp: string;
|
|
15
|
-
operation: string;
|
|
16
|
-
entityId: string;
|
|
17
|
-
payload?: AuditEntityPayload | AuditRelationshipPayload | null;
|
|
18
|
-
}
|
|
19
|
-
export interface AuditEntityPayload {
|
|
20
|
-
kind: "entity";
|
|
21
|
-
entityType: string;
|
|
22
|
-
changeKind?: "created" | "updated";
|
|
23
|
-
title?: string;
|
|
24
|
-
source?: string;
|
|
25
|
-
textRef?: string;
|
|
26
|
-
properties: Record<string, unknown>;
|
|
27
|
-
}
|
|
28
|
-
export interface AuditRelationshipPayload {
|
|
29
|
-
kind: "relationship";
|
|
30
|
-
relationshipType: string;
|
|
31
|
-
properties: Record<string, unknown>;
|
|
32
|
-
}
|
|
33
|
-
export declare function computeAuditDelta(workspaceRoot: string, branch: string, previousCursor: AuditCursor | null): AuditDelta;
|
|
34
|
-
export declare function getLatestAuditCursor(workspaceRoot: string, branch: string): AuditCursor | null;
|
|
35
|
-
export declare function getAuditTailCursor(workspaceRoot: string, branch: string): AuditCursor | null;
|
|
36
|
-
export declare function guardBranchChanged(startBranch: string, currentBranch: string): boolean;
|
package/dist/idle-brief-audit.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import * as crypto from "node:crypto";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { parsePrologValue, parsePropertyList, splitTopLevelGeneral, } from "kibi-cli/prolog/codec";
|
|
5
|
-
import { resolveAuditLogPath } from "./idle-brief-paths.js";
|
|
6
|
-
function asOptionalString(value) {
|
|
7
|
-
return typeof value === "string" ? value : undefined;
|
|
8
|
-
}
|
|
9
|
-
// Parse a single changeset line from the audit log
|
|
10
|
-
function parseChangesetLine(line) {
|
|
11
|
-
const trimmedLine = line.trim();
|
|
12
|
-
if (!trimmedLine.startsWith("changeset(") || !trimmedLine.endsWith(").")) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
const argsLiteral = trimmedLine.slice("changeset(".length, -2);
|
|
16
|
-
const parts = splitTopLevelGeneral(argsLiteral, ",").map((part) => part.trim());
|
|
17
|
-
if (parts.length < 4) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
const timestamp = parsePrologValue(parts[0] ?? "");
|
|
21
|
-
const operation = parsePrologValue(parts[1] ?? "");
|
|
22
|
-
const entityId = parsePrologValue(parts[2] ?? "");
|
|
23
|
-
if (typeof timestamp !== "string" ||
|
|
24
|
-
typeof operation !== "string" ||
|
|
25
|
-
typeof entityId !== "string") {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
const rawPayload = parts.slice(3).join(",");
|
|
29
|
-
const payload = parsePayload(rawPayload.trim());
|
|
30
|
-
return {
|
|
31
|
-
timestamp,
|
|
32
|
-
operation,
|
|
33
|
-
entityId,
|
|
34
|
-
...(payload === undefined ? {} : { payload }),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
function parsePayload(rawPayload) {
|
|
38
|
-
if (rawPayload === "null")
|
|
39
|
-
return null;
|
|
40
|
-
const match = rawPayload.match(/^([A-Za-z0-9_]+)-(.+)$/s);
|
|
41
|
-
if (!match)
|
|
42
|
-
return null;
|
|
43
|
-
const [, payloadType = "unknown", rawProps = ""] = match;
|
|
44
|
-
const properties = parsePropertyList(rawProps);
|
|
45
|
-
if (payloadType === "rel") {
|
|
46
|
-
return {
|
|
47
|
-
kind: "relationship",
|
|
48
|
-
relationshipType: payloadType,
|
|
49
|
-
properties,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
const title = asOptionalString(properties.title);
|
|
53
|
-
const source = asOptionalString(properties.source);
|
|
54
|
-
const textRef = asOptionalString(properties.text_ref);
|
|
55
|
-
const changeKindRaw = properties.change_kind;
|
|
56
|
-
const changeKind = changeKindRaw === "created" || changeKindRaw === "updated"
|
|
57
|
-
? changeKindRaw
|
|
58
|
-
: undefined;
|
|
59
|
-
return {
|
|
60
|
-
kind: "entity",
|
|
61
|
-
entityType: payloadType,
|
|
62
|
-
...(changeKind ? { changeKind } : {}),
|
|
63
|
-
...(title ? { title } : {}),
|
|
64
|
-
...(source ? { source } : {}),
|
|
65
|
-
...(textRef ? { textRef } : {}),
|
|
66
|
-
properties,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
// implements REQ-opencode-kibi-briefing-v4
|
|
70
|
-
// Read audit log and compute delta since last cursor
|
|
71
|
-
export function computeAuditDelta(workspaceRoot, branch, previousCursor) {
|
|
72
|
-
const auditPath = resolveAuditLogPath(workspaceRoot, branch);
|
|
73
|
-
if (!fs.existsSync(auditPath)) {
|
|
74
|
-
return {
|
|
75
|
-
hasChanges: false,
|
|
76
|
-
entries: [],
|
|
77
|
-
newCursor: previousCursor ?? {
|
|
78
|
-
lastTimestamp: "",
|
|
79
|
-
lastOperation: "",
|
|
80
|
-
entryCount: 0,
|
|
81
|
-
fileSize: 0,
|
|
82
|
-
},
|
|
83
|
-
contentHash: "",
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
const content = fs.readFileSync(auditPath, "utf-8");
|
|
87
|
-
const lines = content
|
|
88
|
-
.split("\n")
|
|
89
|
-
.filter((l) => l.trim().includes("changeset("));
|
|
90
|
-
const fileSize = Buffer.byteLength(content, "utf-8");
|
|
91
|
-
// If no previous cursor or file hasn't grown, check if content changed
|
|
92
|
-
if (previousCursor &&
|
|
93
|
-
fileSize === previousCursor.fileSize &&
|
|
94
|
-
lines.length === previousCursor.entryCount) {
|
|
95
|
-
return {
|
|
96
|
-
hasChanges: false,
|
|
97
|
-
entries: [],
|
|
98
|
-
newCursor: previousCursor,
|
|
99
|
-
contentHash: computeSimpleHash(lines),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
// Parse all entries
|
|
103
|
-
const entries = lines
|
|
104
|
-
.map(parseChangesetLine)
|
|
105
|
-
.filter((e) => e !== null)
|
|
106
|
-
.filter((e) => ["upsert", "upsert_rel", "delete"].includes(e.operation));
|
|
107
|
-
// If we have a previous cursor, filter to only new entries
|
|
108
|
-
let newEntries = entries;
|
|
109
|
-
if (previousCursor?.lastTimestamp) {
|
|
110
|
-
const lastIdx = entries.findIndex((e) => e.timestamp === previousCursor.lastTimestamp &&
|
|
111
|
-
e.operation === previousCursor.lastOperation);
|
|
112
|
-
if (lastIdx >= 0) {
|
|
113
|
-
newEntries = entries.slice(lastIdx + 1);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const lastEntry = entries[entries.length - 1];
|
|
117
|
-
const newCursor = {
|
|
118
|
-
lastTimestamp: lastEntry?.timestamp ?? "",
|
|
119
|
-
lastOperation: lastEntry?.operation ?? "",
|
|
120
|
-
entryCount: lines.length,
|
|
121
|
-
fileSize,
|
|
122
|
-
};
|
|
123
|
-
return {
|
|
124
|
-
hasChanges: newEntries.length > 0,
|
|
125
|
-
entries: newEntries,
|
|
126
|
-
newCursor,
|
|
127
|
-
contentHash: computeSimpleHash(lines),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
function computeSimpleHash(lines) {
|
|
131
|
-
return crypto
|
|
132
|
-
.createHash("sha256")
|
|
133
|
-
.update(lines.join("\n"))
|
|
134
|
-
.digest("hex")
|
|
135
|
-
.slice(0, 16);
|
|
136
|
-
}
|
|
137
|
-
// implements REQ-opencode-kibi-briefing-v4
|
|
138
|
-
// Extract the latest audit cursor from the most recent brief for this branch
|
|
139
|
-
export function getLatestAuditCursor(workspaceRoot, branch) {
|
|
140
|
-
// Read .kb/briefs/ directory and find the latest brief for this branch
|
|
141
|
-
const briefsDir = path.join(workspaceRoot, ".kb", "briefs");
|
|
142
|
-
if (!fs.existsSync(briefsDir))
|
|
143
|
-
return null;
|
|
144
|
-
const files = fs
|
|
145
|
-
.readdirSync(briefsDir)
|
|
146
|
-
.filter((f) => f.endsWith("_brief.json") && !f.endsWith(".tmp"))
|
|
147
|
-
.map((f) => {
|
|
148
|
-
const fullPath = path.join(briefsDir, f);
|
|
149
|
-
const [rawTimestamp = "0"] = f.split("_");
|
|
150
|
-
const timestamp = Number.parseInt(rawTimestamp, 10);
|
|
151
|
-
return {
|
|
152
|
-
path: fullPath,
|
|
153
|
-
timestamp: Number.isNaN(timestamp) ? 0 : timestamp,
|
|
154
|
-
};
|
|
155
|
-
})
|
|
156
|
-
.sort((a, b) => b.timestamp - a.timestamp);
|
|
157
|
-
for (const file of files) {
|
|
158
|
-
try {
|
|
159
|
-
const brief = JSON.parse(fs.readFileSync(file.path, "utf-8"));
|
|
160
|
-
if (brief.branch === branch && brief.auditCursor) {
|
|
161
|
-
return brief.auditCursor;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
// skip invalid JSON
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
export function getAuditTailCursor(
|
|
171
|
-
// implements REQ-opencode-kibi-briefing-v6
|
|
172
|
-
workspaceRoot, branch) {
|
|
173
|
-
const auditPath = resolveAuditLogPath(workspaceRoot, branch);
|
|
174
|
-
if (!fs.existsSync(auditPath)) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
const delta = computeAuditDelta(workspaceRoot, branch, null);
|
|
178
|
-
return delta.newCursor.entryCount > 0 || delta.newCursor.fileSize > 0
|
|
179
|
-
? delta.newCursor
|
|
180
|
-
: null;
|
|
181
|
-
}
|
|
182
|
-
// implements REQ-opencode-kibi-briefing-v4
|
|
183
|
-
// Guard: abort if branch changed since idle-start
|
|
184
|
-
export function guardBranchChanged(startBranch, currentBranch) {
|
|
185
|
-
return startBranch !== currentBranch;
|
|
186
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export declare function resolveBriefsDir(workspaceRoot: string): string;
|
|
2
|
-
export declare function resolveAuditLogPath(workspaceRoot: string, branch: string): string;
|
|
3
|
-
export declare function resolveBriefFilePath(workspaceRoot: string, timestamp: number): string;
|
|
4
|
-
export declare function resolveTempBriefPath(workspaceRoot: string, timestamp: number): string;
|
|
5
|
-
export declare function atomicWriteBrief(workspaceRoot: string, timestamp: number, content: string): void;
|
|
6
|
-
export declare function pruneOldBriefs(workspaceRoot: string, branch: string): void;
|