kibi-cli 2.18.0 → 2.20.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.
Files changed (106) hide show
  1. package/dist/cli-operation-metadata.d.ts +2 -2
  2. package/dist/cli-operation-metadata.js +2 -2
  3. package/dist/cli-register-maintenance.js +1 -1
  4. package/dist/commands/doctor.js +11 -5
  5. package/dist/commands/engine.d.ts +2 -1
  6. package/dist/commands/engine.d.ts.map +1 -1
  7. package/dist/commands/engine.js +6 -1
  8. package/dist/commands/sync/manifest.d.ts +10 -0
  9. package/dist/commands/sync/manifest.d.ts.map +1 -1
  10. package/dist/commands/sync/manifest.js +27 -0
  11. package/dist/commands/sync/untracked-targets.d.ts +17 -0
  12. package/dist/commands/sync/untracked-targets.d.ts.map +1 -1
  13. package/dist/commands/sync/untracked-targets.js +43 -1
  14. package/dist/commands/sync.d.ts.map +1 -1
  15. package/dist/commands/sync.js +51 -2
  16. package/dist/engine.d.ts.map +1 -1
  17. package/dist/engine.js +33 -22
  18. package/dist/intent-search.d.ts +7 -0
  19. package/dist/intent-search.d.ts.map +1 -1
  20. package/dist/intent-search.js +66 -4
  21. package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
  22. package/dist/operations/modeling/predicate-applicability.js +10 -4
  23. package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
  24. package/dist/operations/modeling/predicate-applyplan.js +16 -5
  25. package/dist/operations/modeling/predicate-catalog-4.d.ts.map +1 -1
  26. package/dist/operations/modeling/predicate-catalog-4.js +3 -0
  27. package/dist/operations/modeling/predicate-inference-1.d.ts.map +1 -1
  28. package/dist/operations/modeling/predicate-inference-1.js +34 -7
  29. package/dist/operations/modeling/predicate-score-3.js +1 -1
  30. package/dist/operations/modeling/predicate-types.d.ts +6 -0
  31. package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
  32. package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
  33. package/dist/operations/modeling/suggest-predicates.js +111 -0
  34. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  35. package/dist/operations/mutation/source-authoring.js +5 -12
  36. package/dist/operations/mutation/types.d.ts +26 -0
  37. package/dist/operations/mutation/types.d.ts.map +1 -1
  38. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  39. package/dist/operations/mutation/upsert.js +8 -2
  40. package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
  41. package/dist/operations/mutation/validate-upsert.js +116 -4
  42. package/dist/operations/mutation/warnings.d.ts +10 -0
  43. package/dist/operations/mutation/warnings.d.ts.map +1 -1
  44. package/dist/operations/mutation/warnings.js +26 -0
  45. package/dist/operations/mutation/what-if.d.ts +25 -0
  46. package/dist/operations/mutation/what-if.d.ts.map +1 -0
  47. package/dist/operations/mutation/what-if.js +59 -0
  48. package/dist/operations/planning/compile-intent.d.ts +15 -20
  49. package/dist/operations/planning/compile-intent.d.ts.map +1 -1
  50. package/dist/operations/planning/compile-intent.js +64 -53
  51. package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
  52. package/dist/operations/semantic-advisor/clauses.js +8 -7
  53. package/dist/prolog/janitor.d.ts +26 -2
  54. package/dist/prolog/janitor.d.ts.map +1 -1
  55. package/dist/prolog/janitor.js +55 -3
  56. package/dist/prolog/store-lock.d.ts +47 -3
  57. package/dist/prolog/store-lock.d.ts.map +1 -1
  58. package/dist/prolog/store-lock.js +148 -11
  59. package/dist/proof/inspect.d.ts +1 -1
  60. package/dist/proof/inspect.d.ts.map +1 -1
  61. package/dist/proof/inspect.js +9 -2
  62. package/dist/public/operations/catalog.d.ts +97 -4
  63. package/dist/public/operations/catalog.d.ts.map +1 -1
  64. package/dist/public/operations/check-executor.d.ts +2 -2
  65. package/dist/public/operations/check-executor.d.ts.map +1 -1
  66. package/dist/public/operations/check-executor.js +35 -0
  67. package/dist/public/operations/check-format-shared.d.ts +16 -0
  68. package/dist/public/operations/check-format-shared.d.ts.map +1 -1
  69. package/dist/public/operations/check-format-shared.js +19 -0
  70. package/dist/public/operations/contracts.d.ts.map +1 -1
  71. package/dist/public/operations/contracts.js +11 -4
  72. package/dist/public/operations/discovery-executors.d.ts +39 -0
  73. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  74. package/dist/public/operations/discovery-executors.js +368 -3
  75. package/dist/public/operations/specs/composite.d.ts +1 -1
  76. package/dist/public/operations/specs/composite.js +1 -1
  77. package/dist/public/operations/specs/discovery.d.ts +52 -3
  78. package/dist/public/operations/specs/discovery.d.ts.map +1 -1
  79. package/dist/public/operations/specs/discovery.js +58 -3
  80. package/dist/public/operations/specs/upsert.d.ts +44 -0
  81. package/dist/public/operations/specs/upsert.d.ts.map +1 -1
  82. package/dist/public/operations/specs/upsert.js +14 -0
  83. package/dist/public/skills/kibi-usage/SKILL.md +44 -1
  84. package/dist/public/skills/kibi-usage/resources/fact-lanes.md +4 -2
  85. package/dist/runtime/cli-runtime.d.ts.map +1 -1
  86. package/dist/runtime/cli-runtime.js +7 -2
  87. package/dist/search-answer.d.ts +25 -3
  88. package/dist/search-answer.d.ts.map +1 -1
  89. package/dist/search-answer.js +96 -8
  90. package/dist/utils/branch-store.d.ts.map +1 -1
  91. package/dist/utils/branch-store.js +4 -2
  92. package/dist/utils/content-freshness.d.ts.map +1 -1
  93. package/dist/utils/content-freshness.js +12 -2
  94. package/dist/utils/symbol-manifest-layout.d.ts +9 -0
  95. package/dist/utils/symbol-manifest-layout.d.ts.map +1 -0
  96. package/dist/utils/symbol-manifest-layout.js +18 -0
  97. package/package.json +3 -3
  98. package/src/public/operations/check-executor.ts +44 -1
  99. package/src/public/operations/check-format-shared.ts +35 -1
  100. package/src/public/operations/contracts.ts +15 -4
  101. package/src/public/operations/discovery-executors.ts +484 -2
  102. package/src/public/operations/specs/composite.ts +1 -1
  103. package/src/public/operations/specs/discovery.ts +70 -3
  104. package/src/public/operations/specs/upsert.ts +15 -0
  105. package/src/public/skills/kibi-usage/SKILL.md +44 -1
  106. package/src/public/skills/kibi-usage/resources/fact-lanes.md +4 -2
@@ -11,8 +11,45 @@ import { BRANCH_STORE_NOT_COMPILED, branchStoreReason, inspectBranchStore, store
11
11
  import { confirmSourceNewerReasons } from "../../utils/content-freshness.js";
12
12
  import { loadSearchCandidates, paginateResults, queryEntitiesViaQuery, reloadFullEntities, validateEntityType, } from "./discovery-entities.js";
13
13
  import { buildActionsFromStatus, readMigrationConfigStatus, } from "./migration-plan.js";
14
- import { OperationJsonDecodeError, runOperationJsonQuery, } from "./prolog-json.js";
14
+ import { OperationJsonDecodeError, runOperationJsonQuery, toPrologAtom, } from "./prolog-json.js";
15
15
  import { readWorkspaceSnapshot } from "./workspace-snapshot.js";
16
+ export const FACT_KINDS = [
17
+ "subject",
18
+ "property_value",
19
+ "predicate",
20
+ "predicate_schema",
21
+ "observation",
22
+ "meta",
23
+ "rule",
24
+ "rule_schema",
25
+ ];
26
+ /** Which typed filters each question mode reads; any other is refused. */
27
+ const MODE_FILTERS = {
28
+ schemas: new Set(["factKind", "predicate", "namespace"]),
29
+ predicate: new Set([
30
+ "predicate",
31
+ "namespace",
32
+ "where",
33
+ "args",
34
+ "polarity",
35
+ "scope",
36
+ "distinct",
37
+ ]),
38
+ property: new Set(["subjectKey", "propertyKey", "scope"]),
39
+ fact_kind: new Set(["factKind"]),
40
+ };
41
+ const TYPED_FACT_FILTERS = [
42
+ "factKind",
43
+ "predicate",
44
+ "namespace",
45
+ "where",
46
+ "args",
47
+ "polarity",
48
+ "scope",
49
+ "distinct",
50
+ "subjectKey",
51
+ "propertyKey",
52
+ ];
16
53
  /**
17
54
  * An empty result from a store that was never compiled says nothing about
18
55
  * the authored requirements, so search reports the store state itself (the
@@ -51,11 +88,334 @@ function isStatusPayload(value) {
51
88
  typeof record.dirty === "boolean" &&
52
89
  typeof record.syncState === "string");
53
90
  }
91
+ function hasTypedFactFilters(input) {
92
+ return TYPED_FACT_FILTERS.some((key) => input[key] !== undefined);
93
+ }
94
+ function requireAtomString(value, field) {
95
+ if (typeof value !== "string" || value.trim() === "") {
96
+ throw new Error(`${field} must be a non-empty string.`);
97
+ }
98
+ return value.trim();
99
+ }
100
+ function requireEnum(value, field, allowed) {
101
+ if (typeof value !== "string" || !allowed.includes(value)) {
102
+ throw new Error(`${field} must be one of: ${allowed.join(", ")}.`);
103
+ }
104
+ return value;
105
+ }
106
+ /**
107
+ * Builds the Prolog argument spec of a predicate question. Named `where`
108
+ * entries are resolved against the predicate's declared argument names in
109
+ * Prolog; positional `args` use `null` as a wildcard.
110
+ */
111
+ // implements REQ-kibi-query-fact-filters
112
+ function predicateArgSpec(input) {
113
+ if (input.where !== undefined && input.args !== undefined) {
114
+ throw new Error("Use either where (named arguments) or args (positional), not both.");
115
+ }
116
+ if (input.where !== undefined) {
117
+ if (input.where === null ||
118
+ typeof input.where !== "object" ||
119
+ Array.isArray(input.where)) {
120
+ throw new Error("where must be an object of argument name to value.");
121
+ }
122
+ const pairs = Object.entries(input.where).map(([key, value]) => {
123
+ const name = requireAtomString(key, "where argument name");
124
+ const atom = requireAtomString(value, `where.${key}`);
125
+ return `${toPrologAtom(name)}=${toPrologAtom(atom)}`;
126
+ });
127
+ return `where([${pairs.join(",")}])`;
128
+ }
129
+ if (input.args !== undefined) {
130
+ if (!Array.isArray(input.args)) {
131
+ throw new Error("args must be an array of constants (null = any).");
132
+ }
133
+ const entries = input.args.map((value, index) => value === null || value === "*"
134
+ ? "'$any'"
135
+ : toPrologAtom(requireAtomString(value, `args[${index}]`)));
136
+ return `[${entries.join(",")}]`;
137
+ }
138
+ return "[]";
139
+ }
140
+ function callText(args) {
141
+ return `kb_query(${JSON.stringify(args)})`;
142
+ }
143
+ /**
144
+ * Every typed answer names the exact call that makes sense next, so an
145
+ * agent never has to guess the follow-up from the shape of the data.
146
+ */
147
+ // implements REQ-kibi-query-fact-filters
148
+ function factQueryNext(input, response) {
149
+ const next = [];
150
+ const first = response.rows[0];
151
+ const firstId = typeof first?.id === "string" ? first.id : undefined;
152
+ switch (response.mode) {
153
+ case "predicate": {
154
+ if (firstId !== undefined) {
155
+ next.push({
156
+ call: callText({ id: firstId }),
157
+ why: "full fact text and relationships of one row",
158
+ });
159
+ }
160
+ const filters = {
161
+ predicate: input.predicate,
162
+ ...(input.namespace !== undefined
163
+ ? { namespace: input.namespace }
164
+ : {}),
165
+ ...(input.where !== undefined ? { where: input.where } : {}),
166
+ ...(input.args !== undefined ? { args: input.args } : {}),
167
+ ...(input.polarity !== undefined ? { polarity: input.polarity } : {}),
168
+ ...(input.scope !== undefined ? { scope: input.scope } : {}),
169
+ };
170
+ const names = Array.isArray(response.schema?.argumentNames)
171
+ ? response.schema.argumentNames.filter((name) => typeof name === "string")
172
+ : [];
173
+ const bound = new Set(Object.keys(input.where ?? {}));
174
+ const free = names.filter((name) => !bound.has(name));
175
+ if (input.distinct === undefined &&
176
+ response.count > 0 &&
177
+ free.length > 0) {
178
+ const argument = free[free.length - 1] ?? free[0];
179
+ next.push({
180
+ call: callText({ ...filters, distinct: argument }),
181
+ why: `count the distinct values of '${argument}' across these facts`,
182
+ });
183
+ }
184
+ if (response.count === 0 &&
185
+ response.suggestions &&
186
+ typeof response.suggestions === "object" &&
187
+ (response.warnings ?? []).length === 0) {
188
+ // Suggest a correction only for an argument whose requested value
189
+ // does not exist; a key whose value exists is not the cause.
190
+ for (const [key, values] of Object.entries(response.suggestions)) {
191
+ if (!Array.isArray(values))
192
+ continue;
193
+ const requested = input.where?.[key];
194
+ if (requested !== undefined && values.includes(requested))
195
+ continue;
196
+ const value = values[0];
197
+ if (typeof value === "string") {
198
+ next.push({
199
+ call: callText({
200
+ ...filters,
201
+ where: { ...(input.where ?? {}), [key]: value },
202
+ }),
203
+ why: `'${key}' has no fact with that value; '${value}' is one that exists`,
204
+ });
205
+ break;
206
+ }
207
+ }
208
+ }
209
+ if (next.length === 0) {
210
+ next.push({
211
+ call: callText({ factKind: "predicate_schema" }),
212
+ why: "list the predicates this KB can answer about and their argument names",
213
+ });
214
+ }
215
+ break;
216
+ }
217
+ case "property": {
218
+ if (firstId !== undefined) {
219
+ next.push({
220
+ call: callText({ id: firstId }),
221
+ why: "full fact text and relationships of one row",
222
+ });
223
+ }
224
+ if (input.propertyKey !== undefined) {
225
+ next.push({
226
+ call: callText({ subjectKey: input.subjectKey }),
227
+ why: "every property stated about this subject",
228
+ });
229
+ }
230
+ else if (response.count === 0) {
231
+ next.push({
232
+ call: callText({ factKind: "subject" }),
233
+ why: "list the subject keys that exist",
234
+ });
235
+ }
236
+ break;
237
+ }
238
+ case "schemas": {
239
+ const name = typeof first?.name === "string" ? first.name : undefined;
240
+ if (name !== undefined) {
241
+ next.push({
242
+ call: callText({ predicate: name }),
243
+ why: "every stored fact of the first listed predicate",
244
+ });
245
+ }
246
+ break;
247
+ }
248
+ case "fact_kind": {
249
+ if (input.factKind === "predicate") {
250
+ next.push({
251
+ call: callText({ factKind: "predicate_schema" }),
252
+ why: "argument names and constants for asking by name",
253
+ });
254
+ }
255
+ else if (input.factKind === "property_value" && first) {
256
+ const subjectKey = typeof first.subject_key === "string" ? first.subject_key : undefined;
257
+ if (subjectKey !== undefined) {
258
+ next.push({
259
+ call: callText({ subjectKey }),
260
+ why: "the properties of the first listed subject",
261
+ });
262
+ }
263
+ }
264
+ else if (firstId !== undefined) {
265
+ next.push({
266
+ call: callText({ id: firstId }),
267
+ why: "full fact text of one row",
268
+ });
269
+ }
270
+ break;
271
+ }
272
+ }
273
+ return next;
274
+ }
275
+ function factRowSummary(row) {
276
+ const id = String(row.id ?? "");
277
+ if (typeof row.name === "string" && Array.isArray(row.args)) {
278
+ const effect = typeof row.effect === "string" ? ` -> ${row.effect}` : "";
279
+ const polarity = row.polarity === "deny" ? " [deny]" : "";
280
+ return `${id}: ${row.name}(${row.args.map(String).join(", ")})${effect}${polarity}`;
281
+ }
282
+ if (typeof row.subjectKey === "string" &&
283
+ typeof row.propertyKey === "string") {
284
+ const unit = typeof row.unit === "string" && row.unit ? ` ${row.unit}` : "";
285
+ return `${id}: ${row.subjectKey} ${row.propertyKey} ${String(row.operator ?? "")} ${String(row.value ?? "")}${unit}`;
286
+ }
287
+ if (typeof row.name === "string" && typeof row.arity === "number") {
288
+ const names = Array.isArray(row.argumentNames)
289
+ ? row.argumentNames.map(String).join(", ")
290
+ : "";
291
+ return `${row.name}(${names})`;
292
+ }
293
+ return `${id} (${String(row.title ?? "")})`;
294
+ }
295
+ /**
296
+ * Typed fact questions run as validated Prolog goals in the discovery module;
297
+ * the executor never forwards caller-written Prolog.
298
+ */
299
+ // implements REQ-kibi-query-fact-filters
300
+ async function executeFactQuery(input, context, limit, offset) {
301
+ if (input.type !== undefined && input.type !== "fact") {
302
+ throw new Error("Typed fact filters (factKind, predicate, subjectKey, ...) apply to type 'fact' only.");
303
+ }
304
+ for (const key of ["id", "tags", "sourceFile"]) {
305
+ if (input[key] !== undefined) {
306
+ throw new Error(`${key} cannot be combined with typed fact filters; run a separate kb_query.`);
307
+ }
308
+ }
309
+ const prolog = requireProlog(context);
310
+ if (!Number.isFinite(limit) ||
311
+ !Number.isFinite(offset) ||
312
+ limit < 0 ||
313
+ offset < 0) {
314
+ throw new Error("limit and offset must be non-negative numbers.");
315
+ }
316
+ const page = `${Math.trunc(limit)}, ${Math.trunc(offset)}`;
317
+ const namespace = input.namespace === undefined
318
+ ? "none"
319
+ : toPrologAtom(requireAtomString(input.namespace, "namespace"));
320
+ const mode = input.factKind === "predicate_schema"
321
+ ? "schemas"
322
+ : input.predicate !== undefined
323
+ ? "predicate"
324
+ : input.subjectKey !== undefined
325
+ ? "property"
326
+ : input.factKind !== undefined
327
+ ? "fact_kind"
328
+ : undefined;
329
+ if (mode === undefined) {
330
+ throw new Error("Typed fact filters need a predicate, a subjectKey or a factKind; where/args/polarity/distinct/propertyKey refine one of those.");
331
+ }
332
+ const unused = TYPED_FACT_FILTERS.filter((key) => input[key] !== undefined && !MODE_FILTERS[mode].has(key));
333
+ if (unused.length > 0) {
334
+ throw new Error(`${unused.join(", ")} do${unused.length === 1 ? "es" : ""} not apply to a ${mode} question; allowed with it: ${[...MODE_FILTERS[mode]].join(", ")}.`);
335
+ }
336
+ const scope = input.scope === undefined
337
+ ? "all"
338
+ : requireEnum(input.scope, "scope", ["all", "current_requirements"]);
339
+ const factKind = input.factKind === undefined
340
+ ? undefined
341
+ : requireEnum(input.factKind, "factKind", FACT_KINDS);
342
+ let goal;
343
+ if (mode === "schemas") {
344
+ goal = `discovery:ask_schemas_json(${namespace}, ${input.predicate === undefined ? "none" : toPrologAtom(requireAtomString(input.predicate, "predicate"))}, ${page}, JsonString)`;
345
+ }
346
+ else if (mode === "predicate") {
347
+ const name = requireAtomString(input.predicate, "predicate");
348
+ const polarity = input.polarity === undefined
349
+ ? "none"
350
+ : toPrologAtom(requireEnum(input.polarity, "polarity", ["assert", "deny"]));
351
+ const distinct = input.distinct === undefined
352
+ ? "none"
353
+ : toPrologAtom(requireAtomString(input.distinct, "distinct"));
354
+ goal = `discovery:ask_predicate_json(${namespace}, ${toPrologAtom(name)}, ${predicateArgSpec(input)}, ${polarity}, ${scope}, ${distinct}, ${page}, JsonString)`;
355
+ }
356
+ else if (mode === "property") {
357
+ const subject = requireAtomString(input.subjectKey, "subjectKey");
358
+ const property = input.propertyKey === undefined
359
+ ? "none"
360
+ : toPrologAtom(requireAtomString(input.propertyKey, "propertyKey"));
361
+ goal = `discovery:ask_property_json(${toPrologAtom(subject)}, ${property}, ${scope}, ${page}, JsonString)`;
362
+ }
363
+ else {
364
+ goal = `discovery:ask_fact_kind_json(${toPrologAtom(factKind)}, ${page}, JsonString)`;
365
+ }
366
+ const response = await runOperationJsonQuery(prolog, "discovery.pl", goal, "Query execution", context.signal);
367
+ const rows = Array.isArray(response.rows) ? response.rows : [];
368
+ const count = typeof response.count === "number" ? response.count : rows.length;
369
+ const warnings = Array.isArray(response.warnings) ? response.warnings : [];
370
+ const factQuery = {
371
+ mode: response.mode,
372
+ ...(response.predicate ? { predicate: response.predicate } : {}),
373
+ ...(response.property ? { property: response.property } : {}),
374
+ ...(response.factKind ? { factKind: response.factKind } : {}),
375
+ ...(response.schema !== undefined ? { schema: response.schema } : {}),
376
+ ...(response.subjectFacts ? { subjectFacts: response.subjectFacts } : {}),
377
+ ...(response.distinct !== undefined ? { distinct: response.distinct } : {}),
378
+ ...(response.suggestions !== undefined
379
+ ? { suggestions: response.suggestions }
380
+ : {}),
381
+ warnings,
382
+ next: factQueryNext(input, { ...response, rows, count }),
383
+ };
384
+ const subject = response.mode === "predicate"
385
+ ? `${String(input.predicate)} facts`
386
+ : response.mode === "property"
387
+ ? `properties of ${String(input.subjectKey)}`
388
+ : response.mode === "schemas"
389
+ ? "predicate schemas"
390
+ : `${String(factKind)} facts`;
391
+ const lines = [
392
+ count === 0
393
+ ? `No ${subject} matched.`
394
+ : `${count} ${subject}. Showing ${rows.length} (offset ${offset}, limit ${limit}): ${rows.map(factRowSummary).join("; ")}`,
395
+ ...(factQuery.distinct && typeof factQuery.distinct === "object"
396
+ ? [
397
+ `Distinct ${String(factQuery.distinct.argument)}: ${JSON.stringify(factQuery.distinct.values ?? [])}`,
398
+ ]
399
+ : []),
400
+ ...(factQuery.suggestions && typeof factQuery.suggestions === "object"
401
+ ? [`Values that exist: ${JSON.stringify(factQuery.suggestions)}`]
402
+ : []),
403
+ ...warnings.map((warning) => `Warning: ${warning}`),
404
+ ...factQuery.next.map((entry) => `Next: ${entry.call} (${entry.why})`),
405
+ ];
406
+ return {
407
+ content: [{ type: "text", text: lines.join("\n") }],
408
+ structuredContent: { entities: rows, count, factQuery },
409
+ };
410
+ }
54
411
  export async function executeQuery(input, context) {
55
412
  // implements REQ-kibi-operation-interface-parity
56
413
  const { type, id, tags, sourceFile, limit = 100, offset = 0 } = input;
57
414
  try {
58
415
  validateEntityType(type);
416
+ if (hasTypedFactFilters(input)) {
417
+ return await executeFactQuery(input, context, limit, offset);
418
+ }
59
419
  const prolog = requireProlog(context);
60
420
  const signal = context.signal;
61
421
  const pageInput = {
@@ -133,7 +493,7 @@ async function projectMatches(prolog, matches, fields) {
133
493
  }
134
494
  export async function executeSearch(input, context) {
135
495
  // implements REQ-kibi-operation-interface-parity, REQ-mcp-search-discovery
136
- const { query, type, limit = 20, offset = 0, rankingMode = "intent-v1", semanticFacets, sourceLocations, minScore, fields = "summary", answer, } = input;
496
+ const { query, type, limit = 10, offset = 0, rankingMode = "intent-v1", semanticFacets, sourceLocations, minScore, fields = "summary", answer, } = input;
137
497
  const trimmedQuery = query.trim();
138
498
  if (!trimmedQuery) {
139
499
  throw new Error("Search execution failed: query must be a non-empty string");
@@ -174,17 +534,21 @@ export async function executeSearch(input, context) {
174
534
  : undefined;
175
535
  // The structured payload carries every row; the text is a short
176
536
  // human-readable digest instead of a second copy of the results.
537
+ // A top match no requirement owns is said in the digest too, so the
538
+ // governing list is never read as the top match's requirement.
539
+ const topUngoverned = answerLayer?.ungoverned.find((entry) => entry.rank === 1);
177
540
  const text = `${intentResult.matches.length === 0
178
541
  ? `No intent search results for '${trimmedQuery}' (abstained).`
179
542
  : `Found ${intentResult.matches.length} intent search results for '${trimmedQuery}'${intentResult.analysis.ambiguous ? " (top matches are close; confirm before relying on one)" : ""}. Top: ${paginated
180
543
  .slice(0, 5)
181
544
  .map((match) => String(match.entity.id ?? ""))
182
- .join(", ")}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}.`}${storeNote}`;
545
+ .join(", ")}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}${topUngoverned ? ` (the top match ${topUngoverned.id} is linked to no requirement)` : ""}.`}${storeNote}`;
183
546
  return {
184
547
  content: [{ type: "text", text }],
185
548
  structuredContent: {
186
549
  results: await projectMatches(prolog, paginated, fields),
187
550
  count: intentResult.matches.length,
551
+ returned: paginated.length,
188
552
  truncated: offset + paginated.length < intentResult.matches.length,
189
553
  queryAnalysis: intentResult.analysis,
190
554
  ...(answerLayer ? { answer: answerLayer } : {}),
@@ -211,6 +575,7 @@ export async function executeSearch(input, context) {
211
575
  structuredContent: {
212
576
  results: await projectMatches(prolog, paginated, fields),
213
577
  count: matches.length,
578
+ returned: paginated.length,
214
579
  truncated: offset + paginated.length < matches.length,
215
580
  ...branchFields,
216
581
  },
@@ -344,7 +344,7 @@ export declare const skillsSpec: {
344
344
  export declare const modelSpec: {
345
345
  readonly name: "kb_model";
346
346
  readonly cliName: "model";
347
- readonly description: "Turn requirement prose into checkable structure without writing the KB. mode:analyze returns the semantic advisor receipt: the clause ledger, what is grounded, what is ambiguous and suggested modeling (pass clauses for compound prose and optional typed kibi.logic.v1 interpretations). mode:requirement converts one claim (subjectKey, propertyKey, operator, value, or typed logic) into a strict-lane write set and applyPlan. mode:predicates ranks ontology predicate schemas and returns an applicable predicate-fact plan only for reviewed bindings, otherwise an ontology-gap observation. Apply returned plans with kb_upsert.";
347
+ readonly description: "Turn requirement prose into checkable structure without writing the KB. mode:analyze returns the semantic advisor receipt: the clause ledger, what is grounded, what is ambiguous and suggested modeling (pass clauses for compound prose and optional typed kibi.logic.v1 interpretations). mode:requirement converts one claim (subjectKey, propertyKey, operator, value, or typed logic) into a strict-lane write set and applyPlan. mode:predicates ranks ontology predicate schemas and returns an applicable predicate-fact plan only for reviewed bindings, otherwise an ontology-gap observation. With requirementId, mode:predicates also returns a hash-bound plan (fact write plus requires_predicate link) for kb_apply_plan, whose contradictionAnalysis is the what-if over the current KB; other returned plans apply with kb_upsert, and kb_upsert dryRun with plannedFacts previews a requirement before its facts exist.";
348
348
  readonly businessInputSchema: {
349
349
  readonly type: "object";
350
350
  readonly required: readonly ["mode", "text"];
@@ -152,7 +152,7 @@ export const skillsSpec = {
152
152
  export const modelSpec = {
153
153
  name: "kb_model",
154
154
  cliName: "model",
155
- description: "Turn requirement prose into checkable structure without writing the KB. mode:analyze returns the semantic advisor receipt: the clause ledger, what is grounded, what is ambiguous and suggested modeling (pass clauses for compound prose and optional typed kibi.logic.v1 interpretations). mode:requirement converts one claim (subjectKey, propertyKey, operator, value, or typed logic) into a strict-lane write set and applyPlan. mode:predicates ranks ontology predicate schemas and returns an applicable predicate-fact plan only for reviewed bindings, otherwise an ontology-gap observation. Apply returned plans with kb_upsert.",
155
+ description: "Turn requirement prose into checkable structure without writing the KB. mode:analyze returns the semantic advisor receipt: the clause ledger, what is grounded, what is ambiguous and suggested modeling (pass clauses for compound prose and optional typed kibi.logic.v1 interpretations). mode:requirement converts one claim (subjectKey, propertyKey, operator, value, or typed logic) into a strict-lane write set and applyPlan. mode:predicates ranks ontology predicate schemas and returns an applicable predicate-fact plan only for reviewed bindings, otherwise an ontology-gap observation. With requirementId, mode:predicates also returns a hash-bound plan (fact write plus requires_predicate link) for kb_apply_plan, whose contradictionAnalysis is the what-if over the current KB; other returned plans apply with kb_upsert, and kb_upsert dryRun with plannedFacts previews a requirement before its facts exist.",
156
156
  businessInputSchema: {
157
157
  type: "object",
158
158
  required: ["mode", "text"],
@@ -2,7 +2,7 @@ import { executeQuery, executeSearch, executeStatus } from "../discovery-executo
2
2
  export declare const querySpec: {
3
3
  readonly name: "kb_query";
4
4
  readonly cliName: "query";
5
- readonly description: "Read entities from the KB with filters. Use for discovery and lookup before edits. Do not use for writes. No mutation side effects. Tags filter by metadata tags only, not entity IDs.";
5
+ readonly description: "Read entities from the KB with exact filters, or ask typed fact questions. Use for lookup by id, type, tags or sourceFile, and for precise questions about modeled facts: predicate facts by name with named arguments (predicate + where), property facts of a subject (subjectKey + propertyKey), or the catalog of facts of one kind (factKind; 'predicate_schema' lists the predicates you can ask about with their argument names and constants). Typed answers return matching fact rows plus factQuery: the schema used, distinct counts, values that do exist when nothing matched, warnings about the question and the next call to make. Read-only. Tags filter by metadata tags only, not entity IDs.";
6
6
  readonly businessInputSchema: {
7
7
  readonly type: "object";
8
8
  readonly properties: {
@@ -26,6 +26,55 @@ export declare const querySpec: {
26
26
  readonly type: "string";
27
27
  readonly description: "Optional source-file substring filter. Example: 'src/auth/login.ts'. Uses KB source linkage, not file-system scanning.";
28
28
  };
29
+ readonly factKind: {
30
+ readonly type: "string";
31
+ readonly enum: readonly ["subject", "property_value", "predicate", "predicate_schema", "observation", "meta", "rule", "rule_schema"];
32
+ readonly description: "Typed fact filter: list facts of one kind with their typed fields. 'predicate_schema' returns the catalog of askable predicates (argument names, constants, fact counts). Example: 'predicate_schema'.";
33
+ };
34
+ readonly predicate: {
35
+ readonly type: "string";
36
+ readonly description: "Typed fact filter: predicate name to ask about, e.g. 'has_role' or 'permission_rule'. Combine with where for named arguments. Returns every stored fact of that predicate with its arguments, polarity and allow/deny effect.";
37
+ };
38
+ readonly namespace: {
39
+ readonly type: "string";
40
+ readonly description: "Optional predicate namespace when the same name exists in several. Default: any namespace.";
41
+ };
42
+ readonly where: {
43
+ readonly type: "object";
44
+ readonly additionalProperties: {
45
+ readonly type: "string";
46
+ };
47
+ readonly description: "Named argument values for predicate, order-free, using the argument names from the predicate's schema. Example: {actor: 'admin', decision: 'allow'}. An unknown name returns a warning with the declared names; no match returns the values that do exist.";
48
+ };
49
+ readonly args: {
50
+ readonly type: "array";
51
+ readonly items: {
52
+ readonly type: readonly ["string", "null"];
53
+ };
54
+ readonly description: "Positional argument values for predicate when it has no schema; null matches any value. Example: ['admin', null, null, 'allow']. Prefer where.";
55
+ };
56
+ readonly polarity: {
57
+ readonly type: "string";
58
+ readonly enum: readonly ["assert", "deny"];
59
+ readonly description: "Optional polarity filter for predicate facts. Default: both.";
60
+ };
61
+ readonly scope: {
62
+ readonly type: "string";
63
+ readonly enum: readonly ["all", "current_requirements"];
64
+ readonly description: "Optional: 'current_requirements' keeps only facts a current (not superseded or retired) requirement requires.";
65
+ };
66
+ readonly distinct: {
67
+ readonly type: "string";
68
+ readonly description: "Optional argument name to count distinct values of across the matching predicate facts, e.g. distinct: 'role' with predicate 'has_role' and where {user: 'alice'} answers how many roles alice has.";
69
+ };
70
+ readonly subjectKey: {
71
+ readonly type: "string";
72
+ readonly description: "Typed fact filter: dotted subject key whose property facts to list, e.g. 'user.role_assignment'. Combine with propertyKey for one property.";
73
+ };
74
+ readonly propertyKey: {
75
+ readonly type: "string";
76
+ readonly description: "Optional property key under subjectKey, e.g. 'max_roles'. Returns operator, value and unit of each fact.";
77
+ };
29
78
  readonly limit: {
30
79
  readonly type: "number";
31
80
  readonly default: 100;
@@ -65,8 +114,8 @@ export declare const searchSpec: {
65
114
  readonly limit: {
66
115
  readonly type: "integer";
67
116
  readonly minimum: 0;
68
- readonly default: 20;
69
- readonly description: "Optional max rows to return after ranking. Default: 20.";
117
+ readonly default: 10;
118
+ readonly description: "Optional max rows to return after ranking. Default: 10; raise it or page with offset when the answer layer is not enough.";
70
119
  };
71
120
  readonly offset: {
72
121
  readonly type: "integer";
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../../src/public/operations/specs/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,aAAa,EACd,MAAM,2BAA2B,CAAC;AAsBnC,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgDsC,CAAC;AAE7D,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0GuC,CAAC;AAE/D,eAAO,MAAM,UAAU;;;;;;;;;;;;CAWuC,CAAC"}
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../../src/public/operations/specs/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,aAAa,EACd,MAAM,2BAA2B,CAAC;AAiCnC,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuGsC,CAAC;AAE7D,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2GuC,CAAC;AAE/D,eAAO,MAAM,UAAU;;;;;;;;;;;;CAWuC,CAAC"}
@@ -9,10 +9,20 @@ const ENTITY_TYPES = [
9
9
  "symbol",
10
10
  "fact",
11
11
  ];
12
+ const FACT_KINDS = [
13
+ "subject",
14
+ "property_value",
15
+ "predicate",
16
+ "predicate_schema",
17
+ "observation",
18
+ "meta",
19
+ "rule",
20
+ "rule_schema",
21
+ ];
12
22
  export const querySpec = {
13
23
  name: "kb_query",
14
24
  cliName: "query",
15
- description: "Read entities from the KB with filters. Use for discovery and lookup before edits. Do not use for writes. No mutation side effects. Tags filter by metadata tags only, not entity IDs.",
25
+ description: "Read entities from the KB with exact filters, or ask typed fact questions. Use for lookup by id, type, tags or sourceFile, and for precise questions about modeled facts: predicate facts by name with named arguments (predicate + where), property facts of a subject (subjectKey + propertyKey), or the catalog of facts of one kind (factKind; 'predicate_schema' lists the predicates you can ask about with their argument names and constants). Typed answers return matching fact rows plus factQuery: the schema used, distinct counts, values that do exist when nothing matched, warnings about the question and the next call to make. Read-only. Tags filter by metadata tags only, not entity IDs.",
16
26
  businessInputSchema: {
17
27
  type: "object",
18
28
  properties: {
@@ -34,6 +44,51 @@ export const querySpec = {
34
44
  type: "string",
35
45
  description: "Optional source-file substring filter. Example: 'src/auth/login.ts'. Uses KB source linkage, not file-system scanning.",
36
46
  },
47
+ factKind: {
48
+ type: "string",
49
+ enum: FACT_KINDS,
50
+ description: "Typed fact filter: list facts of one kind with their typed fields. 'predicate_schema' returns the catalog of askable predicates (argument names, constants, fact counts). Example: 'predicate_schema'.",
51
+ },
52
+ predicate: {
53
+ type: "string",
54
+ description: "Typed fact filter: predicate name to ask about, e.g. 'has_role' or 'permission_rule'. Combine with where for named arguments. Returns every stored fact of that predicate with its arguments, polarity and allow/deny effect.",
55
+ },
56
+ namespace: {
57
+ type: "string",
58
+ description: "Optional predicate namespace when the same name exists in several. Default: any namespace.",
59
+ },
60
+ where: {
61
+ type: "object",
62
+ additionalProperties: { type: "string" },
63
+ description: "Named argument values for predicate, order-free, using the argument names from the predicate's schema. Example: {actor: 'admin', decision: 'allow'}. An unknown name returns a warning with the declared names; no match returns the values that do exist.",
64
+ },
65
+ args: {
66
+ type: "array",
67
+ items: { type: ["string", "null"] },
68
+ description: "Positional argument values for predicate when it has no schema; null matches any value. Example: ['admin', null, null, 'allow']. Prefer where.",
69
+ },
70
+ polarity: {
71
+ type: "string",
72
+ enum: ["assert", "deny"],
73
+ description: "Optional polarity filter for predicate facts. Default: both.",
74
+ },
75
+ scope: {
76
+ type: "string",
77
+ enum: ["all", "current_requirements"],
78
+ description: "Optional: 'current_requirements' keeps only facts a current (not superseded or retired) requirement requires.",
79
+ },
80
+ distinct: {
81
+ type: "string",
82
+ description: "Optional argument name to count distinct values of across the matching predicate facts, e.g. distinct: 'role' with predicate 'has_role' and where {user: 'alice'} answers how many roles alice has.",
83
+ },
84
+ subjectKey: {
85
+ type: "string",
86
+ description: "Typed fact filter: dotted subject key whose property facts to list, e.g. 'user.role_assignment'. Combine with propertyKey for one property.",
87
+ },
88
+ propertyKey: {
89
+ type: "string",
90
+ description: "Optional property key under subjectKey, e.g. 'max_roles'. Returns operator, value and unit of each fact.",
91
+ },
37
92
  limit: {
38
93
  type: "number",
39
94
  default: 100,
@@ -73,8 +128,8 @@ export const searchSpec = {
73
128
  limit: {
74
129
  type: "integer",
75
130
  minimum: 0,
76
- default: 20,
77
- description: "Optional max rows to return after ranking. Default: 20.",
131
+ default: 10,
132
+ description: "Optional max rows to return after ranking. Default: 10; raise it or page with offset when the answer layer is not enough.",
78
133
  },
79
134
  offset: {
80
135
  type: "integer",