kibi-cli 2.17.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 (60) hide show
  1. package/dist/cli-register-proof.js +9 -1
  2. package/dist/commands/doctor.d.ts.map +1 -1
  3. package/dist/commands/doctor.js +9 -3
  4. package/dist/commands/sync/untracked-targets.d.ts.map +1 -1
  5. package/dist/commands/sync/untracked-targets.js +39 -9
  6. package/dist/commands/sync.d.ts.map +1 -1
  7. package/dist/commands/sync.js +26 -5
  8. package/dist/engine.d.ts +4 -0
  9. package/dist/engine.d.ts.map +1 -1
  10. package/dist/engine.js +40 -10
  11. package/dist/operations/modeling/predicate-applicability.d.ts.map +1 -1
  12. package/dist/operations/modeling/predicate-applicability.js +5 -2
  13. package/dist/operations/modeling/predicate-catalog-4.d.ts.map +1 -1
  14. package/dist/operations/modeling/predicate-catalog-4.js +4 -0
  15. package/dist/operations/modeling/predicate-loader.d.ts.map +1 -1
  16. package/dist/operations/modeling/predicate-loader.js +3 -0
  17. package/dist/operations/modeling/predicate-score-3.js +1 -1
  18. package/dist/operations/modeling/predicate-types.d.ts +5 -0
  19. package/dist/operations/modeling/predicate-types.d.ts.map +1 -1
  20. package/dist/operations/mutation/source-authoring.d.ts +9 -0
  21. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  22. package/dist/operations/mutation/source-authoring.js +38 -2
  23. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  24. package/dist/operations/planning/apply-plan.js +10 -1
  25. package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
  26. package/dist/operations/semantic-advisor/clauses.js +7 -1
  27. package/dist/prolog/store-lock.d.ts +22 -0
  28. package/dist/prolog/store-lock.d.ts.map +1 -1
  29. package/dist/prolog/store-lock.js +45 -1
  30. package/dist/prolog.d.ts +7 -1
  31. package/dist/prolog.d.ts.map +1 -1
  32. package/dist/prolog.js +21 -3
  33. package/dist/proof/inspect.d.ts +7 -0
  34. package/dist/proof/inspect.d.ts.map +1 -1
  35. package/dist/proof/inspect.js +10 -0
  36. package/dist/public/operations/contracts.d.ts.map +1 -1
  37. package/dist/public/operations/contracts.js +3 -0
  38. package/dist/public/operations/discovery-executors.d.ts +14 -1
  39. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  40. package/dist/public/operations/discovery-executors.js +57 -7
  41. package/dist/public/operations/migration-plan.d.ts +2 -0
  42. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  43. package/dist/public/operations/migration-plan.js +31 -0
  44. package/dist/public/operations/source-relationship-parity.d.ts.map +1 -1
  45. package/dist/public/operations/source-relationship-parity.js +55 -1
  46. package/dist/public/skills/kibi-usage/SKILL.md +12 -3
  47. package/dist/public/skills/kibi-usage/resources/proof.md +23 -0
  48. package/dist/utils/branch-store.d.ts +8 -0
  49. package/dist/utils/branch-store.d.ts.map +1 -1
  50. package/dist/utils/branch-store.js +27 -1
  51. package/dist/utils/content-freshness.d.ts +13 -0
  52. package/dist/utils/content-freshness.d.ts.map +1 -0
  53. package/dist/utils/content-freshness.js +161 -0
  54. package/package.json +2 -2
  55. package/src/public/operations/contracts.ts +3 -0
  56. package/src/public/operations/discovery-executors.ts +84 -6
  57. package/src/public/operations/migration-plan.ts +44 -0
  58. package/src/public/operations/source-relationship-parity.ts +60 -3
  59. package/src/public/skills/kibi-usage/SKILL.md +12 -3
  60. package/src/public/skills/kibi-usage/resources/proof.md +23 -0
@@ -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 { RELATIONSHIP_TYPES } from "../../operations/mutation/relationships.js";
6
7
  import { parseListOfLists, parsePrologValue } from "../../prolog/codec.js";
7
8
  import { readAllShards } from "../../relationships/shards.js";
@@ -173,5 +174,58 @@ export async function collectSourceRelationshipParityViolations(workspaceRoot, p
173
174
  source: ".kb/relationships",
174
175
  });
175
176
  }
176
- return compareRelationshipParity(authored, await compiledRelationships(prolog));
177
+ return explainUntrackedEndpoints(workspaceRoot, compareRelationshipParity(authored, await compiledRelationships(prolog)));
178
+ }
179
+ /**
180
+ * An authored relationship is missing from the compiled KB most often
181
+ * because an endpoint is declared in a file sync cannot see: one Git does not
182
+ * track and no pending-source receipt covers (for example a `.kb/symbols.yaml`
183
+ * that `kibi init` created and kb_upsert appended to before receipts covered
184
+ * existing untracked files). Name that file and the fix.
185
+ */
186
+ // implements REQ-kibi-source-relationship-parity
187
+ function explainUntrackedEndpoints(workspaceRoot, violations) {
188
+ const endpoints = new Set();
189
+ for (const violation of violations) {
190
+ const relationship = authoredToCompiledRelationship(violation);
191
+ if (relationship === null)
192
+ continue;
193
+ endpoints.add(relationship.from);
194
+ endpoints.add(relationship.to);
195
+ }
196
+ if (endpoints.size === 0)
197
+ return violations;
198
+ const untracked = findUntrackedDocumentMatches(workspaceRoot, [...endpoints]);
199
+ if (untracked.size === 0)
200
+ return violations;
201
+ return violations.map((violation) => {
202
+ const relationship = authoredToCompiledRelationship(violation);
203
+ if (relationship === null)
204
+ return violation;
205
+ const sources = [relationship.from, relationship.to].flatMap((id) => {
206
+ const file = untracked.get(id);
207
+ return file === undefined ? [] : [{ entityId: id, file }];
208
+ });
209
+ if (sources.length === 0)
210
+ return violation;
211
+ const files = [...new Set(sources.map((source) => source.file))];
212
+ return {
213
+ ...violation,
214
+ 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.`,
215
+ evidence: {
216
+ ...(violation.evidence ?? {}),
217
+ untrackedSources: sources,
218
+ },
219
+ };
220
+ });
221
+ }
222
+ function authoredToCompiledRelationship(violation) {
223
+ const evidence = violation.evidence;
224
+ if (evidence?.direction !== "authored_to_compiled")
225
+ return null;
226
+ const from = evidence.relationship?.from;
227
+ const to = evidence.relationship?.to;
228
+ return typeof from === "string" && typeof to === "string"
229
+ ? { from, to }
230
+ : null;
177
231
  }
@@ -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.13.0
5
+ version: 2.14.0
6
6
  kibiCompatibility: ">=1.0.0"
7
7
  tags:
8
8
  - kibi
@@ -237,7 +237,11 @@ authored. Each one is the order the write path accepts.
237
237
  fact, `requires_predicate` to `relationshipTarget` and `specified_by` to
238
238
  its scenario: `specified_by` (req → scenario) is sent in the
239
239
  requirement's write, never in the scenario's, so create the scenario
240
- 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
241
245
  `modeled` entry and its grounding link must arrive in the same write
242
246
  (`modeled semantic_inventory entries (n) must equal logical grounding
243
247
  relationships (m)`); a clause not grounded yet stays `status: missing`
@@ -249,7 +253,12 @@ authored. Each one is the order the write path accepts.
249
253
  claim_keys must match logical grounding target claim_keys exactly`.
250
254
  Prefer supersession for a change of meaning: analyze the new prose,
251
255
  write its facts, write the new requirement (`-v2`) with `supersedes` to
252
- 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
253
262
  that keeps the meaning, re-ground in place: analyze the new prose,
254
263
  `kb_upsert` each grounding fact with the new `claim_key`/`claim_text`
255
264
  (same fact id), then `kb_upsert` the requirement with the new ledger,
@@ -97,6 +97,29 @@ Cosmetic edits (descriptions, labels) never stale proof. Execution-relevant
97
97
  edits (command, artifact, targets, options, bindings, contract) change the
98
98
  effective fingerprint and do.
99
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
+
100
123
  ## Attempt semantics (factual, never optimistic)
101
124
 
102
125
  - `attempts: {status: "complete", entries: [...]}` — known history; the
@@ -21,6 +21,14 @@ export declare function inspectBranchStore(workspaceRoot: string, branch: string
21
21
  * auto-heal; the reason exists so operators see the state before it bites.
22
22
  */
23
23
  export declare function storeLockJournalReason(storePath: string): Record<string, unknown> | null;
24
+ /**
25
+ * An informational notice that this branch store's lock was taken over from
26
+ * a dead holder. The takeover itself is safe and automatic; it is reported
27
+ * because it means an engine or prove run crashed or was killed while it
28
+ * held the store, which nothing else shows once the new holder rewrites the
29
+ * ownership journal. Visible for STORE_LOCK_TAKEOVER_VISIBLE_MS.
30
+ */
31
+ export declare function storeLockTakeoverReason(storePath: string, now?: Date): Record<string, unknown> | null;
24
32
  export declare function branchStoreReason(inspection: BranchStoreInspection): Record<string, unknown> | null;
25
33
  /**
26
34
  * The number of authored source files under `.kb/`: entity Markdown in the
@@ -1 +1 @@
1
- {"version":3,"file":"branch-store.d.ts","sourceRoot":"","sources":["../../src/utils/branch-store.ts"],"names":[],"mappings":"AAwBA,mEAAmE;AACnE,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AACrE,yCAAyC;AACzC,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAE1E,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,SAAS,GACT,YAAY,GACZ,YAAY,CAAC;AAEjB,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,KAAK,EAAE,gBAAgB,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC,CAAC;AAEH;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,GACb,qBAAqB,CA0GvB;AAED;;;;GAIG;AAEH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAiBhC;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,qBAAqB,GAChC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAWhC;AA6BD;;;;GAIG;AAEH,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC,CAAC;AAEH;;;;;;GAMG;AAEH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,qBAAqB,GAChC,sBAAsB,CAwBxB;AAkBD;;;;;;GAMG;AAEH,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,qBAAqB,EACjC,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAwBhC"}
1
+ {"version":3,"file":"branch-store.d.ts","sourceRoot":"","sources":["../../src/utils/branch-store.ts"],"names":[],"mappings":"AA0BA,mEAAmE;AACnE,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AACrE,yCAAyC;AACzC,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAE1E,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,SAAS,GACT,YAAY,GACZ,YAAY,CAAC;AAEjB,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,KAAK,EAAE,gBAAgB,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC,CAAC;AAEH;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,GACb,qBAAqB,CA0GvB;AAED;;;;GAIG;AAEH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAiBhC;AAED;;;;;;GAMG;AAEH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,IAAiB,GACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAehC;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,qBAAqB,GAChC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAWhC;AA6BD;;;;GAIG;AAEH,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAOpE;AAED,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC,CAAC;AAEH;;;;;;GAMG;AAEH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,qBAAqB,GAChC,sBAAsB,CAwBxB;AAkBD;;;;;;GAMG;AAEH,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,qBAAqB,EACjC,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAwBhC"}
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { existsSync, readFileSync, readdirSync, statSync, } from "node:fs";
6
6
  import * as path from "node:path";
7
- import { classifyStoreLockHolder, readStoreLockOwner, } from "../prolog/store-lock.js";
7
+ import { STORE_LOCK_TAKEOVER_VISIBLE_MS, classifyStoreLockHolder, readStoreLockOwner, readStoreLockTakeover, } from "../prolog/store-lock.js";
8
8
  import { branchStoreManifestMatches, branchStorePath, legacyBranchStorePath, } from "./branch-store-locator.js";
9
9
  import { ENTITY_LANES, KB_ROOT } from "./kb-paths.js";
10
10
  /** Stale-reason code of a branch store that was never compiled. */
@@ -147,6 +147,32 @@ export function storeLockJournalReason(storePath) {
147
147
  },
148
148
  };
149
149
  }
150
+ /**
151
+ * An informational notice that this branch store's lock was taken over from
152
+ * a dead holder. The takeover itself is safe and automatic; it is reported
153
+ * because it means an engine or prove run crashed or was killed while it
154
+ * held the store, which nothing else shows once the new holder rewrites the
155
+ * ownership journal. Visible for STORE_LOCK_TAKEOVER_VISIBLE_MS.
156
+ */
157
+ // implements REQ-core-journaled-engine-persistence
158
+ export function storeLockTakeoverReason(storePath, now = new Date()) {
159
+ const record = readStoreLockTakeover(storePath);
160
+ if (record === null)
161
+ return null;
162
+ const age = now.getTime() - Date.parse(record.at);
163
+ if (age < 0 || age > STORE_LOCK_TAKEOVER_VISIBLE_MS)
164
+ return null;
165
+ return {
166
+ code: "store_lock_taken_over",
167
+ path: record.path,
168
+ entityIds: [],
169
+ blocking: false,
170
+ brokenPid: record.brokenPid,
171
+ reason: record.reason,
172
+ at: record.at,
173
+ detail: `Kibi broke a stale branch-store lock at ${record.at} (${record.reason || "holder no longer running"}) and reattached. Nothing needs repair; a holder that died while it held the store usually means an engine or kibi prove run crashed or was killed.`,
174
+ };
175
+ }
150
176
  export function branchStoreReason(inspection) {
151
177
  if (inspection.state === "healthy")
152
178
  return null;
@@ -0,0 +1,13 @@
1
+ /** Stale-reason codes that compare file times only. */
2
+ export declare const MTIME_SOURCE_REASON_CODES: ReadonlySet<string>;
3
+ type Reason = Readonly<Record<string, unknown>>;
4
+ /**
5
+ * Drop mtime-only stale reasons whose file content provably matches what the
6
+ * store compiled. Reasons without such evidence are kept.
7
+ */
8
+ export declare function confirmSourceNewerReasons(workspaceRoot: string, storePath: string, reasons: readonly Reason[]): {
9
+ kept: Reason[];
10
+ dropped: Reason[];
11
+ };
12
+ export {};
13
+ //# sourceMappingURL=content-freshness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-freshness.d.ts","sourceRoot":"","sources":["../../src/utils/content-freshness.ts"],"names":[],"mappings":"AAYA,uDAAuD;AACvD,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAIxD,CAAC;AAEH,KAAK,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AA6IhD;;;GAGG;AAEH,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAkBvC"}
@@ -0,0 +1,161 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import * as path from "node:path";
8
+ import { load as parseYAML } from "js-yaml";
9
+ import { hashManifestWithCoordinates } from "../commands/sync/cache.js";
10
+ import { isSymbolsManifestPath } from "./kb-paths.js";
11
+ /** Stale-reason codes that compare file times only. */
12
+ export const MTIME_SOURCE_REASON_CODES = new Set([
13
+ "indexed_source_newer",
14
+ "knowledge_source_newer",
15
+ "documentation_source_newer",
16
+ ]);
17
+ function sha256(bytes) {
18
+ return createHash("sha256").update(bytes).digest("hex");
19
+ }
20
+ function readJson(file) {
21
+ try {
22
+ return JSON.parse(readFileSync(file, "utf8"));
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function record(value) {
29
+ return value !== null && typeof value === "object" && !Array.isArray(value)
30
+ ? value
31
+ : null;
32
+ }
33
+ /**
34
+ * Content evidence recorded by the last compilation: the store's sync-cache
35
+ * source hashes, and the per-symbol `sourceHash` of the generated coordinate
36
+ * artifact. A file whose bytes match that evidence has not changed since it
37
+ * was compiled, however new its mtime (a fresh clone, a checkout, a touch).
38
+ */
39
+ class CompiledContentEvidence {
40
+ workspaceRoot;
41
+ hashes;
42
+ coordinateHashes = null;
43
+ manifestState = new Map();
44
+ constructor(workspaceRoot, storePath) {
45
+ this.workspaceRoot = workspaceRoot;
46
+ const cache = record(readJson(path.join(storePath, "sync-cache.json")));
47
+ const hashes = record(cache?.hashes) ?? {};
48
+ this.hashes = Object.fromEntries(Object.entries(hashes).filter((entry) => typeof entry[1] === "string"));
49
+ }
50
+ fileHash(relative) {
51
+ try {
52
+ return sha256(readFileSync(path.resolve(this.workspaceRoot, relative)));
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ manifest(key) {
59
+ const cached = this.manifestState.get(key);
60
+ if (cached)
61
+ return cached;
62
+ const absolute = path.resolve(this.workspaceRoot, key);
63
+ let state = { matches: false, sources: new Set() };
64
+ try {
65
+ const matches = hashManifestWithCoordinates(this.workspaceRoot, absolute, path.join(path.dirname(absolute), "symbol-coordinates.yaml")) === this.hashes[key];
66
+ const parsed = record(parseYAML(readFileSync(absolute, "utf8")));
67
+ const sources = new Set();
68
+ for (const symbol of Array.isArray(parsed?.symbols)
69
+ ? parsed.symbols
70
+ : []) {
71
+ const row = record(symbol);
72
+ const source = row?.sourceFile ?? row?.source;
73
+ if (typeof source === "string" && source.length > 0)
74
+ sources.add(source.replaceAll("\\", "/"));
75
+ }
76
+ state = { matches, sources };
77
+ }
78
+ catch {
79
+ // An unreadable manifest proves nothing; keep the reason.
80
+ }
81
+ this.manifestState.set(key, state);
82
+ return state;
83
+ }
84
+ coordinates() {
85
+ if (this.coordinateHashes)
86
+ return this.coordinateHashes;
87
+ const byFile = new Map();
88
+ for (const key of Object.keys(this.hashes)) {
89
+ if (!isSymbolsManifestPath(key) || !key.endsWith("symbols.yaml"))
90
+ continue;
91
+ const artifact = path.resolve(this.workspaceRoot, path.dirname(key), "symbol-coordinates.yaml");
92
+ if (!existsSync(artifact))
93
+ continue;
94
+ try {
95
+ const parsed = record(parseYAML(readFileSync(artifact, "utf8")));
96
+ for (const value of Object.values(record(parsed?.coordinates) ?? {})) {
97
+ const row = record(value);
98
+ if (typeof row?.sourceFile !== "string" ||
99
+ typeof row.sourceHash !== "string")
100
+ continue;
101
+ const hashes = byFile.get(row.sourceFile) ?? new Set();
102
+ hashes.add(row.sourceHash);
103
+ byFile.set(row.sourceFile, hashes);
104
+ }
105
+ }
106
+ catch {
107
+ // A malformed artifact proves nothing.
108
+ }
109
+ }
110
+ this.coordinateHashes = byFile;
111
+ return byFile;
112
+ }
113
+ /** True only when recorded evidence proves the file's bytes are unchanged. */
114
+ unchanged(relative) {
115
+ const key = relative.replaceAll("\\", "/");
116
+ if (isSymbolsManifestPath(key) && this.hashes[key] !== undefined)
117
+ return this.manifest(key).matches;
118
+ const current = this.fileHash(key);
119
+ if (current === null)
120
+ return false;
121
+ const cached = this.hashes[key];
122
+ if (cached !== undefined)
123
+ return cached === current;
124
+ const coordinate = this.coordinates().get(key);
125
+ if (coordinate !== undefined && coordinate.size > 0)
126
+ return [...coordinate].every((hash) => hash === current);
127
+ // A code file that is only referenced from a symbol manifest is covered
128
+ // by that manifest's composite hash, which includes every referenced
129
+ // source's bytes.
130
+ return Object.keys(this.hashes).some((manifestKey) => {
131
+ if (!isSymbolsManifestPath(manifestKey))
132
+ return false;
133
+ const manifest = this.manifest(manifestKey);
134
+ return manifest.sources.has(key) && manifest.matches;
135
+ });
136
+ }
137
+ }
138
+ /**
139
+ * Drop mtime-only stale reasons whose file content provably matches what the
140
+ * store compiled. Reasons without such evidence are kept.
141
+ */
142
+ // implements REQ-cli-status-pre-first-sync
143
+ export function confirmSourceNewerReasons(workspaceRoot, storePath, reasons) {
144
+ const kept = [];
145
+ const dropped = [];
146
+ let evidence = null;
147
+ for (const reason of reasons) {
148
+ if (typeof reason.code !== "string" ||
149
+ !MTIME_SOURCE_REASON_CODES.has(reason.code) ||
150
+ typeof reason.path !== "string") {
151
+ kept.push(reason);
152
+ continue;
153
+ }
154
+ evidence ??= new CompiledContentEvidence(workspaceRoot, storePath);
155
+ if (evidence.unchanged(reason.path))
156
+ dropped.push(reason);
157
+ else
158
+ kept.push(reason);
159
+ }
160
+ return { kept, dropped };
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "2.17.0",
3
+ "version": "2.18.0",
4
4
  "type": "module",
5
5
  "description": "Kibi CLI for knowledge base management",
6
6
  "engines": {
@@ -235,7 +235,7 @@
235
235
  "fast-glob": "^3.2.12",
236
236
  "gray-matter": "^4.0.3",
237
237
  "js-yaml": "^4.1.0",
238
- "kibi-core": "^0.18.2",
238
+ "kibi-core": "^0.19.0",
239
239
  "kibi-plugin-builtin": "^0.5.0",
240
240
  "kibi-plugin-sdk": "^0.5.0",
241
241
  "kibi-swipl": "^1.0.1",
@@ -452,6 +452,8 @@ const BASE_DATA_SCHEMAS: Readonly<
452
452
  truncated: booleanValue,
453
453
  queryAnalysis: recordValue,
454
454
  answer: recordValue,
455
+ branch: { type: ["string", "null"] },
456
+ branchStore: recordValue,
455
457
  },
456
458
  ["results", "count"],
457
459
  ),
@@ -476,6 +478,7 @@ const BASE_DATA_SCHEMAS: Readonly<
476
478
  staleReasons: recordArray,
477
479
  staleReasonCount: integerValue,
478
480
  staleReasonsTruncated: booleanValue,
481
+ notices: recordArray,
479
482
  branchAttachment: recordValue,
480
483
  proofSnapshotChanges: recordArray,
481
484
  proofSnapshotChangeCount: integerValue,
@@ -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
  ? {
@@ -514,6 +591,7 @@ export async function executeStatus(
514
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,