kibi-cli 0.2.8 → 0.4.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 (78) hide show
  1. package/dist/cli.d.ts +4 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +91 -14
  4. package/dist/commands/check.d.ts +5 -8
  5. package/dist/commands/check.d.ts.map +1 -1
  6. package/dist/commands/check.js +41 -62
  7. package/dist/commands/coverage.d.ts +12 -0
  8. package/dist/commands/coverage.d.ts.map +1 -0
  9. package/dist/commands/coverage.js +24 -0
  10. package/dist/commands/discovery-shared.d.ts +11 -0
  11. package/dist/commands/discovery-shared.d.ts.map +1 -0
  12. package/dist/commands/discovery-shared.js +281 -0
  13. package/dist/commands/doctor.d.ts +3 -1
  14. package/dist/commands/doctor.d.ts.map +1 -1
  15. package/dist/commands/doctor.js +12 -13
  16. package/dist/commands/gaps.d.ts +12 -0
  17. package/dist/commands/gaps.d.ts.map +1 -0
  18. package/dist/commands/gaps.js +28 -0
  19. package/dist/commands/graph.d.ts +13 -0
  20. package/dist/commands/graph.d.ts.map +1 -0
  21. package/dist/commands/graph.js +35 -0
  22. package/dist/commands/init.d.ts +3 -1
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +4 -3
  25. package/dist/commands/query.d.ts +3 -1
  26. package/dist/commands/query.d.ts.map +1 -1
  27. package/dist/commands/query.js +9 -20
  28. package/dist/commands/search.d.ts +9 -0
  29. package/dist/commands/search.d.ts.map +1 -0
  30. package/dist/commands/search.js +38 -0
  31. package/dist/commands/status.d.ts +6 -0
  32. package/dist/commands/status.d.ts.map +1 -0
  33. package/dist/commands/status.js +9 -0
  34. package/dist/commands/sync/persistence.d.ts.map +1 -1
  35. package/dist/commands/sync/persistence.js +79 -12
  36. package/dist/commands/sync.d.ts +4 -1
  37. package/dist/commands/sync.d.ts.map +1 -1
  38. package/dist/commands/sync.js +79 -31
  39. package/dist/extractors/markdown.d.ts +17 -0
  40. package/dist/extractors/markdown.d.ts.map +1 -1
  41. package/dist/extractors/markdown.js +104 -14
  42. package/dist/prolog/codec.d.ts +32 -5
  43. package/dist/prolog/codec.d.ts.map +1 -1
  44. package/dist/prolog/codec.js +95 -58
  45. package/dist/prolog.d.ts.map +1 -1
  46. package/dist/prolog.js +12 -2
  47. package/dist/public/check-types.d.ts +7 -0
  48. package/dist/public/check-types.d.ts.map +1 -0
  49. package/dist/public/check-types.js +18 -0
  50. package/dist/public/schemas/entity.d.ts +68 -0
  51. package/dist/public/schemas/entity.d.ts.map +1 -1
  52. package/dist/public/schemas/entity.js +52 -0
  53. package/dist/relationships/shards.d.ts.map +1 -1
  54. package/dist/relationships/shards.js +6 -3
  55. package/dist/schemas/entity.schema.json +120 -0
  56. package/dist/search-ranking.d.ts +9 -0
  57. package/dist/search-ranking.d.ts.map +1 -0
  58. package/dist/search-ranking.js +149 -0
  59. package/dist/traceability/symbol-extract.d.ts.map +1 -1
  60. package/dist/traceability/symbol-extract.js +16 -8
  61. package/dist/types/entities.d.ts +19 -1
  62. package/dist/types/entities.d.ts.map +1 -1
  63. package/dist/utils/prolog-cleanup.d.ts +10 -0
  64. package/dist/utils/prolog-cleanup.d.ts.map +1 -0
  65. package/dist/utils/prolog-cleanup.js +39 -0
  66. package/dist/utils/rule-registry.d.ts +8 -0
  67. package/dist/utils/rule-registry.d.ts.map +1 -1
  68. package/dist/utils/rule-registry.js +14 -12
  69. package/package.json +10 -2
  70. package/schema/config.json +7 -1
  71. package/schema/entities.pl +18 -0
  72. package/schema/validation.pl +115 -4
  73. package/src/public/check-types.ts +37 -0
  74. package/src/public/schemas/entity.ts +58 -0
  75. package/src/schemas/entity.schema.json +56 -1
  76. package/dist/kb/target-resolver.d.ts +0 -80
  77. package/dist/kb/target-resolver.d.ts.map +0 -1
  78. package/dist/kb/target-resolver.js +0 -313
@@ -0,0 +1,149 @@
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(
104
+ // implements REQ-007, REQ-mcp-search-discovery
105
+ source, workspaceRoot) {
106
+ if (!source) {
107
+ return null;
108
+ }
109
+ const normalizedSource = source.split("#", 1)[0]?.trim() ?? "";
110
+ if (!normalizedSource.endsWith(".md")) {
111
+ return null;
112
+ }
113
+ // Resolve to absolute path; relative paths are resolved against workspaceRoot.
114
+ const resolved = path.resolve(path.isAbsolute(normalizedSource)
115
+ ? normalizedSource
116
+ : path.join(workspaceRoot, normalizedSource));
117
+ // Reject paths that escape the workspace root to prevent path traversal.
118
+ const normalizedRoot = path.resolve(workspaceRoot);
119
+ if (!resolved.startsWith(normalizedRoot + path.sep)) {
120
+ return null;
121
+ }
122
+ try {
123
+ const fileContent = await fs.readFile(resolved, "utf8");
124
+ return matter(fileContent).content;
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
130
+ function normalize(value) {
131
+ return value.trim().toLowerCase();
132
+ }
133
+ function countTokenMatches(haystack, tokens) {
134
+ return tokens.filter((token) => haystack.includes(token)).length;
135
+ }
136
+ function buildSnippet(bodyText, query) {
137
+ const lines = bodyText
138
+ .split(/\r?\n/)
139
+ .map((line) => line.trim())
140
+ .filter(Boolean);
141
+ const normalizedQuery = normalize(query);
142
+ const matchedLine = lines.find((line) => normalize(line).includes(normalizedQuery)) ?? lines[0];
143
+ if (!matchedLine) {
144
+ return undefined;
145
+ }
146
+ return matchedLine.length > 160
147
+ ? `${matchedLine.slice(0, 157)}...`
148
+ : matchedLine;
149
+ }
@@ -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;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"}
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,CAE1C,UAAU,EAAE,UAAU,EACtB,cAAc,CAAC,EAAE,cAAc,GAC9B,eAAe,EAAE,CAuNnB"}
@@ -45,7 +45,9 @@ function parseReqDirectives(text) {
45
45
  function rangesIntersect(aStart, aEnd, bStart, bEnd) {
46
46
  return aStart <= bEnd && bStart <= aEnd;
47
47
  }
48
- export function extractSymbolsFromStagedFile(stagedFile, manifestLookup) {
48
+ export function extractSymbolsFromStagedFile(
49
+ // implements REQ-008
50
+ stagedFile, manifestLookup) {
49
51
  const content = stagedFile.content ?? "";
50
52
  const sha = computeContentSha(`${content}|${stagedFile.path}`);
51
53
  // TTL cache lookup
@@ -108,7 +110,9 @@ export function extractSymbolsFromStagedFile(stagedFile, manifestLookup) {
108
110
  reqLinks: mergedReqLinks,
109
111
  });
110
112
  }
111
- catch { }
113
+ catch {
114
+ // skip: individual declaration extraction may fail on malformed AST nodes
115
+ }
112
116
  }
113
117
  // Classes
114
118
  for (const cls of sf.getClasses()) {
@@ -139,7 +143,9 @@ export function extractSymbolsFromStagedFile(stagedFile, manifestLookup) {
139
143
  reqLinks: mergedReqLinks,
140
144
  });
141
145
  }
142
- catch { }
146
+ catch {
147
+ // skip: individual declaration extraction may fail on malformed AST nodes
148
+ }
143
149
  }
144
150
  // Enums
145
151
  for (const en of sf.getEnums()) {
@@ -167,7 +173,9 @@ export function extractSymbolsFromStagedFile(stagedFile, manifestLookup) {
167
173
  reqLinks: mergedReqLinks,
168
174
  });
169
175
  }
170
- catch { }
176
+ catch {
177
+ // skip: individual declaration extraction may fail on malformed AST nodes
178
+ }
171
179
  }
172
180
  // Variable statements (exported)
173
181
  for (const vs of sf.getVariableStatements()) {
@@ -196,7 +204,9 @@ export function extractSymbolsFromStagedFile(stagedFile, manifestLookup) {
196
204
  reqLinks: mergedReqLinks,
197
205
  });
198
206
  }
199
- catch { }
207
+ catch {
208
+ // skip: individual declaration extraction may fail on malformed AST nodes
209
+ }
200
210
  }
201
211
  }
202
212
  // Filter to only include symbols that intersect with at least one hunk
@@ -219,9 +229,7 @@ function resolveSymbolId(filePath, name, manifestLookup) {
219
229
  // First, check the provided manifest lookup if available
220
230
  if (manifestLookup) {
221
231
  // Normalize the source file path for consistent lookup
222
- const normalizedSource = filePath.startsWith("/")
223
- ? filePath
224
- : `${filePath}`;
232
+ const normalizedSource = filePath;
225
233
  const lookupKey = `${normalizedSource}:${name}`;
226
234
  const entry = manifestLookup.get(lookupKey);
227
235
  if (entry) {
@@ -12,6 +12,24 @@ export interface BaseEntity {
12
12
  links?: string[];
13
13
  text_ref?: string;
14
14
  }
15
+ export interface FactFields {
16
+ fact_kind?: "subject" | "property_value" | "observation" | "meta";
17
+ subject_key?: string;
18
+ property_key?: string;
19
+ operator?: "eq" | "neq" | "lt" | "lte" | "gt" | "gte";
20
+ value_type?: "string" | "int" | "number" | "bool";
21
+ value_string?: string;
22
+ value_int?: number;
23
+ value_number?: number;
24
+ value_bool?: boolean;
25
+ unit?: string;
26
+ scope?: string;
27
+ polarity?: "require" | "forbid";
28
+ closed_world?: boolean;
29
+ valid_from?: string;
30
+ valid_to?: string;
31
+ canonical_key?: string;
32
+ }
15
33
  export type Requirement = BaseEntity & {
16
34
  type: "req";
17
35
  };
@@ -33,7 +51,7 @@ export type Event = BaseEntity & {
33
51
  export type Symbol = BaseEntity & {
34
52
  type: "symbol";
35
53
  };
36
- export type Fact = BaseEntity & {
54
+ export type Fact = BaseEntity & FactFields & {
37
55
  type: "fact";
38
56
  };
39
57
  export type Entity = Requirement | Scenario | TestEntity | ADR | Flag | Event | Symbol | Fact;
@@ -1 +1 @@
1
- {"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/types/entities.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AACvD,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AACzD,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACvD,MAAM,MAAM,GAAG,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAC/C,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AACnD,MAAM,MAAM,MAAM,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AACrD,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,MAAM,MAAM,MAAM,GACd,WAAW,GACX,QAAQ,GACR,UAAU,GACV,GAAG,GACH,IAAI,GACJ,KAAK,GACL,MAAM,GACN,IAAI,CAAC"}
1
+ {"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/types/entities.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,SAAS,GAAG,gBAAgB,GAAG,aAAa,GAAG,MAAM,CAAC;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;IACtD,UAAU,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAChC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AACvD,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC;AACzD,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACvD,MAAM,MAAM,GAAG,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAC/C,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjD,MAAM,MAAM,KAAK,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AACnD,MAAM,MAAM,MAAM,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AACrD,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,MAAM,MAAM,MAAM,GACd,WAAW,GACX,QAAQ,GACR,UAAU,GACV,GAAG,GACH,IAAI,GACJ,KAAK,GACL,MAAM,GACN,IAAI,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Best-effort Prolog process cleanup.
3
+ * Detaches the KB and terminates the process, swallowing any errors.
4
+ * Safe to call in finally blocks where the process may already be dead.
5
+ */
6
+ export declare function safeCleanupProlog(prolog: {
7
+ query: (q: string) => Promise<unknown>;
8
+ terminate: () => Promise<void>;
9
+ } | null | undefined): Promise<void>;
10
+ //# sourceMappingURL=prolog-cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prolog-cleanup.d.ts","sourceRoot":"","sources":["../../src/utils/prolog-cleanup.ts"],"names":[],"mappings":"AAkBA;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EACF;IAAE,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,GAC1E,IAAI,GACJ,SAAS,GACZ,OAAO,CAAC,IAAI,CAAC,CAaf"}
@@ -0,0 +1,39 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ /**
19
+ * Best-effort Prolog process cleanup.
20
+ * Detaches the KB and terminates the process, swallowing any errors.
21
+ * Safe to call in finally blocks where the process may already be dead.
22
+ */
23
+ export async function safeCleanupProlog(prolog) {
24
+ // implements REQ-003
25
+ if (!prolog)
26
+ return;
27
+ try {
28
+ await prolog.query("kb_detach");
29
+ }
30
+ catch {
31
+ // best-effort: process may already be dead or KB was never attached
32
+ }
33
+ try {
34
+ await prolog.terminate();
35
+ }
36
+ catch {
37
+ // best-effort: process may already be dead
38
+ }
39
+ }
@@ -11,6 +11,14 @@ export interface RuleDefinition {
11
11
  export interface SymbolTraceabilityOptions {
12
12
  requireAdr: boolean;
13
13
  }
14
+ /** A single KB check violation. */
15
+ export interface Violation {
16
+ rule: string;
17
+ entityId: string;
18
+ description: string;
19
+ suggestion?: string;
20
+ source?: string;
21
+ }
14
22
  export interface ChecksConfig {
15
23
  rules: Record<string, boolean>;
16
24
  symbolTraceability: SymbolTraceabilityOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"rule-registry.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,cAAc,CAAC;CACnE;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,kBAAkB,EAAE,yBAAyB,CAAC;CAC/C;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,SAAS,cAAc,EAsDjC,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,UAAU,aAAoC,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,YAKnC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,QAAQ,CAAC,EAAE,MAAM,GAChB,GAAG,CAAC,MAAM,CAAC,CA4Bb;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAC9B,YAAY,CAWd"}
1
+ {"version":3,"file":"rule-registry.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,cAAc,CAAC;CACnE;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,mCAAmC;AACnC,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,kBAAkB,EAAE,yBAAyB,CAAC;CAC/C;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,SAAS,cAAc,EA8DjC,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,UAAU,aAAoC,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,YAKnC,CAAC;AAEF;;;;;GAKG;AAEH,wBAAgB,iBAAiB,CAC/B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,QAAQ,CAAC,EAAE,MAAM,GAChB,GAAG,CAAC,MAAM,CAAC,CAsBb;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAC9B,YAAY,CAWd"}
@@ -20,6 +20,7 @@
20
20
  * When adding a new rule, add it here.
21
21
  */
22
22
  export const RULES = [
23
+ // implements REQ-006
23
24
  {
24
25
  name: "must-priority-coverage",
25
26
  description: "Every must-priority requirement must have a scenario and a test",
@@ -68,6 +69,12 @@ export const RULES = [
68
69
  defaultEnabled: true,
69
70
  category: "integrity",
70
71
  },
72
+ {
73
+ name: "strict-fact-shape",
74
+ description: "Detect malformed strict facts (facts with fact_kind that are missing required fields)",
75
+ defaultEnabled: false,
76
+ category: "integrity",
77
+ },
71
78
  ];
72
79
  /**
73
80
  * Set of all rule names for quick lookups.
@@ -88,7 +95,14 @@ export const DEFAULT_CHECKS_CONFIG = {
88
95
  * @param cliRules Optional comma-separated list from --rules CLI flag
89
96
  * @returns Set of rule names that should run
90
97
  */
98
+ // implements REQ-006
91
99
  export function getEffectiveRules(configRules, cliRules) {
100
+ if (cliRules) {
101
+ return new Set(cliRules
102
+ .split(",")
103
+ .map((s) => s.trim())
104
+ .filter((s) => RULE_NAMES.has(s)));
105
+ }
92
106
  // Start with all known rules
93
107
  const effective = new Set();
94
108
  for (const rule of RULES) {
@@ -98,18 +112,6 @@ export function getEffectiveRules(configRules, cliRules) {
98
112
  effective.add(rule.name);
99
113
  }
100
114
  }
101
- // CLI --rules is an intersection filter (narrows to specified rules)
102
- if (cliRules) {
103
- const allowed = new Set(cliRules
104
- .split(",")
105
- .map((s) => s.trim())
106
- .filter((s) => RULE_NAMES.has(s)));
107
- for (const rule of effective) {
108
- if (!allowed.has(rule)) {
109
- effective.delete(rule);
110
- }
111
- }
112
- }
113
115
  return effective;
114
116
  }
115
117
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "0.2.8",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Kibi CLI for knowledge base management",
6
6
  "engines": {
@@ -58,6 +58,14 @@
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"
65
+ },
66
+ "./public/check-types": {
67
+ "types": "./dist/public/check-types.d.ts",
68
+ "default": "./dist/public/check-types.js"
61
69
  }
62
70
  },
63
71
  "types": "./dist/cli.d.ts",
@@ -68,7 +76,7 @@
68
76
  "fast-glob": "^3.2.12",
69
77
  "gray-matter": "^4.0.3",
70
78
  "js-yaml": "^4.1.0",
71
- "kibi-core": "^0.1.10",
79
+ "kibi-core": "^0.3.0",
72
80
  "ts-morph": "^23.0.0"
73
81
  },
74
82
  "devDependencies": {
@@ -108,6 +108,11 @@
108
108
  "type": "boolean",
109
109
  "description": "Detect contradictions between requirements constraining the same fact",
110
110
  "default": true
111
+ },
112
+ "strict-fact-shape": {
113
+ "type": "boolean",
114
+ "description": "Detect malformed strict facts (facts with fact_kind that are missing required fields)",
115
+ "default": false
111
116
  }
112
117
  },
113
118
  "additionalProperties": false
@@ -150,7 +155,8 @@
150
155
  "no-cycles": true,
151
156
  "required-fields": true,
152
157
  "deprecated-adr-no-successor": true,
153
- "domain-contradictions": true
158
+ "domain-contradictions": true,
159
+ "strict-fact-shape": false
154
160
  },
155
161
  "symbolTraceability": {
156
162
  "requireAdr": false
@@ -29,6 +29,24 @@ entity_property(_, severity, atom).
29
29
  entity_property(_, links, list).
30
30
  entity_property(_, text_ref, uri).
31
31
 
32
+ % Typed fact fields - only valid for fact entities
33
+ entity_property(fact, fact_kind, atom).
34
+ entity_property(fact, subject_key, string).
35
+ entity_property(fact, property_key, string).
36
+ entity_property(fact, operator, atom).
37
+ entity_property(fact, value_type, atom).
38
+ entity_property(fact, value_string, string).
39
+ entity_property(fact, value_int, integer).
40
+ entity_property(fact, value_number, number).
41
+ entity_property(fact, value_bool, boolean).
42
+ entity_property(fact, unit, string).
43
+ entity_property(fact, scope, string).
44
+ entity_property(fact, polarity, atom).
45
+ entity_property(fact, closed_world, boolean).
46
+ entity_property(fact, valid_from, datetime).
47
+ entity_property(fact, valid_to, datetime).
48
+ entity_property(fact, canonical_key, string).
49
+
32
50
  % Required properties for all entity types
33
51
  required_property(Type, id) :- entity_type(Type).
34
52
  required_property(Type, title) :- entity_type(Type).
@@ -17,7 +17,9 @@ validate_entity(Type, Props) :-
17
17
  % required properties present
18
18
  forall(required_property(Type, P), memberchk(P=_Val, Props)),
19
19
  % all properties have correct types
20
- forall(member(Key=Val, Props), validate_property_type(Type, Key, Val)).
20
+ forall(member(Key=Val, Props), validate_property_type(Type, Key, Val)),
21
+ % validate entity-specific shape constraints
22
+ validate_entity_shape(Type, Props).
21
23
 
22
24
  % validate_relationship(+RelType, +From, +To)
23
25
  % From and To are pairs Type=Id or structures type(Type) - allow Type or Type=Id
@@ -33,9 +35,8 @@ type_of(Type, Type) :- atom(Type), entity_type(Type), !.
33
35
  type_of(Type=_Id, Type) :- atom(Type), entity_type(Type), !.
34
36
 
35
37
  % validate_property_type(+EntityType, +Prop, +Value)
36
- validate_property_type(_Type, Prop, Value) :-
37
- % find declared property type, default to atom
38
- ( entity_property(_Any, Prop, Kind) -> true ; Kind = atom ),
38
+ validate_property_type(Type, Prop, Value) :-
39
+ entity_property(Type, Prop, Kind),
39
40
  check_kind(Kind, Value), !.
40
41
 
41
42
  % check_kind(Kind, Value) succeeds if Value matches Kind
@@ -44,6 +45,116 @@ check_kind(string, V) :- string(V).
44
45
  check_kind(datetime, V) :- string(V). % accept ISO strings for now
45
46
  check_kind(list, V) :- is_list(V).
46
47
  check_kind(uri, V) :- string(V).
48
+ check_kind(integer, V) :- integer(V).
49
+ check_kind(number, V) :- number(V).
50
+ check_kind(boolean, true).
51
+ check_kind(boolean, false).
47
52
 
48
53
  % Fallback false
49
54
  check_kind(_, _) :- fail.
55
+
56
+ % validate_entity_shape(+Type, +Props)
57
+ % Validates entity-specific shape constraints (e.g., strict fact shapes)
58
+ validate_entity_shape(fact, Props) :-
59
+ !,
60
+ valid_optional_fact_enums(Props),
61
+ valid_polarity_in_props(Props),
62
+ ( memberchk(fact_kind=RawKind, Props) -> validate_fact_shape(RawKind, Props) ; true ).
63
+ validate_entity_shape(Type, Props) :-
64
+ Type \= fact,
65
+ % Non-fact entities cannot have fact-only fields
66
+ forall(member(Key=_Val, Props), \+ is_fact_only_field(Key)).
67
+
68
+ % is_fact_only_field(+Key) - true if Key is a fact-specific field
69
+ is_fact_only_field(fact_kind).
70
+ is_fact_only_field(subject_key).
71
+ is_fact_only_field(property_key).
72
+ is_fact_only_field(operator).
73
+ is_fact_only_field(value_type).
74
+ is_fact_only_field(value_string).
75
+ is_fact_only_field(value_int).
76
+ is_fact_only_field(value_number).
77
+ is_fact_only_field(value_bool).
78
+ is_fact_only_field(unit).
79
+ is_fact_only_field(scope).
80
+ is_fact_only_field(polarity).
81
+ is_fact_only_field(closed_world).
82
+ is_fact_only_field(valid_from).
83
+ is_fact_only_field(valid_to).
84
+ is_fact_only_field(canonical_key).
85
+
86
+ % validate_fact_shape(+Kind, +Props)
87
+ validate_fact_shape(subject, Props) :-
88
+ memberchk(subject_key=_Val, Props),
89
+ valid_optional_fact_enums(Props),
90
+ valid_polarity_in_props(Props).
91
+ validate_fact_shape(property_value, Props) :-
92
+ memberchk(subject_key=_Subject, Props),
93
+ memberchk(property_key=_Property, Props),
94
+ memberchk(operator=Op, Props),
95
+ valid_operator(Op),
96
+ memberchk(value_type=VT, Props),
97
+ valid_value_type(VT),
98
+ exactly_one_value_field(Props),
99
+ value_type_matches_field(VT, Props),
100
+ valid_optional_fact_enums(Props),
101
+ valid_polarity_in_props(Props).
102
+ validate_fact_shape(observation, Props) :-
103
+ % Observation facts are allowed but don't require full strict property tuple yet
104
+ valid_optional_fact_enums(Props),
105
+ valid_polarity_in_props(Props).
106
+ validate_fact_shape(meta, Props) :-
107
+ % Meta facts are allowed but don't require full strict property tuple yet
108
+ valid_optional_fact_enums(Props),
109
+ valid_polarity_in_props(Props).
110
+ validate_fact_shape(Kind, _Props) :-
111
+ % Unknown fact_kind values fail validation
112
+ \+ memberchk(Kind, [subject, property_value, observation, meta]),
113
+ fail.
114
+
115
+ % valid_operator(+Op)
116
+ valid_operator(eq).
117
+ valid_operator(neq).
118
+ valid_operator(lt).
119
+ valid_operator(lte).
120
+ valid_operator(gt).
121
+ valid_operator(gte).
122
+
123
+ % valid_value_type(+VT)
124
+ valid_value_type(string).
125
+ valid_value_type(int).
126
+ valid_value_type(number).
127
+ valid_value_type(bool).
128
+
129
+ % valid_polarity_in_props(+Props)
130
+ % Validates polarity if present; succeeds if no polarity in props
131
+ valid_polarity_in_props(Props) :-
132
+ ( memberchk(polarity=P, Props) -> valid_polarity(P) ; true ).
133
+
134
+ % valid_optional_fact_enums(+Props)
135
+ % Validates enum-typed fact fields whenever they are present
136
+ valid_optional_fact_enums(Props) :-
137
+ ( memberchk(operator=Op, Props) -> valid_operator(Op) ; true ),
138
+ ( memberchk(value_type=VT, Props) -> valid_value_type(VT) ; true ).
139
+
140
+ % valid_polarity(+P)
141
+ valid_polarity(require).
142
+ valid_polarity(forbid).
143
+
144
+ % exactly_one_value_field(+Props)
145
+ exactly_one_value_field(Props) :-
146
+ findall(F, (member(F=_, Props), is_value_field(F)), Fields),
147
+ length(Fields, 1).
148
+
149
+ % is_value_field(+Field)
150
+ is_value_field(value_string).
151
+ is_value_field(value_int).
152
+ is_value_field(value_number).
153
+ is_value_field(value_bool).
154
+
155
+ % value_type_matches_field(+ValueType, +Props)
156
+ % Ensures that value_type matches the actual value field present
157
+ value_type_matches_field(string, Props) :- memberchk(value_string=_, Props), !.
158
+ value_type_matches_field(int, Props) :- memberchk(value_int=_, Props), !.
159
+ value_type_matches_field(number, Props) :- memberchk(value_number=_, Props), !.
160
+ value_type_matches_field(bool, Props) :- memberchk(value_bool=_, Props), !.
@@ -0,0 +1,37 @@
1
+ /*
2
+ Kibi — repo-local, per-branch, queryable long-term memory for software projects
3
+ Copyright (C) 2026 Piotr Franczyk
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as published by
7
+ the Free Software Foundation, either version 3 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+
19
+ /**
20
+ * Public re-export barrel for shared check types.
21
+ * Import from "kibi-cli/public/check-types" in MCP or external consumers.
22
+ */
23
+ export type {
24
+ ChecksConfig,
25
+ RuleDefinition,
26
+ SymbolTraceabilityOptions,
27
+ Violation,
28
+ } from "../utils/rule-registry.js";
29
+
30
+ export {
31
+ DEFAULT_CHECKS_CONFIG,
32
+ RULE_NAMES,
33
+ RULES,
34
+ getEffectiveRules,
35
+ mergeChecksConfig,
36
+ validateRuleName,
37
+ } from "../utils/rule-registry.js";