kibi-cli 0.7.0 → 0.8.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.
@@ -8,11 +8,14 @@ interface StagingDeps {
8
8
  cwd: () => string;
9
9
  existsSync: typeof existsSync;
10
10
  fg: typeof fg;
11
+ isProcessAlive: (pid: number) => boolean;
11
12
  mkdirSync: typeof mkdirSync;
12
13
  moduleDir: string;
13
14
  renameSync: typeof renameSync;
14
15
  rmSync: typeof rmSync;
15
16
  }
17
+ export declare function createUniqueStagingPath(currentBranch: string, rootDir: string, pid?: number, now?: number): string;
18
+ export declare function cleanupAbandonedStagingDirectories(stagingPath: string, deps?: Partial<StagingDeps>): Promise<void>;
16
19
  export declare function prepareStagingEnvironment(stagingPath: string, livePath: string, rebuild: boolean, deps?: Partial<StagingDeps>): Promise<void>;
17
20
  export declare function atomicPublish(stagingPath: string, livePath: string, deps?: Partial<StagingDeps>): void;
18
21
  export declare function cleanupStaging(stagingPath: string, deps?: Partial<StagingDeps>): void;
@@ -1 +1 @@
1
- {"version":3,"file":"staging.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/staging.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,MAAM,WAAW,CAAC;AAC3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE,UAAU,WAAW;IACnB,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,EAAE,EAAE,OAAO,EAAE,CAAC;IACd,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,MAAM,EAAE,OAAO,MAAM,CAAC;CACvB;AAiBD,wBAAsB,yBAAyB,CAE7C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,OAAO,CAAC,IAAI,CAAC,CAaf;AA0CD,wBAAgB,aAAa,CAE3B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,IAAI,CAeN;AAED,wBAAgB,cAAc,CAE5B,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,IAAI,CAKN"}
1
+ {"version":3,"file":"staging.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/staging.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,MAAM,WAAW,CAAC;AAC3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE,UAAU,WAAW;IACnB,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,EAAE,EAAE,OAAO,EAAE,CAAC;IACd,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,MAAM,EAAE,OAAO,MAAM,CAAC;CACvB;AA8BD,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,GAAG,SAAc,EACjB,GAAG,SAAa,GACf,MAAM,CAOR;AAGD,wBAAsB,kCAAkC,CACtD,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,OAAO,CAAC,IAAI,CAAC,CAkDf;AAED,wBAAsB,yBAAyB,CAE7C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,OAAO,CAAC,IAAI,CAAC,CAaf;AA0CD,wBAAgB,aAAa,CAE3B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,IAAI,CAeN;AAED,wBAAgB,cAAc,CAE5B,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC1B,IAAI,CAKN"}
@@ -27,6 +27,17 @@ function resolveDeps(overrides) {
27
27
  cwd: () => process.cwd(),
28
28
  existsSync,
29
29
  fg,
30
+ isProcessAlive: (pid) => {
31
+ try {
32
+ process.kill(pid, 0);
33
+ return true;
34
+ }
35
+ catch (error) {
36
+ return !(error instanceof Error &&
37
+ "code" in error &&
38
+ error.code === "ESRCH");
39
+ }
40
+ },
30
41
  mkdirSync,
31
42
  moduleDir: import.meta.dirname,
32
43
  renameSync,
@@ -34,11 +45,54 @@ function resolveDeps(overrides) {
34
45
  ...overrides,
35
46
  };
36
47
  }
48
+ // implements REQ-003
49
+ export function createUniqueStagingPath(currentBranch, rootDir, pid = process.pid, now = Date.now()) {
50
+ return path.join(rootDir, ".kb", "branches", `${currentBranch}.staging.${pid}.${now}`);
51
+ }
52
+ // implements REQ-003
53
+ export async function cleanupAbandonedStagingDirectories(stagingPath, deps) {
54
+ const resolved = resolveDeps(deps);
55
+ const stagingDir = path.dirname(stagingPath);
56
+ const stagingBase = path.basename(stagingPath);
57
+ const match = /^(?<branch>.+)\.staging\.(?<pid>\d+)\.(?<timestamp>\d+)$/.exec(stagingBase);
58
+ if (!match?.groups) {
59
+ return;
60
+ }
61
+ const branch = match.groups.branch;
62
+ if (!branch) {
63
+ return;
64
+ }
65
+ const candidates = await resolved.fg(`${branch}.staging.*`, {
66
+ cwd: stagingDir,
67
+ absolute: true,
68
+ onlyDirectories: true,
69
+ suppressErrors: true,
70
+ });
71
+ for (const candidate of candidates) {
72
+ if (candidate === stagingPath) {
73
+ continue;
74
+ }
75
+ const candidateBase = path.basename(candidate);
76
+ const candidateMatch = new RegExp(`^${escapeRegex(branch)}\\.staging\\.(\\d+)\\.(\\d+)$`).exec(candidateBase);
77
+ if (!candidateMatch) {
78
+ continue;
79
+ }
80
+ const candidatePidText = candidateMatch[1];
81
+ if (!candidatePidText) {
82
+ continue;
83
+ }
84
+ const candidatePid = Number.parseInt(candidatePidText, 10);
85
+ if (!Number.isFinite(candidatePid) || resolved.isProcessAlive(candidatePid)) {
86
+ continue;
87
+ }
88
+ cleanupStaging(candidate, resolved);
89
+ }
90
+ }
37
91
  export async function prepareStagingEnvironment(
38
92
  // implements REQ-003
39
93
  stagingPath, livePath, rebuild, deps) {
40
94
  const resolved = resolveDeps(deps);
41
- // Cleanup any existing staging directory
95
+ await cleanupAbandonedStagingDirectories(stagingPath, resolved);
42
96
  cleanupStaging(stagingPath, resolved);
43
97
  resolved.mkdirSync(stagingPath, { recursive: true });
44
98
  if (!rebuild && resolved.existsSync(livePath)) {
@@ -107,3 +161,6 @@ stagingPath, deps) {
107
161
  resolved.rmSync(stagingPath, { recursive: true, force: true });
108
162
  }
109
163
  }
164
+ function escapeRegex(value) {
165
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
166
+ }
@@ -1,13 +1,30 @@
1
1
  import type { SyncSummary } from "../diagnostics.js";
2
+ import { PrologProcess } from "../prolog.js";
2
3
  export declare class SyncError extends Error {
3
4
  constructor(message: string);
4
5
  }
5
6
  export interface SyncResult extends SyncSummary {
6
7
  exitCode?: number;
7
8
  }
9
+ interface SyncCommandRuntimeContext {
10
+ currentBranch: string;
11
+ livePath: string;
12
+ rebuild: boolean;
13
+ stagingPath: string;
14
+ validateOnly: boolean;
15
+ }
16
+ interface SyncCommandRuntime {
17
+ afterAttach?: (context: SyncCommandRuntimeContext) => Promise<void> | void;
18
+ beforeSave?: (context: SyncCommandRuntimeContext & {
19
+ kbModified: boolean;
20
+ }) => Promise<void> | void;
21
+ createProlog?: (options: {
22
+ timeout: number;
23
+ }) => PrologProcess;
24
+ }
8
25
  export declare function syncCommand(options?: {
9
26
  validateOnly?: boolean;
10
27
  rebuild?: boolean;
11
- }): Promise<SyncResult>;
28
+ }, runtime?: SyncCommandRuntime): Promise<SyncResult>;
12
29
  export { normalizeMarkdownPath } from "./sync/discovery.js";
13
30
  //# sourceMappingURL=sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA0CjE,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AAGD,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,wBAAsB,WAAW,CAC/B,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1D,OAAO,CAAC,UAAU,CAAC,CAqarB;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAejE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA4B7C,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AAGD,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,yBAAyB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,UAAU,kBAAkB;IAC1B,WAAW,CAAC,EAAE,CACZ,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,CACX,OAAO,EAAE,yBAAyB,GAAG;QAAE,UAAU,EAAE,OAAO,CAAA;KAAE,KACzD,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,aAAa,CAAC;CAChE;AAGD,wBAAsB,WAAW,CAC/B,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,EAC3D,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,UAAU,CAAC,CAmbrB;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -29,7 +29,7 @@ import { discoverSourceFiles, } from "./sync/discovery.js";
29
29
  import { processExtractions } from "./sync/extraction.js";
30
30
  import { refreshManifestCoordinates } from "./sync/manifest.js";
31
31
  import { persistEntities, persistRelationships } from "./sync/persistence.js";
32
- import { atomicPublish, cleanupStaging, prepareStagingEnvironment, } from "./sync/staging.js";
32
+ import { atomicPublish, cleanupStaging, createUniqueStagingPath, prepareStagingEnvironment, } from "./sync/staging.js";
33
33
  export class SyncError extends Error {
34
34
  constructor(message) {
35
35
  super(message);
@@ -37,7 +37,7 @@ export class SyncError extends Error {
37
37
  }
38
38
  }
39
39
  // implements REQ-003, REQ-007
40
- export async function syncCommand(options = {}) {
40
+ export async function syncCommand(options = {}, runtime = {}) {
41
41
  const validateOnly = options.validateOnly ?? false;
42
42
  const rebuild = options.rebuild ?? false;
43
43
  const startTime = Date.now();
@@ -45,6 +45,7 @@ export async function syncCommand(options = {}) {
45
45
  const entityCounts = {};
46
46
  let published = false;
47
47
  let currentBranch;
48
+ let stagingPath;
48
49
  const getCurrentCommit = () => {
49
50
  try {
50
51
  return execSync("git rev-parse HEAD", {
@@ -199,16 +200,25 @@ export async function syncCommand(options = {}) {
199
200
  if (!kbExists && !rebuild) {
200
201
  diagnostics.push(createKbMissingDiagnostic(currentBranch, livePath));
201
202
  }
202
- const stagingPath = path.join(process.cwd(), `.kb/branches/${currentBranch}.staging`);
203
+ stagingPath = createUniqueStagingPath(currentBranch, process.cwd());
204
+ const runtimeContext = {
205
+ currentBranch,
206
+ livePath,
207
+ rebuild,
208
+ stagingPath,
209
+ validateOnly,
210
+ };
203
211
  await prepareStagingEnvironment(stagingPath, livePath, rebuild);
204
212
  try {
205
- const prolog = new PrologProcess({ timeout: 120000 });
213
+ const prolog = runtime.createProlog?.({ timeout: 120000 }) ??
214
+ new PrologProcess({ timeout: 120000 });
206
215
  await prolog.start();
207
216
  const attachResult = await prolog.query(`kb_attach('${stagingPath}')`);
208
217
  if (!attachResult.success) {
209
218
  await prolog.terminate();
210
219
  throw new SyncError(`Failed to attach to staging KB: ${attachResult.error || "Unknown error"}`);
211
220
  }
221
+ await runtime.afterAttach?.(runtimeContext);
212
222
  const entityIds = new Set();
213
223
  for (const { entity } of results) {
214
224
  entityCounts[entity.type] = (entityCounts[entity.type] || 0) + 1;
@@ -263,6 +273,7 @@ export async function syncCommand(options = {}) {
263
273
  if (kbModified) {
264
274
  prolog.invalidateCache();
265
275
  }
276
+ await runtime.beforeSave?.({ ...runtimeContext, kbModified });
266
277
  const saveResult = await prolog.query("kb_save");
267
278
  if (!saveResult.success) {
268
279
  throw new SyncError(`Failed to save staging KB: ${saveResult.error || "Unknown error"}`);
@@ -270,6 +281,7 @@ export async function syncCommand(options = {}) {
270
281
  await prolog.query("kb_detach");
271
282
  await prolog.terminate();
272
283
  atomicPublish(stagingPath, livePath);
284
+ cleanupStaging(stagingPath);
273
285
  const evictedHashes = {};
274
286
  const evictedSeenAt = {};
275
287
  for (const [key, hash] of Object.entries(nextHashes)) {
@@ -312,6 +324,9 @@ export async function syncCommand(options = {}) {
312
324
  }
313
325
  }
314
326
  catch (error) {
327
+ if (stagingPath) {
328
+ cleanupStaging(stagingPath);
329
+ }
315
330
  const errorMessage = error instanceof Error ? error.message : String(error);
316
331
  console.error(`Error: ${errorMessage}`);
317
332
  const commit = getCurrentCommit();
@@ -0,0 +1,2 @@
1
+ export declare function isOperationalArtifactPath(pathLike: string): boolean;
2
+ //# sourceMappingURL=operational-artifacts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operational-artifacts.d.ts","sourceRoot":"","sources":["../../src/public/operational-artifacts.ts"],"names":[],"mappings":"AAAA,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAInE"}
@@ -0,0 +1,4 @@
1
+ export function isOperationalArtifactPath(pathLike) {
2
+ const normalized = pathLike.replaceAll("\\", "/");
3
+ return /(^|\/)\.sisyphus\//.test(normalized);
4
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"search-ranking.d.ts","sourceRoot":"","sources":["../src/search-ranking.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACnC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAyBxB;AA+FD,wBAAsB,gBAAgB,CAEpC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6BxB"}
1
+ {"version":3,"file":"search-ranking.d.ts","sourceRoot":"","sources":["../src/search-ranking.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAgDD,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACnC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CA8BxB;AAmGD,wBAAsB,gBAAgB,CAEpC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6BxB"}
@@ -1,10 +1,47 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ const SEARCH_STOP_WORDS = new Set([
4
+ "to",
5
+ "in",
6
+ "out",
7
+ "log",
8
+ "logged",
9
+ "unable",
10
+ "the",
11
+ "a",
12
+ "an",
13
+ "is",
14
+ "are",
15
+ "was",
16
+ "were",
17
+ "be",
18
+ "been",
19
+ "being",
20
+ "have",
21
+ "has",
22
+ "had",
23
+ "do",
24
+ "does",
25
+ "did",
26
+ "will",
27
+ "would",
28
+ "could",
29
+ "should",
30
+ "may",
31
+ "might",
32
+ "shall",
33
+ "can",
34
+ "not",
35
+ ]);
3
36
  // implements REQ-mcp-search-discovery, REQ-002, REQ-003
4
37
  export async function rankEntities(entities, query, workspaceRoot) {
38
+ const queryContext = buildSearchQueryContext(query);
39
+ if (!queryContext.rawTrimmedQuery || queryContext.signalTokens.length === 0) {
40
+ return [];
41
+ }
5
42
  const matches = [];
6
43
  for (const entity of entities) {
7
- const match = await rankEntity(entity, query, workspaceRoot);
44
+ const match = await rankEntity(entity, queryContext, workspaceRoot);
8
45
  if (match) {
9
46
  matches.push(match);
10
47
  }
@@ -22,9 +59,8 @@ export async function rankEntities(entities, query, workspaceRoot) {
22
59
  });
23
60
  return matches;
24
61
  }
25
- async function rankEntity(entity, query, workspaceRoot) {
26
- const normalizedQuery = normalize(query);
27
- const tokens = normalizedQuery.split(/\s+/).filter(Boolean);
62
+ // implements REQ-mcp-search-discovery
63
+ async function rankEntity(entity, queryContext, workspaceRoot) {
28
64
  const reasons = [];
29
65
  let score = 0;
30
66
  const id = String(entity.id ?? "");
@@ -37,55 +73,56 @@ async function rankEntity(entity, query, workspaceRoot) {
37
73
  const tags = Array.isArray(entity.tags)
38
74
  ? entity.tags.map((tag) => String(tag))
39
75
  : [];
40
- const normalizedTitle = normalize(title);
41
- const normalizedId = normalize(id);
42
- if (normalizedTitle === normalizedQuery) {
76
+ const titleForms = buildSearchTextForms(title);
77
+ const idForms = buildSearchTextForms(id);
78
+ if (isExactSearchMatch(titleForms, queryContext.phrase)) {
43
79
  score += 100;
44
80
  reasons.push("exact title match");
45
81
  }
46
- else if (normalizedTitle.includes(normalizedQuery)) {
82
+ else if (isPhraseSearchMatch(titleForms, queryContext.phrase)) {
47
83
  score += 60;
48
84
  reasons.push("title phrase match");
49
85
  }
50
- if (normalizedId === normalizedQuery) {
86
+ if (isExactSearchMatch(idForms, queryContext.phrase)) {
51
87
  score += 90;
52
88
  reasons.push("exact ID match");
53
89
  }
54
- else if (normalizedId.includes(normalizedQuery)) {
90
+ else if (isPhraseSearchMatch(idForms, queryContext.phrase)) {
55
91
  score += 55;
56
92
  reasons.push("ID match");
57
93
  }
58
94
  const metadataFields = [type, source, owner, priority, severity];
59
- const metadataMatched = metadataFields.some((field) => normalize(field).includes(normalizedQuery));
95
+ const metadataMatched = metadataFields.some((field) => isPhraseSearchMatch(buildSearchTextForms(field), queryContext.phrase));
60
96
  if (metadataMatched) {
61
97
  score += 20;
62
98
  reasons.push("metadata match");
63
99
  }
64
- const matchingTags = tags.filter((tag) => normalize(tag).includes(normalizedQuery));
100
+ const matchingTags = tags.filter((tag) => isPhraseSearchMatch(buildSearchTextForms(tag), queryContext.phrase));
65
101
  if (matchingTags.length > 0) {
66
102
  score += 30;
67
103
  reasons.push("tag match");
68
104
  }
69
- const titleTokenMatches = countTokenMatches(normalizedTitle, tokens);
105
+ const titleTokenMatches = countTokenMatches(titleForms, queryContext.signalTokens);
70
106
  if (titleTokenMatches > 0) {
71
107
  score += titleTokenMatches * 8;
72
108
  reasons.push("title token coverage");
73
109
  }
74
- const bodyText = await loadMarkdownBody(source, workspaceRoot);
110
+ const bodyText = (await loadMarkdownBody(source, workspaceRoot)) ??
111
+ getInlineBodyText(entity);
75
112
  let snippet;
76
113
  if (bodyText) {
77
- const normalizedBody = normalize(bodyText);
78
- if (normalizedBody.includes(normalizedQuery)) {
114
+ const bodyForms = buildSearchTextForms(bodyText);
115
+ if (isPhraseSearchMatch(bodyForms, queryContext.phrase)) {
79
116
  score += 15;
80
117
  reasons.push("markdown body match");
81
- snippet = buildSnippet(bodyText, query);
118
+ snippet = buildSnippet(bodyText, queryContext.phrase);
82
119
  }
83
120
  else {
84
- const bodyTokenMatches = countTokenMatches(normalizedBody, tokens);
121
+ const bodyTokenMatches = countTokenMatches(bodyForms, queryContext.signalTokens);
85
122
  if (bodyTokenMatches > 0) {
86
123
  score += bodyTokenMatches * 3;
87
124
  reasons.push("markdown body token coverage");
88
- snippet = buildSnippet(bodyText, query);
125
+ snippet = buildSnippet(bodyText, queryContext.phrase);
89
126
  }
90
127
  }
91
128
  }
@@ -126,6 +163,7 @@ source, workspaceRoot) {
126
163
  return null;
127
164
  }
128
165
  }
166
+ // implements REQ-mcp-search-discovery
129
167
  function stripFrontmatter(content) {
130
168
  const trimmedContent = content.trimStart();
131
169
  if (!trimmedContent.startsWith("---")) {
@@ -143,19 +181,88 @@ function stripFrontmatter(content) {
143
181
  }
144
182
  return trimmedContent.slice(match.index + match[0].length);
145
183
  }
146
- function normalize(value) {
147
- return value.trim().toLowerCase();
184
+ // implements REQ-mcp-search-discovery
185
+ function buildSearchQueryContext(query) {
186
+ return {
187
+ phrase: buildSearchTextForms(query),
188
+ signalTokens: tokenizeSignalTerms(query),
189
+ rawTrimmedQuery: query.trim(),
190
+ };
191
+ }
192
+ // implements REQ-mcp-search-discovery
193
+ function buildSearchTextForms(value) {
194
+ const normalized = normalizeSearchText(value);
195
+ return {
196
+ normalized,
197
+ compact: normalized.replace(/\s+/g, ""),
198
+ };
199
+ }
200
+ // implements REQ-mcp-search-discovery
201
+ function normalizeSearchText(value) {
202
+ return value
203
+ .trim()
204
+ .toLowerCase()
205
+ .replace(/[-_]+/g, " ")
206
+ .replace(/[^a-z0-9\s]+/g, " ")
207
+ .trim()
208
+ .split(/\s+/)
209
+ .filter(Boolean)
210
+ .map(singularizeSimplePlural)
211
+ .join(" ");
212
+ }
213
+ // implements REQ-mcp-search-discovery
214
+ function tokenizeSignalTerms(value) {
215
+ return Array.from(new Set(normalizeSearchText(value)
216
+ .split(/\s+/)
217
+ .filter((token) => token && !SEARCH_STOP_WORDS.has(token))));
148
218
  }
219
+ // implements REQ-mcp-search-discovery
220
+ function singularizeSimplePlural(token) {
221
+ if (token.length <= 4 ||
222
+ !token.endsWith("s") ||
223
+ token.endsWith("ss") ||
224
+ token.endsWith("us") ||
225
+ token.endsWith("is")) {
226
+ return token;
227
+ }
228
+ return token.slice(0, -1);
229
+ }
230
+ // implements REQ-mcp-search-discovery
231
+ function isExactSearchMatch(haystack, needle) {
232
+ return (haystack.normalized === needle.normalized ||
233
+ (needle.compact !== "" && haystack.compact === needle.compact));
234
+ }
235
+ // implements REQ-mcp-search-discovery
236
+ function isPhraseSearchMatch(haystack, needle) {
237
+ return (haystack.normalized.includes(needle.normalized) ||
238
+ (needle.compact !== "" && haystack.compact.includes(needle.compact)));
239
+ }
240
+ // implements REQ-mcp-search-discovery
149
241
  function countTokenMatches(haystack, tokens) {
150
- return tokens.filter((token) => haystack.includes(token)).length;
242
+ return tokens.filter((token) => haystack.normalized.includes(token) || haystack.compact.includes(token)).length;
243
+ }
244
+ // implements REQ-mcp-search-discovery
245
+ function getInlineBodyText(entity) {
246
+ const candidates = [
247
+ entity.body,
248
+ entity.markdownBody,
249
+ entity.markdown_body,
250
+ entity.content,
251
+ ];
252
+ for (const candidate of candidates) {
253
+ if (typeof candidate === "string" && candidate.trim() !== "") {
254
+ return candidate;
255
+ }
256
+ }
257
+ return null;
151
258
  }
152
- function buildSnippet(bodyText, query) {
259
+ // implements REQ-mcp-search-discovery
260
+ function buildSnippet(bodyText, queryForms) {
153
261
  const lines = bodyText
154
262
  .split(/\r?\n/)
155
263
  .map((line) => line.trim())
156
264
  .filter(Boolean);
157
- const normalizedQuery = normalize(query);
158
- const matchedLine = lines.find((line) => normalize(line).includes(normalizedQuery)) ?? lines[0];
265
+ const matchedLine = lines.find((line) => isPhraseSearchMatch(buildSearchTextForms(line), queryForms)) ?? lines[0];
159
266
  if (!matchedLine) {
160
267
  return undefined;
161
268
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Kibi CLI for knowledge base management",
6
6
  "engines": {
@@ -78,6 +78,10 @@
78
78
  "./brief-config": {
79
79
  "types": "./dist/public/brief-config.d.ts",
80
80
  "default": "./dist/public/brief-config.js"
81
+ },
82
+ "./operational-artifacts": {
83
+ "types": "./dist/public/operational-artifacts.d.ts",
84
+ "default": "./dist/public/operational-artifacts.js"
81
85
  }
82
86
  },
83
87
  "types": "./dist/cli.d.ts",
@@ -0,0 +1,5 @@
1
+ export function isOperationalArtifactPath(pathLike: string): boolean { // implements REQ-001
2
+ const normalized = pathLike.replaceAll("\\", "/");
3
+
4
+ return /(^|\/)\.sisyphus\//.test(normalized);
5
+ }