@plebeiantech/lat.md 0.12.3-fork.15 → 0.12.3-fork.17

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 (182) hide show
  1. package/README.md +1 -1
  2. package/dist/src/cli/checklist-menu.d.ts +2 -1
  3. package/dist/src/cli/checklist-menu.js +3 -2
  4. package/dist/src/cli/index.js +59 -16
  5. package/dist/src/cli/init-preferences.d.ts +4 -0
  6. package/dist/src/cli/init-preferences.js +35 -0
  7. package/dist/src/cli/init.js +10 -5
  8. package/dist/src/cli/reindex.js +4 -8
  9. package/dist/src/cli/search.d.ts +3 -2
  10. package/dist/src/cli/search.js +71 -66
  11. package/dist/src/cli/ui-build-server.d.ts +12 -0
  12. package/dist/src/cli/ui-build-server.js +26 -0
  13. package/dist/src/cli/ui-build.js +1 -1
  14. package/dist/src/cli/ui.js +1 -1
  15. package/dist/src/format.d.ts +5 -0
  16. package/dist/src/format.js +32 -4
  17. package/dist/src/lattice-model.d.ts +8 -1
  18. package/dist/src/lattice-model.js +4 -1
  19. package/dist/src/lattice.d.ts +2 -2
  20. package/dist/src/lattice.js +17 -34
  21. package/dist/src/markdown-analysis.d.ts +11 -0
  22. package/dist/src/markdown-analysis.js +31 -1
  23. package/dist/src/mcp/server.js +6 -6
  24. package/dist/src/parser-cache.d.ts +1 -1
  25. package/dist/src/parser-cache.js +1 -1
  26. package/dist/src/search/cache.d.ts +3 -0
  27. package/dist/src/search/cache.js +71 -0
  28. package/dist/src/search/chunks.d.ts +23 -0
  29. package/dist/src/search/chunks.js +197 -0
  30. package/dist/src/search/db.d.ts +27 -16
  31. package/dist/src/search/db.js +108 -38
  32. package/dist/src/search/index.d.ts +5 -2
  33. package/dist/src/search/index.js +173 -69
  34. package/dist/src/search/lexical.d.ts +6 -0
  35. package/dist/src/search/lexical.js +36 -0
  36. package/dist/src/search/lock.d.ts +2 -0
  37. package/dist/src/search/lock.js +23 -0
  38. package/dist/src/search/query.d.ts +6 -3
  39. package/dist/src/search/query.js +45 -26
  40. package/dist/src/search/search.d.ts +13 -8
  41. package/dist/src/search/search.js +155 -23
  42. package/dist/src/search/types.d.ts +31 -0
  43. package/dist/src/search/types.js +1 -0
  44. package/dist/src/source-formats.d.ts +1 -1
  45. package/dist/src/source-formats.js +4 -0
  46. package/dist/src/source-parser.js +397 -1
  47. package/dist/src/view/client/assets/GraphView-BWlRdQM4.js +456 -0
  48. package/dist/src/view/client/assets/{MarkdownEditor-B7quqPb8.js → MarkdownEditor-MW-Ck1O0.js} +8 -8
  49. package/dist/src/view/client/assets/_commonjsHelpers-CqkleIqs.js +1 -0
  50. package/dist/src/view/client/assets/abnfDiagram-VCTEODGH-Bn3RshMV.js +1 -0
  51. package/dist/src/view/client/assets/{arc-BnwR8Bu7.js → arc-En9tdFQF.js} +1 -1
  52. package/dist/src/view/client/assets/architectureDiagram-5GKGNRK7-B-9J1VAM.js +36 -0
  53. package/dist/src/view/client/assets/{blockDiagram-I7D4REHJ-DETl8e_H.js → blockDiagram-I7D4REHJ-QlznwalP.js} +2 -2
  54. package/dist/src/view/client/assets/{c4Diagram-7LVT6UL2-DJRAT1Ve.js → c4Diagram-7LVT6UL2-Bp2y8B4_.js} +2 -2
  55. package/dist/src/view/client/assets/channel-uy0fFvcW.js +1 -0
  56. package/dist/src/view/client/assets/{chunk-2Q5K7J3B-C0TmOC1Q.js → chunk-2Q5K7J3B-CnBKL-lm.js} +1 -1
  57. package/dist/src/view/client/assets/{chunk-5VM5RSS4-dfBOrYE6.js → chunk-5VM5RSS4-LLYMi_n6.js} +1 -1
  58. package/dist/src/view/client/assets/{chunk-F27PBJKO-DAnks99K.js → chunk-F27PBJKO-VIeO7VF0.js} +1 -1
  59. package/dist/src/view/client/assets/{chunk-IMKFNOWR-CPjB07if.js → chunk-IMKFNOWR-D5i67DVD.js} +1 -1
  60. package/dist/src/view/client/assets/{chunk-JWPE2WC7-VvU_gZI6.js → chunk-JWPE2WC7-D4qAtoL3.js} +1 -1
  61. package/dist/src/view/client/assets/{chunk-POPQ4Y6H-DDUbVF55.js → chunk-POPQ4Y6H-BMzOyiW_.js} +1 -1
  62. package/dist/src/view/client/assets/{chunk-SVP7TREG-BHROkr6D.js → chunk-SVP7TREG-DWy5nbfp.js} +1 -1
  63. package/dist/src/view/client/assets/{chunk-TICWLB2K-95PzGIOg.js → chunk-TICWLB2K-CVqCANFB.js} +1 -1
  64. package/dist/src/view/client/assets/{chunk-XXDRQBXY-MdmuLhIs.js → chunk-XXDRQBXY-Bp23h6Tf.js} +1 -1
  65. package/dist/src/view/client/assets/classDiagram-ZZMXUADV-DbSEqjA5.js +1 -0
  66. package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-DbSEqjA5.js +1 -0
  67. package/dist/src/view/client/assets/cose-bilkent-JH36ORCC-BCw7fCC2.js +1 -0
  68. package/dist/src/view/client/assets/{cynefin-OW5HDTMX-CeA8IKWo.js → cynefin-OW5HDTMX--lYN4N9m.js} +1 -1
  69. package/dist/src/view/client/assets/cynefinDiagram-5FMLGOSQ-9sK5EV7v.js +62 -0
  70. package/dist/src/view/client/assets/{dagre-GXQ25YYZ-DLQwTa3H.js → dagre-GXQ25YYZ-CoMdEFCq.js} +2 -2
  71. package/dist/src/view/client/assets/{diagram-S7CK7UJ4-kbKdIyXG.js → diagram-S7CK7UJ4-D13dyWdd.js} +2 -2
  72. package/dist/src/view/client/assets/diagram-UQ7AKVKN-PdwLPPPA.js +41 -0
  73. package/dist/src/view/client/assets/{diagram-VSXAHHWV-EXJTU8qq.js → diagram-VSXAHHWV-CtWWa_A7.js} +2 -2
  74. package/dist/src/view/client/assets/diagram-VX7I27RA-BeCYcSU-.js +24 -0
  75. package/dist/src/view/client/assets/diagram-Z3DM3KII-DiWabOBM.js +24 -0
  76. package/dist/src/view/client/assets/ebnfDiagram-PWID7BFC-CZQ3MD_K.js +1 -0
  77. package/dist/src/view/client/assets/{erDiagram-RLTQ6QDP-C925J5A3.js → erDiagram-RLTQ6QDP-B1AhXIrP.js} +2 -2
  78. package/dist/src/view/client/assets/{flowDiagram-HODETNUW-DzoCa-Nq.js → flowDiagram-HODETNUW-Bhr0JM6P.js} +1 -1
  79. package/dist/src/view/client/assets/ganttDiagram-EL5Y4UJY-CbP8iAkg.js +292 -0
  80. package/dist/src/view/client/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  81. package/dist/src/view/client/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  82. package/dist/src/view/client/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  83. package/dist/src/view/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  84. package/dist/src/view/client/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  85. package/dist/src/view/client/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  86. package/dist/src/view/client/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  87. package/dist/src/view/client/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  88. package/dist/src/view/client/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  89. package/dist/src/view/client/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  90. package/dist/src/view/client/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  91. package/dist/src/view/client/assets/gitGraphDiagram-WWUBYQGX-B9uuGdLF.js +106 -0
  92. package/dist/src/view/client/assets/index-DG0Ez15D.css +1 -0
  93. package/dist/src/view/client/assets/index-nBmbYfun.js +13 -0
  94. package/dist/src/view/client/assets/infoDiagram-27XIBGKW-BrfwQ_Cq.js +2 -0
  95. package/dist/src/view/client/assets/{ishikawaDiagram-5VMMS53U-CW4mto2d.js → ishikawaDiagram-5VMMS53U-CHtJ36re.js} +2 -2
  96. package/dist/src/view/client/assets/{journeyDiagram-3NMN7TZE-8R4eacFW.js → journeyDiagram-3NMN7TZE-BChcgX1s.js} +2 -2
  97. package/dist/src/view/client/assets/{kanban-definition-UXKFOSKX-Cy9ioXEJ.js → kanban-definition-UXKFOSKX-BnwFsKiB.js} +6 -6
  98. package/dist/src/view/client/assets/{layout-C0MFuhth.js → layout-Dnd0pLdb.js} +1 -1
  99. package/dist/src/view/client/assets/{linear-Dv3N4dQR.js → linear-CFhW2ozn.js} +1 -1
  100. package/dist/src/view/client/assets/{maplibre-gl-DnqZgfUS.js → maplibre-gl-CDX6Zuow.js} +1 -1
  101. package/dist/src/view/client/assets/{mermaid.core-bds9A7e2.js → mermaid.core--OP6lXtW.js} +11 -11
  102. package/dist/src/view/client/assets/{mindmap-definition-YA3MSWOX-DjmGW355.js → mindmap-definition-YA3MSWOX-D51y2zbm.js} +2 -2
  103. package/dist/src/view/client/assets/pegDiagram-XKGWAZYB--zyQJDvI.js +1 -0
  104. package/dist/src/view/client/assets/{pieDiagram-E7YTZNPT-CCH2gGR-.js → pieDiagram-E7YTZNPT-Cj8mvpJy.js} +2 -2
  105. package/dist/src/view/client/assets/{quadrantDiagram-AXDQQJYC-CjocPJFz.js → quadrantDiagram-AXDQQJYC-qvhJrnBf.js} +4 -4
  106. package/dist/src/view/client/assets/railroadDiagram-O6MQD6OU-CtWvxpSo.js +1 -0
  107. package/dist/src/view/client/assets/{requirementDiagram-BXWQKSXE-Ct6AoXRr.js → requirementDiagram-BXWQKSXE-CBxen_KG.js} +2 -2
  108. package/dist/src/view/client/assets/{sankeyDiagram-P5KCCOFB-CIpgGN_w.js → sankeyDiagram-P5KCCOFB-B_fUkmfc.js} +2 -2
  109. package/dist/src/view/client/assets/{sequenceDiagram-WJ2MYXX4-DrmLFj6w.js → sequenceDiagram-WJ2MYXX4-Dclv4p68.js} +2 -2
  110. package/dist/src/view/client/assets/sizeCapture-INFHLROL-GfG0sMTZ.js +1 -0
  111. package/dist/src/view/client/assets/stateDiagram-D77RDMKH-YQWPnzxU.js +1 -0
  112. package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-gUj6Hzgl.js +1 -0
  113. package/dist/src/view/client/assets/swimlanes-42K2YHIH-5yiCveVS.js +1 -0
  114. package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-DR7JhrxL.js +8 -0
  115. package/dist/src/view/client/assets/{timeline-definition-24CTP7MA-BExPu6X5.js → timeline-definition-24CTP7MA-BvAbB5Zs.js} +2 -2
  116. package/dist/src/view/client/assets/{vennDiagram-4TSXK5OY-PC1MU3si.js → vennDiagram-4TSXK5OY-guC-r60n.js} +2 -2
  117. package/dist/src/view/client/assets/{wardleyDiagram-VM6X3IG4-DNWqU3fa.js → wardleyDiagram-VM6X3IG4-C0yXcU2x.js} +2 -2
  118. package/dist/src/view/client/assets/{xychartDiagram-S5SC5T6Z-E1uA28_D.js → xychartDiagram-S5SC5T6Z-RPmkPNfK.js} +2 -2
  119. package/dist/src/view/client/index.html +2 -2
  120. package/dist/src/view/document-route.d.ts +2 -0
  121. package/dist/src/view/document-route.js +40 -7
  122. package/dist/src/view/graph.d.ts +2 -2
  123. package/dist/src/view/graph.js +7 -3
  124. package/dist/src/view/highlight.js +207 -203
  125. package/dist/src/view/markdown.js +19 -0
  126. package/dist/src/view/preindexed-search.d.ts +15 -0
  127. package/dist/src/view/preindexed-search.js +25 -0
  128. package/dist/src/view/protocol.d.ts +10 -1
  129. package/dist/src/view/references.js +6 -7
  130. package/dist/src/view/repository.js +10 -2
  131. package/dist/src/view/search-result.d.ts +4 -0
  132. package/dist/src/view/search-result.js +25 -0
  133. package/dist/src/view/search.d.ts +7 -2
  134. package/dist/src/view/search.js +23 -30
  135. package/dist/src/view/server-build.d.ts +21 -0
  136. package/dist/src/view/server-build.js +145 -0
  137. package/dist/src/view/server-deployment.d.ts +32 -0
  138. package/dist/src/view/server-deployment.js +134 -0
  139. package/dist/src/view/server-index-worker.d.ts +6 -0
  140. package/dist/src/view/server-index-worker.js +14 -0
  141. package/dist/src/view/server.js +25 -7
  142. package/dist/src/view/source-target.d.ts +2 -0
  143. package/dist/src/view/source-target.js +24 -0
  144. package/dist/src/view/static-build.d.ts +16 -1
  145. package/dist/src/view/static-build.js +187 -89
  146. package/dist/src/view/static-protocol.d.ts +7 -0
  147. package/dist/src/view/static-protocol.js +5 -0
  148. package/dist/src/view/store.js +17 -7
  149. package/dist/src/view/vercel-build.d.ts +24 -0
  150. package/dist/src/view/vercel-build.js +140 -0
  151. package/dist/src/view/vercel-server-build.d.ts +10 -0
  152. package/dist/src/view/vercel-server-build.js +61 -0
  153. package/dist/vendor/lat.md-server/index.d.ts +1 -1
  154. package/dist/vendor/lat.md-server/index.js +1 -1
  155. package/package.json +20 -5
  156. package/templates/AGENTS.md +1 -1
  157. package/templates/cursor-rules.md +1 -1
  158. package/templates/skill/SKILL.md +1 -1
  159. package/dist/src/view/client/assets/abnfDiagram-VCTEODGH-V7-T6qsl.js +0 -1
  160. package/dist/src/view/client/assets/architectureDiagram-5GKGNRK7-u97vDna9.js +0 -36
  161. package/dist/src/view/client/assets/channel-BwQbnWCN.js +0 -1
  162. package/dist/src/view/client/assets/classDiagram-ZZMXUADV-HvQKkIS-.js +0 -1
  163. package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-HvQKkIS-.js +0 -1
  164. package/dist/src/view/client/assets/cose-bilkent-JH36ORCC-CauUf_rN.js +0 -1
  165. package/dist/src/view/client/assets/cynefinDiagram-5FMLGOSQ-L1rtcT8l.js +0 -62
  166. package/dist/src/view/client/assets/diagram-UQ7AKVKN-kmzp92RC.js +0 -41
  167. package/dist/src/view/client/assets/diagram-VX7I27RA-DAiIyMCG.js +0 -24
  168. package/dist/src/view/client/assets/diagram-Z3DM3KII-G9wz50ZV.js +0 -24
  169. package/dist/src/view/client/assets/ebnfDiagram-PWID7BFC-D2tVRCNA.js +0 -1
  170. package/dist/src/view/client/assets/ganttDiagram-EL5Y4UJY-D2ebbg4p.js +0 -292
  171. package/dist/src/view/client/assets/gitGraphDiagram-WWUBYQGX-B4QratYy.js +0 -106
  172. package/dist/src/view/client/assets/index-D0ajr_Ja.css +0 -1
  173. package/dist/src/view/client/assets/index-KZcnpXJH.js +0 -321
  174. package/dist/src/view/client/assets/infoDiagram-27XIBGKW-DL4mtHkt.js +0 -2
  175. package/dist/src/view/client/assets/pegDiagram-XKGWAZYB-BWsZp0ab.js +0 -1
  176. package/dist/src/view/client/assets/railroadDiagram-O6MQD6OU-BjLeb2KF.js +0 -1
  177. package/dist/src/view/client/assets/sizeCapture-INFHLROL-BdFMe2Lr.js +0 -1
  178. package/dist/src/view/client/assets/stateDiagram-D77RDMKH-Zd9F1goW.js +0 -1
  179. package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-ByIKCftl.js +0 -1
  180. package/dist/src/view/client/assets/swimlanes-42K2YHIH-a7VFwi00.js +0 -1
  181. package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-CzxaUHB-.js +0 -8
  182. /package/dist/src/view/client/assets/{logo-small-RR_KBOAT.svg → logo-RR_KBOAT.svg} +0 -0
package/README.md CHANGED
@@ -103,7 +103,7 @@ Alternatively, install `@plebeiantech/lat.md` via npm/mise, run `lat init` in yo
103
103
 
104
104
  ## How it works
105
105
 
106
- Run `lat init` to scaffold a `lat.md/` directory, then write markdown files describing your architecture, business logic, test specs — whatever matters. Link between sections using `[[file#Section#Subsection]]` syntax. Link to source code symbols with `[[src/auth.ts#validateToken]]`. Annotate source code with `// @lat: [[section-id]]` (or `# @lat: [[section-id]]` in Python) comments to tie implementation back to concepts.
106
+ Run `lat init` to scaffold a `lat.md/` directory, then write markdown files describing your architecture, business logic, test specs — whatever matters. Link between sections using `[[file#Section#Subsection]]` syntax. Link to source code symbols with `[[src/auth.ts#validateToken]]`. Annotate source code with `// @lat: [[section-id]]` (or `# @lat: [[section-id]]` in Python and PHP) comments to tie implementation back to concepts.
107
107
 
108
108
  ```
109
109
  my-project/
@@ -5,8 +5,9 @@ export interface ChecklistOption {
5
5
  /**
6
6
  * Display an interactive multi-select checklist with arrow-key navigation.
7
7
  * Returns an array of checked values.
8
+ * Known initial values start checked; unknown values are ignored.
8
9
  *
9
10
  * Keys: Up/Down (j/k) to move, Space to toggle, Enter to confirm, Ctrl+C to exit.
10
11
  * Non-TTY fallback: returns [].
11
12
  */
12
- export declare function checklistMenu(options: ChecklistOption[], prompt?: string): Promise<string[]>;
13
+ export declare function checklistMenu(options: ChecklistOption[], prompt?: string, initialValues?: readonly string[]): Promise<string[]>;
@@ -2,18 +2,19 @@ import { styleText } from 'node:util';
2
2
  /**
3
3
  * Display an interactive multi-select checklist with arrow-key navigation.
4
4
  * Returns an array of checked values.
5
+ * Known initial values start checked; unknown values are ignored.
5
6
  *
6
7
  * Keys: Up/Down (j/k) to move, Space to toggle, Enter to confirm, Ctrl+C to exit.
7
8
  * Non-TTY fallback: returns [].
8
9
  */
9
- export async function checklistMenu(options, prompt) {
10
+ export async function checklistMenu(options, prompt, initialValues = []) {
10
11
  if (options.length === 0)
11
12
  return [];
12
13
  if (!process.stdin.isTTY)
13
14
  return [];
14
15
  return new Promise((resolve) => {
15
16
  let cursor = 0;
16
- const checked = new Set();
17
+ const checked = new Set(options.flatMap((option, index) => initialValues.includes(option.value) ? [index] : []));
17
18
  const stdin = process.stdin;
18
19
  const wasRaw = stdin.isRaw;
19
20
  stdin.setRawMode(true);
@@ -8,7 +8,7 @@ import { dirname, join } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { Command, InvalidArgumentError } from 'commander';
10
10
  import { resolveCheckContext, resolveContext } from './context.js';
11
- import { DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_THRESHOLD, } from '../search/search.js';
11
+ import { DEFAULT_SEARCH_LIMIT, DEFAULT_MIN_SIMILARITY, } from '../search/search.js';
12
12
  function parsePort(value) {
13
13
  if (!/^\d+$/.test(value)) {
14
14
  throw new InvalidArgumentError('port must be an integer from 1 to 65535');
@@ -19,10 +19,22 @@ function parsePort(value) {
19
19
  }
20
20
  return port;
21
21
  }
22
+ function parseUiServerBuildTarget(value) {
23
+ if (value !== 'node' && value !== 'vercel') {
24
+ throw new InvalidArgumentError('target must be node or vercel');
25
+ }
26
+ return value;
27
+ }
28
+ function configureUiRun(command) {
29
+ return command
30
+ .option('--logo-text <text>', 'top-left logo text')
31
+ .option('--no-git', 'disable Git working-tree integration')
32
+ .option('--port <number>', 'server port (default: 4242; explicit ports are strict)', parsePort);
33
+ }
22
34
  function parseSimilarityThreshold(value) {
23
35
  const threshold = Number(value);
24
36
  if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
25
- throw new InvalidArgumentError('threshold must be a number from 0 to 1');
37
+ throw new InvalidArgumentError('min-similarity must be a number from 0 to 1');
26
38
  }
27
39
  return threshold;
28
40
  }
@@ -108,27 +120,52 @@ program
108
120
  const { sectionCommand } = await import('./section.js');
109
121
  handleResult(await sectionCommand(ctx, query));
110
122
  });
111
- const ui = program
112
- .command('ui')
113
- .description('Open lat.md in a local browser')
114
- .option('--logo-text <text>', 'top-left logo text')
115
- .option('--port <number>', 'server port (default: 4242; explicit ports are strict)', parsePort)
116
- .action(async (opts) => {
123
+ async function runUi(opts) {
117
124
  const ctx = resolveContext(program.opts());
118
125
  const { uiCommand } = await import('./ui.js');
119
- handleResult(await uiCommand(ctx, { logoText: opts.logoText, port: opts.port }));
126
+ handleResult(await uiCommand(ctx, {
127
+ git: opts.git,
128
+ logoText: opts.logoText,
129
+ port: opts.port,
130
+ }));
131
+ }
132
+ const ui = configureUiRun(program.command('ui').description('Run or build the Lat UI')).action(runUi);
133
+ configureUiRun(ui.command('run').description('Run the local Lat UI server')).action(async (opts) => {
134
+ await runUi(opts);
120
135
  });
121
- ui.command('build')
122
- .description('Export lat.md as a static website')
123
- .argument('[output]', 'output directory relative to the project', 'lat-ui')
136
+ const uiBuild = ui.command('build').description('Build a deployable Lat UI');
137
+ uiBuild
138
+ .command('static')
139
+ .description('Build a fully static, read-only Lat UI')
140
+ .argument('[output]', 'output directory relative to the project (default: .lat-build/static)', '.lat-build/static')
124
141
  .option('--base <path>', 'deployment base path', '/')
142
+ .option('--force', 'replace an existing output path')
125
143
  .option('--logo-text <text>', 'top-left logo text')
126
144
  .action(async (output, opts) => {
127
145
  const ctx = resolveContext(program.opts());
128
146
  const { uiBuildCommand } = await import('./ui-build.js');
129
147
  handleResult(await uiBuildCommand(ctx, output, {
130
148
  basePath: opts.base,
149
+ force: opts.force,
150
+ logoText: opts.logoText ?? ui.opts().logoText,
151
+ }));
152
+ });
153
+ uiBuild
154
+ .command('server')
155
+ .description('Build a static Lat UI with a portable search server')
156
+ .argument('[output]', 'output directory (default: .lat-build/server for node, .vercel/output for vercel)')
157
+ .option('--base <path>', 'deployment base path', '/')
158
+ .option('--force', 'replace an existing output path')
159
+ .option('--logo-text <text>', 'top-left logo text')
160
+ .option('--target <target>', 'deployment target: node or vercel', parseUiServerBuildTarget, 'node')
161
+ .action(async (output, opts) => {
162
+ const ctx = resolveContext(program.opts());
163
+ const { uiBuildServerCommand } = await import('./ui-build-server.js');
164
+ handleResult(await uiBuildServerCommand(ctx, output, {
165
+ basePath: opts.base,
166
+ force: opts.force,
131
167
  logoText: opts.logoText ?? ui.opts().logoText,
168
+ target: opts.target,
132
169
  }));
133
170
  });
134
171
  program
@@ -302,11 +339,16 @@ program
302
339
  });
303
340
  program
304
341
  .command('search')
305
- .description('Semantic search across lat.md sections')
342
+ .description('Hybrid search across lat.md sections')
306
343
  .argument('[query]', 'search query in plain English')
307
344
  .option('--limit <n>', `max results (default: ${DEFAULT_SEARCH_LIMIT})`, String(DEFAULT_SEARCH_LIMIT))
308
- .option('--debug', 'show result similarity scores')
309
- .option('--threshold <score>', `minimum cosine similarity score (default: ${DEFAULT_SEARCH_THRESHOLD})`, parseSimilarityThreshold)
345
+ .option('--preview <variant>', 'preview passage, intro, or both', (value) => {
346
+ if (!['passage', 'intro', 'both'].includes(value))
347
+ throw new InvalidArgumentError('preview must be passage, intro, or both');
348
+ return value;
349
+ }, 'passage')
350
+ .option('--debug', 'show retrieval scores and candidate diagnostics')
351
+ .option('--min-similarity <score>', `minimum cosine similarity score (default: ${DEFAULT_MIN_SIMILARITY})`, parseSimilarityThreshold)
310
352
  .action(async (query, opts) => {
311
353
  const ctx = resolveContext(program.opts());
312
354
  const { searchCommand, cliProgress } = await import('./search.js');
@@ -314,7 +356,8 @@ program
314
356
  const result = await searchCommand(ctx, query, {
315
357
  limit: parseInt(opts.limit),
316
358
  debug: opts.debug,
317
- threshold: opts.threshold,
359
+ minSimilarity: opts.minSimilarity,
360
+ preview: opts.preview,
318
361
  }, progress);
319
362
  handleResult(result);
320
363
  });
@@ -0,0 +1,4 @@
1
+ /** Read machine-local checklist defaults without inferring installed agents. */
2
+ export declare function readInitAgents(latDir: string): string[];
3
+ /** Update just the confirmed selection, preserving other YAML and comments. */
4
+ export declare function writeInitAgents(latDir: string, agents: string[]): void;
@@ -0,0 +1,35 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { isMap, parseDocument } from 'yaml';
4
+ function readPreferences(latDir) {
5
+ const path = join(latDir, 'config.local.yaml');
6
+ const document = parseDocument(existsSync(path) ? readFileSync(path, 'utf8') : '');
7
+ if (document.errors.length > 0) {
8
+ throw new Error(`Cannot parse ${path}: ${document.errors[0].message}`);
9
+ }
10
+ if (document.contents !== null && !isMap(document.contents)) {
11
+ throw new Error(`${path} must be a mapping`);
12
+ }
13
+ if (document.has('init') && !isMap(document.get('init', true))) {
14
+ throw new Error(`${path}: init must be a mapping`);
15
+ }
16
+ return { path, document };
17
+ }
18
+ /** Read machine-local checklist defaults without inferring installed agents. */
19
+ export function readInitAgents(latDir) {
20
+ const { path, document } = readPreferences(latDir);
21
+ const agents = document.toJS()?.init?.agents;
22
+ if (agents === undefined)
23
+ return [];
24
+ if (!Array.isArray(agents) ||
25
+ agents.some((agent) => typeof agent !== 'string')) {
26
+ throw new Error(`${path}: init.agents must be a list of strings`);
27
+ }
28
+ return agents;
29
+ }
30
+ /** Update just the confirmed selection, preserving other YAML and comments. */
31
+ export function writeInitAgents(latDir, agents) {
32
+ const { path, document } = readPreferences(latDir);
33
+ document.setIn(['init', 'agents'], agents);
34
+ writeFileSync(path, document.toString());
35
+ }
@@ -8,7 +8,7 @@ import { findTemplatesDir } from './templates.js';
8
8
  import { readAgentsTemplate, readCursorRulesTemplate, readPiExtensionTemplate, readOpenCodePluginTemplate, readSkillTemplate, } from './gen.js';
9
9
  import { getLlmKey, getRepoEmbedding, setRepoEmbedding } from '../config.js';
10
10
  import { makeStyler } from './context.js';
11
- import { closeDb, ensureMeta, getStoredModel, openDb } from '../search/db.js';
11
+ import { closeDb, getStoredModel, openDb } from '../search/db.js';
12
12
  import { modelKey } from '../search/embedder.js';
13
13
  import { reindexCommand } from './reindex.js';
14
14
  import { INIT_VERSION, writeInitMeta, readInitVersion, readFileHash, contentHash, } from '../init-version.js';
@@ -18,6 +18,7 @@ import { checklistMenu } from './checklist-menu.js';
18
18
  import { writeForkInstructions } from './fork-instructions.js';
19
19
  import { offerRequireMode, writeForkScaffold } from './fork-scaffold.js';
20
20
  import { setupAntigravity } from '../fork/antigravity-init.js';
21
+ import { readInitAgents, writeInitAgents } from './init-preferences.js';
21
22
  async function confirm(rl, message) {
22
23
  while (true) {
23
24
  let answer;
@@ -667,11 +668,10 @@ async function setupCodex(root, latDir, hashes, ask, style) {
667
668
  ' Run /hooks in Codex to review and trust the project hooks.');
668
669
  }
669
670
  async function readStoredEmbeddingModel(latDir) {
670
- if (!existsSync(join(latDir, '.cache', 'vectors.db')))
671
+ if (!existsSync(join(latDir, '.cache', 'search.db')))
671
672
  return null;
672
- const db = openDb(latDir);
673
+ const db = openDb(latDir, undefined, true);
673
674
  try {
674
- await ensureMeta(db);
675
675
  return await getStoredModel(db);
676
676
  }
677
677
  finally {
@@ -916,6 +916,7 @@ export async function initCmd(targetDir) {
916
916
  writeForkScaffold(latDir);
917
917
  }
918
918
  ensureLatLocalConfigIgnored(latDir);
919
+ ensureGitignored(root, '.lat-build');
919
920
  // Step 2: Configure fresh/outdated setups, ask interactive users about an
920
921
  // available key, and offer to rebuild an index whose backend differs. This
921
922
  // happens before agent selection so "no agents" still completes it.
@@ -931,7 +932,7 @@ export async function initCmd(targetDir) {
931
932
  { label: 'Codex', value: 'codex' },
932
933
  { label: 'Antigravity / Gemini', value: 'antigravity' },
933
934
  ];
934
- const selectedAgents = await checklistMenu(allAgents, 'Which coding agents do you use?');
935
+ const selectedAgents = await checklistMenu(allAgents, 'Which coding agents do you use?', readInitAgents(latDir));
935
936
  const useClaudeCode = selectedAgents.includes('claude');
936
937
  const usePi = selectedAgents.includes('pi');
937
938
  const useCursor = selectedAgents.includes('cursor');
@@ -978,6 +979,8 @@ export async function initCmd(targetDir) {
978
979
  // agent. Stamp the version so future non-interactive runs do not reapply
979
980
  // fresh/outdated defaults and overwrite the chosen backend.
980
981
  writeInitMeta(latDir, {});
982
+ if (interactive)
983
+ writeInitAgents(latDir, selectedAgents);
981
984
  console.log('');
982
985
  console.log(styleText('dim', 'No agents selected. You can re-run') +
983
986
  ' lat init ' +
@@ -1049,6 +1052,8 @@ export async function initCmd(targetDir) {
1049
1052
  await writeForkInstructions(root, latDir, fileHashes, ask);
1050
1053
  // Record init version and file hashes so `lat check` can detect stale setups
1051
1054
  writeInitMeta(latDir, fileHashes);
1055
+ if (interactive)
1056
+ writeInitAgents(latDir, selectedAgents);
1052
1057
  console.log('');
1053
1058
  console.log(styleText('green', 'Done!') +
1054
1059
  ' Run ' +
@@ -1,5 +1,6 @@
1
+ import { writeIndex } from '../search/cache.js';
1
2
  import readline from 'node:readline/promises';
2
- import { openDb, ensureMeta, setStoredModel, ensureSectionsSchema, dropSections, closeDb, } from '../search/db.js';
3
+ import { ensureMeta, setStoredModel, ensureSectionsSchema, dropSections, } from '../search/db.js';
3
4
  import { embedderFromEnv, localEmbedder, modelKey, EmbeddingAuthError, } from '../search/embedder.js';
4
5
  import { getLlmKey, getRepoEmbedding, setRepoEmbedding } from '../config.js';
5
6
  import { indexSections } from '../search/index.js';
@@ -83,11 +84,9 @@ export async function reindexCommand(ctx, opts) {
83
84
  embedder = await localEmbedder();
84
85
  }
85
86
  }
86
- const db = openDb(ctx.latDir);
87
87
  const interactive = ctx.mode === 'cli' && !!process.stderr.isTTY;
88
- try {
88
+ return writeIndex(ctx.latDir, undefined, true, async (db) => {
89
89
  await ensureMeta(db);
90
- await dropSections(db);
91
90
  await ensureSectionsSchema(db, embedder.dimensions);
92
91
  const label = `Reindexing with ${embedder.name}`;
93
92
  // Interactive terminals get a live progress line driven per embed-chunk
@@ -130,8 +129,5 @@ export async function reindexCommand(ctx, opts) {
130
129
  output: s.green(`Reindexed ${stats.added} sections`) +
131
130
  ` using ${s.cyan(embedder.name)}.`,
132
131
  };
133
- }
134
- finally {
135
- await closeDb(db);
136
- }
132
+ });
137
133
  }
@@ -27,7 +27,7 @@ export declare function runSearch(latDir: string, query: string, limit: number,
27
27
  buildIndex?: boolean;
28
28
  project?: MarkdownProjectAnalysis;
29
29
  sectionById?: ReadonlyMap<string, Section>;
30
- threshold?: number;
30
+ minSimilarity?: number;
31
31
  cacheDir?: string;
32
32
  }): Promise<SearchResult>;
33
33
  /**
@@ -41,5 +41,6 @@ export declare function cliProgress(s: Styler): IndexProgress;
41
41
  export declare function searchCommand(ctx: CmdContext, query: string | undefined, opts: {
42
42
  limit: number;
43
43
  debug?: boolean;
44
- threshold?: number;
44
+ minSimilarity?: number;
45
+ preview?: 'passage' | 'intro' | 'both';
45
46
  }, progress?: IndexProgress): Promise<CmdResult>;
@@ -1,71 +1,55 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { LEXICAL_VERSION } from '../search/lexical.js';
3
+ import { hasIndex } from '../search/db.js';
4
+ import { embeddingFingerprint } from '../search/chunks.js';
5
+ import { writeIndex } from '../search/cache.js';
2
6
  import { dirname, join } from 'node:path';
3
- import { openDb, ensureMeta, getStoredModel, setStoredModel, ensureSectionsSchema, dropSections, closeDb, } from '../search/db.js';
4
- import { getLlmKey } from '../config.js';
7
+ import { openDb, getStoredModel, setStoredModel, ensureSectionsSchema, closeDb, } from '../search/db.js';
5
8
  import { embedderForIndex, modelKey, ReindexRequiredError, EmbeddingAuthError, } from '../search/embedder.js';
6
- import { indexSections } from '../search/index.js';
9
+ import { indexSections, projectFingerprint, } from '../search/index.js';
7
10
  import { searchSections } from '../search/search.js';
8
- import { searchIndexedSections } from '../search/query.js';
11
+ import { resolveSearchMatches, searchIndexedSections, } from '../search/query.js';
9
12
  import { analyzeMarkdownProject, commandProjectAnalysis, } from '../project-analysis.js';
10
13
  import { formatSectionPreview, formatNavHints } from '../format.js';
11
14
  import { provenanceNote, formatProvenanceNote } from './check-status.js';
12
15
  async function withDb(latDir, progress, project, cacheDir, fn) {
13
- const db = openDb(latDir, cacheDir);
14
- try {
15
- await ensureMeta(db);
16
- const stored = await getStoredModel(db);
17
- // The stored model is authoritative — no silent backend switch. Throws
18
- // ReindexRequiredError if the environment can't serve the stored index.
19
- // Rebuilding / switching backends is `lat reindex`, never `lat search`.
20
- const embedder = await embedderForIndex(stored, latDir);
21
- await ensureSectionsSchema(db, embedder.dimensions);
22
- const countResult = await db.execute('SELECT COUNT(*) as n FROM sections');
23
- let isEmpty = countResult.rows[0].n === 0;
24
- // Legacy cache: a version before the model was recorded left rows behind
25
- // with no `meta.embedding_model`. Those vectors may be a different backend
26
- // (and dimension) than the resolved embedder, and `CREATE TABLE IF NOT
27
- // EXISTS` won't migrate the column width — so drop and rebuild from scratch
28
- // under the resolved backend rather than querying a mismatched table.
29
- if (!stored && !isEmpty) {
30
- await dropSections(db);
31
- await ensureSectionsSchema(db, embedder.dimensions);
32
- isEmpty = true;
33
- }
34
- // If the repo is pinned to local but a key is set, say so — otherwise it
35
- // looks like the key is being silently ignored.
36
- if (isEmpty && embedder.name.startsWith('local:') && process.stderr.isTTY) {
37
- let hasKey = false;
38
- try {
39
- hasKey = !!getLlmKey();
40
- }
41
- catch {
42
- /* key source misconfigured — irrelevant, we're local */
43
- }
44
- if (hasKey) {
45
- process.stderr.write('This repo is configured for local embeddings; ignoring LAT_LLM_KEY.\n');
46
- }
47
- }
48
- progress?.beforeIndex?.(isEmpty);
16
+ if (hasIndex(cacheDir ?? join(latDir, '.cache'))) {
17
+ const db = openDb(latDir, cacheDir, true);
49
18
  try {
50
- const stats = await indexSections(latDir, db, embedder, undefined, project);
51
- // Pin the backend only after a successful index, so a failed build never
52
- // leaves the repo wrongly pinned to an empty index.
53
- if (!stored)
54
- await setStoredModel(db, modelKey(embedder));
55
- progress?.afterIndex?.(stats, isEmpty);
19
+ const stored = await getStoredModel(db);
20
+ const embedder = await embedderForIndex(stored, latDir);
21
+ const metadata = new Map((await db.execute('SELECT key,value FROM meta')).rows.map((row) => [
22
+ row.key,
23
+ row.value,
24
+ ]));
25
+ if (metadata.get('fingerprint') !== embeddingFingerprint(embedder))
26
+ throw new ReindexRequiredError('Search chunking or embedding model changed; run lat reindex.');
27
+ if (stored &&
28
+ metadata.get('lexical_version') === LEXICAL_VERSION &&
29
+ metadata.get('project_hash') === projectFingerprint(project)) {
30
+ progress?.afterIndex?.({
31
+ added: 0,
32
+ updated: 0,
33
+ removed: 0,
34
+ unchanged: project.sections.length,
35
+ }, false);
36
+ return await fn(db, embedder, project);
37
+ }
56
38
  }
57
- catch (err) {
58
- // Failed fresh build → drop the half-created table so the next run is
59
- // truly fresh (re-resolves the backend cleanly) rather than stuck.
60
- if (!stored)
61
- await dropSections(db);
62
- throw err;
39
+ finally {
40
+ await closeDb(db);
63
41
  }
64
- return await fn(db, embedder, project);
65
- }
66
- finally {
67
- await closeDb(db);
68
42
  }
43
+ return writeIndex(latDir, cacheDir, false, async (db, stored) => {
44
+ const embedder = await embedderForIndex(stored, latDir);
45
+ await ensureSectionsSchema(db, embedder.dimensions);
46
+ const isEmpty = (await db.execute('SELECT COUNT(*) AS n FROM sections')).rows[0].n === 0;
47
+ progress?.beforeIndex?.(isEmpty);
48
+ const stats = await indexSections(latDir, db, embedder, undefined, project);
49
+ await setStoredModel(db, modelKey(embedder));
50
+ progress?.afterIndex?.(stats, isEmpty);
51
+ return fn(db, embedder, project);
52
+ });
69
53
  }
70
54
  /**
71
55
  * Run a semantic search across lat.md sections.
@@ -86,7 +70,7 @@ export async function runSearch(latDir, query, limit, progress, opts) {
86
70
  })).sectionById;
87
71
  return searchIndexedSections(latDir, query, limit, sectionById, {
88
72
  cacheDir: opts.cacheDir,
89
- threshold: opts.threshold,
73
+ minSimilarity: opts.minSimilarity,
90
74
  });
91
75
  }
92
76
  const project = opts?.project ??
@@ -94,15 +78,10 @@ export async function runSearch(latDir, query, limit, progress, opts) {
94
78
  executor: 'auto',
95
79
  }));
96
80
  return withDb(latDir, progress, project, opts?.cacheDir, async (db, embedder, analyzed) => {
97
- const results = await searchSections(db, query, embedder, limit, opts?.threshold);
81
+ const results = await searchSections(db, query, embedder, limit, opts?.minSimilarity);
98
82
  return {
99
83
  query,
100
- matches: results.flatMap((result) => {
101
- const section = analyzed.sectionById.get(result.id.toLowerCase());
102
- return section
103
- ? [{ section, reason: 'semantic match', score: result.score }]
104
- : [];
105
- }),
84
+ matches: resolveSearchMatches(results, analyzed.sectionById),
106
85
  };
107
86
  });
108
87
  }
@@ -163,7 +142,32 @@ async function formatSearchMatches(ctx, query, matches, opts) {
163
142
  lines.push('');
164
143
  lines.push(formatSectionPreview(ctx, matches[i].section, {
165
144
  reason: matches[i].reason,
166
- score: opts?.showScores ? matches[i].score : undefined,
145
+ spans: opts?.preview === 'intro'
146
+ ? undefined
147
+ : matches[i].evidence?.[0]?.spans,
148
+ score: opts?.showScores ? matches[i].rankScore : undefined,
149
+ text: opts?.preview === 'intro'
150
+ ? undefined
151
+ : matches[i].evidence?.length
152
+ ? (opts?.preview === 'both'
153
+ ? matches[i].section.firstParagraph + '\n\n'
154
+ : '') + matches[i].evidence[0].text
155
+ : undefined,
156
+ debug: opts?.showScores && matches[i].rankScore !== undefined
157
+ ? JSON.stringify({
158
+ lexicalRank: matches[i].lexicalRank,
159
+ semanticRank: matches[i].semanticRank,
160
+ lexicalScore: matches[i].lexicalScore,
161
+ semanticSimilarity: matches[i].semanticSimilarity,
162
+ lexicalContribution: matches[i].lexicalRank
163
+ ? 1 / (60 + matches[i].lexicalRank)
164
+ : 0,
165
+ semanticContribution: matches[i].semanticRank
166
+ ? 1 / (60 + matches[i].semanticRank)
167
+ : 0,
168
+ ...matches[i].diagnostics,
169
+ })
170
+ : undefined,
167
171
  }));
168
172
  const note = notes.get(matches[i].section.filePath);
169
173
  if (note)
@@ -182,7 +186,7 @@ export async function searchCommand(ctx, query, opts, progress) {
182
186
  const project = await commandProjectAnalysis(ctx);
183
187
  const result = await runSearch(ctx.latDir, query, opts.limit, progress, {
184
188
  project,
185
- threshold: opts.threshold,
189
+ minSimilarity: opts.minSimilarity,
186
190
  });
187
191
  if (result.matches.length === 0) {
188
192
  return { output: 'No results found.' };
@@ -190,6 +194,7 @@ export async function searchCommand(ctx, query, opts, progress) {
190
194
  return {
191
195
  output: (await formatSearchMatches(ctx, query, result.matches, {
192
196
  showScores: opts.debug,
197
+ preview: opts.preview ?? 'passage',
193
198
  })) + formatNavHints(ctx),
194
199
  };
195
200
  }
@@ -0,0 +1,12 @@
1
+ import type { CmdContext, CmdResult } from '../context.js';
2
+ import { buildServerView, type ServerViewBuildOptions, type ServerViewBuildResult } from '../view/server-build.js';
3
+ export type ServerViewBuildTarget = 'node' | 'vercel';
4
+ export type UiBuildServerOptions = ServerViewBuildOptions & {
5
+ target?: ServerViewBuildTarget;
6
+ };
7
+ export type UiBuildServerDependencies = {
8
+ buildNode: typeof buildServerView;
9
+ buildVercel?: (ctx: CmdContext, output: string, options: ServerViewBuildOptions) => Promise<ServerViewBuildResult>;
10
+ };
11
+ /** Export immutable UI data with a Node or Vercel semantic-search service. */
12
+ export declare function uiBuildServerCommand(ctx: CmdContext, output: string | undefined, options?: UiBuildServerOptions, dependencies?: UiBuildServerDependencies): Promise<CmdResult>;
@@ -0,0 +1,26 @@
1
+ import { buildServerView, } from '../view/server-build.js';
2
+ const defaultDependencies = {
3
+ buildNode: buildServerView,
4
+ };
5
+ /** Export immutable UI data with a Node or Vercel semantic-search service. */
6
+ export async function uiBuildServerCommand(ctx, output, options = {}, dependencies = defaultDependencies) {
7
+ try {
8
+ const { target = 'node', ...buildOptions } = options;
9
+ const resolvedOutput = output ?? (target === 'vercel' ? '.vercel/output' : '.lat-build/server');
10
+ let result;
11
+ if (target === 'vercel') {
12
+ const buildVercel = dependencies.buildVercel ??
13
+ (await import('../view/vercel-server-build.js')).buildVercelServerView;
14
+ result = await buildVercel(ctx, resolvedOutput, buildOptions);
15
+ }
16
+ else {
17
+ result = await dependencies.buildNode(ctx, resolvedOutput, buildOptions);
18
+ }
19
+ return {
20
+ output: `Built ${result.documents} documents and ${result.sources} source views for ${target} at ${result.outputDir}`,
21
+ };
22
+ }
23
+ catch (error) {
24
+ return { output: error.message, isError: true };
25
+ }
26
+ }
@@ -1,6 +1,6 @@
1
1
  import { buildStaticView, } from '../view/static-build.js';
2
2
  /** Export the current vault as a serverless, read-only Lat UI. */
3
- export async function uiBuildCommand(ctx, output = 'lat-ui', options = {}) {
3
+ export async function uiBuildCommand(ctx, output = '.lat-build/static', options = {}) {
4
4
  try {
5
5
  const result = await buildStaticView(ctx, output, options);
6
6
  return {
@@ -38,7 +38,7 @@ export async function uiCommand(ctx, options = {}) {
38
38
  options.onStarted?.(server);
39
39
  const lines = [
40
40
  `Viewing lat.md at ${server.url}`,
41
- 'Note: you can use `lat ui build` to build a static version of the UI',
41
+ 'Note: use `lat ui build static` or `lat ui build server` to build a deployable UI',
42
42
  ];
43
43
  try {
44
44
  await (options.openBrowser ?? openBrowser)(server.url);
@@ -1,11 +1,16 @@
1
+ import type { SourceSpan } from './search/types.js';
1
2
  import type { Section, SectionMatch } from './lattice-model.js';
2
3
  import type { CmdContext, Styler } from './context.js';
3
4
  export declare function formatSectionId(id: string, s: Styler): string;
4
5
  export declare function formatSectionPreview(ctx: CmdContext, section: Section, opts?: {
5
6
  reason?: string;
6
7
  score?: number;
8
+ text?: string;
9
+ debug?: string;
10
+ spans?: SourceSpan[];
7
11
  }): string;
8
12
  export declare function formatResultList(ctx: CmdContext, header: string, matches: SectionMatch[], opts?: {
9
13
  showScores?: boolean;
14
+ preview?: 'passage' | 'intro' | 'both';
10
15
  }): string;
11
16
  export declare function formatNavHints(ctx: CmdContext): string;
@@ -12,6 +12,7 @@ export function formatSectionPreview(ctx, section, opts) {
12
12
  const kind = section.id.includes('#') ? 'Section' : 'File';
13
13
  const details = [
14
14
  opts?.reason,
15
+ opts?.debug,
15
16
  opts?.score === undefined ? undefined : `score: ${opts.score.toFixed(6)}`,
16
17
  ].filter((detail) => detail !== undefined);
17
18
  const detailsSuffix = details.length > 0 ? ' ' + s.dim(`(${details.join(', ')})`) : '';
@@ -19,9 +20,11 @@ export function formatSectionPreview(ctx, section, opts) {
19
20
  `${s.dim('*')} ${s.dim(kind + ':')} [[${formatSectionId(section.id, s)}]]${detailsSuffix}`,
20
21
  ` ${s.dim('Defined in')} ${s.cyan(relPath)}${s.dim(`:${section.startLine}-${section.endLine}`)}`,
21
22
  ];
22
- if (section.firstParagraph) {
23
- lines.push('', ` ${s.dim('>')} ${section.firstParagraph}`);
24
- }
23
+ if (opts?.spans?.length)
24
+ lines.push(` ${s.dim('Matched lines:')} ${opts.spans.map((span) => `${span.startLine}-${span.endLine}`).join(', ')}`);
25
+ const text = opts?.text ?? section.firstParagraph;
26
+ if (text)
27
+ lines.push('', ...text.split('\n').map((line) => ` ${s.dim('>')} ${line}`));
25
28
  return lines.join('\n');
26
29
  }
27
30
  export function formatResultList(ctx, header, matches, opts) {
@@ -31,7 +34,32 @@ export function formatResultList(ctx, header, matches, opts) {
31
34
  lines.push('');
32
35
  lines.push(formatSectionPreview(ctx, matches[i].section, {
33
36
  reason: matches[i].reason,
34
- score: opts?.showScores ? matches[i].score : undefined,
37
+ spans: opts?.preview === 'intro'
38
+ ? undefined
39
+ : matches[i].evidence?.[0]?.spans,
40
+ score: opts?.showScores ? matches[i].rankScore : undefined,
41
+ text: opts?.preview === 'intro'
42
+ ? undefined
43
+ : matches[i].evidence?.length
44
+ ? (opts?.preview === 'both'
45
+ ? matches[i].section.firstParagraph + '\n\n'
46
+ : '') + matches[i].evidence[0].text
47
+ : undefined,
48
+ debug: opts?.showScores && matches[i].rankScore !== undefined
49
+ ? JSON.stringify({
50
+ lexicalRank: matches[i].lexicalRank,
51
+ semanticRank: matches[i].semanticRank,
52
+ lexicalScore: matches[i].lexicalScore,
53
+ semanticSimilarity: matches[i].semanticSimilarity,
54
+ lexicalContribution: matches[i].lexicalRank
55
+ ? 1 / (60 + matches[i].lexicalRank)
56
+ : 0,
57
+ semanticContribution: matches[i].semanticRank
58
+ ? 1 / (60 + matches[i].semanticRank)
59
+ : 0,
60
+ ...matches[i].diagnostics,
61
+ })
62
+ : undefined,
35
63
  }));
36
64
  }
37
65
  lines.push('');