grounder 0.5.0 → 0.6.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 (144) hide show
  1. package/README.md +21 -20
  2. package/dist/agents/claude.d.ts +4 -4
  3. package/dist/agents/claude.d.ts.map +1 -1
  4. package/dist/agents/claude.js +158 -179
  5. package/dist/agents/claude.js.map +1 -1
  6. package/dist/agents/cursor.d.ts +3 -3
  7. package/dist/agents/cursor.d.ts.map +1 -1
  8. package/dist/agents/cursor.js +97 -144
  9. package/dist/agents/cursor.js.map +1 -1
  10. package/dist/agents/home-skills.d.ts +46 -0
  11. package/dist/agents/home-skills.d.ts.map +1 -0
  12. package/dist/agents/home-skills.js +83 -0
  13. package/dist/agents/home-skills.js.map +1 -0
  14. package/dist/agents/hook-fragment.d.ts +52 -0
  15. package/dist/agents/hook-fragment.d.ts.map +1 -0
  16. package/dist/agents/hook-fragment.js +76 -0
  17. package/dist/agents/hook-fragment.js.map +1 -0
  18. package/dist/agents/hook-install.d.ts +62 -0
  19. package/dist/agents/hook-install.d.ts.map +1 -0
  20. package/dist/agents/hook-install.js +74 -0
  21. package/dist/agents/hook-install.js.map +1 -0
  22. package/dist/agents/hook-runtime.d.ts +77 -10
  23. package/dist/agents/hook-runtime.d.ts.map +1 -1
  24. package/dist/agents/hook-runtime.js +274 -70
  25. package/dist/agents/hook-runtime.js.map +1 -1
  26. package/dist/agents/index.d.ts +8 -14
  27. package/dist/agents/index.d.ts.map +1 -1
  28. package/dist/agents/index.js +13 -19
  29. package/dist/agents/index.js.map +1 -1
  30. package/dist/agents/types.d.ts +56 -16
  31. package/dist/agents/types.d.ts.map +1 -1
  32. package/dist/cli.js +4 -0
  33. package/dist/cli.js.map +1 -1
  34. package/dist/commands/apply.d.ts +69 -0
  35. package/dist/commands/apply.d.ts.map +1 -0
  36. package/dist/commands/apply.js +238 -0
  37. package/dist/commands/apply.js.map +1 -0
  38. package/dist/commands/doctor.d.ts.map +1 -1
  39. package/dist/commands/doctor.js +190 -97
  40. package/dist/commands/doctor.js.map +1 -1
  41. package/dist/commands/handoff/peek.d.ts +1 -1
  42. package/dist/commands/handoff/peek.d.ts.map +1 -1
  43. package/dist/commands/handoff/peek.js +28 -12
  44. package/dist/commands/handoff/peek.js.map +1 -1
  45. package/dist/commands/install-drift.d.ts +34 -0
  46. package/dist/commands/install-drift.d.ts.map +1 -0
  47. package/dist/commands/install-drift.js +72 -0
  48. package/dist/commands/install-drift.js.map +1 -0
  49. package/dist/commands/migrate.d.ts +11 -3
  50. package/dist/commands/migrate.d.ts.map +1 -1
  51. package/dist/commands/migrate.js +67 -33
  52. package/dist/commands/migrate.js.map +1 -1
  53. package/dist/commands/overview.d.ts +33 -0
  54. package/dist/commands/overview.d.ts.map +1 -0
  55. package/dist/commands/overview.js +150 -0
  56. package/dist/commands/overview.js.map +1 -0
  57. package/dist/commands/render-artifact-table.d.ts +85 -0
  58. package/dist/commands/render-artifact-table.d.ts.map +1 -0
  59. package/dist/commands/render-artifact-table.js +232 -0
  60. package/dist/commands/render-artifact-table.js.map +1 -0
  61. package/dist/commands/search.d.ts.map +1 -1
  62. package/dist/commands/search.js +15 -4
  63. package/dist/commands/search.js.map +1 -1
  64. package/dist/commands/setup.d.ts.map +1 -1
  65. package/dist/commands/setup.js +86 -13
  66. package/dist/commands/setup.js.map +1 -1
  67. package/dist/commands/status.d.ts +7 -0
  68. package/dist/commands/status.d.ts.map +1 -1
  69. package/dist/commands/status.js +269 -88
  70. package/dist/commands/status.js.map +1 -1
  71. package/dist/connector/ledger-migrations.d.ts +55 -0
  72. package/dist/connector/ledger-migrations.d.ts.map +1 -0
  73. package/dist/connector/ledger-migrations.js +94 -0
  74. package/dist/connector/ledger-migrations.js.map +1 -0
  75. package/dist/connector/state.d.ts +96 -59
  76. package/dist/connector/state.d.ts.map +1 -1
  77. package/dist/connector/state.js +165 -107
  78. package/dist/connector/state.js.map +1 -1
  79. package/dist/help.d.ts +1 -1
  80. package/dist/help.d.ts.map +1 -1
  81. package/dist/help.js +27 -5
  82. package/dist/help.js.map +1 -1
  83. package/dist/reconcile/apply.d.ts +31 -0
  84. package/dist/reconcile/apply.d.ts.map +1 -0
  85. package/dist/reconcile/apply.js +97 -0
  86. package/dist/reconcile/apply.js.map +1 -0
  87. package/dist/reconcile/core.d.ts +82 -0
  88. package/dist/reconcile/core.d.ts.map +1 -0
  89. package/dist/reconcile/core.js +163 -0
  90. package/dist/reconcile/core.js.map +1 -0
  91. package/dist/reconcile/disk.d.ts +13 -0
  92. package/dist/reconcile/disk.d.ts.map +1 -0
  93. package/dist/reconcile/disk.js +28 -0
  94. package/dist/reconcile/disk.js.map +1 -0
  95. package/dist/util/fs.d.ts +14 -0
  96. package/dist/util/fs.d.ts.map +1 -1
  97. package/dist/util/fs.js +27 -1
  98. package/dist/util/fs.js.map +1 -1
  99. package/dist/util/merge-json.d.ts +13 -2
  100. package/dist/util/merge-json.d.ts.map +1 -1
  101. package/dist/util/merge-json.js +24 -10
  102. package/dist/util/merge-json.js.map +1 -1
  103. package/dist/vault/list-handoffs.d.ts +13 -3
  104. package/dist/vault/list-handoffs.d.ts.map +1 -1
  105. package/dist/vault/list-handoffs.js +28 -13
  106. package/dist/vault/list-handoffs.js.map +1 -1
  107. package/dist/vault/list-notes.d.ts +12 -3
  108. package/dist/vault/list-notes.d.ts.map +1 -1
  109. package/dist/vault/list-notes.js +13 -7
  110. package/dist/vault/list-notes.js.map +1 -1
  111. package/dist/vault/list-plans.d.ts +12 -3
  112. package/dist/vault/list-plans.d.ts.map +1 -1
  113. package/dist/vault/list-plans.js +13 -7
  114. package/dist/vault/list-plans.js.map +1 -1
  115. package/dist/vault/search.d.ts.map +1 -1
  116. package/dist/vault/search.js +128 -24
  117. package/dist/vault/search.js.map +1 -1
  118. package/package.json +2 -2
  119. package/templates/agents/claude/skills/grounder-handoff/SKILL.md +73 -0
  120. package/templates/agents/claude/{commands/grounder-note.md → skills/grounder-note/SKILL.md} +6 -0
  121. package/templates/agents/claude/skills/grounder-overview/SKILL.md +21 -0
  122. package/templates/agents/claude/{commands/grounder-plan.md → skills/grounder-plan/SKILL.md} +6 -0
  123. package/templates/agents/claude/skills/grounder-recall/SKILL.md +34 -0
  124. package/templates/agents/claude/{commands/grounder-search.md → skills/grounder-search/SKILL.md} +16 -8
  125. package/templates/agents/cursor/skills/grounder-handoff/SKILL.md +73 -0
  126. package/templates/agents/cursor/{commands/grounder-note.md → skills/grounder-note/SKILL.md} +6 -0
  127. package/templates/agents/cursor/skills/grounder-overview/SKILL.md +21 -0
  128. package/templates/agents/cursor/{commands/grounder-plan.md → skills/grounder-plan/SKILL.md} +6 -0
  129. package/templates/agents/cursor/skills/grounder-recall/SKILL.md +34 -0
  130. package/templates/agents/cursor/{commands/grounder-search.md → skills/grounder-search/SKILL.md} +16 -8
  131. package/templates/vault/plan.md +1 -1
  132. package/templates/vault/session-handoff.md +2 -1
  133. package/dist/agents/install-command.d.ts +0 -35
  134. package/dist/agents/install-command.d.ts.map +0 -1
  135. package/dist/agents/install-command.js +0 -65
  136. package/dist/agents/install-command.js.map +0 -1
  137. package/dist/commands/apply-agent-installs.d.ts +0 -34
  138. package/dist/commands/apply-agent-installs.d.ts.map +0 -1
  139. package/dist/commands/apply-agent-installs.js +0 -137
  140. package/dist/commands/apply-agent-installs.js.map +0 -1
  141. package/templates/agents/claude/commands/grounder-task-handoff.md +0 -58
  142. package/templates/agents/claude/commands/grounder-task.md +0 -22
  143. package/templates/agents/cursor/commands/grounder-task-handoff.md +0 -58
  144. package/templates/agents/cursor/commands/grounder-task.md +0 -22
@@ -1,15 +1,26 @@
1
1
  export type MergeJsonResult = {
2
2
  ok: true;
3
3
  created: boolean;
4
+ changed: boolean;
4
5
  } | {
5
6
  ok: false;
6
7
  error: "unparseable";
7
8
  message: string;
8
9
  };
10
+ export interface MergeJsonFileOptions {
11
+ /** Compute the result without writing to disk. */
12
+ dryRun?: boolean;
13
+ }
9
14
  /**
10
- * Read a JSON object file (default `{}` if missing), apply `merge`, write pretty-printed.
15
+ * Read a JSON object file (default `{}` if missing), apply `merge`, write pretty-printed
16
+ * only when the merged content actually differs from what's on disk, so callers can trust
17
+ * `changed` to mean a real content change rather than "we ran the merge function."
18
+ * A `merge` that returns its `current` argument by reference is treated as a no-op and
19
+ * never writes, even if the file's on-disk formatting (indentation, line endings, key
20
+ * order) differs from `JSON.stringify(current, null, 2)` — callers rely on this to leave
21
+ * a file with nothing to change byte-for-byte untouched instead of reformatting it.
11
22
  * On parse failure or non-object root: leaves the file untouched and returns an error
12
23
  * so callers can warn without clobbering shared config.
13
24
  */
14
- export declare function mergeJsonFile(filePath: string, merge: (current: Record<string, unknown>) => Record<string, unknown>): Promise<MergeJsonResult>;
25
+ export declare function mergeJsonFile(filePath: string, merge: (current: Record<string, unknown>) => Record<string, unknown>, options?: MergeJsonFileOptions): Promise<MergeJsonResult>;
15
26
  //# sourceMappingURL=merge-json.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"merge-json.d.ts","sourceRoot":"","sources":["../../src/util/merge-json.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnE,OAAO,CAAC,eAAe,CAAC,CAiC1B"}
1
+ {"version":3,"file":"merge-json.d.ts","sourceRoot":"","sources":["../../src/util/merge-json.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAChD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpE,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,eAAe,CAAC,CA2C1B"}
@@ -1,19 +1,25 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { fileExists } from "./fs.js";
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { fileExists, writeFileAtomic } from "./fs.js";
4
3
  /**
5
- * Read a JSON object file (default `{}` if missing), apply `merge`, write pretty-printed.
4
+ * Read a JSON object file (default `{}` if missing), apply `merge`, write pretty-printed
5
+ * only when the merged content actually differs from what's on disk, so callers can trust
6
+ * `changed` to mean a real content change rather than "we ran the merge function."
7
+ * A `merge` that returns its `current` argument by reference is treated as a no-op and
8
+ * never writes, even if the file's on-disk formatting (indentation, line endings, key
9
+ * order) differs from `JSON.stringify(current, null, 2)` — callers rely on this to leave
10
+ * a file with nothing to change byte-for-byte untouched instead of reformatting it.
6
11
  * On parse failure or non-object root: leaves the file untouched and returns an error
7
12
  * so callers can warn without clobbering shared config.
8
13
  */
9
- export async function mergeJsonFile(filePath, merge) {
14
+ export async function mergeJsonFile(filePath, merge, options = {}) {
10
15
  const existed = await fileExists(filePath);
11
16
  let current = {};
17
+ let originalRaw;
12
18
  if (existed) {
13
- const raw = await readFile(filePath, "utf8");
19
+ originalRaw = await readFile(filePath, "utf8");
14
20
  let parsed;
15
21
  try {
16
- parsed = JSON.parse(raw);
22
+ parsed = JSON.parse(originalRaw);
17
23
  }
18
24
  catch (err) {
19
25
  const detail = err instanceof Error ? err.message : String(err);
@@ -33,8 +39,16 @@ export async function mergeJsonFile(filePath, merge) {
33
39
  current = parsed;
34
40
  }
35
41
  const next = merge(current);
36
- await mkdir(path.dirname(filePath), { recursive: true });
37
- await writeFile(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
38
- return { ok: true, created: !existed };
42
+ const nextRaw = `${JSON.stringify(next, null, 2)}\n`;
43
+ const changed = next !== current && nextRaw !== originalRaw;
44
+ if (changed && !options.dryRun) {
45
+ // Preserve the existing file's permission bits across the tmp+rename
46
+ // swap — these are host-owned config files (Cursor/Claude Code), not
47
+ // Grounder's own state, so a rename shouldn't reset them to the tmp
48
+ // file's default umask.
49
+ const mode = existed ? (await stat(filePath)).mode & 0o777 : undefined;
50
+ await writeFileAtomic(filePath, nextRaw, mode !== undefined ? { mode } : {});
51
+ }
52
+ return { ok: true, created: !existed, changed };
39
53
  }
40
54
  //# sourceMappingURL=merge-json.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"merge-json.js","sourceRoot":"","sources":["../../src/util/merge-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAMrC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAgB,EAChB,KAAoE;IAEpE,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,OAAO,GAA4B,EAAE,CAAC;IAE1C,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC7C,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChE,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,sBAAsB,QAAQ,mBAAmB,MAAM,GAAG;aACpE,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,sBAAsB,QAAQ,oCAAoC;aAC5E,CAAC;QACJ,CAAC;QAED,OAAO,GAAG,MAAiC,CAAC;IAC9C,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAC5B,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,MAAM,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC;AACzC,CAAC"}
1
+ {"version":3,"file":"merge-json.js","sourceRoot":"","sources":["../../src/util/merge-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAWtD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAgB,EAChB,KAAoE,EACpE,UAAgC,EAAE;IAElC,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,OAAO,GAA4B,EAAE,CAAC;IAC1C,IAAI,WAA+B,CAAC;IAEpC,IAAI,OAAO,EAAE,CAAC;QACZ,WAAW,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC/C,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChE,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,sBAAsB,QAAQ,mBAAmB,MAAM,GAAG;aACpE,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,aAAa;gBACpB,OAAO,EAAE,sBAAsB,QAAQ,oCAAoC;aAC5E,CAAC;QACJ,CAAC;QAED,OAAO,GAAG,MAAiC,CAAC;IAC9C,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAC5B,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IACrD,MAAM,OAAO,GAAG,IAAI,KAAK,OAAO,IAAI,OAAO,KAAK,WAAW,CAAC;IAE5D,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QAC/B,qEAAqE;QACrE,qEAAqE;QACrE,oEAAoE;QACpE,wBAAwB;QACxB,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QACvE,MAAM,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;AAClD,CAAC"}
@@ -2,11 +2,21 @@ export interface ListHandoffsOptions {
2
2
  /** Max paths to return (newest first). Omit to return all. */
3
3
  limit?: number;
4
4
  }
5
+ /** One handoff with its mtime (see {@link listHandoffsDetailed}). */
6
+ export interface HandoffEntry {
7
+ path: string;
8
+ mtimeMs: number;
9
+ }
5
10
  /**
6
11
  * Lists handoff markdown files under `logsDir` recursively, newest basename
7
- * first (timestamp-prefixed names sort correctly). Same basename in different
8
- * subfolders ties break by vault-relative path descending.
9
- * Returns absolute paths. Missing or empty dirs yield `[]`.
12
+ * first (timestamp-prefixed names sort correctly), with each entry's mtime
13
+ * attached. Same basename in different subfolders ties break by
14
+ * vault-relative path descending. Missing or empty dirs yield `[]`.
15
+ *
16
+ * Ranking never reads mtime (filenames already sort correctly), so this
17
+ * stats only the paths a `limit` actually keeps, not the whole bucket.
10
18
  */
19
+ export declare function listHandoffsDetailed(logsDir: string, options?: ListHandoffsOptions): Promise<HandoffEntry[]>;
20
+ /** Same ranking as {@link listHandoffsDetailed}, paths only (no stat calls). */
11
21
  export declare function listHandoffs(logsDir: string, options?: ListHandoffsOptions): Promise<string[]>;
12
22
  //# sourceMappingURL=list-handoffs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-handoffs.d.ts","sourceRoot":"","sources":["../../src/vault/list-handoffs.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,MAAM,EAAE,CAAC,CAwBnB"}
1
+ {"version":3,"file":"list-handoffs.d.ts","sourceRoot":"","sources":["../../src/vault/list-handoffs.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qEAAqE;AACrE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AA6BD;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,YAAY,EAAE,CAAC,CAMzB;AAED,gFAAgF;AAChF,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,MAAM,EAAE,CAAC,CAGnB"}
@@ -1,14 +1,8 @@
1
+ import { stat } from "node:fs/promises";
1
2
  import path from "node:path";
2
3
  import { vaultRelativePath } from "../util/path.js";
3
4
  import { listMarkdownFiles } from "./list-markdown.js";
4
- /**
5
- * Lists handoff markdown files under `logsDir` recursively, newest basename
6
- * first (timestamp-prefixed names sort correctly). Same basename in different
7
- * subfolders ties break by vault-relative path descending.
8
- * Returns absolute paths. Missing or empty dirs yield `[]`.
9
- */
10
- export async function listHandoffs(logsDir, options = {}) {
11
- const mdPaths = await listMarkdownFiles(logsDir);
5
+ function rankHandoffPaths(mdPaths, logsDir) {
12
6
  const ranked = mdPaths.map((filePath) => ({
13
7
  filePath,
14
8
  name: path.basename(filePath),
@@ -20,13 +14,34 @@ export async function listHandoffs(logsDir, options = {}) {
20
14
  }
21
15
  return a.rel < b.rel ? 1 : a.rel > b.rel ? -1 : 0;
22
16
  });
23
- const paths = ranked.map((entry) => entry.filePath);
24
- if (options.limit === undefined) {
25
- return paths;
17
+ return ranked.map((entry) => entry.filePath);
18
+ }
19
+ function applyLimit(paths, limit) {
20
+ if (limit === undefined) {
21
+ return [...paths];
26
22
  }
27
- if (options.limit <= 0) {
23
+ if (limit <= 0) {
28
24
  return [];
29
25
  }
30
- return paths.slice(0, options.limit);
26
+ return paths.slice(0, limit);
27
+ }
28
+ /**
29
+ * Lists handoff markdown files under `logsDir` recursively, newest basename
30
+ * first (timestamp-prefixed names sort correctly), with each entry's mtime
31
+ * attached. Same basename in different subfolders ties break by
32
+ * vault-relative path descending. Missing or empty dirs yield `[]`.
33
+ *
34
+ * Ranking never reads mtime (filenames already sort correctly), so this
35
+ * stats only the paths a `limit` actually keeps, not the whole bucket.
36
+ */
37
+ export async function listHandoffsDetailed(logsDir, options = {}) {
38
+ const mdPaths = await listMarkdownFiles(logsDir);
39
+ const paths = applyLimit(rankHandoffPaths(mdPaths, logsDir), options.limit);
40
+ return Promise.all(paths.map(async (filePath) => ({ path: filePath, mtimeMs: (await stat(filePath)).mtimeMs })));
41
+ }
42
+ /** Same ranking as {@link listHandoffsDetailed}, paths only (no stat calls). */
43
+ export async function listHandoffs(logsDir, options = {}) {
44
+ const mdPaths = await listMarkdownFiles(logsDir);
45
+ return applyLimit(rankHandoffPaths(mdPaths, logsDir), options.limit);
31
46
  }
32
47
  //# sourceMappingURL=list-handoffs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-handoffs.js","sourceRoot":"","sources":["../../src/vault/list-handoffs.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAOvD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAe,EACf,UAA+B,EAAE;IAEjC,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACxC,QAAQ;QACR,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC7B,GAAG,EAAE,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC;KAC1C,CAAC,CAAC,CAAC;IAEJ,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACnB,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;YACtB,OAAO,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClC,CAAC;QACD,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAEpD,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC"}
1
+ {"version":3,"file":"list-handoffs.js","sourceRoot":"","sources":["../../src/vault/list-handoffs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAavD,SAAS,gBAAgB,CAAC,OAA0B,EAAE,OAAe;IACnE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACxC,QAAQ;QACR,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAC7B,GAAG,EAAE,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC;KAC1C,CAAC,CAAC,CAAC;IAEJ,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACnB,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;YACtB,OAAO,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClC,CAAC;QACD,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,UAAU,CAAC,KAAwB,EAAE,KAAyB;IACrE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;IACpB,CAAC;IACD,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QACf,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAe,EACf,UAA+B,EAAE;IAEjC,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,UAAU,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC5E,OAAO,OAAO,CAAC,GAAG,CAChB,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAC7F,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAe,EACf,UAA+B,EAAE;IAEjC,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACjD,OAAO,UAAU,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AACvE,CAAC"}
@@ -2,14 +2,23 @@ export interface ListNotesOptions {
2
2
  /** Max paths to return (newest first). Omit to return all. */
3
3
  limit?: number;
4
4
  }
5
+ /** One note with the mtime already used to rank it (see {@link listNotesDetailed}). */
6
+ export interface NoteEntry {
7
+ path: string;
8
+ mtimeMs: number;
9
+ }
5
10
  /**
6
- * Lists note markdown files under `notesDir` recursively, newest mtime first.
7
- * Returns absolute paths. Missing or empty dirs yield `[]`.
8
- * Ties break by vault-relative path descending for stable output.
11
+ * Lists note markdown files under `notesDir` recursively, newest mtime first,
12
+ * with each entry's mtime attached the same stat this module already does
13
+ * internally to rank notes, just not thrown away after sorting. Missing or
14
+ * empty dirs yield `[]`. Ties break by vault-relative path descending for
15
+ * stable output.
9
16
  *
10
17
  * Matches {@link listPlans} sorting for consistency across list commands
11
18
  * (filename-descending alone is viable for timestamp-prefixed notes, like
12
19
  * {@link listHandoffs}).
13
20
  */
21
+ export declare function listNotesDetailed(notesDir: string, options?: ListNotesOptions): Promise<NoteEntry[]>;
22
+ /** Same ranking as {@link listNotesDetailed}, paths only. */
14
23
  export declare function listNotes(notesDir: string, options?: ListNotesOptions): Promise<string[]>;
15
24
  //# sourceMappingURL=list-notes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-notes.d.ts","sourceRoot":"","sources":["../../src/vault/list-notes.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CAyBnB"}
1
+ {"version":3,"file":"list-notes.d.ts","sourceRoot":"","sources":["../../src/vault/list-notes.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,uFAAuF;AACvF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,SAAS,EAAE,CAAC,CAyBtB;AAED,6DAA6D;AAC7D,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CAEnB"}
@@ -2,15 +2,17 @@ import { stat } from "node:fs/promises";
2
2
  import { vaultRelativePath } from "../util/path.js";
3
3
  import { listMarkdownFiles } from "./list-markdown.js";
4
4
  /**
5
- * Lists note markdown files under `notesDir` recursively, newest mtime first.
6
- * Returns absolute paths. Missing or empty dirs yield `[]`.
7
- * Ties break by vault-relative path descending for stable output.
5
+ * Lists note markdown files under `notesDir` recursively, newest mtime first,
6
+ * with each entry's mtime attached the same stat this module already does
7
+ * internally to rank notes, just not thrown away after sorting. Missing or
8
+ * empty dirs yield `[]`. Ties break by vault-relative path descending for
9
+ * stable output.
8
10
  *
9
11
  * Matches {@link listPlans} sorting for consistency across list commands
10
12
  * (filename-descending alone is viable for timestamp-prefixed notes, like
11
13
  * {@link listHandoffs}).
12
14
  */
13
- export async function listNotes(notesDir, options = {}) {
15
+ export async function listNotesDetailed(notesDir, options = {}) {
14
16
  const mdPaths = await listMarkdownFiles(notesDir);
15
17
  const withMtime = await Promise.all(mdPaths.map(async (filePath) => {
16
18
  const { mtimeMs } = await stat(filePath);
@@ -22,13 +24,17 @@ export async function listNotes(notesDir, options = {}) {
22
24
  }
23
25
  return a.rel < b.rel ? 1 : a.rel > b.rel ? -1 : 0;
24
26
  });
25
- const paths = withMtime.map((entry) => entry.filePath);
27
+ const entries = withMtime.map((entry) => ({ path: entry.filePath, mtimeMs: entry.mtimeMs }));
26
28
  if (options.limit === undefined) {
27
- return paths;
29
+ return entries;
28
30
  }
29
31
  if (options.limit <= 0) {
30
32
  return [];
31
33
  }
32
- return paths.slice(0, options.limit);
34
+ return entries.slice(0, options.limit);
35
+ }
36
+ /** Same ranking as {@link listNotesDetailed}, paths only. */
37
+ export async function listNotes(notesDir, options = {}) {
38
+ return (await listNotesDetailed(notesDir, options)).map((entry) => entry.path);
33
39
  }
34
40
  //# sourceMappingURL=list-notes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-notes.js","sourceRoot":"","sources":["../../src/vault/list-notes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAOvD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,QAAgB,EAChB,UAA4B,EAAE;IAE9B,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CACjC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QAC7B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IAC3E,CAAC,CAAC,CACH,CAAC;IAEF,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACtB,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;YAC5B,OAAO,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;QAC/B,CAAC;QACD,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAEvD,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC"}
1
+ {"version":3,"file":"list-notes.js","sourceRoot":"","sources":["../../src/vault/list-notes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAavD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,QAAgB,EAChB,UAA4B,EAAE;IAE9B,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CACjC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QAC7B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IAC3E,CAAC,CAAC,CACH,CAAC;IAEF,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACtB,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;YAC5B,OAAO,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;QAC/B,CAAC;QACD,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAE7F,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED,6DAA6D;AAC7D,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,QAAgB,EAChB,UAA4B,EAAE;IAE9B,OAAO,CAAC,MAAM,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACjF,CAAC"}
@@ -2,10 +2,19 @@ export interface ListPlansOptions {
2
2
  /** Max paths to return (newest first). Omit to return all. */
3
3
  limit?: number;
4
4
  }
5
+ /** One plan with the mtime already used to rank it (see {@link listPlansDetailed}). */
6
+ export interface PlanEntry {
7
+ path: string;
8
+ mtimeMs: number;
9
+ }
5
10
  /**
6
- * Lists plan markdown files under `plansDir` recursively, newest mtime first.
7
- * Returns absolute paths. Missing or empty dirs yield `[]`.
8
- * Ties break by vault-relative path descending for stable output.
11
+ * Lists plan markdown files under `plansDir` recursively, newest mtime first,
12
+ * with each entry's mtime attached the same stat this module already does
13
+ * internally to rank plans, just not thrown away after sorting. Missing or
14
+ * empty dirs yield `[]`. Ties break by vault-relative path descending for
15
+ * stable output.
9
16
  */
17
+ export declare function listPlansDetailed(plansDir: string, options?: ListPlansOptions): Promise<PlanEntry[]>;
18
+ /** Same ranking as {@link listPlansDetailed}, paths only. */
10
19
  export declare function listPlans(plansDir: string, options?: ListPlansOptions): Promise<string[]>;
11
20
  //# sourceMappingURL=list-plans.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-plans.d.ts","sourceRoot":"","sources":["../../src/vault/list-plans.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CAyBnB"}
1
+ {"version":3,"file":"list-plans.d.ts","sourceRoot":"","sources":["../../src/vault/list-plans.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,uFAAuF;AACvF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,SAAS,EAAE,CAAC,CAyBtB;AAED,6DAA6D;AAC7D,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CAEnB"}
@@ -2,11 +2,13 @@ import { stat } from "node:fs/promises";
2
2
  import { vaultRelativePath } from "../util/path.js";
3
3
  import { listMarkdownFiles } from "./list-markdown.js";
4
4
  /**
5
- * Lists plan markdown files under `plansDir` recursively, newest mtime first.
6
- * Returns absolute paths. Missing or empty dirs yield `[]`.
7
- * Ties break by vault-relative path descending for stable output.
5
+ * Lists plan markdown files under `plansDir` recursively, newest mtime first,
6
+ * with each entry's mtime attached the same stat this module already does
7
+ * internally to rank plans, just not thrown away after sorting. Missing or
8
+ * empty dirs yield `[]`. Ties break by vault-relative path descending for
9
+ * stable output.
8
10
  */
9
- export async function listPlans(plansDir, options = {}) {
11
+ export async function listPlansDetailed(plansDir, options = {}) {
10
12
  const mdPaths = await listMarkdownFiles(plansDir);
11
13
  const withMtime = await Promise.all(mdPaths.map(async (filePath) => {
12
14
  const { mtimeMs } = await stat(filePath);
@@ -18,13 +20,17 @@ export async function listPlans(plansDir, options = {}) {
18
20
  }
19
21
  return a.rel < b.rel ? 1 : a.rel > b.rel ? -1 : 0;
20
22
  });
21
- const paths = withMtime.map((entry) => entry.filePath);
23
+ const entries = withMtime.map((entry) => ({ path: entry.filePath, mtimeMs: entry.mtimeMs }));
22
24
  if (options.limit === undefined) {
23
- return paths;
25
+ return entries;
24
26
  }
25
27
  if (options.limit <= 0) {
26
28
  return [];
27
29
  }
28
- return paths.slice(0, options.limit);
30
+ return entries.slice(0, options.limit);
31
+ }
32
+ /** Same ranking as {@link listPlansDetailed}, paths only. */
33
+ export async function listPlans(plansDir, options = {}) {
34
+ return (await listPlansDetailed(plansDir, options)).map((entry) => entry.path);
29
35
  }
30
36
  //# sourceMappingURL=list-plans.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-plans.js","sourceRoot":"","sources":["../../src/vault/list-plans.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAOvD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,QAAgB,EAChB,UAA4B,EAAE;IAE9B,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CACjC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QAC7B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IAC3E,CAAC,CAAC,CACH,CAAC;IAEF,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACtB,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;YAC5B,OAAO,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;QAC/B,CAAC;QACD,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAEvD,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC"}
1
+ {"version":3,"file":"list-plans.js","sourceRoot":"","sources":["../../src/vault/list-plans.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAavD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,QAAgB,EAChB,UAA4B,EAAE;IAE9B,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CACjC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QAC7B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzC,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IAC3E,CAAC,CAAC,CACH,CAAC;IAEF,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACtB,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;YAC5B,OAAO,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;QAC/B,CAAC;QACD,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAE7F,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED,6DAA6D;AAC7D,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,QAAgB,EAChB,UAA4B,EAAE;IAE9B,OAAO,CAAC,MAAM,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACjF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/vault/search.ts"],"names":[],"mappings":"AAKA,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,kEAAkE;IAClE,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,KAAK,CAAC,EAAE,IAAI,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACpB;AAuOD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAqJhF"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/vault/search.ts"],"names":[],"mappings":"AAKA,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,kEAAkE;IAClE,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,KAAK,CAAC,EAAE,IAAI,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACpB;AAsUD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CA8LhF"}
@@ -80,15 +80,27 @@ function searchMetaPenalty(rootDir, filePath, query) {
80
80
  }
81
81
  return 0;
82
82
  }
83
- function termMatchesLine(line, term) {
84
- if (term.includes(" ")) {
83
+ // Deliberately separate from termMatchesHaystack's dot-aware boundary below —
84
+ // body content has no dotted-pseudo-extension convention to guard against, so
85
+ // plain `\b` is correct here. Do not "unify" the two matchers: a shared regex
86
+ // would either reintroduce `plan` matching `<id>.plan.md` in filenames, or
87
+ // stop `.plan` matching `plan` in body text, silently changing one side.
88
+ /** One compiled matcher per term, built once per search so the per-line scan never recompiles a RegExp. */
89
+ function buildTermMatchers(terms) {
90
+ const matchers = new Map();
91
+ for (const term of terms) {
92
+ matchers.set(term, term.includes(" ") ? null : new RegExp(`\\b${escapeRegex(term)}\\b`, "i"));
93
+ }
94
+ return matchers;
95
+ }
96
+ function termMatchesLine(line, term, matcher) {
97
+ if (matcher === null) {
85
98
  return line.toLowerCase().includes(term.toLowerCase());
86
99
  }
87
- const re = new RegExp(`\\b${escapeRegex(term)}\\b`, "i");
88
- return re.test(line);
100
+ return matcher.test(line);
89
101
  }
90
- function matchingTermsOnLine(line, terms) {
91
- return terms.filter((term) => termMatchesLine(line, term));
102
+ function matchingTermsOnLine(line, terms, matchers) {
103
+ return terms.filter((term) => termMatchesLine(line, term, matchers.get(term) ?? null));
92
104
  }
93
105
  function longestTerm(matches) {
94
106
  return matches.reduce((best, term) => (term.length > best.length ? term : best));
@@ -120,24 +132,79 @@ function buildSnippet(lines, lineIndex, context) {
120
132
  }
121
133
  return parts.join("\n");
122
134
  }
123
- function countFilenameTermMatches(rootDir, filePath, terms) {
124
- const rel = path.relative(rootDir, filePath);
125
- const haystack = rel.replace(/[/\\]/g, " ").replace(/\.md$/i, "").toLowerCase();
126
- let count = 0;
135
+ /**
136
+ * `.` only counts as a word character on the *leading* side of a match
137
+ * (unlike `\b`'s default), so a term can't start right after a dot — this
138
+ * stops a dotted filename convention like `<id>.plan.md` from letting a
139
+ * generic term match its dotted suffix as a standalone word (query "plan"
140
+ * must not match every `*.plan.md` file), while still letting the id itself
141
+ * match (`schema_versioning` must match `schema_versioning.plan.md`) since a
142
+ * term is still allowed to *end* right before a dot. `\w`-based boundaries
143
+ * are otherwise unchanged from before this fix: a hyphen still splits words
144
+ * (`search` matches `search-feature.md`), but `_` does not (`search` does
145
+ * *not* match `search_feature.md` — same as `\b`'s existing behavior).
146
+ */
147
+ function termMatchesHaystack(haystack, lower, matcher) {
148
+ if (matcher === null) {
149
+ return haystack.includes(lower);
150
+ }
151
+ return matcher.test(haystack);
152
+ }
153
+ /** One compiled matcher per lowercased term, built once per search — same rationale as {@link buildTermMatchers}. */
154
+ function buildHaystackMatchers(terms) {
155
+ const matchers = new Map();
127
156
  for (const term of terms) {
128
157
  const lower = term.toLowerCase();
129
- if (lower.includes(" ")) {
130
- if (haystack.includes(lower)) {
131
- count++;
132
- }
133
- continue;
134
- }
135
- const re = new RegExp(`\\b${escapeRegex(lower)}\\b`, "i");
136
- if (re.test(haystack)) {
137
- count++;
158
+ matchers.set(lower, lower.includes(" ") ? null : new RegExp(`(?<![\\w.])${escapeRegex(lower)}(?!\\w)`, "i"));
159
+ }
160
+ return matchers;
161
+ }
162
+ /**
163
+ * Lowercased `terms` that appear in `filePath`'s vault-relative path,
164
+ * including parent directory names (e.g. `plans/p1.md` matches both `plans`
165
+ * and `p1`). Ranking signal only (`filenameTermCount`) — never a matching
166
+ * gate, since a query for a common folder name (`plans`, `notes`, `archive`)
167
+ * would otherwise return every file in that folder regardless of content.
168
+ */
169
+ /** Shared by {@link pathMatchedTerms} and {@link stemMatchedTerms} — only the haystack differs between the two. */
170
+ function matchTermsAgainstHaystack(haystack, terms, haystackMatchers) {
171
+ const matched = new Set();
172
+ for (const term of terms) {
173
+ const lower = term.toLowerCase();
174
+ if (termMatchesHaystack(haystack, lower, haystackMatchers.get(lower) ?? null)) {
175
+ matched.add(lower);
138
176
  }
139
177
  }
140
- return count;
178
+ return matched;
179
+ }
180
+ function pathMatchedTerms(rootDir, filePath, terms, haystackMatchers) {
181
+ const rel = path.relative(rootDir, filePath);
182
+ const haystack = rel.replace(/[/\\]/g, " ").replace(/\.md$/i, "").toLowerCase();
183
+ return matchTermsAgainstHaystack(haystack, terms, haystackMatchers);
184
+ }
185
+ /**
186
+ * Lowercased `terms` that appear in `filePath`'s own filename stem only (no
187
+ * parent directories, no extension) — e.g. `plans/pluggable.md` matches
188
+ * `pluggable` but not `plans`. Narrow enough to gate inclusion for a file
189
+ * with no content match, unlike {@link pathMatchedTerms}.
190
+ */
191
+ /**
192
+ * Strips a leading vault timestamp prefix (`YYYY-MM-DD-HHmm[ss]`, see
193
+ * `dateSecondPrefix`/`dateMinutePrefix` in `timestamp-slug.ts`) so a bare
194
+ * date/time query doesn't stem-match every handoff/log filename — hyphens
195
+ * alone don't stop it, since each numeric segment still matches standalone
196
+ * as a "word" via termMatchesHaystack's boundary check. Strips to empty for
197
+ * a slug-less date-only stem, same as any other narrow-to-nothing case.
198
+ */
199
+ function stripTimestampPrefix(stem) {
200
+ // `_` terminates too, not just `-`/end — a slug-less collision-suffixed
201
+ // stem (`writeUniqueMarkdown`'s `_02`, `_03`, …) attaches directly to the
202
+ // date with no separating hyphen.
203
+ return stem.replace(/^\d{4}-\d{2}-\d{2}-\d{4,6}(-|_|$)/, "");
204
+ }
205
+ function stemMatchedTerms(filePath, terms, haystackMatchers) {
206
+ const stem = stripTimestampPrefix(path.basename(filePath, path.extname(filePath)).toLowerCase());
207
+ return matchTermsAgainstHaystack(stem, terms, haystackMatchers);
141
208
  }
142
209
  function contentHasPhrase(content, query) {
143
210
  const trimmed = query.trim();
@@ -213,6 +280,8 @@ export async function searchVault(options) {
213
280
  };
214
281
  }
215
282
  const filePaths = await listMarkdownFiles(options.rootDir);
283
+ const termMatchers = buildTermMatchers(terms);
284
+ const haystackMatchers = buildHaystackMatchers(terms);
216
285
  const rawFiles = [];
217
286
  let totalMatchCount = 0;
218
287
  // Lowercased doc-freq for ranking; JSON output remaps to original `terms` spelling.
@@ -238,7 +307,7 @@ export async function searchVault(options) {
238
307
  let totalHitCount = 0;
239
308
  for (let i = 0; i < lines.length; i++) {
240
309
  const line = lines[i] ?? "";
241
- const lineMatches = matchingTermsOnLine(line, terms);
310
+ const lineMatches = matchingTermsOnLine(line, terms, termMatchers);
242
311
  if (lineMatches.length === 0) {
243
312
  continue;
244
313
  }
@@ -257,7 +326,37 @@ export async function searchVault(options) {
257
326
  });
258
327
  }
259
328
  }
260
- if (matchedTerms.size > 0) {
329
+ // Path terms (dir segments + stem) stay a ranking-only signal — they must
330
+ // never gate inclusion, or a query for a common folder name (`plans`,
331
+ // `notes`, `archive`) would return every file in that folder regardless
332
+ // of content. The *matcher* underneath did change (shares
333
+ // termMatchesHaystack's dot-boundary fix with stem matching below), so a
334
+ // content-matching `<id>.plan.md` no longer earns filenameTermCount for
335
+ // its dotted suffix — only the gating behavior is unchanged, not the
336
+ // matcher itself.
337
+ const pathTerms = pathMatchedTerms(options.rootDir, filePath, terms, haystackMatchers);
338
+ // Stem terms are narrow enough (the file's own name, not its folder) to
339
+ // justify surfacing a file that has no content match at all — e.g.
340
+ // `pluggable.md`, whose body never repeats the word "pluggable".
341
+ const stemTerms = stemMatchedTerms(filePath, terms, haystackMatchers);
342
+ if (matchedTerms.size === 0 && stemTerms.size > 0) {
343
+ // Every other zero-hit check in the CLI and its consumers (summary
344
+ // text, JSON, the extension's QuickPick) keys off `totalMatchCount`,
345
+ // not file count — count the title match itself so a filename-only
346
+ // result doesn't get reported as "no matches" while still returning a hit.
347
+ totalMatchCount++;
348
+ }
349
+ if (matchedTerms.size > 0 || stemTerms.size > 0) {
350
+ for (const t of stemTerms) {
351
+ // Only stem terms feed doc-freq (not the wider path terms): a term
352
+ // that only ever appears in a title should still show a nonzero
353
+ // `termHitCounts`, so the skill's zero-hit "bad term" broaden check
354
+ // doesn't misfire on it — but a common folder name shouldn't inflate
355
+ // every other term's IDF weighting.
356
+ if (!matchedTerms.has(t)) {
357
+ termDocFreq[t] = (termDocFreq[t] ?? 0) + 1;
358
+ }
359
+ }
261
360
  for (const t of matchedTerms) {
262
361
  termDocFreq[t] = (termDocFreq[t] ?? 0) + 1;
263
362
  }
@@ -266,10 +365,15 @@ export async function searchVault(options) {
266
365
  mtimeMs,
267
366
  topicsMatch,
268
367
  hits: fileHits,
368
+ // Content-only: a filename/stem match justifies inclusion (above)
369
+ // and still earns the flat `filenameTermCount` rank bonus below, but
370
+ // doesn't inflate distinct-term coverage — a file that only matches
371
+ // by title shouldn't outrank one that actually discusses the topic.
269
372
  distinctTermCount: matchedTerms.size,
270
373
  totalHitCount,
271
374
  idfDensity: 0,
272
- filenameTermCount: countFilenameTermMatches(options.rootDir, filePath, terms),
375
+ filenameTermCount: pathTerms.size,
376
+ stemMatchedTerms: stemTerms,
273
377
  phraseMatch: contentHasPhrase(content, options.query),
274
378
  partialPhraseMatch: contentHasPartialPhrase(content, options.query),
275
379
  perTermHits,
@@ -312,7 +416,7 @@ export async function searchVault(options) {
312
416
  topicsMatch: file.topicsMatch,
313
417
  hits: pickBestHits(file.hits, maxHitsPerFile),
314
418
  matchedTerms: terms
315
- .filter((term) => file.perTermHits.has(term.toLowerCase()))
419
+ .filter((term) => file.perTermHits.has(term.toLowerCase()) || file.stemMatchedTerms.has(term.toLowerCase()))
316
420
  .sort((a, b) => b.length - a.length),
317
421
  }));
318
422
  return {