@unbrained/pm-cli 2026.3.12 → 2026.5.1

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 (285) hide show
  1. package/.agents/pm/extensions/.managed-extensions.json +42 -0
  2. package/.agents/pm/extensions/beads/index.js +109 -0
  3. package/.agents/pm/extensions/beads/manifest.json +7 -0
  4. package/{dist/cli/commands/beads.js → .agents/pm/extensions/beads/runtime.js} +31 -21
  5. package/.agents/pm/extensions/beads/runtime.ts +702 -0
  6. package/.agents/pm/extensions/todos/index.js +126 -0
  7. package/.agents/pm/extensions/todos/manifest.json +7 -0
  8. package/{dist/extensions/builtins/todos/import-export.js → .agents/pm/extensions/todos/runtime.js} +39 -29
  9. package/.agents/pm/extensions/todos/runtime.ts +568 -0
  10. package/AGENTS.md +196 -92
  11. package/CHANGELOG.md +399 -0
  12. package/CODE_OF_CONDUCT.md +42 -0
  13. package/CONTRIBUTING.md +144 -0
  14. package/PRD.md +512 -164
  15. package/README.md +1053 -2
  16. package/SECURITY.md +51 -0
  17. package/dist/cli/commands/activity.d.ts +5 -0
  18. package/dist/cli/commands/activity.js +66 -3
  19. package/dist/cli/commands/activity.js.map +1 -1
  20. package/dist/cli/commands/aggregate.d.ts +54 -0
  21. package/dist/cli/commands/aggregate.js +181 -0
  22. package/dist/cli/commands/aggregate.js.map +1 -0
  23. package/dist/cli/commands/append.js +4 -1
  24. package/dist/cli/commands/append.js.map +1 -1
  25. package/dist/cli/commands/calendar.d.ts +109 -0
  26. package/dist/cli/commands/calendar.js +797 -0
  27. package/dist/cli/commands/calendar.js.map +1 -0
  28. package/dist/cli/commands/claim.d.ts +5 -1
  29. package/dist/cli/commands/claim.js +42 -21
  30. package/dist/cli/commands/claim.js.map +1 -1
  31. package/dist/cli/commands/close.d.ts +1 -0
  32. package/dist/cli/commands/close.js +54 -5
  33. package/dist/cli/commands/close.js.map +1 -1
  34. package/dist/cli/commands/comments-audit.d.ts +91 -0
  35. package/dist/cli/commands/comments-audit.js +195 -0
  36. package/dist/cli/commands/comments-audit.js.map +1 -0
  37. package/dist/cli/commands/comments.d.ts +1 -0
  38. package/dist/cli/commands/comments.js +70 -21
  39. package/dist/cli/commands/comments.js.map +1 -1
  40. package/dist/cli/commands/completion.d.ts +10 -4
  41. package/dist/cli/commands/completion.js +1184 -137
  42. package/dist/cli/commands/completion.js.map +1 -1
  43. package/dist/cli/commands/config.d.ts +35 -3
  44. package/dist/cli/commands/config.js +968 -13
  45. package/dist/cli/commands/config.js.map +1 -1
  46. package/dist/cli/commands/context.d.ts +86 -0
  47. package/dist/cli/commands/context.js +299 -0
  48. package/dist/cli/commands/context.js.map +1 -0
  49. package/dist/cli/commands/contracts.d.ts +78 -0
  50. package/dist/cli/commands/contracts.js +920 -0
  51. package/dist/cli/commands/contracts.js.map +1 -0
  52. package/dist/cli/commands/create.d.ts +48 -14
  53. package/dist/cli/commands/create.js +1331 -160
  54. package/dist/cli/commands/create.js.map +1 -1
  55. package/dist/cli/commands/dedupe-audit.d.ts +81 -0
  56. package/dist/cli/commands/dedupe-audit.js +330 -0
  57. package/dist/cli/commands/dedupe-audit.js.map +1 -0
  58. package/dist/cli/commands/deps.d.ts +52 -0
  59. package/dist/cli/commands/deps.js +204 -0
  60. package/dist/cli/commands/deps.js.map +1 -0
  61. package/dist/cli/commands/docs.d.ts +19 -0
  62. package/dist/cli/commands/docs.js +212 -13
  63. package/dist/cli/commands/docs.js.map +1 -1
  64. package/dist/cli/commands/extension.d.ts +122 -0
  65. package/dist/cli/commands/extension.js +1850 -0
  66. package/dist/cli/commands/extension.js.map +1 -0
  67. package/dist/cli/commands/files.d.ts +52 -1
  68. package/dist/cli/commands/files.js +443 -13
  69. package/dist/cli/commands/files.js.map +1 -1
  70. package/dist/cli/commands/gc.d.ts +11 -1
  71. package/dist/cli/commands/gc.js +89 -11
  72. package/dist/cli/commands/gc.js.map +1 -1
  73. package/dist/cli/commands/get.d.ts +13 -0
  74. package/dist/cli/commands/get.js +35 -3
  75. package/dist/cli/commands/get.js.map +1 -1
  76. package/dist/cli/commands/health.d.ts +10 -2
  77. package/dist/cli/commands/health.js +774 -23
  78. package/dist/cli/commands/health.js.map +1 -1
  79. package/dist/cli/commands/history.d.ts +20 -0
  80. package/dist/cli/commands/history.js +152 -6
  81. package/dist/cli/commands/history.js.map +1 -1
  82. package/dist/cli/commands/index.d.ts +16 -3
  83. package/dist/cli/commands/index.js +16 -3
  84. package/dist/cli/commands/index.js.map +1 -1
  85. package/dist/cli/commands/init.d.ts +7 -2
  86. package/dist/cli/commands/init.js +137 -5
  87. package/dist/cli/commands/init.js.map +1 -1
  88. package/dist/cli/commands/learnings.d.ts +17 -0
  89. package/dist/cli/commands/learnings.js +129 -0
  90. package/dist/cli/commands/learnings.js.map +1 -0
  91. package/dist/cli/commands/list.d.ts +29 -1
  92. package/dist/cli/commands/list.js +289 -53
  93. package/dist/cli/commands/list.js.map +1 -1
  94. package/dist/cli/commands/normalize.d.ts +51 -0
  95. package/dist/cli/commands/normalize.js +298 -0
  96. package/dist/cli/commands/normalize.js.map +1 -0
  97. package/dist/cli/commands/notes.d.ts +17 -0
  98. package/dist/cli/commands/notes.js +129 -0
  99. package/dist/cli/commands/notes.js.map +1 -0
  100. package/dist/cli/commands/reindex.d.ts +1 -0
  101. package/dist/cli/commands/reindex.js +208 -32
  102. package/dist/cli/commands/reindex.js.map +1 -1
  103. package/dist/cli/commands/restore.js +164 -30
  104. package/dist/cli/commands/restore.js.map +1 -1
  105. package/dist/cli/commands/search.d.ts +14 -1
  106. package/dist/cli/commands/search.js +475 -81
  107. package/dist/cli/commands/search.js.map +1 -1
  108. package/dist/cli/commands/stats.js +26 -10
  109. package/dist/cli/commands/stats.js.map +1 -1
  110. package/dist/cli/commands/templates.d.ts +26 -0
  111. package/dist/cli/commands/templates.js +179 -0
  112. package/dist/cli/commands/templates.js.map +1 -0
  113. package/dist/cli/commands/test-all.d.ts +19 -1
  114. package/dist/cli/commands/test-all.js +161 -13
  115. package/dist/cli/commands/test-all.js.map +1 -1
  116. package/dist/cli/commands/test-runs.d.ts +63 -0
  117. package/dist/cli/commands/test-runs.js +179 -0
  118. package/dist/cli/commands/test-runs.js.map +1 -0
  119. package/dist/cli/commands/test.d.ts +75 -1
  120. package/dist/cli/commands/test.js +1360 -41
  121. package/dist/cli/commands/test.js.map +1 -1
  122. package/dist/cli/commands/update-many.d.ts +57 -0
  123. package/dist/cli/commands/update-many.js +631 -0
  124. package/dist/cli/commands/update-many.js.map +1 -0
  125. package/dist/cli/commands/update.d.ts +30 -0
  126. package/dist/cli/commands/update.js +1393 -84
  127. package/dist/cli/commands/update.js.map +1 -1
  128. package/dist/cli/commands/validate.d.ts +30 -0
  129. package/dist/cli/commands/validate.js +1140 -0
  130. package/dist/cli/commands/validate.js.map +1 -0
  131. package/dist/cli/error-guidance.d.ts +33 -0
  132. package/dist/cli/error-guidance.js +337 -0
  133. package/dist/cli/error-guidance.js.map +1 -0
  134. package/dist/cli/extension-command-options.d.ts +1 -0
  135. package/dist/cli/extension-command-options.js +92 -0
  136. package/dist/cli/extension-command-options.js.map +1 -1
  137. package/dist/cli/help-content.d.ts +20 -0
  138. package/dist/cli/help-content.js +543 -0
  139. package/dist/cli/help-content.js.map +1 -0
  140. package/dist/cli/main.js +3625 -445
  141. package/dist/cli/main.js.map +1 -1
  142. package/dist/core/extensions/index.d.ts +13 -1
  143. package/dist/core/extensions/index.js +108 -1
  144. package/dist/core/extensions/index.js.map +1 -1
  145. package/dist/core/extensions/item-fields.d.ts +2 -0
  146. package/dist/core/extensions/item-fields.js +79 -0
  147. package/dist/core/extensions/item-fields.js.map +1 -0
  148. package/dist/core/extensions/loader.d.ts +322 -9
  149. package/dist/core/extensions/loader.js +911 -20
  150. package/dist/core/extensions/loader.js.map +1 -1
  151. package/dist/core/extensions/runtime-registrations.d.ts +5 -0
  152. package/dist/core/extensions/runtime-registrations.js +51 -0
  153. package/dist/core/extensions/runtime-registrations.js.map +1 -0
  154. package/dist/core/history/history-stream-policy.d.ts +20 -0
  155. package/dist/core/history/history-stream-policy.js +53 -0
  156. package/dist/core/history/history-stream-policy.js.map +1 -0
  157. package/dist/core/history/history.js +90 -1
  158. package/dist/core/history/history.js.map +1 -1
  159. package/dist/core/item/id.js +4 -1
  160. package/dist/core/item/id.js.map +1 -1
  161. package/dist/core/item/index.d.ts +1 -0
  162. package/dist/core/item/index.js +1 -0
  163. package/dist/core/item/index.js.map +1 -1
  164. package/dist/core/item/item-format.d.ts +11 -5
  165. package/dist/core/item/item-format.js +507 -24
  166. package/dist/core/item/item-format.js.map +1 -1
  167. package/dist/core/item/parent-reference-policy.d.ts +6 -0
  168. package/dist/core/item/parent-reference-policy.js +32 -0
  169. package/dist/core/item/parent-reference-policy.js.map +1 -0
  170. package/dist/core/item/parse.d.ts +5 -0
  171. package/dist/core/item/parse.js +216 -19
  172. package/dist/core/item/parse.js.map +1 -1
  173. package/dist/core/item/sprint-release-format.d.ts +6 -0
  174. package/dist/core/item/sprint-release-format.js +33 -0
  175. package/dist/core/item/sprint-release-format.js.map +1 -0
  176. package/dist/core/item/status.d.ts +3 -0
  177. package/dist/core/item/status.js +24 -0
  178. package/dist/core/item/status.js.map +1 -0
  179. package/dist/core/item/type-registry.d.ts +37 -0
  180. package/dist/core/item/type-registry.js +706 -0
  181. package/dist/core/item/type-registry.js.map +1 -0
  182. package/dist/core/lock/lock.d.ts +1 -1
  183. package/dist/core/lock/lock.js +101 -12
  184. package/dist/core/lock/lock.js.map +1 -1
  185. package/dist/core/output/command-aware.d.ts +1 -0
  186. package/dist/core/output/command-aware.js +394 -0
  187. package/dist/core/output/command-aware.js.map +1 -0
  188. package/dist/core/output/output.d.ts +3 -0
  189. package/dist/core/output/output.js +124 -6
  190. package/dist/core/output/output.js.map +1 -1
  191. package/dist/core/schema/runtime-field-filters.d.ts +3 -0
  192. package/dist/core/schema/runtime-field-filters.js +39 -0
  193. package/dist/core/schema/runtime-field-filters.js.map +1 -0
  194. package/dist/core/schema/runtime-field-values.d.ts +8 -0
  195. package/dist/core/schema/runtime-field-values.js +154 -0
  196. package/dist/core/schema/runtime-field-values.js.map +1 -0
  197. package/dist/core/schema/runtime-schema.d.ts +68 -0
  198. package/dist/core/schema/runtime-schema.js +554 -0
  199. package/dist/core/schema/runtime-schema.js.map +1 -0
  200. package/dist/core/search/cache.d.ts +13 -1
  201. package/dist/core/search/cache.js +123 -14
  202. package/dist/core/search/cache.js.map +1 -1
  203. package/dist/core/search/semantic-defaults.d.ts +6 -0
  204. package/dist/core/search/semantic-defaults.js +120 -0
  205. package/dist/core/search/semantic-defaults.js.map +1 -0
  206. package/dist/core/search/vector-stores.js +3 -1
  207. package/dist/core/search/vector-stores.js.map +1 -1
  208. package/dist/core/shared/command-types.d.ts +2 -0
  209. package/dist/core/shared/conflict-markers.d.ts +7 -0
  210. package/dist/core/shared/conflict-markers.js +27 -0
  211. package/dist/core/shared/conflict-markers.js.map +1 -0
  212. package/dist/core/shared/constants.d.ts +15 -4
  213. package/dist/core/shared/constants.js +141 -1
  214. package/dist/core/shared/constants.js.map +1 -1
  215. package/dist/core/shared/errors.d.ts +10 -1
  216. package/dist/core/shared/errors.js +3 -1
  217. package/dist/core/shared/errors.js.map +1 -1
  218. package/dist/core/shared/text-normalization.d.ts +4 -0
  219. package/dist/core/shared/text-normalization.js +33 -0
  220. package/dist/core/shared/text-normalization.js.map +1 -0
  221. package/dist/core/shared/time.d.ts +1 -2
  222. package/dist/core/shared/time.js +98 -11
  223. package/dist/core/shared/time.js.map +1 -1
  224. package/dist/core/store/index.d.ts +1 -0
  225. package/dist/core/store/index.js +1 -0
  226. package/dist/core/store/index.js.map +1 -1
  227. package/dist/core/store/item-format-migration.d.ts +9 -0
  228. package/dist/core/store/item-format-migration.js +87 -0
  229. package/dist/core/store/item-format-migration.js.map +1 -0
  230. package/dist/core/store/item-store.d.ts +13 -4
  231. package/dist/core/store/item-store.js +238 -51
  232. package/dist/core/store/item-store.js.map +1 -1
  233. package/dist/core/store/paths.d.ts +21 -3
  234. package/dist/core/store/paths.js +59 -4
  235. package/dist/core/store/paths.js.map +1 -1
  236. package/dist/core/store/settings.d.ts +14 -1
  237. package/dist/core/store/settings.js +463 -7
  238. package/dist/core/store/settings.js.map +1 -1
  239. package/dist/core/telemetry/consent.d.ts +2 -0
  240. package/dist/core/telemetry/consent.js +79 -0
  241. package/dist/core/telemetry/consent.js.map +1 -0
  242. package/dist/core/telemetry/runtime.d.ts +38 -0
  243. package/dist/core/telemetry/runtime.js +733 -0
  244. package/dist/core/telemetry/runtime.js.map +1 -0
  245. package/dist/core/test/background-runs.d.ts +117 -0
  246. package/dist/core/test/background-runs.js +760 -0
  247. package/dist/core/test/background-runs.js.map +1 -0
  248. package/dist/core/test/item-test-run-tracking.d.ts +9 -0
  249. package/dist/core/test/item-test-run-tracking.js +50 -0
  250. package/dist/core/test/item-test-run-tracking.js.map +1 -0
  251. package/dist/sdk/cli-contracts.d.ts +92 -0
  252. package/dist/sdk/cli-contracts.js +2357 -0
  253. package/dist/sdk/cli-contracts.js.map +1 -0
  254. package/dist/sdk/index.d.ts +34 -0
  255. package/dist/sdk/index.js +23 -0
  256. package/dist/sdk/index.js.map +1 -0
  257. package/dist/types.d.ts +197 -3
  258. package/dist/types.js +48 -1
  259. package/dist/types.js.map +1 -1
  260. package/docs/ARCHITECTURE.md +368 -39
  261. package/docs/EXTENSIONS.md +454 -49
  262. package/docs/RELEASING.md +68 -19
  263. package/docs/SDK.md +123 -0
  264. package/docs/examples/starter-extension/README.md +48 -0
  265. package/docs/examples/starter-extension/index.js +191 -0
  266. package/docs/examples/starter-extension/manifest.json +17 -0
  267. package/docs/examples/starter-extension/package.json +10 -0
  268. package/package.json +33 -6
  269. package/.pi/extensions/pm-cli/index.ts +0 -778
  270. package/dist/cli/commands/beads.d.ts +0 -16
  271. package/dist/cli/commands/beads.js.map +0 -1
  272. package/dist/cli/commands/install.d.ts +0 -18
  273. package/dist/cli/commands/install.js +0 -87
  274. package/dist/cli/commands/install.js.map +0 -1
  275. package/dist/core/extensions/builtins.d.ts +0 -3
  276. package/dist/core/extensions/builtins.js +0 -47
  277. package/dist/core/extensions/builtins.js.map +0 -1
  278. package/dist/extensions/builtins/beads/index.d.ts +0 -8
  279. package/dist/extensions/builtins/beads/index.js +0 -33
  280. package/dist/extensions/builtins/beads/index.js.map +0 -1
  281. package/dist/extensions/builtins/todos/import-export.d.ts +0 -26
  282. package/dist/extensions/builtins/todos/import-export.js.map +0 -1
  283. package/dist/extensions/builtins/todos/index.d.ts +0 -8
  284. package/dist/extensions/builtins/todos/index.js +0 -38
  285. package/dist/extensions/builtins/todos/index.js.map +0 -1
@@ -1,25 +1,65 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { runActiveOnReadHooks } from "../../core/extensions/index.js";
3
+ import { getActiveExtensionRegistrations, runActiveOnReadHooks } from "../../core/extensions/index.js";
4
+ import { resolveRegisteredSearchProvider, resolveRegisteredVectorStoreAdapter, } from "../../core/extensions/runtime-registrations.js";
5
+ import { resolveItemTypeRegistry, resolveTypeName } from "../../core/item/type-registry.js";
4
6
  import { executeEmbeddingRequest, resolveEmbeddingProviders, } from "../../core/search/providers.js";
7
+ import { resolveSettingsWithSemanticRuntimeDefaults } from "../../core/search/semantic-defaults.js";
5
8
  import { executeVectorQuery, resolveVectorStores, } from "../../core/search/vector-stores.js";
6
9
  import { pathExists } from "../../core/fs/fs-utils.js";
7
10
  import { parseItemDocument } from "../../core/item/item-format.js";
8
- import { EXIT_CODE, TYPE_TO_FOLDER } from "../../core/shared/constants.js";
11
+ import { normalizeStatusInput } from "../../core/item/status.js";
12
+ import { collectRuntimeFilterValues, matchesRuntimeFilters } from "../../core/schema/runtime-field-filters.js";
13
+ import { resolveRuntimeFieldRegistry, resolveRuntimeStatusRegistry, } from "../../core/schema/runtime-schema.js";
14
+ import { EXIT_CODE } from "../../core/shared/constants.js";
9
15
  import { PmCliError } from "../../core/shared/errors.js";
16
+ import { tokenizeAlphaNumeric } from "../../core/shared/text-normalization.js";
10
17
  import { compareTimestampStrings, nowIso, resolveIsoOrRelative } from "../../core/shared/time.js";
11
18
  import { listAllFrontMatter } from "../../core/store/item-store.js";
12
- import { getSettingsPath, resolveGlobalPmRoot, resolvePmRoot } from "../../core/store/paths.js";
19
+ import { getSettingsPath, resolveGlobalPmRoot, resolvePmRoot, getItemPath } from "../../core/store/paths.js";
13
20
  import { readSettings } from "../../core/store/settings.js";
14
- const ITEM_TYPES_BY_LOWER = new Map([
15
- ["epic", "Epic"],
16
- ["feature", "Feature"],
17
- ["task", "Task"],
18
- ["chore", "Chore"],
19
- ["issue", "Issue"],
20
- ]);
21
- function isTerminal(status) {
22
- return status === "closed" || status === "canceled";
21
+ const DEFAULT_COMPACT_SEARCH_FIELDS = [
22
+ "id",
23
+ "title",
24
+ "status",
25
+ "type",
26
+ "priority",
27
+ "updated_at",
28
+ "score",
29
+ "matched_fields",
30
+ ];
31
+ const LONG_QUERY_TOKEN_THRESHOLD = 4;
32
+ const LONG_QUERY_TITLE_EXACT_BONUS = 120;
33
+ const LONG_QUERY_PHRASE_MULTIPLIER = 6;
34
+ const IMPLICIT_AUTO_HYBRID_EMBEDDING_TIMEOUT_MS = 8_000;
35
+ const IMPLICIT_AUTO_HYBRID_VECTOR_TIMEOUT_MS = 8_000;
36
+ function isTerminal(status, statusRegistry) {
37
+ const normalized = normalizeStatusInput(status, statusRegistry) ?? status;
38
+ return statusRegistry.terminal_statuses.has(normalized);
39
+ }
40
+ function classifyImplicitSemanticFallbackReason(error) {
41
+ const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
42
+ if (message.includes("timed out") || message.includes("timeout")) {
43
+ return "timeout";
44
+ }
45
+ if (message.includes("econnrefused") ||
46
+ message.includes("connection refused") ||
47
+ message.includes("connect ") ||
48
+ message.includes("enotfound") ||
49
+ message.includes("eai_again")) {
50
+ return "connection";
51
+ }
52
+ return "error";
53
+ }
54
+ function buildImplicitSemanticFallbackWarning(error) {
55
+ const reason = classifyImplicitSemanticFallbackReason(error);
56
+ if (reason === "timeout") {
57
+ return "search_implicit_semantic_fallback:timeout:using_keyword_mode";
58
+ }
59
+ if (reason === "connection") {
60
+ return "search_implicit_semantic_fallback:connection:using_keyword_mode";
61
+ }
62
+ return "search_implicit_semantic_fallback:error:using_keyword_mode";
23
63
  }
24
64
  function parseMode(raw, context) {
25
65
  if (raw === undefined) {
@@ -34,6 +74,18 @@ function parseMode(raw, context) {
34
74
  function parseIncludeLinked(raw) {
35
75
  return raw === true;
36
76
  }
77
+ function parseTitleExact(raw) {
78
+ return raw === true;
79
+ }
80
+ function parsePhraseExact(raw) {
81
+ return raw === true;
82
+ }
83
+ function normalizeSearchPhrase(value) {
84
+ return value
85
+ .toLowerCase()
86
+ .replace(/\s+/g, " ")
87
+ .trim();
88
+ }
37
89
  function parsePriority(raw) {
38
90
  if (raw === undefined)
39
91
  return undefined;
@@ -43,19 +95,19 @@ function parsePriority(raw) {
43
95
  }
44
96
  return parsed;
45
97
  }
46
- function parseType(raw) {
98
+ function parseType(raw, typeRegistry) {
47
99
  if (raw === undefined)
48
100
  return undefined;
49
- const parsed = ITEM_TYPES_BY_LOWER.get(raw.trim().toLowerCase());
101
+ const parsed = resolveTypeName(raw, typeRegistry);
50
102
  if (!parsed) {
51
- throw new PmCliError("Type filter must be one of Epic|Feature|Task|Chore|Issue", EXIT_CODE.USAGE);
103
+ throw new PmCliError(`Type filter must be one of ${typeRegistry.types.join("|")}`, EXIT_CODE.USAGE);
52
104
  }
53
105
  return parsed;
54
106
  }
55
- function parseDeadline(raw) {
107
+ function parseDeadline(raw, fieldLabel) {
56
108
  if (raw === undefined)
57
109
  return undefined;
58
- return resolveIsoOrRelative(raw);
110
+ return resolveIsoOrRelative(raw, new Date(), fieldLabel);
59
111
  }
60
112
  function parseLimit(raw) {
61
113
  if (raw === undefined)
@@ -66,19 +118,94 @@ function parseLimit(raw) {
66
118
  }
67
119
  return Math.floor(parsed);
68
120
  }
121
+ function parseFieldSelectors(raw) {
122
+ if (raw === undefined) {
123
+ return undefined;
124
+ }
125
+ const selectors = raw
126
+ .split(",")
127
+ .map((entry) => entry.trim())
128
+ .filter((entry) => entry.length > 0);
129
+ if (selectors.length === 0) {
130
+ throw new PmCliError("Search --fields requires a comma-separated list of field names", EXIT_CODE.USAGE);
131
+ }
132
+ return [...new Set(selectors)];
133
+ }
134
+ function parseProjectionConfig(options) {
135
+ const compactRequested = options.compact === true;
136
+ const fullRequested = options.full === true;
137
+ const fieldSelectors = parseFieldSelectors(options.fields);
138
+ const enabledModes = Number(compactRequested) + Number(fullRequested) + Number(fieldSelectors !== undefined);
139
+ if (enabledModes > 1) {
140
+ throw new PmCliError("Search projection options are mutually exclusive. Use one of --compact, --full, or --fields.", EXIT_CODE.USAGE);
141
+ }
142
+ if (compactRequested) {
143
+ return {
144
+ mode: "compact",
145
+ fields: [...DEFAULT_COMPACT_SEARCH_FIELDS],
146
+ };
147
+ }
148
+ if (fullRequested) {
149
+ return {
150
+ mode: "full",
151
+ fields: [],
152
+ };
153
+ }
154
+ if (fieldSelectors) {
155
+ return {
156
+ mode: "fields",
157
+ fields: fieldSelectors,
158
+ };
159
+ }
160
+ return {
161
+ mode: "full",
162
+ fields: [],
163
+ };
164
+ }
69
165
  function parseTokens(query) {
70
- const normalized = query.trim().toLowerCase();
166
+ const normalized = normalizeSearchPhrase(query);
71
167
  if (!normalized) {
72
168
  throw new PmCliError("Search query must not be empty", EXIT_CODE.USAGE);
73
169
  }
74
170
  return normalized.split(/\s+/).filter(Boolean);
75
171
  }
76
- function applyFilters(items, options) {
77
- const typeFilter = parseType(options.type);
172
+ function collectExactPhraseFields(document) {
173
+ const item = document.front_matter;
174
+ return [
175
+ item.title,
176
+ item.description,
177
+ item.status,
178
+ item.tags.join(" "),
179
+ document.body,
180
+ (item.comments ?? []).map((entry) => entry.text).join(" "),
181
+ (item.notes ?? []).map((entry) => entry.text).join(" "),
182
+ (item.learnings ?? []).map((entry) => entry.text).join(" "),
183
+ (item.dependencies ?? []).map((entry) => `${entry.id} ${entry.kind}`).join(" "),
184
+ ];
185
+ }
186
+ function documentContainsExactPhrase(document, normalizedQuery) {
187
+ return collectExactPhraseFields(document).some((fieldValue) => normalizeSearchPhrase(fieldValue).includes(normalizedQuery));
188
+ }
189
+ function applyExactQueryFilters(items, normalizedQuery, options) {
190
+ if (!options.titleExact && !options.phraseExact) {
191
+ return items;
192
+ }
193
+ return items.filter((document) => {
194
+ if (options.titleExact && normalizeSearchPhrase(document.front_matter.title) !== normalizedQuery) {
195
+ return false;
196
+ }
197
+ if (options.phraseExact && !documentContainsExactPhrase(document, normalizedQuery)) {
198
+ return false;
199
+ }
200
+ return true;
201
+ });
202
+ }
203
+ function applyFilters(items, options, typeRegistry, runtimeFieldFilters) {
204
+ const typeFilter = parseType(options.type, typeRegistry);
78
205
  const tagFilter = options.tag?.trim().toLowerCase();
79
206
  const priorityFilter = parsePriority(options.priority);
80
- const deadlineBefore = parseDeadline(options.deadlineBefore);
81
- const deadlineAfter = parseDeadline(options.deadlineAfter);
207
+ const deadlineBefore = parseDeadline(options.deadlineBefore, "deadline-before");
208
+ const deadlineAfter = parseDeadline(options.deadlineAfter, "deadline-after");
82
209
  return items.filter((document) => {
83
210
  const item = document.front_matter;
84
211
  if (typeFilter && item.type !== typeFilter)
@@ -91,6 +218,8 @@ function applyFilters(items, options) {
91
218
  return false;
92
219
  if (deadlineAfter && (!item.deadline || compareTimestampStrings(item.deadline, deadlineAfter) < 0))
93
220
  return false;
221
+ if (!matchesRuntimeFilters(item, runtimeFieldFilters))
222
+ return false;
94
223
  return true;
95
224
  });
96
225
  }
@@ -107,10 +236,7 @@ function countOccurrences(haystack, needle) {
107
236
  }
108
237
  }
109
238
  function tokenizeForExactTokenMatch(value) {
110
- return value
111
- .toLowerCase()
112
- .split(/[^a-z0-9]+/)
113
- .filter((token) => token.length > 0);
239
+ return tokenizeAlphaNumeric(value);
114
240
  }
115
241
  function collectLinkedPaths(item) {
116
242
  const fromFiles = (item.files ?? []).map((entry) => ({
@@ -193,7 +319,7 @@ async function loadLinkedCorpus(document, projectRoot, globalRoot) {
193
319
  }
194
320
  return chunks.join("\n");
195
321
  }
196
- function scoreDocument(document, tokens, linkedCorpus, tuning) {
322
+ function scoreDocument(document, tokens, normalizedQuery, linkedCorpus, tuning) {
197
323
  const item = document.front_matter;
198
324
  const titleTokenCounts = new Map();
199
325
  for (const token of tokenizeForExactTokenMatch(item.title)) {
@@ -232,6 +358,22 @@ function scoreDocument(document, tokens, linkedCorpus, tuning) {
232
358
  }
233
359
  }
234
360
  }
361
+ const isLongPhraseQuery = tokens.length >= LONG_QUERY_TOKEN_THRESHOLD && normalizedQuery.includes(" ");
362
+ if (isLongPhraseQuery) {
363
+ const normalizedTitle = normalizeSearchPhrase(item.title);
364
+ if (normalizedTitle === normalizedQuery) {
365
+ score += LONG_QUERY_TITLE_EXACT_BONUS;
366
+ matched.add("title");
367
+ }
368
+ for (const field of searchableFields) {
369
+ const normalizedField = normalizeSearchPhrase(field.value);
370
+ const phraseOccurrences = countOccurrences(normalizedField, normalizedQuery);
371
+ if (phraseOccurrences > 0) {
372
+ score += phraseOccurrences * field.weight * LONG_QUERY_PHRASE_MULTIPLIER;
373
+ matched.add(field.name);
374
+ }
375
+ }
376
+ }
235
377
  if (score <= 0) {
236
378
  return null;
237
379
  }
@@ -241,13 +383,13 @@ function scoreDocument(document, tokens, linkedCorpus, tuning) {
241
383
  matched_fields: [...matched].sort((a, b) => a.localeCompare(b)),
242
384
  };
243
385
  }
244
- function sortHits(items) {
386
+ function sortHits(items, statusRegistry) {
245
387
  return [...items].sort((a, b) => {
246
388
  const byScore = b.score - a.score;
247
389
  if (byScore !== 0)
248
390
  return byScore;
249
- const aTerminal = isTerminal(a.item.status);
250
- const bTerminal = isTerminal(b.item.status);
391
+ const aTerminal = isTerminal(a.item.status, statusRegistry);
392
+ const bTerminal = isTerminal(b.item.status, statusRegistry);
251
393
  if (aTerminal !== bTerminal) {
252
394
  return aTerminal ? 1 : -1;
253
395
  }
@@ -260,8 +402,8 @@ function sortHits(items) {
260
402
  return a.item.id.localeCompare(b.item.id);
261
403
  });
262
404
  }
263
- function buildHybridLexicalScore(document, tokens, includeLinked, linkedCorpusById, tuning) {
264
- return scoreDocument(document, tokens, includeLinked ? linkedCorpusById.get(document.front_matter.id) ?? "" : "", tuning);
405
+ function buildHybridLexicalScore(document, tokens, normalizedQuery, includeLinked, linkedCorpusById, tuning) {
406
+ return scoreDocument(document, tokens, normalizedQuery, includeLinked ? linkedCorpusById.get(document.front_matter.id) ?? "" : "", tuning);
265
407
  }
266
408
  function normalizeScoreMap(scoreById) {
267
409
  if (scoreById.size === 0) {
@@ -337,7 +479,8 @@ export function resolveSearchTuning(settings) {
337
479
  linked_content_weight: resolveWeight(tuning.linked_content_weight, defaults.linked_content_weight),
338
480
  };
339
481
  }
340
- function emptySearchResult(query, mode, options, includeLinked, scoreThreshold, hybridSemanticWeight) {
482
+ function emptySearchResult(query, mode, options, includeLinked, scoreThreshold, hybridSemanticWeight, projection, warnings) {
483
+ const projectionFields = projection.mode === "full" ? null : [...projection.fields];
341
484
  return {
342
485
  query: query.trim(),
343
486
  mode,
@@ -351,25 +494,115 @@ function emptySearchResult(query, mode, options, includeLinked, scoreThreshold,
351
494
  deadline_before: options.deadlineBefore ?? null,
352
495
  deadline_after: options.deadlineAfter ?? null,
353
496
  include_linked: includeLinked,
497
+ title_exact: options.titleExact === true,
498
+ phrase_exact: options.phraseExact === true,
354
499
  score_threshold: scoreThreshold,
355
500
  hybrid_semantic_weight: mode === "hybrid" ? hybridSemanticWeight : null,
356
501
  limit: options.limit ?? null,
502
+ projection: projection.mode,
503
+ fields: projectionFields,
504
+ },
505
+ projection: {
506
+ mode: projection.mode,
507
+ fields: projectionFields,
357
508
  },
358
509
  now: nowIso(),
510
+ ...(warnings.length > 0 ? { warnings } : {}),
359
511
  };
360
512
  }
361
- function requireSemanticDependencies(requestedMode, providerResolution, vectorResolution) {
513
+ function requireSemanticDependencies(requestedMode, providerResolution, vectorResolution, hasExtensionVectorQuery) {
362
514
  if (!providerResolution.active) {
363
515
  throw new PmCliError(`Search mode '${requestedMode}' requires a configured embedding provider in settings.providers.openai or settings.providers.ollama`, EXIT_CODE.USAGE);
364
516
  }
365
- if (!vectorResolution.active) {
366
- throw new PmCliError(`Search mode '${requestedMode}' requires a configured vector store in settings.vector_store.qdrant or settings.vector_store.lancedb`, EXIT_CODE.USAGE);
517
+ if (!vectorResolution.active && !hasExtensionVectorQuery) {
518
+ throw new PmCliError(`Search mode '${requestedMode}' requires a configured vector store in settings.vector_store.qdrant/settings.vector_store.lancedb or an extension adapter selected by settings.vector_store.adapter`, EXIT_CODE.USAGE);
367
519
  }
368
520
  return {
369
521
  provider: providerResolution.active,
370
- vectorStore: vectorResolution.active,
522
+ vectorStore: vectorResolution.active ?? null,
371
523
  };
372
524
  }
525
+ function toOptionalNonEmptyString(value) {
526
+ if (typeof value !== "string") {
527
+ return undefined;
528
+ }
529
+ const normalized = value.trim();
530
+ return normalized.length > 0 ? normalized : undefined;
531
+ }
532
+ function resolveExtensionSearchProvider(settings) {
533
+ const registrations = getActiveExtensionRegistrations();
534
+ const providerName = toOptionalNonEmptyString(settings.search?.provider);
535
+ const resolved = resolveRegisteredSearchProvider(registrations, providerName);
536
+ if (!resolved) {
537
+ return null;
538
+ }
539
+ const runtimeDefinition = resolved.runtime_definition ?? resolved.definition;
540
+ const query = runtimeDefinition.query;
541
+ if (typeof query !== "function") {
542
+ return null;
543
+ }
544
+ const registeredName = toOptionalNonEmptyString(runtimeDefinition.name) ??
545
+ toOptionalNonEmptyString(resolved.definition.name) ??
546
+ providerName;
547
+ if (!registeredName) {
548
+ return null;
549
+ }
550
+ return {
551
+ providerName: registeredName,
552
+ query: query,
553
+ };
554
+ }
555
+ function resolveExtensionVectorAdapter(settings) {
556
+ const registrations = getActiveExtensionRegistrations();
557
+ const adapterName = toOptionalNonEmptyString(settings.vector_store?.adapter);
558
+ const resolved = resolveRegisteredVectorStoreAdapter(registrations, adapterName);
559
+ if (!resolved) {
560
+ return null;
561
+ }
562
+ const runtimeDefinition = resolved.runtime_definition ?? resolved.definition;
563
+ const query = runtimeDefinition.query;
564
+ if (typeof query !== "function") {
565
+ return null;
566
+ }
567
+ return {
568
+ query: query,
569
+ };
570
+ }
571
+ function normalizeExtensionProviderHits(providerName, raw, filteredById) {
572
+ const rawHits = Array.isArray(raw)
573
+ ? raw
574
+ : raw?.hits;
575
+ if (!Array.isArray(rawHits)) {
576
+ throw new PmCliError(`Extension search provider "${providerName}" must return an array of hits or { hits: [...] }`, EXIT_CODE.GENERIC_FAILURE);
577
+ }
578
+ const seen = new Set();
579
+ const hits = [];
580
+ for (const rawHit of rawHits) {
581
+ if (typeof rawHit !== "object" || rawHit === null) {
582
+ continue;
583
+ }
584
+ const id = toOptionalNonEmptyString(rawHit.id);
585
+ const score = rawHit.score;
586
+ if (!id || typeof score !== "number" || !Number.isFinite(score) || seen.has(id)) {
587
+ continue;
588
+ }
589
+ const document = filteredById.get(id);
590
+ if (!document) {
591
+ continue;
592
+ }
593
+ const matchedFieldsRaw = rawHit.matched_fields;
594
+ const matchedFields = Array.isArray(matchedFieldsRaw) && matchedFieldsRaw.every((entry) => typeof entry === "string")
595
+ ? [...new Set(matchedFieldsRaw.map((entry) => entry.trim()).filter((entry) => entry.length > 0))].sort((a, b) => a.localeCompare(b))
596
+ : [`provider:${providerName}`];
597
+ seen.add(id);
598
+ hits.push({
599
+ item: document.front_matter,
600
+ score,
601
+ matched_fields: matchedFields,
602
+ });
603
+ }
604
+ return hits;
605
+ }
373
606
  function buildSemanticHits(vectorHits, filteredById) {
374
607
  const semanticHits = [];
375
608
  const semanticScores = new Map();
@@ -426,9 +659,32 @@ function combineHybridHits(filteredById, semanticScores, keywordHits, hybridSema
426
659
  }
427
660
  async function computeSemanticOrHybridHits(context) {
428
661
  const semanticLimit = context.limit ?? context.maxResults;
429
- const queryVectors = await executeEmbeddingRequest(context.provider, context.query.trim());
662
+ const embeddingOptions = context.embeddingTimeoutMs !== undefined ? { timeout_ms: context.embeddingTimeoutMs } : {};
663
+ const vectorQueryOptions = context.vectorQueryTimeoutMs !== undefined ? { timeout_ms: context.vectorQueryTimeoutMs } : {};
664
+ const queryVectors = await executeEmbeddingRequest(context.provider, context.query.trim(), embeddingOptions);
430
665
  const semanticVector = queryVectors[0];
431
- const vectorHits = await executeVectorQuery(context.vectorStore, semanticVector, semanticLimit);
666
+ let vectorHits;
667
+ if (context.extensionVectorAdapter?.query) {
668
+ try {
669
+ vectorHits = await Promise.resolve(context.extensionVectorAdapter.query({
670
+ vector: semanticVector,
671
+ limit: semanticLimit,
672
+ settings: context.settings,
673
+ }));
674
+ }
675
+ catch (error) {
676
+ if (!context.vectorStore) {
677
+ throw new PmCliError(`Extension vector adapter query failed and no built-in fallback store is configured (${error instanceof Error ? error.message : String(error)})`, EXIT_CODE.GENERIC_FAILURE);
678
+ }
679
+ vectorHits = await executeVectorQuery(context.vectorStore, semanticVector, semanticLimit, vectorQueryOptions);
680
+ }
681
+ }
682
+ else if (context.vectorStore) {
683
+ vectorHits = await executeVectorQuery(context.vectorStore, semanticVector, semanticLimit, vectorQueryOptions);
684
+ }
685
+ else {
686
+ throw new PmCliError("Semantic search requires either a configured vector store or an extension vector adapter query handler", EXIT_CODE.USAGE);
687
+ }
432
688
  const filteredById = new Map(context.filteredDocuments.map((document) => [document.front_matter.id, document]));
433
689
  const { semanticHits, semanticScores } = buildSemanticHits(vectorHits, filteredById);
434
690
  if (context.requestedMode === "semantic") {
@@ -436,96 +692,234 @@ async function computeSemanticOrHybridHits(context) {
436
692
  }
437
693
  return combineHybridHits(filteredById, semanticScores, context.keywordHits, context.hybridSemanticWeight);
438
694
  }
439
- async function loadDocuments(pmRoot) {
440
- const items = await listAllFrontMatter(pmRoot);
695
+ function alternateFormat(itemFormat) {
696
+ return itemFormat === "toon" ? "json_markdown" : "toon";
697
+ }
698
+ async function loadDocuments(pmRoot, itemFormat, typeToFolder, schema) {
699
+ const listWarnings = [];
700
+ const items = await listAllFrontMatter(pmRoot, itemFormat, typeToFolder, listWarnings, schema);
701
+ const warnings = [...new Set(listWarnings)].sort((left, right) => left.localeCompare(right));
441
702
  const documents = [];
442
703
  for (const item of items) {
443
- const itemPath = path.join(pmRoot, TYPE_TO_FOLDER[item.type], `${item.id}.md`);
444
- const raw = await fs.readFile(itemPath, "utf8");
704
+ const preferredPath = getItemPath(pmRoot, item.type, item.id, itemFormat, typeToFolder);
705
+ try {
706
+ const raw = await fs.readFile(preferredPath, "utf8");
707
+ await runActiveOnReadHooks({
708
+ path: preferredPath,
709
+ scope: "project",
710
+ });
711
+ documents.push(parseItemDocument(raw, { format: itemFormat, schema, onWarning: (warning) => listWarnings.push(warning) }));
712
+ continue;
713
+ }
714
+ catch {
715
+ // Fallback to the alternate format when preferred format path is absent.
716
+ }
717
+ const fallbackFormat = alternateFormat(itemFormat);
718
+ const fallbackPath = getItemPath(pmRoot, item.type, item.id, fallbackFormat, typeToFolder);
719
+ const raw = await fs.readFile(fallbackPath, "utf8");
445
720
  await runActiveOnReadHooks({
446
- path: itemPath,
721
+ path: fallbackPath,
447
722
  scope: "project",
448
723
  });
449
- const parsed = parseItemDocument(raw);
450
- documents.push(parsed);
724
+ documents.push(parseItemDocument(raw, { format: fallbackFormat, schema, onWarning: (warning) => listWarnings.push(warning) }));
725
+ }
726
+ return {
727
+ documents,
728
+ warnings,
729
+ };
730
+ }
731
+ function readSearchFieldValue(hit, field) {
732
+ const normalized = field.trim();
733
+ if (normalized.length === 0) {
734
+ return null;
735
+ }
736
+ if (normalized === "score") {
737
+ return hit.score;
738
+ }
739
+ if (normalized === "matched_fields") {
740
+ return hit.matched_fields;
741
+ }
742
+ if (normalized.startsWith("item.")) {
743
+ const itemKey = normalized.slice("item.".length);
744
+ if (itemKey.length === 0) {
745
+ return null;
746
+ }
747
+ const itemRecord = hit.item;
748
+ return itemRecord[itemKey] ?? null;
749
+ }
750
+ const hitRecord = hit;
751
+ const itemRecord = hit.item;
752
+ if (Object.prototype.hasOwnProperty.call(itemRecord, normalized)) {
753
+ return itemRecord[normalized] ?? null;
451
754
  }
452
- return documents;
755
+ if (Object.prototype.hasOwnProperty.call(hitRecord, normalized)) {
756
+ return hitRecord[normalized] ?? null;
757
+ }
758
+ return null;
759
+ }
760
+ function projectSearchHits(hits, projection) {
761
+ if (projection.mode === "full") {
762
+ return hits;
763
+ }
764
+ return hits.map((hit) => {
765
+ const projected = {};
766
+ for (const field of projection.fields) {
767
+ projected[field] = readSearchFieldValue(hit, field);
768
+ }
769
+ return projected;
770
+ });
453
771
  }
454
772
  export async function runSearch(query, options, global) {
455
773
  const includeLinked = parseIncludeLinked(options.includeLinked);
774
+ const titleExact = parseTitleExact(options.titleExact);
775
+ const phraseExact = parsePhraseExact(options.phraseExact);
456
776
  const tokens = parseTokens(query);
777
+ const normalizedQuery = normalizeSearchPhrase(query);
457
778
  const limit = parseLimit(options.limit);
779
+ const projection = parseProjectionConfig(options);
780
+ const modeWasExplicit = typeof options.mode === "string" && options.mode.trim().length > 0;
458
781
  const pmRoot = resolvePmRoot(process.cwd(), global.path);
459
782
  if (!(await pathExists(getSettingsPath(pmRoot)))) {
460
783
  throw new PmCliError(`Tracker is not initialized at ${pmRoot}. Run pm init first.`, EXIT_CODE.NOT_FOUND);
461
784
  }
462
- const settings = await readSettings(pmRoot);
785
+ const storedSettings = await readSettings(pmRoot);
786
+ const runtimeDefaultsResolution = resolveSettingsWithSemanticRuntimeDefaults(storedSettings);
787
+ const settings = runtimeDefaultsResolution.settings;
788
+ const statusRegistry = resolveRuntimeStatusRegistry(settings.schema);
789
+ const runtimeFieldRegistry = resolveRuntimeFieldRegistry(settings.schema);
790
+ const runtimeFieldFilters = collectRuntimeFilterValues(options, runtimeFieldRegistry, "search");
791
+ const typeRegistry = resolveItemTypeRegistry(settings, getActiveExtensionRegistrations());
463
792
  const maxResults = resolveSearchMaxResults(settings);
464
793
  const scoreThreshold = resolveSearchScoreThreshold(settings);
465
794
  const hybridSemanticWeight = resolveHybridSemanticWeight(settings);
466
795
  const tuning = resolveSearchTuning(settings);
467
796
  const providerResolution = resolveEmbeddingProviders(settings);
468
797
  const vectorResolution = resolveVectorStores(settings);
469
- const requestedMode = parseMode(options.mode, {
470
- hasProvider: providerResolution.active !== null,
471
- hasVectorStore: vectorResolution.active !== null,
798
+ const extensionSearchProvider = resolveExtensionSearchProvider(settings);
799
+ const extensionVectorAdapter = resolveExtensionVectorAdapter(settings);
800
+ let effectiveMode = parseMode(options.mode, {
801
+ hasProvider: providerResolution.active !== null || extensionSearchProvider !== null,
802
+ hasVectorStore: vectorResolution.active !== null || extensionVectorAdapter !== null,
803
+ });
804
+ const loadedDocuments = await loadDocuments(pmRoot, settings.item_format ?? "json_markdown", typeRegistry.type_to_folder, settings.schema);
805
+ const warnings = loadedDocuments.warnings;
806
+ const allDocuments = loadedDocuments.documents;
807
+ const metadataFilteredDocuments = applyFilters(allDocuments, options, typeRegistry, runtimeFieldFilters);
808
+ const filteredDocuments = applyExactQueryFilters(metadataFilteredDocuments, normalizedQuery, {
809
+ titleExact,
810
+ phraseExact,
472
811
  });
473
- const allDocuments = await loadDocuments(pmRoot);
474
- const filteredDocuments = applyFilters(allDocuments, options);
475
- if (requestedMode === "keyword" && (filteredDocuments.length === 0 || limit === 0)) {
476
- return emptySearchResult(query, requestedMode, options, includeLinked, scoreThreshold, hybridSemanticWeight);
812
+ if (effectiveMode === "keyword" && (filteredDocuments.length === 0 || limit === 0)) {
813
+ return emptySearchResult(query, effectiveMode, options, includeLinked, scoreThreshold, hybridSemanticWeight, projection, warnings);
477
814
  }
478
815
  const projectRoot = process.cwd();
479
816
  const globalRoot = resolveGlobalPmRoot(projectRoot);
480
817
  const linkedCorpusById = new Map();
481
- if (includeLinked && (requestedMode === "keyword" || requestedMode === "hybrid")) {
818
+ if (includeLinked && (effectiveMode === "keyword" || effectiveMode === "hybrid")) {
482
819
  for (const document of filteredDocuments) {
483
820
  linkedCorpusById.set(document.front_matter.id, await loadLinkedCorpus(document, projectRoot, globalRoot));
484
821
  }
485
822
  }
486
823
  const keywordHits = filteredDocuments
487
- .map((document) => buildHybridLexicalScore(document, tokens, requestedMode !== "semantic", linkedCorpusById, tuning))
824
+ .map((document) => buildHybridLexicalScore(document, tokens, normalizedQuery, effectiveMode !== "semantic", linkedCorpusById, tuning))
488
825
  .filter((entry) => entry !== null);
489
826
  let hits = keywordHits;
490
- if (requestedMode !== "keyword") {
491
- const { provider, vectorStore } = requireSemanticDependencies(requestedMode, providerResolution, vectorResolution);
492
- if (filteredDocuments.length === 0 || limit === 0) {
493
- return emptySearchResult(query, requestedMode, options, includeLinked, scoreThreshold, hybridSemanticWeight);
494
- }
495
- hits = await computeSemanticOrHybridHits({
496
- requestedMode,
497
- query,
498
- filteredDocuments,
499
- keywordHits,
500
- hybridSemanticWeight,
501
- limit,
502
- maxResults,
503
- provider,
504
- vectorStore,
505
- });
827
+ if (effectiveMode !== "keyword") {
828
+ try {
829
+ if (!extensionSearchProvider) {
830
+ requireSemanticDependencies(effectiveMode, providerResolution, vectorResolution, extensionVectorAdapter !== null);
831
+ }
832
+ if (filteredDocuments.length === 0 || limit === 0) {
833
+ return emptySearchResult(query, effectiveMode, options, includeLinked, scoreThreshold, hybridSemanticWeight, projection, warnings);
834
+ }
835
+ const filteredById = new Map(filteredDocuments.map((document) => [document.front_matter.id, document]));
836
+ const canUseBuiltInSemantic = providerResolution.active !== null && (vectorResolution.active !== null || extensionVectorAdapter !== null);
837
+ if (extensionSearchProvider) {
838
+ try {
839
+ const providerResponse = await Promise.resolve(extensionSearchProvider.query({
840
+ query,
841
+ mode: effectiveMode,
842
+ tokens,
843
+ options,
844
+ settings,
845
+ documents: filteredDocuments,
846
+ }));
847
+ hits = normalizeExtensionProviderHits(extensionSearchProvider.providerName, providerResponse, filteredById);
848
+ }
849
+ catch (error) {
850
+ if (!canUseBuiltInSemantic) {
851
+ throw new PmCliError(`Extension search provider "${extensionSearchProvider.providerName}" failed: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODE.GENERIC_FAILURE);
852
+ }
853
+ }
854
+ }
855
+ if (hits === keywordHits) {
856
+ const implicitAutoHybridMode = runtimeDefaultsResolution.auto_ollama_defaults_applied && !modeWasExplicit && effectiveMode === "hybrid";
857
+ const { provider, vectorStore } = requireSemanticDependencies(effectiveMode, providerResolution, vectorResolution, extensionVectorAdapter !== null);
858
+ hits = await computeSemanticOrHybridHits({
859
+ requestedMode: effectiveMode,
860
+ query,
861
+ filteredDocuments,
862
+ keywordHits,
863
+ hybridSemanticWeight,
864
+ limit,
865
+ maxResults,
866
+ provider,
867
+ vectorStore,
868
+ extensionVectorAdapter,
869
+ settings,
870
+ ...(implicitAutoHybridMode
871
+ ? {
872
+ embeddingTimeoutMs: IMPLICIT_AUTO_HYBRID_EMBEDDING_TIMEOUT_MS,
873
+ vectorQueryTimeoutMs: IMPLICIT_AUTO_HYBRID_VECTOR_TIMEOUT_MS,
874
+ }
875
+ : {}),
876
+ });
877
+ }
878
+ }
879
+ catch (error) {
880
+ const canFallbackToKeyword = runtimeDefaultsResolution.auto_ollama_defaults_applied && !modeWasExplicit && effectiveMode === "hybrid";
881
+ if (!canFallbackToKeyword) {
882
+ throw error;
883
+ }
884
+ effectiveMode = "keyword";
885
+ hits = keywordHits;
886
+ warnings.push(buildImplicitSemanticFallbackWarning(error));
887
+ }
506
888
  }
507
889
  const thresholded = hits.filter((entry) => entry.score >= scoreThreshold);
508
- const sorted = sortHits(thresholded);
509
- const effectiveLimit = requestedMode === "keyword" ? limit : (limit ?? maxResults);
510
- const limited = effectiveLimit === undefined ? sorted : sorted.slice(0, effectiveLimit);
890
+ const sorted = sortHits(thresholded, statusRegistry);
891
+ const resolvedLimit = effectiveMode === "keyword" ? limit : (limit ?? maxResults);
892
+ const limited = resolvedLimit === undefined ? sorted : sorted.slice(0, resolvedLimit);
893
+ const projectedItems = projectSearchHits(limited, projection);
894
+ const projectionFields = projection.mode === "full" ? null : [...projection.fields];
511
895
  return {
512
896
  query: query.trim(),
513
- mode: requestedMode,
514
- items: limited,
515
- count: limited.length,
897
+ mode: effectiveMode,
898
+ items: projectedItems,
899
+ count: projectedItems.length,
516
900
  filters: {
517
- mode: requestedMode,
901
+ mode: effectiveMode,
518
902
  type: options.type ?? null,
519
903
  tag: options.tag ?? null,
520
904
  priority: options.priority ?? null,
521
905
  deadline_before: options.deadlineBefore ?? null,
522
906
  deadline_after: options.deadlineAfter ?? null,
523
907
  include_linked: includeLinked,
908
+ title_exact: titleExact,
909
+ phrase_exact: phraseExact,
524
910
  score_threshold: scoreThreshold,
525
- hybrid_semantic_weight: requestedMode === "hybrid" ? hybridSemanticWeight : null,
911
+ hybrid_semantic_weight: effectiveMode === "hybrid" ? hybridSemanticWeight : null,
526
912
  limit: options.limit ?? null,
913
+ runtime_filters: runtimeFieldFilters,
914
+ projection: projection.mode,
915
+ fields: projectionFields,
916
+ },
917
+ projection: {
918
+ mode: projection.mode,
919
+ fields: projectionFields,
527
920
  },
528
921
  now: nowIso(),
922
+ ...(warnings.length > 0 ? { warnings } : {}),
529
923
  };
530
924
  }
531
925
  //# sourceMappingURL=search.js.map