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,192 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { ExtractionResult } from "../../extractors/markdown.js";
4
+ import type { KibiImpactDiagnostic } from "../../traceability/staged-diagnostics.js";
5
+ import {
6
+ type ExtractedSymbol,
7
+ extractSymbolsFromStagedFile,
8
+ } from "../../traceability/symbol-extract.js";
9
+ import {
10
+ getBehavioralSymbolNames,
11
+ getNonBehavioralSymbolNames,
12
+ isAllowedGranularityReason,
13
+ isTraceabilityRelationshipType,
14
+ } from "../symbol-granularity.js";
15
+ import type {
16
+ QualityDiagnostic,
17
+ SemanticReviewDiagnosticsOptions,
18
+ SymbolGranularityDiagnosticsOptions,
19
+ } from "./types.js";
20
+
21
+ function isNarrowerSymbol(
22
+ manifestSymbolName: string,
23
+ candidate: ExtractedSymbol,
24
+ sourceSymbols: readonly ExtractedSymbol[],
25
+ ): boolean {
26
+ if (candidate.name === manifestSymbolName) return false;
27
+ if (candidate.name.startsWith(`${manifestSymbolName}.`)) return true;
28
+ return !sourceSymbols.some((symbol) => symbol.name === manifestSymbolName);
29
+ }
30
+
31
+ function hasTraceabilityRelationship(result: ExtractionResult): boolean {
32
+ return result.relationships.some((relationship) =>
33
+ isTraceabilityRelationshipType(relationship.type),
34
+ );
35
+ }
36
+
37
+ function hasValidGranularityReason(result: ExtractionResult): boolean {
38
+ return isAllowedGranularityReason(result.entity.granularity_reason);
39
+ }
40
+
41
+ function readSourceSymbols(
42
+ sourceFile: string,
43
+ sourceContentByFile: ReadonlyMap<string, string> | undefined,
44
+ workspaceRoot: string,
45
+ ): readonly ExtractedSymbol[] {
46
+ const stagedContent = sourceContentByFile?.get(sourceFile);
47
+ if (stagedContent !== undefined) {
48
+ return extractSymbolsFromStagedFile({
49
+ path: sourceFile,
50
+ status: "M",
51
+ hunkRanges: [{ start: 1, end: Number.MAX_SAFE_INTEGER }],
52
+ content: stagedContent,
53
+ });
54
+ }
55
+
56
+ const absolutePath = path.isAbsolute(sourceFile)
57
+ ? sourceFile
58
+ : path.resolve(workspaceRoot, sourceFile);
59
+ if (!existsSync(absolutePath)) return [];
60
+
61
+ return extractSymbolsFromStagedFile({
62
+ path: sourceFile,
63
+ status: "M",
64
+ hunkRanges: [{ start: 1, end: Number.MAX_SAFE_INTEGER }],
65
+ content: readFileSync(absolutePath, "utf8"),
66
+ });
67
+ }
68
+
69
+ function getGranularSymbolsForSourceFile(
70
+ sourceFile: string,
71
+ options: SymbolGranularityDiagnosticsOptions,
72
+ ): readonly ExtractedSymbol[] {
73
+ return (
74
+ options.symbolsByFile.get(sourceFile) ??
75
+ readSourceSymbols(
76
+ sourceFile,
77
+ options.sourceContentByFile,
78
+ options.workspaceRoot ?? process.cwd(),
79
+ )
80
+ );
81
+ }
82
+
83
+ function formatRelationshipTargets(symbol: ExtractedSymbol): string {
84
+ const targets = [
85
+ ...new Set(
86
+ (symbol.relationships ?? [])
87
+ .filter((relationship) =>
88
+ isTraceabilityRelationshipType(relationship.type),
89
+ )
90
+ .map((relationship) => relationship.to),
91
+ ),
92
+ ].sort();
93
+
94
+ return targets.length > 0 ? targets.join(", ") : "linked requirements/tests";
95
+ }
96
+
97
+ function isChangedBehavioralReviewCandidate(symbol: ExtractedSymbol): boolean {
98
+ if (symbol.role !== "behavioral") return false;
99
+ if (symbol.kind === "class") return false;
100
+ return symbol.hunkRanges.length > 0;
101
+ }
102
+
103
+ export function createSymbolGranularityDiagnostics(
104
+ options: SymbolGranularityDiagnosticsOptions,
105
+ ): KibiImpactDiagnostic[] {
106
+ const diagnostics: KibiImpactDiagnostic[] = [];
107
+
108
+ for (const result of options.manifestResults) {
109
+ if (
110
+ options.activeEntityIds !== undefined &&
111
+ !options.activeEntityIds.has(result.entity.id)
112
+ ) {
113
+ continue;
114
+ }
115
+ if (!result.sourceFile) continue;
116
+ if (!hasTraceabilityRelationship(result)) continue;
117
+ if (hasValidGranularityReason(result)) continue;
118
+
119
+ const granularSymbols = getGranularSymbolsForSourceFile(
120
+ result.sourceFile,
121
+ options,
122
+ );
123
+ const narrowerSymbols = granularSymbols.filter((symbol) =>
124
+ isNarrowerSymbol(result.entity.title, symbol, granularSymbols),
125
+ );
126
+ if (narrowerSymbols.length === 0) continue;
127
+
128
+ const behavioralNames = getBehavioralSymbolNames(narrowerSymbols);
129
+ if (behavioralNames.length === 0) continue;
130
+
131
+ const nonBehavioralNames = getNonBehavioralSymbolNames(narrowerSymbols);
132
+ const ignoredSymbolsSuggestion =
133
+ nonBehavioralNames.length > 0
134
+ ? ` Non-behavioral symbols ignored for this decision: ${nonBehavioralNames.join(
135
+ ", ",
136
+ )}.`
137
+ : "";
138
+
139
+ diagnostics.push({
140
+ id: "symbol_granularity_violation",
141
+ severity: "error",
142
+ blocking: true,
143
+ category: "symbol",
144
+ files: [result.entity.source, result.sourceFile],
145
+ docs: ["docs/symbol-traceability-taxonomy.md"],
146
+ message: `Symbol ${result.entity.id} links ${result.sourceFile} coarsely while granular symbols are available (behavioral only): ${behavioralNames.join(", ")}`,
147
+ suggestion: `Move ownership/coverage/test relationships to the narrow behavioral symbol, add a manifest behavioral anchor, or add granularity_reason with config-artifact, module-level-behavior, extractor-miss, or legacy-link when the coarse symbol is intentional.${ignoredSymbolsSuggestion}`,
148
+ });
149
+ }
150
+
151
+ return diagnostics;
152
+ }
153
+
154
+ export function createSemanticReviewDiagnostics(
155
+ options: SemanticReviewDiagnosticsOptions,
156
+ ): KibiImpactDiagnostic[] {
157
+ const diagnostics: KibiImpactDiagnostic[] = [];
158
+
159
+ for (const symbols of options.symbolsByFile.values()) {
160
+ for (const symbol of symbols) {
161
+ if (!isChangedBehavioralReviewCandidate(symbol)) continue;
162
+
163
+ const linkedTargets = formatRelationshipTargets(symbol);
164
+ diagnostics.push({
165
+ id: "symbol_semantic_review_needed",
166
+ severity: "warning",
167
+ blocking: false,
168
+ category: "symbol",
169
+ files: [symbol.location.file],
170
+ docs: ["docs/symbol-traceability-taxonomy.md"],
171
+ message: `Changed behavioral symbol ${symbol.name} needs semantic review against linked Kibi coverage: ${linkedTargets}`,
172
+ suggestion: `Use kb_search and kb_query for ${symbol.location.file}, then verify that ${linkedTargets} actually describe and test the changed behavior or UI copy. Update Kibi links or requirements/tests while the edit context is fresh.`,
173
+ });
174
+ }
175
+ }
176
+
177
+ return diagnostics;
178
+ }
179
+
180
+ export function hasBlockingImpactDiagnostics(
181
+ diagnostics: readonly KibiImpactDiagnostic[],
182
+ ): boolean {
183
+ return hasBlockingQualityDiagnostics(diagnostics);
184
+ }
185
+
186
+ export function hasBlockingQualityDiagnostics(
187
+ diagnostics: readonly QualityDiagnostic[],
188
+ ): boolean {
189
+ return diagnostics.some(
190
+ (diagnostic) => diagnostic.blocking || diagnostic.severity === "error",
191
+ );
192
+ }
@@ -0,0 +1,121 @@
1
+ function normalizeDiffContent(line: string): string {
2
+ let normalized = "";
3
+ let quote: '"' | "'" | "`" | null = null;
4
+ let escaped = false;
5
+
6
+ for (const character of line.slice(1)) {
7
+ if (quote !== null) {
8
+ normalized += character;
9
+
10
+ if (escaped) {
11
+ escaped = false;
12
+ continue;
13
+ }
14
+ if (character === "\\") {
15
+ escaped = true;
16
+ continue;
17
+ }
18
+ if (character === quote) quote = null;
19
+ continue;
20
+ }
21
+
22
+ if (character === '"' || character === "'" || character === "`") {
23
+ quote = character;
24
+ normalized += character;
25
+ continue;
26
+ }
27
+
28
+ if (!/\s/.test(character)) normalized += character;
29
+ }
30
+
31
+ return normalized;
32
+ }
33
+
34
+ function removeSyntacticTrailingCommas(content: string): string {
35
+ let normalized = "";
36
+ let quote: '"' | "'" | "`" | null = null;
37
+ let escaped = false;
38
+
39
+ for (let index = 0; index < content.length; index += 1) {
40
+ const character = content[index];
41
+ if (character === undefined) continue;
42
+
43
+ if (quote !== null) {
44
+ normalized += character;
45
+
46
+ if (escaped) {
47
+ escaped = false;
48
+ continue;
49
+ }
50
+ if (character === "\\") {
51
+ escaped = true;
52
+ continue;
53
+ }
54
+ if (character === quote) quote = null;
55
+ continue;
56
+ }
57
+
58
+ if (character === '"' || character === "'" || character === "`") {
59
+ quote = character;
60
+ normalized += character;
61
+ continue;
62
+ }
63
+
64
+ const nextCharacter = content[index + 1];
65
+ if (
66
+ character === "," &&
67
+ (nextCharacter === ")" || nextCharacter === "]" || nextCharacter === "}")
68
+ ) {
69
+ continue;
70
+ }
71
+
72
+ normalized += character;
73
+ }
74
+
75
+ return normalized;
76
+ }
77
+
78
+ function hasMeaningfulDiffHunk(diffLines: readonly string[]): boolean {
79
+ let removedContent = "";
80
+ let addedContent = "";
81
+
82
+ for (const line of diffLines) {
83
+ if (line.startsWith("-") && !line.startsWith("---")) {
84
+ removedContent += normalizeDiffContent(line);
85
+ continue;
86
+ }
87
+ if (line.startsWith("+") && !line.startsWith("+++")) {
88
+ addedContent += normalizeDiffContent(line);
89
+ }
90
+ }
91
+
92
+ const normalizedRemovedContent =
93
+ removeSyntacticTrailingCommas(removedContent);
94
+ const normalizedAddedContent = removeSyntacticTrailingCommas(addedContent);
95
+
96
+ if (
97
+ normalizedRemovedContent.length === 0 &&
98
+ normalizedAddedContent.length === 0
99
+ ) {
100
+ return false;
101
+ }
102
+ return normalizedRemovedContent !== normalizedAddedContent;
103
+ }
104
+
105
+ // implements REQ-cli-staged-impact-enforcement
106
+ export function hasMeaningfulSourceDiff(diffText: string): boolean {
107
+ const hunkLines: string[] = [];
108
+
109
+ for (const line of diffText.split(/\r?\n/)) {
110
+ if (line.startsWith("@@")) {
111
+ if (hasMeaningfulDiffHunk(hunkLines)) return true;
112
+ hunkLines.length = 0;
113
+ continue;
114
+ }
115
+ if (hunkLines.length > 0 || line.startsWith("+") || line.startsWith("-")) {
116
+ hunkLines.push(line);
117
+ }
118
+ }
119
+
120
+ return hasMeaningfulDiffHunk(hunkLines);
121
+ }
@@ -0,0 +1,203 @@
1
+ import type {
2
+ ExtractedEntity,
3
+ ExtractionResult,
4
+ } from "../../extractors/markdown.js";
5
+ import type { PrologProcess } from "../../prolog.js";
6
+ import {
7
+ parseEntityFromList,
8
+ parseListOfLists,
9
+ parseTriples,
10
+ } from "../../prolog/codec.js";
11
+ import { createCoverageDepthQualityDiagnostics } from "./coverage-depth-quality.js";
12
+ import { createRequirementQualityDiagnostics } from "./requirement-quality.js";
13
+ import { createSymbolQualityDiagnostics } from "./symbol-quality.js";
14
+ import type { QualityDiagnostic } from "./types.js";
15
+
16
+ const RELATIONSHIP_TYPES = [
17
+ "depends_on",
18
+ "executable_for",
19
+ "specified_by",
20
+ "verified_by",
21
+ "validates",
22
+ "implements",
23
+ "covered_by",
24
+ "constrained_by",
25
+ "constrains",
26
+ "requires_property",
27
+ "requires_predicate",
28
+ "guards",
29
+ "publishes",
30
+ "consumes",
31
+ "supersedes",
32
+ "relates_to",
33
+ ] as const;
34
+
35
+ type FullKbQualityDiagnosticsOptions = {
36
+ readonly prolog: PrologProcess;
37
+ readonly hardViolationEntityIds?: ReadonlySet<string>;
38
+ readonly maxDiagnostics?: number;
39
+ };
40
+
41
+ function stringField(entity: Record<string, unknown>, key: string): string {
42
+ const value = entity[key];
43
+ return typeof value === "string" ? value : "";
44
+ }
45
+
46
+ function optionalStringField(
47
+ entity: Record<string, unknown>,
48
+ key: string,
49
+ ): string | undefined {
50
+ const value = entity[key];
51
+ return typeof value === "string" && value.length > 0 ? value : undefined;
52
+ }
53
+
54
+ function optionalNumberField(
55
+ entity: Record<string, unknown>,
56
+ key: string,
57
+ ): number | undefined {
58
+ const value = entity[key];
59
+ return typeof value === "number" ? value : undefined;
60
+ }
61
+
62
+ function optionalStringArrayField(
63
+ entity: Record<string, unknown>,
64
+ key: string,
65
+ ): string[] | undefined {
66
+ const value = entity[key];
67
+ if (!Array.isArray(value)) return undefined;
68
+ return value.map((item) => String(item));
69
+ }
70
+
71
+ function toExtractedEntity(entity: Record<string, unknown>): ExtractedEntity {
72
+ const extracted: ExtractedEntity = {
73
+ id: stringField(entity, "id"),
74
+ type: stringField(entity, "type"),
75
+ title: stringField(entity, "title"),
76
+ status: stringField(entity, "status"),
77
+ created_at: stringField(entity, "created_at"),
78
+ updated_at: stringField(entity, "updated_at"),
79
+ source: stringField(entity, "source"),
80
+ };
81
+ const tags = optionalStringArrayField(entity, "tags");
82
+ if (tags !== undefined) extracted.tags = tags;
83
+ const owner = optionalStringField(entity, "owner");
84
+ if (owner !== undefined) extracted.owner = owner;
85
+ const priority = optionalStringField(entity, "priority");
86
+ if (priority !== undefined) extracted.priority = priority;
87
+ const severity = optionalStringField(entity, "severity");
88
+ if (severity !== undefined) extracted.severity = severity;
89
+ const textRef = optionalStringField(entity, "text_ref");
90
+ if (textRef !== undefined) extracted.text_ref = textRef;
91
+ const granularityReason = optionalStringField(entity, "granularity_reason");
92
+ if (granularityReason !== undefined) {
93
+ extracted.granularity_reason = granularityReason;
94
+ }
95
+ const symbolKind = optionalStringField(entity, "symbol_kind");
96
+ if (symbolKind !== undefined) extracted.symbol_kind = symbolKind;
97
+ const symbolRole = optionalStringField(entity, "symbol_role");
98
+ if (symbolRole !== undefined) extracted.symbol_role = symbolRole;
99
+ const verificationScope = optionalStringField(entity, "verification_scope");
100
+ if (
101
+ verificationScope === "unit" ||
102
+ verificationScope === "integration" ||
103
+ verificationScope === "end_to_end"
104
+ ) {
105
+ extracted.verification_scope = verificationScope;
106
+ }
107
+ const sourceLine = optionalNumberField(entity, "sourceLine");
108
+ if (sourceLine !== undefined) extracted.sourceLine = sourceLine;
109
+ const sourceColumn = optionalNumberField(entity, "sourceColumn");
110
+ if (sourceColumn !== undefined) extracted.sourceColumn = sourceColumn;
111
+ const sourceEndLine = optionalNumberField(entity, "sourceEndLine");
112
+ if (sourceEndLine !== undefined) extracted.sourceEndLine = sourceEndLine;
113
+ const sourceEndColumn = optionalNumberField(entity, "sourceEndColumn");
114
+ if (sourceEndColumn !== undefined)
115
+ extracted.sourceEndColumn = sourceEndColumn;
116
+ const factKind = optionalStringField(entity, "fact_kind");
117
+ if (
118
+ factKind === "subject" ||
119
+ factKind === "property_value" ||
120
+ factKind === "observation" ||
121
+ factKind === "meta"
122
+ ) {
123
+ extracted.fact_kind = factKind;
124
+ }
125
+ return extracted;
126
+ }
127
+
128
+ function sourceFileFor(entity: Record<string, unknown>): string | undefined {
129
+ return (
130
+ optionalStringField(entity, "sourceFile") ??
131
+ optionalStringField(entity, "source_file")
132
+ );
133
+ }
134
+
135
+ async function loadKbExtractionResults(
136
+ prolog: PrologProcess,
137
+ ): Promise<ExtractionResult[]> {
138
+ const entityResult = await prolog.query(
139
+ "findall([Id,Type,Props], kb_entity(Id, Type, Props), Results)",
140
+ );
141
+ const entities = entityResult.bindings.Results
142
+ ? parseListOfLists(entityResult.bindings.Results).map(parseEntityFromList)
143
+ : [];
144
+ const relationships = new Map<string, ExtractionResult["relationships"]>();
145
+
146
+ for (const relationshipType of RELATIONSHIP_TYPES) {
147
+ const relResult = await prolog.query(
148
+ `findall([From,To,'${relationshipType}'], kb_relationship(${relationshipType}, From, To), Rels)`,
149
+ );
150
+ const rows = relResult.bindings.Rels
151
+ ? parseTriples(relResult.bindings.Rels)
152
+ : [];
153
+ for (const [from, to, type] of rows) {
154
+ const current = relationships.get(from) ?? [];
155
+ current.push({ from, to, type });
156
+ relationships.set(from, current);
157
+ }
158
+ }
159
+
160
+ return entities.map((entity) => {
161
+ const result: ExtractionResult = {
162
+ entity: toExtractedEntity(entity),
163
+ relationships: relationships.get(stringField(entity, "id")) ?? [],
164
+ };
165
+ const sourceFile = sourceFileFor(entity);
166
+ if (sourceFile !== undefined) {
167
+ result.sourceFile = sourceFile;
168
+ }
169
+ return result;
170
+ });
171
+ }
172
+
173
+ function capDiagnostics(
174
+ diagnostics: readonly QualityDiagnostic[],
175
+ maxDiagnostics: number | undefined,
176
+ ): readonly QualityDiagnostic[] {
177
+ return maxDiagnostics !== undefined && maxDiagnostics >= 0
178
+ ? diagnostics.slice(0, maxDiagnostics)
179
+ : diagnostics;
180
+ }
181
+
182
+ export async function collectFullKbQualityDiagnostics(
183
+ options: FullKbQualityDiagnosticsOptions,
184
+ ): Promise<readonly QualityDiagnostic[]> {
185
+ const manifestResults = await loadKbExtractionResults(options.prolog);
186
+ const coverageDepthDiagnostics =
187
+ createCoverageDepthQualityDiagnostics(manifestResults);
188
+ const diagnostics = [
189
+ ...createRequirementQualityDiagnostics({
190
+ manifestResults,
191
+ ...(options.hardViolationEntityIds !== undefined
192
+ ? { hardViolationEntityIds: options.hardViolationEntityIds }
193
+ : {}),
194
+ }),
195
+ ...coverageDepthDiagnostics,
196
+ ...createSymbolQualityDiagnostics({
197
+ manifestResults,
198
+ symbolsByFile: new Map(),
199
+ }),
200
+ ];
201
+
202
+ return capDiagnostics(diagnostics, options.maxDiagnostics);
203
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { extractFromManifest } from "../../extractors/manifest.js";
4
+ import type { ExtractionResult } from "../../extractors/markdown.js";
5
+ import type { ManifestLookup } from "../../traceability/symbol-extract.js";
6
+ import { loadConfig } from "../../utils/config.js";
7
+ import { isTraceabilityRelationshipType } from "../symbol-granularity.js";
8
+ import { normalizeSourceFile } from "./source-changes.js";
9
+ import type { TraceabilityRelationship } from "./types.js";
10
+
11
+ function toTraceabilityRelationships(
12
+ relationships: ExtractionResult["relationships"],
13
+ ): TraceabilityRelationship[] {
14
+ return relationships
15
+ .filter((relationship) => isTraceabilityRelationshipType(relationship.type))
16
+ .map((relationship) => ({ type: relationship.type, to: relationship.to }));
17
+ }
18
+
19
+ export function readImpactManifestResults(
20
+ workspaceRoot: string,
21
+ ): readonly ExtractionResult[] {
22
+ const config = loadConfig(workspaceRoot);
23
+ const symbolsPath = config.paths.symbols;
24
+ if (!symbolsPath) return [];
25
+
26
+ const absolutePath = path.isAbsolute(symbolsPath)
27
+ ? symbolsPath
28
+ : path.resolve(workspaceRoot, symbolsPath);
29
+ if (!existsSync(absolutePath)) return [];
30
+
31
+ return extractFromManifest(absolutePath).map((result) => ({
32
+ entity: {
33
+ ...result.entity,
34
+ source: normalizeSourceFile(workspaceRoot, result.entity.source),
35
+ },
36
+ relationships: result.relationships,
37
+ ...(result.sourceFile !== undefined
38
+ ? { sourceFile: normalizeSourceFile(workspaceRoot, result.sourceFile) }
39
+ : {}),
40
+ }));
41
+ }
42
+
43
+ export function createImpactManifestLookup(
44
+ manifestResults: readonly ExtractionResult[],
45
+ ): ManifestLookup {
46
+ const manifestLookup: ManifestLookup = new Map();
47
+
48
+ for (const result of manifestResults) {
49
+ const sourceFile = result.sourceFile ?? result.entity.source;
50
+ manifestLookup.set(`${sourceFile}:${result.entity.title}`, {
51
+ id: result.entity.id,
52
+ relationships: toTraceabilityRelationships(result.relationships),
53
+ });
54
+ }
55
+
56
+ return manifestLookup;
57
+ }