kibi-mcp 0.14.2 → 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.
@@ -1,651 +0,0 @@
1
- import { rankEntities } from "kibi-cli/search-ranking";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { classifyActivationState, } from "./autopilot-discovery.js";
5
- import { runJsonModuleQuery, toPrologList } from "./core-module.js";
6
- import { loadEntities } from "./entity-query.js";
7
- import { handleKbStatus } from "./status.js";
8
- import { resolveWorkspaceRoot } from "../workspace.js";
9
- import { isOperationalArtifactPath } from "kibi-cli/operational-artifacts";
10
- import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
11
- import { getSchemaVersionStatus } from "kibi-cli/schema-version";
12
- const ALLOWED_TYPES = [
13
- "req",
14
- "adr",
15
- "scenario",
16
- "test",
17
- "fact",
18
- "flag",
19
- "symbol",
20
- ];
21
- const TYPE_PRIORITY = {
22
- req: 7,
23
- adr: 6,
24
- scenario: 5,
25
- test: 4,
26
- fact: 3,
27
- flag: 2,
28
- symbol: 1,
29
- };
30
- const GRAPH_RELATIONSHIPS = [
31
- "implements",
32
- "covered_by",
33
- "specified_by",
34
- "verified_by",
35
- "constrained_by",
36
- "constrains",
37
- "requires_property",
38
- "guards",
39
- "relates_to",
40
- ];
41
- function activationReasonFor(state) {
42
- switch (state) {
43
- case "vendored_only":
44
- return "Workspace appears to contain vendored Kibi sources only; briefing generation is disabled.";
45
- case "root_partial":
46
- return "Workspace root is partially configured; briefing generation is disabled until Kibi inputs fully resolve.";
47
- case "root_active_seeded":
48
- return "KB attached and ready for citation-backed briefing generation in a seeded workspace.";
49
- case "root_active_thin":
50
- return "KB attached but thin; briefing generation may lack enough evidence.";
51
- default:
52
- return "Workspace root is not fully initialized; briefing generation is disabled until Kibi is attached.";
53
- }
54
- }
55
- function inferTextOnlyActivationState(workspaceRoot) {
56
- try {
57
- const vendoredMarkers = [
58
- ["kibi", "opencode.json"],
59
- ["kibi", "package.json"],
60
- ["kibi", "packages", "mcp"],
61
- ["kibi", "documentation"],
62
- ];
63
- const hasVendoredTree = vendoredMarkers.some((segments) => pathExists(path.join(workspaceRoot, ...segments)));
64
- const hasRootConfig = pathExists(path.join(workspaceRoot, ".kb", "config.json"));
65
- if (!hasRootConfig && hasVendoredTree) {
66
- return "vendored_only";
67
- }
68
- if (!hasRootConfig) {
69
- return "root_uninitialized";
70
- }
71
- }
72
- catch {
73
- // Fall through to the conservative thin state below.
74
- }
75
- return "root_active_thin";
76
- }
77
- function pathExists(candidatePath) {
78
- try {
79
- return fs.existsSync(candidatePath);
80
- }
81
- catch {
82
- return false;
83
- }
84
- }
85
- function unknownFreshness() {
86
- return {
87
- state: "unknown",
88
- syncState: "unknown",
89
- dirty: false,
90
- syncedAt: null,
91
- };
92
- }
93
- function normalizeTaskText(taskText) {
94
- return (taskText ?? "").trim();
95
- }
96
- function normalizeSeedIds(seedIds) {
97
- const normalized = [];
98
- const seen = new Set();
99
- for (const seedId of seedIds ?? []) {
100
- const trimmed = String(seedId ?? "").trim();
101
- if (!trimmed || seen.has(trimmed))
102
- continue;
103
- seen.add(trimmed);
104
- normalized.push(trimmed);
105
- }
106
- return normalized;
107
- }
108
- function normalizeSourceFiles(workspaceRoot, sourceFiles) {
109
- const normalized = [];
110
- const seen = new Set();
111
- const normalizedRoot = path.resolve(workspaceRoot);
112
- const policy = createRepoIgnorePolicy(workspaceRoot);
113
- for (const sourceFile of sourceFiles ?? []) {
114
- const trimmed = String(sourceFile ?? "").trim();
115
- if (!trimmed)
116
- continue;
117
- const candidateAbsolute = path.resolve(path.isAbsolute(trimmed)
118
- ? trimmed
119
- : path.join(normalizedRoot, trimmed));
120
- const relative = path.relative(normalizedRoot, candidateAbsolute);
121
- const repoRelative = !relative.startsWith("..") && !path.isAbsolute(relative)
122
- ? relative
123
- : trimmed;
124
- const normalizedPath = repoRelative
125
- .split(path.sep)
126
- .join("/")
127
- .replace(/^\.\//, "")
128
- .replace(/^\//, "");
129
- if (!normalizedPath || seen.has(normalizedPath) || isOperationalArtifactPath(normalizedPath))
130
- continue;
131
- // Skip paths ignored by repository ignore policy (eg .gitignore, .git/info/exclude, nested .gitignore,
132
- // and hard denylist such as .kb, .git, node_modules, .sisyphus)
133
- try {
134
- if (policy.isIgnored(normalizedPath))
135
- continue;
136
- }
137
- catch {
138
- // Be conservative on errors and do not let ignore policy break briefing generation;
139
- // fall through and allow the path unless other checks exclude it.
140
- }
141
- seen.add(normalizedPath);
142
- normalized.push(normalizedPath);
143
- }
144
- return normalized;
145
- }
146
- function isAllowedType(type) {
147
- return ALLOWED_TYPES.includes(type);
148
- }
149
- function stripOuterSingleQuotes(value) {
150
- return value.startsWith("'") && value.endsWith("'") && value.length >= 2
151
- ? value.slice(1, -1)
152
- : value;
153
- }
154
- function candidateKey(entity) {
155
- return `${String(entity.type ?? "")}::${String(entity.id ?? "")}`;
156
- }
157
- function normalizeEntity(entity, workspaceRoot) {
158
- const type = stripOuterSingleQuotes(String(entity.type ?? "").trim());
159
- if (!isAllowedType(type))
160
- return null;
161
- const source = entity.source ? String(entity.source).trim().split(path.sep).join("/") : undefined;
162
- if (source && isOperationalArtifactPath(source))
163
- return null;
164
- // Respect repository ignore policy for entity sources. Prefer an explicit workspaceRoot when available.
165
- try {
166
- if (source) {
167
- const policyRoot = workspaceRoot ?? process.cwd();
168
- const policy = createRepoIgnorePolicy(policyRoot);
169
- if (policy.isIgnored(source))
170
- return null;
171
- }
172
- }
173
- catch {
174
- // Ignore errors from ignore policy to avoid blocking normalization on policy issues.
175
- }
176
- return {
177
- ...entity,
178
- id: String(entity.id ?? "").trim(),
179
- type,
180
- title: String(entity.title ?? "").trim(),
181
- status: String(entity.status ?? "").trim(),
182
- source,
183
- textRef: entity.textRef
184
- ? String(entity.textRef).trim()
185
- : entity.text_ref
186
- ? String(entity.text_ref).trim()
187
- : undefined,
188
- };
189
- }
190
- function addCandidate(candidates, entity, scoreDelta, reason, workspaceRoot) {
191
- const normalizedEntity = normalizeEntity(entity, workspaceRoot);
192
- if (!normalizedEntity)
193
- return;
194
- const key = candidateKey(normalizedEntity);
195
- const existing = candidates.get(key);
196
- if (existing) {
197
- existing.score += scoreDelta;
198
- if (!existing.reasons.includes(reason)) {
199
- existing.reasons.push(reason);
200
- }
201
- return;
202
- }
203
- candidates.set(key, {
204
- entity: normalizedEntity,
205
- score: scoreDelta,
206
- reasons: [reason],
207
- });
208
- }
209
- function toFreshness(statusPayload) {
210
- const syncState = String(statusPayload.syncState ?? "unknown");
211
- if (statusPayload.dirty || syncState === "stale") {
212
- return {
213
- state: "stale",
214
- syncState,
215
- dirty: Boolean(statusPayload.dirty),
216
- syncedAt: statusPayload.syncedAt ?? null,
217
- };
218
- }
219
- if (syncState === "fresh") {
220
- return {
221
- state: "fresh",
222
- syncState,
223
- dirty: Boolean(statusPayload.dirty),
224
- syncedAt: statusPayload.syncedAt ?? null,
225
- };
226
- }
227
- return {
228
- state: "unknown",
229
- syncState,
230
- dirty: Boolean(statusPayload.dirty),
231
- syncedAt: statusPayload.syncedAt ?? null,
232
- };
233
- }
234
- function summarizeCandidateReason(reasons) {
235
- return reasons.join(", ");
236
- }
237
- function sortedEntities(candidates) {
238
- return Array.from(candidates.values())
239
- .map(({ entity, score, reasons }) => {
240
- const type = String(entity.type);
241
- const typeBonus = TYPE_PRIORITY[type] ?? 0;
242
- return {
243
- id: String(entity.id ?? ""),
244
- type,
245
- title: String(entity.title ?? ""),
246
- status: String(entity.status ?? ""),
247
- ...(entity.source ? { source: String(entity.source) } : {}),
248
- ...(entity.textRef ? { textRef: String(entity.textRef) } : {}),
249
- score: score + typeBonus,
250
- reason: summarizeCandidateReason(reasons),
251
- };
252
- })
253
- .sort((left, right) => {
254
- if (right.score !== left.score) {
255
- return right.score - left.score;
256
- }
257
- const leftPriority = TYPE_PRIORITY[left.type] ?? 0;
258
- const rightPriority = TYPE_PRIORITY[right.type] ?? 0;
259
- if (rightPriority !== leftPriority) {
260
- return rightPriority - leftPriority;
261
- }
262
- return left.id.localeCompare(right.id);
263
- })
264
- .slice(0, 8);
265
- }
266
- function selectCitationIds(entities, predicate) {
267
- return entities.filter(predicate).map((entity) => entity.id);
268
- }
269
- function asSearchableText(entity) {
270
- return `${entity.title} ${entity.source ?? ""} ${entity.textRef ?? ""}`.toLowerCase();
271
- }
272
- function buildConstraints(entities) {
273
- const statements = [];
274
- const adrCitationIds = selectCitationIds(entities, (entity) => entity.type === "adr" &&
275
- asSearchableText(entity).includes("read-only") &&
276
- asSearchableText(entity).includes("mcp"));
277
- if (adrCitationIds.length > 0) {
278
- statements.push({
279
- statement: "Keep the briefing generator read-only and MCP-owned.",
280
- citationIds: adrCitationIds,
281
- });
282
- }
283
- const deterministicCitationIds = selectCitationIds(entities, (entity) => {
284
- if (entity.type !== "req" && entity.type !== "test")
285
- return false;
286
- const searchable = asSearchableText(entity);
287
- return searchable.includes("deterministic") || searchable.includes("citation");
288
- });
289
- if (deterministicCitationIds.length > 0) {
290
- statements.push({
291
- statement: "Return deterministic, citation-backed start-task output.",
292
- citationIds: deterministicCitationIds,
293
- });
294
- }
295
- return statements;
296
- }
297
- function buildRegressionRisks(entities) {
298
- const statements = [];
299
- const orderingCitationIds = selectCitationIds(entities, (entity) => entity.type === "test" &&
300
- (asSearchableText(entity).includes("deterministic") ||
301
- asSearchableText(entity).includes("briefing output")));
302
- if (orderingCitationIds.length > 0) {
303
- statements.push({
304
- statement: "Do not let repeated calls change entity, citation, or prompt ordering.",
305
- citationIds: orderingCitationIds,
306
- });
307
- }
308
- const budgetCitationIds = selectCitationIds(entities, (entity) => entity.type === "fact" &&
309
- (asSearchableText(entity).includes("prompt") ||
310
- asSearchableText(entity).includes("budget")));
311
- if (budgetCitationIds.length > 0) {
312
- statements.push({
313
- statement: "Do not exceed the OpenCode prompt budget.",
314
- citationIds: budgetCitationIds,
315
- });
316
- }
317
- return statements;
318
- }
319
- function buildMissingEvidence(_entities) {
320
- return [];
321
- }
322
- function bulletForEntity(entity) {
323
- const searchable = asSearchableText(entity);
324
- if (entity.type === "req" &&
325
- searchable.includes("deterministic") &&
326
- searchable.includes("citation")) {
327
- return `- ${entity.id}: Keep start-task briefings deterministic and citation-backed.`;
328
- }
329
- if (entity.type === "adr" &&
330
- searchable.includes("read-only") &&
331
- searchable.includes("mcp")) {
332
- return `- ${entity.id}: Keep the MCP tool read-only; do not repair or mutate the workspace.`;
333
- }
334
- if (entity.type === "test" &&
335
- (searchable.includes("deterministic") || searchable.includes("briefing output"))) {
336
- return `- ${entity.id}: Repeated calls must preserve entity, citation, and prompt ordering.`;
337
- }
338
- if (entity.type === "fact" &&
339
- (searchable.includes("prompt") || searchable.includes("budget"))) {
340
- return `- ${entity.id}: Keep the prompt block within 120 words and 5 bullets.`;
341
- }
342
- return null;
343
- }
344
- function buildPromptBlock(entities) {
345
- if (entities.length === 0) {
346
- return "";
347
- }
348
- const allBullets = entities
349
- .map((entity) => bulletForEntity(entity))
350
- .filter((bullet) => bullet !== null);
351
- if (allBullets.length === 0) {
352
- return "";
353
- }
354
- const bullets = allBullets.slice(0, 5);
355
- let promptBlock = bullets.join("\n");
356
- const words = promptBlock.split(/\s+/).filter(Boolean);
357
- if (words.length > 120) {
358
- // Hard-truncate to 120 words, preserving whole bullets where possible
359
- const truncated = [];
360
- let wordCount = 0;
361
- for (const bullet of bullets) {
362
- const bulletWords = bullet.split(/\s+/).filter(Boolean);
363
- if (wordCount + bulletWords.length > 120) {
364
- // Take a partial bullet that fits within budget
365
- const remaining = 120 - wordCount;
366
- if (remaining > 3) {
367
- truncated.push(`${bulletWords.slice(0, remaining).join(" ")}\u2026`);
368
- }
369
- break;
370
- }
371
- truncated.push(bullet);
372
- wordCount += bulletWords.length;
373
- }
374
- promptBlock = truncated.join("\n");
375
- }
376
- return promptBlock;
377
- }
378
- function buildCitations(entities) {
379
- return entities
380
- .filter((entity) => {
381
- if (entity.source && isOperationalArtifactPath(entity.source))
382
- return false;
383
- if (entity.textRef && isOperationalArtifactPath(entity.textRef))
384
- return false;
385
- return true;
386
- })
387
- .map((entity) => ({
388
- id: entity.id,
389
- type: entity.type,
390
- title: entity.title,
391
- ...(entity.source ? { source: entity.source } : {}),
392
- ...(entity.textRef ? { textRef: entity.textRef } : {}),
393
- }));
394
- }
395
- function buildAutomationReviewEntities(entities, confidenceScore) {
396
- const entityConfidence = typeof confidenceScore === "number" && Number.isFinite(confidenceScore)
397
- ? roundScore(confidenceScore)
398
- : 1;
399
- return entities.map((entity) => ({
400
- id: entity.id,
401
- type: entity.type,
402
- title: entity.title,
403
- confidence: entityConfidence,
404
- }));
405
- }
406
- function buildAutomationReview(entities, confidence, migrationWarning, prologNeighbors) {
407
- if (entities.length === 0) {
408
- return null;
409
- }
410
- const generatedEntities = buildAutomationReviewEntities(entities, confidence.score);
411
- const evidenceCitationIds = entities.map((entity) => entity.id);
412
- const strictEntities = entities.filter((entity) => entity.type === "req" || entity.type === "fact");
413
- const strictReadinessScore = roundScore(strictEntities.length > 0
414
- ? Math.min(1, strictEntities.length / entities.length + 0.5)
415
- : 0);
416
- const contradictionRisks = [];
417
- for (const entity of strictEntities) {
418
- if (entity.type === "req" && prologNeighbors.has(entity.id)) {
419
- contradictionRisks.push(`${entity.id} may have contradiction overlap with related requirements.`);
420
- }
421
- }
422
- const migrationWarnings = [];
423
- if (migrationWarning) {
424
- migrationWarnings.push(migrationWarning);
425
- }
426
- return {
427
- generatedEntities,
428
- strictReadinessScore,
429
- confidence: confidence.score,
430
- migrationWarnings,
431
- contradictionRisks,
432
- evidenceCitationIds,
433
- };
434
- }
435
- function roundScore(score) {
436
- return Math.max(0, Math.min(1, Math.round(score * 100) / 100));
437
- }
438
- function buildConfidence(activationState, freshness, entities, missingEvidence, promptBlock) {
439
- const reasons = [];
440
- let score = entities.length > 0 ? 0.82 : 0.35;
441
- if (activationState === "root_active_seeded") {
442
- score += 0.13;
443
- reasons.push("Seeded workspace provides broad KB evidence.");
444
- }
445
- else if (activationState === "root_active_thin") {
446
- score -= 0.2;
447
- reasons.push("Thin workspace reduces available evidence.");
448
- }
449
- else {
450
- score -= 0.4;
451
- reasons.push("Workspace posture does not support reliable briefing generation.");
452
- }
453
- if (freshness.state !== "fresh") {
454
- score -= 0.25;
455
- reasons.push("KB freshness is not clean enough for a ready briefing.");
456
- }
457
- if (freshness.dirty) {
458
- score -= 0.1;
459
- reasons.push("Workspace is dirty, so citations may be stale.");
460
- }
461
- if (missingEvidence.length > 0) {
462
- score -= 0.15;
463
- reasons.push("Some briefing claims are missing supporting evidence.");
464
- }
465
- if (!promptBlock) {
466
- score -= 0.1;
467
- reasons.push("Prompt block could not be assembled within the prompt budget.");
468
- }
469
- const rounded = roundScore(score);
470
- return {
471
- score: rounded,
472
- level: rounded >= 0.8 ? "high" : rounded >= 0.55 ? "medium" : "low",
473
- reasons,
474
- };
475
- }
476
- async function expandGraphNeighbors(prolog, seedIds, workspaceRoot) {
477
- if (seedIds.length === 0) {
478
- return new Map();
479
- }
480
- const payload = await runJsonModuleQuery(prolog, "discovery.pl", `discovery:graph_expand_json(${toPrologList(seedIds)}, ${toPrologList(GRAPH_RELATIONSHIPS)}, 'both', 1, [], 200, 500, JsonString)`, "Briefing graph expansion");
481
- const seedSet = new Set(seedIds);
482
- const connected = new Set();
483
- for (const edge of payload.edges ?? []) {
484
- const from = String(edge.from ?? "");
485
- const to = String(edge.to ?? "");
486
- if (seedSet.has(from) && to)
487
- connected.add(to);
488
- if (seedSet.has(to) && from)
489
- connected.add(from);
490
- }
491
- const neighbors = new Map();
492
- for (const node of payload.nodes ?? []) {
493
- const normalized = normalizeEntity(node, workspaceRoot);
494
- if (!normalized)
495
- continue;
496
- const nodeId = String(normalized.id ?? "");
497
- if (seedSet.has(nodeId) || !connected.has(nodeId))
498
- continue;
499
- neighbors.set(nodeId, normalized);
500
- }
501
- return neighbors;
502
- }
503
- async function loadByIds(prolog, ids) {
504
- const groups = await Promise.all(ids.map((id) => loadEntities(prolog, { id })));
505
- return groups.flat();
506
- }
507
- async function loadBySourceFiles(prolog, sourceFiles) {
508
- const groups = await Promise.all(sourceFiles.map((sourceFile) => loadEntities(prolog, { sourceFile })));
509
- return groups.flat();
510
- }
511
- function buildTldr(briefingState, entities) {
512
- if (briefingState === "ready") {
513
- const citedIds = entities.slice(0, 4).map((entity) => entity.id).join(", ");
514
- return `Ready briefing assembled from ${entities.length} cited entities: ${citedIds}.`;
515
- }
516
- return "No reliable briefing is available from the current workspace posture and freshness state.";
517
- }
518
- export async function handleKbBriefingGenerate(// implements REQ-mcp-kibi-briefing-v1
519
- prolog, args) {
520
- const workspaceRoot = resolveWorkspaceRoot();
521
- const taskText = normalizeTaskText(args.taskText);
522
- const sourceFiles = normalizeSourceFiles(workspaceRoot, args.sourceFiles);
523
- const seedIds = normalizeSeedIds(args.seedIds);
524
- if (!taskText && sourceFiles.length === 0 && seedIds.length === 0) {
525
- throw new Error("Briefing generation failed: at least one of taskText, sourceFiles, or seedIds must be provided");
526
- }
527
- const useTextOnlyFastPath = taskText.length > 0 && sourceFiles.length === 0 && seedIds.length === 0;
528
- const activationState = useTextOnlyFastPath
529
- ? inferTextOnlyActivationState(workspaceRoot)
530
- : await classifyActivationState(workspaceRoot, prolog);
531
- const activationReason = activationReasonFor(activationState);
532
- const freshness = useTextOnlyFastPath
533
- ? unknownFreshness()
534
- : toFreshness((await handleKbStatus(prolog, {})).structuredContent);
535
- if (activationState === "root_uninitialized" ||
536
- activationState === "root_partial" ||
537
- activationState === "vendored_only" ||
538
- freshness.state === "stale") {
539
- const confidence = buildConfidence(activationState, freshness, [], [], "");
540
- return {
541
- content: [{ type: "text", text: "No briefing is available." }],
542
- structuredContent: {
543
- briefingState: "no_briefing",
544
- activationState,
545
- activationReason,
546
- freshness,
547
- confidence,
548
- tldr: buildTldr("no_briefing", []),
549
- promptBlock: "",
550
- entities: [],
551
- constraints: [],
552
- regressionRisks: [],
553
- missingEvidence: [],
554
- citations: [],
555
- automationReview: null,
556
- },
557
- };
558
- }
559
- const candidates = new Map();
560
- for (const entity of await loadByIds(prolog, seedIds)) {
561
- addCandidate(candidates, entity, 100, "seed hit", workspaceRoot);
562
- }
563
- for (const entity of await loadBySourceFiles(prolog, sourceFiles)) {
564
- addCandidate(candidates, entity, 90, "source-file hit", workspaceRoot);
565
- }
566
- const rankedIds = [];
567
- if (taskText) {
568
- const allEntities = (await loadEntities(prolog, {})).filter((entity) => isAllowedType(String(entity.type ?? "")));
569
- const matches = await rankEntities(allEntities, taskText, workspaceRoot);
570
- matches.forEach((match, index) => {
571
- rankedIds.push(String(match.entity.id ?? ""));
572
- addCandidate(candidates, match.entity, 70 - index, `text-search hit (#${index + 1})`, workspaceRoot);
573
- });
574
- }
575
- const graphSeeds = Array.from(new Set([
576
- ...seedIds,
577
- ...sourceFiles.flatMap(() => []),
578
- ...Array.from(candidates.values()).map((candidate) => String(candidate.entity.id ?? "")),
579
- ...rankedIds,
580
- ].filter(Boolean)));
581
- const graphNeighbors = await expandGraphNeighbors(prolog, graphSeeds, workspaceRoot);
582
- for (const neighbor of graphNeighbors.values()) {
583
- addCandidate(candidates, neighbor, 40, "graph neighbor", workspaceRoot);
584
- }
585
- const entities = sortedEntities(candidates);
586
- const constraints = buildConstraints(entities);
587
- const regressionRisks = buildRegressionRisks(entities);
588
- const missingEvidence = buildMissingEvidence(entities);
589
- const promptBlock = buildPromptBlock(entities);
590
- const citations = buildCitations(entities);
591
- const confidence = buildConfidence(activationState, freshness, entities, missingEvidence, promptBlock);
592
- const briefingState = confidence.score >= 0.55 ? "ready" : "no_briefing";
593
- // Compute automation review metadata
594
- let migrationWarning = null;
595
- try {
596
- const configPath = path.join(workspaceRoot, ".kb", "config.json");
597
- let rawConfig;
598
- try {
599
- rawConfig = fs.readFileSync(configPath, "utf8");
600
- const parsed = JSON.parse(rawConfig);
601
- const schemaStatus = getSchemaVersionStatus(parsed ?? undefined);
602
- migrationWarning = schemaStatus.warning;
603
- }
604
- catch {
605
- migrationWarning = null;
606
- }
607
- }
608
- catch {
609
- migrationWarning = null;
610
- }
611
- const graphNeighborIds = new Set(graphNeighbors.keys());
612
- const automationReview = buildAutomationReview(entities, confidence, migrationWarning, graphNeighborIds);
613
- if (briefingState === "no_briefing") {
614
- return {
615
- content: [{ type: "text", text: "No briefing is available." }],
616
- structuredContent: {
617
- briefingState,
618
- activationState,
619
- activationReason,
620
- freshness,
621
- confidence,
622
- tldr: buildTldr("no_briefing", []),
623
- promptBlock: "",
624
- entities: [],
625
- constraints: [],
626
- regressionRisks: [],
627
- missingEvidence: [],
628
- citations: [],
629
- automationReview: null,
630
- },
631
- };
632
- }
633
- return {
634
- content: [{ type: "text", text: `Briefing ready with ${entities.length} cited entities.` }],
635
- structuredContent: {
636
- briefingState,
637
- activationState,
638
- activationReason,
639
- freshness,
640
- confidence,
641
- tldr: buildTldr(briefingState, entities),
642
- promptBlock,
643
- entities,
644
- constraints,
645
- regressionRisks,
646
- missingEvidence,
647
- citations,
648
- automationReview,
649
- },
650
- };
651
- }