@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.
- package/CHANGELOG.md +179 -0
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/bin/ukit +30 -0
- package/manifests/platform.full.yaml +1194 -0
- package/package.json +71 -0
- package/scripts/bug/triage.mjs +37 -0
- package/scripts/index/build-index.mjs +35 -0
- package/scripts/index/query-index.mjs +92 -0
- package/scripts/index/refresh-index.mjs +85 -0
- package/scripts/release/verify-release.mjs +56 -0
- package/src/bug/triageBug.js +123 -0
- package/src/cli/adapters.js +148 -0
- package/src/cli/commands/diff.js +51 -0
- package/src/cli/commands/doctor.js +125 -0
- package/src/cli/commands/indexArgs.js +73 -0
- package/src/cli/commands/indexTools.js +509 -0
- package/src/cli/commands/install.js +293 -0
- package/src/cli/commands/memory.js +126 -0
- package/src/cli/commands/status.js +8 -0
- package/src/cli/commands/uninstall.js +51 -0
- package/src/cli/index.js +109 -0
- package/src/context/detectProjectContext.js +49 -0
- package/src/context/detectProviders.js +12 -0
- package/src/core/applyPlan.js +89 -0
- package/src/core/buildPlan.js +228 -0
- package/src/core/compact/index.js +294 -0
- package/src/core/compact/threshold.js +936 -0
- package/src/core/diffPlan.js +73 -0
- package/src/core/ensureGitignore.js +117 -0
- package/src/core/fileOps.js +188 -0
- package/src/core/memory/hygiene.js +160 -0
- package/src/core/memory/index.js +2 -0
- package/src/core/memory/retrieval.js +476 -0
- package/src/core/memory/store.js +202 -0
- package/src/core/metadata.js +132 -0
- package/src/core/migrateLegacy.js +139 -0
- package/src/core/output/index.js +1309 -0
- package/src/core/paths.js +13 -0
- package/src/core/report.js +17 -0
- package/src/core/router/advisor.js +42 -0
- package/src/core/router/index.js +2 -0
- package/src/core/router/router.js +164 -0
- package/src/core/runInstallPipeline.js +365 -0
- package/src/core/runtimeConfig.js +190 -0
- package/src/core/runtimePaths.js +24 -0
- package/src/core/status.js +186 -0
- package/src/core/token/index.js +328 -0
- package/src/core/uninstall.js +246 -0
- package/src/core/validation/confidence.js +89 -0
- package/src/core/validation/index.js +2 -0
- package/src/core/validation/validator.js +165 -0
- package/src/index/buildIndex.js +1392 -0
- package/src/index/gitHooks.js +109 -0
- package/src/index/importResolution.js +377 -0
- package/src/index/languageTools.js +127 -0
- package/src/index/paths.js +27 -0
- package/src/index/queryIndex.js +637 -0
- package/src/index/relatedTests.js +237 -0
- package/src/index/resolveContext.js +345 -0
- package/src/index/routeCatalog.js +258 -0
- package/src/index/taskRouting.js +677 -0
- package/src/index/verificationPlan.js +437 -0
- package/src/manifest/loadManifest.js +22 -0
- package/src/manifest/selectItems.js +78 -0
- package/src/manifest/validateManifest.js +115 -0
- package/src/render/buildVariables.js +39 -0
- package/src/render/renderTemplate.js +44 -0
- package/src/stack/detectStack.js +213 -0
- package/templates/.claude/agents/bug-debugger.md +57 -0
- package/templates/.claude/agents/feature-implementer.md +55 -0
- package/templates/.claude/config/providers.md +25 -0
- package/templates/.claude/hooks/auto-allow-bash.sh +155 -0
- package/templates/.claude/hooks/auto-prune-bash.sh +75 -0
- package/templates/.claude/hooks/block-dangerous.sh +54 -0
- package/templates/.claude/hooks/compress-output.sh +17 -0
- package/templates/.claude/hooks/protect-files.sh +37 -0
- package/templates/.claude/hooks/reinject-context.sh +28 -0
- package/templates/.claude/hooks/session-start.md +13 -0
- package/templates/.claude/hooks/skill-router.sh +1681 -0
- package/templates/.claude/hooks/verification-guard.sh +271 -0
- package/templates/.claude/settings.json +144 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/mce/mc.xsd +75 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/templates/.claude/skills/_shared/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/pack.py +159 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/unpack.py +29 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validate.py +69 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validation/__init__.py +15 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validation/base.py +951 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validation/docx.py +274 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validation/pptx.py +315 -0
- package/templates/.claude/skills/_shared/ooxml/scripts/validation/redlining.py +279 -0
- package/templates/.claude/skills/backend-api/SKILL.md +26 -0
- package/templates/.claude/skills/canvas-design/LICENSE.txt +202 -0
- package/templates/.claude/skills/canvas-design/SKILL.md +130 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +93 -0
- package/templates/.claude/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
- package/templates/.claude/skills/code-review/SKILL.md +97 -0
- package/templates/.claude/skills/debugging-toolkit/SKILL.md +156 -0
- package/templates/.claude/skills/delivery/SKILL.md +92 -0
- package/templates/.claude/skills/discover-security/SKILL.md +86 -0
- package/templates/.claude/skills/docker-packaging/SKILL.md +60 -0
- package/templates/.claude/skills/docs-manager/SKILL.md +465 -0
- package/templates/.claude/skills/docs-manager/init-project-docs.sh +70 -0
- package/templates/.claude/skills/docs-manager/templates/README.md.template +50 -0
- package/templates/.claude/skills/docs-manager/templates/agent-roles.md.template +24 -0
- package/templates/.claude/skills/docs-manager/templates/coding-conventions.md.template +28 -0
- package/templates/.claude/skills/docs-manager/templates/memory.md.template +30 -0
- package/templates/.claude/skills/docs-manager/templates/onboarding.md.template +20 -0
- package/templates/.claude/skills/docs-manager/templates/project.md.template +26 -0
- package/templates/.claude/skills/docs-quality/SKILL.md +148 -0
- package/templates/.claude/skills/docx/LICENSE.txt +30 -0
- package/templates/.claude/skills/docx/SKILL.md +197 -0
- package/templates/.claude/skills/docx/docx-js.md +350 -0
- package/templates/.claude/skills/docx/ooxml.md +610 -0
- package/templates/.claude/skills/docx/scripts/__init__.py +1 -0
- package/templates/.claude/skills/docx/scripts/document.py +1276 -0
- package/templates/.claude/skills/docx/scripts/templates/comments.xml +3 -0
- package/templates/.claude/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/templates/.claude/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/templates/.claude/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/templates/.claude/skills/docx/scripts/templates/people.xml +3 -0
- package/templates/.claude/skills/docx/scripts/utilities.py +374 -0
- package/templates/.claude/skills/duraone/SKILL.md +204 -0
- package/templates/.claude/skills/duraone/references/backend.md +636 -0
- package/templates/.claude/skills/duraone/references/frontend.md +1506 -0
- package/templates/.claude/skills/duraone/references/sql.md +631 -0
- package/templates/.claude/skills/duraone/references/workflow.md +520 -0
- package/templates/.claude/skills/executing-plans/SKILL.md +76 -0
- package/templates/.claude/skills/file-organizer/SKILL.md +433 -0
- package/templates/.claude/skills/frontend/SKILL.md +26 -0
- package/templates/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/templates/.claude/skills/frontend-design/SKILL.md +42 -0
- package/templates/.claude/skills/frontend-vue/SKILL.md +127 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Box.vue +137 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Button.vue +93 -0
- package/templates/.claude/skills/frontend-vue/components/Control/ButtonBar.vue +29 -0
- package/templates/.claude/skills/frontend-vue/components/Control/ButtonFloat.vue +62 -0
- package/templates/.claude/skills/frontend-vue/components/Control/CheckButton.vue +75 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Checkbox.vue +58 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Datetime.vue +148 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Dropdownlist.vue +156 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Input.vue +106 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Label.vue +38 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Master/BoxColumn.vue +24 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Popup/Confirm.vue +33 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Popup/Info.vue +32 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Popup/ModalInfo.vue +39 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Popup/Reject.vue +64 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Tag.vue +82 -0
- package/templates/.claude/skills/frontend-vue/components/Control/Upload.vue +61 -0
- package/templates/.claude/skills/frontend-vue/components/ControlMobile/Dropdownlist.vue +103 -0
- package/templates/.claude/skills/frontend-vue/components/ControlMobile/PagingBar.vue +108 -0
- package/templates/.claude/skills/frontend-vue/components/ControlMobile/UploadImage.vue +137 -0
- package/templates/.claude/skills/frontend-vue/components/Grid/AG.vue +806 -0
- package/templates/.claude/skills/frontend-vue/components/Grid/AntTable.vue +253 -0
- package/templates/.claude/skills/frontend-vue/components/Grid/CustomDropdownEditor.vue +43 -0
- package/templates/.claude/skills/frontend-vue/components/Grid/CustomDropdownEditorEnable.vue +55 -0
- package/templates/.claude/skills/frontend-vue/components/Grid/HtmlTable.vue +40 -0
- package/templates/.claude/skills/frontend-vue/components/PDFViewer.vue +25 -0
- package/templates/.claude/skills/frontend-vue/components/Panel/FormView.vue +309 -0
- package/templates/.claude/skills/frontend-vue/components/Partial/Footer.vue +23 -0
- package/templates/.claude/skills/frontend-vue/components/Partial/Header.vue +265 -0
- package/templates/.claude/skills/frontend-vue/components/Partial/Sidebar.vue +122 -0
- package/templates/.claude/skills/frontend-vue/components/Template.vue +16 -0
- package/templates/.claude/skills/frontend-vue/components/View/Form.vue +89 -0
- package/templates/.claude/skills/frontend-vue/composables/indexDBStore.js +140 -0
- package/templates/.claude/skills/frontend-vue/composables/masterApi.js +362 -0
- package/templates/.claude/skills/frontend-vue/composables/state.js +578 -0
- package/templates/.claude/skills/frontend-vue/composables/useRequest.js +221 -0
- package/templates/.claude/skills/frontend-vue/composables/useSession.js +179 -0
- package/templates/.claude/skills/frontend-vue/composables/useTranslation.js +54 -0
- package/templates/.claude/skills/frontend-vue/composables/useWebSocket.js +257 -0
- package/templates/.claude/skills/frontend-vue/composables/userObj.js +111 -0
- package/templates/.claude/skills/frontend-vue/composables/utils.js +322 -0
- package/templates/.claude/skills/frontend-vue/reference/composables-example.vue +320 -0
- package/templates/.claude/skills/frontend-vue/reference/form-example.vue +183 -0
- package/templates/.claude/skills/frontend-vue/reference/grid-example.vue +147 -0
- package/templates/.claude/skills/frontend-vue/reference/masterdata-example/[id].vue +106 -0
- package/templates/.claude/skills/frontend-vue/reference/masterdata-example/index.vue +58 -0
- package/templates/.claude/skills/frontend-vue/reference/popup-example.vue +159 -0
- package/templates/.claude/skills/pdf/LICENSE.txt +30 -0
- package/templates/.claude/skills/pdf/SKILL.md +294 -0
- package/templates/.claude/skills/pdf/forms.md +205 -0
- package/templates/.claude/skills/pdf/reference.md +612 -0
- package/templates/.claude/skills/pdf/scripts/check_bounding_boxes.py +70 -0
- package/templates/.claude/skills/pdf/scripts/check_bounding_boxes_test.py +226 -0
- package/templates/.claude/skills/pdf/scripts/check_fillable_fields.py +12 -0
- package/templates/.claude/skills/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/templates/.claude/skills/pdf/scripts/create_validation_image.py +41 -0
- package/templates/.claude/skills/pdf/scripts/extract_form_field_info.py +152 -0
- package/templates/.claude/skills/pdf/scripts/fill_fillable_fields.py +114 -0
- package/templates/.claude/skills/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/templates/.claude/skills/pdf-processing/SKILL.md +107 -0
- package/templates/.claude/skills/pdf-processing-pro/FORMS.md +610 -0
- package/templates/.claude/skills/pdf-processing-pro/OCR.md +137 -0
- package/templates/.claude/skills/pdf-processing-pro/SKILL.md +296 -0
- package/templates/.claude/skills/pdf-processing-pro/TABLES.md +626 -0
- package/templates/.claude/skills/pdf-processing-pro/scripts/analyze_form.py +307 -0
- package/templates/.claude/skills/postgres/SKILL.md +69 -0
- package/templates/.claude/skills/postgres/reference/fn_get_examples.sql +208 -0
- package/templates/.claude/skills/postgres/reference/fn_rpt_examples.sql +239 -0
- package/templates/.claude/skills/postgres/reference/utility_functions.sql +94 -0
- package/templates/.claude/skills/pptx/LICENSE.txt +30 -0
- package/templates/.claude/skills/pptx/SKILL.md +484 -0
- package/templates/.claude/skills/pptx/html2pptx.md +625 -0
- package/templates/.claude/skills/pptx/ooxml.md +427 -0
- package/templates/.claude/skills/pptx/scripts/html2pptx.js +979 -0
- package/templates/.claude/skills/pptx/scripts/inventory.py +1020 -0
- package/templates/.claude/skills/pptx/scripts/rearrange.py +231 -0
- package/templates/.claude/skills/pptx/scripts/replace.py +385 -0
- package/templates/.claude/skills/pptx/scripts/thumbnail.py +450 -0
- package/templates/.claude/skills/repo-maintenance/SKILL.md +97 -0
- package/templates/.claude/skills/research/EXAMPLES.md +434 -0
- package/templates/.claude/skills/research/REFERENCE.md +399 -0
- package/templates/.claude/skills/research/SKILL.md +136 -0
- package/templates/.claude/skills/root-cause-tracing/SKILL.md +174 -0
- package/templates/.claude/skills/root-cause-tracing/find-polluter.sh +63 -0
- package/templates/.claude/skills/sharing-skills/SKILL.md +194 -0
- package/templates/.claude/skills/sql-optimization-patterns/SKILL.md +493 -0
- package/templates/.claude/skills/subagent-driven-development/SKILL.md +189 -0
- package/templates/.claude/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/templates/.claude/skills/systematic-debugging/SKILL.md +295 -0
- package/templates/.claude/skills/systematic-debugging/test-academic.md +14 -0
- package/templates/.claude/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/templates/.claude/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/templates/.claude/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/templates/.claude/skills/test-driven-development/SKILL.md +364 -0
- package/templates/.claude/skills/testing-anti-patterns/SKILL.md +302 -0
- package/templates/.claude/skills/testing-quality/SKILL.md +97 -0
- package/templates/.claude/skills/verification-before-completion/SKILL.md +139 -0
- package/templates/.claude/skills/webapp-testing/LICENSE.txt +202 -0
- package/templates/.claude/skills/webapp-testing/SKILL.md +96 -0
- package/templates/.claude/skills/webapp-testing/examples/console_logging.py +35 -0
- package/templates/.claude/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/templates/.claude/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/templates/.claude/skills/webapp-testing/scripts/with_server.py +106 -0
- package/templates/.claude/ukit/index/build-index.mjs +28 -0
- package/templates/.claude/ukit/index/cache-utils.mjs +140 -0
- package/templates/.claude/ukit/index/lib/index-core.mjs +2800 -0
- package/templates/.claude/ukit/index/query-index.mjs +150 -0
- package/templates/.claude/ukit/index/refresh-index.mjs +57 -0
- package/templates/.claude/ukit/index/reset-auto-permissions.mjs +76 -0
- package/templates/.claude/ukit/index/resolve-context.mjs +279 -0
- package/templates/.claude/ukit/index/route-catalog.mjs +258 -0
- package/templates/.claude/ukit/index/route-task.mjs +1994 -0
- package/templates/.claude/ukit/index/triage.mjs +133 -0
- package/templates/.claude/ukit/index/verify-context.mjs +689 -0
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +1013 -0
- package/templates/.claude/ukit/runtime/output-compression.mjs +1340 -0
- package/templates/.claude/ukit/runtime/reinject-context.mjs +874 -0
- package/templates/.claude/ukit/runtime/token-utils.mjs +500 -0
- package/templates/.codex/README.md +83 -0
- package/templates/.codex/settings.json +187 -0
- package/templates/.gitignore +75 -0
- package/templates/AGENTS.md +116 -0
- package/templates/CLAUDE.md +93 -0
- package/templates/adapter-presets/antigravity/README.md +22 -0
- package/templates/adapter-presets/antigravity/rules.md +49 -0
- package/templates/adapter-presets/claude/settings.local.json +42 -0
- package/templates/adapter-presets/codex/settings.local.json +6 -0
- package/templates/adapter-presets/opencode/opencode.template.json +1 -0
- package/templates/docs/BUGFIX.md +20 -0
- package/templates/docs/BUG_INDEX.md +12 -0
- package/templates/docs/BUG_METRICS.md +7 -0
- package/templates/docs/BUG_TEMPLATE.md +13 -0
- package/templates/docs/CODE_MAP.md +35 -0
- package/templates/docs/INSTALL.md +113 -0
- package/templates/docs/MEMORY.md +49 -0
- package/templates/docs/PROJECT.md +50 -0
- package/templates/docs/UKIT_USAGE_GUIDE.md +147 -0
- package/templates/docs/WORKLOG.md +10 -0
- package/templates/ukit/README.md +14 -0
- package/templates/ukit/storage/cache/compact-history.json +3 -0
- package/templates/ukit/storage/cache/compact-pressure.json +1 -0
- package/templates/ukit/storage/cache/output-history.json +3 -0
- package/templates/ukit/storage/cache/prompt-cache.json +3 -0
- package/templates/ukit/storage/config.json +37 -0
- package/templates/ukit/storage/memory/projects/.gitkeep +2 -0
- package/templates/ukit/storage/memory/sessions/.gitkeep +0 -0
- 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
|
+
}
|