kibi-cli 2.21.2 → 2.22.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 (96) hide show
  1. package/dist/cli-register-foundation.d.ts.map +1 -1
  2. package/dist/cli-register-foundation.js +1 -0
  3. package/dist/commands/check.d.ts +0 -1
  4. package/dist/commands/check.d.ts.map +1 -1
  5. package/dist/commands/check.js +47 -111
  6. package/dist/commands/doctor.d.ts.map +1 -1
  7. package/dist/commands/doctor.js +96 -6
  8. package/dist/commands/init-helpers.d.ts +14 -1
  9. package/dist/commands/init-helpers.d.ts.map +1 -1
  10. package/dist/commands/init-helpers.js +49 -10
  11. package/dist/commands/init.d.ts +1 -0
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +12 -1
  14. package/dist/commands/legacy-storage-migration.d.ts +46 -1
  15. package/dist/commands/legacy-storage-migration.d.ts.map +1 -1
  16. package/dist/commands/legacy-storage-migration.js +219 -9
  17. package/dist/commands/migrate.d.ts.map +1 -1
  18. package/dist/commands/migrate.js +48 -182
  19. package/dist/commands/sync/manifest.d.ts.map +1 -1
  20. package/dist/commands/sync/manifest.js +9 -0
  21. package/dist/extractors/symbols-coordinator.js +1 -0
  22. package/dist/intent-search.d.ts.map +1 -1
  23. package/dist/intent-search.js +28 -6
  24. package/dist/operations/migration/inventory-rederive.d.ts +40 -1
  25. package/dist/operations/migration/inventory-rederive.d.ts.map +1 -1
  26. package/dist/operations/migration/inventory-rederive.js +99 -8
  27. package/dist/operations/migration/requirement-status.d.ts +25 -0
  28. package/dist/operations/migration/requirement-status.d.ts.map +1 -0
  29. package/dist/operations/migration/requirement-status.js +105 -0
  30. package/dist/operations/migration/schema6.d.ts +25 -0
  31. package/dist/operations/migration/schema6.d.ts.map +1 -1
  32. package/dist/operations/migration/schema6.js +265 -7
  33. package/dist/operations/migration/symbol-legacy-links.d.ts +51 -0
  34. package/dist/operations/migration/symbol-legacy-links.d.ts.map +1 -0
  35. package/dist/operations/migration/symbol-legacy-links.js +319 -0
  36. package/dist/operations/mutation/context-warning.d.ts +12 -2
  37. package/dist/operations/mutation/context-warning.d.ts.map +1 -1
  38. package/dist/operations/mutation/context-warning.js +6 -6
  39. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  40. package/dist/operations/mutation/source-authoring.js +130 -35
  41. package/dist/operations/mutation/types.d.ts +2 -1
  42. package/dist/operations/mutation/types.d.ts.map +1 -1
  43. package/dist/operations/mutation/upsert.d.ts +17 -2
  44. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  45. package/dist/operations/mutation/upsert.js +28 -11
  46. package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
  47. package/dist/operations/mutation/validate-upsert.js +40 -8
  48. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  49. package/dist/operations/planning/apply-plan.js +18 -3
  50. package/dist/public/impact/diagnostics.d.ts.map +1 -1
  51. package/dist/public/impact/diagnostics.js +24 -14
  52. package/dist/public/operations/catalog.d.ts +3 -3
  53. package/dist/public/operations/check-executor.d.ts.map +1 -1
  54. package/dist/public/operations/check-executor.js +3 -0
  55. package/dist/public/operations/contracts.d.ts.map +1 -1
  56. package/dist/public/operations/contracts.js +6 -1
  57. package/dist/public/operations/discovery-executors.d.ts +2 -0
  58. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  59. package/dist/public/operations/discovery-executors.js +9 -2
  60. package/dist/public/operations/migration-plan.d.ts +2 -0
  61. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  62. package/dist/public/operations/migration-plan.js +101 -16
  63. package/dist/public/operations/result-envelope.d.ts.map +1 -1
  64. package/dist/public/operations/result-envelope.js +27 -1
  65. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  66. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  67. package/dist/public/operations/specs/check-rules.generated.js +2 -1
  68. package/dist/public/operations/specs/check.d.ts +1 -1
  69. package/dist/public/operations/specs/planning.d.ts +2 -2
  70. package/dist/public/operations/specs/planning.js +2 -2
  71. package/dist/public/operations/symbol-granularity-check.d.ts +18 -0
  72. package/dist/public/operations/symbol-granularity-check.d.ts.map +1 -0
  73. package/dist/public/operations/symbol-granularity-check.js +175 -0
  74. package/dist/public/symbol-granularity.d.ts +17 -0
  75. package/dist/public/symbol-granularity.d.ts.map +1 -1
  76. package/dist/public/symbol-granularity.js +21 -0
  77. package/dist/traceability/evidence-model.d.ts +7 -3
  78. package/dist/traceability/evidence-model.d.ts.map +1 -1
  79. package/dist/traceability/staged-symbols-manifest.js +6 -5
  80. package/dist/traceability/validate.d.ts +14 -1
  81. package/dist/traceability/validate.d.ts.map +1 -1
  82. package/dist/traceability/validate.js +41 -0
  83. package/dist/utils/rule-registry.generated.d.ts +5 -0
  84. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  85. package/dist/utils/rule-registry.generated.js +6 -0
  86. package/package.json +3 -3
  87. package/src/public/impact/diagnostics.ts +29 -17
  88. package/src/public/operations/check-executor.ts +7 -0
  89. package/src/public/operations/contracts.ts +6 -1
  90. package/src/public/operations/discovery-executors.ts +12 -2
  91. package/src/public/operations/migration-plan.ts +100 -19
  92. package/src/public/operations/result-envelope.ts +34 -1
  93. package/src/public/operations/specs/check-rules.generated.ts +2 -1
  94. package/src/public/operations/specs/planning.ts +2 -2
  95. package/src/public/operations/symbol-granularity-check.ts +233 -0
  96. package/src/public/symbol-granularity.ts +37 -0
@@ -264,8 +264,7 @@ function getEffectiveManifestRecords(options) {
264
264
  stagedCoordinateArtifact,
265
265
  };
266
266
  }
267
- /** Max missing/extra symbol titles surfaced per file before "+N more". */
268
- const MAX_DETAIL_ITEMS = 6;
267
+ // implements REQ-cli-staged-impact-enforcement
269
268
  function diffSymbolEvidence(sourcePath, expectedSymbols, stagedSymbols) {
270
269
  const stagedTitles = new Set(stagedSymbols.map((symbol) => symbol.title));
271
270
  const expectedTitles = new Set(expectedSymbols.map((symbol) => symbol.title));
@@ -282,12 +281,14 @@ function diffSymbolEvidence(sourcePath, expectedSymbols, stagedSymbols) {
282
281
  if (missing.length === 0 && extra.length === 0) {
283
282
  return null;
284
283
  }
284
+ // Keep the complete lists: human-facing renderers truncate (with an
285
+ // accurate "… and N more"), while JSON evidence carries every name.
285
286
  return {
286
287
  path: sourcePath,
287
288
  expectedCount: expectedSymbols.length,
288
- coveredCount: stagedSymbols.length,
289
- missing: missing.slice(0, MAX_DETAIL_ITEMS),
290
- extra: extra.slice(0, MAX_DETAIL_ITEMS),
289
+ coveredCount: expectedSymbols.length - missing.length,
290
+ missing,
291
+ extra,
291
292
  };
292
293
  }
293
294
  export function assessStagedSymbolsManifest(options) {
@@ -1,4 +1,5 @@
1
- import type { PrologProcess } from "../prolog.js";
1
+ import { type PrologProcess } from "../prolog.js";
2
+ import { type ParsedViolation } from "../prolog/codec.js";
2
3
  export interface ValidationOptions {
3
4
  minLinks: number;
4
5
  prolog: PrologProcess;
@@ -17,6 +18,18 @@ declare function splitTopLevelComma(s: string): string[];
17
18
  declare function splitTopLevelLists(value: string): string[];
18
19
  declare function parsePrologListOfLists(value: string): string[][];
19
20
  export declare function validateStagedSymbols(options: ValidationOptions): Promise<Violation[]>;
21
+ /**
22
+ * Full-check `symbol-coverage` for the staged change set: the changed source
23
+ * symbols in the consulted overlay plus `extraSymbolIds` (symbols whose staged
24
+ * manifest entry changed). The Prolog rule is the one `kibi check` runs, so a
25
+ * commit cannot pass the staged gate with coverage the full check rejects.
26
+ */
27
+ export declare function validateStagedSymbolCoverage(options: {
28
+ prolog: PrologProcess;
29
+ extraSymbolIds?: readonly string[];
30
+ }): Promise<ParsedViolation[]>;
31
+ /** Text lines for staged symbol-coverage violations, in `kibi check` form. */
32
+ export declare function formatCoverageViolations(violations: readonly ParsedViolation[]): string;
20
33
  export declare function formatViolations(violations: Violation[]): string;
21
34
  export declare const __test__: {
22
35
  unquoteAtom: typeof unquoteAtom;
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/traceability/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,iBAAS,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO1C;AAED,iBAAS,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CA0B/C;AAED,iBAAS,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8CnD;AAED,iBAAS,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAuBzD;AAGD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,EAAE,CAAC,CAgDtB;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,CAkBhE;AAGD,eAAO,MAAM,QAAQ;;;;;CAKpB,CAAC"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/traceability/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,aAAa,EAAmB,MAAM,cAAc,CAAC;AACnE,OAAO,EACL,KAAK,eAAe,EAIrB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,iBAAS,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO1C;AAED,iBAAS,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CA0B/C;AAED,iBAAS,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8CnD;AAED,iBAAS,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAuBzD;AAGD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,EAAE,CAAC,CAgDtB;AAED;;;;;GAKG;AAEH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE;IAC1D,MAAM,EAAE,aAAa,CAAC;IACtB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAgB7B;AAED,8EAA8E;AAE9E,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,SAAS,eAAe,EAAE,GACrC,MAAM,CAaR;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,CAkBhE;AAGD,eAAO,MAAM,QAAQ;;;;;CAKpB,CAAC"}
@@ -1,3 +1,6 @@
1
+ import path from "node:path";
2
+ import { resolveKbPlPath } from "../prolog.js";
3
+ import { escapeAtom, parseViolationRows, toPrologAtom, } from "../prolog/codec.js";
1
4
  function unquoteAtom(value) {
2
5
  // remove surrounding single quotes and unescape doubled quotes
3
6
  const trimmed = value.trim();
@@ -141,6 +144,44 @@ export async function validateStagedSymbols(options) {
141
144
  }
142
145
  return violations;
143
146
  }
147
+ /**
148
+ * Full-check `symbol-coverage` for the staged change set: the changed source
149
+ * symbols in the consulted overlay plus `extraSymbolIds` (symbols whose staged
150
+ * manifest entry changed). The Prolog rule is the one `kibi check` runs, so a
151
+ * commit cannot pass the staged gate with coverage the full check rejects.
152
+ */
153
+ // implements REQ-014
154
+ export async function validateStagedSymbolCoverage(options) {
155
+ const extra = `[${[...new Set(options.extraSymbolIds ?? [])]
156
+ .sort()
157
+ .map(toPrologAtom)
158
+ .join(",")}]`;
159
+ const checksPl = escapeAtom(path.join(path.dirname(resolveKbPlPath()), "checks.pl"));
160
+ const goal = `(use_module('${checksPl}'), call(checks:check_changed_symbol_coverage(${extra}, Violations)))`;
161
+ const res = await options.prolog.query(goal);
162
+ if (!res.success) {
163
+ throw new Error(`Prolog symbol-coverage query failed: ${res.error || "unknown error"}`);
164
+ }
165
+ return parseViolationRows(res.bindings.Violations ?? "[]");
166
+ }
167
+ /** Text lines for staged symbol-coverage violations, in `kibi check` form. */
168
+ // implements REQ-014
169
+ export function formatCoverageViolations(violations) {
170
+ if (violations.length === 0)
171
+ return "";
172
+ const lines = [
173
+ `Found ${violations.length} symbol-coverage violation(s) in staged symbols (the same rule kibi check runs):`,
174
+ ];
175
+ for (const violation of violations) {
176
+ lines.push("", `[${violation.rule}] ${violation.entityId}`);
177
+ if (violation.source)
178
+ lines.push(` Source: ${violation.source}`);
179
+ lines.push(` ${violation.description}`);
180
+ if (violation.suggestion)
181
+ lines.push(` Suggestion: ${violation.suggestion}`);
182
+ }
183
+ return lines.join("\n");
184
+ }
144
185
  export function formatViolations(violations) {
145
186
  // implements REQ-014
146
187
  if (!violations || violations.length === 0)
@@ -13,6 +13,11 @@ export declare const GENERATED_RULES: readonly [{
13
13
  readonly description: "Production symbols must directly implement requirements for ownership; covered_by is coverage only and executable_for is test identity only";
14
14
  readonly enforcementClass: "canonical";
15
15
  readonly category: "traceability";
16
+ }, {
17
+ readonly name: "symbol-granularity";
18
+ readonly description: "A symbol with traceability relationships must not link its source file coarsely while narrower behavioral symbols are available, unless it carries an allowed granularity_reason; the staged gate's symbol_granularity_violation evaluated KB-wide";
19
+ readonly enforcementClass: "canonical";
20
+ readonly category: "traceability";
16
21
  }, {
17
22
  readonly name: "no-dangling-refs";
18
23
  readonly description: "All relationship targets must exist in the KB";
@@ -1 +1 @@
1
- {"version":3,"file":"rule-registry.generated.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.generated.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyWlB,CAAC"}
1
+ {"version":3,"file":"rule-registry.generated.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.generated.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgXlB,CAAC"}
@@ -26,6 +26,12 @@ export const GENERATED_RULES = [
26
26
  enforcementClass: "canonical",
27
27
  category: "traceability",
28
28
  },
29
+ {
30
+ name: "symbol-granularity",
31
+ description: "A symbol with traceability relationships must not link its source file coarsely while narrower behavioral symbols are available, unless it carries an allowed granularity_reason; the staged gate's symbol_granularity_violation evaluated KB-wide",
32
+ enforcementClass: "canonical",
33
+ category: "traceability",
34
+ },
29
35
  {
30
36
  name: "no-dangling-refs",
31
37
  description: "All relationship targets must exist in the KB",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "2.21.2",
3
+ "version": "2.22.0",
4
4
  "type": "module",
5
5
  "description": "Kibi CLI for knowledge base management",
6
6
  "engines": {
@@ -235,8 +235,8 @@
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.21.1",
239
- "kibi-plugin-builtin": "^0.5.1",
238
+ "kibi-core": "^0.22.0",
239
+ "kibi-plugin-builtin": "^0.5.2",
240
240
  "kibi-plugin-sdk": "^0.5.0",
241
241
  "kibi-swipl": "^1.0.1",
242
242
  "saxes": "^6.0.0",
@@ -12,6 +12,7 @@ import {
12
12
  getNonBehavioralSymbolNames,
13
13
  isAllowedGranularityReason,
14
14
  isTraceabilityRelationshipType,
15
+ narrowerSymbols,
15
16
  } from "../symbol-granularity.js";
16
17
  import type {
17
18
  QualityDiagnostic,
@@ -19,16 +20,6 @@ import type {
19
20
  SymbolGranularityDiagnosticsOptions,
20
21
  } from "./types.js";
21
22
 
22
- function isNarrowerSymbol(
23
- manifestSymbolName: string,
24
- candidate: ExtractedSymbol,
25
- sourceSymbols: readonly ExtractedSymbol[],
26
- ): boolean {
27
- if (candidate.name === manifestSymbolName) return false;
28
- if (candidate.name.startsWith(`${manifestSymbolName}.`)) return true;
29
- return !sourceSymbols.some((symbol) => symbol.name === manifestSymbolName);
30
- }
31
-
32
23
  function hasTraceabilityRelationship(result: ExtractionResult): boolean {
33
24
  return result.relationships.some((relationship) =>
34
25
  isTraceabilityRelationshipType(relationship.type),
@@ -110,6 +101,7 @@ function isChangedBehavioralReviewCandidate(symbol: ExtractedSymbol): boolean {
110
101
  return symbol.hunkRanges.length > 0;
111
102
  }
112
103
 
104
+ // implements REQ-cli-staged-impact-enforcement, REQ-cli-check
113
105
  export function createSymbolGranularityDiagnostics(
114
106
  options: SymbolGranularityDiagnosticsOptions,
115
107
  ): KibiImpactDiagnostic[] {
@@ -130,15 +122,17 @@ export function createSymbolGranularityDiagnostics(
130
122
  result.sourceFile,
131
123
  options,
132
124
  );
133
- const narrowerSymbols = candidateSymbols.filter((symbol) =>
134
- isNarrowerSymbol(result.entity.title, symbol, sourceSymbols),
125
+ const narrower = narrowerSymbols(
126
+ result.entity.title,
127
+ candidateSymbols,
128
+ sourceSymbols,
135
129
  );
136
- if (narrowerSymbols.length === 0) continue;
130
+ if (narrower.length === 0) continue;
137
131
 
138
- const behavioralNames = getBehavioralSymbolNames(narrowerSymbols);
132
+ const behavioralNames = getBehavioralSymbolNames(narrower);
139
133
  if (behavioralNames.length === 0) continue;
140
134
 
141
- const nonBehavioralNames = getNonBehavioralSymbolNames(narrowerSymbols);
135
+ const nonBehavioralNames = getNonBehavioralSymbolNames(narrower);
142
136
  const ignoredSymbolsSuggestion =
143
137
  nonBehavioralNames.length > 0
144
138
  ? ` Non-behavioral symbols ignored for this decision: ${nonBehavioralNames.join(
@@ -146,15 +140,33 @@ export function createSymbolGranularityDiagnostics(
146
140
  )}.`
147
141
  : "";
148
142
 
143
+ // A title the extractor does not produce (a private class member, a
144
+ // renamed or removed declaration) can never become granular by refresh:
145
+ // say so instead of only calling the link coarse.
146
+ const titleExtracted = sourceSymbols.some(
147
+ (symbol) => symbol.name === result.entity.title,
148
+ );
149
149
  diagnostics.push({
150
150
  id: "symbol_granularity_violation",
151
151
  severity: "error",
152
152
  blocking: true,
153
153
  category: "symbol",
154
+ entityId: result.entity.id,
155
+ ...(result.entity.source ? { source: result.entity.source } : {}),
154
156
  files: [result.entity.source, result.sourceFile],
155
157
  docs: ["docs/symbol-traceability-taxonomy.md"],
156
- message: `Symbol ${result.entity.id} links ${result.sourceFile} coarsely while granular symbols are available (behavioral only): ${behavioralNames.join(", ")}`,
157
- suggestion: `Move ownership/coverage/test relationships to the narrow behavioral symbol, add a manifest behavioral anchor, or add granularity_reason with ${ALLOWED_GRANULARITY_REASONS_PROSE} when the coarse symbol is intentional.${ignoredSymbolsSuggestion}`,
158
+ message: titleExtracted
159
+ ? `Symbol ${result.entity.id} links ${result.sourceFile} coarsely while granular symbols are available (behavioral only): ${behavioralNames.join(", ")}`
160
+ : `Symbol ${result.entity.id} names '${result.entity.title}', which the source extractor does not produce for ${result.sourceFile} (private class members and removed or renamed declarations are not tracked), so it links ${result.sourceFile} coarsely while granular symbols are available (behavioral only): ${behavioralNames.join(", ")}`,
161
+ suggestion: titleExtracted
162
+ ? `Move ownership/coverage/test relationships to the narrow behavioral symbol, add a manifest behavioral anchor, or add granularity_reason with ${ALLOWED_GRANULARITY_REASONS_PROSE} when the coarse symbol is intentional.${ignoredSymbolsSuggestion}`
163
+ : `Re-point this symbol's relationships to the public declaration that owns the behavior (one of: ${behavioralNames.join(", ")}) or remove the symbol; add granularity_reason with ${ALLOWED_GRANULARITY_REASONS_PROSE} only when coarse ownership is intentional.${ignoredSymbolsSuggestion}`,
164
+ evidence: {
165
+ sourceFile: result.sourceFile,
166
+ title: result.entity.title,
167
+ titleExtracted,
168
+ granularSymbols: behavioralNames,
169
+ },
158
170
  });
159
171
  }
160
172
 
@@ -57,6 +57,7 @@ import {
57
57
  collectSourcePathDanglingViolations,
58
58
  } from "./source-path-dangling.js";
59
59
  import { collectSourceRelationshipParityViolations } from "./source-relationship-parity.js";
60
+ import { collectSymbolGranularityViolations } from "./symbol-granularity-check.js";
60
61
  import type { OperationResult } from "./types.js";
61
62
  import { readWorkspaceSnapshot } from "./workspace-snapshot.js";
62
63
 
@@ -367,6 +368,11 @@ export async function executeCheck(
367
368
  const sourcePathFindings = rulesAllowlist.has(SOURCE_PATH_DANGLING_RULE)
368
369
  ? collectSourcePathDanglingViolations(workspaceRoot)
369
370
  : [];
371
+ const symbolGranularityFindings = await collectSymbolGranularityViolations(
372
+ prolog,
373
+ workspaceRoot,
374
+ rulesAllowlist,
375
+ );
370
376
  const originReviewFindings = await collectOriginReviewViolations(
371
377
  prolog,
372
378
  rulesAllowlist,
@@ -390,6 +396,7 @@ export async function executeCheck(
390
396
  ...queryPlanViolations,
391
397
  ...sourceRelationshipParityViolations,
392
398
  ...sourcePathFindings,
399
+ ...symbolGranularityFindings,
393
400
  ...predicateConformanceFindings,
394
401
  ...originReviewFindings,
395
402
  ...entityContextFindings,
@@ -442,7 +442,12 @@ const BASE_DATA_SCHEMAS: Readonly<
442
442
  }),
443
443
  kb_skills_read: objectData({ content: stringValue }, ["content"]),
444
444
  kb_query: objectData(
445
- { entities: recordArray, count: integerValue, factQuery: recordValue },
445
+ {
446
+ entities: recordArray,
447
+ count: integerValue,
448
+ factQuery: recordValue,
449
+ branchStore: recordValue,
450
+ },
446
451
  ["entities", "count"],
447
452
  ),
448
453
  kb_search: objectData(
@@ -95,6 +95,8 @@ export type QueryPayload = {
95
95
  readonly entities: readonly Record<string, unknown>[];
96
96
  readonly count: number;
97
97
  readonly factQuery?: FactQueryPayload;
98
+ /** Present when an empty answer came from a store never compiled for this branch. */
99
+ readonly branchStore?: SearchBranchStoreNotice;
98
100
  };
99
101
 
100
102
  export const FACT_KINDS = [
@@ -711,9 +713,13 @@ export async function executeQuery(
711
713
  ? entity
712
714
  : { ...entity, firmness };
713
715
  });
716
+ // An empty page from a never-compiled store says nothing about the
717
+ // authored entities, so the answer names the store state.
718
+ const storeNotice =
719
+ indexedPage.count === 0 ? uncompiledSearchNotice(context) : null;
714
720
  const text =
715
721
  indexedPage.count === 0
716
- ? `No entities found${type ? ` of type '${type}'` : ""}.`
722
+ ? `No entities found${type ? ` of type '${type}'` : ""}.${storeNotice ? ` Note: ${storeNotice.detail} Run kibi sync before relying on query results.` : ""}`
717
723
  : `Found ${indexedPage.count} entities${type ? ` of type '${type}'` : ""}. Showing ${paginated.length} (offset ${offset}, limit ${limit}): ${paginated
718
724
  .map((entity) => {
719
725
  const entityId = String(entity.id ?? "").replace(
@@ -725,7 +731,11 @@ export async function executeQuery(
725
731
  .join(", ")}`;
726
732
  return {
727
733
  content: [{ type: "text", text }],
728
- structuredContent: { entities: paginated, count: indexedPage.count },
734
+ structuredContent: {
735
+ entities: paginated,
736
+ count: indexedPage.count,
737
+ ...(storeNotice ? { branchStore: storeNotice } : {}),
738
+ },
729
739
  };
730
740
  } catch (error) {
731
741
  const message = error instanceof Error ? error.message : String(error);
@@ -396,6 +396,66 @@ function predicateSchemaAlignmentAction(
396
396
  /** Migration code for mechanical predicate namespace/alias repairs. */
397
397
  export const PREDICATE_SCHEMA_ALIGNMENT_CODE = "predicate_schema_alignment";
398
398
 
399
+ /** Migration code of the grouped untracked-source parity findings. */
400
+ export const UNTRACKED_KNOWLEDGE_SOURCES_CODE = "untracked_knowledge_sources";
401
+
402
+ /**
403
+ * Every source-relationship-parity finding explained by untracked files
404
+ * (evidence `untrackedSources`) has one cause and one fix: the files Git
405
+ * does not track and no pending-source receipt covers. After a storage move
406
+ * that can be thousands of relationships, so they become one review action
407
+ * naming the files instead of one per relationship.
408
+ */
409
+ // implements REQ-agent-guided-migration-orchestration, REQ-kibi-source-relationship-parity
410
+ function untrackedSourceActionFromViolations(
411
+ violations: readonly Readonly<Record<string, unknown>>[],
412
+ ): { action: MigrationAction; consumed: ReadonlySet<number> } | null {
413
+ const consumed = new Set<number>();
414
+ const files = new Set<string>();
415
+ const entityIds = new Set<string>();
416
+ const relationships: unknown[] = [];
417
+ for (const [index, violation] of violations.entries()) {
418
+ if (violation.rule !== "source-relationship-parity") continue;
419
+ const evidence = isRecord(violation.evidence) ? violation.evidence : {};
420
+ const sources = Array.isArray(evidence.untrackedSources)
421
+ ? evidence.untrackedSources.filter(isRecord)
422
+ : [];
423
+ if (sources.length === 0) continue;
424
+ consumed.add(index);
425
+ for (const source of sources) {
426
+ if (typeof source.file === "string") files.add(source.file);
427
+ if (typeof source.entityId === "string") entityIds.add(source.entityId);
428
+ }
429
+ if (evidence.relationship !== undefined)
430
+ relationships.push(evidence.relationship);
431
+ }
432
+ if (consumed.size === 0) return null;
433
+ const listed = [...files].sort();
434
+ return {
435
+ consumed,
436
+ action: migrationAction({
437
+ id: "untracked-knowledge-sources",
438
+ code: UNTRACKED_KNOWLEDGE_SOURCES_CODE,
439
+ category: "relationship",
440
+ safety: "review",
441
+ invocation: {
442
+ kind: "review",
443
+ instruction: `${consumed.size} authored relationship(s) are missing from the compiled KB because ${listed.length} file(s) declaring their endpoints are not tracked by Git and no pending-source receipt covers them: ${listed.join(", ")}. Stage them (git add; staging is enough, Kibi never stages files itself) or rewrite the entities with kb_upsert, then run kibi sync and rerun kibi migrate.`,
444
+ },
445
+ affectedEntityIds: [...entityIds],
446
+ affectedFiles: listed,
447
+ postconditions: [{ rule: "source-relationship-parity", findings: 0 }],
448
+ evidence: {
449
+ relationshipCount: consumed.size,
450
+ files: listed,
451
+ // A sample; the count above is the full number.
452
+ relationships: relationships.slice(0, 20),
453
+ },
454
+ dispositionRequired: true,
455
+ }),
456
+ };
457
+ }
458
+
399
459
  // implements REQ-agent-guided-migration-orchestration, REQ-kibi-predicate-vocabulary-migration, REQ-cli-schema-migration
400
460
  export function buildActionsFromCheck(input: {
401
461
  violations?: readonly Readonly<Record<string, unknown>>[];
@@ -404,8 +464,11 @@ export function buildActionsFromCheck(input: {
404
464
  const actions: MigrationAction[] = [];
405
465
  const lifecycle = lifecycleActionsFromViolations(input.violations ?? []);
406
466
  actions.push(...lifecycle.actions.map(migrationAction));
467
+ const untracked = untrackedSourceActionFromViolations(input.violations ?? []);
468
+ if (untracked !== null) actions.push(untracked.action);
407
469
  for (const [index, violation] of (input.violations ?? []).entries()) {
408
470
  if (lifecycle.consumed.has(index)) continue;
471
+ if (untracked?.consumed.has(index)) continue;
409
472
  const rule = typeof violation.rule === "string" ? violation.rule : "check";
410
473
  const entityId =
411
474
  typeof violation.entityId === "string" ? violation.entityId : "";
@@ -621,6 +684,18 @@ function readLegacySchemaVersion(
621
684
  /** Migration action code of the automatic recompile of changed sources. */
622
685
  export const FRESHNESS_RESYNC = "freshness_resync";
623
686
 
687
+ // implements REQ-cli-status-pre-first-sync
688
+ /**
689
+ * Stale reasons one `kibi sync` clears: sources whose content changed since
690
+ * the last compilation, and a store compiled under another compiler contract
691
+ * (the sync discards that cache, re-imports every source and stamps the
692
+ * current contract).
693
+ */
694
+ const RESYNC_REASON_CODES: ReadonlySet<string> = new Set([
695
+ ...MTIME_SOURCE_REASON_CODES,
696
+ "compiler_changed",
697
+ ]);
698
+
624
699
  export function buildActionsFromStatus(input: {
625
700
  workspaceRoot: string;
626
701
  branchAttachment?: Readonly<Record<string, unknown>>;
@@ -686,17 +761,20 @@ export function buildActionsFromStatus(input: {
686
761
  }
687
762
 
688
763
  const storeState = typeof store?.state === "string" ? store.state : null;
689
- const legacyStorageMigrationRequired = legacyConfigExists(
690
- input.workspaceRoot,
691
- );
764
+ // Legacy storage is evaluated whether or not the legacy config survives:
765
+ // legacy copies left beside a migrated .kb/ (moved, identical or stale)
766
+ // are the same finding 'kibi doctor' and 'kibi migrate' report.
767
+ // implements REQ-cli-schema-migration
768
+ const legacyStorage = planLegacyStorageMigration(input.workspaceRoot);
769
+ const legacyStorageMigrationRequired =
770
+ legacyConfigExists(input.workspaceRoot) ||
771
+ legacyStorage.moves.length > 0 ||
772
+ legacyStorage.removals.length > 0 ||
773
+ legacyStorage.blockers.length > 0;
692
774
  // A legacy cutover that cannot move its files (a destination already
693
775
  // exists, a malformed config) is an operator decision, not an automatic
694
776
  // step that fails when applied and takes the compile with it.
695
- const legacyStorage = legacyStorageMigrationRequired
696
- ? planLegacyStorageMigration(input.workspaceRoot)
697
- : null;
698
- const legacyStorageBlocked =
699
- legacyStorage !== null && legacyStorage.blockers.length > 0;
777
+ const legacyStorageBlocked = legacyStorage.blockers.length > 0;
700
778
  if (storeState === "missing") {
701
779
  actions.push(
702
780
  migrationAction({
@@ -750,26 +828,29 @@ export function buildActionsFromStatus(input: {
750
828
  invocation: legacyStorageBlocked
751
829
  ? {
752
830
  kind: "review",
753
- instruction: `The legacy .kb/config.json still names knowledge files outside .kb/ and Kibi cannot move them: ${legacyStorage?.blockers.join(" ")} Compare each legacy copy with its .kb/ copy and keep the current one: replace the .kb/ copy when the legacy one is newer, or remove (git rm) the legacy copy when it is a stale duplicate. Then run 'kibi migrate --yes' to retire .kb/config.json.`,
831
+ instruction: `Legacy knowledge files outside .kb/ cannot be migrated automatically: ${legacyStorage.blockers.join(" ")} Then run 'kibi migrate --yes'.`,
754
832
  }
755
833
  : { kind: "cli", command_argv: ["kibi", "migrate", "--yes"] },
756
834
  affectedFiles: [
757
835
  ".kb/manifest.json",
758
836
  ".kb/migrations",
759
837
  ".kb/config.json",
760
- ...(legacyStorage?.moves.map((move) => move.from) ?? []),
838
+ ...legacyStorage.moves.map((move) => move.from),
839
+ ...legacyStorage.removals.map((removal) => removal.path),
761
840
  ],
762
841
  evidence: {
763
842
  configStatus,
764
- ...(legacyStorage === null
765
- ? {}
766
- : {
843
+ ...(legacyStorageMigrationRequired
844
+ ? {
767
845
  legacyStorage: {
768
846
  moves: legacyStorage.moves,
847
+ removals: legacyStorage.removals,
848
+ conflicts: legacyStorage.conflicts,
769
849
  blockers: legacyStorage.blockers,
770
850
  legacyConfig: legacyStorage.legacyConfig,
771
851
  },
772
- }),
852
+ }
853
+ : {}),
773
854
  },
774
855
  dependsOn: storeState === "missing" ? ["branch-store-ensure"] : [],
775
856
  autoApplicable: !operatorOnly,
@@ -816,12 +897,12 @@ export function buildActionsFromStatus(input: {
816
897
  // implements REQ-cli-status-pre-first-sync
817
898
  // Sources whose content changed since the last compilation (kb_status has
818
899
  // already dropped mtime-only reasons) are fixed by recompiling them: one
819
- // automatic `kibi sync`, not a review per file. An uncompiled store is
820
- // compiled by the step above, which also covers these files.
900
+ // automatic `kibi sync`, not a review per file. A store compiled under
901
+ // another compiler contract is fixed by the same sync. An uncompiled store
902
+ // is compiled by the step above, which also covers these reasons.
821
903
  const resyncReasons = (input.staleReasons ?? []).filter(
822
904
  (reason) =>
823
- typeof reason.code === "string" &&
824
- MTIME_SOURCE_REASON_CODES.has(reason.code),
905
+ typeof reason.code === "string" && RESYNC_REASON_CODES.has(reason.code),
825
906
  );
826
907
  if (resyncReasons.length > 0 && uncompiled === undefined) {
827
908
  actions.push(
@@ -856,7 +937,7 @@ export function buildActionsFromStatus(input: {
856
937
  // Planned above as an automatic compile step, not a review.
857
938
  if (code === BRANCH_STORE_NOT_COMPILED) continue;
858
939
  // Planned above as one automatic resync (or covered by the compile).
859
- if (MTIME_SOURCE_REASON_CODES.has(code)) continue;
940
+ if (RESYNC_REASON_CODES.has(code)) continue;
860
941
  const file = typeof reason.path === "string" ? reason.path : "";
861
942
  actions.push(
862
943
  migrationAction({
@@ -17,6 +17,36 @@ function diagnostics(value: unknown): KibiResult["diagnostics"] {
17
17
  });
18
18
  }
19
19
 
20
+ // Discovery operations whose empty answer may come from an uncompiled store.
21
+ const STORE_READ_OPERATIONS = new Set(["kb_search", "kb_query"]);
22
+
23
+ /**
24
+ * A search or query answered from a branch store that was never compiled
25
+ * reads an empty KB. The payload already names the store state; the envelope
26
+ * also raises it as a warning, so a caller that reads only `status` and
27
+ * `diagnostics` does not take the empty answer as "nothing governs this".
28
+ */
29
+ // implements REQ-cli-status-pre-first-sync, REQ-mcp-search-discovery
30
+ function uncompiledStoreDiagnostics(
31
+ operation: string,
32
+ row: Record<string, unknown> | undefined,
33
+ ): KibiResult["diagnostics"] {
34
+ const store = row?.branchStore;
35
+ if (
36
+ !STORE_READ_OPERATIONS.has(operation) ||
37
+ !record(store) ||
38
+ store.state !== "not_compiled"
39
+ )
40
+ return [];
41
+ return [
42
+ {
43
+ ...(typeof store.code === "string" ? { code: store.code } : {}),
44
+ severity: "warning",
45
+ message: `${typeof store.detail === "string" ? `${store.detail} ` : ""}An empty answer here does not mean no requirement governs this; run \`kibi sync\` and ask again.`,
46
+ },
47
+ ];
48
+ }
49
+
20
50
  function nextActions(value: unknown): KibiResult["nextActions"] {
21
51
  if (!Array.isArray(value)) return [];
22
52
  return value.flatMap((entry) => {
@@ -214,7 +244,10 @@ export function toKibiResult<T>(
214
244
  ...spec.effects.map((effect) => effectStatus(effect, row, outcome)),
215
245
  ...extraFailedEffects(spec.effects, row),
216
246
  ],
217
- diagnostics: options.diagnostics ?? diagnostics(row?.diagnostics),
247
+ diagnostics: options.diagnostics ?? [
248
+ ...diagnostics(row?.diagnostics),
249
+ ...uncompiledStoreDiagnostics(String(spec.name), row),
250
+ ],
218
251
  nextActions: options.nextActions ?? nextActions(row?.nextActions),
219
252
  ...(resultError ? { error: resultError } : {}),
220
253
  };
@@ -13,6 +13,7 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
13
13
  "must-priority-coverage",
14
14
  "symbol-coverage",
15
15
  "symbol-traceability",
16
+ "symbol-granularity",
16
17
  "no-dangling-refs",
17
18
  "source-relationship-parity",
18
19
  "branch-store-not-compiled",
@@ -66,4 +67,4 @@ export const SELECTABLE_RULE_NAMES: readonly string[] = [
66
67
  // implements REQ-006
67
68
  /** Comma-separated "Allowed:" fragment for the kb_check rules description. */
68
69
  export const SELECTABLE_RULES_ALLOWED =
69
- "must-priority-coverage, symbol-coverage, symbol-traceability, no-dangling-refs, source-relationship-parity, branch-store-not-compiled, source-path-dangling, no-cycles, required-fields, deprecated-adr-no-successor, superseded-requirement-open, scenario-feasibility, scenario-feasibility-unknown, exception-claim-keys, numeric-string-value, rule-key-arguments-missing, domain-contradictions, logic-coverage, rule-safety, rule-verifiability, query-plan-safety, req-status-vocabulary, strict-fact-shape, strict-req-fact-pairing, predicate-verifiability, strict-readiness, semantic-completeness, related-requirement-unmodeled, proof-contract-symbols, entity-id-style, domain-redundancy, domain-implication, subject-key-identity, subject-key-shape, ontology-quality, exception-unapproved, exception-approval-self-attested, agent-requirement-unapproved, requirement-rationale-missing, symbol-owner-superseded, adr-unlinked, adr-proposed, predicate-schema-conformance, entity-context-missing, entity-context-acknowledged, decision-firm-unreferenced, decision-firm-superseded, supersedes-reason-missing, supersedes-reason-acknowledged, policy-ownership, policy-markers";
70
+ "must-priority-coverage, symbol-coverage, symbol-traceability, symbol-granularity, no-dangling-refs, source-relationship-parity, branch-store-not-compiled, source-path-dangling, no-cycles, required-fields, deprecated-adr-no-successor, superseded-requirement-open, scenario-feasibility, scenario-feasibility-unknown, exception-claim-keys, numeric-string-value, rule-key-arguments-missing, domain-contradictions, logic-coverage, rule-safety, rule-verifiability, query-plan-safety, req-status-vocabulary, strict-fact-shape, strict-req-fact-pairing, predicate-verifiability, strict-readiness, semantic-completeness, related-requirement-unmodeled, proof-contract-symbols, entity-id-style, domain-redundancy, domain-implication, subject-key-identity, subject-key-shape, ontology-quality, exception-unapproved, exception-approval-self-attested, agent-requirement-unapproved, requirement-rationale-missing, symbol-owner-superseded, adr-unlinked, adr-proposed, predicate-schema-conformance, entity-context-missing, entity-context-acknowledged, decision-firm-unreferenced, decision-firm-superseded, supersedes-reason-missing, supersedes-reason-acknowledged, policy-ownership, policy-markers";
@@ -236,13 +236,13 @@ export const applyPlanSpec = {
236
236
  type: "object",
237
237
  required: ["version", "planHash"],
238
238
  description:
239
- "The complete kibi.bootstrap-plan.v1, kibi.compile-plan.v1, kibi.migration-plan.v2, or hash-bound kibi.entity-deletion-plan.v1 object. Migration plans require approvedActionIds and reject blocked/non-automatic actions.",
239
+ "The complete kibi.bootstrap-plan.v1, kibi.compile-plan.v1, kibi.migration-plan.v2, or hash-bound kibi.entity-deletion-plan.v1 object. Migration plans require approvedActionIds and reject blocked actions and review actions Kibi cannot execute.",
240
240
  },
241
241
  approvedActionIds: {
242
242
  type: "array",
243
243
  items: { type: "string", minLength: 1 },
244
244
  description:
245
- "Required for migration-plan.v2. Exact automatic action IDs explicitly approved for this application.",
245
+ "Required for migration-plan.v2. Exact action IDs explicitly approved for this application: automatic actions, or review actions with a cli invocation that Kibi executes only when approved by id (for example semantic_inventory_rederive_drop_grounding).",
246
246
  },
247
247
  recoveryJournalId: {
248
248
  type: "string",