kibi-cli 0.2.7 → 0.3.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 +62 -0
- package/dist/commands/aggregated-checks.js +4 -6
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +26 -24
- package/dist/commands/coverage.d.ts +12 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +24 -0
- package/dist/commands/discovery-shared.d.ts +11 -0
- package/dist/commands/discovery-shared.d.ts.map +1 -0
- package/dist/commands/discovery-shared.js +280 -0
- package/dist/commands/doctor.js +8 -8
- package/dist/commands/gaps.d.ts +12 -0
- package/dist/commands/gaps.d.ts.map +1 -0
- package/dist/commands/gaps.js +28 -0
- package/dist/commands/graph.d.ts +13 -0
- package/dist/commands/graph.d.ts.map +1 -0
- package/dist/commands/graph.js +35 -0
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +2 -8
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +38 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +9 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +0 -15
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +0 -2
- package/dist/extractors/manifest.d.ts +2 -0
- package/dist/extractors/manifest.d.ts.map +1 -1
- package/dist/extractors/manifest.js +1 -0
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +9 -1
- package/dist/public/extractors/symbols-coordinator.d.ts.map +1 -1
- package/dist/public/extractors/symbols-coordinator.js +1 -2
- package/dist/public/prolog/index.d.ts.map +1 -1
- package/dist/public/prolog/index.js +1 -2
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +1 -3
- package/dist/public/schemas/relationship.d.ts.map +1 -1
- package/dist/public/schemas/relationship.js +1 -3
- package/dist/search-ranking.d.ts +9 -0
- package/dist/search-ranking.d.ts.map +1 -0
- package/dist/search-ranking.js +143 -0
- package/dist/traceability/git-staged.d.ts.map +1 -1
- package/dist/traceability/git-staged.js +33 -7
- package/dist/traceability/symbol-extract.d.ts +6 -1
- package/dist/traceability/symbol-extract.d.ts.map +1 -1
- package/dist/traceability/symbol-extract.js +62 -34
- package/dist/traceability/temp-kb.d.ts.map +1 -1
- package/dist/traceability/temp-kb.js +4 -3
- package/dist/traceability/validate.d.ts.map +1 -1
- package/dist/traceability/validate.js +8 -7
- package/package.json +10 -2
- package/src/public/extractors/symbols-coordinator.ts +1 -2
- package/src/public/prolog/index.ts +1 -2
- package/src/public/schemas/entity.ts +1 -3
- package/src/public/schemas/relationship.ts +1 -3
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
// implements REQ-mcp-search-discovery, REQ-002, REQ-003
|
|
5
|
+
export async function rankEntities(entities, query, workspaceRoot) {
|
|
6
|
+
const matches = [];
|
|
7
|
+
for (const entity of entities) {
|
|
8
|
+
const match = await rankEntity(entity, query, workspaceRoot);
|
|
9
|
+
if (match) {
|
|
10
|
+
matches.push(match);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
matches.sort((left, right) => {
|
|
14
|
+
if (right.score !== left.score) {
|
|
15
|
+
return right.score - left.score;
|
|
16
|
+
}
|
|
17
|
+
const leftType = String(left.entity.type ?? "");
|
|
18
|
+
const rightType = String(right.entity.type ?? "");
|
|
19
|
+
if (leftType !== rightType) {
|
|
20
|
+
return leftType.localeCompare(rightType);
|
|
21
|
+
}
|
|
22
|
+
return String(left.entity.id ?? "").localeCompare(String(right.entity.id ?? ""));
|
|
23
|
+
});
|
|
24
|
+
return matches;
|
|
25
|
+
}
|
|
26
|
+
async function rankEntity(entity, query, workspaceRoot) {
|
|
27
|
+
const normalizedQuery = normalize(query);
|
|
28
|
+
const tokens = normalizedQuery.split(/\s+/).filter(Boolean);
|
|
29
|
+
const reasons = [];
|
|
30
|
+
let score = 0;
|
|
31
|
+
const id = String(entity.id ?? "");
|
|
32
|
+
const title = String(entity.title ?? "");
|
|
33
|
+
const type = String(entity.type ?? "");
|
|
34
|
+
const source = String(entity.source ?? "");
|
|
35
|
+
const owner = String(entity.owner ?? "");
|
|
36
|
+
const priority = String(entity.priority ?? "");
|
|
37
|
+
const severity = String(entity.severity ?? "");
|
|
38
|
+
const tags = Array.isArray(entity.tags)
|
|
39
|
+
? entity.tags.map((tag) => String(tag))
|
|
40
|
+
: [];
|
|
41
|
+
const normalizedTitle = normalize(title);
|
|
42
|
+
const normalizedId = normalize(id);
|
|
43
|
+
if (normalizedTitle === normalizedQuery) {
|
|
44
|
+
score += 100;
|
|
45
|
+
reasons.push("exact title match");
|
|
46
|
+
}
|
|
47
|
+
else if (normalizedTitle.includes(normalizedQuery)) {
|
|
48
|
+
score += 60;
|
|
49
|
+
reasons.push("title phrase match");
|
|
50
|
+
}
|
|
51
|
+
if (normalizedId === normalizedQuery) {
|
|
52
|
+
score += 90;
|
|
53
|
+
reasons.push("exact ID match");
|
|
54
|
+
}
|
|
55
|
+
else if (normalizedId.includes(normalizedQuery)) {
|
|
56
|
+
score += 55;
|
|
57
|
+
reasons.push("ID match");
|
|
58
|
+
}
|
|
59
|
+
const metadataFields = [type, source, owner, priority, severity];
|
|
60
|
+
const metadataMatched = metadataFields.some((field) => normalize(field).includes(normalizedQuery));
|
|
61
|
+
if (metadataMatched) {
|
|
62
|
+
score += 20;
|
|
63
|
+
reasons.push("metadata match");
|
|
64
|
+
}
|
|
65
|
+
const matchingTags = tags.filter((tag) => normalize(tag).includes(normalizedQuery));
|
|
66
|
+
if (matchingTags.length > 0) {
|
|
67
|
+
score += 30;
|
|
68
|
+
reasons.push("tag match");
|
|
69
|
+
}
|
|
70
|
+
const titleTokenMatches = countTokenMatches(normalizedTitle, tokens);
|
|
71
|
+
if (titleTokenMatches > 0) {
|
|
72
|
+
score += titleTokenMatches * 8;
|
|
73
|
+
reasons.push("title token coverage");
|
|
74
|
+
}
|
|
75
|
+
const bodyText = await loadMarkdownBody(source, workspaceRoot);
|
|
76
|
+
let snippet;
|
|
77
|
+
if (bodyText) {
|
|
78
|
+
const normalizedBody = normalize(bodyText);
|
|
79
|
+
if (normalizedBody.includes(normalizedQuery)) {
|
|
80
|
+
score += 15;
|
|
81
|
+
reasons.push("markdown body match");
|
|
82
|
+
snippet = buildSnippet(bodyText, query);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const bodyTokenMatches = countTokenMatches(normalizedBody, tokens);
|
|
86
|
+
if (bodyTokenMatches > 0) {
|
|
87
|
+
score += bodyTokenMatches * 3;
|
|
88
|
+
reasons.push("markdown body token coverage");
|
|
89
|
+
snippet = buildSnippet(bodyText, query);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (score === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
entity,
|
|
98
|
+
score,
|
|
99
|
+
reasons: Array.from(new Set(reasons)),
|
|
100
|
+
snippet,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export async function loadMarkdownBody(source, workspaceRoot) {
|
|
104
|
+
if (!source) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const normalizedSource = source.split("#", 1)[0]?.trim() ?? "";
|
|
108
|
+
if (!normalizedSource.endsWith(".md")) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// Resolve to absolute path; relative paths are resolved against workspaceRoot.
|
|
112
|
+
const resolved = path.resolve(path.isAbsolute(normalizedSource) ? normalizedSource : path.join(workspaceRoot, normalizedSource));
|
|
113
|
+
// Reject paths that escape the workspace root to prevent path traversal.
|
|
114
|
+
const normalizedRoot = path.resolve(workspaceRoot);
|
|
115
|
+
if (!resolved.startsWith(normalizedRoot + path.sep)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const fileContent = await fs.readFile(resolved, "utf8");
|
|
120
|
+
return matter(fileContent).content;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function normalize(value) {
|
|
127
|
+
return value.trim().toLowerCase();
|
|
128
|
+
}
|
|
129
|
+
function countTokenMatches(haystack, tokens) {
|
|
130
|
+
return tokens.filter((token) => haystack.includes(token)).length;
|
|
131
|
+
}
|
|
132
|
+
function buildSnippet(bodyText, query) {
|
|
133
|
+
const lines = bodyText
|
|
134
|
+
.split(/\r?\n/)
|
|
135
|
+
.map((line) => line.trim())
|
|
136
|
+
.filter(Boolean);
|
|
137
|
+
const normalizedQuery = normalize(query);
|
|
138
|
+
const matchedLine = lines.find((line) => normalize(line).includes(normalizedQuery)) ?? lines[0];
|
|
139
|
+
if (!matchedLine) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
return matchedLine.length > 160 ? `${matchedLine.slice(0, 157)}...` : matchedLine;
|
|
143
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-staged.d.ts","sourceRoot":"","sources":["../../src/traceability/git-staged.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,MAAM,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,KAAK,MAAM,CAAC;AAalE;;GAEG;
|
|
1
|
+
{"version":3,"file":"git-staged.d.ts","sourceRoot":"","sources":["../../src/traceability/git-staged.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,MAAM,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,KAAK,MAAM,CAAC;AAalE;;GAEG;AAEH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,GACZ,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAwB5C;AAiDD;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,SAAS,UAAQ,GAChB,SAAS,EAAE,CAoBb;AAED;;GAEG;AAEH,wBAAgB,cAAc,CAAC,IAAI,GAAE,MAAiB,GAAG,UAAU,EAAE,CA6FpE;AAED,eAAe,cAAc,CAAC"}
|
|
@@ -13,16 +13,28 @@ function runGit(cmd, exec) {
|
|
|
13
13
|
/**
|
|
14
14
|
* Parse null-separated name-status output from git
|
|
15
15
|
*/
|
|
16
|
+
// implements REQ-014
|
|
16
17
|
export function parseNameStatusNull(input) {
|
|
17
18
|
if (!input)
|
|
18
19
|
return [];
|
|
19
20
|
const entries = input.split("\0").filter(Boolean);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const rows = [];
|
|
22
|
+
for (let i = 0; i < entries.length;) {
|
|
23
|
+
const entry = entries[i] ?? "";
|
|
24
|
+
if (entry.includes("\t")) {
|
|
25
|
+
const cols = entry.split("\t");
|
|
26
|
+
rows.push({ status: cols[0] ?? "", parts: cols.slice(1) });
|
|
27
|
+
i += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const status = entry;
|
|
31
|
+
const isRenameOrCopy = /^[RC]\d*$/.test(status);
|
|
32
|
+
const partCount = isRenameOrCopy ? 2 : 1;
|
|
33
|
+
const parts = entries.slice(i + 1, i + 1 + partCount);
|
|
34
|
+
rows.push({ status, parts });
|
|
35
|
+
i += 1 + partCount;
|
|
36
|
+
}
|
|
37
|
+
return rows;
|
|
26
38
|
}
|
|
27
39
|
const SUPPORTED_EXT = new Set([
|
|
28
40
|
".ts",
|
|
@@ -34,6 +46,7 @@ const SUPPORTED_EXT = new Set([
|
|
|
34
46
|
".mjs",
|
|
35
47
|
".cjs",
|
|
36
48
|
]);
|
|
49
|
+
const SUPPORTED_MANIFEST = new Set(["symbols.yaml", "symbols.yml"]);
|
|
37
50
|
const ENTITY_MARKDOWN_DIRS = ["/requirements/", "/scenarios/", "/tests/"];
|
|
38
51
|
function shouldLogTraceDebug() {
|
|
39
52
|
return Boolean(process.env.KIBI_TRACE || process.env.KIBI_DEBUG);
|
|
@@ -57,6 +70,16 @@ function isEntityMarkdown(p) {
|
|
|
57
70
|
}
|
|
58
71
|
return false;
|
|
59
72
|
}
|
|
73
|
+
function isManifestFile(p) {
|
|
74
|
+
const base = p.split(/[\/]/).pop();
|
|
75
|
+
if (!base)
|
|
76
|
+
return false;
|
|
77
|
+
for (const name of SUPPORTED_MANIFEST) {
|
|
78
|
+
if (base === name)
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
60
83
|
/**
|
|
61
84
|
* Parse unified diff hunks (new-file coordinates) from git diff output
|
|
62
85
|
*/
|
|
@@ -86,6 +109,7 @@ export function parseHunksFromDiff(diffText, isNewFile = false) {
|
|
|
86
109
|
/**
|
|
87
110
|
* Get staged files with statuses, hunks and content.
|
|
88
111
|
*/
|
|
112
|
+
// implements REQ-014
|
|
89
113
|
export function getStagedFiles(exec = execSync) {
|
|
90
114
|
// 1. get staged name-status -z
|
|
91
115
|
let nameStatus;
|
|
@@ -115,7 +139,9 @@ export function getStagedFiles(exec = execSync) {
|
|
|
115
139
|
path = entry.parts[1];
|
|
116
140
|
}
|
|
117
141
|
}
|
|
118
|
-
if (!hasSupportedExt(path) &&
|
|
142
|
+
if (!hasSupportedExt(path) &&
|
|
143
|
+
!isEntityMarkdown(path) &&
|
|
144
|
+
!isManifestFile(path)) {
|
|
119
145
|
if (shouldLogTraceDebug()) {
|
|
120
146
|
console.debug(`Skipping unsupported extension: ${path}`);
|
|
121
147
|
}
|
|
@@ -11,5 +11,10 @@ export interface ExtractedSymbol {
|
|
|
11
11
|
hunkRanges: HunkRange[];
|
|
12
12
|
reqLinks: string[];
|
|
13
13
|
}
|
|
14
|
-
export
|
|
14
|
+
export interface ManifestLookupEntry {
|
|
15
|
+
id: string;
|
|
16
|
+
links?: string[];
|
|
17
|
+
}
|
|
18
|
+
export type ManifestLookup = Map<string, ManifestLookupEntry>;
|
|
19
|
+
export declare function extractSymbolsFromStagedFile(stagedFile: StagedFile, manifestLookup?: ManifestLookup): ExtractedSymbol[];
|
|
15
20
|
//# sourceMappingURL=symbol-extract.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"symbol-extract.d.ts","sourceRoot":"","sources":["../../src/traceability/symbol-extract.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7D,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"symbol-extract.d.ts","sourceRoot":"","sources":["../../src/traceability/symbol-extract.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7D,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AA4D9D,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,UAAU,EACtB,cAAc,CAAC,EAAE,cAAc,GAC9B,eAAe,EAAE,CA+MnB"}
|
|
@@ -28,7 +28,10 @@ function parseReqDirectives(text) {
|
|
|
28
28
|
const regex = new RegExp(`implements\\s*:?\\s*(${REQ_ID}(?:\\s*,\\s*${REQ_ID})*)\\s*$`, "gim");
|
|
29
29
|
const reqs = new Set();
|
|
30
30
|
let m;
|
|
31
|
-
while (
|
|
31
|
+
while (true) {
|
|
32
|
+
m = regex.exec(text);
|
|
33
|
+
if (!m)
|
|
34
|
+
break;
|
|
32
35
|
const list = m[1];
|
|
33
36
|
for (const part of list.split(/[,\s]+/)) {
|
|
34
37
|
const p = part.trim();
|
|
@@ -42,9 +45,9 @@ function parseReqDirectives(text) {
|
|
|
42
45
|
function rangesIntersect(aStart, aEnd, bStart, bEnd) {
|
|
43
46
|
return aStart <= bEnd && bStart <= aEnd;
|
|
44
47
|
}
|
|
45
|
-
export function extractSymbolsFromStagedFile(stagedFile) {
|
|
48
|
+
export function extractSymbolsFromStagedFile(stagedFile, manifestLookup) {
|
|
46
49
|
const content = stagedFile.content ?? "";
|
|
47
|
-
const sha = computeContentSha(content
|
|
50
|
+
const sha = computeContentSha(`${content}|${stagedFile.path}`);
|
|
48
51
|
// TTL cache lookup
|
|
49
52
|
const now = Date.now();
|
|
50
53
|
let cached = sourceFileCache.get(sha);
|
|
@@ -52,7 +55,7 @@ export function extractSymbolsFromStagedFile(stagedFile) {
|
|
|
52
55
|
// create or recreate SourceFile in project (in-memory)
|
|
53
56
|
try {
|
|
54
57
|
const scriptKind = chooseScriptKind(stagedFile.path);
|
|
55
|
-
const sf = project.createSourceFile(stagedFile.path
|
|
58
|
+
const sf = project.createSourceFile(`${stagedFile.path}::staged`, content, {
|
|
56
59
|
overwrite: true,
|
|
57
60
|
scriptKind,
|
|
58
61
|
});
|
|
@@ -85,13 +88,13 @@ export function extractSymbolsFromStagedFile(stagedFile) {
|
|
|
85
88
|
const start = nameNode ? nameNode.getStart() : fn.getStart();
|
|
86
89
|
const end = fn.getEnd();
|
|
87
90
|
const span = getSpan(start, end);
|
|
88
|
-
const reqLinks = parseReqDirectives(fn.getFullText()
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
91
|
+
const reqLinks = parseReqDirectives(`${fn.getFullText()}\n${fn
|
|
92
|
+
.getJsDocs()
|
|
93
|
+
.map((d) => d.getFullText())
|
|
94
|
+
.join("\n")}`);
|
|
95
|
+
const { id, reqLinks: manifestLinks } = resolveSymbolId(stagedFile.path, name, manifestLookup);
|
|
96
|
+
// Merge manifest links with inline directive links (manifest links as fallback)
|
|
97
|
+
const mergedReqLinks = reqLinks.length > 0 ? reqLinks : (manifestLinks ?? []);
|
|
95
98
|
results.push({
|
|
96
99
|
id,
|
|
97
100
|
name,
|
|
@@ -102,7 +105,7 @@ export function extractSymbolsFromStagedFile(stagedFile) {
|
|
|
102
105
|
endLine: span.endLine,
|
|
103
106
|
},
|
|
104
107
|
hunkRanges: intersectingHunks(span.startLine, span.endLine, stagedFile.hunkRanges),
|
|
105
|
-
reqLinks,
|
|
108
|
+
reqLinks: mergedReqLinks,
|
|
106
109
|
});
|
|
107
110
|
}
|
|
108
111
|
catch { }
|
|
@@ -116,13 +119,13 @@ export function extractSymbolsFromStagedFile(stagedFile) {
|
|
|
116
119
|
const start = cls.getNameNode()?.getStart() ?? cls.getStart();
|
|
117
120
|
const end = cls.getEnd();
|
|
118
121
|
const span = getSpan(start, end);
|
|
119
|
-
const reqLinks = parseReqDirectives(cls.getText()
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
122
|
+
const reqLinks = parseReqDirectives(`${cls.getText()}\n${cls
|
|
123
|
+
.getJsDocs()
|
|
124
|
+
.map((d) => d.getFullText())
|
|
125
|
+
.join("\n")}`);
|
|
126
|
+
const { id, reqLinks: manifestLinks } = resolveSymbolId(stagedFile.path, name, manifestLookup);
|
|
127
|
+
// Merge manifest links with inline directive links (manifest links as fallback)
|
|
128
|
+
const mergedReqLinks = reqLinks.length > 0 ? reqLinks : (manifestLinks ?? []);
|
|
126
129
|
results.push({
|
|
127
130
|
id,
|
|
128
131
|
name,
|
|
@@ -133,7 +136,7 @@ export function extractSymbolsFromStagedFile(stagedFile) {
|
|
|
133
136
|
endLine: span.endLine,
|
|
134
137
|
},
|
|
135
138
|
hunkRanges: intersectingHunks(span.startLine, span.endLine, stagedFile.hunkRanges),
|
|
136
|
-
reqLinks,
|
|
139
|
+
reqLinks: mergedReqLinks,
|
|
137
140
|
});
|
|
138
141
|
}
|
|
139
142
|
catch { }
|
|
@@ -148,7 +151,9 @@ export function extractSymbolsFromStagedFile(stagedFile) {
|
|
|
148
151
|
const end = en.getEnd();
|
|
149
152
|
const span = getSpan(start, end);
|
|
150
153
|
const reqLinks = parseReqDirectives(en.getText());
|
|
151
|
-
const id = resolveSymbolId(stagedFile.path, name);
|
|
154
|
+
const { id, reqLinks: manifestLinks } = resolveSymbolId(stagedFile.path, name, manifestLookup);
|
|
155
|
+
// Merge manifest links with inline directive links (manifest links as fallback)
|
|
156
|
+
const mergedReqLinks = reqLinks.length > 0 ? reqLinks : (manifestLinks ?? []);
|
|
152
157
|
results.push({
|
|
153
158
|
id,
|
|
154
159
|
name,
|
|
@@ -159,7 +164,7 @@ export function extractSymbolsFromStagedFile(stagedFile) {
|
|
|
159
164
|
endLine: span.endLine,
|
|
160
165
|
},
|
|
161
166
|
hunkRanges: intersectingHunks(span.startLine, span.endLine, stagedFile.hunkRanges),
|
|
162
|
-
reqLinks,
|
|
167
|
+
reqLinks: mergedReqLinks,
|
|
163
168
|
});
|
|
164
169
|
}
|
|
165
170
|
catch { }
|
|
@@ -175,7 +180,9 @@ export function extractSymbolsFromStagedFile(stagedFile) {
|
|
|
175
180
|
const end = decl.getEnd();
|
|
176
181
|
const span = getSpan(start, end);
|
|
177
182
|
const reqLinks = parseReqDirectives(decl.getText());
|
|
178
|
-
const id = resolveSymbolId(stagedFile.path, name);
|
|
183
|
+
const { id, reqLinks: manifestLinks } = resolveSymbolId(stagedFile.path, name, manifestLookup);
|
|
184
|
+
// Merge manifest links with inline directive links (manifest links as fallback)
|
|
185
|
+
const mergedReqLinks = reqLinks.length > 0 ? reqLinks : (manifestLinks ?? []);
|
|
179
186
|
results.push({
|
|
180
187
|
id,
|
|
181
188
|
name,
|
|
@@ -186,7 +193,7 @@ export function extractSymbolsFromStagedFile(stagedFile) {
|
|
|
186
193
|
endLine: span.endLine,
|
|
187
194
|
},
|
|
188
195
|
hunkRanges: intersectingHunks(span.startLine, span.endLine, stagedFile.hunkRanges),
|
|
189
|
-
reqLinks,
|
|
196
|
+
reqLinks: mergedReqLinks,
|
|
190
197
|
});
|
|
191
198
|
}
|
|
192
199
|
catch { }
|
|
@@ -208,21 +215,42 @@ function intersectingHunks(startLine, endLine, hunks) {
|
|
|
208
215
|
}
|
|
209
216
|
return out;
|
|
210
217
|
}
|
|
211
|
-
function resolveSymbolId(filePath, name) {
|
|
218
|
+
function resolveSymbolId(filePath, name, manifestLookup) {
|
|
219
|
+
// First, check the provided manifest lookup if available
|
|
220
|
+
if (manifestLookup) {
|
|
221
|
+
// Normalize the source file path for consistent lookup
|
|
222
|
+
const normalizedSource = filePath.startsWith("/")
|
|
223
|
+
? filePath
|
|
224
|
+
: `${filePath}`;
|
|
225
|
+
const lookupKey = `${normalizedSource}:${name}`;
|
|
226
|
+
const entry = manifestLookup.get(lookupKey);
|
|
227
|
+
if (entry) {
|
|
228
|
+
return { id: entry.id, reqLinks: entry.links };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Fallback: attempt to read manifest entries from a local symbols.yaml (best-effort)
|
|
212
232
|
try {
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
233
|
+
// Try to find a symbols.yaml in the same directory as the file
|
|
234
|
+
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
235
|
+
if (dir) {
|
|
236
|
+
const manifestPath = `${dir}/symbols.yaml`;
|
|
237
|
+
const ents = extractFromManifest(manifestPath);
|
|
238
|
+
for (const e of ents) {
|
|
239
|
+
if (e.entity.title === name) {
|
|
240
|
+
// Extract requirement links from relationships
|
|
241
|
+
const reqLinks = e.relationships
|
|
242
|
+
.filter((r) => r.type === "implements" && r.to.match(/^[A-Z][A-Z0-9\-_]*$/))
|
|
243
|
+
.map((r) => r.to);
|
|
244
|
+
return { id: e.entity.id, reqLinks };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
219
247
|
}
|
|
220
248
|
}
|
|
221
249
|
catch {
|
|
222
|
-
// ignore
|
|
250
|
+
// ignore - no local manifest or parse error
|
|
223
251
|
}
|
|
224
|
-
// deterministic id: sha(file:path:name)
|
|
252
|
+
// Final fallback: deterministic id: sha(file:path:name)
|
|
225
253
|
const h = createHash("sha256");
|
|
226
254
|
h.update(`${filePath}:${name}`);
|
|
227
|
-
return h.digest("hex").slice(0, 16);
|
|
255
|
+
return { id: h.digest("hex").slice(0, 16) };
|
|
228
256
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temp-kb.d.ts","sourceRoot":"","sources":["../../src/traceability/temp-kb.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC;CACvB;AAmDD,iBAAe,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAe/D;AAED,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2C7E;
|
|
1
|
+
{"version":3,"file":"temp-kb.d.ts","sourceRoot":"","sources":["../../src/traceability/temp-kb.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC;CACvB;AAmDD,iBAAe,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAe/D;AAED,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2C7E;AAGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,MAAM,CAkBrE;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BlE"}
|
|
@@ -82,14 +82,15 @@ export async function createTempKb(baseKbPath) {
|
|
|
82
82
|
trace(`temporary KB ready at ${kbPath}`);
|
|
83
83
|
return ctx;
|
|
84
84
|
}
|
|
85
|
+
// implements REQ-014
|
|
85
86
|
export function createOverlayFacts(symbols) {
|
|
86
87
|
const lines = [];
|
|
87
88
|
for (const symbol of symbols) {
|
|
88
|
-
lines.push(`changed_symbol(${escapePrologAtom(symbol.id)}).`);
|
|
89
|
-
lines.push(`changed_symbol_loc(${escapePrologAtom(symbol.id)}, ${escapePrologAtom(symbol.location.file)}, ${symbol.location.startLine}, 0, ${escapePrologAtom(symbol.name)}).`);
|
|
89
|
+
lines.push(`kb:changed_symbol(${escapePrologAtom(symbol.id)}).`);
|
|
90
|
+
lines.push(`kb:changed_symbol_loc(${escapePrologAtom(symbol.id)}, ${escapePrologAtom(symbol.location.file)}, ${symbol.location.startLine}, 0, ${escapePrologAtom(symbol.name)}).`);
|
|
90
91
|
// Emit overlay facts for requirement links from code-comment directives.
|
|
91
92
|
for (const reqId of symbol.reqLinks) {
|
|
92
|
-
lines.push(`changed_symbol_req(${escapePrologAtom(symbol.id)}, ${escapePrologAtom(reqId)}).`);
|
|
93
|
+
lines.push(`kb:changed_symbol_req(${escapePrologAtom(symbol.id)}, ${escapePrologAtom(reqId)}).`);
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
return lines.join("\n");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/traceability/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/traceability/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAiHD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,EAAE,CAAC,CAqCtB;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,CAiBhE"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
function unquoteAtom(
|
|
1
|
+
function unquoteAtom(value) {
|
|
2
2
|
// remove surrounding single quotes and unescape doubled quotes
|
|
3
|
-
|
|
4
|
-
if (
|
|
5
|
-
|
|
3
|
+
const trimmed = value.trim();
|
|
4
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
5
|
+
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
6
6
|
}
|
|
7
|
-
return
|
|
7
|
+
return trimmed;
|
|
8
8
|
}
|
|
9
9
|
function splitTopLevelComma(s) {
|
|
10
10
|
const parts = [];
|
|
@@ -98,14 +98,15 @@ function parsePrologListOfLists(value) {
|
|
|
98
98
|
}
|
|
99
99
|
return out;
|
|
100
100
|
}
|
|
101
|
+
// implements REQ-014
|
|
101
102
|
export async function validateStagedSymbols(options) {
|
|
102
103
|
const { minLinks, prolog } = options;
|
|
103
|
-
const goal = `findall([Sym,Count,File,Line,Col,Name], changed_symbol_violation(Sym, ${minLinks}, Count, File, Line, Col, Name), Rows)`;
|
|
104
|
+
const goal = `findall([Sym,Count,File,Line,Col,Name], kb:changed_symbol_violation(Sym, ${minLinks}, Count, File, Line, Col, Name), Rows)`;
|
|
104
105
|
const res = await prolog.query(goal);
|
|
105
106
|
if (!res.success) {
|
|
106
107
|
throw new Error(`Prolog query failed: ${res.error || "unknown error"}`);
|
|
107
108
|
}
|
|
108
|
-
const rowsRaw = res.bindings
|
|
109
|
+
const rowsRaw = res.bindings.Rows ?? "[]";
|
|
109
110
|
const lists = parsePrologListOfLists(rowsRaw);
|
|
110
111
|
const violations = [];
|
|
111
112
|
for (const row of lists) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Kibi CLI for knowledge base management",
|
|
6
6
|
"engines": {
|
|
@@ -58,6 +58,10 @@
|
|
|
58
58
|
"./diagnostics": {
|
|
59
59
|
"types": "./dist/diagnostics.d.ts",
|
|
60
60
|
"default": "./dist/diagnostics.js"
|
|
61
|
+
},
|
|
62
|
+
"./search-ranking": {
|
|
63
|
+
"types": "./dist/search-ranking.d.ts",
|
|
64
|
+
"default": "./dist/search-ranking.js"
|
|
61
65
|
}
|
|
62
66
|
},
|
|
63
67
|
"types": "./dist/cli.d.ts",
|
|
@@ -68,9 +72,13 @@
|
|
|
68
72
|
"fast-glob": "^3.2.12",
|
|
69
73
|
"gray-matter": "^4.0.3",
|
|
70
74
|
"js-yaml": "^4.1.0",
|
|
71
|
-
"kibi-core": "^0.
|
|
75
|
+
"kibi-core": "^0.2.0",
|
|
72
76
|
"ts-morph": "^23.0.0"
|
|
73
77
|
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@types/node": "latest",
|
|
80
|
+
"typescript": "^5.7.0"
|
|
81
|
+
},
|
|
74
82
|
"license": "AGPL-3.0-or-later",
|
|
75
83
|
"author": "Piotr Franczyk",
|
|
76
84
|
"repository": {
|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
-
*/
|
|
17
|
+
*/
|
|
18
18
|
|
|
19
|
-
// Public re-export of symbols coordinator
|
|
20
19
|
export {
|
|
21
20
|
enrichSymbolCoordinates,
|
|
22
21
|
type ManifestSymbolEntry,
|
|
@@ -14,10 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
-
*/
|
|
17
|
+
*/
|
|
18
18
|
|
|
19
|
-
// Public export of entity schema
|
|
20
|
-
// Generated from entity.schema.json
|
|
21
19
|
const entitySchema = {
|
|
22
20
|
$id: "entity.schema.json",
|
|
23
21
|
title: "Entity",
|
|
@@ -14,10 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
-
*/
|
|
17
|
+
*/
|
|
18
18
|
|
|
19
|
-
// Public export of relationship schema
|
|
20
|
-
// Generated from relationship.schema.json
|
|
21
19
|
const relationshipSchema = {
|
|
22
20
|
$id: "relationship.schema.json",
|
|
23
21
|
title: "Relationship",
|