kibi-cli 2.17.0 → 2.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.
- package/dist/cli-operation-metadata.d.ts +1 -1
- package/dist/cli-operation-metadata.js +1 -1
- package/dist/cli-register-proof.js +9 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +18 -6
- 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 +81 -9
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +76 -6
- package/dist/engine.d.ts +4 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +56 -33
- package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-applicability.js +5 -2
- package/dist/operations/modeling/predicate-catalog-4.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-catalog-4.js +4 -0
- package/dist/operations/modeling/predicate-loader.d.ts.map +1 -1
- package/dist/operations/modeling/predicate-loader.js +3 -0
- package/dist/operations/modeling/predicate-score-3.js +1 -1
- package/dist/operations/modeling/predicate-types.d.ts +5 -0
- package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.d.ts +9 -0
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +30 -1
- package/dist/operations/planning/apply-plan.d.ts.map +1 -1
- package/dist/operations/planning/apply-plan.js +10 -1
- package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/clauses.js +7 -1
- package/dist/prolog/store-lock.d.ts +41 -0
- package/dist/prolog/store-lock.d.ts.map +1 -1
- package/dist/prolog/store-lock.js +91 -1
- package/dist/prolog.d.ts +7 -1
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +21 -3
- package/dist/proof/inspect.d.ts +8 -1
- package/dist/proof/inspect.d.ts.map +1 -1
- package/dist/proof/inspect.js +19 -2
- package/dist/public/operations/catalog.d.ts +50 -1
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +4 -4
- package/dist/public/operations/discovery-executors.d.ts +50 -1
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +418 -8
- package/dist/public/operations/migration-plan.d.ts +2 -0
- package/dist/public/operations/migration-plan.d.ts.map +1 -1
- package/dist/public/operations/migration-plan.js +31 -0
- package/dist/public/operations/source-relationship-parity.d.ts.map +1 -1
- package/dist/public/operations/source-relationship-parity.js +55 -1
- package/dist/public/operations/specs/discovery.d.ts +50 -1
- package/dist/public/operations/specs/discovery.d.ts.map +1 -1
- package/dist/public/operations/specs/discovery.js +56 -1
- package/dist/public/skills/kibi-usage/SKILL.md +20 -3
- package/dist/public/skills/kibi-usage/resources/proof.md +23 -0
- package/dist/runtime/cli-runtime.d.ts.map +1 -1
- package/dist/runtime/cli-runtime.js +7 -2
- package/dist/search-answer.d.ts +12 -3
- package/dist/search-answer.d.ts.map +1 -1
- package/dist/search-answer.js +51 -4
- package/dist/utils/branch-store.d.ts +8 -0
- package/dist/utils/branch-store.d.ts.map +1 -1
- package/dist/utils/branch-store.js +27 -1
- package/dist/utils/content-freshness.d.ts +13 -0
- package/dist/utils/content-freshness.d.ts.map +1 -0
- package/dist/utils/content-freshness.js +171 -0
- 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 +2 -2
- package/src/public/operations/contracts.ts +7 -4
- package/src/public/operations/discovery-executors.ts +556 -6
- package/src/public/operations/migration-plan.ts +44 -0
- package/src/public/operations/source-relationship-parity.ts +60 -3
- package/src/public/operations/specs/discovery.ts +67 -1
- package/src/public/skills/kibi-usage/SKILL.md +20 -3
- package/src/public/skills/kibi-usage/resources/proof.md +23 -0
|
@@ -21,8 +21,10 @@ import {
|
|
|
21
21
|
branchStoreReason,
|
|
22
22
|
inspectBranchStore,
|
|
23
23
|
storeLockJournalReason,
|
|
24
|
+
storeLockTakeoverReason,
|
|
24
25
|
uncompiledBranchStoreReason,
|
|
25
26
|
} from "../../utils/branch-store.js";
|
|
27
|
+
import { confirmSourceNewerReasons } from "../../utils/content-freshness.js";
|
|
26
28
|
import {
|
|
27
29
|
loadEntities,
|
|
28
30
|
loadSearchCandidates,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
import {
|
|
41
43
|
OperationJsonDecodeError,
|
|
42
44
|
runOperationJsonQuery,
|
|
45
|
+
toPrologAtom,
|
|
43
46
|
} from "./prolog-json.js";
|
|
44
47
|
import type { OperationContext, PrologPort } from "./runtime-types.js";
|
|
45
48
|
import type { OperationResult } from "./types.js";
|
|
@@ -52,13 +55,87 @@ export type QueryInput = {
|
|
|
52
55
|
readonly sourceFile?: string;
|
|
53
56
|
readonly limit?: number;
|
|
54
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;
|
|
55
69
|
};
|
|
56
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
|
+
|
|
57
93
|
export type QueryPayload = {
|
|
58
94
|
readonly entities: readonly Record<string, unknown>[];
|
|
59
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"]),
|
|
60
124
|
};
|
|
61
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
|
+
|
|
62
139
|
export type SearchInput = {
|
|
63
140
|
readonly query: string;
|
|
64
141
|
readonly type?: string;
|
|
@@ -78,8 +155,47 @@ export type SearchPayload = {
|
|
|
78
155
|
readonly truncated?: boolean;
|
|
79
156
|
readonly queryAnalysis?: IntentSearchAnalysis;
|
|
80
157
|
readonly answer?: SearchAnswer;
|
|
158
|
+
/** The Kibi branch whose store was searched, when the runtime attached one. */
|
|
159
|
+
readonly branch?: string | null;
|
|
160
|
+
/** Present when the searched store was never compiled for this branch. */
|
|
161
|
+
readonly branchStore?: SearchBranchStoreNotice;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export type SearchBranchStoreNotice = {
|
|
165
|
+
readonly state: "not_compiled";
|
|
166
|
+
readonly code: typeof BRANCH_STORE_NOT_COMPILED;
|
|
167
|
+
readonly path: string;
|
|
168
|
+
readonly detail: string;
|
|
169
|
+
readonly remediation: Readonly<Record<string, unknown>>;
|
|
81
170
|
};
|
|
82
171
|
|
|
172
|
+
/**
|
|
173
|
+
* An empty result from a store that was never compiled says nothing about
|
|
174
|
+
* the authored requirements, so search reports the store state itself (the
|
|
175
|
+
* same reason kb_status and kb_check use) instead of a bare "no results".
|
|
176
|
+
*/
|
|
177
|
+
// implements REQ-cli-status-pre-first-sync, REQ-mcp-search-discovery
|
|
178
|
+
function uncompiledSearchNotice(
|
|
179
|
+
context: OperationContext,
|
|
180
|
+
): SearchBranchStoreNotice | null {
|
|
181
|
+
const attachment = context.branchAttachment;
|
|
182
|
+
if (attachment === undefined) return null;
|
|
183
|
+
const store = inspectBranchStore(context.workspaceRoot, attachment.kbBranch);
|
|
184
|
+
const reason = uncompiledBranchStoreReason(
|
|
185
|
+
context.workspaceRoot,
|
|
186
|
+
store,
|
|
187
|
+
attachment.kbBranch,
|
|
188
|
+
);
|
|
189
|
+
if (reason === null) return null;
|
|
190
|
+
return {
|
|
191
|
+
state: "not_compiled",
|
|
192
|
+
code: BRANCH_STORE_NOT_COMPILED,
|
|
193
|
+
path: store.path,
|
|
194
|
+
detail: String(reason.detail),
|
|
195
|
+
remediation: reason.remediation as Readonly<Record<string, unknown>>,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
83
199
|
export type StatusInput = Readonly<Record<string, never>>;
|
|
84
200
|
|
|
85
201
|
export type StatusPayload = {
|
|
@@ -99,6 +215,8 @@ export type StatusPayload = {
|
|
|
99
215
|
readonly staleReasons?: readonly Record<string, unknown>[];
|
|
100
216
|
readonly staleReasonCount?: number;
|
|
101
217
|
readonly staleReasonsTruncated?: boolean;
|
|
218
|
+
/** Informational, non-blocking observations (never make the KB stale). */
|
|
219
|
+
readonly notices?: readonly Record<string, unknown>[];
|
|
102
220
|
readonly branchAttachment?: {
|
|
103
221
|
readonly gitBranch: string;
|
|
104
222
|
readonly kbBranch: string;
|
|
@@ -152,6 +270,400 @@ function isStatusPayload(value: unknown): value is StatusPayload {
|
|
|
152
270
|
);
|
|
153
271
|
}
|
|
154
272
|
|
|
273
|
+
function hasTypedFactFilters(input: QueryInput): boolean {
|
|
274
|
+
return TYPED_FACT_FILTERS.some((key) => input[key] !== undefined);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function requireAtomString(value: unknown, field: string): string {
|
|
278
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
279
|
+
throw new Error(`${field} must be a non-empty string.`);
|
|
280
|
+
}
|
|
281
|
+
return value.trim();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function requireEnum<T extends string>(
|
|
285
|
+
value: unknown,
|
|
286
|
+
field: string,
|
|
287
|
+
allowed: readonly T[],
|
|
288
|
+
): T {
|
|
289
|
+
if (typeof value !== "string" || !allowed.includes(value as T)) {
|
|
290
|
+
throw new Error(`${field} must be one of: ${allowed.join(", ")}.`);
|
|
291
|
+
}
|
|
292
|
+
return value as T;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Builds the Prolog argument spec of a predicate question. Named `where`
|
|
297
|
+
* entries are resolved against the predicate's declared argument names in
|
|
298
|
+
* Prolog; positional `args` use `null` as a wildcard.
|
|
299
|
+
*/
|
|
300
|
+
// implements REQ-kibi-query-fact-filters
|
|
301
|
+
function predicateArgSpec(input: QueryInput): string {
|
|
302
|
+
if (input.where !== undefined && input.args !== undefined) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
"Use either where (named arguments) or args (positional), not both.",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (input.where !== undefined) {
|
|
308
|
+
if (
|
|
309
|
+
input.where === null ||
|
|
310
|
+
typeof input.where !== "object" ||
|
|
311
|
+
Array.isArray(input.where)
|
|
312
|
+
) {
|
|
313
|
+
throw new Error("where must be an object of argument name to value.");
|
|
314
|
+
}
|
|
315
|
+
const pairs = Object.entries(input.where).map(([key, value]) => {
|
|
316
|
+
const name = requireAtomString(key, "where argument name");
|
|
317
|
+
const atom = requireAtomString(value, `where.${key}`);
|
|
318
|
+
return `${toPrologAtom(name)}=${toPrologAtom(atom)}`;
|
|
319
|
+
});
|
|
320
|
+
return `where([${pairs.join(",")}])`;
|
|
321
|
+
}
|
|
322
|
+
if (input.args !== undefined) {
|
|
323
|
+
if (!Array.isArray(input.args)) {
|
|
324
|
+
throw new Error("args must be an array of constants (null = any).");
|
|
325
|
+
}
|
|
326
|
+
const entries = input.args.map((value, index) =>
|
|
327
|
+
value === null || value === "*"
|
|
328
|
+
? "'$any'"
|
|
329
|
+
: toPrologAtom(requireAtomString(value, `args[${index}]`)),
|
|
330
|
+
);
|
|
331
|
+
return `[${entries.join(",")}]`;
|
|
332
|
+
}
|
|
333
|
+
return "[]";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
type FactQueryResponse = Readonly<{
|
|
337
|
+
mode: FactQueryPayload["mode"];
|
|
338
|
+
rows: readonly Record<string, unknown>[];
|
|
339
|
+
count: number;
|
|
340
|
+
warnings?: readonly string[];
|
|
341
|
+
predicate?: Record<string, unknown>;
|
|
342
|
+
property?: Record<string, unknown>;
|
|
343
|
+
factKind?: string;
|
|
344
|
+
schema?: Record<string, unknown> | null;
|
|
345
|
+
subjectFacts?: readonly string[];
|
|
346
|
+
distinct?: Record<string, unknown> | null;
|
|
347
|
+
suggestions?: Record<string, unknown> | null;
|
|
348
|
+
}>;
|
|
349
|
+
|
|
350
|
+
function callText(args: Record<string, unknown>): string {
|
|
351
|
+
return `kb_query(${JSON.stringify(args)})`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Every typed answer names the exact call that makes sense next, so an
|
|
356
|
+
* agent never has to guess the follow-up from the shape of the data.
|
|
357
|
+
*/
|
|
358
|
+
// implements REQ-kibi-query-fact-filters
|
|
359
|
+
function factQueryNext(
|
|
360
|
+
input: QueryInput,
|
|
361
|
+
response: FactQueryResponse,
|
|
362
|
+
): QueryNextCall[] {
|
|
363
|
+
const next: QueryNextCall[] = [];
|
|
364
|
+
const first = response.rows[0];
|
|
365
|
+
const firstId = typeof first?.id === "string" ? first.id : undefined;
|
|
366
|
+
switch (response.mode) {
|
|
367
|
+
case "predicate": {
|
|
368
|
+
if (firstId !== undefined) {
|
|
369
|
+
next.push({
|
|
370
|
+
call: callText({ id: firstId }),
|
|
371
|
+
why: "full fact text and relationships of one row",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const filters = {
|
|
375
|
+
predicate: input.predicate,
|
|
376
|
+
...(input.namespace !== undefined
|
|
377
|
+
? { namespace: input.namespace }
|
|
378
|
+
: {}),
|
|
379
|
+
...(input.where !== undefined ? { where: input.where } : {}),
|
|
380
|
+
...(input.args !== undefined ? { args: input.args } : {}),
|
|
381
|
+
...(input.polarity !== undefined ? { polarity: input.polarity } : {}),
|
|
382
|
+
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
383
|
+
};
|
|
384
|
+
const names = Array.isArray(
|
|
385
|
+
(response.schema as { argumentNames?: unknown } | null)?.argumentNames,
|
|
386
|
+
)
|
|
387
|
+
? (
|
|
388
|
+
response.schema as { argumentNames: unknown[] }
|
|
389
|
+
).argumentNames.filter(
|
|
390
|
+
(name): name is string => typeof name === "string",
|
|
391
|
+
)
|
|
392
|
+
: [];
|
|
393
|
+
const bound = new Set(Object.keys(input.where ?? {}));
|
|
394
|
+
const free = names.filter((name) => !bound.has(name));
|
|
395
|
+
if (
|
|
396
|
+
input.distinct === undefined &&
|
|
397
|
+
response.count > 0 &&
|
|
398
|
+
free.length > 0
|
|
399
|
+
) {
|
|
400
|
+
const argument = free[free.length - 1] ?? free[0];
|
|
401
|
+
next.push({
|
|
402
|
+
call: callText({ ...filters, distinct: argument }),
|
|
403
|
+
why: `count the distinct values of '${argument}' across these facts`,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (
|
|
407
|
+
response.count === 0 &&
|
|
408
|
+
response.suggestions &&
|
|
409
|
+
typeof response.suggestions === "object" &&
|
|
410
|
+
(response.warnings ?? []).length === 0
|
|
411
|
+
) {
|
|
412
|
+
// Suggest a correction only for an argument whose requested value
|
|
413
|
+
// does not exist; a key whose value exists is not the cause.
|
|
414
|
+
for (const [key, values] of Object.entries(response.suggestions)) {
|
|
415
|
+
if (!Array.isArray(values)) continue;
|
|
416
|
+
const requested = input.where?.[key];
|
|
417
|
+
if (requested !== undefined && values.includes(requested)) continue;
|
|
418
|
+
const value = values[0];
|
|
419
|
+
if (typeof value === "string") {
|
|
420
|
+
next.push({
|
|
421
|
+
call: callText({
|
|
422
|
+
...filters,
|
|
423
|
+
where: { ...(input.where ?? {}), [key]: value },
|
|
424
|
+
}),
|
|
425
|
+
why: `'${key}' has no fact with that value; '${value}' is one that exists`,
|
|
426
|
+
});
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (next.length === 0) {
|
|
432
|
+
next.push({
|
|
433
|
+
call: callText({ factKind: "predicate_schema" }),
|
|
434
|
+
why: "list the predicates this KB can answer about and their argument names",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
case "property": {
|
|
440
|
+
if (firstId !== undefined) {
|
|
441
|
+
next.push({
|
|
442
|
+
call: callText({ id: firstId }),
|
|
443
|
+
why: "full fact text and relationships of one row",
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (input.propertyKey !== undefined) {
|
|
447
|
+
next.push({
|
|
448
|
+
call: callText({ subjectKey: input.subjectKey }),
|
|
449
|
+
why: "every property stated about this subject",
|
|
450
|
+
});
|
|
451
|
+
} else if (response.count === 0) {
|
|
452
|
+
next.push({
|
|
453
|
+
call: callText({ factKind: "subject" }),
|
|
454
|
+
why: "list the subject keys that exist",
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
case "schemas": {
|
|
460
|
+
const name = typeof first?.name === "string" ? first.name : undefined;
|
|
461
|
+
if (name !== undefined) {
|
|
462
|
+
next.push({
|
|
463
|
+
call: callText({ predicate: name }),
|
|
464
|
+
why: "every stored fact of the first listed predicate",
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
case "fact_kind": {
|
|
470
|
+
if (input.factKind === "predicate") {
|
|
471
|
+
next.push({
|
|
472
|
+
call: callText({ factKind: "predicate_schema" }),
|
|
473
|
+
why: "argument names and constants for asking by name",
|
|
474
|
+
});
|
|
475
|
+
} else if (input.factKind === "property_value" && first) {
|
|
476
|
+
const subjectKey =
|
|
477
|
+
typeof first.subject_key === "string" ? first.subject_key : undefined;
|
|
478
|
+
if (subjectKey !== undefined) {
|
|
479
|
+
next.push({
|
|
480
|
+
call: callText({ subjectKey }),
|
|
481
|
+
why: "the properties of the first listed subject",
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
} else if (firstId !== undefined) {
|
|
485
|
+
next.push({
|
|
486
|
+
call: callText({ id: firstId }),
|
|
487
|
+
why: "full fact text of one row",
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return next;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function factRowSummary(row: Record<string, unknown>): string {
|
|
497
|
+
const id = String(row.id ?? "");
|
|
498
|
+
if (typeof row.name === "string" && Array.isArray(row.args)) {
|
|
499
|
+
const effect = typeof row.effect === "string" ? ` -> ${row.effect}` : "";
|
|
500
|
+
const polarity = row.polarity === "deny" ? " [deny]" : "";
|
|
501
|
+
return `${id}: ${row.name}(${row.args.map(String).join(", ")})${effect}${polarity}`;
|
|
502
|
+
}
|
|
503
|
+
if (
|
|
504
|
+
typeof row.subjectKey === "string" &&
|
|
505
|
+
typeof row.propertyKey === "string"
|
|
506
|
+
) {
|
|
507
|
+
const unit = typeof row.unit === "string" && row.unit ? ` ${row.unit}` : "";
|
|
508
|
+
return `${id}: ${row.subjectKey} ${row.propertyKey} ${String(row.operator ?? "")} ${String(row.value ?? "")}${unit}`;
|
|
509
|
+
}
|
|
510
|
+
if (typeof row.name === "string" && typeof row.arity === "number") {
|
|
511
|
+
const names = Array.isArray(row.argumentNames)
|
|
512
|
+
? row.argumentNames.map(String).join(", ")
|
|
513
|
+
: "";
|
|
514
|
+
return `${row.name}(${names})`;
|
|
515
|
+
}
|
|
516
|
+
return `${id} (${String(row.title ?? "")})`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Typed fact questions run as validated Prolog goals in the discovery module;
|
|
521
|
+
* the executor never forwards caller-written Prolog.
|
|
522
|
+
*/
|
|
523
|
+
// implements REQ-kibi-query-fact-filters
|
|
524
|
+
async function executeFactQuery(
|
|
525
|
+
input: QueryInput,
|
|
526
|
+
context: OperationContext,
|
|
527
|
+
limit: number,
|
|
528
|
+
offset: number,
|
|
529
|
+
): Promise<OperationResult<QueryPayload>> {
|
|
530
|
+
if (input.type !== undefined && input.type !== "fact") {
|
|
531
|
+
throw new Error(
|
|
532
|
+
"Typed fact filters (factKind, predicate, subjectKey, ...) apply to type 'fact' only.",
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
for (const key of ["id", "tags", "sourceFile"] as const) {
|
|
536
|
+
if (input[key] !== undefined) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
`${key} cannot be combined with typed fact filters; run a separate kb_query.`,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const prolog = requireProlog(context);
|
|
543
|
+
if (
|
|
544
|
+
!Number.isFinite(limit) ||
|
|
545
|
+
!Number.isFinite(offset) ||
|
|
546
|
+
limit < 0 ||
|
|
547
|
+
offset < 0
|
|
548
|
+
) {
|
|
549
|
+
throw new Error("limit and offset must be non-negative numbers.");
|
|
550
|
+
}
|
|
551
|
+
const page = `${Math.trunc(limit)}, ${Math.trunc(offset)}`;
|
|
552
|
+
const namespace =
|
|
553
|
+
input.namespace === undefined
|
|
554
|
+
? "none"
|
|
555
|
+
: toPrologAtom(requireAtomString(input.namespace, "namespace"));
|
|
556
|
+
const mode =
|
|
557
|
+
input.factKind === "predicate_schema"
|
|
558
|
+
? "schemas"
|
|
559
|
+
: input.predicate !== undefined
|
|
560
|
+
? "predicate"
|
|
561
|
+
: input.subjectKey !== undefined
|
|
562
|
+
? "property"
|
|
563
|
+
: input.factKind !== undefined
|
|
564
|
+
? "fact_kind"
|
|
565
|
+
: undefined;
|
|
566
|
+
if (mode === undefined) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
"Typed fact filters need a predicate, a subjectKey or a factKind; where/args/polarity/distinct/propertyKey refine one of those.",
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
const unused = TYPED_FACT_FILTERS.filter(
|
|
572
|
+
(key) => input[key] !== undefined && !MODE_FILTERS[mode].has(key),
|
|
573
|
+
);
|
|
574
|
+
if (unused.length > 0) {
|
|
575
|
+
throw new Error(
|
|
576
|
+
`${unused.join(", ")} do${unused.length === 1 ? "es" : ""} not apply to a ${mode} question; allowed with it: ${[...MODE_FILTERS[mode]].join(", ")}.`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const scope =
|
|
580
|
+
input.scope === undefined
|
|
581
|
+
? "all"
|
|
582
|
+
: requireEnum(input.scope, "scope", ["all", "current_requirements"]);
|
|
583
|
+
const factKind =
|
|
584
|
+
input.factKind === undefined
|
|
585
|
+
? undefined
|
|
586
|
+
: requireEnum(input.factKind, "factKind", FACT_KINDS);
|
|
587
|
+
let goal: string;
|
|
588
|
+
if (mode === "schemas") {
|
|
589
|
+
goal = `discovery:ask_schemas_json(${namespace}, ${input.predicate === undefined ? "none" : toPrologAtom(requireAtomString(input.predicate, "predicate"))}, ${page}, JsonString)`;
|
|
590
|
+
} else if (mode === "predicate") {
|
|
591
|
+
const name = requireAtomString(input.predicate, "predicate");
|
|
592
|
+
const polarity =
|
|
593
|
+
input.polarity === undefined
|
|
594
|
+
? "none"
|
|
595
|
+
: toPrologAtom(
|
|
596
|
+
requireEnum(input.polarity, "polarity", ["assert", "deny"]),
|
|
597
|
+
);
|
|
598
|
+
const distinct =
|
|
599
|
+
input.distinct === undefined
|
|
600
|
+
? "none"
|
|
601
|
+
: toPrologAtom(requireAtomString(input.distinct, "distinct"));
|
|
602
|
+
goal = `discovery:ask_predicate_json(${namespace}, ${toPrologAtom(name)}, ${predicateArgSpec(input)}, ${polarity}, ${scope}, ${distinct}, ${page}, JsonString)`;
|
|
603
|
+
} else if (mode === "property") {
|
|
604
|
+
const subject = requireAtomString(input.subjectKey, "subjectKey");
|
|
605
|
+
const property =
|
|
606
|
+
input.propertyKey === undefined
|
|
607
|
+
? "none"
|
|
608
|
+
: toPrologAtom(requireAtomString(input.propertyKey, "propertyKey"));
|
|
609
|
+
goal = `discovery:ask_property_json(${toPrologAtom(subject)}, ${property}, ${scope}, ${page}, JsonString)`;
|
|
610
|
+
} else {
|
|
611
|
+
goal = `discovery:ask_fact_kind_json(${toPrologAtom(factKind)}, ${page}, JsonString)`;
|
|
612
|
+
}
|
|
613
|
+
const response = await runOperationJsonQuery<FactQueryResponse>(
|
|
614
|
+
prolog,
|
|
615
|
+
"discovery.pl",
|
|
616
|
+
goal,
|
|
617
|
+
"Query execution",
|
|
618
|
+
context.signal,
|
|
619
|
+
);
|
|
620
|
+
const rows = Array.isArray(response.rows) ? response.rows : [];
|
|
621
|
+
const count =
|
|
622
|
+
typeof response.count === "number" ? response.count : rows.length;
|
|
623
|
+
const warnings = Array.isArray(response.warnings) ? response.warnings : [];
|
|
624
|
+
const factQuery: FactQueryPayload = {
|
|
625
|
+
mode: response.mode,
|
|
626
|
+
...(response.predicate ? { predicate: response.predicate } : {}),
|
|
627
|
+
...(response.property ? { property: response.property } : {}),
|
|
628
|
+
...(response.factKind ? { factKind: response.factKind } : {}),
|
|
629
|
+
...(response.schema !== undefined ? { schema: response.schema } : {}),
|
|
630
|
+
...(response.subjectFacts ? { subjectFacts: response.subjectFacts } : {}),
|
|
631
|
+
...(response.distinct !== undefined ? { distinct: response.distinct } : {}),
|
|
632
|
+
...(response.suggestions !== undefined
|
|
633
|
+
? { suggestions: response.suggestions }
|
|
634
|
+
: {}),
|
|
635
|
+
warnings,
|
|
636
|
+
next: factQueryNext(input, { ...response, rows, count }),
|
|
637
|
+
};
|
|
638
|
+
const subject =
|
|
639
|
+
response.mode === "predicate"
|
|
640
|
+
? `${String(input.predicate)} facts`
|
|
641
|
+
: response.mode === "property"
|
|
642
|
+
? `properties of ${String(input.subjectKey)}`
|
|
643
|
+
: response.mode === "schemas"
|
|
644
|
+
? "predicate schemas"
|
|
645
|
+
: `${String(factKind)} facts`;
|
|
646
|
+
const lines = [
|
|
647
|
+
count === 0
|
|
648
|
+
? `No ${subject} matched.`
|
|
649
|
+
: `${count} ${subject}. Showing ${rows.length} (offset ${offset}, limit ${limit}): ${rows.map(factRowSummary).join("; ")}`,
|
|
650
|
+
...(factQuery.distinct && typeof factQuery.distinct === "object"
|
|
651
|
+
? [
|
|
652
|
+
`Distinct ${String((factQuery.distinct as { argument?: unknown }).argument)}: ${JSON.stringify((factQuery.distinct as { values?: unknown }).values ?? [])}`,
|
|
653
|
+
]
|
|
654
|
+
: []),
|
|
655
|
+
...(factQuery.suggestions && typeof factQuery.suggestions === "object"
|
|
656
|
+
? [`Values that exist: ${JSON.stringify(factQuery.suggestions)}`]
|
|
657
|
+
: []),
|
|
658
|
+
...warnings.map((warning) => `Warning: ${warning}`),
|
|
659
|
+
...factQuery.next.map((entry) => `Next: ${entry.call} (${entry.why})`),
|
|
660
|
+
];
|
|
661
|
+
return {
|
|
662
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
663
|
+
structuredContent: { entities: rows, count, factQuery },
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
155
667
|
export async function executeQuery(
|
|
156
668
|
input: QueryInput,
|
|
157
669
|
context: OperationContext,
|
|
@@ -160,6 +672,9 @@ export async function executeQuery(
|
|
|
160
672
|
const { type, id, tags, sourceFile, limit = 100, offset = 0 } = input;
|
|
161
673
|
try {
|
|
162
674
|
validateEntityType(type);
|
|
675
|
+
if (hasTypedFactFilters(input)) {
|
|
676
|
+
return await executeFactQuery(input, context, limit, offset);
|
|
677
|
+
}
|
|
163
678
|
const prolog = requireProlog(context);
|
|
164
679
|
const signal = context.signal;
|
|
165
680
|
const pageInput = {
|
|
@@ -275,6 +790,15 @@ export async function executeSearch(
|
|
|
275
790
|
validateEntityType(type);
|
|
276
791
|
try {
|
|
277
792
|
const prolog = requireProlog(context);
|
|
793
|
+
const branch = context.branchAttachment?.kbBranch ?? null;
|
|
794
|
+
const storeNotice = uncompiledSearchNotice(context);
|
|
795
|
+
const branchFields = {
|
|
796
|
+
branch,
|
|
797
|
+
...(storeNotice ? { branchStore: storeNotice } : {}),
|
|
798
|
+
};
|
|
799
|
+
const storeNote = storeNotice
|
|
800
|
+
? ` Note: ${storeNotice.detail} Run kibi sync before relying on search results.`
|
|
801
|
+
: "";
|
|
278
802
|
const intentMode =
|
|
279
803
|
rankingMode === "intent-v1" ||
|
|
280
804
|
semanticFacets !== undefined ||
|
|
@@ -305,7 +829,7 @@ export async function executeSearch(
|
|
|
305
829
|
: undefined;
|
|
306
830
|
// The structured payload carries every row; the text is a short
|
|
307
831
|
// human-readable digest instead of a second copy of the results.
|
|
308
|
-
const text =
|
|
832
|
+
const text = `${
|
|
309
833
|
intentResult.matches.length === 0
|
|
310
834
|
? `No intent search results for '${trimmedQuery}' (abstained).`
|
|
311
835
|
: `Found ${intentResult.matches.length} intent search results for '${trimmedQuery}'${intentResult.analysis.ambiguous ? " (top matches are close; confirm before relying on one)" : ""}. Top: ${paginated
|
|
@@ -313,7 +837,8 @@ export async function executeSearch(
|
|
|
313
837
|
.map((match) => String(match.entity.id ?? ""))
|
|
314
838
|
.join(
|
|
315
839
|
", ",
|
|
316
|
-
)}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}
|
|
840
|
+
)}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}.`
|
|
841
|
+
}${storeNote}`;
|
|
317
842
|
return {
|
|
318
843
|
content: [{ type: "text", text }],
|
|
319
844
|
structuredContent: {
|
|
@@ -322,6 +847,7 @@ export async function executeSearch(
|
|
|
322
847
|
truncated: offset + paginated.length < intentResult.matches.length,
|
|
323
848
|
queryAnalysis: intentResult.analysis,
|
|
324
849
|
...(answerLayer ? { answer: answerLayer } : {}),
|
|
850
|
+
...branchFields,
|
|
325
851
|
},
|
|
326
852
|
};
|
|
327
853
|
}
|
|
@@ -342,7 +868,7 @@ export async function executeSearch(
|
|
|
342
868
|
context.workspaceRoot,
|
|
343
869
|
);
|
|
344
870
|
const paginated = paginateResults(matches, limit, offset);
|
|
345
|
-
const text =
|
|
871
|
+
const text = `${
|
|
346
872
|
matches.length === 0
|
|
347
873
|
? `No search results for '${trimmedQuery}'.`
|
|
348
874
|
: `Found ${matches.length} search results for '${trimmedQuery}'. Showing ${paginated.length} (offset ${offset}, limit ${limit}): ${paginated
|
|
@@ -350,13 +876,15 @@ export async function executeSearch(
|
|
|
350
876
|
(match) =>
|
|
351
877
|
`${String(match.entity.id ?? "")} [${match.reasons.join(", ")}]`,
|
|
352
878
|
)
|
|
353
|
-
.join(", ")}
|
|
879
|
+
.join(", ")}`
|
|
880
|
+
}${storeNote}`;
|
|
354
881
|
return {
|
|
355
882
|
content: [{ type: "text", text }],
|
|
356
883
|
structuredContent: {
|
|
357
884
|
results: await projectMatches(prolog, paginated, fields),
|
|
358
885
|
count: matches.length,
|
|
359
886
|
truncated: offset + paginated.length < matches.length,
|
|
887
|
+
...branchFields,
|
|
360
888
|
},
|
|
361
889
|
};
|
|
362
890
|
} catch (error) {
|
|
@@ -460,7 +988,25 @@ export async function executeStatus(
|
|
|
460
988
|
await ownedEngine?.terminate();
|
|
461
989
|
}
|
|
462
990
|
const snapshotEvidence = await readWorkspaceSnapshot(context);
|
|
463
|
-
|
|
991
|
+
// implements REQ-cli-status-pre-first-sync
|
|
992
|
+
// Prolog compares file times only. A fresh clone or a checkout rewrites
|
|
993
|
+
// every mtime without changing a byte, so confirm each "newer" source
|
|
994
|
+
// against the content hashes the last compilation recorded.
|
|
995
|
+
const prologReasons = payload.staleReasons ?? [];
|
|
996
|
+
const confirmed = confirmSourceNewerReasons(
|
|
997
|
+
context.workspaceRoot,
|
|
998
|
+
store.path,
|
|
999
|
+
prologReasons,
|
|
1000
|
+
);
|
|
1001
|
+
const existingReasons = confirmed.kept;
|
|
1002
|
+
if (
|
|
1003
|
+
confirmed.dropped.length > 0 &&
|
|
1004
|
+
confirmed.kept.length === 0 &&
|
|
1005
|
+
payload.staleReasonsTruncated !== true &&
|
|
1006
|
+
payload.syncState === "stale"
|
|
1007
|
+
) {
|
|
1008
|
+
payload = { ...payload, syncState: "fresh", dirty: false };
|
|
1009
|
+
}
|
|
464
1010
|
// implements REQ-cli-status-pre-first-sync
|
|
465
1011
|
// A missing or empty store beside authored sources gets the one reason
|
|
466
1012
|
// whose remediation works (kibi sync); `kibi branch ensure` alone would
|
|
@@ -473,6 +1019,9 @@ export async function executeStatus(
|
|
|
473
1019
|
) ?? branchStoreReason(store);
|
|
474
1020
|
const storeUncompiled = storeReason?.code === BRANCH_STORE_NOT_COMPILED;
|
|
475
1021
|
const lockReason = storeLockJournalReason(store.path);
|
|
1022
|
+
// implements REQ-core-journaled-engine-persistence
|
|
1023
|
+
// A stale-lock takeover heals itself; report it without making the KB stale.
|
|
1024
|
+
const takeoverNotice = storeLockTakeoverReason(store.path);
|
|
476
1025
|
const compilerReason = compilerFingerprintReason(store.path);
|
|
477
1026
|
const engineReason = engineStatus
|
|
478
1027
|
? {
|
|
@@ -514,6 +1063,7 @@ export async function executeStatus(
|
|
|
514
1063
|
...(storeUncompiled ? { syncState: "stale", dirty: true } : {}),
|
|
515
1064
|
branchAttachment: attachment,
|
|
516
1065
|
branchStore: store,
|
|
1066
|
+
...(takeoverNotice ? { notices: [takeoverNotice] } : {}),
|
|
517
1067
|
...(engineStatus ? { engineStatus } : {}),
|
|
518
1068
|
staleReasons,
|
|
519
1069
|
staleReasonCount: staleReasons.length,
|
|
@@ -661,7 +1211,7 @@ export async function executeStatus(
|
|
|
661
1211
|
content: [
|
|
662
1212
|
{
|
|
663
1213
|
type: "text",
|
|
664
|
-
text: `Branch ${payload.branch} is ${enrichedPayload.syncState} (snapshot ${payload.snapshotId}, dirty=${enrichedPayload.dirty}, proofSnapshot=${enrichedPayload.proofSnapshot})${storeUncompiled ? `. Blocking: ${String(storeReason?.detail)} Run kibi sync.` : ""}`,
|
|
1214
|
+
text: `Branch ${payload.branch} is ${enrichedPayload.syncState} (snapshot ${payload.snapshotId}, dirty=${enrichedPayload.dirty}, proofSnapshot=${enrichedPayload.proofSnapshot})${storeUncompiled ? `. Blocking: ${String(storeReason?.detail)} Run kibi sync.` : ""}${takeoverNotice ? `. Note: ${String(takeoverNotice.detail)}` : ""}`,
|
|
665
1215
|
},
|
|
666
1216
|
],
|
|
667
1217
|
structuredContent: statusWithPlan,
|