kibi-mcp 0.17.4 → 0.19.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.
@@ -0,0 +1,159 @@
1
+ import { createHash } from "node:crypto";
2
+ const SECRET_LIKE_KEY_RE = /(token|secret|password|authorization|api[_-]?key|bearer)/i;
3
+ const DIAGNOSTIC_PHASE_BY_STAGE = {
4
+ persistence: "persistence",
5
+ prolog_runtime: "runtime",
6
+ prolog_lifecycle: "runtime",
7
+ tool_timeout: "runtime",
8
+ validation: "validation",
9
+ handler: "handler",
10
+ };
11
+ function isPlainObject(value) {
12
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+ function redactSecretLikeValue(value) {
15
+ if (Array.isArray(value))
16
+ return value.map(redactSecretLikeValue);
17
+ if (!isPlainObject(value))
18
+ return value;
19
+ const redacted = {};
20
+ for (const [key, nestedValue] of Object.entries(value)) {
21
+ redacted[key] = SECRET_LIKE_KEY_RE.test(key)
22
+ ? "[REDACTED]"
23
+ : redactSecretLikeValue(nestedValue);
24
+ }
25
+ return redacted;
26
+ }
27
+ function canonicalizeJsonValue(value) {
28
+ if (Array.isArray(value))
29
+ return value.map(canonicalizeJsonValue);
30
+ if (!isPlainObject(value))
31
+ return value;
32
+ return Object.fromEntries(Object.keys(value)
33
+ .sort()
34
+ .map((key) => [key, canonicalizeJsonValue(value[key])]));
35
+ }
36
+ function stableJsonStringify(value) {
37
+ return JSON.stringify(canonicalizeJsonValue(value));
38
+ }
39
+ export function redactDiagnosticArgs(args) {
40
+ return redactSecretLikeValue(args);
41
+ }
42
+ export function deriveDiagnosticRetryKey(toolName, args) {
43
+ const hash = createHash("sha256")
44
+ .update(toolName)
45
+ .update("\0")
46
+ .update(stableJsonStringify(redactDiagnosticArgs(args)))
47
+ .digest("hex");
48
+ return `retry_${hash.slice(0, 16)}`;
49
+ }
50
+ export function classifyDiagnosticError(error) {
51
+ const err = error instanceof Error ? error : new Error(String(error));
52
+ const lower = err.message.toLowerCase();
53
+ if (lower.includes("stale_snapshot"))
54
+ return buildErrorFields(err, "stale_snapshot", "persistence", "KB snapshot is stale; refresh/retry after the latest KB state is attached.");
55
+ if (lower.includes("unknown option") && lower.includes("h for help"))
56
+ return buildErrorFields(err, "prolog_unknown_option", "prolog_runtime", "Prolog rejected startup/module/query options; inspect MCP package wiring and Prolog invocation.");
57
+ if (lower.includes("prolog process not started"))
58
+ return buildErrorFields(err, "prolog_process_not_started", "prolog_lifecycle", "Prolog process is unavailable; restart the MCP server before retrying.");
59
+ if (lower.includes("resetting prolog worker") ||
60
+ lower.includes("prolog worker reset"))
61
+ return buildErrorFields(err, "prolog_worker_reset", "prolog_lifecycle", "Prolog worker was reset so the next MCP call can start from a fresh worker.");
62
+ if (/timed out after \d+ms/i.test(err.message) ||
63
+ lower.includes("tool timeout"))
64
+ return buildErrorFields(err, "tool_timeout", "tool_timeout", "MCP tool execution exceeded its bounded timeout.");
65
+ if (lower.includes("coarsely while granular symbols are available"))
66
+ return buildErrorFields(err, "coarse_symbol_linkage", "validation", "Symbol traceability targeted a coarse file/module while narrower exported symbols exist.");
67
+ if (err.message.startsWith("Entity validation failed:"))
68
+ return buildErrorFields(err, "entity_validation_failed", "validation", "Entity payload failed schema validation.");
69
+ if (err.message.startsWith("Relationship validation failed"))
70
+ return buildErrorFields(err, "relationship_validation_failed", "validation", "Relationship payload failed schema validation.");
71
+ if (err.message.startsWith("Relationship source must match the upserted entity"))
72
+ return buildErrorFields(err, "relationship_source_mismatch", "validation", "Relationship source did not match the entity being upserted.");
73
+ if (lower.includes("contradiction detected for requirement")) {
74
+ const fields = buildErrorFields(err, "semantic_contradiction", "validation", "Requirement prose/facts contradict existing contradiction-ready requirements.");
75
+ return {
76
+ ...fields,
77
+ semantic_outcome: "conflict-blocked",
78
+ ...semanticContradictionIds(err.message),
79
+ };
80
+ }
81
+ if (lower.includes("module load failed"))
82
+ return buildErrorFields(err, "prolog_module_load_failed", "prolog_runtime", "Prolog failed to load an execution module.");
83
+ if (lower.includes("query failed"))
84
+ return buildErrorFields(err, "prolog_query_failed", "prolog_runtime", "Prolog query execution failed.");
85
+ return buildErrorFields(err, "handler_error", "handler", "Unhandled MCP handler error.");
86
+ }
87
+ function semanticContradictionIds(message) {
88
+ const checked = message.match(/Contradiction detected for requirement\s+([^:\s]+)/i);
89
+ const conflicts = [
90
+ ...message.matchAll(/Conflicts with\s+([^:\s]+)/gi),
91
+ ].flatMap((match) => (match[1] ? [match[1]] : []));
92
+ return {
93
+ ...(checked?.[1] ? { semantic_checked_req_id: checked[1] } : {}),
94
+ ...(conflicts.length > 0
95
+ ? { semantic_conflicting_req_ids: conflicts }
96
+ : {}),
97
+ };
98
+ }
99
+ export function deriveDiagnosticHints(input) {
100
+ const error = input.error instanceof Error ? input.error : new Error(String(input.error));
101
+ const message = error.message.toLowerCase();
102
+ if (message.includes("invalid value 'implemented'"))
103
+ return "invalid_status: use one of the schema-accepted statuses (for example open, active, accepted, passing).";
104
+ if (message.includes("additional properties"))
105
+ return "additional_properties: remove unsupported fields from the payload and retry with schema-only arguments.";
106
+ const { error_category, error_stage, error_summary } = classifyDiagnosticError(error);
107
+ const mappedStage = DIAGNOSTIC_PHASE_BY_STAGE[error_stage] ?? error_stage;
108
+ if (error_category === "tool_timeout")
109
+ return `tool_timeout ${mappedStage}: retry the same request after reducing payload size or scope.`;
110
+ if (error_category === "relationship_source_mismatch")
111
+ return `relationship_source_mismatch ${mappedStage}: align the relationship source with the upserted entity id.`;
112
+ if (error_category === "semantic_contradiction")
113
+ return `semantic_contradiction ${mappedStage}: add a supersedes relationship to the conflicting requirement, deprecate the older requirement, or align the modeled facts.`;
114
+ if (error_category === "prolog_process_not_started")
115
+ return `prolog_process_not_started ${mappedStage}: restart the MCP server before retrying the tool call.`;
116
+ if (error_category === "prolog_query_failed")
117
+ return `prolog_query_failed ${mappedStage}: inspect the Prolog query or workspace state and retry.`;
118
+ return `${error_category} ${mappedStage}: ${error_summary}`;
119
+ }
120
+ export function buildDiagnosticToolCall(input) {
121
+ const diagnosticError = input.error
122
+ ? classifyDiagnosticError(input.error)
123
+ : null;
124
+ const payload = isPlainObject(input.args)
125
+ ? extractToolCallPayload(input.args)
126
+ : { businessArgs: null, telemetry: null };
127
+ return {
128
+ schema_version: 1,
129
+ canonical_tool: input.tool,
130
+ request_id: input.requestId,
131
+ diagnostic_phase: input.diagnosticPhase,
132
+ business_args: payload.businessArgs,
133
+ raw_args_redacted: redactDiagnosticArgs(input.args),
134
+ retry_key: deriveDiagnosticRetryKey(input.tool, input.args),
135
+ hint: deriveDiagnosticHints({
136
+ tool: input.tool,
137
+ error: input.error ?? new Error("diagnostic tool call"),
138
+ businessArgs: payload.businessArgs,
139
+ }),
140
+ diagnostic_error: diagnosticError,
141
+ diagnostic_telemetry: input.telemetry ?? payload.telemetry,
142
+ };
143
+ }
144
+ export function extractToolCallPayload(args) {
145
+ const { _diagnostic_telemetry, ...businessArgs } = args;
146
+ const telemetry = _diagnostic_telemetry && typeof _diagnostic_telemetry === "object"
147
+ ? _diagnostic_telemetry
148
+ : null;
149
+ return { businessArgs, telemetry };
150
+ }
151
+ function buildErrorFields(error, category, stage, summary) {
152
+ return {
153
+ error_name: error.name,
154
+ error_message: error.message,
155
+ error_category: category,
156
+ error_stage: stage,
157
+ error_summary: summary,
158
+ };
159
+ }
@@ -1,39 +1,19 @@
1
1
  /*
2
- Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
- Copyright (C) 2026 Piotr Franczyk
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as published by
7
- the Free Software Foundation, either version 3 of the License, or
8
- (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <https://www.gnu.org/licenses/>.
2
+ * Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ * Copyright (C) 2026 Piotr Franczyk
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
17
9
  */
18
10
  import fs from "node:fs";
19
11
  import path from "node:path";
20
12
  import { resolveWorkspaceRoot } from "./workspace.js";
13
+ export { buildDiagnosticToolCall, classifyDiagnosticError, deriveDiagnosticHints, deriveDiagnosticRetryKey, extractToolCallPayload, redactDiagnosticArgs, } from "./diagnostics-helpers.js";
21
14
  const DIAGNOSTIC_MODE_FLAG = "--diagnostic-mode";
22
- /**
23
- * Whether diagnostic mode is enabled via CLI flag.
24
- * Set during server startup and never changes at runtime.
25
- */
26
15
  export const DIAGNOSTIC_MODE_ENABLED = process.argv.includes(DIAGNOSTIC_MODE_FLAG);
27
- /**
28
- * Path to the diagnostic usage log file.
29
- * Only valid when DIAGNOSTIC_MODE_ENABLED is true.
30
- */
31
16
  let diagnosticUsageLogPath = null;
32
- /**
33
- * Initialize diagnostic mode: set up usage.log path.
34
- * Called once during server startup.
35
- */
36
- // implements REQ-008
37
17
  export function initializeDiagnosticMode(enabled = DIAGNOSTIC_MODE_ENABLED) {
38
18
  diagnosticUsageLogPath = null;
39
19
  if (!enabled) {
@@ -44,24 +24,14 @@ export function initializeDiagnosticMode(enabled = DIAGNOSTIC_MODE_ENABLED) {
44
24
  diagnosticUsageLogPath = path.join(workspaceRoot, ".kb", "usage.log");
45
25
  process.env.KIBI_MCP_DIAGNOSTIC_MODE = "1";
46
26
  }
47
- /**
48
- * Append a JSON line to the usage.log file.
49
- * No-op if diagnostic mode is not enabled.
50
- */
51
- // implements REQ-008
52
27
  export function appendUsageLogLine(entry) {
53
- if (!diagnosticUsageLogPath) {
28
+ if (!diagnosticUsageLogPath)
54
29
  return;
55
- }
56
- const logDir = path.dirname(diagnosticUsageLogPath);
57
- fs.mkdirSync(logDir, { recursive: true });
30
+ fs.mkdirSync(path.dirname(diagnosticUsageLogPath), { recursive: true });
58
31
  fs.appendFileSync(diagnosticUsageLogPath, `${JSON.stringify(entry)}\n`, {
59
32
  encoding: "utf8",
60
33
  });
61
34
  }
62
- /**
63
- * Schema for _diagnostic_telemetry field added to tool inputs in diagnostic mode.
64
- */
65
35
  export const DIAGNOSTIC_TELEMETRY_SCHEMA = {
66
36
  type: "object",
67
37
  description: "REQUIRED when diagnostic mode is on. Provide self-reflection metadata about this tool call.",
@@ -88,58 +58,88 @@ export const DIAGNOSTIC_TELEMETRY_SCHEMA = {
88
58
  },
89
59
  },
90
60
  };
91
- // implements REQ-002
92
- export function classifyDiagnosticError(error) {
93
- const err = error instanceof Error ? error : new Error(String(error));
94
- const message = err.message;
95
- const lower = message.toLowerCase();
96
- if (lower.includes("stale_snapshot")) {
97
- return buildErrorFields(err, "stale_snapshot", "persistence", "KB snapshot is stale; refresh/retry after the latest KB state is attached.");
98
- }
99
- if (lower.includes("unknown option") && lower.includes("h for help")) {
100
- return buildErrorFields(err, "prolog_unknown_option", "prolog_runtime", "Prolog rejected startup/module/query options; inspect MCP package wiring and Prolog invocation.");
101
- }
102
- if (lower.includes("prolog process not started")) {
103
- return buildErrorFields(err, "prolog_process_not_started", "prolog_lifecycle", "Prolog process is unavailable; restart the MCP server before retrying.");
104
- }
105
- if (lower.includes("resetting prolog worker") ||
106
- lower.includes("prolog worker reset")) {
107
- return buildErrorFields(err, "prolog_worker_reset", "prolog_lifecycle", "Prolog worker was reset so the next MCP call can start from a fresh worker.");
108
- }
109
- if (/timed out after \d+ms/i.test(message) ||
110
- lower.includes("tool timeout")) {
111
- return buildErrorFields(err, "tool_timeout", "tool_timeout", "MCP tool execution exceeded its bounded timeout.");
112
- }
113
- if (lower.includes("coarsely while granular symbols are available")) {
114
- return buildErrorFields(err, "coarse_symbol_linkage", "validation", "Symbol traceability targeted a coarse file/module while narrower exported symbols exist.");
115
- }
116
- if (message.startsWith("Entity validation failed:")) {
117
- return buildErrorFields(err, "entity_validation_failed", "validation", "Entity payload failed schema validation.");
61
+ function isRecord(value) {
62
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
63
+ }
64
+ function structuredContentFrom(result) {
65
+ if (!isRecord(result))
66
+ return undefined;
67
+ const structuredContent = result.structuredContent;
68
+ return isRecord(structuredContent) ? structuredContent : undefined;
69
+ }
70
+ function stringArray(value) {
71
+ if (!Array.isArray(value))
72
+ return [];
73
+ return value.filter((item) => typeof item === "string");
74
+ }
75
+ function suggestionKindsFrom(value) {
76
+ if (!Array.isArray(value))
77
+ return [];
78
+ return value.flatMap((item) => {
79
+ if (!isRecord(item) || typeof item.kind !== "string")
80
+ return [];
81
+ return [item.kind];
82
+ });
83
+ }
84
+ function appendSemanticAdvisorFields(fields, receipt) {
85
+ if (!isRecord(receipt))
86
+ return;
87
+ const readiness = receipt.logic_readiness;
88
+ const lane = receipt.candidate_lane;
89
+ const suggestionKinds = suggestionKindsFrom(receipt.suggestions);
90
+ if (typeof readiness === "string") {
91
+ fields.semantic_logic_readiness = readiness;
92
+ }
93
+ if (typeof lane === "string") {
94
+ fields.semantic_candidate_lane = lane;
95
+ }
96
+ fields.semantic_suggestion_kinds = suggestionKinds;
97
+ fields.semantic_suggestion_count = suggestionKinds.length;
98
+ fields.semantic_next_tools = stringArray(receipt.suggested_next_tools);
99
+ }
100
+ function appendPredicateSuggestionFields(fields, structuredContent) {
101
+ const candidates = Array.isArray(structuredContent.candidates)
102
+ ? structuredContent.candidates
103
+ : [];
104
+ const topCandidate = candidates.find(isRecord);
105
+ fields.predicate_candidate_count = candidates.length;
106
+ if (topCandidate) {
107
+ if (typeof topCandidate.predicate_name === "string") {
108
+ fields.predicate_top_name = topCandidate.predicate_name;
109
+ }
110
+ if (typeof topCandidate.score === "number") {
111
+ fields.predicate_top_score = topCandidate.score;
112
+ }
113
+ }
114
+ if (typeof structuredContent.recommendedAction === "string") {
115
+ fields.predicate_recommended_action = structuredContent.recommendedAction;
116
+ }
117
+ fields.predicate_relationship_plan = isRecord(structuredContent.relationshipPlan);
118
+ }
119
+ function appendContradictionCheckFields(fields, contradictionCheck) {
120
+ if (!isRecord(contradictionCheck))
121
+ return;
122
+ const outcome = contradictionCheck.outcome;
123
+ if (typeof outcome === "string") {
124
+ fields.semantic_contradiction_outcome = outcome;
118
125
  }
119
- if (message.startsWith("Relationship validation failed")) {
120
- return buildErrorFields(err, "relationship_validation_failed", "validation", "Relationship payload failed schema validation.");
126
+ const checkedReqId = contradictionCheck.checked_req_id;
127
+ if (typeof checkedReqId === "string") {
128
+ fields.semantic_checked_req_id = checkedReqId;
121
129
  }
122
- if (message.startsWith("Relationship source must match the upserted entity")) {
123
- return buildErrorFields(err, "relationship_source_mismatch", "validation", "Relationship source did not match the entity being upserted.");
130
+ const strictReadiness = contradictionCheck.strict_readiness;
131
+ if (typeof strictReadiness === "string") {
132
+ fields.semantic_strict_readiness = strictReadiness;
124
133
  }
125
- if (lower.includes("module load failed")) {
126
- return buildErrorFields(err, "prolog_module_load_failed", "prolog_runtime", "Prolog failed to load an execution module.");
134
+ const subjectKey = contradictionCheck.subject_key;
135
+ if (typeof subjectKey === "string") {
136
+ fields.semantic_subject_key = subjectKey;
127
137
  }
128
- if (lower.includes("query failed")) {
129
- return buildErrorFields(err, "prolog_query_failed", "prolog_runtime", "Prolog query execution failed.");
138
+ const propertyKey = contradictionCheck.property_key;
139
+ if (typeof propertyKey === "string") {
140
+ fields.semantic_property_key = propertyKey;
130
141
  }
131
- return buildErrorFields(err, "handler_error", "handler", "Unhandled MCP handler error.");
132
142
  }
133
- function buildErrorFields(error, category, stage, summary) {
134
- return {
135
- error_name: error.name,
136
- error_message: error.message,
137
- error_category: category,
138
- error_stage: stage,
139
- error_summary: summary,
140
- };
141
- }
142
- // implements REQ-002
143
143
  export function deriveDiagnosticFields(toolName, args, telemetry, result) {
144
144
  const fields = {
145
145
  telemetry_status: telemetry ? "provided" : "missing",
@@ -149,10 +149,7 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
149
149
  fields.telemetry_confidence_score = telemetry.confidence_score ?? null;
150
150
  fields.telemetry_attempt_number = telemetry.attempt_number ?? null;
151
151
  }
152
- const structuredContent = result && typeof result === "object" && "structuredContent" in result
153
- ? result
154
- .structuredContent
155
- : undefined;
152
+ const structuredContent = structuredContentFrom(result);
156
153
  if (toolName === "kb_query" || toolName === "kb_search") {
157
154
  const resultCount = Number(structuredContent?.count ?? 0);
158
155
  fields.result_count = resultCount;
@@ -167,18 +164,38 @@ export function deriveDiagnosticFields(toolName, args, telemetry, result) {
167
164
  fields.result_summary =
168
165
  violationCount === 0 ? "0 violations" : `${violationCount} violations`;
169
166
  }
167
+ if (toolName === "kb_semantic_advisor" && structuredContent) {
168
+ appendSemanticAdvisorFields(fields, structuredContent.receipt);
169
+ const readiness = fields.semantic_logic_readiness;
170
+ const lane = fields.semantic_candidate_lane;
171
+ if (typeof readiness === "string" && typeof lane === "string") {
172
+ fields.result_summary = `semantic advisor ${readiness} via ${lane}`;
173
+ }
174
+ }
175
+ if (toolName === "kb_suggest_predicates" && structuredContent) {
176
+ appendPredicateSuggestionFields(fields, structuredContent);
177
+ const count = Number(fields.predicate_candidate_count ?? 0);
178
+ const topName = fields.predicate_top_name;
179
+ fields.result_summary =
180
+ typeof topName === "string"
181
+ ? `${count} predicate candidates; top=${topName}`
182
+ : `${count} predicate candidates`;
183
+ }
184
+ if (toolName === "kb_upsert" && structuredContent) {
185
+ const created = Number(structuredContent.created ?? 0);
186
+ const updated = Number(structuredContent.updated ?? 0);
187
+ fields.upsert_created = created;
188
+ fields.upsert_updated = updated;
189
+ fields.upsert_relationships_created = Number(structuredContent.relationships_created ?? 0);
190
+ appendContradictionCheckFields(fields, structuredContent.contradictionCheck);
191
+ appendSemanticAdvisorFields(fields, structuredContent.semanticAdvisor);
192
+ const readiness = fields.semantic_logic_readiness;
193
+ if (typeof readiness === "string") {
194
+ fields.result_summary = `upsert ${created > 0 ? "created" : "updated"}; semantic ${readiness}`;
195
+ }
196
+ }
170
197
  if (!fields.result_summary) {
171
198
  fields.result_summary = `${toolName} completed`;
172
199
  }
173
200
  return fields;
174
201
  }
175
- /**
176
- * Extract business args and telemetry from tool call arguments.
177
- */
178
- export function extractToolCallPayload(args) {
179
- const { _diagnostic_telemetry, ...businessArgs } = args;
180
- const telemetry = _diagnostic_telemetry && typeof _diagnostic_telemetry === "object"
181
- ? _diagnostic_telemetry
182
- : null;
183
- return { businessArgs, telemetry };
184
- }
@@ -385,6 +385,16 @@ function detectStrictPropertySuggestion(payload, statement) {
385
385
  value_int: Number(comparative.groups.value),
386
386
  }, "Comparative numeric prose is a strict property constraint.", 0.86);
387
387
  }
388
+ const decisecondSlot = statement.match(/^.+?\s+(?:must|shall|should)\s+normalize\s+into\s+canonical\s+integer\s+decisecond\s+(?<subject>[a-z][a-z0-9_-]*)\s+values?\.?$/i);
389
+ if (decisecondSlot?.groups?.subject) {
390
+ return strictSuggestion(payload, decisecondSlot[0], {
391
+ subject_key: normalizeSubjectKey(decisecondSlot.groups.subject),
392
+ property_key: "slot_precision",
393
+ operator: "eq",
394
+ value_type: "string",
395
+ value_string: "decisecond",
396
+ }, "Canonical decisecond slot prose is a strict precision property.", 0.9);
397
+ }
388
398
  const threshold = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+return\s+within\s+(?<value>\d+)\s+(?<unit>ms|milliseconds?|s|sec|secs|seconds?)\.?$/i);
389
399
  if (threshold?.groups?.subject &&
390
400
  threshold.groups.value &&
@@ -598,6 +608,21 @@ function detectPredicateSuggestion(payload, statement) {
598
608
  };
599
609
  return predicateSuggestion(payload, ownership[0], predicate, "Ownership prose assigns responsibility for a resource or behavior and should be queryable as a predicate.");
600
610
  }
611
+ const mergePolicy = statement.match(/^(?<inputs>.+?)\s+saved\s+in\s+the\s+same\s+(?<slot>.+?)\s+must\s+merge\s+into\s+(?<target>.+?)\s+instead\s+of\s+creating\s+.+?\.?$/i);
612
+ if (mergePolicy?.groups?.inputs &&
613
+ mergePolicy.groups.slot &&
614
+ mergePolicy.groups.target) {
615
+ const inputs = normalizePredicateToken(mergePolicy.groups.inputs);
616
+ const slot = normalizePredicateToken(mergePolicy.groups.slot);
617
+ const target = normalizePredicateToken(mergePolicy.groups.target);
618
+ const predicate = {
619
+ predicate_name: "merge_policy",
620
+ predicate_args: [inputs, slot, target],
621
+ canonical_key: `merge_policy(${inputs},${slot},${target})`,
622
+ polarity: "assert",
623
+ };
624
+ return predicateSuggestion(payload, mergePolicy[0], predicate, "Same-slot merge prose defines ontology-lite merge behavior and should be queryable as a predicate.");
625
+ }
601
626
  const retryPolicy = statement.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+retry\s+up\s+to\s+(?<count>\d+)\s+(?<unit>times|attempts?)\.?$/i);
602
627
  if (retryPolicy?.groups?.subject &&
603
628
  retryPolicy.groups.count &&
@@ -0,0 +1,74 @@
1
+ import { buildDiagnosticToolCall, deriveDiagnosticHints, } from "../diagnostics.js";
2
+ // implements REQ-002
3
+ export async function appendDiagnosticSuccessUsage(input) {
4
+ const finishedAt = new Date();
5
+ const diagnosticFields = input.runtime.deriveDiagnosticFields(input.toolName, input.businessArgs, input.telemetry, input.result);
6
+ const toolCall = buildDiagnosticToolCall({
7
+ tool: input.toolName,
8
+ requestId: input.requestId,
9
+ args: input.args,
10
+ diagnosticPhase: "success",
11
+ telemetry: input.telemetry,
12
+ });
13
+ const processHandle = await input.runtime.prologProcess();
14
+ const branchName = await input.runtime.activeBranchName();
15
+ input.runtime.appendUsageLogLine({
16
+ timestamp: finishedAt.toISOString(),
17
+ request_id: input.requestId,
18
+ tool: input.toolName,
19
+ telemetry: input.telemetry,
20
+ business_args: input.businessArgs,
21
+ tool_call: toolCall,
22
+ retry_key: toolCall.retry_key,
23
+ diagnostic_phase: toolCall.diagnostic_phase,
24
+ status: "success",
25
+ started_at: input.startedAt.toISOString(),
26
+ finished_at: finishedAt.toISOString(),
27
+ duration_ms: finishedAt.getTime() - input.startedAt.getTime(),
28
+ prolog_pid: processHandle?.getPid() ?? null,
29
+ active_branch: branchName,
30
+ ...diagnosticFields,
31
+ });
32
+ }
33
+ // implements REQ-002
34
+ export async function appendDiagnosticErrorUsage(input) {
35
+ const finishedAt = new Date();
36
+ const err = input.error instanceof Error ? input.error : new Error(String(input.error));
37
+ const diagnosticErrorFields = input.runtime.classifyDiagnosticError(err);
38
+ const diagnosticHints = deriveDiagnosticHints({
39
+ tool: input.toolName,
40
+ error: err,
41
+ businessArgs: input.businessArgs,
42
+ });
43
+ const toolCall = buildDiagnosticToolCall({
44
+ tool: input.toolName,
45
+ requestId: input.requestId,
46
+ args: input.args,
47
+ diagnosticPhase: "error",
48
+ error: err,
49
+ telemetry: input.telemetry,
50
+ });
51
+ const processHandle = await input.runtime.prologProcess();
52
+ const branchName = await input.runtime.activeBranchName();
53
+ input.runtime.appendUsageLogLine({
54
+ timestamp: finishedAt.toISOString(),
55
+ request_id: input.requestId,
56
+ tool: input.toolName,
57
+ telemetry: input.telemetry,
58
+ business_args: input.businessArgs,
59
+ tool_call: toolCall,
60
+ retry_key: toolCall.retry_key,
61
+ diagnostic_phase: toolCall.diagnostic_phase,
62
+ status: "error",
63
+ started_at: input.startedAt.toISOString(),
64
+ finished_at: finishedAt.toISOString(),
65
+ duration_ms: finishedAt.getTime() - input.startedAt.getTime(),
66
+ prolog_pid: processHandle?.getPid() ?? null,
67
+ active_branch: branchName,
68
+ reset_attempted: input.resetState.resetAttempted,
69
+ reset_succeeded: input.resetState.resetSucceeded,
70
+ reset_error: input.resetState.resetError,
71
+ diagnostic_hints: [diagnosticHints],
72
+ ...diagnosticErrorFields,
73
+ });
74
+ }
@@ -107,7 +107,7 @@ export const PROMPTS = [
107
107
  "- `kb_graph`: Bounded graph traversal from seed IDs",
108
108
  "- `kb_upsert`: Create or update entities and their relationships",
109
109
  "- `kb_delete`: Remove entities by ID (with dependency safety checks)",
110
- "- `kb_check`: Validate KB integrity against configurable rules",
110
+ "- `kb_check`: Validate KB integrity against configurable rules; omit `rules` for final full validation plus full-KB `qualityDiagnostics[]` audit review",
111
111
  "",
112
112
  "Core modeling principles:",
113
113
  "- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
@@ -118,7 +118,7 @@ export const PROMPTS = [
118
118
  "- v1 contradictions are limited to exact-value, boolean/enum, numeric range, and polarity conflicts.",
119
119
  "- Use `kb_search` first for discovery, then `kb_query` for exact follow-up before any mutation.",
120
120
  "- Use `kb_upsert` and `kb_delete` only for intentional, traceable KB changes.",
121
- "- Run `kb_check` after meaningful mutations to catch integrity issues early.",
121
+ "- Run `kb_check` with explicit `rules` during iteration for scoped feedback; run an unfiltered `kb_check` before completion to include the full-KB `qualityDiagnostics[]` audit scan.",
122
122
  "- Prefer explicit IDs and enum values to avoid invalid parameters.",
123
123
  "- Model requirements by first creating/reusing fact entities (create-before-link).",
124
124
  "- flag gates runtime/config behavior; use `fact` with `fact_kind: observation` or `meta` for bug and workaround notes.",
@@ -138,7 +138,8 @@ export const PROMPTS = [
138
138
  "5. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property` (automated via `kb_model_requirement`).",
139
139
  "6. **Suggest predicates before prose**: For ontology-lane requirements, spell out the prose claim and call `kb_suggest_predicates` before writing `fact_kind: observation`. Apply the selected `fact_kind: predicate` applyPlan, then attach the returned `relationshipPlan` as `requires_predicate` while preserving existing req metadata; use the returned `review:ontology-gap` observation when no predicate fits.",
140
140
  "7. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
141
- "8. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need.",
141
+ "8. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need so scoped validation stays fast and skips the full-KB advisory scan.",
142
+ "9. **Final check**: Run `kb_check` without `rules` before completion so hard `violations[]` and advisory full-KB `qualityDiagnostics[]` are both reviewed.",
142
143
  "",
143
144
  "If a tool returns empty results, do not assume failure. Re-check filters (type, id, tags, sourceFile, limit, or offset).",
144
145
  ].join("\n"),
@@ -203,7 +204,7 @@ function registerDocResources() {
203
204
  "2. `kb_upsert` for the fact entity first (create-before-link)",
204
205
  "3. `kb_upsert` for the req entity and include `relationships` with `constrains` and `requires_property`",
205
206
  "4. Reuse the same constrained fact ID across related requirements; vary property facts only when semantics differ",
206
- '5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
207
+ '5. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation; supplying `rules` skips the full-KB advisory scan',
207
208
  "",
208
209
  "## Model requirements as ontology predicates",
209
210
  '1. Spell out the requirement prose and call `kb_suggest_predicates` with `{ "text": "...", "requirementId": "REQ-..." }`',
@@ -222,9 +223,9 @@ function registerDocResources() {
222
223
  '1. `kb_query` with `{ "type": "test" }` to check for existing test IDs',
223
224
  '2. `kb_query` with `{ "id": "REQ-XXX" }` to verify the requirement exists',
224
225
  "3. `kb_upsert` with entity payload and `relationships` containing `verified_by`",
225
- '4. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation',
226
+ '4. `kb_check` with `{ "rules": ["required-fields","no-dangling-refs"] }` for targeted validation; use an unfiltered `kb_check` before completion for full-KB `qualityDiagnostics[]`',
226
227
  "",
227
- "Note: Always use query-first pattern. Specify only needed rules in kb_check for faster validation.",
228
+ "Note: Always use query-first pattern. Specify only needed rules in kb_check for faster iteration; omit rules for the final full validation and full-KB quality diagnostics review.",
228
229
  ].join("\n");
229
230
  return [
230
231
  {