kibi-cli 2.21.2 → 2.22.1

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 (113) 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 +108 -6
  8. package/dist/commands/init-helpers.d.ts +16 -2
  9. package/dist/commands/init-helpers.d.ts.map +1 -1
  10. package/dist/commands/init-helpers.js +52 -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 +37 -0
  18. package/dist/commands/migrate.d.ts.map +1 -1
  19. package/dist/commands/migrate.js +72 -188
  20. package/dist/commands/sync/manifest.d.ts.map +1 -1
  21. package/dist/commands/sync/manifest.js +9 -0
  22. package/dist/extractors/symbols-coordinator.js +1 -0
  23. package/dist/intent-search.d.ts.map +1 -1
  24. package/dist/intent-search.js +28 -6
  25. package/dist/operations/migration/inventory-rederive.d.ts +40 -1
  26. package/dist/operations/migration/inventory-rederive.d.ts.map +1 -1
  27. package/dist/operations/migration/inventory-rederive.js +99 -8
  28. package/dist/operations/migration/requirement-status.d.ts +25 -0
  29. package/dist/operations/migration/requirement-status.d.ts.map +1 -0
  30. package/dist/operations/migration/requirement-status.js +105 -0
  31. package/dist/operations/migration/schema6.d.ts +25 -0
  32. package/dist/operations/migration/schema6.d.ts.map +1 -1
  33. package/dist/operations/migration/schema6.js +265 -7
  34. package/dist/operations/migration/symbol-legacy-links.d.ts +51 -0
  35. package/dist/operations/migration/symbol-legacy-links.d.ts.map +1 -0
  36. package/dist/operations/migration/symbol-legacy-links.js +319 -0
  37. package/dist/operations/mutation/context-warning.d.ts +12 -2
  38. package/dist/operations/mutation/context-warning.d.ts.map +1 -1
  39. package/dist/operations/mutation/context-warning.js +6 -6
  40. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  41. package/dist/operations/mutation/source-authoring.js +130 -35
  42. package/dist/operations/mutation/types.d.ts +2 -1
  43. package/dist/operations/mutation/types.d.ts.map +1 -1
  44. package/dist/operations/mutation/upsert.d.ts +17 -2
  45. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  46. package/dist/operations/mutation/upsert.js +28 -11
  47. package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
  48. package/dist/operations/mutation/validate-upsert.js +40 -8
  49. package/dist/operations/mutation/validation.d.ts.map +1 -1
  50. package/dist/operations/mutation/validation.js +43 -1
  51. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  52. package/dist/operations/planning/apply-plan.js +34 -10
  53. package/dist/operations/semantic-advisor/ingestion-boundary.d.ts.map +1 -1
  54. package/dist/operations/semantic-advisor/ingestion-boundary.js +25 -1
  55. package/dist/operations/semantic-advisor/stored-ledger-drift.d.ts +19 -0
  56. package/dist/operations/semantic-advisor/stored-ledger-drift.d.ts.map +1 -0
  57. package/dist/operations/semantic-advisor/stored-ledger-drift.js +78 -0
  58. package/dist/public/impact/diagnostics.d.ts.map +1 -1
  59. package/dist/public/impact/diagnostics.js +24 -14
  60. package/dist/public/operations/catalog.d.ts +3 -3
  61. package/dist/public/operations/check-executor.d.ts.map +1 -1
  62. package/dist/public/operations/check-executor.js +3 -0
  63. package/dist/public/operations/contracts.d.ts.map +1 -1
  64. package/dist/public/operations/contracts.js +6 -1
  65. package/dist/public/operations/decision-firmness.d.ts +3 -1
  66. package/dist/public/operations/decision-firmness.d.ts.map +1 -1
  67. package/dist/public/operations/decision-firmness.js +13 -3
  68. package/dist/public/operations/discovery-executors.d.ts +2 -0
  69. package/dist/public/operations/discovery-executors.d.ts.map +1 -1
  70. package/dist/public/operations/discovery-executors.js +24 -2
  71. package/dist/public/operations/migration-plan.d.ts +15 -0
  72. package/dist/public/operations/migration-plan.d.ts.map +1 -1
  73. package/dist/public/operations/migration-plan.js +130 -17
  74. package/dist/public/operations/result-envelope.d.ts.map +1 -1
  75. package/dist/public/operations/result-envelope.js +27 -1
  76. package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
  77. package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
  78. package/dist/public/operations/specs/check-rules.generated.js +2 -1
  79. package/dist/public/operations/specs/check.d.ts +1 -1
  80. package/dist/public/operations/specs/planning.d.ts +2 -2
  81. package/dist/public/operations/specs/planning.js +2 -2
  82. package/dist/public/operations/specs/semantic.d.ts.map +1 -1
  83. package/dist/public/operations/specs/semantic.js +26 -1
  84. package/dist/public/operations/symbol-granularity-check.d.ts +18 -0
  85. package/dist/public/operations/symbol-granularity-check.d.ts.map +1 -0
  86. package/dist/public/operations/symbol-granularity-check.js +175 -0
  87. package/dist/public/skills/kibi-usage/SKILL.md +8 -3
  88. package/dist/public/symbol-granularity.d.ts +17 -0
  89. package/dist/public/symbol-granularity.d.ts.map +1 -1
  90. package/dist/public/symbol-granularity.js +21 -0
  91. package/dist/traceability/evidence-model.d.ts +7 -3
  92. package/dist/traceability/evidence-model.d.ts.map +1 -1
  93. package/dist/traceability/staged-symbols-manifest.js +6 -5
  94. package/dist/traceability/validate.d.ts +14 -1
  95. package/dist/traceability/validate.d.ts.map +1 -1
  96. package/dist/traceability/validate.js +41 -0
  97. package/dist/utils/rule-registry.generated.d.ts +5 -0
  98. package/dist/utils/rule-registry.generated.d.ts.map +1 -1
  99. package/dist/utils/rule-registry.generated.js +6 -0
  100. package/package.json +3 -3
  101. package/src/public/impact/diagnostics.ts +29 -17
  102. package/src/public/operations/check-executor.ts +7 -0
  103. package/src/public/operations/contracts.ts +6 -1
  104. package/src/public/operations/decision-firmness.ts +18 -2
  105. package/src/public/operations/discovery-executors.ts +28 -2
  106. package/src/public/operations/migration-plan.ts +147 -20
  107. package/src/public/operations/result-envelope.ts +34 -1
  108. package/src/public/operations/specs/check-rules.generated.ts +2 -1
  109. package/src/public/operations/specs/planning.ts +2 -2
  110. package/src/public/operations/specs/semantic.ts +37 -2
  111. package/src/public/operations/symbol-granularity-check.ts +233 -0
  112. package/src/public/skills/kibi-usage/SKILL.md +8 -3
  113. package/src/public/symbol-granularity.ts +37 -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(
@@ -63,6 +63,16 @@ export const DECISION_FIRMNESS_RULES = [
63
63
 
64
64
  const DECISION_LANES = ["requirements", "adr"] as const;
65
65
 
66
+ /** The schema version from which a supersede must explain itself. */
67
+ const SUPERSEDES_REASON_SCHEMA_VERSION = 9;
68
+
69
+ /**
70
+ * Told on a KB older than schema 9: the pending schema upgrade acknowledges
71
+ * supersedes recorded before the rule existed, so no edit is required now.
72
+ */
73
+ const PRE_SCHEMA9_SUPERSEDES_NOTE =
74
+ " Supersedes recorded before schema 9 are acknowledged by the plan's schema-config-upgrade (kibi migrate --yes); add the reason only when you next touch the requirement.";
75
+
66
76
  /** Every authored requirement and ADR. */
67
77
  // implements REQ-kb-decision-firmness
68
78
  export function listDecisionEntities(workspaceRoot: string): AuthoredEntity[] {
@@ -164,7 +174,11 @@ export function evaluateDecisionFirmness(
164
174
  edges: ReadonlyMap<string, Set<string>>,
165
175
  rules: ReadonlySet<string> = new Set(DECISION_FIRMNESS_RULES),
166
176
  acknowledged: ReadonlySet<string> = new Set(),
177
+ options: Readonly<{ schemaVersion?: number | null }> = {},
167
178
  ): Violation[] {
179
+ const preSchema9 =
180
+ typeof options.schemaVersion === "number" &&
181
+ options.schemaVersion < SUPERSEDES_REASON_SCHEMA_VERSION;
168
182
  const findings: Violation[] = [];
169
183
  const superseded = supersededIds(edges);
170
184
  const byId = new Map(entities.map((entity) => [entity.id, entity]));
@@ -241,7 +255,7 @@ export function evaluateDecisionFirmness(
241
255
  rule: SUPERSEDES_REASON_MISSING_RULE,
242
256
  entityId: entity.id,
243
257
  description: `${entity.id} supersedes ${unacknowledged.join(", ")} without a '## ${WHY_THIS_CHANGED_HEADING}' section of at least ${MIN_WHY_CHANGED_WORDS} words`,
244
- suggestion: `Add '## ${WHY_THIS_CHANGED_HEADING}' to the body: name the evidence (a failing test, a measured cost, a constraint the old decision missed, a new requirement) and answer the superseded decision's reasoning. A preference is not evidence; propose it instead of superseding.`,
258
+ suggestion: `Add '## ${WHY_THIS_CHANGED_HEADING}' to the body: name the evidence (a failing test, a measured cost, a constraint the old decision missed, a new requirement) and answer the superseded decision's reasoning. A preference is not evidence; propose it instead of superseding.${preSchema9 ? PRE_SCHEMA9_SUPERSEDES_NOTE : ""}`,
245
259
  source: entity.path,
246
260
  evidence: { type: entity.type, supersedes: unacknowledged },
247
261
  });
@@ -271,10 +285,12 @@ export function collectDecisionFirmnessViolations(
271
285
  ): Violation[] {
272
286
  if (!DECISION_FIRMNESS_RULES.some((rule) => rules.has(rule))) return [];
273
287
  const entities = listDecisionEntities(workspaceRoot);
288
+ const manifest = readKbManifest(workspaceRoot);
274
289
  return evaluateDecisionFirmness(
275
290
  entities,
276
291
  authoredSupersedes(workspaceRoot, entities),
277
292
  rules,
278
- new Set(readKbManifest(workspaceRoot)?.supersedesReasonAcknowledged ?? []),
293
+ new Set(manifest?.supersedesReasonAcknowledged ?? []),
294
+ { schemaVersion: manifest?.schemaVersion ?? null },
279
295
  );
280
296
  }
@@ -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);
@@ -1111,6 +1121,13 @@ export async function executeStatus(
1111
1121
  : { proofSnapshotError: snapshotEvidence.error }),
1112
1122
  };
1113
1123
  const schemaStatus = readMigrationConfigStatus(context.workspaceRoot);
1124
+ // implements REQ-agent-guided-migration-orchestration
1125
+ // A schema upgrade plans the source rewrites kibi migrate performs with
1126
+ // it; the planner loads only when an upgrade is due.
1127
+ const sourcePlanner = schemaStatus.needsMigration
1128
+ ? (await import("../../operations/migration/schema6.js"))
1129
+ .buildSchema6MigrationFragment
1130
+ : undefined;
1114
1131
  const migrationPlan = buildActionsFromStatus({
1115
1132
  workspaceRoot: context.workspaceRoot,
1116
1133
  branchAttachment: attachment,
@@ -1125,6 +1142,15 @@ export async function executeStatus(
1125
1142
  ? snapshotEvidence.snapshot.hash
1126
1143
  : null,
1127
1144
  configStatus: schemaStatus,
1145
+ ...(sourcePlanner !== undefined
1146
+ ? {
1147
+ planSchemaUpgradeSources: (currentSchemaVersion: number | null) =>
1148
+ sourcePlanner({
1149
+ workspaceRoot: context.workspaceRoot,
1150
+ currentSchemaVersion,
1151
+ }),
1152
+ }
1153
+ : {}),
1128
1154
  });
1129
1155
  const listed = context.fs?.glob
1130
1156
  ? await context.fs.glob(
@@ -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 : "";
@@ -553,6 +616,14 @@ export function buildActionsFromCoverage(input: {
553
616
  return actions;
554
617
  }
555
618
 
619
+ /** Source-rewrite actions a schema upgrade performs, as its planner returns them. */
620
+ export type SchemaUpgradeSourceActions = Readonly<{
621
+ actions: readonly MigrationAction[];
622
+ /** Automatic actions that rewrite authored sources; the upgrade waits on them. */
623
+ sourceRewriteActionIds: readonly string[];
624
+ diagnostics: readonly string[];
625
+ }>;
626
+
556
627
  export type MigrationConfigStatus = Readonly<{
557
628
  status: "missing" | "invalid" | "older" | "current" | "newer";
558
629
  currentVersion: number | null;
@@ -621,6 +692,18 @@ function readLegacySchemaVersion(
621
692
  /** Migration action code of the automatic recompile of changed sources. */
622
693
  export const FRESHNESS_RESYNC = "freshness_resync";
623
694
 
695
+ // implements REQ-cli-status-pre-first-sync
696
+ /**
697
+ * Stale reasons one `kibi sync` clears: sources whose content changed since
698
+ * the last compilation, and a store compiled under another compiler contract
699
+ * (the sync discards that cache, re-imports every source and stamps the
700
+ * current contract).
701
+ */
702
+ const RESYNC_REASON_CODES: ReadonlySet<string> = new Set([
703
+ ...MTIME_SOURCE_REASON_CODES,
704
+ "compiler_changed",
705
+ ]);
706
+
624
707
  export function buildActionsFromStatus(input: {
625
708
  workspaceRoot: string;
626
709
  branchAttachment?: Readonly<Record<string, unknown>>;
@@ -631,8 +714,17 @@ export function buildActionsFromStatus(input: {
631
714
  kbSnapshotId?: string | null;
632
715
  workspaceSnapshot?: string | null;
633
716
  configStatus?: MigrationConfigStatus;
717
+ /**
718
+ * Plans the authored-source rewrites `kibi migrate --yes` performs on its
719
+ * way to the latest schema (the planner `kibi migrate` itself uses), so
720
+ * the schema upgrade declares every file it touches. Read-only.
721
+ */
722
+ planSchemaUpgradeSources?: (
723
+ currentSchemaVersion: number | null,
724
+ ) => SchemaUpgradeSourceActions;
634
725
  }): MigrationPlan {
635
726
  const actions: MigrationAction[] = [];
727
+ const diagnostics: string[] = [];
636
728
  const attachment = input.branchAttachment;
637
729
  const store = input.branchStore;
638
730
  const configStatus =
@@ -686,17 +778,20 @@ export function buildActionsFromStatus(input: {
686
778
  }
687
779
 
688
780
  const storeState = typeof store?.state === "string" ? store.state : null;
689
- const legacyStorageMigrationRequired = legacyConfigExists(
690
- input.workspaceRoot,
691
- );
781
+ // Legacy storage is evaluated whether or not the legacy config survives:
782
+ // legacy copies left beside a migrated .kb/ (moved, identical or stale)
783
+ // are the same finding 'kibi doctor' and 'kibi migrate' report.
784
+ // implements REQ-cli-schema-migration
785
+ const legacyStorage = planLegacyStorageMigration(input.workspaceRoot);
786
+ const legacyStorageMigrationRequired =
787
+ legacyConfigExists(input.workspaceRoot) ||
788
+ legacyStorage.moves.length > 0 ||
789
+ legacyStorage.removals.length > 0 ||
790
+ legacyStorage.blockers.length > 0;
692
791
  // A legacy cutover that cannot move its files (a destination already
693
792
  // exists, a malformed config) is an operator decision, not an automatic
694
793
  // 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;
794
+ const legacyStorageBlocked = legacyStorage.blockers.length > 0;
700
795
  if (storeState === "missing") {
701
796
  actions.push(
702
797
  migrationAction({
@@ -735,6 +830,21 @@ export function buildActionsFromStatus(input: {
735
830
  if (configStatus.needsMigration || legacyStorageMigrationRequired) {
736
831
  const operatorOnly =
737
832
  configStatus.status === "newer" || legacyStorageBlocked;
833
+ // implements REQ-agent-guided-migration-orchestration, REQ-cli-schema-migration
834
+ // The source rewrites the migrate run performs (legacy-link marks, origin
835
+ // backfill, lifecycle repairs, ...) are planned as their own actions with
836
+ // their own files, and the upgrade waits on them, so approving it means
837
+ // approving them. Knowledge still outside .kb/ is planned after it moves.
838
+ const sources =
839
+ !operatorOnly &&
840
+ !legacyStorageMigrationRequired &&
841
+ input.planSchemaUpgradeSources !== undefined
842
+ ? input.planSchemaUpgradeSources(configStatus.currentVersion)
843
+ : null;
844
+ if (sources !== null) {
845
+ actions.push(...sources.actions);
846
+ diagnostics.push(...sources.diagnostics);
847
+ }
738
848
  actions.push(
739
849
  migrationAction({
740
850
  id: "schema-config-upgrade",
@@ -750,28 +860,44 @@ export function buildActionsFromStatus(input: {
750
860
  invocation: legacyStorageBlocked
751
861
  ? {
752
862
  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.`,
863
+ instruction: `Legacy knowledge files outside .kb/ cannot be migrated automatically: ${legacyStorage.blockers.join(" ")} Then run 'kibi migrate --yes'.`,
754
864
  }
755
865
  : { kind: "cli", command_argv: ["kibi", "migrate", "--yes"] },
756
866
  affectedFiles: [
757
867
  ".kb/manifest.json",
758
868
  ".kb/migrations",
759
869
  ".kb/config.json",
760
- ...(legacyStorage?.moves.map((move) => move.from) ?? []),
870
+ // The migrate run keeps the canonical derived entries ignored.
871
+ ".gitignore",
872
+ // Unplanned source rewrites can touch any authored .kb/ file.
873
+ ...(sources === null ? [".kb/"] : []),
874
+ ...legacyStorage.moves.map((move) => move.from),
875
+ ...legacyStorage.removals.map((removal) => removal.path),
761
876
  ],
762
877
  evidence: {
763
878
  configStatus,
764
- ...(legacyStorage === null
765
- ? {}
766
- : {
879
+ ...(sources === null
880
+ ? {
881
+ sourceRewrites:
882
+ "Not planned as separate actions: kibi migrate --yes also marks coarse symbol links legacy-link, backfills origin and repairs lifecycle findings in authored .kb/ sources. The plan lists them as their own actions once the upgrade can run automatically on the canonical .kb/ layout.",
883
+ }
884
+ : {}),
885
+ ...(legacyStorageMigrationRequired
886
+ ? {
767
887
  legacyStorage: {
768
888
  moves: legacyStorage.moves,
889
+ removals: legacyStorage.removals,
890
+ conflicts: legacyStorage.conflicts,
769
891
  blockers: legacyStorage.blockers,
770
892
  legacyConfig: legacyStorage.legacyConfig,
771
893
  },
772
- }),
894
+ }
895
+ : {}),
773
896
  },
774
- dependsOn: storeState === "missing" ? ["branch-store-ensure"] : [],
897
+ dependsOn: [
898
+ ...(storeState === "missing" ? ["branch-store-ensure"] : []),
899
+ ...(sources?.sourceRewriteActionIds ?? []),
900
+ ],
775
901
  autoApplicable: !operatorOnly,
776
902
  }),
777
903
  );
@@ -816,12 +942,12 @@ export function buildActionsFromStatus(input: {
816
942
  // implements REQ-cli-status-pre-first-sync
817
943
  // Sources whose content changed since the last compilation (kb_status has
818
944
  // 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.
945
+ // automatic `kibi sync`, not a review per file. A store compiled under
946
+ // another compiler contract is fixed by the same sync. An uncompiled store
947
+ // is compiled by the step above, which also covers these reasons.
821
948
  const resyncReasons = (input.staleReasons ?? []).filter(
822
949
  (reason) =>
823
- typeof reason.code === "string" &&
824
- MTIME_SOURCE_REASON_CODES.has(reason.code),
950
+ typeof reason.code === "string" && RESYNC_REASON_CODES.has(reason.code),
825
951
  );
826
952
  if (resyncReasons.length > 0 && uncompiled === undefined) {
827
953
  actions.push(
@@ -856,7 +982,7 @@ export function buildActionsFromStatus(input: {
856
982
  // Planned above as an automatic compile step, not a review.
857
983
  if (code === BRANCH_STORE_NOT_COMPILED) continue;
858
984
  // Planned above as one automatic resync (or covered by the compile).
859
- if (MTIME_SOURCE_REASON_CODES.has(code)) continue;
985
+ if (RESYNC_REASON_CODES.has(code)) continue;
860
986
  const file = typeof reason.path === "string" ? reason.path : "";
861
987
  actions.push(
862
988
  migrationAction({
@@ -940,5 +1066,6 @@ export function buildActionsFromStatus(input: {
940
1066
  ],
941
1067
  incompleteDomains,
942
1068
  actions,
1069
+ diagnostics,
943
1070
  });
944
1071
  }
@@ -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",