kibi-cli 2.16.0 → 2.18.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 (83) hide show
  1. package/dist/cli-protocol.d.ts.map +1 -1
  2. package/dist/cli-protocol.js +13 -0
  3. package/dist/cli-register-proof.d.ts +6 -0
  4. package/dist/cli-register-proof.d.ts.map +1 -1
  5. package/dist/cli-register-proof.js +46 -2
  6. package/dist/commands/doctor.d.ts.map +1 -1
  7. package/dist/commands/doctor.js +9 -3
  8. package/dist/commands/sync/untracked-targets.d.ts.map +1 -1
  9. package/dist/commands/sync/untracked-targets.js +39 -9
  10. package/dist/commands/sync.d.ts.map +1 -1
  11. package/dist/commands/sync.js +26 -5
  12. package/dist/engine.d.ts +4 -0
  13. package/dist/engine.d.ts.map +1 -1
  14. package/dist/engine.js +40 -10
  15. package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
  16. package/dist/operations/modeling/predicate-applicability.js +12 -3
  17. package/dist/operations/modeling/predicate-catalog-4.d.ts.map +1 -1
  18. package/dist/operations/modeling/predicate-catalog-4.js +4 -0
  19. package/dist/operations/modeling/predicate-loader.d.ts.map +1 -1
  20. package/dist/operations/modeling/predicate-loader.js +3 -0
  21. package/dist/operations/modeling/predicate-score-3.js +1 -1
  22. package/dist/operations/modeling/predicate-types.d.ts +5 -0
  23. package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
  24. package/dist/operations/modeling/predicate-utils.d.ts.map +1 -1
  25. package/dist/operations/modeling/predicate-utils.js +2 -3
  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 +38 -2
  29. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  30. package/dist/operations/planning/apply-plan.js +33 -8
  31. package/dist/operations/planning/migration-refusal.d.ts +14 -0
  32. package/dist/operations/planning/migration-refusal.d.ts.map +1 -0
  33. package/dist/operations/planning/migration-refusal.js +17 -0
  34. package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
  35. package/dist/operations/semantic-advisor/clauses.js +7 -1
  36. package/dist/operations/semantic-advisor/shared.d.ts +7 -0
  37. package/dist/operations/semantic-advisor/shared.d.ts.map +1 -1
  38. package/dist/operations/semantic-advisor/shared.js +22 -3
  39. package/dist/prolog/store-lock.d.ts +22 -0
  40. package/dist/prolog/store-lock.d.ts.map +1 -1
  41. package/dist/prolog/store-lock.js +45 -1
  42. package/dist/prolog.d.ts +7 -1
  43. package/dist/prolog.d.ts.map +1 -1
  44. package/dist/prolog.js +21 -3
  45. package/dist/proof/inspect.d.ts +9 -0
  46. package/dist/proof/inspect.d.ts.map +1 -1
  47. package/dist/proof/inspect.js +10 -0
  48. package/dist/proof/integration-plan.d.ts +2 -1
  49. package/dist/proof/integration-plan.d.ts.map +1 -1
  50. package/dist/proof/integration-plan.js +30 -13
  51. package/dist/public/impact/coverage-depth-quality.d.ts.map +1 -1
  52. package/dist/public/impact/coverage-depth-quality.js +29 -1
  53. package/dist/public/operations/contracts.d.ts.map +1 -1
  54. package/dist/public/operations/contracts.js +3 -0
  55. package/dist/public/operations/discovery-executors.d.ts +14 -1
  56. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  57. package/dist/public/operations/discovery-executors.js +62 -12
  58. package/dist/public/operations/migration-plan.d.ts +2 -0
  59. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  60. package/dist/public/operations/migration-plan.js +31 -0
  61. package/dist/public/operations/result-envelope.d.ts.map +1 -1
  62. package/dist/public/operations/result-envelope.js +23 -3
  63. package/dist/public/operations/source-relationship-parity.d.ts.map +1 -1
  64. package/dist/public/operations/source-relationship-parity.js +55 -1
  65. package/dist/public/skills/kibi-usage/SKILL.md +19 -4
  66. package/dist/public/skills/kibi-usage/resources/fact-lanes.md +3 -3
  67. package/dist/public/skills/kibi-usage/resources/proof.md +44 -3
  68. package/dist/utils/branch-store.d.ts +8 -0
  69. package/dist/utils/branch-store.d.ts.map +1 -1
  70. package/dist/utils/branch-store.js +27 -1
  71. package/dist/utils/content-freshness.d.ts +13 -0
  72. package/dist/utils/content-freshness.d.ts.map +1 -0
  73. package/dist/utils/content-freshness.js +161 -0
  74. package/package.json +2 -2
  75. package/src/public/impact/coverage-depth-quality.ts +46 -1
  76. package/src/public/operations/contracts.ts +3 -0
  77. package/src/public/operations/discovery-executors.ts +89 -11
  78. package/src/public/operations/migration-plan.ts +44 -0
  79. package/src/public/operations/result-envelope.ts +31 -3
  80. package/src/public/operations/source-relationship-parity.ts +60 -3
  81. package/src/public/skills/kibi-usage/SKILL.md +19 -4
  82. package/src/public/skills/kibi-usage/resources/fact-lanes.md +3 -3
  83. package/src/public/skills/kibi-usage/resources/proof.md +44 -3
@@ -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,
@@ -78,8 +80,47 @@ export type SearchPayload = {
78
80
  readonly truncated?: boolean;
79
81
  readonly queryAnalysis?: IntentSearchAnalysis;
80
82
  readonly answer?: SearchAnswer;
83
+ /** The Kibi branch whose store was searched, when the runtime attached one. */
84
+ readonly branch?: string | null;
85
+ /** Present when the searched store was never compiled for this branch. */
86
+ readonly branchStore?: SearchBranchStoreNotice;
81
87
  };
82
88
 
89
+ export type SearchBranchStoreNotice = {
90
+ readonly state: "not_compiled";
91
+ readonly code: typeof BRANCH_STORE_NOT_COMPILED;
92
+ readonly path: string;
93
+ readonly detail: string;
94
+ readonly remediation: Readonly<Record<string, unknown>>;
95
+ };
96
+
97
+ /**
98
+ * An empty result from a store that was never compiled says nothing about
99
+ * the authored requirements, so search reports the store state itself (the
100
+ * same reason kb_status and kb_check use) instead of a bare "no results".
101
+ */
102
+ // implements REQ-cli-status-pre-first-sync, REQ-mcp-search-discovery
103
+ function uncompiledSearchNotice(
104
+ context: OperationContext,
105
+ ): SearchBranchStoreNotice | null {
106
+ const attachment = context.branchAttachment;
107
+ if (attachment === undefined) return null;
108
+ const store = inspectBranchStore(context.workspaceRoot, attachment.kbBranch);
109
+ const reason = uncompiledBranchStoreReason(
110
+ context.workspaceRoot,
111
+ store,
112
+ attachment.kbBranch,
113
+ );
114
+ if (reason === null) return null;
115
+ return {
116
+ state: "not_compiled",
117
+ code: BRANCH_STORE_NOT_COMPILED,
118
+ path: store.path,
119
+ detail: String(reason.detail),
120
+ remediation: reason.remediation as Readonly<Record<string, unknown>>,
121
+ };
122
+ }
123
+
83
124
  export type StatusInput = Readonly<Record<string, never>>;
84
125
 
85
126
  export type StatusPayload = {
@@ -99,6 +140,8 @@ export type StatusPayload = {
99
140
  readonly staleReasons?: readonly Record<string, unknown>[];
100
141
  readonly staleReasonCount?: number;
101
142
  readonly staleReasonsTruncated?: boolean;
143
+ /** Informational, non-blocking observations (never make the KB stale). */
144
+ readonly notices?: readonly Record<string, unknown>[];
102
145
  readonly branchAttachment?: {
103
146
  readonly gitBranch: string;
104
147
  readonly kbBranch: string;
@@ -275,6 +318,15 @@ export async function executeSearch(
275
318
  validateEntityType(type);
276
319
  try {
277
320
  const prolog = requireProlog(context);
321
+ const branch = context.branchAttachment?.kbBranch ?? null;
322
+ const storeNotice = uncompiledSearchNotice(context);
323
+ const branchFields = {
324
+ branch,
325
+ ...(storeNotice ? { branchStore: storeNotice } : {}),
326
+ };
327
+ const storeNote = storeNotice
328
+ ? ` Note: ${storeNotice.detail} Run kibi sync before relying on search results.`
329
+ : "";
278
330
  const intentMode =
279
331
  rankingMode === "intent-v1" ||
280
332
  semanticFacets !== undefined ||
@@ -305,7 +357,7 @@ export async function executeSearch(
305
357
  : undefined;
306
358
  // The structured payload carries every row; the text is a short
307
359
  // human-readable digest instead of a second copy of the results.
308
- const text =
360
+ const text = `${
309
361
  intentResult.matches.length === 0
310
362
  ? `No intent search results for '${trimmedQuery}' (abstained).`
311
363
  : `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 +365,8 @@ export async function executeSearch(
313
365
  .map((match) => String(match.entity.id ?? ""))
314
366
  .join(
315
367
  ", ",
316
- )}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}.`;
368
+ )}${answerLayer ? `. Governing: ${answerLayer.governing.map((req) => req.id).join(", ") || "none found"}` : ""}.`
369
+ }${storeNote}`;
317
370
  return {
318
371
  content: [{ type: "text", text }],
319
372
  structuredContent: {
@@ -322,6 +375,7 @@ export async function executeSearch(
322
375
  truncated: offset + paginated.length < intentResult.matches.length,
323
376
  queryAnalysis: intentResult.analysis,
324
377
  ...(answerLayer ? { answer: answerLayer } : {}),
378
+ ...branchFields,
325
379
  },
326
380
  };
327
381
  }
@@ -342,7 +396,7 @@ export async function executeSearch(
342
396
  context.workspaceRoot,
343
397
  );
344
398
  const paginated = paginateResults(matches, limit, offset);
345
- const text =
399
+ const text = `${
346
400
  matches.length === 0
347
401
  ? `No search results for '${trimmedQuery}'.`
348
402
  : `Found ${matches.length} search results for '${trimmedQuery}'. Showing ${paginated.length} (offset ${offset}, limit ${limit}): ${paginated
@@ -350,13 +404,15 @@ export async function executeSearch(
350
404
  (match) =>
351
405
  `${String(match.entity.id ?? "")} [${match.reasons.join(", ")}]`,
352
406
  )
353
- .join(", ")}`;
407
+ .join(", ")}`
408
+ }${storeNote}`;
354
409
  return {
355
410
  content: [{ type: "text", text }],
356
411
  structuredContent: {
357
412
  results: await projectMatches(prolog, paginated, fields),
358
413
  count: matches.length,
359
414
  truncated: offset + paginated.length < matches.length,
415
+ ...branchFields,
360
416
  },
361
417
  };
362
418
  } catch (error) {
@@ -460,7 +516,25 @@ export async function executeStatus(
460
516
  await ownedEngine?.terminate();
461
517
  }
462
518
  const snapshotEvidence = await readWorkspaceSnapshot(context);
463
- const existingReasons = payload.staleReasons ?? [];
519
+ // implements REQ-cli-status-pre-first-sync
520
+ // Prolog compares file times only. A fresh clone or a checkout rewrites
521
+ // every mtime without changing a byte, so confirm each "newer" source
522
+ // against the content hashes the last compilation recorded.
523
+ const prologReasons = payload.staleReasons ?? [];
524
+ const confirmed = confirmSourceNewerReasons(
525
+ context.workspaceRoot,
526
+ store.path,
527
+ prologReasons,
528
+ );
529
+ const existingReasons = confirmed.kept;
530
+ if (
531
+ confirmed.dropped.length > 0 &&
532
+ confirmed.kept.length === 0 &&
533
+ payload.staleReasonsTruncated !== true &&
534
+ payload.syncState === "stale"
535
+ ) {
536
+ payload = { ...payload, syncState: "fresh", dirty: false };
537
+ }
464
538
  // implements REQ-cli-status-pre-first-sync
465
539
  // A missing or empty store beside authored sources gets the one reason
466
540
  // whose remediation works (kibi sync); `kibi branch ensure` alone would
@@ -473,6 +547,9 @@ export async function executeStatus(
473
547
  ) ?? branchStoreReason(store);
474
548
  const storeUncompiled = storeReason?.code === BRANCH_STORE_NOT_COMPILED;
475
549
  const lockReason = storeLockJournalReason(store.path);
550
+ // implements REQ-core-journaled-engine-persistence
551
+ // A stale-lock takeover heals itself; report it without making the KB stale.
552
+ const takeoverNotice = storeLockTakeoverReason(store.path);
476
553
  const compilerReason = compilerFingerprintReason(store.path);
477
554
  const engineReason = engineStatus
478
555
  ? {
@@ -507,13 +584,14 @@ export async function executeStatus(
507
584
  // Source hashes can all match while the compilation itself is lossy, so
508
585
  // a compiler contract change alone makes the store stale.
509
586
  ...(compilerReason ? { syncState: "stale", dirty: true } : {}),
510
- // An empty store has no stale file to report, yet nothing in it is
511
- // current: the authored sources were never compiled.
512
- ...(storeUncompiled && store.state === "healthy"
513
- ? { syncState: "stale", dirty: true }
514
- : {}),
587
+ // A missing or empty store has no stale file to report, yet nothing in
588
+ // it is current: the authored sources were never compiled. (A missing
589
+ // store without authored sources, and an unreadable or incomplete one,
590
+ // stay "unknown".)
591
+ ...(storeUncompiled ? { syncState: "stale", dirty: true } : {}),
515
592
  branchAttachment: attachment,
516
593
  branchStore: store,
594
+ ...(takeoverNotice ? { notices: [takeoverNotice] } : {}),
517
595
  ...(engineStatus ? { engineStatus } : {}),
518
596
  staleReasons,
519
597
  staleReasonCount: staleReasons.length,
@@ -661,7 +739,7 @@ export async function executeStatus(
661
739
  content: [
662
740
  {
663
741
  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.` : ""}`,
742
+ 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
743
  },
666
744
  ],
667
745
  structuredContent: statusWithPlan,
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { BRANCH_STORE_NOT_COMPILED } from "../../utils/branch-store.js";
5
5
  import { legacyConfigExists } from "../../utils/config.js";
6
+ import { MTIME_SOURCE_REASON_CODES } from "../../utils/content-freshness.js";
6
7
  import { readKbManifestStatus } from "../../utils/kb-manifest.js";
7
8
  import {
8
9
  getSchemaVersionStatus,
@@ -616,6 +617,9 @@ function readLegacySchemaVersion(
616
617
  }
617
618
 
618
619
  // implements REQ-agent-guided-migration-orchestration
620
+ /** Migration action code of the automatic recompile of changed sources. */
621
+ export const FRESHNESS_RESYNC = "freshness_resync";
622
+
619
623
  export function buildActionsFromStatus(input: {
620
624
  workspaceRoot: string;
621
625
  branchAttachment?: Readonly<Record<string, unknown>>;
@@ -777,10 +781,50 @@ export function buildActionsFromStatus(input: {
777
781
  );
778
782
  }
779
783
 
784
+ // implements REQ-cli-status-pre-first-sync
785
+ // Sources whose content changed since the last compilation (kb_status has
786
+ // already dropped mtime-only reasons) are fixed by recompiling them: one
787
+ // automatic `kibi sync`, not a review per file. An uncompiled store is
788
+ // compiled by the step above, which also covers these files.
789
+ const resyncReasons = (input.staleReasons ?? []).filter(
790
+ (reason) =>
791
+ typeof reason.code === "string" &&
792
+ MTIME_SOURCE_REASON_CODES.has(reason.code),
793
+ );
794
+ if (resyncReasons.length > 0 && uncompiled === undefined) {
795
+ actions.push(
796
+ migrationAction({
797
+ id: "freshness-resync",
798
+ code: FRESHNESS_RESYNC,
799
+ category: "freshness",
800
+ safety: "automatic",
801
+ invocation: { kind: "cli", command_argv: ["kibi", "sync"] },
802
+ affectedFiles: [
803
+ ...new Set(
804
+ resyncReasons.flatMap((reason) =>
805
+ typeof reason.path === "string" ? [reason.path] : [],
806
+ ),
807
+ ),
808
+ ].sort(),
809
+ evidence: { reasons: resyncReasons },
810
+ dependsOn: actions
811
+ .map((action) => action.id)
812
+ .filter(
813
+ (id) =>
814
+ id === "branch-store-ensure" || id === "schema-config-upgrade",
815
+ ),
816
+ postconditions: [{ syncState: "fresh" }],
817
+ autoApplicable: true,
818
+ }),
819
+ );
820
+ }
821
+
780
822
  for (const reason of input.staleReasons ?? []) {
781
823
  const code = typeof reason.code === "string" ? reason.code : "stale_source";
782
824
  // Planned above as an automatic compile step, not a review.
783
825
  if (code === BRANCH_STORE_NOT_COMPILED) continue;
826
+ // Planned above as one automatic resync (or covered by the compile).
827
+ if (MTIME_SOURCE_REASON_CODES.has(code)) continue;
784
828
  const file = typeof reason.path === "string" ? reason.path : "";
785
829
  actions.push(
786
830
  migrationAction({
@@ -1,3 +1,4 @@
1
+ import { MIGRATION_PLAN_REFUSED } from "../../operations/planning/migration-refusal.js";
1
2
  import type { KibiResult, OperationEffect, OperationName } from "./types.js";
2
3
 
3
4
  export const KIBI_PROTOCOL_VERSION = 1 as const;
@@ -139,6 +140,16 @@ export function resultVersion(spec: {
139
140
  return spec.resultVersion ?? `kibi.${spec.name}.v1`;
140
141
  }
141
142
 
143
+ function refusalDetail(row: Record<string, unknown>): string {
144
+ const results = Array.isArray(row.actionResults) ? row.actionResults : [];
145
+ const failure = results.find(
146
+ (entry) => record(entry) && entry.outcome === "failed",
147
+ );
148
+ return record(failure) && typeof failure.detail === "string"
149
+ ? failure.detail
150
+ : "";
151
+ }
152
+
142
153
  export function toKibiResult<T>(
143
154
  spec: {
144
155
  readonly name: OperationName | string;
@@ -158,12 +169,17 @@ export function toKibiResult<T>(
158
169
  } = {},
159
170
  ): KibiResult<T> {
160
171
  const row = record(data) ? data : undefined;
172
+ // A migration plan whose actions were all refused changed nothing: it is
173
+ // an error, not a success that needs reconciliation.
174
+ const migrationRefused =
175
+ row?.version === "kibi.migration-apply-result.v1" &&
176
+ row.outcome === "refused";
161
177
  let status = options.status;
162
178
  if (!status) {
163
179
  status = "success";
164
180
  if (row?.status === "committed_with_repairs")
165
181
  status = "committed_with_repairs";
166
- if (row?.status === "rejected") status = "error";
182
+ if (row?.status === "rejected" || migrationRefused) status = "error";
167
183
  }
168
184
  const terminalError =
169
185
  row?.status === "rejected"
@@ -173,9 +189,21 @@ export function toKibiResult<T>(
173
189
  "Bootstrap stopped at a deterministic failure. Inspect actionResults for committed actions and re-plan from the current state.",
174
190
  retryable: false,
175
191
  }
176
- : undefined;
192
+ : migrationRefused
193
+ ? {
194
+ code: MIGRATION_PLAN_REFUSED,
195
+ message:
196
+ `Migration plan refused; nothing was changed. ${refusalDetail(row)}`.trim(),
197
+ retryable: false,
198
+ }
199
+ : undefined;
177
200
  const resultError = options.error ?? terminalError;
178
- const outcome = effectOutcome(status, options);
201
+ const outcome = effectOutcome(
202
+ status,
203
+ migrationRefused && options.attempted === undefined
204
+ ? { ...options, attempted: false }
205
+ : options,
206
+ );
179
207
  return {
180
208
  kibiProtocol: KIBI_PROTOCOL_VERSION,
181
209
  operation: spec.name as OperationName,
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { discoverSourceFiles } from "../../commands/sync/discovery.js";
4
4
  import { processExtractions } from "../../commands/sync/extraction.js";
5
+ import { findUntrackedDocumentMatches } from "../../commands/sync/untracked-targets.js";
5
6
  import type { ExtractedRelationship } from "../../extractors/markdown.js";
6
7
  import { RELATIONSHIP_TYPES } from "../../operations/mutation/relationships.js";
7
8
  import { parseListOfLists, parsePrologValue } from "../../prolog/codec.js";
@@ -234,8 +235,64 @@ export async function collectSourceRelationshipParityViolations(
234
235
  });
235
236
  }
236
237
 
237
- return compareRelationshipParity(
238
- authored,
239
- await compiledRelationships(prolog),
238
+ return explainUntrackedEndpoints(
239
+ workspaceRoot,
240
+ compareRelationshipParity(authored, await compiledRelationships(prolog)),
240
241
  );
241
242
  }
243
+
244
+ /**
245
+ * An authored relationship is missing from the compiled KB most often
246
+ * because an endpoint is declared in a file sync cannot see: one Git does not
247
+ * track and no pending-source receipt covers (for example a `.kb/symbols.yaml`
248
+ * that `kibi init` created and kb_upsert appended to before receipts covered
249
+ * existing untracked files). Name that file and the fix.
250
+ */
251
+ // implements REQ-kibi-source-relationship-parity
252
+ function explainUntrackedEndpoints(
253
+ workspaceRoot: string,
254
+ violations: Violation[],
255
+ ): Violation[] {
256
+ const endpoints = new Set<string>();
257
+ for (const violation of violations) {
258
+ const relationship = authoredToCompiledRelationship(violation);
259
+ if (relationship === null) continue;
260
+ endpoints.add(relationship.from);
261
+ endpoints.add(relationship.to);
262
+ }
263
+ if (endpoints.size === 0) return violations;
264
+ const untracked = findUntrackedDocumentMatches(workspaceRoot, [...endpoints]);
265
+ if (untracked.size === 0) return violations;
266
+ return violations.map((violation) => {
267
+ const relationship = authoredToCompiledRelationship(violation);
268
+ if (relationship === null) return violation;
269
+ const sources = [relationship.from, relationship.to].flatMap((id) => {
270
+ const file = untracked.get(id);
271
+ return file === undefined ? [] : [{ entityId: id, file }];
272
+ });
273
+ if (sources.length === 0) return violation;
274
+ const files = [...new Set(sources.map((source) => source.file))];
275
+ return {
276
+ ...violation,
277
+ suggestion: `${sources.map((source) => source.entityId).join(" and ")} ${sources.length === 1 ? "is" : "are"} declared in ${files.join(", ")}, which Git does not track and no pending-source receipt covers, so sync never compiles it. Run 'git add ${files.join(" ")}' (staging is enough) or rewrite the entity with kb_upsert, then run kibi sync and rerun kb_check.`,
278
+ evidence: {
279
+ ...(violation.evidence ?? {}),
280
+ untrackedSources: sources,
281
+ },
282
+ };
283
+ });
284
+ }
285
+
286
+ function authoredToCompiledRelationship(
287
+ violation: Violation,
288
+ ): { from: string; to: string } | null {
289
+ const evidence = violation.evidence as
290
+ | { direction?: unknown; relationship?: { from?: unknown; to?: unknown } }
291
+ | undefined;
292
+ if (evidence?.direction !== "authored_to_compiled") return null;
293
+ const from = evidence.relationship?.from;
294
+ const to = evidence.relationship?.to;
295
+ return typeof from === "string" && typeof to === "string"
296
+ ? { from, to }
297
+ : null;
298
+ }
@@ -2,7 +2,7 @@
2
2
  id: kibi-usage
3
3
  name: Kibi Usage
4
4
  description: Use Kibi's source-first, exact-Git, migration-aware, proof-aware operations safely across MCP or the trusted local CLI, including partial completion repair.
5
- version: 2.12.0
5
+ version: 2.14.0
6
6
  kibiCompatibility: ">=1.0.0"
7
7
  tags:
8
8
  - kibi
@@ -148,7 +148,13 @@ error `BOOTSTRAP_PLAN_REJECTED` can follow earlier committed actions: inspect
148
148
  `data.actionResults` and `data.applied`, then re-plan from the current state.
149
149
  Its journal is terminal; never recover or replay it. `BOOTSTRAP_PLAN_INVALID`
150
150
  fails preflight before bootstrap writes or a new journal; obtain a corrected
151
- plan. If the status is
151
+ plan. A migration plan whose actions were refused before changing anything
152
+ returns `status: error`, `MIGRATION_PLAN_REFUSED` and `data.outcome:
153
+ refused`: nothing changed, so read the action `detail` and get a new plan.
154
+ `data.outcome: reconciliation_required` (status success) means an action
155
+ failed mid-way: inspect its effect before retrying. `kb_delete` of an
156
+ authored entity returns status success with `deleted: 0`; its `errors` say
157
+ why nothing was deleted. If the status is
152
158
  `committed_with_repairs`, the mutation already committed: inspect failed effects,
153
159
  execute each required repair action in order, and never retry the original
154
160
  operation. Record effect failures, followed actions, and unsafe retries in
@@ -231,7 +237,11 @@ authored. Each one is the order the write path accepts.
231
237
  fact, `requires_predicate` to `relationshipTarget` and `specified_by` to
232
238
  its scenario: `specified_by` (req → scenario) is sent in the
233
239
  requirement's write, never in the scenario's, so create the scenario
234
- first. A
240
+ first, with `assumes` links (scenario → `property_value` fact, sent in
241
+ the scenario's own write) to the values its outcome relies on. Set
242
+ `expects` only on a scenario that has `assumes` links; `expects: success`
243
+ without them only yields `scenario-feasibility-unknown` ("assumes
244
+ nothing"). A
235
245
  `modeled` entry and its grounding link must arrive in the same write
236
246
  (`modeled semantic_inventory entries (n) must equal logical grounding
237
247
  relationships (m)`); a clause not grounded yet stays `status: missing`
@@ -243,7 +253,12 @@ authored. Each one is the order the write path accepts.
243
253
  claim_keys must match logical grounding target claim_keys exactly`.
244
254
  Prefer supersession for a change of meaning: analyze the new prose,
245
255
  write its facts, write the new requirement (`-v2`) with `supersedes` to
246
- the old one, then set the old one to `status: closed`. For a wording fix
256
+ the old one, then set the old one to `status: closed`. Its scenario
257
+ follows the same rule as in recipe 1: create the new scenario (with
258
+ `assumes` to the `property_value` facts it relies on, and `expects` only
259
+ when it has them) before the `-v2` write that sends `specified_by`, then
260
+ re-point the old scenario (its `assumes` go in its own write) or close
261
+ it. For a wording fix
247
262
  that keeps the meaning, re-ground in place: analyze the new prose,
248
263
  `kb_upsert` each grounding fact with the new `claim_key`/`claim_text`
249
264
  (same fact id), then `kb_upsert` the requirement with the new ledger,
@@ -61,16 +61,16 @@ A schema can close an argument with `argument_constants` (allowed values keyed b
61
61
 
62
62
  ### Deny predicate
63
63
 
64
- Claim: “Suspended users must not publish articles.” When `permission_rule` fits, preserve its positive schema name and encode prohibition as polarity:
64
+ Claim: “Suspended users must not publish articles.” When `permission_rule` fits, preserve its positive schema name and encode prohibition as polarity. `kb_model` (`mode: "predicates"`) plans this fact for the claim; the built-in schema's fourth argument, `decision`, repeats the polarity:
65
65
 
66
66
  ```yaml
67
67
  id: FACT-SUSPENDED-PUBLISH-DENIED
68
68
  status: active
69
69
  fact_kind: predicate
70
70
  predicate_name: permission_rule
71
- predicate_args: [suspended_user, publish, article]
71
+ predicate_args: [suspended_user, publish, articles, deny]
72
72
  polarity: deny
73
- canonical_key: permission_rule(suspended_user,publish,article)
73
+ canonical_key: permission_rule(suspended_user,publish,articles,deny)
74
74
  claim_key: <advisor-issued key for this exact clause>
75
75
  claim_text: Suspended users must not publish articles.
76
76
  relationship: { type: requires_predicate, from: REQ-SUSPENDED-PUBLISH-DENIED, to: FACT-SUSPENDED-PUBLISH-DENIED }
@@ -51,6 +51,15 @@ proof_bindings: # optional provenance metadata
51
51
 
52
52
  Provenance lives in `proof_bindings`; the contract stays semantic.
53
53
 
54
+ - Every `required_proofs[].symbol_id` must name an existing `symbol` entity,
55
+ or `kb_check` reports `proof-contract-symbols`. Create the symbols first
56
+ (`kb_upsert`, narrow method-level granularity, never a `type-shape`
57
+ symbol): a production symbol `implements` the requirement and is
58
+ `covered_by` the test; an executable test symbol is `executable_for` it.
59
+ - `proof_bindings[].source_file`, when set, must equal the named symbol's
60
+ `sourceFile`: for a production symbol that is the production file, not the
61
+ spec file that exercises it. Omit it when unsure.
62
+
54
63
  ## Integration configuration
55
64
 
56
65
  `.kb/proof/integrations.json` (`kibi.proof-integration.v1`) is tracked,
@@ -62,12 +71,21 @@ returns `integrationPlan`, a hash-bound `kibi.migration-plan.v2`, plus the
62
71
  proposal to the human, then apply the plan with `kb_apply_plan` (or
63
72
  `kibi apply-plan --input -`) passing `plan`, `approvedPlanHash` (the plan's
64
73
  `planHash`) and `approvedActionIds`. Once the file exists, a create plan is
65
- refused; `kibi proof inspect --update <id> --json` plans adding or replacing
66
- one named integration.
74
+ refused (`status: error`, `MIGRATION_PLAN_REFUSED`, `data.outcome: refused`;
75
+ nothing is written); `kibi proof inspect --update <id> --json` plans adding or
76
+ replacing one named integration.
67
77
 
68
78
  - `producer: command` — Kibi synthesizes the envelope from the process
69
79
  outcome; obligations are bound with `aggregate_run` provenance and the
70
- single process invocation counts as the known first attempt.
80
+ single process invocation counts as the known first attempt. The run is
81
+ judged as a whole: any failing test in it fails every obligation of every
82
+ test that names the integration, so a suite with unrelated failures proves
83
+ nothing. Narrow the command to the proof-bearing tests with
84
+ `kibi proof inspect --update <id> --command "<command>" --json` (without
85
+ `--command` the proposal is the `package.json` `test` script or the
86
+ detected runner again), or make the run attributable per test: have the
87
+ command write a `kibi.proof-test-report.v1` to the path in
88
+ `KIBI_PROOF_TEST_REPORT` so each test is judged by its own steps.
71
89
  - `producer: playwright` (or custom) — the child emits `kibi.proof-run.v1`
72
90
  itself (bundled reporter: `kibi-cli/playwright-reporter`).
73
91
  - `producer: junit` / `tap` — Kibi converts the native report at `artifact`
@@ -79,6 +97,29 @@ Cosmetic edits (descriptions, labels) never stale proof. Execution-relevant
79
97
  edits (command, artifact, targets, options, bindings, contract) change the
80
98
  effective fingerprint and do.
81
99
 
100
+ ## What counts toward proofProven
101
+
102
+ A requirement is proven only by its scenarios' tests, and a scenario test
103
+ counts only when it is an **end-to-end** test with a valid, fresh, passing
104
+ receipt for the current snapshot.
105
+
106
+ - The scope comes from the test entity's `verification_scope`
107
+ (`end_to_end`, `integration`, `unit`), never from the integration id or the
108
+ producer. Without `verification_scope`, a test tagged `e2e` (or whose
109
+ `source` path contains `e2e`) counts as end-to-end; anything else is
110
+ `unknown` scope.
111
+ - A `unit` or `integration` receipt is still recorded on the test entity, but
112
+ it does not move `proofProven` for a behavioural requirement: the
113
+ requirement keeps the `missing_passing_e2e` gap in `proofGaps`. The one
114
+ exception is a scenario whose specifying requirements are all grounded only
115
+ in component-scope UI predicates (`ui_pattern`, `same_pattern`,
116
+ `pattern_marker`); a unit or integration test that renders the component
117
+ may prove it.
118
+ - `coverage_depth` (`kb_coverage`, `kb_find_gaps`) follows the test's
119
+ authored `status` (`passing`) and scope, not its receipts: a passing
120
+ receipt does not change it, and a `passing` status without a fresh receipt
121
+ proves nothing.
122
+
82
123
  ## Attempt semantics (factual, never optimistic)
83
124
 
84
125
  - `attempts: {status: "complete", entries: [...]}` — known history; the