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.
- package/dist/cli-operation-metadata.d.ts +2 -2
- package/dist/cli-operation-metadata.js +2 -2
- package/dist/cli-register-maintenance.js +1 -1
- package/dist/commands/doctor.js +11 -5
- package/dist/commands/engine.d.ts +2 -1
- package/dist/commands/engine.d.ts.map +1 -1
- package/dist/commands/engine.js +6 -1
- package/dist/commands/sync/manifest.d.ts +10 -0
- package/dist/commands/sync/manifest.d.ts.map +1 -1
- package/dist/commands/sync/manifest.js +27 -0
- package/dist/commands/sync/untracked-targets.d.ts +17 -0
- package/dist/commands/sync/untracked-targets.d.ts.map +1 -1
- package/dist/commands/sync/untracked-targets.js +43 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +51 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +33 -22
- package/dist/intent-search.d.ts +7 -0
- package/dist/intent-search.d.ts.map +1 -1
- package/dist/intent-search.js +66 -4
- package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applicability.js +10 -4
- package/dist/operations/modeling/predicate-applyplan.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applyplan.js +16 -5
- package/dist/operations/modeling/predicate-catalog-4.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-catalog-4.js +3 -0
- package/dist/operations/modeling/predicate-inference-1.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-inference-1.js +34 -7
- package/dist/operations/modeling/predicate-score-3.js +1 -1
- package/dist/operations/modeling/predicate-types.d.ts +6 -0
- package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.d.ts.map +1 -1
- package/dist/operations/modeling/suggest-predicates.js +111 -0
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +5 -12
- package/dist/operations/mutation/types.d.ts +26 -0
- package/dist/operations/mutation/types.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +8 -2
- package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
- package/dist/operations/mutation/validate-upsert.js +116 -4
- package/dist/operations/mutation/warnings.d.ts +10 -0
- package/dist/operations/mutation/warnings.d.ts.map +1 -1
- package/dist/operations/mutation/warnings.js +26 -0
- package/dist/operations/mutation/what-if.d.ts +25 -0
- package/dist/operations/mutation/what-if.d.ts.map +1 -0
- package/dist/operations/mutation/what-if.js +59 -0
- package/dist/operations/planning/compile-intent.d.ts +15 -20
- package/dist/operations/planning/compile-intent.d.ts.map +1 -1
- package/dist/operations/planning/compile-intent.js +64 -53
- package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/clauses.js +8 -7
- package/dist/prolog/janitor.d.ts +26 -2
- package/dist/prolog/janitor.d.ts.map +1 -1
- package/dist/prolog/janitor.js +55 -3
- package/dist/prolog/store-lock.d.ts +47 -3
- package/dist/prolog/store-lock.d.ts.map +1 -1
- package/dist/prolog/store-lock.js +148 -11
- package/dist/proof/inspect.d.ts +1 -1
- package/dist/proof/inspect.d.ts.map +1 -1
- package/dist/proof/inspect.js +9 -2
- package/dist/public/operations/catalog.d.ts +97 -4
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/check-executor.d.ts +2 -2
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +35 -0
- package/dist/public/operations/check-format-shared.d.ts +16 -0
- package/dist/public/operations/check-format-shared.d.ts.map +1 -1
- package/dist/public/operations/check-format-shared.js +19 -0
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +11 -4
- package/dist/public/operations/discovery-executors.d.ts +39 -0
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +368 -3
- package/dist/public/operations/specs/composite.d.ts +1 -1
- package/dist/public/operations/specs/composite.js +1 -1
- package/dist/public/operations/specs/discovery.d.ts +52 -3
- package/dist/public/operations/specs/discovery.d.ts.map +1 -1
- package/dist/public/operations/specs/discovery.js +58 -3
- package/dist/public/operations/specs/upsert.d.ts +44 -0
- package/dist/public/operations/specs/upsert.d.ts.map +1 -1
- package/dist/public/operations/specs/upsert.js +14 -0
- package/dist/public/skills/kibi-usage/SKILL.md +44 -1
- package/dist/public/skills/kibi-usage/resources/fact-lanes.md +4 -2
- package/dist/runtime/cli-runtime.d.ts.map +1 -1
- package/dist/runtime/cli-runtime.js +7 -2
- package/dist/search-answer.d.ts +25 -3
- package/dist/search-answer.d.ts.map +1 -1
- package/dist/search-answer.js +96 -8
- package/dist/utils/branch-store.d.ts.map +1 -1
- package/dist/utils/branch-store.js +4 -2
- package/dist/utils/content-freshness.d.ts.map +1 -1
- package/dist/utils/content-freshness.js +12 -2
- package/dist/utils/symbol-manifest-layout.d.ts +9 -0
- package/dist/utils/symbol-manifest-layout.d.ts.map +1 -0
- package/dist/utils/symbol-manifest-layout.js +18 -0
- package/package.json +3 -3
- package/src/public/operations/check-executor.ts +44 -1
- package/src/public/operations/check-format-shared.ts +35 -1
- package/src/public/operations/contracts.ts +15 -4
- package/src/public/operations/discovery-executors.ts +484 -2
- package/src/public/operations/specs/composite.ts +1 -1
- package/src/public/operations/specs/discovery.ts +70 -3
- package/src/public/operations/specs/upsert.ts +15 -0
- package/src/public/skills/kibi-usage/SKILL.md +44 -1
- package/src/public/skills/kibi-usage/resources/fact-lanes.md +4 -2
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
import {
|
|
43
43
|
OperationJsonDecodeError,
|
|
44
44
|
runOperationJsonQuery,
|
|
45
|
+
toPrologAtom,
|
|
45
46
|
} from "./prolog-json.js";
|
|
46
47
|
import type { OperationContext, PrologPort } from "./runtime-types.js";
|
|
47
48
|
import type { OperationResult } from "./types.js";
|
|
@@ -54,13 +55,87 @@ export type QueryInput = {
|
|
|
54
55
|
readonly sourceFile?: string;
|
|
55
56
|
readonly limit?: number;
|
|
56
57
|
readonly offset?: number;
|
|
58
|
+
/** Typed fact filters (fact entities only); see `FACT_KINDS`. */
|
|
59
|
+
readonly factKind?: string;
|
|
60
|
+
readonly predicate?: string;
|
|
61
|
+
readonly namespace?: string;
|
|
62
|
+
readonly where?: Readonly<Record<string, string>>;
|
|
63
|
+
readonly args?: readonly (string | null)[];
|
|
64
|
+
readonly polarity?: "assert" | "deny";
|
|
65
|
+
readonly scope?: "all" | "current_requirements";
|
|
66
|
+
readonly distinct?: string;
|
|
67
|
+
readonly subjectKey?: string;
|
|
68
|
+
readonly propertyKey?: string;
|
|
57
69
|
};
|
|
58
70
|
|
|
71
|
+
/** A ready-to-run follow-up call the query suggests. */
|
|
72
|
+
export type QueryNextCall = Readonly<{ call: string; why: string }>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Typed-fact view of a kb_query answer: which predicate, property or fact
|
|
76
|
+
* kind was asked, the schema that names its arguments, distinct value counts,
|
|
77
|
+
* the values that do exist when nothing matched, warnings about the question
|
|
78
|
+
* itself and the follow-up calls that make sense next.
|
|
79
|
+
*/
|
|
80
|
+
export type FactQueryPayload = Readonly<{
|
|
81
|
+
mode: "predicate" | "property" | "schemas" | "fact_kind";
|
|
82
|
+
predicate?: Record<string, unknown>;
|
|
83
|
+
property?: Record<string, unknown>;
|
|
84
|
+
factKind?: string;
|
|
85
|
+
schema?: Record<string, unknown> | null;
|
|
86
|
+
subjectFacts?: readonly string[];
|
|
87
|
+
distinct?: Record<string, unknown> | null;
|
|
88
|
+
suggestions?: Record<string, unknown> | null;
|
|
89
|
+
warnings: readonly string[];
|
|
90
|
+
next: readonly QueryNextCall[];
|
|
91
|
+
}>;
|
|
92
|
+
|
|
59
93
|
export type QueryPayload = {
|
|
60
94
|
readonly entities: readonly Record<string, unknown>[];
|
|
61
95
|
readonly count: number;
|
|
96
|
+
readonly factQuery?: FactQueryPayload;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const FACT_KINDS = [
|
|
100
|
+
"subject",
|
|
101
|
+
"property_value",
|
|
102
|
+
"predicate",
|
|
103
|
+
"predicate_schema",
|
|
104
|
+
"observation",
|
|
105
|
+
"meta",
|
|
106
|
+
"rule",
|
|
107
|
+
"rule_schema",
|
|
108
|
+
] as const;
|
|
109
|
+
|
|
110
|
+
/** Which typed filters each question mode reads; any other is refused. */
|
|
111
|
+
const MODE_FILTERS: Record<FactQueryPayload["mode"], ReadonlySet<string>> = {
|
|
112
|
+
schemas: new Set(["factKind", "predicate", "namespace"]),
|
|
113
|
+
predicate: new Set([
|
|
114
|
+
"predicate",
|
|
115
|
+
"namespace",
|
|
116
|
+
"where",
|
|
117
|
+
"args",
|
|
118
|
+
"polarity",
|
|
119
|
+
"scope",
|
|
120
|
+
"distinct",
|
|
121
|
+
]),
|
|
122
|
+
property: new Set(["subjectKey", "propertyKey", "scope"]),
|
|
123
|
+
fact_kind: new Set(["factKind"]),
|
|
62
124
|
};
|
|
63
125
|
|
|
126
|
+
const TYPED_FACT_FILTERS = [
|
|
127
|
+
"factKind",
|
|
128
|
+
"predicate",
|
|
129
|
+
"namespace",
|
|
130
|
+
"where",
|
|
131
|
+
"args",
|
|
132
|
+
"polarity",
|
|
133
|
+
"scope",
|
|
134
|
+
"distinct",
|
|
135
|
+
"subjectKey",
|
|
136
|
+
"propertyKey",
|
|
137
|
+
] as const satisfies readonly (keyof QueryInput)[];
|
|
138
|
+
|
|
64
139
|
export type SearchInput = {
|
|
65
140
|
readonly query: string;
|
|
66
141
|
readonly type?: string;
|
|
@@ -76,7 +151,10 @@ export type SearchInput = {
|
|
|
76
151
|
|
|
77
152
|
export type SearchPayload = {
|
|
78
153
|
readonly results: readonly (SearchMatch | IntentSearchMatch)[];
|
|
154
|
+
/** Matches in the whole ranking, before `limit` and `offset`. */
|
|
79
155
|
readonly count: number;
|
|
156
|
+
/** Rows in `results`: the page this call returned. */
|
|
157
|
+
readonly returned: number;
|
|
80
158
|
readonly truncated?: boolean;
|
|
81
159
|
readonly queryAnalysis?: IntentSearchAnalysis;
|
|
82
160
|
readonly answer?: SearchAnswer;
|
|
@@ -195,6 +273,400 @@ function isStatusPayload(value: unknown): value is StatusPayload {
|
|
|
195
273
|
);
|
|
196
274
|
}
|
|
197
275
|
|
|
276
|
+
function hasTypedFactFilters(input: QueryInput): boolean {
|
|
277
|
+
return TYPED_FACT_FILTERS.some((key) => input[key] !== undefined);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function requireAtomString(value: unknown, field: string): string {
|
|
281
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
282
|
+
throw new Error(`${field} must be a non-empty string.`);
|
|
283
|
+
}
|
|
284
|
+
return value.trim();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function requireEnum<T extends string>(
|
|
288
|
+
value: unknown,
|
|
289
|
+
field: string,
|
|
290
|
+
allowed: readonly T[],
|
|
291
|
+
): T {
|
|
292
|
+
if (typeof value !== "string" || !allowed.includes(value as T)) {
|
|
293
|
+
throw new Error(`${field} must be one of: ${allowed.join(", ")}.`);
|
|
294
|
+
}
|
|
295
|
+
return value as T;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Builds the Prolog argument spec of a predicate question. Named `where`
|
|
300
|
+
* entries are resolved against the predicate's declared argument names in
|
|
301
|
+
* Prolog; positional `args` use `null` as a wildcard.
|
|
302
|
+
*/
|
|
303
|
+
// implements REQ-kibi-query-fact-filters
|
|
304
|
+
function predicateArgSpec(input: QueryInput): string {
|
|
305
|
+
if (input.where !== undefined && input.args !== undefined) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
"Use either where (named arguments) or args (positional), not both.",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (input.where !== undefined) {
|
|
311
|
+
if (
|
|
312
|
+
input.where === null ||
|
|
313
|
+
typeof input.where !== "object" ||
|
|
314
|
+
Array.isArray(input.where)
|
|
315
|
+
) {
|
|
316
|
+
throw new Error("where must be an object of argument name to value.");
|
|
317
|
+
}
|
|
318
|
+
const pairs = Object.entries(input.where).map(([key, value]) => {
|
|
319
|
+
const name = requireAtomString(key, "where argument name");
|
|
320
|
+
const atom = requireAtomString(value, `where.${key}`);
|
|
321
|
+
return `${toPrologAtom(name)}=${toPrologAtom(atom)}`;
|
|
322
|
+
});
|
|
323
|
+
return `where([${pairs.join(",")}])`;
|
|
324
|
+
}
|
|
325
|
+
if (input.args !== undefined) {
|
|
326
|
+
if (!Array.isArray(input.args)) {
|
|
327
|
+
throw new Error("args must be an array of constants (null = any).");
|
|
328
|
+
}
|
|
329
|
+
const entries = input.args.map((value, index) =>
|
|
330
|
+
value === null || value === "*"
|
|
331
|
+
? "'$any'"
|
|
332
|
+
: toPrologAtom(requireAtomString(value, `args[${index}]`)),
|
|
333
|
+
);
|
|
334
|
+
return `[${entries.join(",")}]`;
|
|
335
|
+
}
|
|
336
|
+
return "[]";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
type FactQueryResponse = Readonly<{
|
|
340
|
+
mode: FactQueryPayload["mode"];
|
|
341
|
+
rows: readonly Record<string, unknown>[];
|
|
342
|
+
count: number;
|
|
343
|
+
warnings?: readonly string[];
|
|
344
|
+
predicate?: Record<string, unknown>;
|
|
345
|
+
property?: Record<string, unknown>;
|
|
346
|
+
factKind?: string;
|
|
347
|
+
schema?: Record<string, unknown> | null;
|
|
348
|
+
subjectFacts?: readonly string[];
|
|
349
|
+
distinct?: Record<string, unknown> | null;
|
|
350
|
+
suggestions?: Record<string, unknown> | null;
|
|
351
|
+
}>;
|
|
352
|
+
|
|
353
|
+
function callText(args: Record<string, unknown>): string {
|
|
354
|
+
return `kb_query(${JSON.stringify(args)})`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Every typed answer names the exact call that makes sense next, so an
|
|
359
|
+
* agent never has to guess the follow-up from the shape of the data.
|
|
360
|
+
*/
|
|
361
|
+
// implements REQ-kibi-query-fact-filters
|
|
362
|
+
function factQueryNext(
|
|
363
|
+
input: QueryInput,
|
|
364
|
+
response: FactQueryResponse,
|
|
365
|
+
): QueryNextCall[] {
|
|
366
|
+
const next: QueryNextCall[] = [];
|
|
367
|
+
const first = response.rows[0];
|
|
368
|
+
const firstId = typeof first?.id === "string" ? first.id : undefined;
|
|
369
|
+
switch (response.mode) {
|
|
370
|
+
case "predicate": {
|
|
371
|
+
if (firstId !== undefined) {
|
|
372
|
+
next.push({
|
|
373
|
+
call: callText({ id: firstId }),
|
|
374
|
+
why: "full fact text and relationships of one row",
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const filters = {
|
|
378
|
+
predicate: input.predicate,
|
|
379
|
+
...(input.namespace !== undefined
|
|
380
|
+
? { namespace: input.namespace }
|
|
381
|
+
: {}),
|
|
382
|
+
...(input.where !== undefined ? { where: input.where } : {}),
|
|
383
|
+
...(input.args !== undefined ? { args: input.args } : {}),
|
|
384
|
+
...(input.polarity !== undefined ? { polarity: input.polarity } : {}),
|
|
385
|
+
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
386
|
+
};
|
|
387
|
+
const names = Array.isArray(
|
|
388
|
+
(response.schema as { argumentNames?: unknown } | null)?.argumentNames,
|
|
389
|
+
)
|
|
390
|
+
? (
|
|
391
|
+
response.schema as { argumentNames: unknown[] }
|
|
392
|
+
).argumentNames.filter(
|
|
393
|
+
(name): name is string => typeof name === "string",
|
|
394
|
+
)
|
|
395
|
+
: [];
|
|
396
|
+
const bound = new Set(Object.keys(input.where ?? {}));
|
|
397
|
+
const free = names.filter((name) => !bound.has(name));
|
|
398
|
+
if (
|
|
399
|
+
input.distinct === undefined &&
|
|
400
|
+
response.count > 0 &&
|
|
401
|
+
free.length > 0
|
|
402
|
+
) {
|
|
403
|
+
const argument = free[free.length - 1] ?? free[0];
|
|
404
|
+
next.push({
|
|
405
|
+
call: callText({ ...filters, distinct: argument }),
|
|
406
|
+
why: `count the distinct values of '${argument}' across these facts`,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (
|
|
410
|
+
response.count === 0 &&
|
|
411
|
+
response.suggestions &&
|
|
412
|
+
typeof response.suggestions === "object" &&
|
|
413
|
+
(response.warnings ?? []).length === 0
|
|
414
|
+
) {
|
|
415
|
+
// Suggest a correction only for an argument whose requested value
|
|
416
|
+
// does not exist; a key whose value exists is not the cause.
|
|
417
|
+
for (const [key, values] of Object.entries(response.suggestions)) {
|
|
418
|
+
if (!Array.isArray(values)) continue;
|
|
419
|
+
const requested = input.where?.[key];
|
|
420
|
+
if (requested !== undefined && values.includes(requested)) continue;
|
|
421
|
+
const value = values[0];
|
|
422
|
+
if (typeof value === "string") {
|
|
423
|
+
next.push({
|
|
424
|
+
call: callText({
|
|
425
|
+
...filters,
|
|
426
|
+
where: { ...(input.where ?? {}), [key]: value },
|
|
427
|
+
}),
|
|
428
|
+
why: `'${key}' has no fact with that value; '${value}' is one that exists`,
|
|
429
|
+
});
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (next.length === 0) {
|
|
435
|
+
next.push({
|
|
436
|
+
call: callText({ factKind: "predicate_schema" }),
|
|
437
|
+
why: "list the predicates this KB can answer about and their argument names",
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
case "property": {
|
|
443
|
+
if (firstId !== undefined) {
|
|
444
|
+
next.push({
|
|
445
|
+
call: callText({ id: firstId }),
|
|
446
|
+
why: "full fact text and relationships of one row",
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (input.propertyKey !== undefined) {
|
|
450
|
+
next.push({
|
|
451
|
+
call: callText({ subjectKey: input.subjectKey }),
|
|
452
|
+
why: "every property stated about this subject",
|
|
453
|
+
});
|
|
454
|
+
} else if (response.count === 0) {
|
|
455
|
+
next.push({
|
|
456
|
+
call: callText({ factKind: "subject" }),
|
|
457
|
+
why: "list the subject keys that exist",
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case "schemas": {
|
|
463
|
+
const name = typeof first?.name === "string" ? first.name : undefined;
|
|
464
|
+
if (name !== undefined) {
|
|
465
|
+
next.push({
|
|
466
|
+
call: callText({ predicate: name }),
|
|
467
|
+
why: "every stored fact of the first listed predicate",
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
case "fact_kind": {
|
|
473
|
+
if (input.factKind === "predicate") {
|
|
474
|
+
next.push({
|
|
475
|
+
call: callText({ factKind: "predicate_schema" }),
|
|
476
|
+
why: "argument names and constants for asking by name",
|
|
477
|
+
});
|
|
478
|
+
} else if (input.factKind === "property_value" && first) {
|
|
479
|
+
const subjectKey =
|
|
480
|
+
typeof first.subject_key === "string" ? first.subject_key : undefined;
|
|
481
|
+
if (subjectKey !== undefined) {
|
|
482
|
+
next.push({
|
|
483
|
+
call: callText({ subjectKey }),
|
|
484
|
+
why: "the properties of the first listed subject",
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
} else if (firstId !== undefined) {
|
|
488
|
+
next.push({
|
|
489
|
+
call: callText({ id: firstId }),
|
|
490
|
+
why: "full fact text of one row",
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return next;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function factRowSummary(row: Record<string, unknown>): string {
|
|
500
|
+
const id = String(row.id ?? "");
|
|
501
|
+
if (typeof row.name === "string" && Array.isArray(row.args)) {
|
|
502
|
+
const effect = typeof row.effect === "string" ? ` -> ${row.effect}` : "";
|
|
503
|
+
const polarity = row.polarity === "deny" ? " [deny]" : "";
|
|
504
|
+
return `${id}: ${row.name}(${row.args.map(String).join(", ")})${effect}${polarity}`;
|
|
505
|
+
}
|
|
506
|
+
if (
|
|
507
|
+
typeof row.subjectKey === "string" &&
|
|
508
|
+
typeof row.propertyKey === "string"
|
|
509
|
+
) {
|
|
510
|
+
const unit = typeof row.unit === "string" && row.unit ? ` ${row.unit}` : "";
|
|
511
|
+
return `${id}: ${row.subjectKey} ${row.propertyKey} ${String(row.operator ?? "")} ${String(row.value ?? "")}${unit}`;
|
|
512
|
+
}
|
|
513
|
+
if (typeof row.name === "string" && typeof row.arity === "number") {
|
|
514
|
+
const names = Array.isArray(row.argumentNames)
|
|
515
|
+
? row.argumentNames.map(String).join(", ")
|
|
516
|
+
: "";
|
|
517
|
+
return `${row.name}(${names})`;
|
|
518
|
+
}
|
|
519
|
+
return `${id} (${String(row.title ?? "")})`;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Typed fact questions run as validated Prolog goals in the discovery module;
|
|
524
|
+
* the executor never forwards caller-written Prolog.
|
|
525
|
+
*/
|
|
526
|
+
// implements REQ-kibi-query-fact-filters
|
|
527
|
+
async function executeFactQuery(
|
|
528
|
+
input: QueryInput,
|
|
529
|
+
context: OperationContext,
|
|
530
|
+
limit: number,
|
|
531
|
+
offset: number,
|
|
532
|
+
): Promise<OperationResult<QueryPayload>> {
|
|
533
|
+
if (input.type !== undefined && input.type !== "fact") {
|
|
534
|
+
throw new Error(
|
|
535
|
+
"Typed fact filters (factKind, predicate, subjectKey, ...) apply to type 'fact' only.",
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
for (const key of ["id", "tags", "sourceFile"] as const) {
|
|
539
|
+
if (input[key] !== undefined) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`${key} cannot be combined with typed fact filters; run a separate kb_query.`,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const prolog = requireProlog(context);
|
|
546
|
+
if (
|
|
547
|
+
!Number.isFinite(limit) ||
|
|
548
|
+
!Number.isFinite(offset) ||
|
|
549
|
+
limit < 0 ||
|
|
550
|
+
offset < 0
|
|
551
|
+
) {
|
|
552
|
+
throw new Error("limit and offset must be non-negative numbers.");
|
|
553
|
+
}
|
|
554
|
+
const page = `${Math.trunc(limit)}, ${Math.trunc(offset)}`;
|
|
555
|
+
const namespace =
|
|
556
|
+
input.namespace === undefined
|
|
557
|
+
? "none"
|
|
558
|
+
: toPrologAtom(requireAtomString(input.namespace, "namespace"));
|
|
559
|
+
const mode =
|
|
560
|
+
input.factKind === "predicate_schema"
|
|
561
|
+
? "schemas"
|
|
562
|
+
: input.predicate !== undefined
|
|
563
|
+
? "predicate"
|
|
564
|
+
: input.subjectKey !== undefined
|
|
565
|
+
? "property"
|
|
566
|
+
: input.factKind !== undefined
|
|
567
|
+
? "fact_kind"
|
|
568
|
+
: undefined;
|
|
569
|
+
if (mode === undefined) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
"Typed fact filters need a predicate, a subjectKey or a factKind; where/args/polarity/distinct/propertyKey refine one of those.",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const unused = TYPED_FACT_FILTERS.filter(
|
|
575
|
+
(key) => input[key] !== undefined && !MODE_FILTERS[mode].has(key),
|
|
576
|
+
);
|
|
577
|
+
if (unused.length > 0) {
|
|
578
|
+
throw new Error(
|
|
579
|
+
`${unused.join(", ")} do${unused.length === 1 ? "es" : ""} not apply to a ${mode} question; allowed with it: ${[...MODE_FILTERS[mode]].join(", ")}.`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
const scope =
|
|
583
|
+
input.scope === undefined
|
|
584
|
+
? "all"
|
|
585
|
+
: requireEnum(input.scope, "scope", ["all", "current_requirements"]);
|
|
586
|
+
const factKind =
|
|
587
|
+
input.factKind === undefined
|
|
588
|
+
? undefined
|
|
589
|
+
: requireEnum(input.factKind, "factKind", FACT_KINDS);
|
|
590
|
+
let goal: string;
|
|
591
|
+
if (mode === "schemas") {
|
|
592
|
+
goal = `discovery:ask_schemas_json(${namespace}, ${input.predicate === undefined ? "none" : toPrologAtom(requireAtomString(input.predicate, "predicate"))}, ${page}, JsonString)`;
|
|
593
|
+
} else if (mode === "predicate") {
|
|
594
|
+
const name = requireAtomString(input.predicate, "predicate");
|
|
595
|
+
const polarity =
|
|
596
|
+
input.polarity === undefined
|
|
597
|
+
? "none"
|
|
598
|
+
: toPrologAtom(
|
|
599
|
+
requireEnum(input.polarity, "polarity", ["assert", "deny"]),
|
|
600
|
+
);
|
|
601
|
+
const distinct =
|
|
602
|
+
input.distinct === undefined
|
|
603
|
+
? "none"
|
|
604
|
+
: toPrologAtom(requireAtomString(input.distinct, "distinct"));
|
|
605
|
+
goal = `discovery:ask_predicate_json(${namespace}, ${toPrologAtom(name)}, ${predicateArgSpec(input)}, ${polarity}, ${scope}, ${distinct}, ${page}, JsonString)`;
|
|
606
|
+
} else if (mode === "property") {
|
|
607
|
+
const subject = requireAtomString(input.subjectKey, "subjectKey");
|
|
608
|
+
const property =
|
|
609
|
+
input.propertyKey === undefined
|
|
610
|
+
? "none"
|
|
611
|
+
: toPrologAtom(requireAtomString(input.propertyKey, "propertyKey"));
|
|
612
|
+
goal = `discovery:ask_property_json(${toPrologAtom(subject)}, ${property}, ${scope}, ${page}, JsonString)`;
|
|
613
|
+
} else {
|
|
614
|
+
goal = `discovery:ask_fact_kind_json(${toPrologAtom(factKind)}, ${page}, JsonString)`;
|
|
615
|
+
}
|
|
616
|
+
const response = await runOperationJsonQuery<FactQueryResponse>(
|
|
617
|
+
prolog,
|
|
618
|
+
"discovery.pl",
|
|
619
|
+
goal,
|
|
620
|
+
"Query execution",
|
|
621
|
+
context.signal,
|
|
622
|
+
);
|
|
623
|
+
const rows = Array.isArray(response.rows) ? response.rows : [];
|
|
624
|
+
const count =
|
|
625
|
+
typeof response.count === "number" ? response.count : rows.length;
|
|
626
|
+
const warnings = Array.isArray(response.warnings) ? response.warnings : [];
|
|
627
|
+
const factQuery: FactQueryPayload = {
|
|
628
|
+
mode: response.mode,
|
|
629
|
+
...(response.predicate ? { predicate: response.predicate } : {}),
|
|
630
|
+
...(response.property ? { property: response.property } : {}),
|
|
631
|
+
...(response.factKind ? { factKind: response.factKind } : {}),
|
|
632
|
+
...(response.schema !== undefined ? { schema: response.schema } : {}),
|
|
633
|
+
...(response.subjectFacts ? { subjectFacts: response.subjectFacts } : {}),
|
|
634
|
+
...(response.distinct !== undefined ? { distinct: response.distinct } : {}),
|
|
635
|
+
...(response.suggestions !== undefined
|
|
636
|
+
? { suggestions: response.suggestions }
|
|
637
|
+
: {}),
|
|
638
|
+
warnings,
|
|
639
|
+
next: factQueryNext(input, { ...response, rows, count }),
|
|
640
|
+
};
|
|
641
|
+
const subject =
|
|
642
|
+
response.mode === "predicate"
|
|
643
|
+
? `${String(input.predicate)} facts`
|
|
644
|
+
: response.mode === "property"
|
|
645
|
+
? `properties of ${String(input.subjectKey)}`
|
|
646
|
+
: response.mode === "schemas"
|
|
647
|
+
? "predicate schemas"
|
|
648
|
+
: `${String(factKind)} facts`;
|
|
649
|
+
const lines = [
|
|
650
|
+
count === 0
|
|
651
|
+
? `No ${subject} matched.`
|
|
652
|
+
: `${count} ${subject}. Showing ${rows.length} (offset ${offset}, limit ${limit}): ${rows.map(factRowSummary).join("; ")}`,
|
|
653
|
+
...(factQuery.distinct && typeof factQuery.distinct === "object"
|
|
654
|
+
? [
|
|
655
|
+
`Distinct ${String((factQuery.distinct as { argument?: unknown }).argument)}: ${JSON.stringify((factQuery.distinct as { values?: unknown }).values ?? [])}`,
|
|
656
|
+
]
|
|
657
|
+
: []),
|
|
658
|
+
...(factQuery.suggestions && typeof factQuery.suggestions === "object"
|
|
659
|
+
? [`Values that exist: ${JSON.stringify(factQuery.suggestions)}`]
|
|
660
|
+
: []),
|
|
661
|
+
...warnings.map((warning) => `Warning: ${warning}`),
|
|
662
|
+
...factQuery.next.map((entry) => `Next: ${entry.call} (${entry.why})`),
|
|
663
|
+
];
|
|
664
|
+
return {
|
|
665
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
666
|
+
structuredContent: { entities: rows, count, factQuery },
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
198
670
|
export async function executeQuery(
|
|
199
671
|
input: QueryInput,
|
|
200
672
|
context: OperationContext,
|
|
@@ -203,6 +675,9 @@ export async function executeQuery(
|
|
|
203
675
|
const { type, id, tags, sourceFile, limit = 100, offset = 0 } = input;
|
|
204
676
|
try {
|
|
205
677
|
validateEntityType(type);
|
|
678
|
+
if (hasTypedFactFilters(input)) {
|
|
679
|
+
return await executeFactQuery(input, context, limit, offset);
|
|
680
|
+
}
|
|
206
681
|
const prolog = requireProlog(context);
|
|
207
682
|
const signal = context.signal;
|
|
208
683
|
const pageInput = {
|
|
@@ -300,7 +775,7 @@ export async function executeSearch(
|
|
|
300
775
|
const {
|
|
301
776
|
query,
|
|
302
777
|
type,
|
|
303
|
-
limit =
|
|
778
|
+
limit = 10,
|
|
304
779
|
offset = 0,
|
|
305
780
|
rankingMode = "intent-v1",
|
|
306
781
|
semanticFacets,
|
|
@@ -357,6 +832,11 @@ export async function executeSearch(
|
|
|
357
832
|
: undefined;
|
|
358
833
|
// The structured payload carries every row; the text is a short
|
|
359
834
|
// human-readable digest instead of a second copy of the results.
|
|
835
|
+
// A top match no requirement owns is said in the digest too, so the
|
|
836
|
+
// governing list is never read as the top match's requirement.
|
|
837
|
+
const topUngoverned = answerLayer?.ungoverned.find(
|
|
838
|
+
(entry) => entry.rank === 1,
|
|
839
|
+
);
|
|
360
840
|
const text = `${
|
|
361
841
|
intentResult.matches.length === 0
|
|
362
842
|
? `No intent search results for '${trimmedQuery}' (abstained).`
|
|
@@ -365,13 +845,14 @@ export async function executeSearch(
|
|
|
365
845
|
.map((match) => String(match.entity.id ?? ""))
|
|
366
846
|
.join(
|
|
367
847
|
", ",
|
|
368
|
-
)}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}.`
|
|
848
|
+
)}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}${topUngoverned ? ` (the top match ${topUngoverned.id} is linked to no requirement)` : ""}.`
|
|
369
849
|
}${storeNote}`;
|
|
370
850
|
return {
|
|
371
851
|
content: [{ type: "text", text }],
|
|
372
852
|
structuredContent: {
|
|
373
853
|
results: await projectMatches(prolog, paginated, fields),
|
|
374
854
|
count: intentResult.matches.length,
|
|
855
|
+
returned: paginated.length,
|
|
375
856
|
truncated: offset + paginated.length < intentResult.matches.length,
|
|
376
857
|
queryAnalysis: intentResult.analysis,
|
|
377
858
|
...(answerLayer ? { answer: answerLayer } : {}),
|
|
@@ -411,6 +892,7 @@ export async function executeSearch(
|
|
|
411
892
|
structuredContent: {
|
|
412
893
|
results: await projectMatches(prolog, paginated, fields),
|
|
413
894
|
count: matches.length,
|
|
895
|
+
returned: paginated.length,
|
|
414
896
|
truncated: offset + paginated.length < matches.length,
|
|
415
897
|
...branchFields,
|
|
416
898
|
},
|
|
@@ -217,7 +217,7 @@ export const modelSpec = {
|
|
|
217
217
|
name: "kb_model",
|
|
218
218
|
cliName: "model",
|
|
219
219
|
description:
|
|
220
|
-
"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.
|
|
220
|
+
"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.",
|
|
221
221
|
businessInputSchema: {
|
|
222
222
|
type: "object",
|
|
223
223
|
required: ["mode", "text"],
|
|
@@ -24,11 +24,22 @@ const ENTITY_TYPES = [
|
|
|
24
24
|
"fact",
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
|
+
const FACT_KINDS = [
|
|
28
|
+
"subject",
|
|
29
|
+
"property_value",
|
|
30
|
+
"predicate",
|
|
31
|
+
"predicate_schema",
|
|
32
|
+
"observation",
|
|
33
|
+
"meta",
|
|
34
|
+
"rule",
|
|
35
|
+
"rule_schema",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
27
38
|
export const querySpec = {
|
|
28
39
|
name: "kb_query",
|
|
29
40
|
cliName: "query",
|
|
30
41
|
description:
|
|
31
|
-
"Read entities from the KB with filters. Use for
|
|
42
|
+
"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.",
|
|
32
43
|
businessInputSchema: {
|
|
33
44
|
type: "object",
|
|
34
45
|
properties: {
|
|
@@ -54,6 +65,61 @@ export const querySpec = {
|
|
|
54
65
|
description:
|
|
55
66
|
"Optional source-file substring filter. Example: 'src/auth/login.ts'. Uses KB source linkage, not file-system scanning.",
|
|
56
67
|
},
|
|
68
|
+
factKind: {
|
|
69
|
+
type: "string",
|
|
70
|
+
enum: FACT_KINDS,
|
|
71
|
+
description:
|
|
72
|
+
"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'.",
|
|
73
|
+
},
|
|
74
|
+
predicate: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description:
|
|
77
|
+
"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.",
|
|
78
|
+
},
|
|
79
|
+
namespace: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description:
|
|
82
|
+
"Optional predicate namespace when the same name exists in several. Default: any namespace.",
|
|
83
|
+
},
|
|
84
|
+
where: {
|
|
85
|
+
type: "object",
|
|
86
|
+
additionalProperties: { type: "string" },
|
|
87
|
+
description:
|
|
88
|
+
"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.",
|
|
89
|
+
},
|
|
90
|
+
args: {
|
|
91
|
+
type: "array",
|
|
92
|
+
items: { type: ["string", "null"] },
|
|
93
|
+
description:
|
|
94
|
+
"Positional argument values for predicate when it has no schema; null matches any value. Example: ['admin', null, null, 'allow']. Prefer where.",
|
|
95
|
+
},
|
|
96
|
+
polarity: {
|
|
97
|
+
type: "string",
|
|
98
|
+
enum: ["assert", "deny"],
|
|
99
|
+
description:
|
|
100
|
+
"Optional polarity filter for predicate facts. Default: both.",
|
|
101
|
+
},
|
|
102
|
+
scope: {
|
|
103
|
+
type: "string",
|
|
104
|
+
enum: ["all", "current_requirements"],
|
|
105
|
+
description:
|
|
106
|
+
"Optional: 'current_requirements' keeps only facts a current (not superseded or retired) requirement requires.",
|
|
107
|
+
},
|
|
108
|
+
distinct: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description:
|
|
111
|
+
"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.",
|
|
112
|
+
},
|
|
113
|
+
subjectKey: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description:
|
|
116
|
+
"Typed fact filter: dotted subject key whose property facts to list, e.g. 'user.role_assignment'. Combine with propertyKey for one property.",
|
|
117
|
+
},
|
|
118
|
+
propertyKey: {
|
|
119
|
+
type: "string",
|
|
120
|
+
description:
|
|
121
|
+
"Optional property key under subjectKey, e.g. 'max_roles'. Returns operator, value and unit of each fact.",
|
|
122
|
+
},
|
|
57
123
|
limit: {
|
|
58
124
|
type: "number",
|
|
59
125
|
default: 100,
|
|
@@ -99,8 +165,9 @@ export const searchSpec = {
|
|
|
99
165
|
limit: {
|
|
100
166
|
type: "integer",
|
|
101
167
|
minimum: 0,
|
|
102
|
-
default:
|
|
103
|
-
description:
|
|
168
|
+
default: 10,
|
|
169
|
+
description:
|
|
170
|
+
"Optional max rows to return after ranking. Default: 10; raise it or page with offset when the answer layer is not enough.",
|
|
104
171
|
},
|
|
105
172
|
offset: {
|
|
106
173
|
type: "integer",
|
|
@@ -38,6 +38,21 @@ export const upsertSpec = {
|
|
|
38
38
|
description:
|
|
39
39
|
"Validate the payload and return schema/modeling errors, a normalized preview and the semantic advisor receipt without writing the KB or workspace. Use it as the preflight before the real write.",
|
|
40
40
|
},
|
|
41
|
+
plannedFacts: {
|
|
42
|
+
type: "array",
|
|
43
|
+
description:
|
|
44
|
+
"dryRun only. Fact upsert payloads (type, id, properties, optional relationships) that will be written before this payload, typically the applyPlan entries kb_model returned. They are validated in order and staged with the payload, so a requires_predicate or requires_property target that does not exist yet resolves, its claim_key is read from the plan, and the contradiction preview runs against the KB as it would be after the whole batch (result: stagedFacts, whatIf). A conflict the batch would introduce, such as an allow and a deny permission_rule on the same key, is refused as the commit would refuse it. Rejected without dryRun: write the facts first.",
|
|
45
|
+
items: {
|
|
46
|
+
type: "object",
|
|
47
|
+
required: ["type", "id", "properties"],
|
|
48
|
+
properties: {
|
|
49
|
+
type: { type: "string", const: "fact" },
|
|
50
|
+
id: { type: "string", minLength: 1 },
|
|
51
|
+
properties: { type: "object" },
|
|
52
|
+
relationships: RELATIONSHIPS_SCHEMA,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
41
56
|
document: {
|
|
42
57
|
type: "object",
|
|
43
58
|
additionalProperties: false,
|