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.
Files changed (80) hide show
  1. package/dist/cli-operation-metadata.d.ts +1 -1
  2. package/dist/cli-operation-metadata.js +1 -1
  3. package/dist/cli-register-proof.js +9 -1
  4. package/dist/commands/doctor.d.ts.map +1 -1
  5. package/dist/commands/doctor.js +18 -6
  6. package/dist/commands/sync/manifest.d.ts +10 -0
  7. package/dist/commands/sync/manifest.d.ts.map +1 -1
  8. package/dist/commands/sync/manifest.js +27 -0
  9. package/dist/commands/sync/untracked-targets.d.ts +17 -0
  10. package/dist/commands/sync/untracked-targets.d.ts.map +1 -1
  11. package/dist/commands/sync/untracked-targets.js +81 -9
  12. package/dist/commands/sync.d.ts.map +1 -1
  13. package/dist/commands/sync.js +76 -6
  14. package/dist/engine.d.ts +4 -0
  15. package/dist/engine.d.ts.map +1 -1
  16. package/dist/engine.js +56 -33
  17. package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
  18. package/dist/operations/modeling/predicate-applicability.js +5 -2
  19. package/dist/operations/modeling/predicate-catalog-4.d.ts.map +1 -1
  20. package/dist/operations/modeling/predicate-catalog-4.js +4 -0
  21. package/dist/operations/modeling/predicate-loader.d.ts.map +1 -1
  22. package/dist/operations/modeling/predicate-loader.js +3 -0
  23. package/dist/operations/modeling/predicate-score-3.js +1 -1
  24. package/dist/operations/modeling/predicate-types.d.ts +5 -0
  25. package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
  26. package/dist/operations/mutation/source-authoring.d.ts +9 -0
  27. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  28. package/dist/operations/mutation/source-authoring.js +30 -1
  29. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  30. package/dist/operations/planning/apply-plan.js +10 -1
  31. package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
  32. package/dist/operations/semantic-advisor/clauses.js +7 -1
  33. package/dist/prolog/store-lock.d.ts +41 -0
  34. package/dist/prolog/store-lock.d.ts.map +1 -1
  35. package/dist/prolog/store-lock.js +91 -1
  36. package/dist/prolog.d.ts +7 -1
  37. package/dist/prolog.d.ts.map +1 -1
  38. package/dist/prolog.js +21 -3
  39. package/dist/proof/inspect.d.ts +8 -1
  40. package/dist/proof/inspect.d.ts.map +1 -1
  41. package/dist/proof/inspect.js +19 -2
  42. package/dist/public/operations/catalog.d.ts +50 -1
  43. package/dist/public/operations/catalog.d.ts.map +1 -1
  44. package/dist/public/operations/contracts.d.ts.map +1 -1
  45. package/dist/public/operations/contracts.js +4 -4
  46. package/dist/public/operations/discovery-executors.d.ts +50 -1
  47. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  48. package/dist/public/operations/discovery-executors.js +418 -8
  49. package/dist/public/operations/migration-plan.d.ts +2 -0
  50. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  51. package/dist/public/operations/migration-plan.js +31 -0
  52. package/dist/public/operations/source-relationship-parity.d.ts.map +1 -1
  53. package/dist/public/operations/source-relationship-parity.js +55 -1
  54. package/dist/public/operations/specs/discovery.d.ts +50 -1
  55. package/dist/public/operations/specs/discovery.d.ts.map +1 -1
  56. package/dist/public/operations/specs/discovery.js +56 -1
  57. package/dist/public/skills/kibi-usage/SKILL.md +20 -3
  58. package/dist/public/skills/kibi-usage/resources/proof.md +23 -0
  59. package/dist/runtime/cli-runtime.d.ts.map +1 -1
  60. package/dist/runtime/cli-runtime.js +7 -2
  61. package/dist/search-answer.d.ts +12 -3
  62. package/dist/search-answer.d.ts.map +1 -1
  63. package/dist/search-answer.js +51 -4
  64. package/dist/utils/branch-store.d.ts +8 -0
  65. package/dist/utils/branch-store.d.ts.map +1 -1
  66. package/dist/utils/branch-store.js +27 -1
  67. package/dist/utils/content-freshness.d.ts +13 -0
  68. package/dist/utils/content-freshness.d.ts.map +1 -0
  69. package/dist/utils/content-freshness.js +171 -0
  70. package/dist/utils/symbol-manifest-layout.d.ts +9 -0
  71. package/dist/utils/symbol-manifest-layout.d.ts.map +1 -0
  72. package/dist/utils/symbol-manifest-layout.js +18 -0
  73. package/package.json +2 -2
  74. package/src/public/operations/contracts.ts +7 -4
  75. package/src/public/operations/discovery-executors.ts +556 -6
  76. package/src/public/operations/migration-plan.ts +44 -0
  77. package/src/public/operations/source-relationship-parity.ts +60 -3
  78. package/src/public/operations/specs/discovery.ts +67 -1
  79. package/src/public/skills/kibi-usage/SKILL.md +20 -3
  80. 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
- const existingReasons = payload.staleReasons ?? [];
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,