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.
Files changed (59) hide show
  1. package/dist/cli.js +62 -0
  2. package/dist/commands/aggregated-checks.js +4 -6
  3. package/dist/commands/check.d.ts.map +1 -1
  4. package/dist/commands/check.js +26 -24
  5. package/dist/commands/coverage.d.ts +12 -0
  6. package/dist/commands/coverage.d.ts.map +1 -0
  7. package/dist/commands/coverage.js +24 -0
  8. package/dist/commands/discovery-shared.d.ts +11 -0
  9. package/dist/commands/discovery-shared.d.ts.map +1 -0
  10. package/dist/commands/discovery-shared.js +280 -0
  11. package/dist/commands/doctor.js +8 -8
  12. package/dist/commands/gaps.d.ts +12 -0
  13. package/dist/commands/gaps.d.ts.map +1 -0
  14. package/dist/commands/gaps.js +28 -0
  15. package/dist/commands/graph.d.ts +13 -0
  16. package/dist/commands/graph.d.ts.map +1 -0
  17. package/dist/commands/graph.js +35 -0
  18. package/dist/commands/query.d.ts.map +1 -1
  19. package/dist/commands/query.js +2 -8
  20. package/dist/commands/search.d.ts +9 -0
  21. package/dist/commands/search.d.ts.map +1 -0
  22. package/dist/commands/search.js +38 -0
  23. package/dist/commands/status.d.ts +6 -0
  24. package/dist/commands/status.d.ts.map +1 -0
  25. package/dist/commands/status.js +9 -0
  26. package/dist/commands/sync.d.ts.map +1 -1
  27. package/dist/commands/sync.js +0 -15
  28. package/dist/diagnostics.d.ts.map +1 -1
  29. package/dist/diagnostics.js +0 -2
  30. package/dist/extractors/manifest.d.ts +2 -0
  31. package/dist/extractors/manifest.d.ts.map +1 -1
  32. package/dist/extractors/manifest.js +1 -0
  33. package/dist/extractors/markdown.d.ts.map +1 -1
  34. package/dist/extractors/markdown.js +9 -1
  35. package/dist/public/extractors/symbols-coordinator.d.ts.map +1 -1
  36. package/dist/public/extractors/symbols-coordinator.js +1 -2
  37. package/dist/public/prolog/index.d.ts.map +1 -1
  38. package/dist/public/prolog/index.js +1 -2
  39. package/dist/public/schemas/entity.d.ts.map +1 -1
  40. package/dist/public/schemas/entity.js +1 -3
  41. package/dist/public/schemas/relationship.d.ts.map +1 -1
  42. package/dist/public/schemas/relationship.js +1 -3
  43. package/dist/search-ranking.d.ts +9 -0
  44. package/dist/search-ranking.d.ts.map +1 -0
  45. package/dist/search-ranking.js +143 -0
  46. package/dist/traceability/git-staged.d.ts.map +1 -1
  47. package/dist/traceability/git-staged.js +33 -7
  48. package/dist/traceability/symbol-extract.d.ts +6 -1
  49. package/dist/traceability/symbol-extract.d.ts.map +1 -1
  50. package/dist/traceability/symbol-extract.js +62 -34
  51. package/dist/traceability/temp-kb.d.ts.map +1 -1
  52. package/dist/traceability/temp-kb.js +4 -3
  53. package/dist/traceability/validate.d.ts.map +1 -1
  54. package/dist/traceability/validate.js +8 -7
  55. package/package.json +10 -2
  56. package/src/public/extractors/symbols-coordinator.ts +1 -2
  57. package/src/public/prolog/index.ts +1 -2
  58. package/src/public/schemas/entity.ts +1 -3
  59. 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;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,GACZ,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAS5C;AAsCD;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,SAAS,UAAQ,GAChB,SAAS,EAAE,CAoBb;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,GAAE,MAAiB,GAAG,UAAU,EAAE,CA4FpE;AAED,eAAe,cAAc,CAAC"}
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
- return entries.map((entry) => {
21
- const cols = entry.split("\t");
22
- const status = cols[0];
23
- const parts = cols.slice(1);
24
- return { status, parts };
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) && !isEntityMarkdown(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 declare function extractSymbolsFromStagedFile(stagedFile: StagedFile): ExtractedSymbol[];
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;AA0DD,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,UAAU,GACrB,eAAe,EAAE,CAuLnB"}
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 ((m = regex.exec(text))) {
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 + "|" + stagedFile.path);
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 + "::staged", content, {
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
- "\n" +
90
- fn
91
- .getJsDocs()
92
- .map((d) => d.getFullText())
93
- .join("\n"));
94
- const id = resolveSymbolId(stagedFile.path, name);
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
- "\n" +
121
- cls
122
- .getJsDocs()
123
- .map((d) => d.getFullText())
124
- .join("\n"));
125
- const id = resolveSymbolId(stagedFile.path, name);
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
- // attempt to read manifest entries for explicit id (best-effort)
214
- // extractFromManifest expects a file path; if manifest not present it will throw — catch it
215
- const ents = extractFromManifest(filePath);
216
- for (const e of ents) {
217
- if (e.entity.title === name)
218
- return e.entity.id;
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;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,MAAM,CAkBrE;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BlE"}
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;AAgHD,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
+ {"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(v) {
1
+ function unquoteAtom(value) {
2
2
  // remove surrounding single quotes and unescape doubled quotes
3
- v = v.trim();
4
- if (v.startsWith("'") && v.endsWith("'")) {
5
- v = v.slice(1, -1).replace(/''/g, "'");
3
+ const trimmed = value.trim();
4
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
5
+ return trimmed.slice(1, -1).replace(/''/g, "'");
6
6
  }
7
- return v;
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["Rows"] ?? "[]";
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.2.7",
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.1.10",
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,7 +14,6 @@
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 PrologProcess
20
19
  export { PrologProcess } from "../../prolog.js";
@@ -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",