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.
- package/dist/cli.js +1 -0
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +117 -82
- package/dist/commands/discovery-shared.d.ts +1 -0
- package/dist/commands/discovery-shared.d.ts.map +1 -1
- package/dist/commands/discovery-shared.js +2 -191
- package/dist/commands/discovery-table.d.ts +3 -0
- package/dist/commands/discovery-table.d.ts.map +1 -0
- package/dist/commands/discovery-table.js +204 -0
- package/dist/commands/sync/discovery.d.ts.map +1 -1
- package/dist/commands/sync/discovery.js +8 -1
- package/dist/commands/sync/extraction.d.ts +6 -1
- package/dist/commands/sync/extraction.d.ts.map +1 -1
- package/dist/commands/sync/extraction.js +7 -3
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +29 -1
- package/dist/extractors/manifest.d.ts.map +1 -1
- package/dist/extractors/manifest.js +18 -0
- package/dist/extractors/markdown.d.ts +6 -0
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/symbols-coordinator.d.ts +1 -1
- package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
- package/dist/extractors/symbols-ts.js +62 -0
- package/dist/public/impact/analyzer.d.ts +3 -0
- package/dist/public/impact/analyzer.d.ts.map +1 -0
- package/dist/public/impact/analyzer.js +54 -0
- package/dist/public/impact/coverage-depth-quality.d.ts +4 -0
- package/dist/public/impact/coverage-depth-quality.d.ts.map +1 -0
- package/dist/public/impact/coverage-depth-quality.js +179 -0
- package/dist/public/impact/diagnostics.d.ts +7 -0
- package/dist/public/impact/diagnostics.d.ts.map +1 -0
- package/dist/public/impact/diagnostics.js +122 -0
- package/dist/public/impact/diff-meaning.d.ts +2 -0
- package/dist/public/impact/diff-meaning.d.ts.map +1 -0
- package/dist/public/impact/diff-meaning.js +101 -0
- package/dist/public/impact/full-kb-quality.d.ts +10 -0
- package/dist/public/impact/full-kb-quality.d.ts.map +1 -0
- package/dist/public/impact/full-kb-quality.js +158 -0
- package/dist/public/impact/manifest.d.ts +5 -0
- package/dist/public/impact/manifest.d.ts.map +1 -0
- package/dist/public/impact/manifest.js +43 -0
- package/dist/public/impact/requirement-quality.d.ts +4 -0
- package/dist/public/impact/requirement-quality.d.ts.map +1 -0
- package/dist/public/impact/requirement-quality.js +216 -0
- package/dist/public/impact/source-changes.d.ts +5 -0
- package/dist/public/impact/source-changes.d.ts.map +1 -0
- package/dist/public/impact/source-changes.js +129 -0
- package/dist/public/impact/summaries.d.ts +6 -0
- package/dist/public/impact/summaries.d.ts.map +1 -0
- package/dist/public/impact/summaries.js +68 -0
- package/dist/public/impact/symbol-quality-model.d.ts +21 -0
- package/dist/public/impact/symbol-quality-model.d.ts.map +1 -0
- package/dist/public/impact/symbol-quality-model.js +116 -0
- package/dist/public/impact/symbol-quality.d.ts +4 -0
- package/dist/public/impact/symbol-quality.d.ts.map +1 -0
- package/dist/public/impact/symbol-quality.js +159 -0
- package/dist/public/impact/types.d.ts +79 -0
- package/dist/public/impact/types.d.ts.map +1 -0
- package/dist/public/impact/types.js +1 -0
- package/dist/public/impact-diagnostics.d.ts +7 -0
- package/dist/public/impact-diagnostics.d.ts.map +1 -0
- package/dist/public/impact-diagnostics.js +5 -0
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +21 -0
- package/dist/public/skills/kibi-usage/SKILL.md +13 -5
- package/dist/public/skills/kibi-usage/resources/fact-lanes.md +5 -5
- package/dist/public/skills/kibi-usage/resources/relationship-directions.md +30 -0
- package/dist/public/skills/kibi-usage/resources/workflows.md +14 -0
- package/dist/public/symbol-granularity.d.ts +1 -1
- package/dist/public/symbol-granularity.d.ts.map +1 -1
- package/dist/public/symbol-granularity.js +2 -0
- package/dist/traceability/evidence-model.d.ts +1 -1
- package/dist/traceability/evidence-model.d.ts.map +1 -1
- package/dist/traceability/evidence-model.js +1 -1
- package/dist/traceability/staged-diagnostics.d.ts +6 -6
- package/dist/traceability/staged-diagnostics.d.ts.map +1 -1
- package/dist/traceability/staged-diagnostics.js +12 -6
- package/dist/traceability/staged-impact-contract.js +1 -1
- package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
- package/dist/traceability/staged-symbols-manifest.js +23 -1
- package/dist/traceability/symbol-extract.d.ts.map +1 -1
- package/dist/traceability/symbol-extract.js +41 -0
- package/dist/types/entities.d.ts +5 -1
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/utils/prolog-query-plan-safety.d.ts +8 -0
- package/dist/utils/prolog-query-plan-safety.d.ts.map +1 -0
- package/dist/utils/prolog-query-plan-safety.js +47 -0
- package/dist/utils/rule-registry.d.ts.map +1 -1
- package/dist/utils/rule-registry.js +12 -0
- package/package.json +6 -2
- package/schema/entities.pl +4 -0
- package/src/public/impact/analyzer.ts +90 -0
- package/src/public/impact/coverage-depth-quality.ts +268 -0
- package/src/public/impact/diagnostics.ts +192 -0
- package/src/public/impact/diff-meaning.ts +121 -0
- package/src/public/impact/full-kb-quality.ts +203 -0
- package/src/public/impact/manifest.ts +57 -0
- package/src/public/impact/requirement-quality.ts +282 -0
- package/src/public/impact/source-changes.ts +180 -0
- package/src/public/impact/summaries.ts +100 -0
- package/src/public/impact/symbol-quality-model.ts +171 -0
- package/src/public/impact/symbol-quality.ts +207 -0
- package/src/public/impact/types.ts +99 -0
- package/src/public/impact-diagnostics.ts +22 -0
- package/src/public/schemas/entity.ts +23 -0
- package/src/public/skills/kibi-usage/SKILL.md +13 -5
- package/src/public/skills/kibi-usage/resources/fact-lanes.md +5 -5
- package/src/public/skills/kibi-usage/resources/relationship-directions.md +30 -0
- package/src/public/skills/kibi-usage/resources/workflows.md +14 -0
- 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 `
|
|
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:
|
|
119
|
-
|
|
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:
|
|
126
|
-
|
|
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:
|
|
52
|
-
|
|
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:
|
|
61
|
-
|
|
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
|