@ngockhoale/ukit 1.1.6

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 (344) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/LICENSE +21 -0
  3. package/README.md +189 -0
  4. package/bin/ukit +30 -0
  5. package/manifests/platform.full.yaml +1194 -0
  6. package/package.json +71 -0
  7. package/scripts/bug/triage.mjs +37 -0
  8. package/scripts/index/build-index.mjs +35 -0
  9. package/scripts/index/query-index.mjs +92 -0
  10. package/scripts/index/refresh-index.mjs +85 -0
  11. package/scripts/release/verify-release.mjs +56 -0
  12. package/src/bug/triageBug.js +123 -0
  13. package/src/cli/adapters.js +148 -0
  14. package/src/cli/commands/diff.js +51 -0
  15. package/src/cli/commands/doctor.js +125 -0
  16. package/src/cli/commands/indexArgs.js +73 -0
  17. package/src/cli/commands/indexTools.js +509 -0
  18. package/src/cli/commands/install.js +293 -0
  19. package/src/cli/commands/memory.js +126 -0
  20. package/src/cli/commands/status.js +8 -0
  21. package/src/cli/commands/uninstall.js +51 -0
  22. package/src/cli/index.js +109 -0
  23. package/src/context/detectProjectContext.js +49 -0
  24. package/src/context/detectProviders.js +12 -0
  25. package/src/core/applyPlan.js +89 -0
  26. package/src/core/buildPlan.js +228 -0
  27. package/src/core/compact/index.js +294 -0
  28. package/src/core/compact/threshold.js +936 -0
  29. package/src/core/diffPlan.js +73 -0
  30. package/src/core/ensureGitignore.js +117 -0
  31. package/src/core/fileOps.js +188 -0
  32. package/src/core/memory/hygiene.js +160 -0
  33. package/src/core/memory/index.js +2 -0
  34. package/src/core/memory/retrieval.js +476 -0
  35. package/src/core/memory/store.js +202 -0
  36. package/src/core/metadata.js +132 -0
  37. package/src/core/migrateLegacy.js +139 -0
  38. package/src/core/output/index.js +1309 -0
  39. package/src/core/paths.js +13 -0
  40. package/src/core/report.js +17 -0
  41. package/src/core/router/advisor.js +42 -0
  42. package/src/core/router/index.js +2 -0
  43. package/src/core/router/router.js +164 -0
  44. package/src/core/runInstallPipeline.js +365 -0
  45. package/src/core/runtimeConfig.js +190 -0
  46. package/src/core/runtimePaths.js +24 -0
  47. package/src/core/status.js +186 -0
  48. package/src/core/token/index.js +328 -0
  49. package/src/core/uninstall.js +246 -0
  50. package/src/core/validation/confidence.js +89 -0
  51. package/src/core/validation/index.js +2 -0
  52. package/src/core/validation/validator.js +165 -0
  53. package/src/index/buildIndex.js +1392 -0
  54. package/src/index/gitHooks.js +109 -0
  55. package/src/index/importResolution.js +377 -0
  56. package/src/index/languageTools.js +127 -0
  57. package/src/index/paths.js +27 -0
  58. package/src/index/queryIndex.js +637 -0
  59. package/src/index/relatedTests.js +237 -0
  60. package/src/index/resolveContext.js +345 -0
  61. package/src/index/routeCatalog.js +258 -0
  62. package/src/index/taskRouting.js +677 -0
  63. package/src/index/verificationPlan.js +437 -0
  64. package/src/manifest/loadManifest.js +22 -0
  65. package/src/manifest/selectItems.js +78 -0
  66. package/src/manifest/validateManifest.js +115 -0
  67. package/src/render/buildVariables.js +39 -0
  68. package/src/render/renderTemplate.js +44 -0
  69. package/src/stack/detectStack.js +213 -0
  70. package/templates/.claude/agents/bug-debugger.md +57 -0
  71. package/templates/.claude/agents/feature-implementer.md +55 -0
  72. package/templates/.claude/config/providers.md +25 -0
  73. package/templates/.claude/hooks/auto-allow-bash.sh +155 -0
  74. package/templates/.claude/hooks/auto-prune-bash.sh +75 -0
  75. package/templates/.claude/hooks/block-dangerous.sh +54 -0
  76. package/templates/.claude/hooks/compress-output.sh +17 -0
  77. package/templates/.claude/hooks/protect-files.sh +37 -0
  78. package/templates/.claude/hooks/reinject-context.sh +28 -0
  79. package/templates/.claude/hooks/session-start.md +13 -0
  80. package/templates/.claude/hooks/skill-router.sh +1681 -0
  81. package/templates/.claude/hooks/verification-guard.sh +271 -0
  82. package/templates/.claude/settings.json +144 -0
  83. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  84. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  85. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  86. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  87. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  88. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  89. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  90. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  91. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  92. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  93. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  94. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  95. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  96. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  97. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  98. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  99. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  100. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  101. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  102. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  103. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  104. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  105. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  106. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  107. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  108. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  109. package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  110. package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  111. package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  112. package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  113. package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  114. package/templates/.claude/skills/_shared/ooxml/schemas/mce/mc.xsd +75 -0
  115. package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
  116. package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
  117. package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
  118. package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
  119. package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
  120. package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  121. package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
  122. package/templates/.claude/skills/_shared/ooxml/scripts/pack.py +159 -0
  123. package/templates/.claude/skills/_shared/ooxml/scripts/unpack.py +29 -0
  124. package/templates/.claude/skills/_shared/ooxml/scripts/validate.py +69 -0
  125. package/templates/.claude/skills/_shared/ooxml/scripts/validation/__init__.py +15 -0
  126. package/templates/.claude/skills/_shared/ooxml/scripts/validation/base.py +951 -0
  127. package/templates/.claude/skills/_shared/ooxml/scripts/validation/docx.py +274 -0
  128. package/templates/.claude/skills/_shared/ooxml/scripts/validation/pptx.py +315 -0
  129. package/templates/.claude/skills/_shared/ooxml/scripts/validation/redlining.py +279 -0
  130. package/templates/.claude/skills/backend-api/SKILL.md +26 -0
  131. package/templates/.claude/skills/canvas-design/LICENSE.txt +202 -0
  132. package/templates/.claude/skills/canvas-design/SKILL.md +130 -0
  133. package/templates/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  134. package/templates/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  135. package/templates/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  136. package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  137. package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  138. package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  139. package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  140. package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  141. package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  142. package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  143. package/templates/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  144. package/templates/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  145. package/templates/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  146. package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
  147. package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  148. package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
  149. package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-OFL.txt +93 -0
  150. package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
  151. package/templates/.claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  152. package/templates/.claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  153. package/templates/.claude/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
  154. package/templates/.claude/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +93 -0
  155. package/templates/.claude/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
  156. package/templates/.claude/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
  157. package/templates/.claude/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +93 -0
  158. package/templates/.claude/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
  159. package/templates/.claude/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +93 -0
  160. package/templates/.claude/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  161. package/templates/.claude/skills/code-review/SKILL.md +97 -0
  162. package/templates/.claude/skills/debugging-toolkit/SKILL.md +156 -0
  163. package/templates/.claude/skills/delivery/SKILL.md +92 -0
  164. package/templates/.claude/skills/discover-security/SKILL.md +86 -0
  165. package/templates/.claude/skills/docker-packaging/SKILL.md +60 -0
  166. package/templates/.claude/skills/docs-manager/SKILL.md +465 -0
  167. package/templates/.claude/skills/docs-manager/init-project-docs.sh +70 -0
  168. package/templates/.claude/skills/docs-manager/templates/README.md.template +50 -0
  169. package/templates/.claude/skills/docs-manager/templates/agent-roles.md.template +24 -0
  170. package/templates/.claude/skills/docs-manager/templates/coding-conventions.md.template +28 -0
  171. package/templates/.claude/skills/docs-manager/templates/memory.md.template +30 -0
  172. package/templates/.claude/skills/docs-manager/templates/onboarding.md.template +20 -0
  173. package/templates/.claude/skills/docs-manager/templates/project.md.template +26 -0
  174. package/templates/.claude/skills/docs-quality/SKILL.md +148 -0
  175. package/templates/.claude/skills/docx/LICENSE.txt +30 -0
  176. package/templates/.claude/skills/docx/SKILL.md +197 -0
  177. package/templates/.claude/skills/docx/docx-js.md +350 -0
  178. package/templates/.claude/skills/docx/ooxml.md +610 -0
  179. package/templates/.claude/skills/docx/scripts/__init__.py +1 -0
  180. package/templates/.claude/skills/docx/scripts/document.py +1276 -0
  181. package/templates/.claude/skills/docx/scripts/templates/comments.xml +3 -0
  182. package/templates/.claude/skills/docx/scripts/templates/commentsExtended.xml +3 -0
  183. package/templates/.claude/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
  184. package/templates/.claude/skills/docx/scripts/templates/commentsIds.xml +3 -0
  185. package/templates/.claude/skills/docx/scripts/templates/people.xml +3 -0
  186. package/templates/.claude/skills/docx/scripts/utilities.py +374 -0
  187. package/templates/.claude/skills/duraone/SKILL.md +204 -0
  188. package/templates/.claude/skills/duraone/references/backend.md +636 -0
  189. package/templates/.claude/skills/duraone/references/frontend.md +1506 -0
  190. package/templates/.claude/skills/duraone/references/sql.md +631 -0
  191. package/templates/.claude/skills/duraone/references/workflow.md +520 -0
  192. package/templates/.claude/skills/executing-plans/SKILL.md +76 -0
  193. package/templates/.claude/skills/file-organizer/SKILL.md +433 -0
  194. package/templates/.claude/skills/frontend/SKILL.md +26 -0
  195. package/templates/.claude/skills/frontend-design/LICENSE.txt +177 -0
  196. package/templates/.claude/skills/frontend-design/SKILL.md +42 -0
  197. package/templates/.claude/skills/frontend-vue/SKILL.md +127 -0
  198. package/templates/.claude/skills/frontend-vue/components/Control/Box.vue +137 -0
  199. package/templates/.claude/skills/frontend-vue/components/Control/Button.vue +93 -0
  200. package/templates/.claude/skills/frontend-vue/components/Control/ButtonBar.vue +29 -0
  201. package/templates/.claude/skills/frontend-vue/components/Control/ButtonFloat.vue +62 -0
  202. package/templates/.claude/skills/frontend-vue/components/Control/CheckButton.vue +75 -0
  203. package/templates/.claude/skills/frontend-vue/components/Control/Checkbox.vue +58 -0
  204. package/templates/.claude/skills/frontend-vue/components/Control/Datetime.vue +148 -0
  205. package/templates/.claude/skills/frontend-vue/components/Control/Dropdownlist.vue +156 -0
  206. package/templates/.claude/skills/frontend-vue/components/Control/Input.vue +106 -0
  207. package/templates/.claude/skills/frontend-vue/components/Control/Label.vue +38 -0
  208. package/templates/.claude/skills/frontend-vue/components/Control/Master/BoxColumn.vue +24 -0
  209. package/templates/.claude/skills/frontend-vue/components/Control/Popup/Confirm.vue +33 -0
  210. package/templates/.claude/skills/frontend-vue/components/Control/Popup/Info.vue +32 -0
  211. package/templates/.claude/skills/frontend-vue/components/Control/Popup/ModalInfo.vue +39 -0
  212. package/templates/.claude/skills/frontend-vue/components/Control/Popup/Reject.vue +64 -0
  213. package/templates/.claude/skills/frontend-vue/components/Control/Tag.vue +82 -0
  214. package/templates/.claude/skills/frontend-vue/components/Control/Upload.vue +61 -0
  215. package/templates/.claude/skills/frontend-vue/components/ControlMobile/Dropdownlist.vue +103 -0
  216. package/templates/.claude/skills/frontend-vue/components/ControlMobile/PagingBar.vue +108 -0
  217. package/templates/.claude/skills/frontend-vue/components/ControlMobile/UploadImage.vue +137 -0
  218. package/templates/.claude/skills/frontend-vue/components/Grid/AG.vue +806 -0
  219. package/templates/.claude/skills/frontend-vue/components/Grid/AntTable.vue +253 -0
  220. package/templates/.claude/skills/frontend-vue/components/Grid/CustomDropdownEditor.vue +43 -0
  221. package/templates/.claude/skills/frontend-vue/components/Grid/CustomDropdownEditorEnable.vue +55 -0
  222. package/templates/.claude/skills/frontend-vue/components/Grid/HtmlTable.vue +40 -0
  223. package/templates/.claude/skills/frontend-vue/components/PDFViewer.vue +25 -0
  224. package/templates/.claude/skills/frontend-vue/components/Panel/FormView.vue +309 -0
  225. package/templates/.claude/skills/frontend-vue/components/Partial/Footer.vue +23 -0
  226. package/templates/.claude/skills/frontend-vue/components/Partial/Header.vue +265 -0
  227. package/templates/.claude/skills/frontend-vue/components/Partial/Sidebar.vue +122 -0
  228. package/templates/.claude/skills/frontend-vue/components/Template.vue +16 -0
  229. package/templates/.claude/skills/frontend-vue/components/View/Form.vue +89 -0
  230. package/templates/.claude/skills/frontend-vue/composables/indexDBStore.js +140 -0
  231. package/templates/.claude/skills/frontend-vue/composables/masterApi.js +362 -0
  232. package/templates/.claude/skills/frontend-vue/composables/state.js +578 -0
  233. package/templates/.claude/skills/frontend-vue/composables/useRequest.js +221 -0
  234. package/templates/.claude/skills/frontend-vue/composables/useSession.js +179 -0
  235. package/templates/.claude/skills/frontend-vue/composables/useTranslation.js +54 -0
  236. package/templates/.claude/skills/frontend-vue/composables/useWebSocket.js +257 -0
  237. package/templates/.claude/skills/frontend-vue/composables/userObj.js +111 -0
  238. package/templates/.claude/skills/frontend-vue/composables/utils.js +322 -0
  239. package/templates/.claude/skills/frontend-vue/reference/composables-example.vue +320 -0
  240. package/templates/.claude/skills/frontend-vue/reference/form-example.vue +183 -0
  241. package/templates/.claude/skills/frontend-vue/reference/grid-example.vue +147 -0
  242. package/templates/.claude/skills/frontend-vue/reference/masterdata-example/[id].vue +106 -0
  243. package/templates/.claude/skills/frontend-vue/reference/masterdata-example/index.vue +58 -0
  244. package/templates/.claude/skills/frontend-vue/reference/popup-example.vue +159 -0
  245. package/templates/.claude/skills/pdf/LICENSE.txt +30 -0
  246. package/templates/.claude/skills/pdf/SKILL.md +294 -0
  247. package/templates/.claude/skills/pdf/forms.md +205 -0
  248. package/templates/.claude/skills/pdf/reference.md +612 -0
  249. package/templates/.claude/skills/pdf/scripts/check_bounding_boxes.py +70 -0
  250. package/templates/.claude/skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
  251. package/templates/.claude/skills/pdf/scripts/check_fillable_fields.py +12 -0
  252. package/templates/.claude/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
  253. package/templates/.claude/skills/pdf/scripts/create_validation_image.py +41 -0
  254. package/templates/.claude/skills/pdf/scripts/extract_form_field_info.py +152 -0
  255. package/templates/.claude/skills/pdf/scripts/fill_fillable_fields.py +114 -0
  256. package/templates/.claude/skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
  257. package/templates/.claude/skills/pdf-processing/SKILL.md +107 -0
  258. package/templates/.claude/skills/pdf-processing-pro/FORMS.md +610 -0
  259. package/templates/.claude/skills/pdf-processing-pro/OCR.md +137 -0
  260. package/templates/.claude/skills/pdf-processing-pro/SKILL.md +296 -0
  261. package/templates/.claude/skills/pdf-processing-pro/TABLES.md +626 -0
  262. package/templates/.claude/skills/pdf-processing-pro/scripts/analyze_form.py +307 -0
  263. package/templates/.claude/skills/postgres/SKILL.md +69 -0
  264. package/templates/.claude/skills/postgres/reference/fn_get_examples.sql +208 -0
  265. package/templates/.claude/skills/postgres/reference/fn_rpt_examples.sql +239 -0
  266. package/templates/.claude/skills/postgres/reference/utility_functions.sql +94 -0
  267. package/templates/.claude/skills/pptx/LICENSE.txt +30 -0
  268. package/templates/.claude/skills/pptx/SKILL.md +484 -0
  269. package/templates/.claude/skills/pptx/html2pptx.md +625 -0
  270. package/templates/.claude/skills/pptx/ooxml.md +427 -0
  271. package/templates/.claude/skills/pptx/scripts/html2pptx.js +979 -0
  272. package/templates/.claude/skills/pptx/scripts/inventory.py +1020 -0
  273. package/templates/.claude/skills/pptx/scripts/rearrange.py +231 -0
  274. package/templates/.claude/skills/pptx/scripts/replace.py +385 -0
  275. package/templates/.claude/skills/pptx/scripts/thumbnail.py +450 -0
  276. package/templates/.claude/skills/repo-maintenance/SKILL.md +97 -0
  277. package/templates/.claude/skills/research/EXAMPLES.md +434 -0
  278. package/templates/.claude/skills/research/REFERENCE.md +399 -0
  279. package/templates/.claude/skills/research/SKILL.md +136 -0
  280. package/templates/.claude/skills/root-cause-tracing/SKILL.md +174 -0
  281. package/templates/.claude/skills/root-cause-tracing/find-polluter.sh +63 -0
  282. package/templates/.claude/skills/sharing-skills/SKILL.md +194 -0
  283. package/templates/.claude/skills/sql-optimization-patterns/SKILL.md +493 -0
  284. package/templates/.claude/skills/subagent-driven-development/SKILL.md +189 -0
  285. package/templates/.claude/skills/systematic-debugging/CREATION-LOG.md +119 -0
  286. package/templates/.claude/skills/systematic-debugging/SKILL.md +295 -0
  287. package/templates/.claude/skills/systematic-debugging/test-academic.md +14 -0
  288. package/templates/.claude/skills/systematic-debugging/test-pressure-1.md +58 -0
  289. package/templates/.claude/skills/systematic-debugging/test-pressure-2.md +68 -0
  290. package/templates/.claude/skills/systematic-debugging/test-pressure-3.md +69 -0
  291. package/templates/.claude/skills/test-driven-development/SKILL.md +364 -0
  292. package/templates/.claude/skills/testing-anti-patterns/SKILL.md +302 -0
  293. package/templates/.claude/skills/testing-quality/SKILL.md +97 -0
  294. package/templates/.claude/skills/verification-before-completion/SKILL.md +139 -0
  295. package/templates/.claude/skills/webapp-testing/LICENSE.txt +202 -0
  296. package/templates/.claude/skills/webapp-testing/SKILL.md +96 -0
  297. package/templates/.claude/skills/webapp-testing/examples/console_logging.py +35 -0
  298. package/templates/.claude/skills/webapp-testing/examples/element_discovery.py +40 -0
  299. package/templates/.claude/skills/webapp-testing/examples/static_html_automation.py +33 -0
  300. package/templates/.claude/skills/webapp-testing/scripts/with_server.py +106 -0
  301. package/templates/.claude/ukit/index/build-index.mjs +28 -0
  302. package/templates/.claude/ukit/index/cache-utils.mjs +140 -0
  303. package/templates/.claude/ukit/index/lib/index-core.mjs +2800 -0
  304. package/templates/.claude/ukit/index/query-index.mjs +150 -0
  305. package/templates/.claude/ukit/index/refresh-index.mjs +57 -0
  306. package/templates/.claude/ukit/index/reset-auto-permissions.mjs +76 -0
  307. package/templates/.claude/ukit/index/resolve-context.mjs +279 -0
  308. package/templates/.claude/ukit/index/route-catalog.mjs +258 -0
  309. package/templates/.claude/ukit/index/route-task.mjs +1994 -0
  310. package/templates/.claude/ukit/index/triage.mjs +133 -0
  311. package/templates/.claude/ukit/index/verify-context.mjs +689 -0
  312. package/templates/.claude/ukit/runtime/compact-threshold.mjs +1013 -0
  313. package/templates/.claude/ukit/runtime/output-compression.mjs +1340 -0
  314. package/templates/.claude/ukit/runtime/reinject-context.mjs +874 -0
  315. package/templates/.claude/ukit/runtime/token-utils.mjs +500 -0
  316. package/templates/.codex/README.md +83 -0
  317. package/templates/.codex/settings.json +187 -0
  318. package/templates/.gitignore +75 -0
  319. package/templates/AGENTS.md +116 -0
  320. package/templates/CLAUDE.md +93 -0
  321. package/templates/adapter-presets/antigravity/README.md +22 -0
  322. package/templates/adapter-presets/antigravity/rules.md +49 -0
  323. package/templates/adapter-presets/claude/settings.local.json +42 -0
  324. package/templates/adapter-presets/codex/settings.local.json +6 -0
  325. package/templates/adapter-presets/opencode/opencode.template.json +1 -0
  326. package/templates/docs/BUGFIX.md +20 -0
  327. package/templates/docs/BUG_INDEX.md +12 -0
  328. package/templates/docs/BUG_METRICS.md +7 -0
  329. package/templates/docs/BUG_TEMPLATE.md +13 -0
  330. package/templates/docs/CODE_MAP.md +35 -0
  331. package/templates/docs/INSTALL.md +113 -0
  332. package/templates/docs/MEMORY.md +49 -0
  333. package/templates/docs/PROJECT.md +50 -0
  334. package/templates/docs/UKIT_USAGE_GUIDE.md +147 -0
  335. package/templates/docs/WORKLOG.md +10 -0
  336. package/templates/ukit/README.md +14 -0
  337. package/templates/ukit/storage/cache/compact-history.json +3 -0
  338. package/templates/ukit/storage/cache/compact-pressure.json +1 -0
  339. package/templates/ukit/storage/cache/output-history.json +3 -0
  340. package/templates/ukit/storage/cache/prompt-cache.json +3 -0
  341. package/templates/ukit/storage/config.json +37 -0
  342. package/templates/ukit/storage/memory/projects/.gitkeep +2 -0
  343. package/templates/ukit/storage/memory/sessions/.gitkeep +0 -0
  344. package/templates/ukit/storage/memory/user.json +5 -0
@@ -0,0 +1,2800 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+
5
+ const SOURCE_DIRS = ['src', 'tests', 'test', 'specs', 'spec', '__tests__', 'manifests'];
6
+ const EXCLUDED_DIR_NAMES = new Set([
7
+ 'node_modules',
8
+ '.git',
9
+ '.cache',
10
+ '.next',
11
+ '.nuxt',
12
+ '.output',
13
+ '.vercel',
14
+ '.turbo',
15
+ '.yarn',
16
+ 'dist',
17
+ 'build',
18
+ 'out',
19
+ 'coverage',
20
+ 'tmp',
21
+ 'temp',
22
+ 'vendor',
23
+ '.venv',
24
+ 'venv',
25
+ ]);
26
+ const CODE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue']);
27
+ const STYLE_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less']);
28
+ const TRACKED_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.json', '.yaml', '.yml', '.md']);
29
+ const DISCOVERED_EXTENSIONS = new Set([...TRACKED_EXTENSIONS, ...STYLE_EXTENSIONS]);
30
+ const INDEX_SCHEMA_VERSION = 6;
31
+ export const DEFAULT_INDEX_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
32
+ const INDEX_PARSE_BATCH_SIZE = 8;
33
+ const MAX_IMPORTER_HOPS = 2;
34
+
35
+ export const INDEX_ARTIFACTS = {
36
+ meta: 'meta.json',
37
+ files: 'files.json',
38
+ symbols: 'symbols.json',
39
+ imports: 'imports.json',
40
+ testsMap: 'tests-map.json',
41
+ hotspots: 'hotspots.json',
42
+ archetypes: 'archetypes.json',
43
+ relations: 'relations.json',
44
+ analogs: 'analogs.json',
45
+ };
46
+
47
+ export function getIndexDir(rootDir) {
48
+ return path.join(rootDir, '.cache', 'index');
49
+ }
50
+
51
+ function getArtifactPath(rootDir, artifactName) {
52
+ return path.join(getIndexDir(rootDir), artifactName);
53
+ }
54
+
55
+ function normalizeRelative(rootDir, absolutePath) {
56
+ return path.relative(rootDir, absolutePath).replace(/\\/g, '/');
57
+ }
58
+
59
+ const ARTIFACT_CACHE = new Map();
60
+ const QUERY_SEARCH_BUNDLE_CACHE = new Map();
61
+ const QUERY_SUPPORT_BUNDLE_CACHE = new Map();
62
+ const QUERY_RESULT_CACHE = new Map();
63
+ const ANALOG_RESULT_CACHE = new Map();
64
+ const RELATED_TEST_LOOKUP_CACHE = new Map();
65
+ const RESOLUTION_SUFFIXES = ['', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.vue', '/index.js', '/index.ts', '/index.jsx', '/index.tsx', '/index.mjs', '/index.cjs', '/index.vue'];
66
+ const DEFAULT_ALIAS_RULES = [
67
+ { pattern: '@/*', targets: ['src/*', '*'] },
68
+ { pattern: '~/*', targets: ['*', 'src/*'] },
69
+ ];
70
+ const ALIAS_CONTEXT_CACHE = new Map();
71
+ const ROOT_ALIAS_CONFIG_FILES = ['tsconfig.json', 'jsconfig.json'];
72
+ const GENERIC_STOPWORDS = new Set([
73
+ 'a', 'an', 'and', 'as', 'at', 'be', 'by', 'do', 'for', 'from', 'help', 'i', 'in', 'into',
74
+ 'is', 'it', 'me', 'my', 'of', 'on', 'or', 'please', 'the', 'this', 'that', 'to', 'with',
75
+ 'ban', 'cho', 'cua', 'di', 'dung', 'giup', 'hay', 'khong', 'la', 'lam', 'luon', 'mot', 'nay',
76
+ 'neu', 'nhung', 'nua', 'roi', 'toi', 'tren', 'truoc', 'va', 'voi',
77
+ ]);
78
+ const VIETNAMESE_PHRASE_ALIASES = [
79
+ { regex: /\bbo nho dem\b/gu, expansions: ['cache'] },
80
+ { regex: /\bduong dan\b/gu, expansions: ['route', 'path'] },
81
+ { regex: /\bxac thuc\b/gu, expansions: ['auth', 'authentication'] },
82
+ { regex: /\bdang nhap\b/gu, expansions: ['login', 'signin', 'auth'] },
83
+ { regex: /\bquyen truy cap\b/gu, expansions: ['permission', 'access'] },
84
+ { regex: /\bkiem tra\b/gu, expansions: ['review', 'check', 'verify'] },
85
+ { regex: /\bbao mat\b/gu, expansions: ['security'] },
86
+ { regex: /\bco so du lieu\b/gu, expansions: ['database'] },
87
+ { regex: /\btai lieu\b/gu, expansions: ['docs', 'documentation'] },
88
+ { regex: /\bgiao dien\b/gu, expansions: ['ui', 'interface', 'frontend'] },
89
+ { regex: /\bthanh phan\b/gu, expansions: ['component'] },
90
+ { regex: /\bkiem thu\b/gu, expansions: ['test', 'testing'] },
91
+ { regex: /\bbo cuc\b/gu, expansions: ['layout'] },
92
+ ];
93
+ const VIETNAMESE_TOKEN_ALIASES = new Map([
94
+ ['sua', ['fix']],
95
+ ['loi', ['bug', 'error']],
96
+ ['them', ['add']],
97
+ ['xoa', ['delete', 'remove']],
98
+ ['quyen', ['permission', 'access']],
99
+ ['token', ['token']],
100
+ ['cache', ['cache']],
101
+ ['route', ['route', 'path']],
102
+ ['path', ['path', 'route']],
103
+ ['helper', ['helper']],
104
+ ['docker', ['docker']],
105
+ ['compose', ['compose']],
106
+ ['page', ['page']],
107
+ ['component', ['component']],
108
+ ['test', ['test', 'testing']],
109
+ ['soat', ['review', 'audit']],
110
+ ['trien', ['implement', 'build', 'ship']],
111
+ ['khai', ['implement', 'build', 'ship']],
112
+ ]);
113
+
114
+ // ── Build Index ──
115
+
116
+ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
117
+ const absoluteRoot = path.resolve(rootDir);
118
+ const indexDir = getIndexDir(absoluteRoot);
119
+
120
+ const previousFilesArtifact = await readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.files);
121
+ const canReusePrevious = previousFilesArtifact?.schemaVersion === INDEX_SCHEMA_VERSION;
122
+ const previousCodeFileRecords = canReusePrevious
123
+ ? (previousFilesArtifact.items ?? []).filter((item) => CODE_EXTENSIONS.has(item?.ext))
124
+ : [];
125
+
126
+ const previousCodeMetaByPath = new Map(
127
+ previousCodeFileRecords.map((item) => [item.filePath, { mtimeMs: Number(item.mtimeMs ?? -1), size: Number(item.size ?? -1) }]),
128
+ );
129
+
130
+ const discoveredFiles = await collectFiles(await resolveScanRoots(absoluteRoot));
131
+ const sourceFingerprint = createSourceFingerprint(absoluteRoot, discoveredFiles);
132
+ const styleFilePaths = discoveredFiles
133
+ .map((entry) => {
134
+ const ext = path.extname(entry.absolutePath).toLowerCase();
135
+ if (!STYLE_EXTENSIONS.has(ext)) return null;
136
+ return normalizeRelative(absoluteRoot, entry.absolutePath);
137
+ })
138
+ .filter(Boolean)
139
+ .sort((a, b) => a.localeCompare(b));
140
+
141
+ const fileRecords = discoveredFiles
142
+ .map((entry) => {
143
+ const ext = path.extname(entry.absolutePath).toLowerCase();
144
+ if (!TRACKED_EXTENSIONS.has(ext)) return null;
145
+ const filePath = normalizeRelative(absoluteRoot, entry.absolutePath);
146
+ return {
147
+ filePath,
148
+ domain: filePath.split('/')[0] ?? 'other',
149
+ ext,
150
+ mtimeMs: entry.mtimeMs,
151
+ size: entry.size,
152
+ };
153
+ })
154
+ .filter(Boolean)
155
+ .sort((a, b) => a.filePath.localeCompare(b.filePath));
156
+ const canReuseFilesArtifact = canReusePrevious
157
+ && areFileRecordSnapshotsEqual(previousFilesArtifact?.items ?? [], fileRecords);
158
+
159
+ const codeFiles = fileRecords.filter((f) => CODE_EXTENSIONS.has(f.ext));
160
+ const symbols = [];
161
+ const imports = [];
162
+ const reusableCodeFiles = [];
163
+ let filesToParse = [];
164
+ let reusedCodeFileCount = 0;
165
+ let canReuseParsedArtifacts = false;
166
+
167
+ for (const file of codeFiles) {
168
+ const previousMeta = previousCodeMetaByPath.get(file.filePath);
169
+ const isUnchanged = previousMeta
170
+ && previousMeta.mtimeMs === file.mtimeMs
171
+ && previousMeta.size === file.size;
172
+
173
+ if (isUnchanged) {
174
+ reusableCodeFiles.push(file.filePath);
175
+ continue;
176
+ }
177
+
178
+ filesToParse.push(file.filePath);
179
+ }
180
+
181
+ if (reusableCodeFiles.length > 0) {
182
+ const [previousSymbolsArtifact, previousImportsArtifact] = await Promise.all([
183
+ readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.symbols),
184
+ readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.imports),
185
+ ]);
186
+ canReuseParsedArtifacts = previousSymbolsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
187
+ && previousImportsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION;
188
+
189
+ if (canReuseParsedArtifacts) {
190
+ const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item.filePath);
191
+ const previousImportsByPath = groupBy(previousImportsArtifact.items ?? [], (item) => item.from);
192
+
193
+ for (const filePath of reusableCodeFiles) {
194
+ symbols.push(...(previousSymbolsByPath.get(filePath) ?? []));
195
+ imports.push(...(previousImportsByPath.get(filePath) ?? []));
196
+ reusedCodeFileCount += 1;
197
+ }
198
+ } else {
199
+ filesToParse = codeFiles.map((file) => file.filePath);
200
+ }
201
+ }
202
+ const canReuseAllParsedArtifacts = canReuseParsedArtifacts && filesToParse.length === 0 && reusableCodeFiles.length === codeFiles.length;
203
+
204
+ for (let start = 0; start < filesToParse.length; start += INDEX_PARSE_BATCH_SIZE) {
205
+ const batch = filesToParse.slice(start, start + INDEX_PARSE_BATCH_SIZE);
206
+ const parsedBatch = (await Promise.all(batch.map(async (filePath) => {
207
+ try {
208
+ const absolutePath = path.join(absoluteRoot, filePath);
209
+ const content = await fs.readFile(absolutePath, 'utf8');
210
+ const scriptContent = extractScriptContent(filePath, content);
211
+ return {
212
+ filePath,
213
+ symbols: extractSymbols(filePath, scriptContent),
214
+ imports: [
215
+ ...extractImports(filePath, scriptContent),
216
+ ...extractSupplementalImports(filePath, content),
217
+ ],
218
+ };
219
+ } catch {
220
+ return null;
221
+ }
222
+ }))).filter(Boolean);
223
+
224
+ for (const parsedFile of parsedBatch) {
225
+ symbols.push(...parsedFile.symbols);
226
+ imports.push(...parsedFile.imports);
227
+ }
228
+ }
229
+
230
+ const canReuseTestsMapCandidate = canReusePrevious
231
+ && haveStableTrackedFileIdentities(previousFilesArtifact?.items ?? [], fileRecords);
232
+ const previousTestsMapArtifact = canReuseTestsMapCandidate
233
+ ? await readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.testsMap)
234
+ : null;
235
+ const canReuseTestsMap = canReuseTestsMapCandidate
236
+ && previousTestsMapArtifact?.schemaVersion === INDEX_SCHEMA_VERSION;
237
+ const testsMap = canReuseTestsMap
238
+ ? previousTestsMapArtifact.items
239
+ : buildTestsMap(fileRecords);
240
+ const bugIndexSnapshot = await readBugIndexSnapshot(absoluteRoot);
241
+ const previousHotspotsArtifact = canReusePrevious
242
+ ? await readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.hotspots)
243
+ : null;
244
+ const canReuseHotspots = previousHotspotsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
245
+ && areBugIndexSnapshotsEqual(previousHotspotsArtifact?.sourceSnapshot, bugIndexSnapshot);
246
+ const hotspots = canReuseHotspots
247
+ ? previousHotspotsArtifact.items
248
+ : await buildHotspots(absoluteRoot, bugIndexSnapshot);
249
+
250
+ const currentCodePaths = new Set(codeFiles.map((f) => f.filePath));
251
+ const canReuseArchetypesCandidate = canReusePrevious
252
+ && filesToParse.length === 0
253
+ && codeFiles.every((file) => previousCodeMetaByPath.has(file.filePath));
254
+ const previousArchetypesArtifact = canReuseArchetypesCandidate
255
+ ? await readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.archetypes)
256
+ : null;
257
+ const canReuseArchetypes = canReuseArchetypesCandidate
258
+ && previousArchetypesArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
259
+ && previousArchetypesArtifact?.items?.length === codeFiles.length
260
+ && (previousArchetypesArtifact.items ?? []).every((item) => currentCodePaths.has(item.filePath));
261
+
262
+ // Reuse cached archetypes only when every indexed code file is unchanged
263
+ const archetypes = canReuseArchetypes
264
+ ? previousArchetypesArtifact.items
265
+ : classifyArchetypes(fileRecords, symbols);
266
+ const needsAliasContext = importsNeedAliasContext(imports);
267
+ const canReuseGraphArtifactsCandidate = canReusePrevious
268
+ && filesToParse.length === 0
269
+ && haveStableTrackedFileIdentities(previousCodeFileRecords, codeFiles);
270
+ const [previousRelationsArtifact, previousAnalogsArtifact] = canReuseGraphArtifactsCandidate
271
+ ? await Promise.all([
272
+ readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.relations),
273
+ readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.analogs),
274
+ ])
275
+ : [null, null];
276
+ const importAliasState = needsAliasContext
277
+ ? await loadImportAliasContextState({ rootDir: absoluteRoot })
278
+ : null;
279
+ const importAliasContext = importAliasState?.context ?? null;
280
+ const styleSnapshot = {
281
+ styleFiles: styleFilePaths,
282
+ importAliasSnapshot: importAliasState?.snapshot ?? [],
283
+ };
284
+ const canReuseRelations = canReuseGraphArtifactsCandidate
285
+ && previousRelationsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
286
+ && areStringArraysEqual(previousRelationsArtifact?.sourceSnapshot?.styleFiles, styleFilePaths)
287
+ && arePathSnapshotsEqual(
288
+ previousRelationsArtifact?.sourceSnapshot?.importAliasSnapshot,
289
+ styleSnapshot.importAliasSnapshot,
290
+ );
291
+ const canReuseAnalogs = canReuseGraphArtifactsCandidate
292
+ && previousAnalogsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
293
+ && areStringArraysEqual(previousAnalogsArtifact?.sourceSnapshot?.styleFiles, styleFilePaths)
294
+ && arePathSnapshotsEqual(
295
+ previousAnalogsArtifact?.sourceSnapshot?.importAliasSnapshot,
296
+ styleSnapshot.importAliasSnapshot,
297
+ );
298
+
299
+ const indexedFileSet = new Set(codeFiles.map((file) => file.filePath));
300
+ const resolvedImportsBySource = canReuseRelations && canReuseAnalogs
301
+ ? null
302
+ : buildResolvedImportMap(
303
+ absoluteRoot,
304
+ imports,
305
+ indexedFileSet,
306
+ importAliasContext,
307
+ );
308
+ const resolvedStyleImportsBySource = canReuseRelations
309
+ ? null
310
+ : buildResolvedImportMap(
311
+ absoluteRoot,
312
+ imports,
313
+ new Set(styleFilePaths),
314
+ importAliasContext,
315
+ );
316
+ const relations = canReuseRelations
317
+ ? previousRelationsArtifact.items
318
+ : buildRelations(fileRecords, archetypes, testsMap, resolvedImportsBySource, resolvedStyleImportsBySource);
319
+ const analogs = canReuseAnalogs
320
+ ? previousAnalogsArtifact.items
321
+ : buildAnalogs(fileRecords, archetypes, relations, resolvedImportsBySource);
322
+
323
+ const generatedAt = new Date().toISOString();
324
+
325
+ await fs.mkdir(indexDir, { recursive: true });
326
+ if (!canReuseFilesArtifact) {
327
+ await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.files, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, items: fileRecords });
328
+ }
329
+ if (!canReuseAllParsedArtifacts) {
330
+ await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.symbols, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, items: symbols });
331
+ await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.imports, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, items: imports });
332
+ }
333
+ if (!canReuseTestsMap) {
334
+ await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.testsMap, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, items: testsMap });
335
+ }
336
+ if (!canReuseHotspots) {
337
+ await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.hotspots, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, sourceSnapshot: bugIndexSnapshot, items: hotspots });
338
+ }
339
+ if (!canReuseArchetypes) {
340
+ await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.archetypes, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, items: archetypes });
341
+ }
342
+ if (!canReuseRelations) {
343
+ await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.relations, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, sourceSnapshot: styleSnapshot, items: relations });
344
+ }
345
+ if (!canReuseAnalogs) {
346
+ await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.analogs, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, sourceSnapshot: styleSnapshot, items: analogs });
347
+ }
348
+ await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.meta, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, sourceFingerprint });
349
+ const preservedRuntimeCaches = canReuseFilesArtifact
350
+ && canReuseAllParsedArtifacts
351
+ && canReuseTestsMap
352
+ && canReuseHotspots
353
+ && canReuseArchetypes
354
+ && canReuseRelations
355
+ && canReuseAnalogs;
356
+ if (!preservedRuntimeCaches) {
357
+ clearIndexArtifactCache(absoluteRoot);
358
+ RELATED_TEST_LOOKUP_CACHE.delete(absoluteRoot);
359
+ }
360
+
361
+ return {
362
+ indexDir,
363
+ generatedAt,
364
+ generatedAtMs: Date.parse(generatedAt),
365
+ fileCount: fileRecords.length,
366
+ symbolCount: symbols.length,
367
+ importCount: imports.length,
368
+ testsMapCount: testsMap.length,
369
+ hotspotCount: hotspots.length,
370
+ archetypeCount: archetypes.length,
371
+ relationCount: relations.length,
372
+ analogCount: analogs.length,
373
+ parsedCodeFileCount: filesToParse.length,
374
+ reusedCodeFileCount,
375
+ reusedTestsMap: canReuseTestsMap,
376
+ reusedHotspots: canReuseHotspots,
377
+ reusedRelations: canReuseRelations,
378
+ reusedAnalogs: canReuseAnalogs,
379
+ preservedRuntimeCaches,
380
+ };
381
+ }
382
+
383
+ // ── Query Index ──
384
+
385
+ export async function queryCodeIndex({ rootDir = process.cwd(), query, limit = 5 } = {}) {
386
+ if (!query || !query.trim()) return [];
387
+ if (limit <= 0) return Object.freeze([]);
388
+
389
+ const absoluteRoot = path.resolve(rootDir);
390
+ const normalizedQuery = String(query).trim();
391
+ const queryDescriptor = buildSearchDescriptor(normalizedQuery, { expandVietnameseAliases: true });
392
+ if (queryDescriptor.tokens.size === 0) return Object.freeze([]);
393
+ const queryCacheKey = JSON.stringify({
394
+ rootDir: absoluteRoot,
395
+ query: normalizedQuery,
396
+ limit,
397
+ });
398
+ if (QUERY_RESULT_CACHE.has(queryCacheKey)) {
399
+ return QUERY_RESULT_CACHE.get(queryCacheKey);
400
+ }
401
+ const queryPromise = (async () => {
402
+ const { files, searchDescriptorsByFile } = await loadQuerySearchBundle(absoluteRoot);
403
+ const preferredArchetypes = inferPreferredArchetypes(queryDescriptor.tokens);
404
+ const includeLikelyTestFiles = shouldIncludeLikelyTestFiles(queryDescriptor);
405
+
406
+ const directScores = new Map();
407
+ for (const file of files.items ?? []) {
408
+ const filePath = file.filePath;
409
+ if (isLikelyTestFilePath(filePath) && !includeLikelyTestFiles) continue;
410
+ const searchDescriptor = searchDescriptorsByFile.get(filePath) ?? createFileSearchDescriptor({ filePath, symbols: [] });
411
+ const { score, reasons } = scoreDirectFileMatch({
412
+ queryDescriptor,
413
+ filePath,
414
+ searchDescriptor,
415
+ });
416
+
417
+ if (score > 0) {
418
+ directScores.set(filePath, { filePath, score, reasons: [...new Set(reasons)] });
419
+ }
420
+ }
421
+
422
+ if (directScores.size === 0) return Object.freeze([]);
423
+
424
+ const {
425
+ hotspotScores,
426
+ testsMapBySource: testsBySource,
427
+ resolvedImportsBySource,
428
+ importersByTarget,
429
+ archetypeByFile,
430
+ } = await loadQuerySupportBundle(absoluteRoot);
431
+
432
+ const enrichedDirectScores = new Map();
433
+ for (const [filePath, entry] of directScores) {
434
+ let score = entry.score;
435
+ const reasons = [...(entry.reasons ?? [])];
436
+ const archetype = archetypeByFile.get(filePath) ?? 'other';
437
+ if (preferredArchetypes.has(archetype)) {
438
+ score += 4;
439
+ reasons.push(`archetype_hint:${archetype}`);
440
+ }
441
+
442
+ enrichedDirectScores.set(filePath, {
443
+ filePath,
444
+ score,
445
+ reasons: [...new Set(reasons)],
446
+ tests: testsBySource.get(filePath) ?? [],
447
+ });
448
+ }
449
+
450
+ const relatedScores = applyImportGraphBoosts({
451
+ directScores: enrichedDirectScores,
452
+ resolvedImportsBySource,
453
+ importersByTarget,
454
+ testsMapBySource: testsBySource,
455
+ });
456
+ const boosted = applyHotspotBoosts({ scores: relatedScores, hotspotScores });
457
+ return freezeQueryResults(
458
+ [...boosted.values()].sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath)).slice(0, limit),
459
+ );
460
+ })().catch((error) => {
461
+ QUERY_RESULT_CACHE.delete(queryCacheKey);
462
+ throw error;
463
+ });
464
+
465
+ QUERY_RESULT_CACHE.set(queryCacheKey, queryPromise);
466
+ return queryPromise;
467
+ }
468
+
469
+ // ── Analog Query ──
470
+
471
+ export async function queryAnalog({ rootDir = process.cwd(), filePath, limit = 5 } = {}) {
472
+ if (!filePath || !filePath.trim()) return [];
473
+
474
+ const absoluteRoot = path.resolve(rootDir);
475
+ const normalizedFilePath = String(filePath).trim();
476
+ const analogCacheKey = JSON.stringify({
477
+ rootDir: absoluteRoot,
478
+ filePath: normalizedFilePath,
479
+ limit,
480
+ });
481
+ if (ANALOG_RESULT_CACHE.has(analogCacheKey)) {
482
+ return ANALOG_RESULT_CACHE.get(analogCacheKey);
483
+ }
484
+ const analogPromise = readArtifact(absoluteRoot, INDEX_ARTIFACTS.analogs)
485
+ .then((analogs) => {
486
+ const safeAnalogs = ensureArtifact(analogs);
487
+ const entry = (safeAnalogs.items ?? []).find((item) => item.filePath === normalizedFilePath);
488
+ if (!entry) return Object.freeze([]);
489
+ return freezeAnalogResults(entry.analogs?.slice(0, limit) ?? []);
490
+ })
491
+ .catch((error) => {
492
+ ANALOG_RESULT_CACHE.delete(analogCacheKey);
493
+ throw error;
494
+ });
495
+
496
+ ANALOG_RESULT_CACHE.set(analogCacheKey, analogPromise);
497
+ return analogPromise;
498
+ }
499
+
500
+ export function clearIndexArtifactCache(rootDir = null) {
501
+ if (!rootDir) {
502
+ ARTIFACT_CACHE.clear();
503
+ QUERY_SEARCH_BUNDLE_CACHE.clear();
504
+ QUERY_SUPPORT_BUNDLE_CACHE.clear();
505
+ QUERY_RESULT_CACHE.clear();
506
+ ANALOG_RESULT_CACHE.clear();
507
+ RELATED_TEST_LOOKUP_CACHE.clear();
508
+ return;
509
+ }
510
+
511
+ const absoluteRoot = path.resolve(rootDir);
512
+ const indexDir = getIndexDir(absoluteRoot);
513
+
514
+ for (const artifactPath of ARTIFACT_CACHE.keys()) {
515
+ if (artifactPath.startsWith(indexDir)) {
516
+ ARTIFACT_CACHE.delete(artifactPath);
517
+ }
518
+ }
519
+
520
+ QUERY_SEARCH_BUNDLE_CACHE.delete(absoluteRoot);
521
+ QUERY_SUPPORT_BUNDLE_CACHE.delete(absoluteRoot);
522
+ for (const cacheKey of QUERY_RESULT_CACHE.keys()) {
523
+ if (cacheKey.includes(`"rootDir":"${escapeJsonString(absoluteRoot)}"`)) {
524
+ QUERY_RESULT_CACHE.delete(cacheKey);
525
+ }
526
+ }
527
+ for (const cacheKey of ANALOG_RESULT_CACHE.keys()) {
528
+ if (cacheKey.includes(`"rootDir":"${escapeJsonString(absoluteRoot)}"`)) {
529
+ ANALOG_RESULT_CACHE.delete(cacheKey);
530
+ }
531
+ }
532
+ RELATED_TEST_LOOKUP_CACHE.delete(absoluteRoot);
533
+ }
534
+
535
+ function freezeQueryResults(results) {
536
+ return Object.freeze(
537
+ results.map((item) => Object.freeze({
538
+ ...item,
539
+ reasons: Object.freeze([...(item.reasons ?? [])]),
540
+ tests: Object.freeze([...(item.tests ?? [])]),
541
+ })),
542
+ );
543
+ }
544
+
545
+ function freezeAnalogResults(results) {
546
+ return Object.freeze(results.map((item) => Object.freeze({ ...item })));
547
+ }
548
+
549
+ function ensureArtifact(artifact) {
550
+ return artifact && typeof artifact === 'object' ? artifact : { items: [] };
551
+ }
552
+
553
+ function escapeJsonString(value) {
554
+ return String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"');
555
+ }
556
+
557
+ // ── Context Resolver ──
558
+
559
+ const TASK_TYPE_BUDGETS = {
560
+ trivial: { minFiles: 1, maxFiles: 2 },
561
+ simple: { minFiles: 2, maxFiles: 5 },
562
+ 'non-trivial': { minFiles: 4, maxFiles: 8 },
563
+ };
564
+
565
+ const TRIVIAL_SIGNALS = ['typo', 'label', 'text', 'rename', 'color', 'spacing', 'toggle', 'config', 'comment'];
566
+ const RISKY_SIGNALS = [
567
+ 'auth', 'security', 'migration', 'uninstall', 'password', 'token', 'permission',
568
+ 'delete all', 'drop table', 'race', 'flaky', 'intermittent', 'timeout', 'deadlock',
569
+ 'core', 'shared', 'runtime',
570
+ ];
571
+ const EXPANSION_SIGNALS = [
572
+ 'similar',
573
+ 'same as',
574
+ 'like',
575
+ 'pattern',
576
+ 'clone',
577
+ 'analog',
578
+ 'follow',
579
+ 'example',
580
+ 'search',
581
+ 'find',
582
+ 'where',
583
+ 'which file',
584
+ 'across',
585
+ 'everywhere',
586
+ 'multiple',
587
+ 'project',
588
+ 'repo',
589
+ 'workspace',
590
+ 'all ',
591
+ 'every ',
592
+ ];
593
+
594
+ export async function resolveContext({
595
+ rootDir = process.cwd(),
596
+ intent = '',
597
+ targetFile = null,
598
+ taskType = null,
599
+ } = {}) {
600
+ const classifiedType = taskType ?? classifyTask(intent);
601
+ const budget = TASK_TYPE_BUDGETS[classifiedType] ?? TASK_TYPE_BUDGETS.simple;
602
+ const queryWasUsed = shouldQueryIndex({ intent, targetFile, taskType: classifiedType });
603
+
604
+ const result = {
605
+ taskType: classifiedType,
606
+ contextBudget: { ...budget, taskType: classifiedType },
607
+ primaryTargets: [],
608
+ analogFiles: [],
609
+ sharedAbstractions: [],
610
+ relatedTests: [],
611
+ styleFiles: [],
612
+ explanations: {
613
+ primaryTargets: [],
614
+ analogFiles: [],
615
+ sharedAbstractions: [],
616
+ relatedTests: [],
617
+ styleFiles: [],
618
+ },
619
+ };
620
+
621
+ if (targetFile) {
622
+ addExplainedFile(result, 'primaryTargets', targetFile, 'explicit target');
623
+ }
624
+
625
+ if (queryWasUsed) {
626
+ const queryResults = await queryCodeIndex({ rootDir, query: intent, limit: budget.maxFiles });
627
+ for (const qr of queryResults) {
628
+ addExplainedFile(result, 'primaryTargets', qr.filePath, buildQueryReason(qr));
629
+ }
630
+ }
631
+
632
+ if (result.primaryTargets.length === 0) {
633
+ return result;
634
+ }
635
+
636
+ const { relationsArtifact, relationsMap } = await loadRelatedTestArtifacts({
637
+ rootDir,
638
+ analogsArtifact: { items: [] },
639
+ });
640
+
641
+ for (const primary of result.primaryTargets) {
642
+ const rels = relationsMap.get(primary) ?? {};
643
+ for (const testPath of rels.tests ?? []) {
644
+ addExplainedFile(result, 'relatedTests', testPath, `test linked to ${primary}`);
645
+ }
646
+ for (const stylePath of rels.styles ?? []) {
647
+ addExplainedFile(result, 'styleFiles', stylePath, `style imported by ${primary}`);
648
+ }
649
+ }
650
+
651
+ const preferLocalizedDirectContext = shouldPreferLocalizedDirectTargetContext({
652
+ result,
653
+ taskType: classifiedType,
654
+ targetFile,
655
+ queryWasUsed,
656
+ });
657
+
658
+ if (!preferLocalizedDirectContext) {
659
+ for (const primary of result.primaryTargets) {
660
+ const rels = relationsMap.get(primary) ?? {};
661
+ for (const absType of ['composables', 'utils', 'services']) {
662
+ for (const absPath of rels[absType] ?? []) {
663
+ if (result.primaryTargets.includes(absPath)) continue;
664
+ addExplainedFile(result, 'sharedAbstractions', absPath, `related ${absType} used by ${primary}`);
665
+ }
666
+ }
667
+ }
668
+
669
+ const inferredTests = inferRelatedTestsFromArtifacts({
670
+ candidateFiles: result.primaryTargets,
671
+ analogsMap: new Map(),
672
+ relationsMap,
673
+ limit: budget.maxFiles,
674
+ });
675
+
676
+ for (const inferredTest of inferredTests) {
677
+ addExplainedFile(result, 'relatedTests', inferredTest.filePath, inferredTest.reason);
678
+ }
679
+ }
680
+
681
+ if (shouldLoadAnalogArtifacts({
682
+ result,
683
+ taskType: classifiedType,
684
+ targetFile,
685
+ maxFiles: budget.maxFiles,
686
+ queryWasUsed,
687
+ preferLocalizedDirectContext,
688
+ })) {
689
+ const { analogsMap } = await loadRelatedTestArtifacts({
690
+ rootDir,
691
+ relationsArtifact,
692
+ });
693
+
694
+ for (const primary of result.primaryTargets.slice(0, 2)) {
695
+ const analogs = analogsMap.get(primary) ?? [];
696
+ for (const analog of analogs.slice(0, 3)) {
697
+ if (result.primaryTargets.includes(analog.filePath)) continue;
698
+ addExplainedFile(
699
+ result,
700
+ 'analogFiles',
701
+ analog.filePath,
702
+ `analog of ${primary}${analog.reason ? `; ${analog.reason}` : ''}`,
703
+ );
704
+ }
705
+ }
706
+
707
+ const inferredAnalogTests = inferRelatedTestsFromArtifacts({
708
+ candidateFiles: result.primaryTargets,
709
+ analogsMap,
710
+ relationsMap,
711
+ limit: budget.maxFiles,
712
+ });
713
+
714
+ for (const inferredTest of inferredAnalogTests) {
715
+ addExplainedFile(result, 'relatedTests', inferredTest.filePath, inferredTest.reason);
716
+ }
717
+ }
718
+
719
+ enforceContextBudget(result, budget.maxFiles);
720
+ includeDirectTestTargets(result);
721
+
722
+ return result;
723
+ }
724
+
725
+ export function classifyTask(intent) {
726
+ const lower = intent.toLowerCase();
727
+
728
+ if (RISKY_SIGNALS.some((signal) => lower.includes(signal))) {
729
+ return 'non-trivial';
730
+ }
731
+
732
+ if (TRIVIAL_SIGNALS.some((signal) => lower.includes(signal))) {
733
+ if (lower.includes('all ') || lower.includes('every ') || lower.includes('multiple')) {
734
+ return 'simple';
735
+ }
736
+ return 'trivial';
737
+ }
738
+
739
+ return 'simple';
740
+ }
741
+
742
+ function shouldQueryIndex({
743
+ intent = '',
744
+ targetFile = null,
745
+ taskType = 'simple',
746
+ } = {}) {
747
+ const normalizedIntent = String(intent || '').trim().toLowerCase();
748
+ if (!normalizedIntent) {
749
+ return false;
750
+ }
751
+ if (!targetFile) {
752
+ return true;
753
+ }
754
+ if (taskType === 'non-trivial') {
755
+ return true;
756
+ }
757
+ return EXPANSION_SIGNALS.some((signal) => normalizedIntent.includes(signal));
758
+ }
759
+
760
+ function enforceContextBudget(result, maxFiles) {
761
+ const prioritizedGroups = [
762
+ 'primaryTargets',
763
+ 'relatedTests',
764
+ 'styleFiles',
765
+ 'sharedAbstractions',
766
+ 'analogFiles',
767
+ ];
768
+ const kept = new Set();
769
+
770
+ for (const group of prioritizedGroups) {
771
+ const nextItems = [];
772
+ for (const item of result[group] ?? []) {
773
+ if (kept.size >= maxFiles) break;
774
+ if (kept.has(item)) continue;
775
+ kept.add(item);
776
+ nextItems.push(item);
777
+ }
778
+ result[group] = nextItems;
779
+ result.explanations[group] = (result.explanations[group] ?? [])
780
+ .filter((entry) => nextItems.includes(entry.filePath));
781
+ }
782
+ }
783
+
784
+ function addExplainedFile(result, group, filePath, reason) {
785
+ if (!result[group].includes(filePath)) {
786
+ result[group].push(filePath);
787
+ }
788
+
789
+ const explanations = result.explanations[group] ?? [];
790
+ const existing = explanations.find((entry) => entry.filePath === filePath);
791
+ if (!existing) {
792
+ explanations.push({ filePath, reason });
793
+ result.explanations[group] = explanations;
794
+ return;
795
+ }
796
+
797
+ if (!existing.reason.includes(reason)) {
798
+ existing.reason = `${existing.reason}; ${reason}`;
799
+ }
800
+ }
801
+
802
+ function shouldLoadAnalogArtifacts({
803
+ result,
804
+ taskType = 'simple',
805
+ targetFile = null,
806
+ maxFiles = 5,
807
+ queryWasUsed = false,
808
+ preferLocalizedDirectContext = false,
809
+ } = {}) {
810
+ const selectedCount = countSelectedFiles(result);
811
+ if (selectedCount >= maxFiles) {
812
+ return false;
813
+ }
814
+
815
+ if (preferLocalizedDirectContext) {
816
+ return false;
817
+ }
818
+
819
+ if (
820
+ taskType === 'trivial'
821
+ && targetFile
822
+ && !queryWasUsed
823
+ && (result.relatedTests?.length ?? 0) > 0
824
+ ) {
825
+ return false;
826
+ }
827
+
828
+ return true;
829
+ }
830
+
831
+ function shouldPreferLocalizedDirectTargetContext({
832
+ result,
833
+ taskType = 'simple',
834
+ targetFile = null,
835
+ queryWasUsed = false,
836
+ } = {}) {
837
+ if (queryWasUsed || !targetFile) {
838
+ return false;
839
+ }
840
+
841
+ if (taskType !== 'simple' && taskType !== 'trivial') {
842
+ return false;
843
+ }
844
+
845
+ return isTestLikeFile(targetFile)
846
+ || (result.relatedTests?.length ?? 0) > 0;
847
+ }
848
+
849
+ function countSelectedFiles(result) {
850
+ const selected = new Set();
851
+ for (const group of ['primaryTargets', 'relatedTests', 'styleFiles', 'sharedAbstractions', 'analogFiles']) {
852
+ for (const filePath of result[group] ?? []) {
853
+ selected.add(filePath);
854
+ }
855
+ }
856
+ return selected.size;
857
+ }
858
+
859
+ function buildQueryReason(queryResult) {
860
+ const reasons = queryResult.reasons ?? [];
861
+ if (reasons.length === 0) return 'query match';
862
+ return `query match: ${reasons.join(', ')}`;
863
+ }
864
+
865
+ function includeDirectTestTargets(result) {
866
+ for (const primaryTarget of result.primaryTargets ?? []) {
867
+ if (!isTestLikeFile(primaryTarget)) continue;
868
+ addExplainedFile(result, 'relatedTests', primaryTarget, `direct test target ${primaryTarget}`);
869
+ }
870
+ }
871
+
872
+ function isTestLikeFile(filePath) {
873
+ return /\.(test|spec)\.[a-z0-9]+$/i.test(filePath)
874
+ || /(^|\/)(?:__tests__|tests?|specs?)\//i.test(filePath);
875
+ }
876
+
877
+ const RELATED_TEST_RELATION_TYPES = [
878
+ { key: 'components', label: 'component' },
879
+ { key: 'composables', label: 'composable' },
880
+ { key: 'services', label: 'service' },
881
+ { key: 'stores', label: 'store' },
882
+ { key: 'utils', label: 'util' },
883
+ ];
884
+
885
+ function buildRelatedTestLookups({
886
+ analogsArtifact = { items: [] },
887
+ relationsArtifact = { items: [] },
888
+ } = {}) {
889
+ return {
890
+ analogsMap: new Map((analogsArtifact.items ?? []).map((item) => [item.filePath, item.analogs ?? []])),
891
+ relationsMap: new Map((relationsArtifact.items ?? []).map((item) => [item.filePath, item.relations ?? {}])),
892
+ };
893
+ }
894
+
895
+ function inferRelatedTestsFromArtifacts({
896
+ candidateFiles = [],
897
+ analogsMap = new Map(),
898
+ relationsMap = new Map(),
899
+ limit = 5,
900
+ } = {}) {
901
+ const normalizedCandidates = [...new Set(
902
+ candidateFiles
903
+ .map((filePath) => String(filePath ?? '').trim())
904
+ .filter(Boolean),
905
+ )];
906
+
907
+ if (normalizedCandidates.length === 0) {
908
+ return [];
909
+ }
910
+
911
+ const suggestions = new Map();
912
+
913
+ for (const [candidateRank, candidateFile] of normalizedCandidates.entries()) {
914
+ const rankPenalty = candidateRank * 5;
915
+ const relations = relationsMap.get(candidateFile) ?? {};
916
+
917
+ addTestsFromSource({
918
+ sourceFilePath: candidateFile,
919
+ relationsMap,
920
+ suggestions,
921
+ score: 120 - rankPenalty,
922
+ reason: `direct test for ${candidateFile}`,
923
+ });
924
+
925
+ for (const siblingFile of (relations.siblings ?? []).slice(0, 3)) {
926
+ addTestsFromSource({
927
+ sourceFilePath: siblingFile,
928
+ relationsMap,
929
+ suggestions,
930
+ score: 90 - rankPenalty,
931
+ reason: `sibling test via ${siblingFile}`,
932
+ });
933
+ }
934
+
935
+ for (const analog of (analogsMap.get(candidateFile) ?? []).slice(0, 3)) {
936
+ addTestsFromSource({
937
+ sourceFilePath: analog.filePath,
938
+ relationsMap,
939
+ suggestions,
940
+ score: 80 + Math.round((Number(analog.score) || 0) * 20) - rankPenalty,
941
+ reason: `analog test via ${analog.filePath}${analog.reason ? `; ${analog.reason}` : ''}`,
942
+ });
943
+ }
944
+
945
+ for (const { key, label } of RELATED_TEST_RELATION_TYPES) {
946
+ for (const relatedFile of (relations[key] ?? []).slice(0, 2)) {
947
+ addTestsFromSource({
948
+ sourceFilePath: relatedFile,
949
+ relationsMap,
950
+ suggestions,
951
+ score: 60 - rankPenalty,
952
+ reason: `${label} test via ${relatedFile}`,
953
+ });
954
+ }
955
+ }
956
+ }
957
+
958
+ return [...suggestions.values()]
959
+ .sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath))
960
+ .slice(0, Math.max(limit, 0))
961
+ .map((suggestion) => ({
962
+ filePath: suggestion.filePath,
963
+ score: suggestion.score,
964
+ reason: suggestion.reasons.join('; '),
965
+ }));
966
+ }
967
+
968
+ function addTestsFromSource({
969
+ sourceFilePath,
970
+ relationsMap,
971
+ suggestions,
972
+ score,
973
+ reason,
974
+ }) {
975
+ if (!sourceFilePath) {
976
+ return;
977
+ }
978
+
979
+ const tests = relationsMap.get(sourceFilePath)?.tests ?? [];
980
+ for (const testFilePath of tests) {
981
+ if (!suggestions.has(testFilePath)) {
982
+ suggestions.set(testFilePath, {
983
+ filePath: testFilePath,
984
+ score,
985
+ reasons: [],
986
+ });
987
+ }
988
+
989
+ const suggestion = suggestions.get(testFilePath);
990
+ suggestion.score = Math.max(suggestion.score, score);
991
+ if (!suggestion.reasons.includes(reason)) {
992
+ suggestion.reasons.push(reason);
993
+ }
994
+ }
995
+ }
996
+
997
+ // ── Bug Triage ──
998
+
999
+ export async function triageBug({ rootDir = process.cwd(), signature } = {}) {
1000
+ if (!signature || !signature.trim()) {
1001
+ throw new Error('Bug signature is required. Usage: node .claude/ukit/index/triage.mjs "<error signature>"');
1002
+ }
1003
+
1004
+ const deepKeywords = ['race', 'flaky', 'intermittent', 'timeout', 'deadlock'];
1005
+ const queryResults = await queryCodeIndex({ rootDir, query: signature, limit: 3 });
1006
+ const prioritized = [...queryResults].sort((a, b) => {
1007
+ const as = a.filePath.startsWith('src/') ? 1 : 0;
1008
+ const bs = b.filePath.startsWith('src/') ? 1 : 0;
1009
+ if (as !== bs) return bs - as;
1010
+ return b.score - a.score;
1011
+ });
1012
+
1013
+ const top = prioritized[0] ?? null;
1014
+ const lane = deepKeywords.some((k) => signature.toLowerCase().includes(k))
1015
+ ? 'deep'
1016
+ : (top && top.score >= 8 ? 'fast' : 'deep');
1017
+ const directRecommendedTestFile = prioritized.flatMap((item) => item.tests ?? []).find(Boolean);
1018
+ const shouldLoadRelatedArtifacts = Boolean(top) || !directRecommendedTestFile;
1019
+ const relatedArtifacts = shouldLoadRelatedArtifacts
1020
+ ? await loadRelatedTestArtifacts({ rootDir })
1021
+ : null;
1022
+ const inferredRecommendedTests = directRecommendedTestFile
1023
+ ? []
1024
+ : inferRelatedTestsFromArtifacts({
1025
+ candidateFiles: prioritized.map((item) => item.filePath),
1026
+ analogsMap: relatedArtifacts?.analogsMap,
1027
+ relationsMap: relatedArtifacts?.relationsMap,
1028
+ limit: 3,
1029
+ });
1030
+
1031
+ const analogFiles = top
1032
+ ? (relatedArtifacts?.analogsMap.get(top.filePath) ?? []).slice(0, 2)
1033
+ : [];
1034
+
1035
+ return {
1036
+ signature,
1037
+ lane,
1038
+ confidence: top ? Math.min(top.score / 20, 1) : 0,
1039
+ suspectFiles: prioritized.map((x) => x.filePath),
1040
+ analogFiles: analogFiles.map((a) => a.filePath),
1041
+ reasons: top?.reasons ?? [],
1042
+ recommendedTestCommand: await buildTestCommand(
1043
+ rootDir,
1044
+ directRecommendedTestFile
1045
+ ?? inferredRecommendedTests[0]?.filePath
1046
+ ?? '<target-test-file>',
1047
+ ),
1048
+ suggestedLoop: [
1049
+ 'Reproduce exactly once with the failing command',
1050
+ 'Open only top 1-3 suspect files from index output',
1051
+ 'If analogs found, open 1 analog file for pattern reference',
1052
+ 'Run one 15-minute loop: hypothesis -> patch -> targeted test',
1053
+ 'If two loops fail, switch to deep debugging lane',
1054
+ ],
1055
+ };
1056
+ }
1057
+
1058
+ export async function inferRelatedTests({
1059
+ rootDir = process.cwd(),
1060
+ candidateFiles = [],
1061
+ limit = 5,
1062
+ analogsArtifact = null,
1063
+ relationsArtifact = null,
1064
+ } = {}) {
1065
+ const {
1066
+ analogsMap,
1067
+ relationsMap,
1068
+ } = await loadRelatedTestArtifacts({
1069
+ rootDir,
1070
+ analogsArtifact,
1071
+ relationsArtifact,
1072
+ });
1073
+
1074
+ return inferRelatedTestsFromArtifacts({
1075
+ candidateFiles,
1076
+ limit,
1077
+ analogsMap,
1078
+ relationsMap,
1079
+ });
1080
+ }
1081
+
1082
+ async function loadRelatedTestArtifacts({
1083
+ rootDir = process.cwd(),
1084
+ analogsArtifact = null,
1085
+ relationsArtifact = null,
1086
+ } = {}) {
1087
+ const absoluteRoot = path.resolve(rootDir);
1088
+
1089
+ if (!analogsArtifact && !relationsArtifact) {
1090
+ if (!RELATED_TEST_LOOKUP_CACHE.has(absoluteRoot)) {
1091
+ RELATED_TEST_LOOKUP_CACHE.set(
1092
+ absoluteRoot,
1093
+ Promise.all([
1094
+ readArtifact(absoluteRoot, INDEX_ARTIFACTS.analogs),
1095
+ readArtifact(absoluteRoot, INDEX_ARTIFACTS.relations),
1096
+ ])
1097
+ .then(([resolvedAnalogsArtifact, resolvedRelationsArtifact]) => ({
1098
+ analogsArtifact: resolvedAnalogsArtifact,
1099
+ relationsArtifact: resolvedRelationsArtifact,
1100
+ ...buildRelatedTestLookups({
1101
+ analogsArtifact: resolvedAnalogsArtifact,
1102
+ relationsArtifact: resolvedRelationsArtifact,
1103
+ }),
1104
+ }))
1105
+ .catch((error) => {
1106
+ RELATED_TEST_LOOKUP_CACHE.delete(absoluteRoot);
1107
+ throw error;
1108
+ }),
1109
+ );
1110
+ }
1111
+
1112
+ return RELATED_TEST_LOOKUP_CACHE.get(absoluteRoot);
1113
+ }
1114
+
1115
+ const [resolvedAnalogsArtifact, resolvedRelationsArtifact] = await Promise.all([
1116
+ analogsArtifact ?? readArtifact(absoluteRoot, INDEX_ARTIFACTS.analogs),
1117
+ relationsArtifact ?? readArtifact(absoluteRoot, INDEX_ARTIFACTS.relations),
1118
+ ]);
1119
+
1120
+ return {
1121
+ analogsArtifact: resolvedAnalogsArtifact,
1122
+ relationsArtifact: resolvedRelationsArtifact,
1123
+ ...buildRelatedTestLookups({
1124
+ analogsArtifact: resolvedAnalogsArtifact,
1125
+ relationsArtifact: resolvedRelationsArtifact,
1126
+ }),
1127
+ };
1128
+ }
1129
+
1130
+ // ── Index V2: Archetype Classification ──
1131
+
1132
+ const ARCHETYPE_RULES = [
1133
+ { pattern: /(?:^|\/)(?:__tests__|test|tests|spec|specs)\//, archetype: 'test' },
1134
+ { pattern: /(?:^|\/)pages\/.*(?:List|list)\b/, archetype: 'list-page' },
1135
+ { pattern: /(?:^|\/)pages\/.*(?:Detail|detail|View|view)\b/, archetype: 'detail-page' },
1136
+ { pattern: /(?:^|\/)pages\/.*(?:Form|form|Edit|edit|Create|create|Add|add)\b/, archetype: 'form-page' },
1137
+ { pattern: /(?:^|\/)pages\/|^app\.vue$|^layouts\//, archetype: 'page' },
1138
+ { pattern: /(?:^|\/)components\/.*(?:Modal|modal|Popup|popup|Dialog|dialog)\b/, archetype: 'modal' },
1139
+ { pattern: /(?:^|\/)components\/.*(?:Grid|grid|Table|table)\b/, archetype: 'grid' },
1140
+ { pattern: /(?:^|\/)components\//, archetype: 'component' },
1141
+ { pattern: /(?:^|\/)composables\/|^use[A-Z]/, archetype: 'composable' },
1142
+ { pattern: /(?:^|\/)hooks\//, archetype: 'hook' },
1143
+ { pattern: /(?:^|\/)(?:api|apis?|services?)\//, archetype: 'api' },
1144
+ { pattern: /(?:^|\/)store(?:s)?\//, archetype: 'store' },
1145
+ { pattern: /(?:^|\/)(?:utils?|helpers?)\//, archetype: 'util' },
1146
+ { pattern: /(?:^|\/)middleware\//, archetype: 'middleware' },
1147
+ { pattern: /(?:^|\/)layouts?\//, archetype: 'layout' },
1148
+ { pattern: /(?:^|\/)(?:config|conf|settings)\./, archetype: 'config' },
1149
+ { pattern: /(?:^|\/)migrations?\//, archetype: 'migration' },
1150
+ { pattern: /(?:^|\/)(?:models?|schemas?)\//, archetype: 'schema' },
1151
+ ];
1152
+
1153
+ function classifyArchetypes(fileRecords, symbols) {
1154
+ const symbolNamesByPath = groupBy(symbols, (s) => s.filePath);
1155
+
1156
+ return fileRecords
1157
+ .filter((f) => CODE_EXTENSIONS.has(f.ext))
1158
+ .map((file) => {
1159
+ if (isLikelyTestFile(file.filePath)) {
1160
+ return { filePath: file.filePath, archetype: 'test' };
1161
+ }
1162
+
1163
+ let archetype = null;
1164
+
1165
+ for (const rule of ARCHETYPE_RULES) {
1166
+ if (rule.pattern.test(file.filePath)) {
1167
+ archetype = rule.archetype;
1168
+ break;
1169
+ }
1170
+ }
1171
+
1172
+ if (!archetype) {
1173
+ const fileSymbols = symbolNamesByPath.get(file.filePath) ?? [];
1174
+ const hasDefaultExport = fileSymbols.some((s) => s.name === 'default');
1175
+ const hasComponentSignal = file.ext === '.vue'
1176
+ || fileSymbols.some((s) =>
1177
+ s.type === 'component-name'
1178
+ || s.name.toLowerCase().includes('component')
1179
+ || s.name.toLowerCase().includes('page'),
1180
+ );
1181
+
1182
+ if ((hasDefaultExport || file.ext === '.vue') && hasComponentSignal) {
1183
+ archetype = 'component';
1184
+ } else {
1185
+ archetype = 'other';
1186
+ }
1187
+ }
1188
+
1189
+ return { filePath: file.filePath, archetype };
1190
+ });
1191
+ }
1192
+
1193
+ // ── Index V2: Feature Relations ──
1194
+
1195
+ function buildRelations(fileRecords, archetypes, testsMap, resolvedImportsBySource, resolvedStyleImportsBySource = new Map()) {
1196
+ const archetypeMap = new Map(archetypes.map((a) => [a.filePath, a.archetype]));
1197
+ const testsBySource = new Map((testsMap ?? []).map((t) => [t.sourceFile, t.tests ?? []]));
1198
+ const codeFiles = fileRecords.filter((f) => CODE_EXTENSIONS.has(f.ext));
1199
+ const featureDirByFile = new Map(codeFiles.map((file) => [file.filePath, getFeatureDir(file.filePath)]));
1200
+ const siblingsByKey = groupBy(
1201
+ codeFiles,
1202
+ (file) => `${featureDirByFile.get(file.filePath)}::${archetypeMap.get(file.filePath) ?? 'other'}`,
1203
+ );
1204
+
1205
+ return codeFiles.map((file) => {
1206
+ const filePath = file.filePath;
1207
+ const archetype = archetypeMap.get(filePath) ?? 'other';
1208
+ const fileImports = [...(resolvedImportsBySource.get(filePath) ?? new Set())];
1209
+ const styleImports = [...(resolvedStyleImportsBySource.get(filePath) ?? new Set())];
1210
+ const featureDir = featureDirByFile.get(filePath) ?? '';
1211
+ const relations = {};
1212
+
1213
+ for (const resolved of fileImports) {
1214
+ const targetArchetype = archetypeMap.get(resolved) ?? guessArchetypeFromPath(resolved);
1215
+ if (!targetArchetype) continue;
1216
+
1217
+ if (targetArchetype === 'component' || targetArchetype === 'modal' || targetArchetype === 'grid') {
1218
+ relations.components ??= [];
1219
+ if (!relations.components.includes(resolved)) relations.components.push(resolved);
1220
+ } else if (targetArchetype === 'composable' || targetArchetype === 'hook') {
1221
+ relations.composables ??= [];
1222
+ if (!relations.composables.includes(resolved)) relations.composables.push(resolved);
1223
+ } else if (targetArchetype === 'api' || targetArchetype === 'service') {
1224
+ relations.services ??= [];
1225
+ if (!relations.services.includes(resolved)) relations.services.push(resolved);
1226
+ } else if (targetArchetype === 'store') {
1227
+ relations.stores ??= [];
1228
+ if (!relations.stores.includes(resolved)) relations.stores.push(resolved);
1229
+ } else if (targetArchetype === 'util') {
1230
+ relations.utils ??= [];
1231
+ if (!relations.utils.includes(resolved)) relations.utils.push(resolved);
1232
+ }
1233
+ }
1234
+
1235
+ const relatedTests = testsBySource.get(filePath) ?? [];
1236
+ if (relatedTests.length > 0) relations.tests = relatedTests;
1237
+
1238
+ if (styleImports.length > 0) relations.styles = styleImports;
1239
+
1240
+ const siblings = (siblingsByKey.get(`${featureDir}::${archetype}`) ?? [])
1241
+ .filter((candidate) => candidate.filePath !== filePath)
1242
+ .map((candidate) => candidate.filePath);
1243
+ if (siblings.length > 0) relations.siblings = siblings;
1244
+
1245
+ const hasAnyRelation = Object.keys(relations).length > 0;
1246
+ return {
1247
+ filePath,
1248
+ archetype,
1249
+ ...(hasAnyRelation ? { relations } : {}),
1250
+ };
1251
+ });
1252
+ }
1253
+
1254
+ function getFeatureDir(filePath) {
1255
+ const parts = filePath.split('/');
1256
+ if (parts.length >= 3) return parts.slice(0, 3).join('/');
1257
+ if (parts.length >= 2) return parts.slice(0, 2).join('/');
1258
+ return parts[0] ?? '';
1259
+ }
1260
+
1261
+ function guessArchetypeFromPath(filePath) {
1262
+ for (const rule of ARCHETYPE_RULES) {
1263
+ if (rule.pattern.test(filePath)) return rule.archetype;
1264
+ }
1265
+ return null;
1266
+ }
1267
+
1268
+ // ── Index V2: Analog Candidates ──
1269
+
1270
+ const ANALOG_RELATION_TYPES = ['components', 'composables', 'services'];
1271
+
1272
+ function buildAnalogs(fileRecords, archetypes, relations, resolvedImportsByFile) {
1273
+ const archetypeMap = new Map(archetypes.map((a) => [a.filePath, a.archetype]));
1274
+ const codeFiles = fileRecords.filter((f) => CODE_EXTENSIONS.has(f.ext));
1275
+ const relationLookupByFile = new Map(
1276
+ relations.map((relation) => [relation.filePath, buildAnalogRelationLookup(relation.relations ?? {})]),
1277
+ );
1278
+ const descriptors = codeFiles.map((file) => buildAnalogDescriptor({
1279
+ filePath: file.filePath,
1280
+ archetype: archetypeMap.get(file.filePath) ?? 'other',
1281
+ imports: resolvedImportsByFile.get(file.filePath) ?? new Set(),
1282
+ relationLookup: relationLookupByFile.get(file.filePath) ?? buildAnalogRelationLookup(),
1283
+ }));
1284
+ const analogIndexesByArchetype = buildAnalogIndexes(descriptors);
1285
+
1286
+ return descriptors.map((descriptor) => {
1287
+ const analogIndex = analogIndexesByArchetype.get(descriptor.archetype) ?? createEmptyAnalogIndex();
1288
+ const candidateScores = new Map();
1289
+
1290
+ for (const importPath of descriptor.imports) {
1291
+ accumulateAnalogCandidates({
1292
+ candidatePaths: analogIndex.byImport.get(importPath),
1293
+ selfPath: descriptor.filePath,
1294
+ candidateScores,
1295
+ scoreDelta: 3,
1296
+ applyReason: (candidate) => candidate.sharedImports.add(importPath),
1297
+ });
1298
+ }
1299
+
1300
+ if (descriptor.featureDir) {
1301
+ accumulateAnalogCandidates({
1302
+ candidatePaths: analogIndex.byFeatureDir.get(descriptor.featureDir),
1303
+ selfPath: descriptor.filePath,
1304
+ candidateScores,
1305
+ scoreDelta: 2,
1306
+ applyReason: (candidate) => {
1307
+ candidate.sameFeatureDir = true;
1308
+ },
1309
+ });
1310
+ }
1311
+
1312
+ for (const relType of ANALOG_RELATION_TYPES) {
1313
+ for (const target of descriptor.relationLookup[relType]) {
1314
+ accumulateAnalogCandidates({
1315
+ candidatePaths: analogIndex.byRelationTarget.get(`${relType}:${target}`),
1316
+ selfPath: descriptor.filePath,
1317
+ candidateScores,
1318
+ scoreDelta: 2,
1319
+ applyReason: (candidate) => {
1320
+ candidate.sharedRelations[relType].add(target);
1321
+ },
1322
+ });
1323
+ }
1324
+ }
1325
+
1326
+ if (descriptor.namePrefix3.length >= 3) {
1327
+ accumulateAnalogCandidates({
1328
+ candidatePaths: analogIndex.byNamePrefix.get(descriptor.namePrefix3),
1329
+ selfPath: descriptor.filePath,
1330
+ candidateScores,
1331
+ scoreDelta: 1,
1332
+ applyReason: (candidate) => {
1333
+ candidate.namePrefix = descriptor.namePrefix3;
1334
+ },
1335
+ });
1336
+ }
1337
+
1338
+ const analogs = [...candidateScores.entries()]
1339
+ .map(([filePath, candidate]) => ({
1340
+ filePath,
1341
+ score: Math.min(candidate.score / 10, 1),
1342
+ reason: formatAnalogReasons(candidate),
1343
+ }))
1344
+ .filter((candidate) => candidate.reason)
1345
+ .sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath))
1346
+ .slice(0, 5);
1347
+
1348
+ return { filePath: descriptor.filePath, archetype: descriptor.archetype, analogs };
1349
+ });
1350
+ }
1351
+
1352
+ function buildAnalogDescriptor({ filePath, archetype, imports, relationLookup }) {
1353
+ const baseName = path.basename(filePath, path.extname(filePath)).toLowerCase();
1354
+
1355
+ return {
1356
+ filePath,
1357
+ archetype,
1358
+ featureDir: getFeatureDir(filePath),
1359
+ imports,
1360
+ relationLookup,
1361
+ namePrefix3: baseName.slice(0, 3),
1362
+ };
1363
+ }
1364
+
1365
+ function buildAnalogIndexes(descriptors) {
1366
+ const indexesByArchetype = new Map();
1367
+
1368
+ for (const descriptor of descriptors) {
1369
+ let analogIndex = indexesByArchetype.get(descriptor.archetype);
1370
+ if (!analogIndex) {
1371
+ analogIndex = createEmptyAnalogIndex();
1372
+ indexesByArchetype.set(descriptor.archetype, analogIndex);
1373
+ }
1374
+
1375
+ for (const importPath of descriptor.imports) {
1376
+ indexAnalogValue(analogIndex.byImport, importPath, descriptor.filePath);
1377
+ }
1378
+
1379
+ if (descriptor.featureDir) {
1380
+ indexAnalogValue(analogIndex.byFeatureDir, descriptor.featureDir, descriptor.filePath);
1381
+ }
1382
+
1383
+ for (const relType of ANALOG_RELATION_TYPES) {
1384
+ for (const target of descriptor.relationLookup[relType]) {
1385
+ indexAnalogValue(analogIndex.byRelationTarget, `${relType}:${target}`, descriptor.filePath);
1386
+ }
1387
+ }
1388
+
1389
+ if (descriptor.namePrefix3.length >= 3) {
1390
+ indexAnalogValue(analogIndex.byNamePrefix, descriptor.namePrefix3, descriptor.filePath);
1391
+ }
1392
+ }
1393
+
1394
+ return indexesByArchetype;
1395
+ }
1396
+
1397
+ function createEmptyAnalogIndex() {
1398
+ return {
1399
+ byImport: new Map(),
1400
+ byFeatureDir: new Map(),
1401
+ byRelationTarget: new Map(),
1402
+ byNamePrefix: new Map(),
1403
+ };
1404
+ }
1405
+
1406
+ function indexAnalogValue(indexMap, key, filePath) {
1407
+ if (!key) return;
1408
+
1409
+ if (!indexMap.has(key)) {
1410
+ indexMap.set(key, new Set());
1411
+ }
1412
+ indexMap.get(key).add(filePath);
1413
+ }
1414
+
1415
+ function buildAnalogRelationLookup(relations = {}) {
1416
+ return {
1417
+ components: new Set(relations.components ?? []),
1418
+ composables: new Set(relations.composables ?? []),
1419
+ services: new Set(relations.services ?? []),
1420
+ };
1421
+ }
1422
+
1423
+ function accumulateAnalogCandidates({
1424
+ candidatePaths,
1425
+ selfPath,
1426
+ candidateScores,
1427
+ scoreDelta,
1428
+ applyReason,
1429
+ }) {
1430
+ if (!candidatePaths) return;
1431
+
1432
+ for (const candidatePath of candidatePaths) {
1433
+ if (candidatePath === selfPath) continue;
1434
+
1435
+ if (!candidateScores.has(candidatePath)) {
1436
+ candidateScores.set(candidatePath, {
1437
+ score: 0,
1438
+ sharedImports: new Set(),
1439
+ sameFeatureDir: false,
1440
+ sharedRelations: buildAnalogRelationLookup(),
1441
+ namePrefix: '',
1442
+ });
1443
+ }
1444
+
1445
+ const candidate = candidateScores.get(candidatePath);
1446
+ candidate.score += scoreDelta;
1447
+ applyReason(candidate);
1448
+ }
1449
+ }
1450
+
1451
+ function formatAnalogReasons(candidate) {
1452
+ const reasons = [];
1453
+
1454
+ if (candidate.sharedImports.size > 0) {
1455
+ reasons.push(`shared imports: ${[...candidate.sharedImports].slice(0, 3).join(', ')}`);
1456
+ }
1457
+ if (candidate.sameFeatureDir) {
1458
+ reasons.push('same feature dir');
1459
+ }
1460
+ for (const relType of ANALOG_RELATION_TYPES) {
1461
+ const sharedTargets = [...candidate.sharedRelations[relType]];
1462
+ if (sharedTargets.length > 0) {
1463
+ reasons.push(`shared ${relType}: ${sharedTargets.join(', ')}`);
1464
+ }
1465
+ }
1466
+ if (candidate.namePrefix) {
1467
+ reasons.push(`similar name prefix: ${candidate.namePrefix}`);
1468
+ }
1469
+
1470
+ return reasons.join('; ');
1471
+ }
1472
+
1473
+ function buildResolvedImportMap(rootDir, imports, indexedFileSet, importAliasContext) {
1474
+ const resolvedImportsBySource = new Map();
1475
+
1476
+ for (const imp of imports) {
1477
+ if (!imp?.from || !imp?.to) continue;
1478
+ const resolved = resolveImportSpecifier({
1479
+ rootDir,
1480
+ fromFilePath: imp.from,
1481
+ specifier: imp.to,
1482
+ indexedFileSet,
1483
+ aliasContext: importAliasContext,
1484
+ });
1485
+ if (!resolved) continue;
1486
+
1487
+ if (!resolvedImportsBySource.has(imp.from)) {
1488
+ resolvedImportsBySource.set(imp.from, new Set());
1489
+ }
1490
+ resolvedImportsBySource.get(imp.from).add(resolved);
1491
+ }
1492
+
1493
+ return resolvedImportsBySource;
1494
+ }
1495
+
1496
+ // ── Helpers ──
1497
+
1498
+ async function collectFiles(scanRoots) {
1499
+ const result = [];
1500
+ const stack = [...scanRoots];
1501
+ const visitedDirectoryRealPaths = new Set();
1502
+
1503
+ while (stack.length > 0) {
1504
+ const current = stack.pop();
1505
+ let currentRealPath;
1506
+ try {
1507
+ currentRealPath = await fs.realpath(current);
1508
+ } catch {
1509
+ continue;
1510
+ }
1511
+
1512
+ if (visitedDirectoryRealPaths.has(currentRealPath)) {
1513
+ continue;
1514
+ }
1515
+ visitedDirectoryRealPaths.add(currentRealPath);
1516
+
1517
+ let entries;
1518
+ try {
1519
+ entries = await fs.readdir(current, { withFileTypes: true });
1520
+ } catch {
1521
+ continue;
1522
+ }
1523
+
1524
+ const symlinkDirectories = [];
1525
+ const directDirectories = [];
1526
+ const symlinkFiles = [];
1527
+ for (const entry of entries) {
1528
+ const fullPath = path.join(current, entry.name);
1529
+ if (entry.isDirectory()) {
1530
+ if (shouldSkipDirectory(entry.name)) continue;
1531
+ directDirectories.push(fullPath);
1532
+ } else if (entry.isSymbolicLink()) {
1533
+ try {
1534
+ const stat = await fs.stat(fullPath);
1535
+ if (stat.isDirectory() && !shouldSkipDirectory(entry.name)) {
1536
+ symlinkDirectories.push(fullPath);
1537
+ continue;
1538
+ }
1539
+
1540
+ if (stat.isFile()) {
1541
+ const ext = path.extname(entry.name).toLowerCase();
1542
+ if (DISCOVERED_EXTENSIONS.has(ext)) {
1543
+ symlinkFiles.push({
1544
+ absolutePath: fullPath,
1545
+ mtimeMs: Math.floor(stat.mtimeMs),
1546
+ size: stat.size,
1547
+ });
1548
+ }
1549
+ }
1550
+ } catch {
1551
+ // broken symlink, skip
1552
+ }
1553
+ }
1554
+ }
1555
+ stack.push(...symlinkDirectories, ...directDirectories);
1556
+
1557
+ const fileEntries = entries.filter((entry) => {
1558
+ if (!entry.isFile()) return false;
1559
+ const ext = path.extname(entry.name).toLowerCase();
1560
+ return DISCOVERED_EXTENSIONS.has(ext);
1561
+ });
1562
+ const fileStats = (await Promise.all(fileEntries.map(async (entry) => {
1563
+ try {
1564
+ const fullPath = path.join(current, entry.name);
1565
+ const stat = await fs.stat(fullPath);
1566
+ return { absolutePath: fullPath, mtimeMs: Math.floor(stat.mtimeMs), size: stat.size };
1567
+ } catch {
1568
+ return null;
1569
+ }
1570
+ }))).filter(Boolean);
1571
+ result.push(...fileStats, ...symlinkFiles);
1572
+ }
1573
+
1574
+ return result;
1575
+ }
1576
+
1577
+ function extractScriptContent(filePath, content) {
1578
+ if (!filePath.endsWith('.vue')) return content;
1579
+ const allMatches = [...content.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/gi)];
1580
+ if (allMatches.length === 0) return '';
1581
+ return allMatches.map((m) => m[1]).join('\n');
1582
+ }
1583
+
1584
+ function extractSymbols(filePath, content) {
1585
+ const out = [];
1586
+ const addSymbol = (name, type) => {
1587
+ if (!name) return;
1588
+ out.push({ name, type, filePath });
1589
+ };
1590
+ const addMatches = (regex, type, idx = 1) => {
1591
+ for (const match of content.matchAll(regex)) {
1592
+ const name = match[idx];
1593
+ if (!name) continue;
1594
+ addSymbol(name, type);
1595
+ }
1596
+ };
1597
+
1598
+ addMatches(/export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'function');
1599
+ addMatches(/export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'class');
1600
+ addMatches(/(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\(/g, 'callable');
1601
+ addMatches(/(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/g, 'callable');
1602
+ addMatches(/export\s+default\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'default-function');
1603
+ addMatches(/export\s+default\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g, 'default-class');
1604
+ addMatches(/export\s+default\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:;|\n|$)/g, 'default-reference');
1605
+ addMatches(/defineOptions\(\s*\{[\s\S]*?name\s*:\s*['"]([^'"]+)['"]/g, 'component-name');
1606
+ addMatches(/defineComponent\(\s*\{[\s\S]*?name\s*:\s*['"]([^'"]+)['"]/g, 'component-name');
1607
+ addMatches(/export\s+default\s*\{[\s\S]*?name\s*:\s*['"]([^'"]+)['"]/g, 'component-name');
1608
+
1609
+ if (/\bexport\s+default\b/.test(content)) {
1610
+ addSymbol('default', 'default-export');
1611
+ }
1612
+
1613
+ for (const match of content.matchAll(/export\s*\{([^}]+)\}/g)) {
1614
+ for (const part of (match[1] ?? '').split(',').map((x) => x.trim()).filter(Boolean)) {
1615
+ const parsed = part.match(/^([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*))?$/);
1616
+ if (!parsed) continue;
1617
+ addSymbol(parsed[2] ?? parsed[1], 'named-export');
1618
+ }
1619
+ }
1620
+
1621
+ const seen = new Set();
1622
+ return out.filter((item) => {
1623
+ const key = `${item.filePath}:${item.type}:${item.name}`;
1624
+ if (seen.has(key)) return false;
1625
+ seen.add(key);
1626
+ return true;
1627
+ });
1628
+ }
1629
+
1630
+ function extractImports(filePath, content) {
1631
+ const out = [];
1632
+ const stripped = content
1633
+ .replace(/\/\*[\s\S]*?\*\//g, '')
1634
+ .replace(/\/\/.*$/gm, '');
1635
+ for (const match of stripped.matchAll(/import\s+(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g)) {
1636
+ out.push({ from: filePath, to: match[1], kind: 'import' });
1637
+ }
1638
+ for (const match of stripped.matchAll(/import\(\s*['"]([^'"]+)['"]\s*\)/g)) {
1639
+ out.push({ from: filePath, to: match[1], kind: 'dynamic-import' });
1640
+ }
1641
+ for (const match of stripped.matchAll(/require\(\s*['"]([^'"]+)['"]\s*\)/g)) {
1642
+ out.push({ from: filePath, to: match[1], kind: 'require' });
1643
+ }
1644
+ for (const match of stripped.matchAll(/export\s+(?:type\s+)?\{[^}]+\}\s+from\s+['"]([^'"]+)['"]/g)) {
1645
+ out.push({ from: filePath, to: match[1], kind: 're-export' });
1646
+ }
1647
+ for (const match of stripped.matchAll(/export\s+\*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s+from\s+['"]([^'"]+)['"]/g)) {
1648
+ out.push({ from: filePath, to: match[1], kind: 're-export-all' });
1649
+ }
1650
+ return out;
1651
+ }
1652
+
1653
+ function extractSupplementalImports(filePath, content) {
1654
+ if (!filePath.endsWith('.vue')) {
1655
+ return [];
1656
+ }
1657
+
1658
+ const out = [];
1659
+ for (const match of content.matchAll(/<style\b[^>]*\bsrc=['"]([^'"]+)['"][^>]*>/gi)) {
1660
+ out.push({ from: filePath, to: match[1], kind: 'style-src' });
1661
+ }
1662
+
1663
+ return out;
1664
+ }
1665
+
1666
+ function buildTestsMap(fileRecords) {
1667
+ const tests = fileRecords.filter((x) => isLikelyTestFile(x.filePath));
1668
+ const sources = fileRecords.filter((x) => CODE_EXTENSIONS.has(x.ext) && !isLikelyTestFile(x.filePath));
1669
+
1670
+ return sources.map((source) => {
1671
+ const matches = tests
1672
+ .map((test) => ({
1673
+ filePath: test.filePath,
1674
+ score: scoreTestMatch(source.filePath, test.filePath),
1675
+ }))
1676
+ .filter((match) => match.score > 0)
1677
+ .sort((a, b) => b.score - a.score || a.filePath.localeCompare(b.filePath))
1678
+ .map((match) => match.filePath)
1679
+ .slice(0, 10);
1680
+
1681
+ return { sourceFile: source.filePath, tests: matches };
1682
+ });
1683
+ }
1684
+
1685
+ async function buildHotspots(rootDir, bugIndexSnapshot = null) {
1686
+ const snapshot = bugIndexSnapshot ?? await readBugIndexSnapshot(rootDir);
1687
+ if (!snapshot.exists) {
1688
+ return [];
1689
+ }
1690
+ const bugIndexPath = path.join(rootDir, 'docs', 'BUG_INDEX.md');
1691
+ let content;
1692
+ try {
1693
+ content = await fs.readFile(bugIndexPath, 'utf8');
1694
+ } catch {
1695
+ return [];
1696
+ }
1697
+
1698
+ const scores = new Map();
1699
+ const filePattern = /([A-Za-z0-9_@()[\].-]+(?:\/[A-Za-z0-9_@()[\].-]+)+\.(?:tsx|jsx|ts|js|mjs|cjs|vue))/g;
1700
+ for (const match of content.matchAll(filePattern)) {
1701
+ const fp = match[1];
1702
+ scores.set(fp, (scores.get(fp) ?? 0) + 1);
1703
+ }
1704
+
1705
+ return [...scores.entries()].map(([filePath, count]) => ({ filePath, count })).sort((a, b) => b.count - a.count || a.filePath.localeCompare(b.filePath));
1706
+ }
1707
+
1708
+ async function readBugIndexSnapshot(rootDir) {
1709
+ const bugIndexPath = path.join(rootDir, 'docs', 'BUG_INDEX.md');
1710
+
1711
+ try {
1712
+ const stat = await fs.stat(bugIndexPath);
1713
+ return {
1714
+ exists: true,
1715
+ mtimeMs: Math.floor(stat.mtimeMs),
1716
+ size: stat.size,
1717
+ };
1718
+ } catch {
1719
+ return { exists: false };
1720
+ }
1721
+ }
1722
+
1723
+ function areBugIndexSnapshotsEqual(previousSnapshot, nextSnapshot) {
1724
+ if (!previousSnapshot || !nextSnapshot) {
1725
+ return false;
1726
+ }
1727
+
1728
+ if (previousSnapshot.exists !== nextSnapshot.exists) {
1729
+ return false;
1730
+ }
1731
+
1732
+ if (!previousSnapshot.exists) {
1733
+ return true;
1734
+ }
1735
+
1736
+ return previousSnapshot.mtimeMs === nextSnapshot.mtimeMs
1737
+ && previousSnapshot.size === nextSnapshot.size;
1738
+ }
1739
+
1740
+ async function writeArtifact(rootDir, artifactName, payload) {
1741
+ await fs.writeFile(getArtifactPath(rootDir, artifactName), JSON.stringify(payload, null, 2));
1742
+ }
1743
+
1744
+ async function readArtifact(rootDir, artifactName) {
1745
+ const artifactPath = getArtifactPath(path.resolve(rootDir), artifactName);
1746
+
1747
+ if (!ARTIFACT_CACHE.has(artifactPath)) {
1748
+ ARTIFACT_CACHE.set(
1749
+ artifactPath,
1750
+ fs.readFile(artifactPath, 'utf8')
1751
+ .then((content) => {
1752
+ try { return JSON.parse(content); } catch (e) { ARTIFACT_CACHE.delete(artifactPath); return null; }
1753
+ })
1754
+ .catch((error) => {
1755
+ ARTIFACT_CACHE.delete(artifactPath);
1756
+ return null;
1757
+ }),
1758
+ );
1759
+ }
1760
+
1761
+ return ARTIFACT_CACHE.get(artifactPath);
1762
+ }
1763
+
1764
+ async function loadQuerySearchBundle(rootDir) {
1765
+ const absoluteRoot = path.resolve(rootDir);
1766
+
1767
+ if (!QUERY_SEARCH_BUNDLE_CACHE.has(absoluteRoot)) {
1768
+ QUERY_SEARCH_BUNDLE_CACHE.set(
1769
+ absoluteRoot,
1770
+ Promise.all([
1771
+ readArtifact(absoluteRoot, INDEX_ARTIFACTS.files),
1772
+ readArtifact(absoluteRoot, INDEX_ARTIFACTS.symbols),
1773
+ ])
1774
+ .then(([files, symbols]) => {
1775
+ const safeFiles = ensureArtifact(files);
1776
+ const safeSymbols = ensureArtifact(symbols);
1777
+ const symbolMap = groupBy(safeSymbols.items ?? [], (item) => item.filePath);
1778
+ return {
1779
+ files: safeFiles,
1780
+ searchDescriptorsByFile: buildFileSearchDescriptors(safeFiles.items ?? [], symbolMap),
1781
+ };
1782
+ })
1783
+ .catch((error) => {
1784
+ QUERY_SEARCH_BUNDLE_CACHE.delete(absoluteRoot);
1785
+ throw error;
1786
+ }),
1787
+ );
1788
+ }
1789
+
1790
+ return QUERY_SEARCH_BUNDLE_CACHE.get(absoluteRoot);
1791
+ }
1792
+
1793
+ async function loadQuerySupportBundle(rootDir) {
1794
+ const absoluteRoot = path.resolve(rootDir);
1795
+
1796
+ if (!QUERY_SUPPORT_BUNDLE_CACHE.has(absoluteRoot)) {
1797
+ QUERY_SUPPORT_BUNDLE_CACHE.set(
1798
+ absoluteRoot,
1799
+ loadQuerySearchBundle(absoluteRoot)
1800
+ .then(({ files }) => Promise.all([
1801
+ Promise.resolve(files),
1802
+ readArtifact(absoluteRoot, INDEX_ARTIFACTS.imports),
1803
+ readArtifact(absoluteRoot, INDEX_ARTIFACTS.testsMap),
1804
+ readArtifact(absoluteRoot, INDEX_ARTIFACTS.hotspots),
1805
+ readArtifact(absoluteRoot, INDEX_ARTIFACTS.archetypes),
1806
+ ]))
1807
+ .then(async ([files, imports, testsMap, hotspots, archetypes]) => {
1808
+ const safeFiles = ensureArtifact(files);
1809
+ const safeImports = ensureArtifact(imports);
1810
+ const safeTestsMap = ensureArtifact(testsMap);
1811
+ const safeHotspots = ensureArtifact(hotspots);
1812
+ const safeArchetypes = ensureArtifact(archetypes);
1813
+ const indexedFileSet = new Set((safeFiles.items ?? []).map((item) => item.filePath));
1814
+ const importAliasContext = importsNeedAliasContext(safeImports.items ?? [])
1815
+ ? await loadImportAliasContext({ rootDir: absoluteRoot })
1816
+ : null;
1817
+ const { resolvedImportsBySource, importersByTarget } = buildResolvedImportGraphs(
1818
+ absoluteRoot,
1819
+ safeImports.items ?? [],
1820
+ indexedFileSet,
1821
+ importAliasContext,
1822
+ );
1823
+
1824
+ return {
1825
+ hotspotScores: new Map((safeHotspots.items ?? []).map((item) => [item.filePath, item.count])),
1826
+ testsMapBySource: new Map((safeTestsMap.items ?? []).map((item) => [item.sourceFile, item.tests ?? []])),
1827
+ resolvedImportsBySource,
1828
+ importersByTarget,
1829
+ archetypeByFile: new Map((safeArchetypes.items ?? []).map((item) => [item.filePath, item.archetype])),
1830
+ };
1831
+ })
1832
+ .catch((error) => {
1833
+ QUERY_SUPPORT_BUNDLE_CACHE.delete(absoluteRoot);
1834
+ throw error;
1835
+ }),
1836
+ );
1837
+ }
1838
+
1839
+ return QUERY_SUPPORT_BUNDLE_CACHE.get(absoluteRoot);
1840
+ }
1841
+
1842
+ async function readArtifactIfExists(rootDir, artifactName) {
1843
+ try {
1844
+ const artifactPath = getArtifactPath(rootDir, artifactName);
1845
+ const content = await fs.readFile(artifactPath, 'utf8');
1846
+ return JSON.parse(content);
1847
+ } catch {
1848
+ return null;
1849
+ }
1850
+ }
1851
+
1852
+ function tokenize(query) {
1853
+ return tokenizeFoldedText(foldTextForSearch(addCamelBoundaries(query)));
1854
+ }
1855
+
1856
+ function scoreDirectFileMatch({ queryDescriptor, filePath, searchDescriptor }) {
1857
+ let score = 0;
1858
+ const reasons = [];
1859
+
1860
+ if (
1861
+ queryDescriptor.compact.length >= 6
1862
+ && searchDescriptor.path.compact.includes(queryDescriptor.compact)
1863
+ ) {
1864
+ score += 6;
1865
+ reasons.push(`path_compact:${queryDescriptor.compact}`);
1866
+ }
1867
+
1868
+ for (const token of queryDescriptor.tokens) {
1869
+ if (searchDescriptor.path.tokens.has(token)) {
1870
+ score += 3;
1871
+ reasons.push(`path_token:${token}`);
1872
+ } else if (searchDescriptor.path.rawLower.includes(token)) {
1873
+ score += 1;
1874
+ reasons.push(`path_contains:${token}`);
1875
+ }
1876
+ }
1877
+
1878
+ for (const symbolDescriptor of searchDescriptor.symbols) {
1879
+ if (
1880
+ queryDescriptor.compact.length >= 4
1881
+ && symbolDescriptor.descriptor.compact === queryDescriptor.compact
1882
+ ) {
1883
+ score += 8;
1884
+ reasons.push(`symbol_exact:${symbolDescriptor.name}`);
1885
+ continue;
1886
+ }
1887
+
1888
+ if (
1889
+ queryDescriptor.compact.length >= 6
1890
+ && symbolDescriptor.descriptor.compact.includes(queryDescriptor.compact)
1891
+ ) {
1892
+ score += 5;
1893
+ reasons.push(`symbol_compact:${symbolDescriptor.name}`);
1894
+ }
1895
+
1896
+ for (const token of queryDescriptor.tokens) {
1897
+ if (symbolDescriptor.descriptor.tokens.has(token)) {
1898
+ score += 3;
1899
+ reasons.push(`symbol_token:${symbolDescriptor.name}:${token}`);
1900
+ } else if (symbolDescriptor.descriptor.rawLower.includes(token)) {
1901
+ score += 1;
1902
+ reasons.push(`symbol_contains:${symbolDescriptor.name}`);
1903
+ }
1904
+ }
1905
+ }
1906
+
1907
+ return { score, reasons: [...new Set(reasons)], filePath };
1908
+ }
1909
+
1910
+ function groupBy(items, keyFn) {
1911
+ const map = new Map();
1912
+ for (const item of items) {
1913
+ const key = keyFn(item);
1914
+ if (!map.has(key)) map.set(key, []);
1915
+ map.get(key).push(item);
1916
+ }
1917
+ return map;
1918
+ }
1919
+
1920
+ function haveStableTrackedFileIdentities(previousRecords, nextRecords) {
1921
+ if (previousRecords.length !== nextRecords.length) {
1922
+ return false;
1923
+ }
1924
+
1925
+ return previousRecords.every((record, index) => {
1926
+ const nextRecord = nextRecords[index];
1927
+ return nextRecord
1928
+ && record?.filePath === nextRecord.filePath
1929
+ && record?.ext === nextRecord.ext;
1930
+ });
1931
+ }
1932
+
1933
+ function areStringArraysEqual(previousValues, nextValues) {
1934
+ if (!Array.isArray(previousValues) || !Array.isArray(nextValues)) {
1935
+ return false;
1936
+ }
1937
+
1938
+ if (previousValues.length !== nextValues.length) {
1939
+ return false;
1940
+ }
1941
+
1942
+ return previousValues.every((value, index) => value === nextValues[index]);
1943
+ }
1944
+
1945
+ function arePathSnapshotsEqual(previousSnapshot, nextSnapshot) {
1946
+ if (!Array.isArray(previousSnapshot) || !Array.isArray(nextSnapshot)) {
1947
+ return false;
1948
+ }
1949
+
1950
+ if (previousSnapshot.length !== nextSnapshot.length) {
1951
+ return false;
1952
+ }
1953
+
1954
+ return previousSnapshot.every((entry, index) => {
1955
+ const nextEntry = nextSnapshot[index];
1956
+ return nextEntry
1957
+ && entry?.filePath === nextEntry.filePath
1958
+ && entry?.exists === nextEntry.exists
1959
+ && entry?.size === nextEntry.size
1960
+ && entry?.mtimeMs === nextEntry.mtimeMs;
1961
+ });
1962
+ }
1963
+
1964
+ function areFileRecordSnapshotsEqual(previousRecords, nextRecords) {
1965
+ if (previousRecords.length !== nextRecords.length) {
1966
+ return false;
1967
+ }
1968
+
1969
+ return previousRecords.every((record, index) => {
1970
+ const nextRecord = nextRecords[index];
1971
+ return nextRecord
1972
+ && record?.filePath === nextRecord.filePath
1973
+ && record?.domain === nextRecord.domain
1974
+ && record?.ext === nextRecord.ext
1975
+ && Number(record?.mtimeMs ?? -1) === Number(nextRecord.mtimeMs ?? -1)
1976
+ && Number(record?.size ?? -1) === Number(nextRecord.size ?? -1);
1977
+ });
1978
+ }
1979
+
1980
+ function buildFileSearchDescriptors(files, symbolMap) {
1981
+ const descriptors = new Map();
1982
+
1983
+ for (const file of files) {
1984
+ const filePath = file.filePath;
1985
+ descriptors.set(
1986
+ filePath,
1987
+ createFileSearchDescriptor({
1988
+ filePath,
1989
+ symbols: symbolMap.get(filePath) ?? [],
1990
+ }),
1991
+ );
1992
+ }
1993
+
1994
+ return descriptors;
1995
+ }
1996
+
1997
+ function createFileSearchDescriptor({ filePath, symbols }) {
1998
+ return {
1999
+ path: buildTextSearchDescriptor(filePath),
2000
+ symbols: symbols.map((symbol) => ({
2001
+ name: symbol.name,
2002
+ descriptor: buildTextSearchDescriptor(symbol.name),
2003
+ })),
2004
+ };
2005
+ }
2006
+
2007
+ function buildTextSearchDescriptor(value) {
2008
+ return buildSearchDescriptor(value);
2009
+ }
2010
+
2011
+ export function buildRouteSignalText(...values) {
2012
+ const baseText = values
2013
+ .map((value) => String(value || '').trim())
2014
+ .filter(Boolean)
2015
+ .join('\n');
2016
+
2017
+ if (!baseText) {
2018
+ return '';
2019
+ }
2020
+
2021
+ const withCamelBoundaries = addCamelBoundaries(baseText);
2022
+ const foldedLower = foldTextForSearch(withCamelBoundaries);
2023
+ const baseTokens = tokenizeFoldedText(foldedLower);
2024
+ const aliasTokens = collectVietnameseAliasTokens(foldedLower, baseTokens);
2025
+
2026
+ return [
2027
+ String(baseText).toLowerCase(),
2028
+ foldedLower,
2029
+ [...new Set([...baseTokens, ...aliasTokens])].join(' '),
2030
+ ].filter(Boolean).join('\n');
2031
+ }
2032
+
2033
+ export function buildSearchDescriptor(value, { expandVietnameseAliases = false } = {}) {
2034
+ const withCamelBoundaries = addCamelBoundaries(value);
2035
+ const foldedLower = foldTextForSearch(withCamelBoundaries);
2036
+ const tokens = new Set(tokenizeFoldedText(foldedLower));
2037
+
2038
+ if (expandVietnameseAliases) {
2039
+ for (const token of collectVietnameseAliasTokens(foldedLower, tokens)) {
2040
+ tokens.add(token);
2041
+ }
2042
+ }
2043
+
2044
+ return {
2045
+ rawLower: foldedLower,
2046
+ tokens,
2047
+ compact: [...tokens].join(''),
2048
+ };
2049
+ }
2050
+
2051
+ function addCamelBoundaries(value) {
2052
+ return String(value || '')
2053
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
2054
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
2055
+ }
2056
+
2057
+ function tokenizeFoldedText(value) {
2058
+ return String(value || '')
2059
+ .split(/[^\p{L}\p{N}]+/u)
2060
+ .map((token) => token.trim())
2061
+ .filter((token) => token.length >= 2 && !GENERIC_STOPWORDS.has(token));
2062
+ }
2063
+
2064
+ function collectVietnameseAliasTokens(foldedLower, baseTokens) {
2065
+ const aliases = [];
2066
+
2067
+ for (const entry of VIETNAMESE_PHRASE_ALIASES) {
2068
+ entry.regex.lastIndex = 0;
2069
+ if (!entry.regex.test(foldedLower)) {
2070
+ continue;
2071
+ }
2072
+ aliases.push(...entry.expansions);
2073
+ }
2074
+
2075
+ for (const token of baseTokens) {
2076
+ aliases.push(...(VIETNAMESE_TOKEN_ALIASES.get(token) ?? []));
2077
+ }
2078
+
2079
+ return [...new Set(
2080
+ aliases
2081
+ .map((token) => foldTextForSearch(token))
2082
+ .filter((token) => token.length >= 2 && !GENERIC_STOPWORDS.has(token)),
2083
+ )];
2084
+ }
2085
+
2086
+ function foldTextForSearch(value) {
2087
+ return String(value || '')
2088
+ .replaceAll('đ', 'd')
2089
+ .replaceAll('Đ', 'D')
2090
+ .normalize('NFD')
2091
+ .replace(/\p{M}+/gu, '')
2092
+ .toLowerCase();
2093
+ }
2094
+
2095
+ function inferPreferredArchetypes(tokens) {
2096
+ const preferred = new Set();
2097
+
2098
+ const addIfAny = (signals, archetypes) => {
2099
+ if (!signals.some((signal) => tokens.has(signal))) return;
2100
+ for (const archetype of archetypes) preferred.add(archetype);
2101
+ };
2102
+
2103
+ addIfAny(['page', 'screen', 'view', 'route'], ['page', 'detail-page', 'form-page', 'list-page']);
2104
+ addIfAny(['list'], ['list-page', 'grid']);
2105
+ addIfAny(['detail'], ['detail-page', 'page']);
2106
+ addIfAny(['form', 'edit', 'create', 'add'], ['form-page']);
2107
+ addIfAny(['component', 'widget', 'card', 'button'], ['component']);
2108
+ addIfAny(['modal', 'dialog', 'popup'], ['modal']);
2109
+ addIfAny(['grid', 'table'], ['grid']);
2110
+ addIfAny(['hook', 'composable', 'use'], ['hook', 'composable']);
2111
+ addIfAny(['service', 'api', 'client', 'request', 'fetch'], ['api']);
2112
+ addIfAny(['store', 'state'], ['store']);
2113
+ addIfAny(['util', 'helper', 'format'], ['util']);
2114
+ addIfAny(['test', 'spec'], ['test']);
2115
+
2116
+ return preferred;
2117
+ }
2118
+
2119
+ function shouldIncludeLikelyTestFiles(queryDescriptor) {
2120
+ return queryDescriptor.tokens.has('test')
2121
+ || queryDescriptor.tokens.has('tests')
2122
+ || queryDescriptor.tokens.has('spec')
2123
+ || queryDescriptor.tokens.has('specs')
2124
+ || queryDescriptor.rawLower.includes('.test')
2125
+ || queryDescriptor.rawLower.includes('.spec');
2126
+ }
2127
+
2128
+ function applyImportGraphBoosts({
2129
+ directScores,
2130
+ resolvedImportsBySource,
2131
+ importersByTarget,
2132
+ testsMapBySource,
2133
+ }) {
2134
+ const scores = cloneScoreMap(directScores);
2135
+
2136
+ for (const [sourceFilePath] of directScores) {
2137
+ for (const targetFilePath of resolvedImportsBySource.get(sourceFilePath) ?? []) {
2138
+ const target = ensureScoreEntry(scores, targetFilePath, testsMapBySource);
2139
+ target.score += 1;
2140
+ target.reasons = [...new Set([...target.reasons, `imported_by_match:${sourceFilePath}`])];
2141
+ }
2142
+ }
2143
+
2144
+ for (const [matchedFilePath] of directScores) {
2145
+ propagateImporterMatches({
2146
+ startFilePath: matchedFilePath,
2147
+ scores,
2148
+ importersByTarget,
2149
+ testsMapBySource,
2150
+ });
2151
+ }
2152
+
2153
+ return scores;
2154
+ }
2155
+
2156
+ function applyHotspotBoosts({ scores, hotspotScores }) {
2157
+ const boostedScores = cloneScoreMap(scores);
2158
+
2159
+ for (const [filePath, entry] of boostedScores) {
2160
+ const hotspot = hotspotScores.get(filePath) ?? 0;
2161
+ if (hotspot <= 0) continue;
2162
+
2163
+ entry.score += Math.min(hotspot * 2, 8);
2164
+ entry.reasons = [...new Set([...entry.reasons, `hotspot:${hotspot}`])];
2165
+ }
2166
+
2167
+ return boostedScores;
2168
+ }
2169
+
2170
+ function cloneScoreMap(scores) {
2171
+ const clone = new Map();
2172
+
2173
+ for (const [filePath, entry] of scores) {
2174
+ clone.set(filePath, {
2175
+ filePath,
2176
+ score: entry.score,
2177
+ reasons: [...(entry.reasons ?? [])],
2178
+ tests: [...(entry.tests ?? [])],
2179
+ });
2180
+ }
2181
+
2182
+ return clone;
2183
+ }
2184
+
2185
+ function ensureScoreEntry(scores, filePath, testsMapBySource) {
2186
+ if (!scores.has(filePath)) {
2187
+ scores.set(filePath, {
2188
+ filePath,
2189
+ score: 0,
2190
+ reasons: [],
2191
+ tests: testsMapBySource.get(filePath) ?? [],
2192
+ });
2193
+ }
2194
+
2195
+ return scores.get(filePath);
2196
+ }
2197
+
2198
+ function propagateImporterMatches({
2199
+ startFilePath,
2200
+ scores,
2201
+ importersByTarget,
2202
+ testsMapBySource,
2203
+ }) {
2204
+ const visited = new Set([startFilePath]);
2205
+ const queue = [{ filePath: startFilePath, depth: 0 }];
2206
+
2207
+ while (queue.length > 0) {
2208
+ const current = queue.shift();
2209
+ if (current.depth >= MAX_IMPORTER_HOPS) continue;
2210
+
2211
+ for (const importerFilePath of importersByTarget.get(current.filePath) ?? []) {
2212
+ if (visited.has(importerFilePath)) continue;
2213
+ visited.add(importerFilePath);
2214
+
2215
+ if (isLikelyTestFilePath(importerFilePath)) continue;
2216
+
2217
+ const importer = ensureScoreEntry(scores, importerFilePath, testsMapBySource);
2218
+ const scoreDelta = current.depth === 0 ? 2 : 1;
2219
+ const reason = current.depth === 0
2220
+ ? `imports_match:${startFilePath}`
2221
+ : `imports_match_via:${current.filePath}->${startFilePath}`;
2222
+
2223
+ importer.score += scoreDelta;
2224
+ importer.reasons = [...new Set([...importer.reasons, reason])];
2225
+ queue.push({ filePath: importerFilePath, depth: current.depth + 1 });
2226
+ }
2227
+ }
2228
+ }
2229
+
2230
+ function buildResolvedImportGraphs(rootDir, imports, indexedFileSet, importAliasContext) {
2231
+ const resolvedImportsBySource = new Map();
2232
+ const importersByTarget = new Map();
2233
+
2234
+ for (const edge of imports) {
2235
+ if (!edge?.from || !edge?.to) continue;
2236
+
2237
+ const target = resolveImportSpecifier({
2238
+ rootDir,
2239
+ fromFilePath: edge.from,
2240
+ specifier: edge.to,
2241
+ indexedFileSet,
2242
+ aliasContext: importAliasContext,
2243
+ });
2244
+ if (!target) continue;
2245
+
2246
+ if (!resolvedImportsBySource.has(edge.from)) {
2247
+ resolvedImportsBySource.set(edge.from, new Set());
2248
+ }
2249
+ resolvedImportsBySource.get(edge.from).add(target);
2250
+
2251
+ if (!importersByTarget.has(target)) {
2252
+ importersByTarget.set(target, new Set());
2253
+ }
2254
+ importersByTarget.get(target).add(edge.from);
2255
+ }
2256
+
2257
+ return { resolvedImportsBySource, importersByTarget };
2258
+ }
2259
+
2260
+ function isLikelyTestFilePath(filePath) {
2261
+ const lower = filePath.toLowerCase();
2262
+ return (
2263
+ lower.startsWith('__tests__/')
2264
+ || lower.startsWith('test/')
2265
+ || lower.startsWith('tests/')
2266
+ || lower.startsWith('spec/')
2267
+ || lower.startsWith('specs/')
2268
+ || lower.includes('/__tests__/')
2269
+ || lower.includes('/test/')
2270
+ || lower.includes('/spec/')
2271
+ || lower.includes('/specs/')
2272
+ || lower.endsWith('.test.js')
2273
+ || lower.endsWith('.test.ts')
2274
+ || lower.endsWith('.test.vue')
2275
+ || lower.endsWith('.test.tsx')
2276
+ || lower.endsWith('.test.jsx')
2277
+ || lower.endsWith('.test.mjs')
2278
+ || lower.endsWith('.test.cjs')
2279
+ || lower.endsWith('.spec.js')
2280
+ || lower.endsWith('.spec.ts')
2281
+ || lower.endsWith('.spec.vue')
2282
+ || lower.endsWith('.spec.tsx')
2283
+ || lower.endsWith('.spec.jsx')
2284
+ || lower.endsWith('.spec.mjs')
2285
+ || lower.endsWith('.spec.cjs')
2286
+ );
2287
+ }
2288
+
2289
+ async function loadImportAliasContext({ rootDir }) {
2290
+ const loaded = await loadImportAliasContextState({ rootDir });
2291
+ return loaded.context;
2292
+ }
2293
+
2294
+ async function loadImportAliasContextState({ rootDir }) {
2295
+ const absoluteRoot = path.resolve(rootDir);
2296
+ const cached = ALIAS_CONTEXT_CACHE.get(absoluteRoot);
2297
+ if (cached) {
2298
+ const cachedEntry = await cached;
2299
+ if (await isAliasSnapshotValid(cachedEntry.snapshot)) {
2300
+ return cachedEntry;
2301
+ }
2302
+ ALIAS_CONTEXT_CACHE.delete(absoluteRoot);
2303
+ }
2304
+
2305
+ const contextPromise = loadFreshImportAliasContext(absoluteRoot)
2306
+ .catch((error) => {
2307
+ ALIAS_CONTEXT_CACHE.delete(absoluteRoot);
2308
+ throw error;
2309
+ });
2310
+ ALIAS_CONTEXT_CACHE.set(absoluteRoot, contextPromise);
2311
+
2312
+ return contextPromise;
2313
+ }
2314
+
2315
+ function importsNeedAliasContext(imports = []) {
2316
+ return imports.some((entry) => specifierNeedsAliasContext(entry?.to));
2317
+ }
2318
+
2319
+ async function loadFreshImportAliasContext(rootDir) {
2320
+ const tracker = new Map();
2321
+ const tsconfigPath = path.join(rootDir, 'tsconfig.json');
2322
+ const jsconfigPath = path.join(rootDir, 'jsconfig.json');
2323
+ const config = await loadAliasConfig(tsconfigPath, new Set(), tracker)
2324
+ ?? await loadAliasConfig(jsconfigPath, new Set(), tracker);
2325
+
2326
+ for (const configName of ROOT_ALIAS_CONFIG_FILES) {
2327
+ const configPath = path.resolve(rootDir, configName);
2328
+ if (!tracker.has(configPath)) {
2329
+ tracker.set(configPath, await readPathSnapshot(configPath));
2330
+ }
2331
+ }
2332
+
2333
+ const configuredRules = config?.pathRules ?? [];
2334
+ const configuredBaseUrls = config?.baseUrlDirs ?? [];
2335
+
2336
+ return {
2337
+ context: {
2338
+ rootDir,
2339
+ pathRules: [
2340
+ ...configuredRules,
2341
+ ...DEFAULT_ALIAS_RULES.map((rule) => buildAliasRule({
2342
+ pattern: rule.pattern,
2343
+ targets: rule.targets,
2344
+ baseDir: rootDir,
2345
+ })),
2346
+ ],
2347
+ baseUrlDirs: [...new Set(configuredBaseUrls)],
2348
+ },
2349
+ snapshot: [...tracker.values()].sort((a, b) => a.filePath.localeCompare(b.filePath)),
2350
+ };
2351
+ }
2352
+
2353
+ function resolveImportSpecifier({
2354
+ rootDir,
2355
+ fromFilePath,
2356
+ specifier,
2357
+ indexedFileSet,
2358
+ aliasContext,
2359
+ }) {
2360
+ if (!specifier?.trim()) return null;
2361
+
2362
+ if (specifier.startsWith('.')) {
2363
+ const fromDir = path.posix.dirname(fromFilePath);
2364
+ const base = path.posix.normalize(path.posix.join(fromDir, specifier));
2365
+ return resolveCandidateBase(base, indexedFileSet);
2366
+ }
2367
+
2368
+ if (specifier.startsWith('/')) {
2369
+ return resolveCandidateBase(specifier.slice(1), indexedFileSet);
2370
+ }
2371
+
2372
+ for (const rule of aliasContext?.pathRules ?? []) {
2373
+ const wildcardValue = matchAliasPattern(specifier, rule.pattern);
2374
+ if (wildcardValue === null) continue;
2375
+
2376
+ for (const targetPattern of rule.targets) {
2377
+ const replacedTarget = targetPattern.replaceAll('*', wildcardValue);
2378
+ const absoluteBase = path.resolve(rule.baseDir, replacedTarget);
2379
+ const relativeBase = normalizeRelative(rootDir, absoluteBase);
2380
+ const resolved = resolveCandidateBase(relativeBase, indexedFileSet);
2381
+ if (resolved) return resolved;
2382
+ }
2383
+ }
2384
+
2385
+ for (const baseUrlDir of aliasContext?.baseUrlDirs ?? []) {
2386
+ const absoluteBase = path.resolve(baseUrlDir, specifier);
2387
+ const relativeBase = normalizeRelative(rootDir, absoluteBase);
2388
+ const resolved = resolveCandidateBase(relativeBase, indexedFileSet);
2389
+ if (resolved) return resolved;
2390
+ }
2391
+
2392
+ return null;
2393
+ }
2394
+
2395
+ function resolveCandidateBase(relativeBase, indexedFileSet) {
2396
+ const normalizedBase = relativeBase.replace(/\\/g, '/');
2397
+
2398
+ for (const suffix of RESOLUTION_SUFFIXES) {
2399
+ const candidate = path.posix.normalize(`${normalizedBase}${suffix}`);
2400
+ if (indexedFileSet.has(candidate)) return candidate;
2401
+ }
2402
+
2403
+ return null;
2404
+ }
2405
+
2406
+ function specifierNeedsAliasContext(specifier) {
2407
+ const normalized = String(specifier ?? '').trim();
2408
+ if (!normalized) return false;
2409
+ return !normalized.startsWith('.') && !normalized.startsWith('/');
2410
+ }
2411
+
2412
+ function matchAliasPattern(specifier, pattern) {
2413
+ if (!pattern.includes('*')) return specifier === pattern ? '' : null;
2414
+
2415
+ const [prefix, suffix] = pattern.split('*');
2416
+ if (!specifier.startsWith(prefix)) return null;
2417
+ if (suffix && !specifier.endsWith(suffix)) return null;
2418
+
2419
+ return specifier.slice(prefix.length, specifier.length - suffix.length);
2420
+ }
2421
+
2422
+ function buildAliasRule({ pattern, targets, baseDir }) {
2423
+ return {
2424
+ pattern,
2425
+ targets: targets.map((target) => target.replace(/\\/g, '/')),
2426
+ baseDir,
2427
+ };
2428
+ }
2429
+
2430
+ async function loadAliasConfig(configPath, visited = new Set(), tracker = null) {
2431
+ const resolvedPath = path.resolve(configPath);
2432
+ if (visited.has(resolvedPath)) return null;
2433
+ visited.add(resolvedPath);
2434
+
2435
+ const snapshot = await readPathSnapshot(resolvedPath);
2436
+ if (tracker) {
2437
+ tracker.set(resolvedPath, snapshot);
2438
+ }
2439
+ if (!snapshot.exists) {
2440
+ return null;
2441
+ }
2442
+
2443
+ let raw;
2444
+ try {
2445
+ raw = await fs.readFile(resolvedPath, 'utf8');
2446
+ } catch {
2447
+ return null;
2448
+ }
2449
+
2450
+ let parsed;
2451
+ try {
2452
+ parsed = parseJsonc(raw);
2453
+ } catch {
2454
+ return null;
2455
+ }
2456
+
2457
+ const configDir = path.dirname(resolvedPath);
2458
+ const compilerOptions = parsed?.compilerOptions ?? {};
2459
+
2460
+ let inherited = null;
2461
+ const extendsValue = typeof parsed?.extends === 'string' ? parsed.extends.trim() : '';
2462
+ if (extendsValue && (extendsValue.startsWith('.') || extendsValue.startsWith('/'))) {
2463
+ const inheritedPath = resolveExtendedConfigPath(configDir, extendsValue);
2464
+ inherited = await loadAliasConfig(inheritedPath, visited, tracker);
2465
+ }
2466
+
2467
+ const inheritedRules = inherited?.pathRules ?? [];
2468
+ const inheritedBaseUrls = inherited?.baseUrlDirs ?? [];
2469
+ const currentRules = buildPathRules({ rootDir: configDir, compilerOptions });
2470
+ const currentBaseUrls = buildBaseUrlDirs({ rootDir: configDir, compilerOptions });
2471
+
2472
+ const pathRuleMap = new Map(inheritedRules.map((rule) => [rule.pattern, rule]));
2473
+ for (const rule of currentRules) {
2474
+ pathRuleMap.set(rule.pattern, rule);
2475
+ }
2476
+
2477
+ return {
2478
+ pathRules: [...pathRuleMap.values()],
2479
+ baseUrlDirs: [...new Set([...currentBaseUrls, ...inheritedBaseUrls])],
2480
+ };
2481
+ }
2482
+
2483
+ function buildPathRules({ rootDir, compilerOptions }) {
2484
+ const paths = compilerOptions?.paths;
2485
+ if (!paths || typeof paths !== 'object') return [];
2486
+
2487
+ const baseDir = path.resolve(rootDir, String(compilerOptions?.baseUrl ?? '.'));
2488
+ return Object.entries(paths)
2489
+ .filter(([pattern, targets]) => typeof pattern === 'string' && Array.isArray(targets) && targets.length > 0)
2490
+ .map(([pattern, targets]) => buildAliasRule({
2491
+ pattern,
2492
+ targets: targets.filter((target) => typeof target === 'string'),
2493
+ baseDir,
2494
+ }));
2495
+ }
2496
+
2497
+ function buildBaseUrlDirs({ rootDir, compilerOptions }) {
2498
+ const baseUrl = String(compilerOptions?.baseUrl ?? '').trim();
2499
+ if (!baseUrl) return [];
2500
+ return [path.resolve(rootDir, baseUrl)];
2501
+ }
2502
+
2503
+ function resolveExtendedConfigPath(configDir, extendsValue) {
2504
+ const withExtension = extendsValue.endsWith('.json')
2505
+ ? extendsValue
2506
+ : `${extendsValue}.json`;
2507
+ return path.resolve(configDir, withExtension);
2508
+ }
2509
+
2510
+ async function isAliasSnapshotValid(snapshot = []) {
2511
+ const current = await Promise.all(snapshot.map((entry) => readPathSnapshot(entry.filePath)));
2512
+ return current.every((entry, index) => isSameSnapshotEntry(entry, snapshot[index]));
2513
+ }
2514
+
2515
+ async function readPathSnapshot(filePath) {
2516
+ try {
2517
+ const stat = await fs.stat(filePath);
2518
+ return {
2519
+ filePath,
2520
+ exists: true,
2521
+ size: stat.size,
2522
+ mtimeMs: stat.mtimeMs,
2523
+ };
2524
+ } catch {
2525
+ return {
2526
+ filePath,
2527
+ exists: false,
2528
+ size: null,
2529
+ mtimeMs: null,
2530
+ };
2531
+ }
2532
+ }
2533
+
2534
+ function isSameSnapshotEntry(current, previous) {
2535
+ return current.filePath === previous.filePath
2536
+ && current.exists === previous.exists
2537
+ && current.size === previous.size
2538
+ && current.mtimeMs === previous.mtimeMs;
2539
+ }
2540
+
2541
+ function parseJsonc(raw) {
2542
+ const withoutBom = raw.replace(/^\uFEFF/, '');
2543
+ const withoutBlockComments = withoutBom.replace(/\/\*[\s\S]*?\*\//g, '');
2544
+ const withoutLineComments = stripLineComments(withoutBlockComments);
2545
+ const withoutTrailingCommas = withoutLineComments.replace(/,\s*([}\]])/g, '$1');
2546
+ return JSON.parse(withoutTrailingCommas);
2547
+ }
2548
+
2549
+ function stripLineComments(raw) {
2550
+ let result = '';
2551
+ let inString = false;
2552
+ let stringQuote = '';
2553
+ let isEscaped = false;
2554
+
2555
+ for (let index = 0; index < raw.length; index += 1) {
2556
+ const char = raw[index];
2557
+ const nextChar = raw[index + 1];
2558
+
2559
+ if (inString) {
2560
+ result += char;
2561
+ if (isEscaped) {
2562
+ isEscaped = false;
2563
+ } else if (char === '\\') {
2564
+ isEscaped = true;
2565
+ } else if (char === stringQuote) {
2566
+ inString = false;
2567
+ stringQuote = '';
2568
+ }
2569
+ continue;
2570
+ }
2571
+
2572
+ if ((char === '"' || char === "'")) {
2573
+ inString = true;
2574
+ stringQuote = char;
2575
+ result += char;
2576
+ continue;
2577
+ }
2578
+
2579
+ if (char === '/' && nextChar === '/') {
2580
+ while (index < raw.length && raw[index] !== '\n') {
2581
+ index += 1;
2582
+ }
2583
+ if (index < raw.length) result += '\n';
2584
+ continue;
2585
+ }
2586
+
2587
+ result += char;
2588
+ }
2589
+
2590
+ return result;
2591
+ }
2592
+
2593
+ function scoreTestMatch(sourceFilePath, testFilePath) {
2594
+ const sourcePathLower = sourceFilePath.toLowerCase();
2595
+ const testPathLower = testFilePath.toLowerCase();
2596
+ const sourceBase = path.basename(sourcePathLower, path.extname(sourcePathLower));
2597
+ const sourceParentDir = path.posix.basename(path.posix.dirname(sourcePathLower));
2598
+ const sourceStem = sourceBase === 'index' ? sourceParentDir : sourceBase;
2599
+ const testBase = path.basename(testPathLower, path.extname(testPathLower));
2600
+ const testStem = normalizeTestStem(testBase);
2601
+
2602
+ let score = 0;
2603
+
2604
+ if (testStem === sourceStem) score += 100;
2605
+ if (testPathLower.includes(`/${sourceStem}.`) || testPathLower.includes(`/${sourceStem}/`)) score += 50;
2606
+ if (sourceBase === 'index' && testStem === 'index' && testPathLower.includes(`/${sourceParentDir}/`)) score += 90;
2607
+ if (sourceBase === 'index' && testStem === sourceParentDir) score += 80;
2608
+ if (
2609
+ sourceStem.length >= 4
2610
+ && testStem !== sourceStem
2611
+ && (testStem.includes(sourceStem) || sourceStem.includes(testStem))
2612
+ ) {
2613
+ score += 20;
2614
+ }
2615
+
2616
+ return score;
2617
+ }
2618
+
2619
+ function normalizeTestStem(testBase) {
2620
+ return testBase
2621
+ .replace(/(?:\.test|\.spec)+$/g, '')
2622
+ .replace(/[-_.](test|spec)$/g, '')
2623
+ .toLowerCase();
2624
+ }
2625
+
2626
+ async function resolveScanRoots(rootDir) {
2627
+ const concreteRoots = [];
2628
+ for (const relativeDir of SOURCE_DIRS) {
2629
+ const dirPath = path.join(rootDir, relativeDir);
2630
+ try {
2631
+ const stat = await fs.stat(dirPath);
2632
+ if (stat.isDirectory()) concreteRoots.push(dirPath);
2633
+ } catch {
2634
+ // ignore
2635
+ }
2636
+ }
2637
+ return concreteRoots.length > 0 ? concreteRoots : [rootDir];
2638
+ }
2639
+
2640
+ function shouldSkipDirectory(name) {
2641
+ if (EXCLUDED_DIR_NAMES.has(name)) return true;
2642
+ return name.startsWith('.') && name !== '.claude' && name !== '.codex' && name !== '.antigravity';
2643
+ }
2644
+
2645
+ function isLikelyTestFile(filePath) {
2646
+ const lower = filePath.toLowerCase();
2647
+ return (
2648
+ lower.startsWith('__tests__/')
2649
+ || lower.startsWith('test/')
2650
+ || lower.startsWith('tests/')
2651
+ || lower.startsWith('spec/')
2652
+ || lower.startsWith('specs/')
2653
+ || lower.includes('/__tests__/')
2654
+ || lower.includes('/test/')
2655
+ || lower.includes('/spec/')
2656
+ || lower.includes('/specs/')
2657
+ || lower.endsWith('.test.js')
2658
+ || lower.endsWith('.test.ts')
2659
+ || lower.endsWith('.test.vue')
2660
+ || lower.endsWith('.test.tsx')
2661
+ || lower.endsWith('.test.jsx')
2662
+ || lower.endsWith('.test.mjs')
2663
+ || lower.endsWith('.test.cjs')
2664
+ || lower.endsWith('.spec.js')
2665
+ || lower.endsWith('.spec.ts')
2666
+ || lower.endsWith('.spec.vue')
2667
+ || lower.endsWith('.spec.tsx')
2668
+ || lower.endsWith('.spec.jsx')
2669
+ || lower.endsWith('.spec.mjs')
2670
+ || lower.endsWith('.spec.cjs')
2671
+ );
2672
+ }
2673
+
2674
+ function createSourceFingerprint(rootDir, discoveredFiles) {
2675
+ const hash = crypto.createHash('sha256');
2676
+ const normalizedEntries = discoveredFiles
2677
+ .map((entry) => {
2678
+ const filePath = normalizeRelative(rootDir, entry.absolutePath);
2679
+ if (!filePath || filePath === '.' || filePath.startsWith('../') || path.isAbsolute(filePath)) return null;
2680
+ return {
2681
+ filePath,
2682
+ mtimeMs: Number(entry.mtimeMs ?? -1),
2683
+ size: Number(entry.size ?? -1),
2684
+ };
2685
+ })
2686
+ .filter(Boolean)
2687
+ .sort((a, b) => a.filePath.localeCompare(b.filePath));
2688
+
2689
+ let newestMtimeMs = 0;
2690
+ for (const entry of normalizedEntries) {
2691
+ newestMtimeMs = Math.max(newestMtimeMs, entry.mtimeMs);
2692
+ hash.update(entry.filePath);
2693
+ hash.update('\0');
2694
+ hash.update(String(entry.mtimeMs));
2695
+ hash.update('\0');
2696
+ hash.update(String(entry.size));
2697
+ hash.update('\n');
2698
+ }
2699
+
2700
+ return {
2701
+ fileCount: normalizedEntries.length,
2702
+ newestMtimeMs,
2703
+ digest: hash.digest('base64url'),
2704
+ };
2705
+ }
2706
+
2707
+ function areSourceFingerprintsEqual(previousFingerprint, nextFingerprint) {
2708
+ return previousFingerprint?.fileCount === nextFingerprint?.fileCount
2709
+ && previousFingerprint?.newestMtimeMs === nextFingerprint?.newestMtimeMs
2710
+ && previousFingerprint?.digest === nextFingerprint?.digest;
2711
+ }
2712
+
2713
+ async function buildTestCommand(rootDir, testFile) {
2714
+ const packageManager = await detectPackageManager(rootDir);
2715
+ if (packageManager === 'pnpm') return `pnpm test ${testFile}`;
2716
+ if (packageManager === 'yarn') return `yarn test ${testFile}`;
2717
+ if (packageManager === 'bun') return `bun test ${testFile}`;
2718
+ return `npm test -- ${testFile}`;
2719
+ }
2720
+
2721
+ async function detectPackageManager(rootDir) {
2722
+ const packageJsonPath = path.join(rootDir, 'package.json');
2723
+ try {
2724
+ const raw = await fs.readFile(packageJsonPath, 'utf8');
2725
+ const pkg = JSON.parse(raw);
2726
+ const declared = String(pkg?.packageManager ?? '').toLowerCase();
2727
+ if (declared.startsWith('pnpm')) return 'pnpm';
2728
+ if (declared.startsWith('yarn')) return 'yarn';
2729
+ if (declared.startsWith('bun')) return 'bun';
2730
+ if (declared.startsWith('npm')) return 'npm';
2731
+ } catch {
2732
+ // fallback below
2733
+ }
2734
+
2735
+ const checks = [
2736
+ ['pnpm-lock.yaml', 'pnpm'],
2737
+ ['yarn.lock', 'yarn'],
2738
+ ['bun.lockb', 'bun'],
2739
+ ['package-lock.json', 'npm'],
2740
+ ];
2741
+
2742
+ for (const [lockfile, pm] of checks) {
2743
+ try {
2744
+ await fs.access(path.join(rootDir, lockfile));
2745
+ return pm;
2746
+ } catch {
2747
+ // continue
2748
+ }
2749
+ }
2750
+
2751
+ return 'npm';
2752
+ }
2753
+
2754
+ function parseArtifactGeneratedAt(artifact) {
2755
+ const generatedAtRaw = String(artifact?.generatedAt ?? '').trim();
2756
+
2757
+ if (!generatedAtRaw) return null;
2758
+
2759
+ const generatedAtMs = Date.parse(generatedAtRaw);
2760
+ if (Number.isNaN(generatedAtMs)) return null;
2761
+
2762
+ return generatedAtMs;
2763
+ }
2764
+
2765
+ export async function getIndexArtifactGeneratedAt({
2766
+ rootDir = process.cwd(),
2767
+ artifactName = INDEX_ARTIFACTS.files,
2768
+ } = {}) {
2769
+ const absoluteRoot = path.resolve(rootDir);
2770
+ if (artifactName === INDEX_ARTIFACTS.files || artifactName === INDEX_ARTIFACTS.meta) {
2771
+ const metaArtifact = await readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.meta);
2772
+ const metaGeneratedAtMs = parseArtifactGeneratedAt(metaArtifact);
2773
+ if (metaGeneratedAtMs !== null) return metaGeneratedAtMs;
2774
+ }
2775
+
2776
+ const artifact = await readArtifactIfExists(absoluteRoot, artifactName);
2777
+ return parseArtifactGeneratedAt(artifact);
2778
+ }
2779
+
2780
+ export async function isIndexStale({
2781
+ rootDir = process.cwd(),
2782
+ maxAgeMs = DEFAULT_INDEX_CACHE_MAX_AGE_MS,
2783
+ now = Date.now(),
2784
+ generatedAtMs = null,
2785
+ } = {}) {
2786
+ const absoluteRoot = path.resolve(rootDir);
2787
+ const metaArtifact = await readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.meta);
2788
+ const effectiveGeneratedAtMs = Number.isFinite(generatedAtMs)
2789
+ ? generatedAtMs
2790
+ : parseArtifactGeneratedAt(metaArtifact);
2791
+ if (effectiveGeneratedAtMs === null) return true;
2792
+ if (typeof maxAgeMs !== 'number' || Number.isNaN(maxAgeMs) || maxAgeMs < 0) return true;
2793
+ if ((now - effectiveGeneratedAtMs) >= maxAgeMs) return true;
2794
+ if (!metaArtifact?.sourceFingerprint) return true;
2795
+ const currentSourceFingerprint = createSourceFingerprint(
2796
+ absoluteRoot,
2797
+ await collectFiles(await resolveScanRoots(absoluteRoot)),
2798
+ );
2799
+ return !areSourceFingerprintsEqual(metaArtifact.sourceFingerprint, currentSourceFingerprint);
2800
+ }