kibi-cli 0.12.8 → 0.14.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 (111) hide show
  1. package/dist/cli.js +1 -0
  2. package/dist/commands/check.d.ts +1 -0
  3. package/dist/commands/check.d.ts.map +1 -1
  4. package/dist/commands/check.js +117 -82
  5. package/dist/commands/discovery-shared.d.ts +1 -0
  6. package/dist/commands/discovery-shared.d.ts.map +1 -1
  7. package/dist/commands/discovery-shared.js +2 -191
  8. package/dist/commands/discovery-table.d.ts +3 -0
  9. package/dist/commands/discovery-table.d.ts.map +1 -0
  10. package/dist/commands/discovery-table.js +204 -0
  11. package/dist/commands/sync/discovery.d.ts.map +1 -1
  12. package/dist/commands/sync/discovery.js +8 -1
  13. package/dist/commands/sync/extraction.d.ts +6 -1
  14. package/dist/commands/sync/extraction.d.ts.map +1 -1
  15. package/dist/commands/sync/extraction.js +7 -3
  16. package/dist/commands/sync/persistence.d.ts.map +1 -1
  17. package/dist/commands/sync/persistence.js +29 -1
  18. package/dist/extractors/manifest.d.ts.map +1 -1
  19. package/dist/extractors/manifest.js +18 -0
  20. package/dist/extractors/markdown.d.ts +6 -0
  21. package/dist/extractors/markdown.d.ts.map +1 -1
  22. package/dist/extractors/symbols-coordinator.d.ts +1 -1
  23. package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
  24. package/dist/extractors/symbols-ts.js +62 -0
  25. package/dist/public/impact/analyzer.d.ts +3 -0
  26. package/dist/public/impact/analyzer.d.ts.map +1 -0
  27. package/dist/public/impact/analyzer.js +54 -0
  28. package/dist/public/impact/coverage-depth-quality.d.ts +4 -0
  29. package/dist/public/impact/coverage-depth-quality.d.ts.map +1 -0
  30. package/dist/public/impact/coverage-depth-quality.js +179 -0
  31. package/dist/public/impact/diagnostics.d.ts +7 -0
  32. package/dist/public/impact/diagnostics.d.ts.map +1 -0
  33. package/dist/public/impact/diagnostics.js +122 -0
  34. package/dist/public/impact/diff-meaning.d.ts +2 -0
  35. package/dist/public/impact/diff-meaning.d.ts.map +1 -0
  36. package/dist/public/impact/diff-meaning.js +101 -0
  37. package/dist/public/impact/full-kb-quality.d.ts +10 -0
  38. package/dist/public/impact/full-kb-quality.d.ts.map +1 -0
  39. package/dist/public/impact/full-kb-quality.js +158 -0
  40. package/dist/public/impact/manifest.d.ts +5 -0
  41. package/dist/public/impact/manifest.d.ts.map +1 -0
  42. package/dist/public/impact/manifest.js +43 -0
  43. package/dist/public/impact/requirement-quality.d.ts +4 -0
  44. package/dist/public/impact/requirement-quality.d.ts.map +1 -0
  45. package/dist/public/impact/requirement-quality.js +216 -0
  46. package/dist/public/impact/source-changes.d.ts +5 -0
  47. package/dist/public/impact/source-changes.d.ts.map +1 -0
  48. package/dist/public/impact/source-changes.js +129 -0
  49. package/dist/public/impact/summaries.d.ts +6 -0
  50. package/dist/public/impact/summaries.d.ts.map +1 -0
  51. package/dist/public/impact/summaries.js +68 -0
  52. package/dist/public/impact/symbol-quality-model.d.ts +21 -0
  53. package/dist/public/impact/symbol-quality-model.d.ts.map +1 -0
  54. package/dist/public/impact/symbol-quality-model.js +116 -0
  55. package/dist/public/impact/symbol-quality.d.ts +4 -0
  56. package/dist/public/impact/symbol-quality.d.ts.map +1 -0
  57. package/dist/public/impact/symbol-quality.js +159 -0
  58. package/dist/public/impact/types.d.ts +79 -0
  59. package/dist/public/impact/types.d.ts.map +1 -0
  60. package/dist/public/impact/types.js +1 -0
  61. package/dist/public/impact-diagnostics.d.ts +7 -0
  62. package/dist/public/impact-diagnostics.d.ts.map +1 -0
  63. package/dist/public/impact-diagnostics.js +5 -0
  64. package/dist/public/schemas/entity.d.ts.map +1 -1
  65. package/dist/public/schemas/entity.js +21 -0
  66. package/dist/public/skills/kibi-usage/SKILL.md +13 -5
  67. package/dist/public/skills/kibi-usage/resources/fact-lanes.md +5 -5
  68. package/dist/public/skills/kibi-usage/resources/relationship-directions.md +30 -0
  69. package/dist/public/skills/kibi-usage/resources/workflows.md +14 -0
  70. package/dist/public/symbol-granularity.d.ts +1 -1
  71. package/dist/public/symbol-granularity.d.ts.map +1 -1
  72. package/dist/public/symbol-granularity.js +2 -0
  73. package/dist/traceability/evidence-model.d.ts +1 -1
  74. package/dist/traceability/evidence-model.d.ts.map +1 -1
  75. package/dist/traceability/evidence-model.js +1 -1
  76. package/dist/traceability/staged-diagnostics.d.ts +6 -6
  77. package/dist/traceability/staged-diagnostics.d.ts.map +1 -1
  78. package/dist/traceability/staged-diagnostics.js +12 -6
  79. package/dist/traceability/staged-impact-contract.js +1 -1
  80. package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
  81. package/dist/traceability/staged-symbols-manifest.js +23 -1
  82. package/dist/traceability/symbol-extract.d.ts.map +1 -1
  83. package/dist/traceability/symbol-extract.js +41 -0
  84. package/dist/types/entities.d.ts +5 -1
  85. package/dist/types/entities.d.ts.map +1 -1
  86. package/dist/utils/prolog-query-plan-safety.d.ts +8 -0
  87. package/dist/utils/prolog-query-plan-safety.d.ts.map +1 -0
  88. package/dist/utils/prolog-query-plan-safety.js +47 -0
  89. package/dist/utils/rule-registry.d.ts.map +1 -1
  90. package/dist/utils/rule-registry.js +12 -0
  91. package/package.json +6 -2
  92. package/schema/entities.pl +4 -0
  93. package/src/public/impact/analyzer.ts +90 -0
  94. package/src/public/impact/coverage-depth-quality.ts +268 -0
  95. package/src/public/impact/diagnostics.ts +192 -0
  96. package/src/public/impact/diff-meaning.ts +121 -0
  97. package/src/public/impact/full-kb-quality.ts +203 -0
  98. package/src/public/impact/manifest.ts +57 -0
  99. package/src/public/impact/requirement-quality.ts +282 -0
  100. package/src/public/impact/source-changes.ts +180 -0
  101. package/src/public/impact/summaries.ts +100 -0
  102. package/src/public/impact/symbol-quality-model.ts +171 -0
  103. package/src/public/impact/symbol-quality.ts +207 -0
  104. package/src/public/impact/types.ts +99 -0
  105. package/src/public/impact-diagnostics.ts +22 -0
  106. package/src/public/schemas/entity.ts +23 -0
  107. package/src/public/skills/kibi-usage/SKILL.md +13 -5
  108. package/src/public/skills/kibi-usage/resources/fact-lanes.md +5 -5
  109. package/src/public/skills/kibi-usage/resources/relationship-directions.md +30 -0
  110. package/src/public/skills/kibi-usage/resources/workflows.md +14 -0
  111. package/src/public/symbol-granularity.ts +4 -0
@@ -0,0 +1,171 @@
1
+ import type { ExtractionResult } from "../../extractors/markdown.js";
2
+ import type { ExtractedSymbol } from "../../traceability/symbol-extract.js";
3
+ import {
4
+ type SymbolKind,
5
+ type SymbolRole,
6
+ getSymbolRole,
7
+ } from "../symbol-granularity.js";
8
+ import type { SymbolQualityDiagnosticsOptions } from "./types.js";
9
+
10
+ export type SymbolMetadata = {
11
+ readonly result: ExtractionResult;
12
+ readonly id: string;
13
+ readonly title: string;
14
+ readonly sourceFile: string;
15
+ readonly source: string;
16
+ readonly kind: SymbolKind | "component" | "service" | "module" | "config";
17
+ readonly role: SymbolRole;
18
+ readonly requirementTargetIds: readonly string[];
19
+ readonly coordinateKey: string | null;
20
+ };
21
+
22
+ export const AGGREGATE_KINDS: ReadonlySet<string> = new Set([
23
+ "class",
24
+ "component",
25
+ "service",
26
+ "module",
27
+ ]);
28
+
29
+ function isActiveSymbol(result: ExtractionResult): boolean {
30
+ return result.entity.type === "symbol" && result.entity.status === "active";
31
+ }
32
+
33
+ function getRequirementTargets(result: ExtractionResult): readonly string[] {
34
+ return [
35
+ ...new Set(
36
+ result.relationships
37
+ .filter(
38
+ (relationship) =>
39
+ relationship.type === "implements" &&
40
+ relationship.to.startsWith("REQ-"),
41
+ )
42
+ .map((relationship) => relationship.to),
43
+ ),
44
+ ].sort();
45
+ }
46
+
47
+ function inferKind(result: ExtractionResult): SymbolMetadata["kind"] {
48
+ const explicitKind = result.entity.symbol_kind;
49
+ if (
50
+ explicitKind === "component" ||
51
+ explicitKind === "service" ||
52
+ explicitKind === "module" ||
53
+ explicitKind === "config"
54
+ ) {
55
+ return explicitKind;
56
+ }
57
+ if (
58
+ explicitKind === "function" ||
59
+ explicitKind === "class" ||
60
+ explicitKind === "method" ||
61
+ explicitKind === "property" ||
62
+ explicitKind === "accessor" ||
63
+ explicitKind === "interface" ||
64
+ explicitKind === "type" ||
65
+ explicitKind === "variable" ||
66
+ explicitKind === "enum" ||
67
+ explicitKind === "unknown"
68
+ ) {
69
+ return explicitKind;
70
+ }
71
+
72
+ if (result.entity.title.endsWith("Component")) return "component";
73
+ if (result.entity.title.endsWith("Service")) return "service";
74
+ if (result.entity.title.endsWith("Module")) return "module";
75
+ if (result.entity.title.endsWith("Config")) return "config";
76
+ if (result.entity.title.includes(".")) return "method";
77
+ return "unknown";
78
+ }
79
+
80
+ function inferRole(
81
+ result: ExtractionResult,
82
+ kind: SymbolMetadata["kind"],
83
+ ): SymbolRole {
84
+ const explicitRole = result.entity.symbol_role;
85
+ if (
86
+ explicitRole === "behavioral" ||
87
+ explicitRole === "structural" ||
88
+ explicitRole === "type-shape" ||
89
+ explicitRole === "config" ||
90
+ explicitRole === "module" ||
91
+ explicitRole === "unknown"
92
+ ) {
93
+ return explicitRole;
94
+ }
95
+ if (kind === "component" || kind === "service") return "behavioral";
96
+ if (kind === "module") return "module";
97
+ if (kind === "config") return "config";
98
+ return getSymbolRole({ name: result.entity.title, kind });
99
+ }
100
+
101
+ function getCoordinateKey(result: ExtractionResult): string | null {
102
+ const { sourceLine, sourceColumn, sourceEndLine, sourceEndColumn } =
103
+ result.entity;
104
+ if (
105
+ sourceLine === undefined ||
106
+ sourceColumn === undefined ||
107
+ sourceEndLine === undefined ||
108
+ sourceEndColumn === undefined
109
+ ) {
110
+ return null;
111
+ }
112
+
113
+ return `${sourceLine}:${sourceColumn}:${sourceEndLine}:${sourceEndColumn}`;
114
+ }
115
+
116
+ export function collectSymbols(
117
+ options: SymbolQualityDiagnosticsOptions,
118
+ ): readonly SymbolMetadata[] {
119
+ return options.manifestResults
120
+ .filter((result) =>
121
+ options.activeEntityIds === undefined
122
+ ? isActiveSymbol(result)
123
+ : isActiveSymbol(result) &&
124
+ options.activeEntityIds.has(result.entity.id),
125
+ )
126
+ .flatMap((result) => {
127
+ if (result.sourceFile === undefined) return [];
128
+ const kind = inferKind(result);
129
+ return [
130
+ {
131
+ result,
132
+ id: result.entity.id,
133
+ title: result.entity.title,
134
+ sourceFile: result.sourceFile,
135
+ source: result.entity.source,
136
+ kind,
137
+ role: inferRole(result, kind),
138
+ requirementTargetIds: getRequirementTargets(result),
139
+ coordinateKey: getCoordinateKey(result),
140
+ },
141
+ ];
142
+ });
143
+ }
144
+
145
+ export function isModuleOrConfig(symbol: SymbolMetadata): boolean {
146
+ return symbol.role === "module" || symbol.role === "config";
147
+ }
148
+
149
+ export function narrowerManifestSymbols(
150
+ symbol: SymbolMetadata,
151
+ symbols: readonly SymbolMetadata[],
152
+ ): readonly SymbolMetadata[] {
153
+ return symbols.filter(
154
+ (candidate) =>
155
+ candidate.id !== symbol.id &&
156
+ candidate.sourceFile === symbol.sourceFile &&
157
+ candidate.title.startsWith(`${symbol.title}.`),
158
+ );
159
+ }
160
+
161
+ export function narrowerExtractedSymbols(
162
+ symbol: SymbolMetadata,
163
+ symbolsByFile: ReadonlyMap<string, readonly ExtractedSymbol[]>,
164
+ ): readonly ExtractedSymbol[] {
165
+ return (symbolsByFile.get(symbol.sourceFile) ?? []).filter(
166
+ (candidate) =>
167
+ candidate.name !== symbol.title &&
168
+ candidate.name.startsWith(`${symbol.title}.`) &&
169
+ candidate.role === "behavioral",
170
+ );
171
+ }
@@ -0,0 +1,207 @@
1
+ import type { ExtractionResult } from "../../extractors/markdown.js";
2
+ import type { KibiImpactDiagnostic } from "../../traceability/staged-diagnostics.js";
3
+ import { isAllowedGranularityReason } from "../symbol-granularity.js";
4
+ import {
5
+ AGGREGATE_KINDS,
6
+ type SymbolMetadata,
7
+ collectSymbols,
8
+ isModuleOrConfig,
9
+ narrowerExtractedSymbols,
10
+ narrowerManifestSymbols,
11
+ } from "./symbol-quality-model.js";
12
+ import type { SymbolQualityDiagnosticsOptions } from "./types.js";
13
+
14
+ function thresholdFor(symbol: SymbolMetadata): number {
15
+ return AGGREGATE_KINDS.has(symbol.kind) ? 4 : 2;
16
+ }
17
+
18
+ function isMultiRequirementCandidate(symbol: SymbolMetadata): boolean {
19
+ if (isModuleOrConfig(symbol)) {
20
+ return !isAllowedGranularityReason(symbol.result.entity.granularity_reason);
21
+ }
22
+ return symbol.role === "behavioral";
23
+ }
24
+
25
+ function createMultiRequirementDiagnostics(
26
+ symbols: readonly SymbolMetadata[],
27
+ ): readonly KibiImpactDiagnostic[] {
28
+ return symbols.flatMap((symbol) => {
29
+ if (!isMultiRequirementCandidate(symbol)) return [];
30
+ const threshold = thresholdFor(symbol);
31
+ if (symbol.requirementTargetIds.length <= threshold) return [];
32
+
33
+ return [
34
+ {
35
+ id: "multi_requirement_symbol_review",
36
+ severity: "review",
37
+ blocking: false,
38
+ category: "symbol",
39
+ entityId: symbol.id,
40
+ source: symbol.source,
41
+ files: [symbol.source, symbol.sourceFile],
42
+ docs: ["docs/symbol-traceability-taxonomy.md"],
43
+ message: `Symbol ${symbol.id} implements ${symbol.requirementTargetIds.length} requirements, above the ${threshold} target review threshold.`,
44
+ suggestion:
45
+ "Review whether this symbol owns multiple unrelated behaviors; move relationships to narrower behavioral symbols or add a valid granularity_reason when broad ownership is intentional.",
46
+ evidence: {
47
+ targetIds: symbol.requirementTargetIds,
48
+ targetCount: symbol.requirementTargetIds.length,
49
+ threshold,
50
+ symbolKind: symbol.kind,
51
+ symbolRole: symbol.role,
52
+ sourceFile: symbol.sourceFile,
53
+ },
54
+ },
55
+ ];
56
+ });
57
+ }
58
+
59
+ function hasDistinctCoordinates(symbols: readonly SymbolMetadata[]): boolean {
60
+ const coordinateKeys = symbols.map((symbol) => symbol.coordinateKey);
61
+ if (coordinateKeys.some((key) => key === null)) return false;
62
+ return new Set(coordinateKeys).size === coordinateKeys.length;
63
+ }
64
+
65
+ function createDuplicateCoordinateDiagnostics(
66
+ symbols: readonly SymbolMetadata[],
67
+ ): readonly KibiImpactDiagnostic[] {
68
+ const groups = Map.groupBy(
69
+ symbols,
70
+ (symbol) => `${symbol.sourceFile}\u0000${symbol.title}`,
71
+ );
72
+
73
+ return [...groups.values()].flatMap((group) => {
74
+ if (group.length < 2) return [];
75
+ if (hasDistinctCoordinates(group)) return [];
76
+ const first = group[0];
77
+ if (first === undefined) return [];
78
+ const candidateIds = group.map((symbol) => symbol.id).sort();
79
+
80
+ return [
81
+ {
82
+ id: "duplicate_symbol_coordinate_review",
83
+ severity: "review",
84
+ blocking: false,
85
+ category: "coordinate",
86
+ entityId: first.id,
87
+ source: first.source,
88
+ files: [first.source, first.sourceFile],
89
+ docs: ["docs/symbol-traceability-taxonomy.md"],
90
+ message: `Symbols ${candidateIds.join(", ")} share title ${first.title} and source ${first.sourceFile} without distinct coordinate evidence.`,
91
+ suggestion:
92
+ "Refresh symbol coordinates or rename member-level manifest entries so duplicate modeled symbols are distinguishable after parsing.",
93
+ evidence: {
94
+ candidateIds,
95
+ title: first.title,
96
+ sourceFile: first.sourceFile,
97
+ },
98
+ },
99
+ ];
100
+ });
101
+ }
102
+
103
+ function requirementTagsById(
104
+ results: readonly ExtractionResult[],
105
+ ): ReadonlyMap<string, readonly string[]> {
106
+ const tagsById = new Map<string, readonly string[]>();
107
+ for (const result of results) {
108
+ if (result.entity.type !== "req") continue;
109
+ tagsById.set(result.entity.id, [...(result.entity.tags ?? [])].sort());
110
+ }
111
+ return tagsById;
112
+ }
113
+
114
+ function tagsForTargets(
115
+ targetIds: readonly string[],
116
+ tagsByRequirementId: ReadonlyMap<string, readonly string[]>,
117
+ ): readonly string[] {
118
+ return [
119
+ ...new Set(
120
+ targetIds.flatMap((targetId) => tagsByRequirementId.get(targetId) ?? []),
121
+ ),
122
+ ].sort();
123
+ }
124
+
125
+ function hasSharedRequirementTag(
126
+ targetIds: readonly string[],
127
+ tagsByRequirementId: ReadonlyMap<string, readonly string[]>,
128
+ ): boolean {
129
+ const tagSets = targetIds
130
+ .map((targetId) => new Set(tagsByRequirementId.get(targetId) ?? []))
131
+ .filter((tagSet) => tagSet.size > 0);
132
+ const first = tagSets[0];
133
+ if (first === undefined || tagSets.length !== targetIds.length) return false;
134
+ return [...first].some((tag) => tagSets.every((tagSet) => tagSet.has(tag)));
135
+ }
136
+
137
+ function isMixedPurposeCandidate(symbol: SymbolMetadata): boolean {
138
+ return AGGREGATE_KINDS.has(symbol.kind) || isModuleOrConfig(symbol);
139
+ }
140
+
141
+ function createMixedPurposeDiagnostics(
142
+ symbols: readonly SymbolMetadata[],
143
+ options: SymbolQualityDiagnosticsOptions,
144
+ ): readonly KibiImpactDiagnostic[] {
145
+ const tagsByRequirementId = requirementTagsById(options.manifestResults);
146
+ return symbols.flatMap((symbol) => {
147
+ if (!isMixedPurposeCandidate(symbol)) return [];
148
+ const manifestChildren = narrowerManifestSymbols(symbol, symbols);
149
+ const extractedChildren = narrowerExtractedSymbols(
150
+ symbol,
151
+ options.symbolsByFile,
152
+ );
153
+ if (manifestChildren.length === 0 && extractedChildren.length === 0)
154
+ return [];
155
+ const requirementTags = tagsForTargets(
156
+ symbol.requirementTargetIds,
157
+ tagsByRequirementId,
158
+ );
159
+ if (requirementTags.length < 3) return [];
160
+ if (
161
+ hasSharedRequirementTag(symbol.requirementTargetIds, tagsByRequirementId)
162
+ ) {
163
+ return [];
164
+ }
165
+ const narrowerSymbolIds = [
166
+ ...new Set([
167
+ ...manifestChildren.map((child) => child.id),
168
+ ...extractedChildren.map((child) => child.id),
169
+ ]),
170
+ ].sort();
171
+
172
+ return [
173
+ {
174
+ id: "component_mixed_purpose_review",
175
+ severity: "review",
176
+ blocking: false,
177
+ category: "mixed-purpose",
178
+ entityId: symbol.id,
179
+ source: symbol.source,
180
+ files: [symbol.source, symbol.sourceFile],
181
+ docs: ["docs/symbol-traceability-taxonomy.md"],
182
+ message: `Symbol ${symbol.id} spans unrelated requirement tag clusters while narrower same-file symbols exist.`,
183
+ suggestion:
184
+ "Move unrelated requirement ownership to narrower class members/components/services or split the coarse symbol into cohesive behavioral anchors.",
185
+ evidence: {
186
+ targetIds: symbol.requirementTargetIds,
187
+ requirementTags,
188
+ narrowerSymbolIds,
189
+ sourceFile: symbol.sourceFile,
190
+ symbolKind: symbol.kind,
191
+ symbolRole: symbol.role,
192
+ },
193
+ },
194
+ ];
195
+ });
196
+ }
197
+
198
+ export function createSymbolQualityDiagnostics(
199
+ options: SymbolQualityDiagnosticsOptions,
200
+ ): KibiImpactDiagnostic[] {
201
+ const symbols = collectSymbols(options);
202
+ return [
203
+ ...createMultiRequirementDiagnostics(symbols),
204
+ ...createDuplicateCoordinateDiagnostics(symbols),
205
+ ...createMixedPurposeDiagnostics(symbols, options),
206
+ ];
207
+ }
@@ -0,0 +1,99 @@
1
+ import type { ExtractionResult } from "../../extractors/markdown.js";
2
+ import type { HunkRange, StagedFile } from "../../traceability/git-staged.js";
3
+ import type { KibiImpactDiagnostic } from "../../traceability/staged-diagnostics.js";
4
+ import type { ExtractedSymbol } from "../../traceability/symbol-extract.js";
5
+
6
+ export type QualityDiagnosticSeverity = "error" | "warning" | "review" | "info";
7
+
8
+ export type QualityDiagnostic = {
9
+ readonly id: string;
10
+ readonly severity: QualityDiagnosticSeverity;
11
+ readonly blocking: boolean;
12
+ readonly category:
13
+ | "symbol"
14
+ | "requirement"
15
+ | "coverage"
16
+ | "fact"
17
+ | "status"
18
+ | "coordinate"
19
+ | "mixed-purpose"
20
+ | string;
21
+ readonly entityId?: string;
22
+ readonly source?: string;
23
+ readonly files?: readonly string[];
24
+ readonly docs?: readonly string[];
25
+ readonly message: string;
26
+ readonly suggestion: string;
27
+ readonly evidence?: Readonly<Record<string, unknown>>;
28
+ };
29
+
30
+ export type SymbolsByFile = Map<string, readonly ExtractedSymbol[]>;
31
+
32
+ export type TraceabilityRelationship = {
33
+ readonly type: string;
34
+ readonly to: string;
35
+ };
36
+
37
+ export type SymbolGranularityDiagnosticsOptions = {
38
+ readonly manifestResults: readonly ExtractionResult[];
39
+ readonly activeEntityIds?: ReadonlySet<string>;
40
+ readonly symbolsByFile: SymbolsByFile;
41
+ readonly sourceContentByFile?: ReadonlyMap<string, string>;
42
+ readonly workspaceRoot?: string;
43
+ };
44
+
45
+ export type SemanticReviewDiagnosticsOptions = {
46
+ readonly symbolsByFile: SymbolsByFile;
47
+ };
48
+
49
+ export type SymbolQualityDiagnosticsOptions = {
50
+ readonly manifestResults: readonly ExtractionResult[];
51
+ readonly activeEntityIds?: ReadonlySet<string>;
52
+ readonly symbolsByFile: SymbolsByFile;
53
+ };
54
+
55
+ export type RequirementQualityDiagnosticsOptions = {
56
+ readonly manifestResults: readonly ExtractionResult[];
57
+ readonly hardViolationEntityIds?: ReadonlySet<string>;
58
+ };
59
+
60
+ export type ChangedFileImpactOptions = {
61
+ readonly workspaceRoot: string;
62
+ readonly sourceFiles?: readonly string[];
63
+ readonly staged?: boolean;
64
+ readonly includeWorkingTreeDiff?: boolean;
65
+ readonly includeImpactDiagnostics?: boolean;
66
+ readonly maxDiagnostics?: number;
67
+ };
68
+
69
+ export type ImpactExtractedSymbol = {
70
+ readonly id: string;
71
+ readonly name: string;
72
+ readonly kind: string;
73
+ readonly role: string;
74
+ readonly location: ExtractedSymbol["location"];
75
+ readonly hunkRanges: readonly HunkRange[];
76
+ readonly linkedEntityIds: readonly string[];
77
+ };
78
+
79
+ export type ImpactLinkedEntity = {
80
+ readonly id: string;
81
+ readonly relationshipType: string;
82
+ readonly sourceSymbolId: string;
83
+ readonly sourceSymbolName: string;
84
+ };
85
+
86
+ export type ChangedFileImpactResult = {
87
+ readonly impactDiagnostics: readonly KibiImpactDiagnostic[];
88
+ readonly sourceFiles: readonly string[];
89
+ readonly extractedSymbols: readonly ImpactExtractedSymbol[];
90
+ readonly linkedEntities: readonly ImpactLinkedEntity[];
91
+ readonly nextActions: readonly string[];
92
+ };
93
+
94
+ export type SourceChange = {
95
+ readonly file: string;
96
+ readonly status: StagedFile["status"];
97
+ readonly hunkRanges: readonly HunkRange[];
98
+ readonly content: string;
99
+ };
@@ -0,0 +1,22 @@
1
+ export { analyzeChangedFileImpact } from "./impact/analyzer.js";
2
+ export { collectFullKbQualityDiagnostics } from "./impact/full-kb-quality.js";
3
+ export {
4
+ createSemanticReviewDiagnostics,
5
+ createSymbolGranularityDiagnostics,
6
+ hasBlockingImpactDiagnostics,
7
+ hasBlockingQualityDiagnostics,
8
+ } from "./impact/diagnostics.js";
9
+ export { createRequirementQualityDiagnostics } from "./impact/requirement-quality.js";
10
+ export { createSymbolQualityDiagnostics } from "./impact/symbol-quality.js";
11
+ export type {
12
+ ChangedFileImpactOptions,
13
+ ChangedFileImpactResult,
14
+ ImpactExtractedSymbol,
15
+ ImpactLinkedEntity,
16
+ QualityDiagnostic,
17
+ QualityDiagnosticSeverity,
18
+ RequirementQualityDiagnosticsOptions,
19
+ SemanticReviewDiagnosticsOptions,
20
+ SymbolGranularityDiagnosticsOptions,
21
+ SymbolQualityDiagnosticsOptions,
22
+ } from "./impact/types.js";
@@ -29,6 +29,8 @@ type FactKind =
29
29
  type Operator = "eq" | "neq" | "lt" | "lte" | "gt" | "gte";
30
30
  type ValueType = "string" | "int" | "number" | "bool";
31
31
  type Polarity = "require" | "forbid" | "assert" | "deny";
32
+ type VerificationScope = "unit" | "integration" | "end_to_end";
33
+ type VerificationPerspective = "internal" | "consumer";
32
34
 
33
35
  const factConditionals = JSON.parse(`[
34
36
  {
@@ -133,6 +135,14 @@ const entitySchema: Record<string, unknown> = {
133
135
  type: "string",
134
136
  enum: [...SYMBOL_ROLES] satisfies SymbolRole[],
135
137
  },
138
+ verification_scope: {
139
+ type: "string",
140
+ enum: ["unit", "integration", "end_to_end"] satisfies VerificationScope[],
141
+ },
142
+ verification_perspective: {
143
+ type: "string",
144
+ enum: ["internal", "consumer"] satisfies VerificationPerspective[],
145
+ },
136
146
  type: {
137
147
  type: "string",
138
148
  enum: [
@@ -235,6 +245,19 @@ const entitySchema: Record<string, unknown> = {
235
245
  },
236
246
  },
237
247
  },
248
+ {
249
+ if: {
250
+ properties: { type: { const: "test" } },
251
+ },
252
+ else: {
253
+ not: {
254
+ anyOf: [
255
+ { required: ["verification_scope"] },
256
+ { required: ["verification_perspective"] },
257
+ ],
258
+ },
259
+ },
260
+ },
238
261
  ...factConditionals,
239
262
  ],
240
263
  additionalProperties: false,
@@ -101,7 +101,7 @@ See `resources/fact-lanes.md` for the full strict vs observation lane comparison
101
101
 
102
102
  Model one semantic claim per strict `property_value` fact. Reusing the same `subject_key` and `property_key` lets `domain-contradictions` compare requirements mechanically.
103
103
 
104
- Incoherent role-set example: `REQ-ROLE-SET-2` says the allowed user roles are exactly `[user, admin]`, while `REQ-ROLE-SET-3` says the same property is exactly `[user, admin, superadmin]`. Both constrain `FACT-USER-ROLES` and require different values for `user.roles.allowed_set`, so they cannot both be current.
104
+ Incoherent role-set example: `REQ-ROLE-SET-2` says the allowed user roles are exactly `user,admin`, while `REQ-ROLE-SET-3` says the same property is exactly `user,admin,superadmin`. Both constrain `FACT-USER-ROLES` and require different values for `user.roles.allowed_set`, so they cannot both be current.
105
105
 
106
106
  ```yaml
107
107
  subject:
@@ -115,15 +115,15 @@ property_values:
115
115
  subject_key: user.roles
116
116
  property_key: user.roles.allowed_set
117
117
  operator: eq
118
- value_type: list
119
- value_json: '["user", "admin"]'
118
+ value_type: string
119
+ value_string: user,admin
120
120
  - id: FACT-USER-ROLES-ALLOWED-3
121
121
  fact_kind: property_value
122
122
  subject_key: user.roles
123
123
  property_key: user.roles.allowed_set
124
124
  operator: eq
125
- value_type: list
126
- value_json: '["user", "admin", "superadmin"]'
125
+ value_type: string
126
+ value_string: user,admin,superadmin
127
127
 
128
128
  requirements:
129
129
  - id: REQ-ROLE-SET-2
@@ -176,6 +176,14 @@ Keep symbol payloads minimal: include only the fields needed to identify the sym
176
176
 
177
177
  When a generic `Query failed` appears, do not keep retrying the same payload. First call `kb_validate_upsert`, query or create the missing endpoints, reduce the payload to the minimum required fields, and retry once. If it still fails, report the blocker instead of looping.
178
178
 
179
+ ## Small Behavior Fix Impact Evidence
180
+
181
+ For a behavior-changing source edit with no existing requirement, create a requirement for the corrected behavior. If no requirement exists, create one for the corrected behavior, then model strict facts from that requirement when the invariant is contradiction-sensitive.
182
+
183
+ Do not link facts directly to tests. Facts describe the invariant; requirements or scenarios are verified by executable tests. Use `REQ -> TEST` with `verified_by` or `TEST -> REQ` with `validates`, then link the requirement to facts with `constrains` and `requires_property`. Link the touched production symbol to the requirement with `implements` and to the test with `covered_by` when coverage evidence is needed.
184
+
185
+ MCP writes do not automatically stage markdown evidence. When a staged hook requires impact evidence, ensure the repo's tracked documentation artifacts for the requirement, fact, test, symbol metadata, or coordinate refresh are authored and staged alongside the source change.
186
+
179
187
  ## Sequential Upserts
180
188
 
181
189
  Never fire `kb_upsert` calls in parallel. Execute them sequentially to avoid lock contention and ensure deterministic ordering. This is especially important when creating chains of related entities.
@@ -30,7 +30,7 @@ Granular strict facts work best when each `property_value` represents one semant
30
30
 
31
31
  ### Role set conflict
32
32
 
33
- `REQ-ROLE-SET-2` and `REQ-ROLE-SET-3` are incoherent if both are current: they each define the exact allowed role set, but one allows two roles and the other allows three.
33
+ `REQ-ROLE-SET-2` and `REQ-ROLE-SET-3` are incoherent if both are current: they each define the exact allowed role set, but one allows two roles and the other allows three. Encode compact set values as canonical strings because the strict property schema supports string, int, number, and bool values.
34
34
 
35
35
  ```yaml
36
36
  subject:
@@ -48,8 +48,8 @@ property_values:
48
48
  subject_key: user.roles
49
49
  property_key: user.roles.allowed_set
50
50
  operator: eq
51
- value_type: list
52
- value_json: '["user", "admin"]'
51
+ value_type: string
52
+ value_string: user,admin
53
53
  - id: FACT-USER-ROLES-ALLOWED-3
54
54
  title: User, admin, and superadmin roles
55
55
  status: active
@@ -57,8 +57,8 @@ property_values:
57
57
  subject_key: user.roles
58
58
  property_key: user.roles.allowed_set
59
59
  operator: eq
60
- value_type: list
61
- value_json: '["user", "admin", "superadmin"]'
60
+ value_type: string
61
+ value_string: user,admin,superadmin
62
62
 
63
63
  relationships:
64
64
  - { type: constrains, from: REQ-ROLE-SET-2, to: FACT-USER-ROLES }
@@ -87,3 +87,33 @@ relationships:
87
87
  from: SYM-handler
88
88
  to: TEST-005
89
89
  ```
90
+
91
+ ## Invalid Test-Fact Shortcuts
92
+
93
+ Do not model small behavior fixes as direct test-fact pairs. Facts describe invariants; requirements or scenarios are verified by tests.
94
+
95
+ ```yaml
96
+ # WRONG: fact -> test is not a valid verified_by shape
97
+ relationships:
98
+ - type: verified_by
99
+ from: FACT-HEADER-AVATAR-INITIAL-UPPERCASE
100
+ to: TEST-AVATAR-HEADER-FALLBACK
101
+
102
+ # WRONG: test -> fact is not a valid validates shape
103
+ relationships:
104
+ - type: validates
105
+ from: TEST-AVATAR-HEADER-FALLBACK
106
+ to: FACT-HEADER-AVATAR-INITIAL-UPPERCASE
107
+ ```
108
+
109
+ Use a requirement as the verification target/source, then link that requirement to strict facts:
110
+
111
+ ```yaml
112
+ relationships:
113
+ - type: verified_by
114
+ from: REQ-HEADER-AVATAR-INITIAL-UPPERCASE
115
+ to: TEST-AVATAR-HEADER-FALLBACK
116
+ - type: requires_property
117
+ from: REQ-HEADER-AVATAR-INITIAL-UPPERCASE
118
+ to: FACT-HEADER-AVATAR-INITIAL-UPPERCASE
119
+ ```
@@ -19,6 +19,20 @@ The canonical workflow for any KB operation follows this pattern:
19
19
  4. kb_check with targeted rules
20
20
  ```
21
21
 
22
+ ## Small Behavior Fix With No Existing Requirement
23
+ ```
24
+ 1. kb_search and kb_query by the changed source file and test file.
25
+ 2. If no requirement exists, create a REQ for the corrected behavior.
26
+ 3. Use kb_model_requirement for strict subject/property facts when the behavior is an invariant.
27
+ 4. Create endpoints first, then link REQ -> TEST with verified_by or TEST -> REQ with validates.
28
+ 5. Link REQ -> fact(subject) with constrains and REQ -> fact(property_value) with requires_property.
29
+ 6. Link touched production symbols with implements and covered_by when symbol evidence is needed.
30
+ 7. Author and stage tracked markdown/symbol evidence; MCP writes do not stage those files automatically.
31
+ 8. Run kb_check with targeted rules, then a final full check.
32
+ ```
33
+
34
+ Do not create a test-fact pair. Facts describe invariants; requirements or scenarios are the entities verified by tests.
35
+
22
36
  ## Fixing a Traceability Gap
23
37
  ```
24
38
  1. kb_query --sourceFile <code-file> to find linked entities