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,282 @@
1
+ import type { ExtractionResult } from "../../extractors/markdown.js";
2
+ import type { KibiImpactDiagnostic } from "../../traceability/staged-diagnostics.js";
3
+ import type { RequirementQualityDiagnosticsOptions } from "./types.js";
4
+
5
+ const CURRENT_REQUIREMENT_STATUSES: ReadonlySet<string> = new Set([
6
+ "open",
7
+ "in_progress",
8
+ "active",
9
+ "accepted",
10
+ ]);
11
+
12
+ const TEST_LIKE_REQUIREMENT_STATUSES: ReadonlySet<string> = new Set([
13
+ "passing",
14
+ "failing",
15
+ "skipped",
16
+ "pending",
17
+ ]);
18
+
19
+ const STRICT_RELATIONSHIP_TYPES: ReadonlySet<string> = new Set([
20
+ "constrains",
21
+ "requires_property",
22
+ "requires_predicate",
23
+ ]);
24
+
25
+ const BROAD_REQUIREMENT_THRESHOLDS = {
26
+ implementingSymbols: 8,
27
+ scenarios: 6,
28
+ tests: 8,
29
+ dependentRequirements: 5,
30
+ } as const;
31
+
32
+ const NORMATIVE_INDICATOR_PATTERNS: readonly [string, RegExp][] = [
33
+ ["must", /\bmust\b/i],
34
+ ["only", /\bonly\b/i],
35
+ ["never", /\bnever\b/i],
36
+ ["limit", /\blimits?\b/i],
37
+ ["minimum", /\bminimum\b/i],
38
+ ["maximum", /\bmaximum\b/i],
39
+ ["at least", /\bat\s+least\b/i],
40
+ ["at most", /\bat\s+most\b/i],
41
+ ];
42
+
43
+ type RequirementFanout = {
44
+ readonly implementingSymbolIds: readonly string[];
45
+ readonly scenarioIds: readonly string[];
46
+ readonly testIds: readonly string[];
47
+ readonly dependentRequirementIds: readonly string[];
48
+ };
49
+
50
+ function isRequirement(result: ExtractionResult): boolean {
51
+ return result.entity.type === "req";
52
+ }
53
+
54
+ function isCurrentRequirement(result: ExtractionResult): boolean {
55
+ return (
56
+ isRequirement(result) &&
57
+ CURRENT_REQUIREMENT_STATUSES.has(result.entity.status)
58
+ );
59
+ }
60
+
61
+ function sortedUnique(values: readonly string[]): readonly string[] {
62
+ return [...new Set(values)].sort();
63
+ }
64
+
65
+ function entityTypeById(
66
+ results: readonly ExtractionResult[],
67
+ ): ReadonlyMap<string, string> {
68
+ const types = new Map<string, string>();
69
+ for (const result of results) {
70
+ types.set(result.entity.id, result.entity.type);
71
+ }
72
+ return types;
73
+ }
74
+
75
+ function targetHasType(
76
+ targetId: string,
77
+ type: string,
78
+ typesByEntityId: ReadonlyMap<string, string>,
79
+ ): boolean {
80
+ const targetType = typesByEntityId.get(targetId);
81
+ return targetType === undefined || targetType === type;
82
+ }
83
+
84
+ function collectFanout(
85
+ requirementId: string,
86
+ results: readonly ExtractionResult[],
87
+ typesByEntityId: ReadonlyMap<string, string>,
88
+ ): RequirementFanout {
89
+ const implementingSymbolIds: string[] = [];
90
+ const scenarioIds: string[] = [];
91
+ const testIds: string[] = [];
92
+ const dependentRequirementIds: string[] = [];
93
+
94
+ for (const result of results) {
95
+ for (const relationship of result.relationships) {
96
+ if (
97
+ relationship.type === "implements" &&
98
+ relationship.to === requirementId
99
+ ) {
100
+ implementingSymbolIds.push(result.entity.id);
101
+ }
102
+ if (
103
+ result.entity.id === requirementId &&
104
+ relationship.type === "specified_by" &&
105
+ targetHasType(relationship.to, "scenario", typesByEntityId)
106
+ ) {
107
+ scenarioIds.push(relationship.to);
108
+ }
109
+ if (
110
+ result.entity.id === requirementId &&
111
+ (relationship.type === "verified_by" ||
112
+ relationship.type === "covered_by") &&
113
+ targetHasType(relationship.to, "test", typesByEntityId)
114
+ ) {
115
+ testIds.push(relationship.to);
116
+ }
117
+ if (relationship.type === "depends_on") {
118
+ if (
119
+ result.entity.id === requirementId &&
120
+ targetHasType(relationship.to, "req", typesByEntityId)
121
+ ) {
122
+ dependentRequirementIds.push(relationship.to);
123
+ }
124
+ if (relationship.to === requirementId && result.entity.type === "req") {
125
+ dependentRequirementIds.push(result.entity.id);
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ return {
132
+ implementingSymbolIds: sortedUnique(implementingSymbolIds),
133
+ scenarioIds: sortedUnique(scenarioIds),
134
+ testIds: sortedUnique(testIds),
135
+ dependentRequirementIds: sortedUnique(dependentRequirementIds),
136
+ };
137
+ }
138
+
139
+ function exceedsBroadThreshold(fanout: RequirementFanout): boolean {
140
+ return (
141
+ fanout.implementingSymbolIds.length >
142
+ BROAD_REQUIREMENT_THRESHOLDS.implementingSymbols ||
143
+ fanout.scenarioIds.length > BROAD_REQUIREMENT_THRESHOLDS.scenarios ||
144
+ fanout.testIds.length > BROAD_REQUIREMENT_THRESHOLDS.tests ||
145
+ fanout.dependentRequirementIds.length >
146
+ BROAD_REQUIREMENT_THRESHOLDS.dependentRequirements
147
+ );
148
+ }
149
+
150
+ function isUmbrellaRequirement(result: ExtractionResult): boolean {
151
+ const tags = result.entity.tags ?? [];
152
+ return tags.some((tag) => tag === "umbrella" || tag === "epic");
153
+ }
154
+
155
+ function createBroadRequirementDiagnostics(
156
+ results: readonly ExtractionResult[],
157
+ ): readonly KibiImpactDiagnostic[] {
158
+ const typesByEntityId = entityTypeById(results);
159
+ return results.flatMap((result) => {
160
+ if (!isCurrentRequirement(result)) return [];
161
+ const fanout = collectFanout(result.entity.id, results, typesByEntityId);
162
+ if (!exceedsBroadThreshold(fanout)) return [];
163
+ if (isUmbrellaRequirement(result)) return [];
164
+
165
+ return [
166
+ {
167
+ id: "broad_requirement_review",
168
+ severity: "review",
169
+ blocking: false,
170
+ category: "requirement",
171
+ entityId: result.entity.id,
172
+ source: result.entity.source,
173
+ files: [result.entity.source],
174
+ docs: ["docs/entity-schema.md"],
175
+ message: `Requirement ${result.entity.id} has broad fanout that exceeds requirement review thresholds.`,
176
+ suggestion:
177
+ "Split this into smaller behavioral requirements, or keep an explicit umbrella/epic tag when broad ownership is intentional.",
178
+ evidence: {
179
+ implementingSymbolIds: fanout.implementingSymbolIds,
180
+ implementingSymbolCount: fanout.implementingSymbolIds.length,
181
+ implementingSymbolThreshold:
182
+ BROAD_REQUIREMENT_THRESHOLDS.implementingSymbols,
183
+ scenarioIds: fanout.scenarioIds,
184
+ scenarioCount: fanout.scenarioIds.length,
185
+ scenarioThreshold: BROAD_REQUIREMENT_THRESHOLDS.scenarios,
186
+ testIds: fanout.testIds,
187
+ testCount: fanout.testIds.length,
188
+ testThreshold: BROAD_REQUIREMENT_THRESHOLDS.tests,
189
+ dependentRequirementIds: fanout.dependentRequirementIds,
190
+ dependentRequirementCount: fanout.dependentRequirementIds.length,
191
+ dependentRequirementThreshold:
192
+ BROAD_REQUIREMENT_THRESHOLDS.dependentRequirements,
193
+ },
194
+ },
195
+ ];
196
+ });
197
+ }
198
+
199
+ function createRequirementStatusDiagnostics(
200
+ results: readonly ExtractionResult[],
201
+ ): readonly KibiImpactDiagnostic[] {
202
+ return results.flatMap((result) => {
203
+ if (!isRequirement(result)) return [];
204
+ if (!TEST_LIKE_REQUIREMENT_STATUSES.has(result.entity.status)) return [];
205
+
206
+ return [
207
+ {
208
+ id: "requirement_status_review",
209
+ severity: "review",
210
+ blocking: false,
211
+ category: "status",
212
+ entityId: result.entity.id,
213
+ source: result.entity.source,
214
+ files: [result.entity.source],
215
+ docs: ["docs/entity-schema.md"],
216
+ message: `Requirement ${result.entity.id} uses test-like status ${result.entity.status}.`,
217
+ suggestion:
218
+ "Use requirement lifecycle statuses such as open, in_progress, active, accepted, closed, deprecated, or superseded; keep passing/failing statuses on test entities.",
219
+ evidence: { status: result.entity.status },
220
+ },
221
+ ];
222
+ });
223
+ }
224
+
225
+ function strictRelationshipTypes(result: ExtractionResult): readonly string[] {
226
+ return sortedUnique(
227
+ result.relationships
228
+ .filter((relationship) =>
229
+ STRICT_RELATIONSHIP_TYPES.has(relationship.type),
230
+ )
231
+ .map((relationship) => relationship.type),
232
+ );
233
+ }
234
+
235
+ function matchedNormativeIndicators(text: string): readonly string[] {
236
+ return NORMATIVE_INDICATOR_PATTERNS.filter((entry) =>
237
+ entry[1].test(text),
238
+ ).map((entry) => entry[0]);
239
+ }
240
+
241
+ function createStrictFactModelingDiagnostics(
242
+ options: RequirementQualityDiagnosticsOptions,
243
+ ): readonly KibiImpactDiagnostic[] {
244
+ return options.manifestResults.flatMap((result) => {
245
+ if (!isCurrentRequirement(result)) return [];
246
+ if (options.hardViolationEntityIds?.has(result.entity.id)) return [];
247
+ const relationshipTypes = strictRelationshipTypes(result);
248
+ if (relationshipTypes.length > 0) return [];
249
+ const indicators = matchedNormativeIndicators(result.entity.title);
250
+ if (indicators.length === 0) return [];
251
+
252
+ return [
253
+ {
254
+ id: "strict_fact_modeling_review",
255
+ severity: "review",
256
+ blocking: false,
257
+ category: "fact",
258
+ entityId: result.entity.id,
259
+ source: result.entity.source,
260
+ files: [result.entity.source],
261
+ docs: ["docs/modeling-cheatsheet.md", "docs/error-reference.md"],
262
+ message: `Requirement ${result.entity.id} contains normative prose but has no strict fact or predicate relationship.`,
263
+ suggestion:
264
+ "Use kb_model_requirement for strict property facts, or kb_suggest_predicates when the claim fits a predicate schema.",
265
+ evidence: {
266
+ matchedIndicators: indicators,
267
+ strictRelationshipTypes: relationshipTypes,
268
+ },
269
+ },
270
+ ];
271
+ });
272
+ }
273
+
274
+ export function createRequirementQualityDiagnostics(
275
+ options: RequirementQualityDiagnosticsOptions,
276
+ ): KibiImpactDiagnostic[] {
277
+ return [
278
+ ...createBroadRequirementDiagnostics(options.manifestResults),
279
+ ...createRequirementStatusDiagnostics(options.manifestResults),
280
+ ...createStrictFactModelingDiagnostics(options),
281
+ ];
282
+ }
@@ -0,0 +1,180 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import * as path from "node:path";
4
+ import type { HunkRange } from "../../traceability/git-staged.js";
5
+ import {
6
+ getStagedFiles,
7
+ parseHunksFromDiff,
8
+ } from "../../traceability/git-staged.js";
9
+ import { hasMeaningfulSourceDiff } from "./diff-meaning.js";
10
+ import type { ChangedFileImpactOptions, SourceChange } from "./types.js";
11
+
12
+ const SOURCE_EXTENSIONS = new Set([
13
+ ".ts",
14
+ ".tsx",
15
+ ".js",
16
+ ".jsx",
17
+ ".mts",
18
+ ".cts",
19
+ ".mjs",
20
+ ".cjs",
21
+ ]);
22
+
23
+ function isSupportedSourcePath(filePath: string): boolean {
24
+ return SOURCE_EXTENSIONS.has(path.extname(filePath));
25
+ }
26
+
27
+ export function normalizeSourceFile(
28
+ workspaceRoot: string,
29
+ sourceFile: string,
30
+ ): string {
31
+ const relativePath = path.isAbsolute(sourceFile)
32
+ ? path.relative(workspaceRoot, sourceFile)
33
+ : sourceFile;
34
+ return relativePath.split(path.sep).join("/");
35
+ }
36
+
37
+ export function uniqueSorted(values: Iterable<string>): string[] {
38
+ return [...new Set(values)].sort();
39
+ }
40
+
41
+ function escapeGitPath(filePath: string): string {
42
+ return filePath.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
43
+ }
44
+
45
+ function runGit(workspaceRoot: string, command: string): string {
46
+ return execSync(command, { cwd: workspaceRoot, encoding: "utf8" });
47
+ }
48
+
49
+ function readWorkingTreeSource(
50
+ workspaceRoot: string,
51
+ sourceFile: string,
52
+ ): string | null {
53
+ const absolutePath = path.resolve(workspaceRoot, sourceFile);
54
+ if (!existsSync(absolutePath)) return null;
55
+ return readFileSync(absolutePath, "utf8");
56
+ }
57
+
58
+ function fullFileHunk(content: string): HunkRange[] {
59
+ return [{ start: 1, end: Math.max(1, content.split(/\r?\n/).length) }];
60
+ }
61
+
62
+ function getWorkingTreeDiffSourceFiles(workspaceRoot: string): string[] {
63
+ try {
64
+ return uniqueSorted(
65
+ runGit(workspaceRoot, "git diff --name-only --diff-filter=ACMR")
66
+ .split(/\r?\n/)
67
+ .map((filePath) => normalizeSourceFile(workspaceRoot, filePath.trim()))
68
+ .filter((filePath) => filePath.length > 0)
69
+ .filter(isSupportedSourcePath),
70
+ );
71
+ } catch {
72
+ return [];
73
+ }
74
+ }
75
+
76
+ function getWorkingTreeHunks(
77
+ workspaceRoot: string,
78
+ sourceFile: string,
79
+ ): HunkRange[] {
80
+ try {
81
+ const diffText = runGit(
82
+ workspaceRoot,
83
+ `git diff -U0 -- "${escapeGitPath(sourceFile)}"`,
84
+ );
85
+ return parseHunksFromDiff(diffText);
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+
91
+ function getStagedSourceChanges(
92
+ workspaceRoot: string,
93
+ sourceFileFilter: ReadonlySet<string>,
94
+ ): SourceChange[] {
95
+ const stagedFiles = getStagedFiles((command, options) =>
96
+ execSync(command, { ...options, cwd: workspaceRoot }),
97
+ );
98
+
99
+ return stagedFiles
100
+ .filter((file) => isSupportedSourcePath(file.path))
101
+ .filter(
102
+ (file) => sourceFileFilter.size === 0 || sourceFileFilter.has(file.path),
103
+ )
104
+ .flatMap((file): SourceChange[] => {
105
+ if (file.content === undefined || file.hunkRanges.length === 0) return [];
106
+ if (
107
+ file.diffText !== undefined &&
108
+ !hasMeaningfulSourceDiff(file.diffText)
109
+ ) {
110
+ return [];
111
+ }
112
+ return [
113
+ {
114
+ file: file.path,
115
+ status: file.status,
116
+ hunkRanges: file.hunkRanges,
117
+ content: file.content,
118
+ },
119
+ ];
120
+ });
121
+ }
122
+
123
+ function getWorkingTreeSourceChanges(
124
+ workspaceRoot: string,
125
+ sourceFileFilter: ReadonlySet<string>,
126
+ ): SourceChange[] {
127
+ const sourceFiles =
128
+ sourceFileFilter.size > 0
129
+ ? [...sourceFileFilter]
130
+ : getWorkingTreeDiffSourceFiles(workspaceRoot);
131
+
132
+ return sourceFiles.flatMap((sourceFile): SourceChange[] => {
133
+ const content = readWorkingTreeSource(workspaceRoot, sourceFile);
134
+ if (content === null) return [];
135
+ const diffText = runGit(
136
+ workspaceRoot,
137
+ `git diff -U0 -- "${escapeGitPath(sourceFile)}"`,
138
+ );
139
+ if (!hasMeaningfulSourceDiff(diffText)) return [];
140
+ const hunkRanges = getWorkingTreeHunks(workspaceRoot, sourceFile);
141
+ if (hunkRanges.length === 0) return [];
142
+ return [{ file: sourceFile, status: "M", hunkRanges, content }];
143
+ });
144
+ }
145
+
146
+ function getExplicitSourceChanges(
147
+ workspaceRoot: string,
148
+ sourceFileFilter: ReadonlySet<string>,
149
+ ): SourceChange[] {
150
+ return [...sourceFileFilter].flatMap((sourceFile): SourceChange[] => {
151
+ const content = readWorkingTreeSource(workspaceRoot, sourceFile);
152
+ if (content === null) return [];
153
+ return [
154
+ {
155
+ file: sourceFile,
156
+ status: "M",
157
+ hunkRanges: fullFileHunk(content),
158
+ content,
159
+ },
160
+ ];
161
+ });
162
+ }
163
+
164
+ export function collectSourceChanges(
165
+ options: ChangedFileImpactOptions,
166
+ ): SourceChange[] {
167
+ const workspaceRoot = path.resolve(options.workspaceRoot);
168
+ const sourceFileFilter = new Set(
169
+ (options.sourceFiles ?? [])
170
+ .map((sourceFile) => normalizeSourceFile(workspaceRoot, sourceFile))
171
+ .filter(isSupportedSourcePath),
172
+ );
173
+
174
+ if (options.staged)
175
+ return getStagedSourceChanges(workspaceRoot, sourceFileFilter);
176
+ if (options.includeWorkingTreeDiff) {
177
+ return getWorkingTreeSourceChanges(workspaceRoot, sourceFileFilter);
178
+ }
179
+ return getExplicitSourceChanges(workspaceRoot, sourceFileFilter);
180
+ }
@@ -0,0 +1,100 @@
1
+ import type { ExtractionResult } from "../../extractors/markdown.js";
2
+ import type { ExtractedSymbol } from "../../traceability/symbol-extract.js";
3
+ import { isTraceabilityRelationshipType } from "../symbol-granularity.js";
4
+ import { uniqueSorted } from "./source-changes.js";
5
+ import type {
6
+ ImpactExtractedSymbol,
7
+ ImpactLinkedEntity,
8
+ SymbolsByFile,
9
+ } from "./types.js";
10
+
11
+ function linkedEntityIdsFor(symbol: ExtractedSymbol): string[] {
12
+ return uniqueSorted(
13
+ (symbol.relationships ?? [])
14
+ .filter((relationship) =>
15
+ isTraceabilityRelationshipType(relationship.type),
16
+ )
17
+ .map((relationship) => relationship.to),
18
+ );
19
+ }
20
+
21
+ export function formatExtractedSymbols(
22
+ symbolsByFile: SymbolsByFile,
23
+ ): ImpactExtractedSymbol[] {
24
+ return [...symbolsByFile.values()]
25
+ .flatMap((symbols) => [...symbols])
26
+ .map((symbol) => ({
27
+ id: symbol.id,
28
+ name: symbol.name,
29
+ kind: symbol.kind,
30
+ role: symbol.role,
31
+ location: symbol.location,
32
+ hunkRanges: symbol.hunkRanges,
33
+ linkedEntityIds: linkedEntityIdsFor(symbol),
34
+ }))
35
+ .sort((left, right) => left.name.localeCompare(right.name));
36
+ }
37
+
38
+ export function collectLinkedEntities(
39
+ symbolsByFile: SymbolsByFile,
40
+ manifestResults: readonly ExtractionResult[],
41
+ activeSourceFiles: ReadonlySet<string>,
42
+ ): ImpactLinkedEntity[] {
43
+ const linkedEntities = new Map<string, ImpactLinkedEntity>();
44
+ const add = (entity: ImpactLinkedEntity): void => {
45
+ linkedEntities.set(
46
+ `${entity.sourceSymbolId}:${entity.relationshipType}:${entity.id}`,
47
+ entity,
48
+ );
49
+ };
50
+
51
+ for (const symbols of symbolsByFile.values()) {
52
+ for (const symbol of symbols) {
53
+ for (const relationship of symbol.relationships ?? []) {
54
+ if (!isTraceabilityRelationshipType(relationship.type)) continue;
55
+ add({
56
+ id: relationship.to,
57
+ relationshipType: relationship.type,
58
+ sourceSymbolId: symbol.id,
59
+ sourceSymbolName: symbol.name,
60
+ });
61
+ }
62
+ }
63
+ }
64
+
65
+ for (const result of manifestResults) {
66
+ if (
67
+ result.sourceFile === undefined ||
68
+ !activeSourceFiles.has(result.sourceFile)
69
+ ) {
70
+ continue;
71
+ }
72
+ for (const relationship of result.relationships) {
73
+ if (!isTraceabilityRelationshipType(relationship.type)) continue;
74
+ add({
75
+ id: relationship.to,
76
+ relationshipType: relationship.type,
77
+ sourceSymbolId: result.entity.id,
78
+ sourceSymbolName: result.entity.title,
79
+ });
80
+ }
81
+ }
82
+
83
+ return [...linkedEntities.values()].sort((left, right) =>
84
+ left.id.localeCompare(right.id),
85
+ );
86
+ }
87
+
88
+ export function buildNextActions(sourceFiles: readonly string[]): string[] {
89
+ if (sourceFiles.length === 0) return [];
90
+
91
+ return [
92
+ `kb_search({ query: "${sourceFiles.join(" ")}" }) to discover requirements/tests that may describe the changed behavior.`,
93
+ ...sourceFiles.map(
94
+ (sourceFile) =>
95
+ `kb_query({ sourceFile: "${sourceFile}" }) to inspect exact source-linked Kibi entities before deciding whether requirements/tests need updates.`,
96
+ ),
97
+ "Update narrower symbols, requirements, tests, or strict facts when diagnostics show broad or stale modeling.",
98
+ "rerun kb_check with the same sourceFiles/impact options after Kibi updates.",
99
+ ];
100
+ }