@unbrained/pm-cli 2026.7.16 → 2026.7.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +17 -2
- package/CONTRIBUTING.md +2 -2
- package/dist/cli/bootstrap-args.d.ts +2 -115
- package/dist/cli/bootstrap-args.js +4 -831
- package/dist/cli/bootstrap-args.js.map +1 -1
- package/dist/cli/commands/eval.d.ts +4 -64
- package/dist/cli/commands/eval.js +4 -153
- package/dist/cli/commands/eval.js.map +1 -1
- package/dist/cli/commands/linked-test-entry.d.ts +6 -6
- package/dist/cli/commands/linked-test-entry.js +4 -65
- package/dist/cli/commands/linked-test-entry.js.map +1 -1
- package/dist/cli/commands/linked-test-parsers.d.ts +3 -45
- package/dist/cli/commands/linked-test-parsers.js +4 -516
- package/dist/cli/commands/linked-test-parsers.js.map +1 -1
- package/dist/cli/commands/list.d.ts +3 -210
- package/dist/cli/commands/list.js +4 -1068
- package/dist/cli/commands/list.js.map +1 -1
- package/dist/cli/commands/search.d.ts +3 -464
- package/dist/cli/commands/search.js +4 -2236
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/stats.d.ts +6 -66
- package/dist/cli/commands/stats.js +4 -157
- package/dist/cli/commands/stats.js.map +1 -1
- package/dist/cli/commands/telemetry.d.ts +6 -14
- package/dist/cli/commands/telemetry.js +4 -349
- package/dist/cli/commands/telemetry.js.map +1 -1
- package/dist/cli/commands/test-all.d.ts +6 -95
- package/dist/cli/commands/test-all.js +4 -439
- package/dist/cli/commands/test-all.js.map +1 -1
- package/dist/cli/commands/test-runs.d.ts +6 -92
- package/dist/cli/commands/test-runs.js +4 -189
- package/dist/cli/commands/test-runs.js.map +1 -1
- package/dist/cli/commands/test.d.ts +6 -244
- package/dist/cli/commands/test.js +4 -1780
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/commands/update-many.d.ts +15 -29
- package/dist/cli/commands/update-many.js +355 -259
- package/dist/cli/commands/update-many.js.map +1 -1
- package/dist/cli/main.js +140 -133
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/search-refresh.js +4 -9
- package/dist/cli/search-refresh.js.map +1 -1
- package/dist/cli/shared-parsers.d.ts +6 -9
- package/dist/cli/shared-parsers.js +4 -37
- package/dist/cli/shared-parsers.js.map +1 -1
- package/dist/cli/telemetry-flush.js +4 -4
- package/dist/cli/telemetry-flush.js.map +1 -1
- package/dist/cli-bundle/bundle-manifest.json +341 -341
- package/dist/cli-bundle/chunks/{append-OJQFUQ4T.js → append-5YF3TO7I.js} +5 -5
- package/dist/cli-bundle/chunks/append-JHO5K6RP.js +11 -0
- package/dist/cli-bundle/chunks/{chunk-WT3ZEOGH.js → chunk-23TCGBRK.js} +16 -7
- package/dist/cli-bundle/chunks/{chunk-WT3ZEOGH.js.map → chunk-23TCGBRK.js.map} +1 -1
- package/dist/cli-bundle/chunks/{chunk-BCOFWYW4.js → chunk-2BX74BAS.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-APRVSW2O.js → chunk-2CXL335K.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-KG44YSKD.js → chunk-2FUOXSRN.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-HDH3JUGG.js → chunk-2IPNL3DR.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-J6364LFM.js → chunk-2MPMKJF2.js} +9 -18
- package/dist/cli-bundle/chunks/{chunk-J6364LFM.js.map → chunk-2MPMKJF2.js.map} +1 -1
- package/dist/cli-bundle/chunks/{chunk-GM5QHQU6.js → chunk-2XOSFSTH.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-K3WII2ME.js → chunk-37DRDP2B.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-2WSE6PO2.js → chunk-37V5MXAU.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-UWQHLMEB.js → chunk-3D4YVTNG.js} +8 -8
- package/dist/cli-bundle/chunks/{chunk-PMOSXADO.js → chunk-47HXN6RX.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-6CFNAVTF.js → chunk-5HGZKQUL.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-2IFFUZLC.js → chunk-5UKQLQM2.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-DM4H7FP4.js → chunk-633PKXLT.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-XSJUSQAE.js → chunk-646MQXFW.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-ZI6VQWQ7.js → chunk-66MKH7SZ.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-S6MQESFR.js → chunk-67RO3H4N.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-34L5OT3Y.js → chunk-6DYOZS6C.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-RRM5JYJC.js → chunk-6O44IN44.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-ONG5V5CI.js → chunk-6XAZB6SE.js} +97 -69
- package/dist/cli-bundle/chunks/chunk-6XAZB6SE.js.map +1 -0
- package/dist/cli-bundle/chunks/{chunk-PENXZX3U.js → chunk-74FKFFXO.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-LPYWLWDS.js → chunk-7CTQ7WW5.js} +37 -37
- package/dist/cli-bundle/chunks/chunk-7CTQ7WW5.js.map +1 -0
- package/dist/cli-bundle/chunks/{chunk-HO4KXD6I.js → chunk-7H5NNCJ5.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-XXXF6PAQ.js → chunk-7LTCLTRG.js} +69 -61
- package/dist/cli-bundle/chunks/chunk-7LTCLTRG.js.map +7 -0
- package/dist/cli-bundle/chunks/{chunk-TQSZFPIS.js → chunk-AFGKSHAM.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-JXQUXVMI.js → chunk-AFSV5HBB.js} +77 -7
- package/dist/cli-bundle/chunks/{chunk-JXQUXVMI.js.map → chunk-AFSV5HBB.js.map} +1 -1
- package/dist/cli-bundle/chunks/{chunk-6N2VXVTQ.js → chunk-ARGDWQ4J.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-3U7ZR6M4.js → chunk-B57RJHBZ.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-7B67ZL4N.js → chunk-B6SH2YNN.js} +75 -5
- package/dist/cli-bundle/chunks/{chunk-7B67ZL4N.js.map → chunk-B6SH2YNN.js.map} +1 -1
- package/dist/cli-bundle/chunks/{chunk-YCQET3OT.js → chunk-BAP7G3N2.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-QKXHFPYM.js → chunk-BIRYX32V.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-RMLWNPH2.js → chunk-BVLOMU6O.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-YBLZ2VE7.js → chunk-CFSUNEL7.js} +320 -239
- package/dist/cli-bundle/chunks/chunk-CFSUNEL7.js.map +7 -0
- package/dist/cli-bundle/chunks/{chunk-2VPAVL7O.js → chunk-CKX4WOPC.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-TKOVBP6C.js → chunk-D52ZN3BF.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-KPMKHGEA.js → chunk-D53RTYVI.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-ODALILG3.js → chunk-D5XHO4Q7.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-E2OJMKML.js → chunk-DCC2AQBD.js} +18 -9
- package/dist/cli-bundle/chunks/{chunk-E2OJMKML.js.map → chunk-DCC2AQBD.js.map} +1 -1
- package/dist/cli-bundle/chunks/{chunk-YAQEBNY7.js → chunk-DDFGYUCQ.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-EAB64KLB.js → chunk-DG2EQRIH.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-IG3SWA4A.js → chunk-EHJFUOXL.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-ONREO4ST.js → chunk-EY7ZE3TG.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-EJEXHTY5.js → chunk-FHOBCGQO.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-H3EIEWGR.js → chunk-FL4D53ND.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-WUJQK4YV.js → chunk-FVNWHUME.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-INU6DKT5.js → chunk-FXFCFL73.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-QEHQ2GVG.js → chunk-FY4RLHSC.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-EACOH7AU.js → chunk-FZGM3PNA.js} +8 -8
- package/dist/cli-bundle/chunks/{chunk-FNSPG7MU.js → chunk-G6KM4YKY.js} +7 -7
- package/dist/cli-bundle/chunks/{chunk-AHVTB5I5.js → chunk-G6MYGULG.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-TSMHDIW4.js → chunk-GLZKZON6.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-2JKGO6QX.js → chunk-H5IOT54C.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-5ZSLZLDN.js → chunk-HF63SBLC.js} +585 -456
- package/dist/cli-bundle/chunks/chunk-HF63SBLC.js.map +1 -0
- package/dist/cli-bundle/chunks/{chunk-SELVXEZK.js → chunk-HO4DLBNK.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-3CLU2XZO.js → chunk-HSYV4ZYT.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-EH2OPHBI.js → chunk-HTXXXF45.js} +23 -19
- package/dist/cli-bundle/chunks/chunk-HTXXXF45.js.map +1 -0
- package/dist/cli-bundle/chunks/{chunk-CRVF2JDM.js → chunk-HXVHBERL.js} +8 -8
- package/dist/cli-bundle/chunks/{chunk-NU3IMIEA.js → chunk-I7DIDUSP.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-2NCIPMQP.js → chunk-I7GY4X6Y.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-JYFOV7FC.js → chunk-IE5KRAKN.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-UY2ACEMF.js → chunk-ILVOOAT4.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-GOSLKLFE.js → chunk-KH6QAHME.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-J6EY5V4N.js → chunk-KOEZCHRF.js} +9 -9
- package/dist/cli-bundle/chunks/{chunk-QKECXBBG.js → chunk-KX62NMZC.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-QKECXBBG.js.map → chunk-KX62NMZC.js.map} +1 -1
- package/dist/cli-bundle/chunks/{chunk-WQPUU7XD.js → chunk-L337QKWJ.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-LSG2U345.js → chunk-L6FVNR7X.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-ILX33QXL.js → chunk-LLODAQMP.js} +706 -532
- package/dist/cli-bundle/chunks/chunk-LLODAQMP.js.map +7 -0
- package/dist/cli-bundle/chunks/{chunk-FMBNM2EN.js → chunk-LSOWTLEM.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-4223PVZT.js → chunk-M7MVOKVV.js} +3 -3
- package/dist/cli-bundle/chunks/chunk-MNV226E4.js +834 -0
- package/dist/cli-bundle/chunks/chunk-MNV226E4.js.map +1 -0
- package/dist/cli-bundle/chunks/{chunk-UMOPHQSU.js → chunk-MR4SHYR4.js} +41 -19
- package/dist/cli-bundle/chunks/chunk-MR4SHYR4.js.map +7 -0
- package/dist/cli-bundle/chunks/{chunk-BKVG6XOA.js → chunk-MSGETE6I.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-3WM75PWC.js → chunk-N33FD2UI.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-UFXU44HV.js → chunk-N7I2DGLP.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-3S5QVM5H.js → chunk-NCAFUBJX.js} +13 -13
- package/dist/cli-bundle/chunks/chunk-NCAFUBJX.js.map +1 -0
- package/dist/cli-bundle/chunks/{chunk-6S3YSUYG.js → chunk-NNANAWFH.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-IJIICKVW.js → chunk-NTRQLE2R.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-SDQLJWBO.js → chunk-NVZ6NVHJ.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-H2Z3FYXQ.js → chunk-OWP6QFBV.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-Q7A43DS4.js → chunk-P22HGFFI.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-NLDFBC6J.js → chunk-P56LKFQX.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-NVPZIJVR.js → chunk-PT7MCCGD.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-P74IZQ7A.js → chunk-QKVVV6QO.js} +7 -7
- package/dist/cli-bundle/chunks/{chunk-5JLUII3O.js → chunk-QNUAZYGW.js} +321 -240
- package/dist/cli-bundle/chunks/chunk-QNUAZYGW.js.map +1 -0
- package/dist/cli-bundle/chunks/{chunk-QIPLHNP7.js → chunk-QWBKN3QC.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-7PIWCHTW.js → chunk-R5XTTZSJ.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-JOUQZENX.js → chunk-RC4UOUXA.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-BEP7WJA2.js → chunk-RGMWJLXF.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-NP22ERDF.js → chunk-RTBX3C4C.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-HYB5AZ63.js → chunk-S46R2M3W.js} +117 -71
- package/dist/cli-bundle/chunks/chunk-S46R2M3W.js.map +7 -0
- package/dist/cli-bundle/chunks/{chunk-H5APFTHZ.js → chunk-S6FF7VW6.js} +18 -18
- package/dist/cli-bundle/chunks/chunk-S6FF7VW6.js.map +7 -0
- package/dist/cli-bundle/chunks/{chunk-UXONS674.js → chunk-SIJV365F.js} +54 -55
- package/dist/cli-bundle/chunks/chunk-SIJV365F.js.map +1 -0
- package/dist/cli-bundle/chunks/{chunk-7Z4LKRS4.js → chunk-SPFZV2GR.js} +9 -9
- package/dist/cli-bundle/chunks/{chunk-ULTHRDBP.js → chunk-TM5CIO3F.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-4XQ3BDV4.js → chunk-V3VBXQQV.js} +36 -36
- package/dist/cli-bundle/chunks/chunk-V3VBXQQV.js.map +7 -0
- package/dist/cli-bundle/chunks/{chunk-YAROI3PD.js → chunk-V7MP6YTN.js} +7 -7
- package/dist/cli-bundle/chunks/{chunk-OW4JWD5M.js → chunk-VAHZIPWS.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-DYH63FX7.js → chunk-VE6ZSTQW.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-3AUGG2DP.js → chunk-W5E2UUSH.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-BSEQUTQP.js → chunk-WACRQ26A.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-BSEQUTQP.js.map → chunk-WACRQ26A.js.map} +1 -1
- package/dist/cli-bundle/chunks/{chunk-UOHFKBUV.js → chunk-WL3SS45P.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-QKJGPFAF.js → chunk-WU3KAN6A.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-IA5FKNII.js → chunk-X2MGQMZO.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-TD3LVC7D.js → chunk-X5YCTFKT.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-B7PE22W6.js → chunk-XC2HEEOV.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-J5AOEGDC.js → chunk-XNPMDSQQ.js} +8 -8
- package/dist/cli-bundle/chunks/{chunk-K2X6U4UA.js → chunk-XXLXJL7V.js} +4 -4
- package/dist/cli-bundle/chunks/chunk-XYJK6GIQ.js +851 -0
- package/dist/cli-bundle/chunks/chunk-XYJK6GIQ.js.map +7 -0
- package/dist/cli-bundle/chunks/{chunk-RLCHOOHC.js → chunk-YJSURTBB.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-PGY23YTC.js → chunk-YKFHF37B.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-JFTKLTEF.js → chunk-YQCWK2MB.js} +4 -4
- package/dist/cli-bundle/chunks/{chunk-EYLCSJAI.js → chunk-YQEZWI3R.js} +2 -2
- package/dist/cli-bundle/chunks/close-BEP7MHF3.js +12 -0
- package/dist/cli-bundle/chunks/{close-CWODTPKG.js → close-Y6QFUJQN.js} +5 -5
- package/dist/cli-bundle/chunks/{close-many-CQFUY7MR.js → close-many-JJFW7KTT.js} +14 -14
- package/dist/cli-bundle/chunks/close-many-QJ6FKJUE.js +23 -0
- package/dist/cli-bundle/chunks/comments-2AOZQ5D2.js +13 -0
- package/dist/cli-bundle/chunks/{comments-WGLHR7LB.js → comments-TMHMALA5.js} +7 -7
- package/dist/cli-bundle/chunks/{copy-LXVDXH7B.js → copy-FWQA5OU4.js} +4 -4
- package/dist/cli-bundle/chunks/copy-N3LCQBMC.js +9 -0
- package/dist/cli-bundle/chunks/{create-LBVCYMEG.js → create-AQ5SIGZC.js} +9 -9
- package/dist/cli-bundle/chunks/{create-J5U6ND4K.js → create-KOGQ3JKD.js} +9 -9
- package/dist/cli-bundle/chunks/delete-6G3MBZ6S.js +10 -0
- package/dist/cli-bundle/chunks/{delete-PMJBNY5G.js → delete-YYYFOSEA.js} +4 -4
- package/dist/cli-bundle/chunks/{deps-OA26IS25.js → deps-5NC4IZH7.js} +5 -5
- package/dist/cli-bundle/chunks/{deps-JZ7TMVCB.js → deps-EVTOQMKW.js} +5 -5
- package/dist/cli-bundle/chunks/{docs-FEWB2DQB.js → docs-53NUGMIQ.js} +6 -6
- package/dist/cli-bundle/chunks/docs-OWRGDMNY.js +14 -0
- package/dist/cli-bundle/chunks/{files-EFIWKFTM.js → files-3PXGXYDB.js} +6 -6
- package/dist/cli-bundle/chunks/{files-6YFBJRBG.js → files-EGLKHD7O.js} +6 -6
- package/dist/cli-bundle/chunks/focus-KZLIA6WS.js +10 -0
- package/dist/cli-bundle/chunks/{focus-4QJR7FOQ.js → focus-SRPASFTN.js} +5 -5
- package/dist/cli-bundle/chunks/{history-compact-CKRPDTJN.js → history-compact-G4DULDCX.js} +6 -6
- package/dist/cli-bundle/chunks/history-compact-HHIUOMZ7.js +16 -0
- package/dist/cli-bundle/chunks/{history-redact-BB3TGDAE.js → history-redact-IUSJ6X3E.js} +4 -4
- package/dist/cli-bundle/chunks/{history-redact-D225BHOZ.js → history-redact-THHGRQXV.js} +4 -4
- package/dist/cli-bundle/chunks/history-repair-KG7SWXI6.js +16 -0
- package/dist/cli-bundle/chunks/{history-repair-RQXKQHL4.js → history-repair-LUQWRS7X.js} +6 -6
- package/dist/cli-bundle/chunks/{learnings-JLNLCOL5.js → learnings-2MRNBFJF.js} +7 -7
- package/dist/cli-bundle/chunks/learnings-ZVHSALJY.js +13 -0
- package/dist/cli-bundle/chunks/{notes-GB47XXQQ.js → notes-73JWKWUD.js} +7 -7
- package/dist/cli-bundle/chunks/notes-BAKUL5DJ.js +13 -0
- package/dist/cli-bundle/chunks/{plan-VDCJ2PCN.js → plan-AZPMCG37.js} +10 -10
- package/dist/cli-bundle/chunks/plan-LEAGLPGP.js +24 -0
- package/dist/cli-bundle/chunks/{profile-IVXQEL3D.js → profile-AGUKS52S.js} +9 -9
- package/dist/cli-bundle/chunks/{profile-FAU7NZML.js → profile-U2B6Y7GE.js} +9 -9
- package/dist/cli-bundle/chunks/{register-list-query-UL4UKBBN.js → register-list-query-A5KRJ5J2.js} +16 -16
- package/dist/cli-bundle/chunks/{register-list-query-DKA3EH5D.js → register-list-query-NEYP3TXO.js} +16 -16
- package/dist/cli-bundle/chunks/{register-mutation-RBBJ52DK.js → register-mutation-6LOPBAHC.js} +29 -29
- package/dist/cli-bundle/chunks/{register-mutation-ARFHRW55.js → register-mutation-GCZOALNJ.js} +29 -29
- package/dist/cli-bundle/chunks/{register-operations-ZIBBRSED.js → register-operations-6ZZIF27A.js} +35 -35
- package/dist/cli-bundle/chunks/{register-operations-ZUJ5JWPE.js → register-operations-REDIAEGI.js} +35 -35
- package/dist/cli-bundle/chunks/{register-setup-TDCGWIOS.js → register-setup-BHPMN5SK.js} +15 -15
- package/dist/cli-bundle/chunks/{register-setup-BCVLFEYS.js → register-setup-J3JS2ZVT.js} +15 -15
- package/dist/cli-bundle/chunks/{restore-25DLT2Q2.js → restore-E3PRGDN3.js} +6 -6
- package/dist/cli-bundle/chunks/restore-KIUYTAEJ.js +14 -0
- package/dist/cli-bundle/chunks/{schema-FNU3WE4O.js → schema-5JNFQFUW.js} +6 -6
- package/dist/cli-bundle/chunks/{schema-NBCM3RAD.js → schema-TMIW4XZC.js} +6 -6
- package/dist/cli-bundle/chunks/update-PQQHLYCC.js +21 -0
- package/dist/cli-bundle/chunks/{update-XHSIAIXE.js → update-SA6AG23P.js} +12 -12
- package/dist/cli-bundle/chunks/update-many-RQVTFJPD.js +36 -0
- package/dist/cli-bundle/chunks/update-many-TV3L3KBX.js +32 -0
- package/dist/cli-bundle/main.js +153 -855
- package/dist/cli-bundle/main.js.map +4 -4
- package/dist/cli-bundle/sdk-runtime.js +54 -54
- package/dist/cli-bundle/sdk-testing.js +6 -6
- package/dist/cli-bundle/sdk.js +195 -59
- package/dist/cli-bundle/sdk.js.map +3 -3
- package/dist/core/extensions/item-fields.js +2 -2
- package/dist/core/extensions/item-fields.js.map +1 -1
- package/dist/sdk/cli-bootstrap.d.ts +122 -0
- package/dist/sdk/cli-bootstrap.js +879 -0
- package/dist/sdk/cli-bootstrap.js.map +1 -0
- package/dist/sdk/eval.d.ts +66 -0
- package/dist/sdk/eval.js +186 -0
- package/dist/sdk/eval.js.map +1 -0
- package/dist/sdk/index.d.ts +16 -4
- package/dist/sdk/index.js +18 -6
- package/dist/sdk/index.js.map +1 -1
- package/dist/{cli/commands → sdk/query}/item-filter-options.d.ts +1 -1
- package/dist/{cli/commands → sdk/query}/item-filter-options.js +3 -3
- package/dist/sdk/query/item-filter-options.js.map +1 -0
- package/dist/sdk/query/list.d.ts +254 -0
- package/dist/sdk/query/list.js +1073 -0
- package/dist/sdk/query/list.js.map +1 -0
- package/dist/sdk/query/parsers.d.ts +9 -0
- package/dist/sdk/query/parsers.js +45 -0
- package/dist/sdk/query/parsers.js.map +1 -0
- package/dist/{cli/commands → sdk/query}/search-pagination.js +4 -4
- package/dist/sdk/query/search-pagination.js.map +1 -0
- package/dist/{cli/commands → sdk/query}/search-rendering.js +3 -3
- package/dist/sdk/query/search-rendering.js.map +1 -0
- package/dist/sdk/query/search.d.ts +469 -0
- package/dist/sdk/query/search.js +2247 -0
- package/dist/sdk/query/search.js.map +1 -0
- package/dist/sdk/runtime.d.ts +7 -7
- package/dist/sdk/runtime.js +13 -7
- package/dist/sdk/runtime.js.map +1 -1
- package/dist/sdk/search-refresh-worker.d.ts +2 -0
- package/dist/sdk/search-refresh-worker.js +19 -0
- package/dist/sdk/search-refresh-worker.js.map +1 -0
- package/dist/sdk/stats.d.ts +66 -0
- package/dist/sdk/stats.js +151 -0
- package/dist/sdk/stats.js.map +1 -0
- package/dist/sdk/telemetry-flush.d.ts +2 -0
- package/dist/sdk/telemetry-flush.js +14 -0
- package/dist/sdk/telemetry-flush.js.map +1 -0
- package/dist/sdk/telemetry.d.ts +14 -0
- package/dist/sdk/telemetry.js +398 -0
- package/dist/sdk/telemetry.js.map +1 -0
- package/dist/sdk/test/batch.d.ts +94 -0
- package/dist/sdk/test/batch.js +492 -0
- package/dist/sdk/test/batch.js.map +1 -0
- package/dist/sdk/test/entry.d.ts +6 -0
- package/dist/sdk/test/entry.js +74 -0
- package/dist/sdk/test/entry.js.map +1 -0
- package/dist/sdk/test/execution.d.ts +282 -0
- package/dist/sdk/test/execution.js +1817 -0
- package/dist/sdk/test/execution.js.map +1 -0
- package/dist/sdk/test/parsers.d.ts +48 -0
- package/dist/sdk/test/parsers.js +522 -0
- package/dist/sdk/test/parsers.js.map +1 -0
- package/dist/sdk/test/runs.d.ts +92 -0
- package/dist/sdk/test/runs.js +195 -0
- package/dist/sdk/test/runs.js.map +1 -0
- package/docs/SDK.md +122 -0
- package/docs/SDK_PRIMITIVE_INVENTORY.md +5 -3
- package/docs/examples/sdk-custom-tool/README.md +27 -0
- package/docs/examples/sdk-custom-tool/package.json +13 -0
- package/docs/examples/sdk-custom-tool/src/cli.ts +36 -0
- package/docs/examples/sdk-custom-tool/src/entry.ts +15 -0
- package/docs/examples/sdk-custom-tool/src/index.ts +223 -0
- package/marketplace.json +2 -2
- package/package.json +4 -4
- package/packages/pm-beads/package.json +1 -1
- package/packages/pm-calendar/package.json +1 -1
- package/packages/pm-command-kit/package.json +1 -1
- package/packages/pm-governance-audit/extensions/governance-audit/comments-audit.ts +77 -57
- package/packages/pm-governance-audit/extensions/governance-audit/dedupe-audit.ts +202 -194
- package/packages/pm-governance-audit/extensions/governance-audit/dedupe-merge.ts +227 -153
- package/packages/pm-governance-audit/extensions/governance-audit/normalize.ts +169 -125
- package/packages/pm-governance-audit/extensions/governance-audit/runtime-utils.ts +130 -81
- package/packages/pm-governance-audit/extensions/governance-audit/sdk.ts +53 -4
- package/packages/pm-governance-audit/package.json +1 -1
- package/packages/pm-guide-shell/package.json +1 -1
- package/packages/pm-kanban/package.json +1 -1
- package/packages/pm-lifecycle-hooks/package.json +1 -1
- package/packages/pm-linked-test-adapters/package.json +1 -1
- package/packages/pm-search-advanced/package.json +1 -1
- package/packages/pm-templates/package.json +1 -1
- package/packages/pm-todos/package.json +1 -1
- package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
- package/dist/cli/commands/item-filter-options.js.map +0 -1
- package/dist/cli/commands/search-pagination.js.map +0 -1
- package/dist/cli/commands/search-rendering.js.map +0 -1
- package/dist/cli-bundle/chunks/append-UZVHUO74.js +0 -11
- package/dist/cli-bundle/chunks/chunk-3S5QVM5H.js.map +0 -1
- package/dist/cli-bundle/chunks/chunk-4XQ3BDV4.js.map +0 -7
- package/dist/cli-bundle/chunks/chunk-5JLUII3O.js.map +0 -1
- package/dist/cli-bundle/chunks/chunk-5ZSLZLDN.js.map +0 -1
- package/dist/cli-bundle/chunks/chunk-EH2OPHBI.js.map +0 -1
- package/dist/cli-bundle/chunks/chunk-H5APFTHZ.js.map +0 -7
- package/dist/cli-bundle/chunks/chunk-HYB5AZ63.js.map +0 -7
- package/dist/cli-bundle/chunks/chunk-ILX33QXL.js.map +0 -7
- package/dist/cli-bundle/chunks/chunk-LPYWLWDS.js.map +0 -1
- package/dist/cli-bundle/chunks/chunk-ONG5V5CI.js.map +0 -1
- package/dist/cli-bundle/chunks/chunk-UMOPHQSU.js.map +0 -7
- package/dist/cli-bundle/chunks/chunk-UXONS674.js.map +0 -1
- package/dist/cli-bundle/chunks/chunk-XXXF6PAQ.js.map +0 -7
- package/dist/cli-bundle/chunks/chunk-YBLZ2VE7.js.map +0 -7
- package/dist/cli-bundle/chunks/chunk-YXSHIAUJ.js +0 -58
- package/dist/cli-bundle/chunks/chunk-YXSHIAUJ.js.map +0 -1
- package/dist/cli-bundle/chunks/chunk-ZKRZY773.js +0 -68
- package/dist/cli-bundle/chunks/chunk-ZKRZY773.js.map +0 -7
- package/dist/cli-bundle/chunks/close-W6LI2PKM.js +0 -12
- package/dist/cli-bundle/chunks/close-many-DLDDVSH5.js +0 -23
- package/dist/cli-bundle/chunks/comments-E3TNTK24.js +0 -13
- package/dist/cli-bundle/chunks/copy-Q5ZJPUKR.js +0 -9
- package/dist/cli-bundle/chunks/delete-WNHB3LYW.js +0 -10
- package/dist/cli-bundle/chunks/docs-HA2AIDBB.js +0 -14
- package/dist/cli-bundle/chunks/focus-POIW5LTJ.js +0 -10
- package/dist/cli-bundle/chunks/history-compact-FWG2C7FZ.js +0 -16
- package/dist/cli-bundle/chunks/history-repair-6GEAM7HE.js +0 -16
- package/dist/cli-bundle/chunks/learnings-RSNLSWDC.js +0 -13
- package/dist/cli-bundle/chunks/notes-2WDH6DXU.js +0 -13
- package/dist/cli-bundle/chunks/plan-DOVI2VMW.js +0 -24
- package/dist/cli-bundle/chunks/restore-YPMVUUEX.js +0 -14
- package/dist/cli-bundle/chunks/update-WWBYOPXO.js +0 -21
- package/dist/cli-bundle/chunks/update-many-OUGIU5SF.js +0 -32
- package/dist/cli-bundle/chunks/update-many-TA7NGQBO.js +0 -36
- /package/dist/cli-bundle/chunks/{append-OJQFUQ4T.js.map → append-5YF3TO7I.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{append-UZVHUO74.js.map → append-JHO5K6RP.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-BCOFWYW4.js.map → chunk-2BX74BAS.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-APRVSW2O.js.map → chunk-2CXL335K.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-KG44YSKD.js.map → chunk-2FUOXSRN.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-HDH3JUGG.js.map → chunk-2IPNL3DR.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-GM5QHQU6.js.map → chunk-2XOSFSTH.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-K3WII2ME.js.map → chunk-37DRDP2B.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-2WSE6PO2.js.map → chunk-37V5MXAU.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-UWQHLMEB.js.map → chunk-3D4YVTNG.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-PMOSXADO.js.map → chunk-47HXN6RX.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-6CFNAVTF.js.map → chunk-5HGZKQUL.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-2IFFUZLC.js.map → chunk-5UKQLQM2.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-DM4H7FP4.js.map → chunk-633PKXLT.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-XSJUSQAE.js.map → chunk-646MQXFW.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-ZI6VQWQ7.js.map → chunk-66MKH7SZ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-S6MQESFR.js.map → chunk-67RO3H4N.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-34L5OT3Y.js.map → chunk-6DYOZS6C.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-RRM5JYJC.js.map → chunk-6O44IN44.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-PENXZX3U.js.map → chunk-74FKFFXO.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-HO4KXD6I.js.map → chunk-7H5NNCJ5.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-TQSZFPIS.js.map → chunk-AFGKSHAM.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-6N2VXVTQ.js.map → chunk-ARGDWQ4J.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-3U7ZR6M4.js.map → chunk-B57RJHBZ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-YCQET3OT.js.map → chunk-BAP7G3N2.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-QKXHFPYM.js.map → chunk-BIRYX32V.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-RMLWNPH2.js.map → chunk-BVLOMU6O.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-2VPAVL7O.js.map → chunk-CKX4WOPC.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-TKOVBP6C.js.map → chunk-D52ZN3BF.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-KPMKHGEA.js.map → chunk-D53RTYVI.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-ODALILG3.js.map → chunk-D5XHO4Q7.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-YAQEBNY7.js.map → chunk-DDFGYUCQ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-EAB64KLB.js.map → chunk-DG2EQRIH.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-IG3SWA4A.js.map → chunk-EHJFUOXL.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-ONREO4ST.js.map → chunk-EY7ZE3TG.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-EJEXHTY5.js.map → chunk-FHOBCGQO.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-H3EIEWGR.js.map → chunk-FL4D53ND.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-WUJQK4YV.js.map → chunk-FVNWHUME.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-INU6DKT5.js.map → chunk-FXFCFL73.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-QEHQ2GVG.js.map → chunk-FY4RLHSC.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-EACOH7AU.js.map → chunk-FZGM3PNA.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-FNSPG7MU.js.map → chunk-G6KM4YKY.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-AHVTB5I5.js.map → chunk-G6MYGULG.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-TSMHDIW4.js.map → chunk-GLZKZON6.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-2JKGO6QX.js.map → chunk-H5IOT54C.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-SELVXEZK.js.map → chunk-HO4DLBNK.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-3CLU2XZO.js.map → chunk-HSYV4ZYT.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-CRVF2JDM.js.map → chunk-HXVHBERL.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-NU3IMIEA.js.map → chunk-I7DIDUSP.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-2NCIPMQP.js.map → chunk-I7GY4X6Y.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-JYFOV7FC.js.map → chunk-IE5KRAKN.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-UY2ACEMF.js.map → chunk-ILVOOAT4.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-GOSLKLFE.js.map → chunk-KH6QAHME.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-J6EY5V4N.js.map → chunk-KOEZCHRF.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-WQPUU7XD.js.map → chunk-L337QKWJ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-LSG2U345.js.map → chunk-L6FVNR7X.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-FMBNM2EN.js.map → chunk-LSOWTLEM.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-4223PVZT.js.map → chunk-M7MVOKVV.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-BKVG6XOA.js.map → chunk-MSGETE6I.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-3WM75PWC.js.map → chunk-N33FD2UI.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-UFXU44HV.js.map → chunk-N7I2DGLP.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-6S3YSUYG.js.map → chunk-NNANAWFH.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-IJIICKVW.js.map → chunk-NTRQLE2R.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-SDQLJWBO.js.map → chunk-NVZ6NVHJ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-H2Z3FYXQ.js.map → chunk-OWP6QFBV.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-Q7A43DS4.js.map → chunk-P22HGFFI.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-NLDFBC6J.js.map → chunk-P56LKFQX.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-NVPZIJVR.js.map → chunk-PT7MCCGD.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-P74IZQ7A.js.map → chunk-QKVVV6QO.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-QIPLHNP7.js.map → chunk-QWBKN3QC.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-7PIWCHTW.js.map → chunk-R5XTTZSJ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-JOUQZENX.js.map → chunk-RC4UOUXA.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-BEP7WJA2.js.map → chunk-RGMWJLXF.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-NP22ERDF.js.map → chunk-RTBX3C4C.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-7Z4LKRS4.js.map → chunk-SPFZV2GR.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-ULTHRDBP.js.map → chunk-TM5CIO3F.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-YAROI3PD.js.map → chunk-V7MP6YTN.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-OW4JWD5M.js.map → chunk-VAHZIPWS.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-DYH63FX7.js.map → chunk-VE6ZSTQW.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-3AUGG2DP.js.map → chunk-W5E2UUSH.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-UOHFKBUV.js.map → chunk-WL3SS45P.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-QKJGPFAF.js.map → chunk-WU3KAN6A.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-IA5FKNII.js.map → chunk-X2MGQMZO.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-TD3LVC7D.js.map → chunk-X5YCTFKT.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-B7PE22W6.js.map → chunk-XC2HEEOV.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-J5AOEGDC.js.map → chunk-XNPMDSQQ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-K2X6U4UA.js.map → chunk-XXLXJL7V.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-RLCHOOHC.js.map → chunk-YJSURTBB.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-PGY23YTC.js.map → chunk-YKFHF37B.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-JFTKLTEF.js.map → chunk-YQCWK2MB.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{chunk-EYLCSJAI.js.map → chunk-YQEZWI3R.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{close-W6LI2PKM.js.map → close-BEP7MHF3.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{close-CWODTPKG.js.map → close-Y6QFUJQN.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{close-many-CQFUY7MR.js.map → close-many-JJFW7KTT.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{close-many-DLDDVSH5.js.map → close-many-QJ6FKJUE.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{comments-E3TNTK24.js.map → comments-2AOZQ5D2.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{comments-WGLHR7LB.js.map → comments-TMHMALA5.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{copy-LXVDXH7B.js.map → copy-FWQA5OU4.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{copy-Q5ZJPUKR.js.map → copy-N3LCQBMC.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{create-LBVCYMEG.js.map → create-AQ5SIGZC.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{create-J5U6ND4K.js.map → create-KOGQ3JKD.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{delete-WNHB3LYW.js.map → delete-6G3MBZ6S.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{delete-PMJBNY5G.js.map → delete-YYYFOSEA.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{deps-OA26IS25.js.map → deps-5NC4IZH7.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{deps-JZ7TMVCB.js.map → deps-EVTOQMKW.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{docs-FEWB2DQB.js.map → docs-53NUGMIQ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{docs-HA2AIDBB.js.map → docs-OWRGDMNY.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{files-EFIWKFTM.js.map → files-3PXGXYDB.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{files-6YFBJRBG.js.map → files-EGLKHD7O.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{focus-POIW5LTJ.js.map → focus-KZLIA6WS.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{focus-4QJR7FOQ.js.map → focus-SRPASFTN.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{history-compact-CKRPDTJN.js.map → history-compact-G4DULDCX.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{history-compact-FWG2C7FZ.js.map → history-compact-HHIUOMZ7.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{history-redact-BB3TGDAE.js.map → history-redact-IUSJ6X3E.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{history-redact-D225BHOZ.js.map → history-redact-THHGRQXV.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{history-repair-6GEAM7HE.js.map → history-repair-KG7SWXI6.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{history-repair-RQXKQHL4.js.map → history-repair-LUQWRS7X.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{learnings-JLNLCOL5.js.map → learnings-2MRNBFJF.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{learnings-RSNLSWDC.js.map → learnings-ZVHSALJY.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{notes-GB47XXQQ.js.map → notes-73JWKWUD.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{notes-2WDH6DXU.js.map → notes-BAKUL5DJ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{plan-VDCJ2PCN.js.map → plan-AZPMCG37.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{plan-DOVI2VMW.js.map → plan-LEAGLPGP.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{profile-IVXQEL3D.js.map → profile-AGUKS52S.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{profile-FAU7NZML.js.map → profile-U2B6Y7GE.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{register-list-query-UL4UKBBN.js.map → register-list-query-A5KRJ5J2.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{register-list-query-DKA3EH5D.js.map → register-list-query-NEYP3TXO.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{register-mutation-RBBJ52DK.js.map → register-mutation-6LOPBAHC.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{register-mutation-ARFHRW55.js.map → register-mutation-GCZOALNJ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{register-operations-ZIBBRSED.js.map → register-operations-6ZZIF27A.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{register-operations-ZUJ5JWPE.js.map → register-operations-REDIAEGI.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{register-setup-TDCGWIOS.js.map → register-setup-BHPMN5SK.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{register-setup-BCVLFEYS.js.map → register-setup-J3JS2ZVT.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{restore-25DLT2Q2.js.map → restore-E3PRGDN3.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{restore-YPMVUUEX.js.map → restore-KIUYTAEJ.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{schema-FNU3WE4O.js.map → schema-5JNFQFUW.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{schema-NBCM3RAD.js.map → schema-TMIW4XZC.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{update-WWBYOPXO.js.map → update-PQQHLYCC.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{update-XHSIAIXE.js.map → update-SA6AG23P.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{update-many-TA7NGQBO.js.map → update-many-RQVTFJPD.js.map} +0 -0
- /package/dist/cli-bundle/chunks/{update-many-OUGIU5SF.js.map → update-many-TV3L3KBX.js.map} +0 -0
- /package/dist/{cli/commands → sdk/query}/search-pagination.d.ts +0 -0
- /package/dist/{cli/commands → sdk/query}/search-rendering.d.ts +0 -0
|
@@ -0,0 +1,2247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module sdk/query/search
|
|
3
|
+
*
|
|
4
|
+
* Implements the pm search command surface and its agent-facing runtime behavior.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7d6721eb-1e28-58ea-a5fc-4494f249275c")}catch(e){}}();
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { coerceNumberInRange, toNonEmptyStringOrUndefined, } from "../../core/shared/primitives.js";
|
|
11
|
+
import { isPathWithinDirectory } from "../../core/fs/path-utils.js";
|
|
12
|
+
import { getActiveExtensionRegistrations, runActiveOnReadHooks, } from "../../core/extensions/index.js";
|
|
13
|
+
import { collectRegisteredItemFieldNames } from "../../core/extensions/item-fields.js";
|
|
14
|
+
import { resolveRegisteredSearchProvider, resolveRegisteredVectorStoreAdapter, } from "../../core/extensions/runtime-registrations.js";
|
|
15
|
+
import { resolveItemTypeRegistry, } from "../../core/item/type-registry.js";
|
|
16
|
+
import { parseLimit, parsePriority, parseType } from "./parsers.js";
|
|
17
|
+
import { executeEmbeddingRequest, resolveEmbeddingProviders, } from "../../core/search/providers.js";
|
|
18
|
+
import { buildDeterministicQueryExpansions, mergeQueryExpansions, normalizeQueryExpansionOutput, normalizeRerankOutput, rerankCandidatesWithEmbeddings, resolveQueryExpansionConfig, resolveRerankConfig, } from "../../core/search/relevance.js";
|
|
19
|
+
import { resolveSettingsWithSemanticRuntimeDefaults } from "../../core/search/semantic-defaults.js";
|
|
20
|
+
import { executeVectorQuery, resolveVectorStores, } from "../../core/search/vector-stores.js";
|
|
21
|
+
import { readVectorizationStatusLedger } from "../../core/search/cache.js";
|
|
22
|
+
import { buildEventCorpus, buildPlanFlatCorpus, buildReminderCorpus, buildSearchCorpus, resolveSearchCorpusFields, } from "../../core/search/corpus.js";
|
|
23
|
+
import { buildBm25Index, flattenSearchCorpusText, resolveBm25Params, scoreBm25Query, tokenizeBm25, } from "../../core/search/bm25.js";
|
|
24
|
+
import { collectStaleVectorizationIds } from "../../core/search/staleness.js";
|
|
25
|
+
import { pathExists } from "../../core/fs/fs-utils.js";
|
|
26
|
+
import { parseItemDocument } from "../../core/item/item-format.js";
|
|
27
|
+
import { toItemRecord } from "../../core/item/item-record.js";
|
|
28
|
+
import { isTerminalStatus } from "../../core/item/status.js";
|
|
29
|
+
import { parseStatusFilterCsv } from "../../core/item/status-filter.js";
|
|
30
|
+
import { collectRuntimeFilterValues, matchesRuntimeFilters, } from "../../core/schema/runtime-field-filters.js";
|
|
31
|
+
import { hasMissingMetadataFilter, itemMatchesMissingMetadata, lifecycleClassifierFromStatusRegistry, } from "../../core/governance/metadata-coverage.js";
|
|
32
|
+
import { hasContentFieldFilter, itemMatchesContentFilters, } from "../../core/governance/content-fields.js";
|
|
33
|
+
import { resolveContentFieldFilters, resolveMissingMetadataFilters, } from "./list.js";
|
|
34
|
+
import { buildCompactSearchFilterSummary, buildVerboseSearchFilters, } from "./search-rendering.js";
|
|
35
|
+
import { resolveRuntimeFieldRegistry, resolveRuntimeStatusRegistry, } from "../../core/schema/runtime-schema.js";
|
|
36
|
+
import { EXIT_CODE } from "../../core/shared/constants.js";
|
|
37
|
+
import { PmCliError } from "../../core/shared/errors.js";
|
|
38
|
+
import { tokenizeAlphaNumeric } from "../../core/shared/text-normalization.js";
|
|
39
|
+
import { compareTimestampStrings, matchesTimestampFilters, nowIso, resolveIsoOrRelative, } from "../../core/shared/time.js";
|
|
40
|
+
import { listAllDocumentCandidatesCached } from "../../core/store/item-metadata-cache.js";
|
|
41
|
+
import { listAllItemMetadata } from "../../core/store/item-store.js";
|
|
42
|
+
import { getItemPath, getSettingsPath, resolveGlobalPmRoot, resolvePmRoot, } from "../../core/store/paths.js";
|
|
43
|
+
import { readSettings } from "../../core/store/settings.js";
|
|
44
|
+
import { resolveSearchPage } from "./search-pagination.js";
|
|
45
|
+
const DEFAULT_COMPACT_SEARCH_FIELDS = [
|
|
46
|
+
"id",
|
|
47
|
+
"title",
|
|
48
|
+
"status",
|
|
49
|
+
"type",
|
|
50
|
+
"priority",
|
|
51
|
+
"updated_at",
|
|
52
|
+
"score",
|
|
53
|
+
"matched_fields",
|
|
54
|
+
];
|
|
55
|
+
const SEARCH_HIT_FIELD_KEYS = new Set([
|
|
56
|
+
"score",
|
|
57
|
+
"matched_fields",
|
|
58
|
+
"highlights",
|
|
59
|
+
]);
|
|
60
|
+
const SEARCH_ITEM_FIELD_KEYS = new Set([
|
|
61
|
+
"id",
|
|
62
|
+
"title",
|
|
63
|
+
"description",
|
|
64
|
+
"type",
|
|
65
|
+
"status",
|
|
66
|
+
"priority",
|
|
67
|
+
"tags",
|
|
68
|
+
"created_at",
|
|
69
|
+
"updated_at",
|
|
70
|
+
"deadline",
|
|
71
|
+
"assignee",
|
|
72
|
+
"author",
|
|
73
|
+
"estimated_minutes",
|
|
74
|
+
"acceptance_criteria",
|
|
75
|
+
"dependencies",
|
|
76
|
+
"comments",
|
|
77
|
+
"notes",
|
|
78
|
+
"learnings",
|
|
79
|
+
"reminders",
|
|
80
|
+
"events",
|
|
81
|
+
"files",
|
|
82
|
+
"tests",
|
|
83
|
+
"docs",
|
|
84
|
+
"close_reason",
|
|
85
|
+
"parent",
|
|
86
|
+
"reviewer",
|
|
87
|
+
"risk",
|
|
88
|
+
"confidence",
|
|
89
|
+
"sprint",
|
|
90
|
+
"release",
|
|
91
|
+
"blocked_by",
|
|
92
|
+
"blocked_reason",
|
|
93
|
+
"reporter",
|
|
94
|
+
"severity",
|
|
95
|
+
"environment",
|
|
96
|
+
"repro_steps",
|
|
97
|
+
"resolution",
|
|
98
|
+
"expected_result",
|
|
99
|
+
"actual_result",
|
|
100
|
+
"affected_version",
|
|
101
|
+
"fixed_version",
|
|
102
|
+
"component",
|
|
103
|
+
"regression",
|
|
104
|
+
"customer_impact",
|
|
105
|
+
"definition_of_ready",
|
|
106
|
+
"order",
|
|
107
|
+
"rank",
|
|
108
|
+
"goal",
|
|
109
|
+
"objective",
|
|
110
|
+
"value",
|
|
111
|
+
"impact",
|
|
112
|
+
"outcome",
|
|
113
|
+
"why_now",
|
|
114
|
+
"plan",
|
|
115
|
+
]);
|
|
116
|
+
const LONG_QUERY_TOKEN_THRESHOLD = 2;
|
|
117
|
+
const LONG_QUERY_TITLE_EXACT_BONUS = 120;
|
|
118
|
+
const LONG_QUERY_PHRASE_MULTIPLIER = 6;
|
|
119
|
+
// GH-181: in default (OR) match mode, multi-token queries that match EVERY
|
|
120
|
+
// distinct query token in some searchable field get an additive ranking bonus so
|
|
121
|
+
// items covering all terms outrank items matching only a subset. This is a
|
|
122
|
+
// RANKING preference, not a hard filter (use --match-mode and for that).
|
|
123
|
+
const ALL_TERMS_COVERAGE_BONUS = 40;
|
|
124
|
+
const EXACT_ID_MATCH_SCORE = 1_000;
|
|
125
|
+
const SHORT_ID_MATCH_SCORE = 900;
|
|
126
|
+
// GH-157 matched-text highlighting (--highlight): markers wrapping each matching
|
|
127
|
+
// token run, and the number of characters of surrounding context retained on
|
|
128
|
+
// each side of the first match in a field. The «…» guillemets are single
|
|
129
|
+
// characters with negligible collision risk against item text, keeping the
|
|
130
|
+
// snippet token-cheap while staying visually unambiguous for agents.
|
|
131
|
+
const HIGHLIGHT_OPEN = "«";
|
|
132
|
+
const HIGHLIGHT_CLOSE = "»";
|
|
133
|
+
const HIGHLIGHT_SNIPPET_RADIUS = 60;
|
|
134
|
+
// GH-157 inline query syntax: bare `field:value` tokens parsed out of the query
|
|
135
|
+
// string and applied as the equivalent filter. The value may itself contain
|
|
136
|
+
// colons (e.g. `tag:area:search`), so only the FIRST colon delimits field from
|
|
137
|
+
// value. Explicit --field flags take precedence over an inline token (see
|
|
138
|
+
// resolveInlineQueryFilters).
|
|
139
|
+
const INLINE_QUERY_FILTER_FIELDS = [
|
|
140
|
+
"tag",
|
|
141
|
+
"status",
|
|
142
|
+
"type",
|
|
143
|
+
"priority",
|
|
144
|
+
];
|
|
145
|
+
/**
|
|
146
|
+
* Aggregate the `code` strings found along an error's `cause` chain.
|
|
147
|
+
*
|
|
148
|
+
* undici (Node's fetch) collapses connection errors to the generic message
|
|
149
|
+
* "fetch failed" and stashes the real syscall code (e.g. `ECONNREFUSED`) on
|
|
150
|
+
* `error.cause.code`. Walking the chain (bounded depth) lets the fallback
|
|
151
|
+
* classifier label a downed/unreachable Ollama backend as "connection" rather
|
|
152
|
+
* than the catch-all "error".
|
|
153
|
+
*/
|
|
154
|
+
export function collectErrorCauseCodes(error) {
|
|
155
|
+
const codes = [];
|
|
156
|
+
let current = error;
|
|
157
|
+
for (let depth = 0; depth < 5 && current && typeof current === "object"; depth += 1) {
|
|
158
|
+
const code = current.code;
|
|
159
|
+
if (typeof code === "string") {
|
|
160
|
+
codes.push(code.toLowerCase());
|
|
161
|
+
}
|
|
162
|
+
current = current.cause;
|
|
163
|
+
}
|
|
164
|
+
return codes.join(" ");
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Classify why an implicit/explicit semantic search degraded to keyword mode,
|
|
168
|
+
* inspecting both the error message and the {@link collectErrorCauseCodes} chain
|
|
169
|
+
* so undici's generic "fetch failed" is recognised as a connection failure.
|
|
170
|
+
*/
|
|
171
|
+
export function classifyImplicitSemanticFallbackReason(error) {
|
|
172
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
173
|
+
const causeCodes = collectErrorCauseCodes(error);
|
|
174
|
+
const haystack = `${message} ${causeCodes}`;
|
|
175
|
+
if (haystack.includes("timed out") ||
|
|
176
|
+
haystack.includes("timeout") ||
|
|
177
|
+
haystack.includes("etimedout")) {
|
|
178
|
+
return "timeout";
|
|
179
|
+
}
|
|
180
|
+
if (haystack.includes("econnrefused") ||
|
|
181
|
+
haystack.includes("connection refused") ||
|
|
182
|
+
haystack.includes("connect ") ||
|
|
183
|
+
haystack.includes("enotfound") ||
|
|
184
|
+
haystack.includes("eai_again") ||
|
|
185
|
+
haystack.includes("econnreset") ||
|
|
186
|
+
haystack.includes("fetch failed")) {
|
|
187
|
+
return "connection";
|
|
188
|
+
}
|
|
189
|
+
return "error";
|
|
190
|
+
}
|
|
191
|
+
// Explicit --semantic/--hybrid searches must never hard-fail an agent when the
|
|
192
|
+
// embedding/vector backend is unreachable or unconfigured: degrade to keyword
|
|
193
|
+
// search and surface a machine-readable warning instead of an unknown_error.
|
|
194
|
+
function buildExplicitSemanticFallbackWarning(requestedMode, error) {
|
|
195
|
+
const reason = classifyImplicitSemanticFallbackReason(error);
|
|
196
|
+
return `search_${requestedMode}_fallback:${reason}:using_keyword_mode`;
|
|
197
|
+
}
|
|
198
|
+
/** Compare the vectorization-status ledger against the filtered corpus and, when the vector index is behind, emit a one-line stderr warning plus a structured `vector_index_stale:N` entry in the JSON warnings array. Best-effort: ledger read failures fall through silently — the existing semantic/hybrid fallback paths cover backend errors. */
|
|
199
|
+
async function maybeEmitVectorIndexStaleWarning(pmRoot, filteredDocuments, warnings) {
|
|
200
|
+
try {
|
|
201
|
+
const ledger = await readVectorizationStatusLedger(pmRoot);
|
|
202
|
+
if (ledger.warnings.length > 0) {
|
|
203
|
+
warnings.push(...ledger.warnings);
|
|
204
|
+
}
|
|
205
|
+
const staleIds = collectStaleVectorizationIds(filteredDocuments.map((document) => ({
|
|
206
|
+
id: document.metadata.id,
|
|
207
|
+
updated_at: document.metadata.updated_at,
|
|
208
|
+
})), ledger.entries);
|
|
209
|
+
if (staleIds.length === 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
warnings.push(`vector_index_stale:${staleIds.length}`);
|
|
213
|
+
/* c8 ignore start -- singular/plural warning text branches are cosmetic and validated in integration UX tests */
|
|
214
|
+
process.stderr.write(`[pm] warning: ${staleIds.length} item${staleIds.length === 1 ? " is" : "s are"} new or modified since the last reindex and ${staleIds.length === 1 ? "is" : "are"} NOT in the semantic index yet — they will be missing from semantic/hybrid results until you run 'pm reindex --mode hybrid'. (Write-time embedding is governed by search.mutation_refresh_policy; staleness means the embed was skipped, failed, or the backend was unreachable.)\n`);
|
|
215
|
+
/* c8 ignore stop */
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Best-effort: missing/unreadable ledger is not a query-blocking concern.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function parseMode(raw, _context) {
|
|
222
|
+
if (raw === undefined) {
|
|
223
|
+
return "keyword";
|
|
224
|
+
}
|
|
225
|
+
const normalized = raw.trim().toLowerCase();
|
|
226
|
+
if (normalized !== "keyword" &&
|
|
227
|
+
normalized !== "semantic" &&
|
|
228
|
+
normalized !== "hybrid") {
|
|
229
|
+
throw new PmCliError("Search mode must be one of keyword|semantic|hybrid", EXIT_CODE.USAGE);
|
|
230
|
+
}
|
|
231
|
+
return normalized;
|
|
232
|
+
}
|
|
233
|
+
function parseIncludeLinked(raw) {
|
|
234
|
+
return raw === true;
|
|
235
|
+
}
|
|
236
|
+
function parseTitleExact(raw) {
|
|
237
|
+
return raw === true;
|
|
238
|
+
}
|
|
239
|
+
function parsePhraseExact(raw) {
|
|
240
|
+
return raw === true;
|
|
241
|
+
}
|
|
242
|
+
function parseSemanticWeightOverride(raw) {
|
|
243
|
+
return coerceNumberInRange(raw, 0, 1) ?? undefined;
|
|
244
|
+
}
|
|
245
|
+
function parseMatchMode(raw) {
|
|
246
|
+
if (raw === undefined) {
|
|
247
|
+
return "or";
|
|
248
|
+
}
|
|
249
|
+
const normalized = raw.trim().toLowerCase();
|
|
250
|
+
if (normalized !== "and" && normalized !== "or" && normalized !== "exact") {
|
|
251
|
+
throw new PmCliError("Search --match-mode must be one of and|or|exact", EXIT_CODE.USAGE);
|
|
252
|
+
}
|
|
253
|
+
return normalized;
|
|
254
|
+
}
|
|
255
|
+
// Per-query --min-score overrides the persistent search.score_threshold for this
|
|
256
|
+
// query only. Accepts a finite number >= 0; anything else is a usage error.
|
|
257
|
+
function parseMinScoreOverride(raw) {
|
|
258
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
/* c8 ignore start -- numeric-vs-string coercion branch is exercised via integration CLI parsing */
|
|
262
|
+
const parsed = typeof raw === "number" ? raw : Number(String(raw).trim());
|
|
263
|
+
/* c8 ignore stop */
|
|
264
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
265
|
+
throw new PmCliError("Search --min-score must be a finite number >= 0", EXIT_CODE.USAGE);
|
|
266
|
+
}
|
|
267
|
+
return parsed;
|
|
268
|
+
}
|
|
269
|
+
// updated/created date-window filters share the deadline ISO+relative resolver
|
|
270
|
+
// so `pm search` matches `pm list` semantics exactly: pass an ISO timestamp or a
|
|
271
|
+
// SIGNED relative offset ("-2h"/"-7d" reach into the past, "+1d" the future;
|
|
272
|
+
// units h/d/w/m, m = months — there is no minutes unit).
|
|
273
|
+
function parseTimestampWindow(raw, fieldLabel) {
|
|
274
|
+
if (raw == null)
|
|
275
|
+
return undefined;
|
|
276
|
+
const value = String(raw).trim();
|
|
277
|
+
if (value.length === 0)
|
|
278
|
+
return undefined;
|
|
279
|
+
return resolveIsoOrRelative(value, new Date(), fieldLabel);
|
|
280
|
+
}
|
|
281
|
+
function normalizeSearchPhrase(value) {
|
|
282
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
283
|
+
}
|
|
284
|
+
function parseDeadline(raw, fieldLabel) {
|
|
285
|
+
if (raw === undefined)
|
|
286
|
+
return undefined;
|
|
287
|
+
return resolveIsoOrRelative(raw, new Date(), fieldLabel);
|
|
288
|
+
}
|
|
289
|
+
function parseFieldSelectors(raw) {
|
|
290
|
+
if (raw === undefined) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
const selectors = raw
|
|
294
|
+
.split(",")
|
|
295
|
+
.map((entry) => entry.trim())
|
|
296
|
+
.filter((entry) => entry.length > 0);
|
|
297
|
+
if (selectors.length === 0) {
|
|
298
|
+
throw new PmCliError("Search --fields requires a comma-separated list of field names", EXIT_CODE.USAGE);
|
|
299
|
+
}
|
|
300
|
+
return [...new Set(selectors)];
|
|
301
|
+
}
|
|
302
|
+
function parseProjectionConfig(options) {
|
|
303
|
+
const compactRequested = options.compact === true;
|
|
304
|
+
const fullRequested = options.full === true;
|
|
305
|
+
const fieldSelectors = parseFieldSelectors(options.fields);
|
|
306
|
+
const enabledModes = Number(compactRequested) +
|
|
307
|
+
Number(fullRequested) +
|
|
308
|
+
Number(fieldSelectors !== undefined);
|
|
309
|
+
if (enabledModes > 1) {
|
|
310
|
+
throw new PmCliError("Search projection options are mutually exclusive. Use one of --compact, --full, or --fields.", EXIT_CODE.USAGE);
|
|
311
|
+
}
|
|
312
|
+
if (compactRequested) {
|
|
313
|
+
return {
|
|
314
|
+
mode: "compact",
|
|
315
|
+
fields: [...DEFAULT_COMPACT_SEARCH_FIELDS],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (fullRequested) {
|
|
319
|
+
return {
|
|
320
|
+
mode: "full",
|
|
321
|
+
fields: [],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (fieldSelectors) {
|
|
325
|
+
return {
|
|
326
|
+
mode: "fields",
|
|
327
|
+
fields: fieldSelectors,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
mode: "full",
|
|
332
|
+
fields: [],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/** Public contract for test only search command, shared by SDK and presentation-layer consumers. */
|
|
336
|
+
export const _testOnlySearchCommand = {
|
|
337
|
+
applyFilters,
|
|
338
|
+
applyExactQueryFilters,
|
|
339
|
+
applyInlineQueryFilters,
|
|
340
|
+
buildHitHighlights,
|
|
341
|
+
buildVerboseSearchFilters,
|
|
342
|
+
buildExplicitSemanticFallbackWarning,
|
|
343
|
+
buildCompactSearchFilterSummary,
|
|
344
|
+
buildHybridLexicalScore,
|
|
345
|
+
buildRerankCorpus,
|
|
346
|
+
buildSemanticHits,
|
|
347
|
+
classifyImplicitSemanticFallbackReason,
|
|
348
|
+
collectErrorCauseCodes,
|
|
349
|
+
collectExactPhraseFields,
|
|
350
|
+
collectLinkedPaths,
|
|
351
|
+
combineHybridHits,
|
|
352
|
+
computeBuiltInBm25Hits,
|
|
353
|
+
computeSemanticOrHybridHits,
|
|
354
|
+
resolveBuiltInBm25Mode,
|
|
355
|
+
countOccurrences,
|
|
356
|
+
dependencyEntries,
|
|
357
|
+
documentContainsExactPhrase,
|
|
358
|
+
emptySearchResult,
|
|
359
|
+
highlightFieldSnippet,
|
|
360
|
+
loadDocuments,
|
|
361
|
+
markTokenRuns,
|
|
362
|
+
parseInlineQueryFilters,
|
|
363
|
+
maybeEmitVectorIndexStaleWarning,
|
|
364
|
+
loadLinkedCorpus,
|
|
365
|
+
mergeVectorHitsById,
|
|
366
|
+
normalizeExtensionProviderHits,
|
|
367
|
+
normalizeScoreMap,
|
|
368
|
+
parseProjectionConfig,
|
|
369
|
+
parseTimestampWindow,
|
|
370
|
+
parseTokens,
|
|
371
|
+
projectSearchHits,
|
|
372
|
+
readSearchFieldValue,
|
|
373
|
+
requireSemanticDependencies,
|
|
374
|
+
resolveExtensionSearchProvider,
|
|
375
|
+
resolveExtensionSearchProviderByName,
|
|
376
|
+
resolveExtensionVectorAdapter,
|
|
377
|
+
resolveLinkedCorpusRoots,
|
|
378
|
+
scoreDocument,
|
|
379
|
+
sortHits,
|
|
380
|
+
stringArray,
|
|
381
|
+
textEntries,
|
|
382
|
+
validateSearchProjectionFields,
|
|
383
|
+
};
|
|
384
|
+
/* c8 ignore start -- projection/runtime-field validation edge permutations are covered by integration query-contract tests */
|
|
385
|
+
function validateSearchProjectionFields(projection, runtimeFieldRegistry) {
|
|
386
|
+
if (projection.mode !== "fields") {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const runtimeKeys = new Set(runtimeFieldRegistry.definitions.flatMap((field) => [
|
|
390
|
+
field.key,
|
|
391
|
+
field.metadata_key,
|
|
392
|
+
]));
|
|
393
|
+
const unknown = projection.fields.filter((field) => {
|
|
394
|
+
const normalized = field.trim();
|
|
395
|
+
const itemKey = normalized.startsWith("item.")
|
|
396
|
+
? normalized.slice("item.".length)
|
|
397
|
+
: normalized;
|
|
398
|
+
return (!SEARCH_HIT_FIELD_KEYS.has(normalized) &&
|
|
399
|
+
!SEARCH_ITEM_FIELD_KEYS.has(itemKey) &&
|
|
400
|
+
!runtimeKeys.has(itemKey));
|
|
401
|
+
});
|
|
402
|
+
if (unknown.length > 0) {
|
|
403
|
+
throw new PmCliError(`Unknown search --fields value(s): ${unknown.join(", ")}`, EXIT_CODE.USAGE, {
|
|
404
|
+
examples: [
|
|
405
|
+
"pm search <query> --fields id,title,status,score",
|
|
406
|
+
"pm search <query> --fields id,title,item.description,matched_fields",
|
|
407
|
+
],
|
|
408
|
+
nextSteps: [
|
|
409
|
+
"Use item.<field> for explicit item metadata fields, or run pm search --help for projection examples.",
|
|
410
|
+
],
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/* c8 ignore stop */
|
|
415
|
+
function parseTokens(query) {
|
|
416
|
+
const normalized = normalizeSearchPhrase(query);
|
|
417
|
+
if (!normalized) {
|
|
418
|
+
throw new PmCliError("Search query must not be empty", EXIT_CODE.USAGE);
|
|
419
|
+
}
|
|
420
|
+
return normalized.split(/\s+/).filter(Boolean);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Split a raw search query into its residual keyword text and any inline
|
|
424
|
+
* `field:value` filter tokens (GH-157). Recognized fields are
|
|
425
|
+
* {@link INLINE_QUERY_FILTER_FIELDS}; the value runs to the end of the token so
|
|
426
|
+
* colon-bearing values like `tag:area:search` parse as `{ tag: "area:search" }`.
|
|
427
|
+
* Only the first occurrence of each field is captured — later duplicates are left
|
|
428
|
+
* in the residual query so they are never silently dropped. Tokens whose prefix
|
|
429
|
+
* is not a recognized field (`foo:bar`) stay in the residual query verbatim.
|
|
430
|
+
*/
|
|
431
|
+
function parseInlineQueryFilters(query) {
|
|
432
|
+
const inlineFilters = {};
|
|
433
|
+
const residualTokens = [];
|
|
434
|
+
for (const token of query.split(/\s+/).filter((entry) => entry.length > 0)) {
|
|
435
|
+
const separatorIndex = token.indexOf(":");
|
|
436
|
+
const field = separatorIndex > 0 ? token.slice(0, separatorIndex).toLowerCase() : "";
|
|
437
|
+
const value = separatorIndex > 0 ? token.slice(separatorIndex + 1) : "";
|
|
438
|
+
const matchedField = INLINE_QUERY_FILTER_FIELDS.includes(field)
|
|
439
|
+
? field
|
|
440
|
+
: undefined;
|
|
441
|
+
if (matchedField &&
|
|
442
|
+
value.length > 0 &&
|
|
443
|
+
inlineFilters[matchedField] === undefined) {
|
|
444
|
+
inlineFilters[matchedField] = value;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
residualTokens.push(token);
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
residualQuery: residualTokens.join(" "),
|
|
451
|
+
inlineFilters,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/** Merge inline `field:value` filters into a search options object (GH-157). Explicit `--field` flags always win: an inline token is applied only when the corresponding option is not already set, and a conflicting inline token is recorded as a `search_inline_filter_ignored:<field>:flag_takes_precedence` warning so the override is observable rather than silent. Returns a fresh options object — the caller's input is never mutated. */
|
|
455
|
+
function applyInlineQueryFilters(options, inlineFilters, warnings) {
|
|
456
|
+
const merged = { ...options };
|
|
457
|
+
for (const field of INLINE_QUERY_FILTER_FIELDS) {
|
|
458
|
+
const inlineValue = inlineFilters[field];
|
|
459
|
+
if (inlineValue === undefined) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (toNonEmptyStringOrUndefined(merged[field]) !== undefined) {
|
|
463
|
+
warnings.push(`search_inline_filter_ignored:${field}:flag_takes_precedence`);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
merged[field] = inlineValue;
|
|
467
|
+
}
|
|
468
|
+
return merged;
|
|
469
|
+
}
|
|
470
|
+
function stringArray(value) {
|
|
471
|
+
return Array.isArray(value)
|
|
472
|
+
? value.filter((entry) => typeof entry === "string")
|
|
473
|
+
: [];
|
|
474
|
+
}
|
|
475
|
+
function textEntries(value) {
|
|
476
|
+
return Array.isArray(value)
|
|
477
|
+
? value.filter((entry) => typeof entry === "object" &&
|
|
478
|
+
entry !== null &&
|
|
479
|
+
typeof entry.text === "string")
|
|
480
|
+
: [];
|
|
481
|
+
}
|
|
482
|
+
function dependencyEntries(value) {
|
|
483
|
+
return Array.isArray(value)
|
|
484
|
+
? value.filter((entry) => typeof entry === "object" &&
|
|
485
|
+
entry !== null &&
|
|
486
|
+
typeof entry.id === "string" &&
|
|
487
|
+
typeof entry.kind === "string")
|
|
488
|
+
: [];
|
|
489
|
+
}
|
|
490
|
+
function collectExactPhraseFields(document) {
|
|
491
|
+
const item = document.metadata;
|
|
492
|
+
return [
|
|
493
|
+
item.title,
|
|
494
|
+
item.description,
|
|
495
|
+
item.status,
|
|
496
|
+
stringArray(item.tags).join(" "),
|
|
497
|
+
document.body,
|
|
498
|
+
textEntries(item.comments)
|
|
499
|
+
.map((entry) => entry.text)
|
|
500
|
+
.join(" "),
|
|
501
|
+
textEntries(item.notes)
|
|
502
|
+
.map((entry) => entry.text)
|
|
503
|
+
.join(" "),
|
|
504
|
+
textEntries(item.learnings)
|
|
505
|
+
.map((entry) => entry.text)
|
|
506
|
+
.join(" "),
|
|
507
|
+
buildReminderCorpus(item).join(" "),
|
|
508
|
+
buildEventCorpus(item).join(" "),
|
|
509
|
+
dependencyEntries(item.dependencies)
|
|
510
|
+
.map((entry) => `${entry.id} ${entry.kind}`)
|
|
511
|
+
.join(" "),
|
|
512
|
+
buildPlanFlatCorpus(item),
|
|
513
|
+
];
|
|
514
|
+
}
|
|
515
|
+
function documentContainsExactPhrase(document, normalizedQuery) {
|
|
516
|
+
return collectExactPhraseFields(document).some((fieldValue) => normalizeSearchPhrase(fieldValue).includes(normalizedQuery));
|
|
517
|
+
}
|
|
518
|
+
function applyExactQueryFilters(items, normalizedQuery, options) {
|
|
519
|
+
if (!options.titleExact && !options.phraseExact) {
|
|
520
|
+
return items;
|
|
521
|
+
}
|
|
522
|
+
return items.filter((document) => {
|
|
523
|
+
if (options.titleExact &&
|
|
524
|
+
normalizeSearchPhrase(document.metadata.title) !== normalizedQuery) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
if (options.phraseExact &&
|
|
528
|
+
!documentContainsExactPhrase(document, normalizedQuery)) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
return true;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
function assertSearchAssigneeFilter(assigneeFilter) {
|
|
535
|
+
// Match pm list: --assignee no longer accepts none/null (unassigned filtering
|
|
536
|
+
// belongs to a dedicated flag there; pm search has no presence flag so reject
|
|
537
|
+
// the sentinel values explicitly rather than silently matching a literal).
|
|
538
|
+
if (assigneeFilter &&
|
|
539
|
+
(assigneeFilter.toLowerCase() === "none" ||
|
|
540
|
+
assigneeFilter.toLowerCase() === "null")) {
|
|
541
|
+
throw new PmCliError('--assignee no longer accepts "none" or "null".', EXIT_CODE.USAGE);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function resolveSearchMetadataFilterSet(options, typeRegistry, statusFilter) {
|
|
545
|
+
const assigneeFilter = options.assignee?.trim();
|
|
546
|
+
assertSearchAssigneeFilter(assigneeFilter);
|
|
547
|
+
const missingMetadataFilters = resolveMissingMetadataFilters(options);
|
|
548
|
+
const contentFieldFilters = resolveContentFieldFilters(options);
|
|
549
|
+
return {
|
|
550
|
+
typeFilter: parseType(options.type, typeRegistry),
|
|
551
|
+
tagFilter: options.tag?.trim().toLowerCase(),
|
|
552
|
+
priorityFilter: parsePriority(options.priority),
|
|
553
|
+
deadlineBefore: parseDeadline(options.deadlineBefore, "deadline-before"),
|
|
554
|
+
deadlineAfter: parseDeadline(options.deadlineAfter, "deadline-after"),
|
|
555
|
+
updatedAfter: parseTimestampWindow(options.updatedAfter, "updated-after"),
|
|
556
|
+
updatedBefore: parseTimestampWindow(options.updatedBefore, "updated-before"),
|
|
557
|
+
createdAfter: parseTimestampWindow(options.createdAfter, "created-after"),
|
|
558
|
+
createdBefore: parseTimestampWindow(options.createdBefore, "created-before"),
|
|
559
|
+
assigneeFilter,
|
|
560
|
+
sprintFilter: options.sprint?.trim(),
|
|
561
|
+
releaseFilter: options.release?.trim(),
|
|
562
|
+
parentFilter: options.parent?.trim(),
|
|
563
|
+
statusSet: statusFilter && statusFilter.length > 0
|
|
564
|
+
? new Set(statusFilter)
|
|
565
|
+
: undefined,
|
|
566
|
+
missingMetadataFilters,
|
|
567
|
+
missingMetadataActive: hasMissingMetadataFilter(missingMetadataFilters),
|
|
568
|
+
contentFieldFilters,
|
|
569
|
+
contentFiltersActive: hasContentFieldFilter(contentFieldFilters),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function matchesScalarSearchFilters(item, filters) {
|
|
573
|
+
return (matchesIdentitySearchFilters(item, filters) &&
|
|
574
|
+
matchesTimestampSearchFilters(item, filters) &&
|
|
575
|
+
matchesOwnerSearchFilters(item, filters));
|
|
576
|
+
}
|
|
577
|
+
function matchesIdentitySearchFilters(item, filters) {
|
|
578
|
+
if (filters.statusSet && !filters.statusSet.has(item.status))
|
|
579
|
+
return false;
|
|
580
|
+
if (filters.typeFilter && item.type !== filters.typeFilter)
|
|
581
|
+
return false;
|
|
582
|
+
if (filters.tagFilter && !stringArray(item.tags).includes(filters.tagFilter))
|
|
583
|
+
return false;
|
|
584
|
+
if (filters.priorityFilter !== undefined &&
|
|
585
|
+
item.priority !== filters.priorityFilter)
|
|
586
|
+
return false;
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
function matchesTimestampSearchFilters(item, filters) {
|
|
590
|
+
return matchesTimestampFilters(item, filters);
|
|
591
|
+
}
|
|
592
|
+
function matchesOwnerSearchFilters(item, filters) {
|
|
593
|
+
if (filters.assigneeFilter !== undefined &&
|
|
594
|
+
item.assignee !== filters.assigneeFilter)
|
|
595
|
+
return false;
|
|
596
|
+
if (filters.sprintFilter !== undefined &&
|
|
597
|
+
item.sprint !== filters.sprintFilter)
|
|
598
|
+
return false;
|
|
599
|
+
/* c8 ignore start -- release/parent metadata filter combinations are covered by integration search fixtures */
|
|
600
|
+
if (filters.releaseFilter !== undefined &&
|
|
601
|
+
item.release !== filters.releaseFilter)
|
|
602
|
+
return false;
|
|
603
|
+
if (filters.parentFilter !== undefined &&
|
|
604
|
+
item.parent !== filters.parentFilter)
|
|
605
|
+
return false;
|
|
606
|
+
/* c8 ignore stop */
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
function matchesSearchMetadataFilters(document, filters, runtimeFieldFilters, lifecycleClassifier) {
|
|
610
|
+
const item = document.metadata;
|
|
611
|
+
if (!matchesScalarSearchFilters(item, filters)) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
if (!matchesRuntimeFilters(item, runtimeFieldFilters)) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
if (filters.missingMetadataActive &&
|
|
618
|
+
!itemMatchesMissingMetadata(item, filters.missingMetadataFilters, lifecycleClassifier)) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
if (filters.contentFiltersActive &&
|
|
622
|
+
!itemMatchesContentFilters(item, filters.contentFieldFilters)) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
function applyFilters(items, options, typeRegistry, runtimeFieldFilters, statusFilter, lifecycleClassifier) {
|
|
628
|
+
const filters = resolveSearchMetadataFilterSet(options, typeRegistry, statusFilter);
|
|
629
|
+
return items.filter((document) => matchesSearchMetadataFilters(document, filters, runtimeFieldFilters, lifecycleClassifier));
|
|
630
|
+
}
|
|
631
|
+
function countOccurrences(haystack, needle) {
|
|
632
|
+
let count = 0;
|
|
633
|
+
let index = 0;
|
|
634
|
+
while (true) {
|
|
635
|
+
const foundAt = haystack.indexOf(needle, index);
|
|
636
|
+
if (foundAt < 0) {
|
|
637
|
+
return count;
|
|
638
|
+
}
|
|
639
|
+
count += 1;
|
|
640
|
+
index = foundAt + needle.length;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function tokenizeForExactTokenMatch(value) {
|
|
644
|
+
return tokenizeAlphaNumeric(value);
|
|
645
|
+
}
|
|
646
|
+
function collectLinkedPaths(item) {
|
|
647
|
+
const fromFiles = (item.files ?? []).map((entry) => ({
|
|
648
|
+
scope: entry.scope,
|
|
649
|
+
path: entry.path.trim(),
|
|
650
|
+
}));
|
|
651
|
+
const fromDocs = (item.docs ?? []).map((entry) => ({
|
|
652
|
+
scope: entry.scope,
|
|
653
|
+
path: entry.path.trim(),
|
|
654
|
+
}));
|
|
655
|
+
const fromTests = (item.tests ?? [])
|
|
656
|
+
.filter((entry) => typeof entry.path === "string" && entry.path.trim().length > 0)
|
|
657
|
+
.map((entry) => ({
|
|
658
|
+
scope: entry.scope,
|
|
659
|
+
path: entry.path.trim(),
|
|
660
|
+
}));
|
|
661
|
+
const sorted = [...fromFiles, ...fromDocs, ...fromTests]
|
|
662
|
+
.filter((entry) => entry.path.length > 0)
|
|
663
|
+
.sort((a, b) => a.scope.localeCompare(b.scope) || a.path.localeCompare(b.path));
|
|
664
|
+
const deduped = new Map();
|
|
665
|
+
for (const entry of sorted) {
|
|
666
|
+
deduped.set(`${entry.scope}:${entry.path}`, entry);
|
|
667
|
+
}
|
|
668
|
+
return [...deduped.values()];
|
|
669
|
+
}
|
|
670
|
+
async function resolveContainmentRoot(root) {
|
|
671
|
+
const resolved = path.resolve(root);
|
|
672
|
+
try {
|
|
673
|
+
const realpathRoot = await fs.realpath(resolved);
|
|
674
|
+
return {
|
|
675
|
+
resolved,
|
|
676
|
+
realpath: realpathRoot,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async function resolveLinkedCorpusRoots(projectRoot, globalRoot) {
|
|
684
|
+
const [projectContainmentRoot, globalContainmentRoot] = await Promise.all([
|
|
685
|
+
resolveContainmentRoot(projectRoot),
|
|
686
|
+
resolveContainmentRoot(globalRoot),
|
|
687
|
+
]);
|
|
688
|
+
return {
|
|
689
|
+
projectContainmentRoot,
|
|
690
|
+
globalContainmentRoot,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
async function loadLinkedCorpus(document, roots) {
|
|
694
|
+
const linkedPaths = collectLinkedPaths(document.metadata);
|
|
695
|
+
const chunks = [];
|
|
696
|
+
for (const linkedPath of linkedPaths) {
|
|
697
|
+
const containmentRoot = linkedPath.scope === "global"
|
|
698
|
+
? roots.globalContainmentRoot
|
|
699
|
+
: roots.projectContainmentRoot;
|
|
700
|
+
if (!containmentRoot) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const resolved = path.resolve(containmentRoot.resolved, linkedPath.path);
|
|
704
|
+
if (!isPathWithinDirectory(containmentRoot.resolved, resolved)) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
let linkedRealpath;
|
|
708
|
+
try {
|
|
709
|
+
linkedRealpath = await fs.realpath(resolved);
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (!isPathWithinDirectory(containmentRoot.realpath, linkedRealpath)) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
await runActiveOnReadHooks({
|
|
719
|
+
path: resolved,
|
|
720
|
+
scope: linkedPath.scope,
|
|
721
|
+
});
|
|
722
|
+
chunks.push(await fs.readFile(resolved, "utf8"));
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
// Best-effort linked-content indexing: unreadable paths are ignored.
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return chunks.join("\n");
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Canonical definition of the document-derived searchable fields shared by the
|
|
732
|
+
* lexical scorer ({@link scoreDocument}) and the matched-text highlighter
|
|
733
|
+
* ({@link buildHitHighlights}). Keeping a single source of field name → text
|
|
734
|
+
* extractor → tuning weight guarantees the highlighter can only ever produce
|
|
735
|
+
* snippets for fields the scorer actually inspected. The `linked_content` field
|
|
736
|
+
* is appended separately by the scorer because its text is supplied per-query
|
|
737
|
+
* rather than derived from the document.
|
|
738
|
+
*/
|
|
739
|
+
const SEARCHABLE_FIELD_BUILDERS = [
|
|
740
|
+
{
|
|
741
|
+
name: "title",
|
|
742
|
+
weightKey: "title_weight",
|
|
743
|
+
value: (document) => document.metadata.title,
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: "description",
|
|
747
|
+
weightKey: "description_weight",
|
|
748
|
+
value: (document) => document.metadata.description,
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
name: "tags",
|
|
752
|
+
weightKey: "tags_weight",
|
|
753
|
+
value: (document) => stringArray(document.metadata.tags).join(" "),
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: "status",
|
|
757
|
+
weightKey: "status_weight",
|
|
758
|
+
value: (document) => typeof document.metadata.status === "string"
|
|
759
|
+
? document.metadata.status
|
|
760
|
+
: "",
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
name: "body",
|
|
764
|
+
weightKey: "body_weight",
|
|
765
|
+
value: (document) => document.body,
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: "comments",
|
|
769
|
+
weightKey: "comments_weight",
|
|
770
|
+
value: (document) => textEntries(document.metadata.comments)
|
|
771
|
+
.map((entry) => entry.text)
|
|
772
|
+
.join(" "),
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: "notes",
|
|
776
|
+
weightKey: "notes_weight",
|
|
777
|
+
value: (document) => textEntries(document.metadata.notes)
|
|
778
|
+
.map((entry) => entry.text)
|
|
779
|
+
.join(" "),
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
name: "learnings",
|
|
783
|
+
weightKey: "learnings_weight",
|
|
784
|
+
value: (document) => textEntries(document.metadata.learnings)
|
|
785
|
+
.map((entry) => entry.text)
|
|
786
|
+
.join(" "),
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
name: "reminders",
|
|
790
|
+
weightKey: "reminders_weight",
|
|
791
|
+
value: (document) => buildReminderCorpus(document.metadata).join(" "),
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
name: "events",
|
|
795
|
+
weightKey: "events_weight",
|
|
796
|
+
value: (document) => buildEventCorpus(document.metadata).join(" "),
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: "dependencies",
|
|
800
|
+
weightKey: "dependencies_weight",
|
|
801
|
+
value: (document) => dependencyEntries(document.metadata.dependencies)
|
|
802
|
+
.map((entry) => `${entry.id} ${entry.kind}`)
|
|
803
|
+
.join(" "),
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
name: "plan",
|
|
807
|
+
weightKey: "body_weight",
|
|
808
|
+
value: (document) => buildPlanFlatCorpus(document.metadata),
|
|
809
|
+
},
|
|
810
|
+
];
|
|
811
|
+
// Name → builder index so the highlighter can resolve a matched field to its
|
|
812
|
+
// text extractor in O(1) and evaluate ONLY the matched fields' values, instead
|
|
813
|
+
// of materializing all twelve (several of which join/build corpora) per hit.
|
|
814
|
+
const SEARCHABLE_FIELD_BUILDER_BY_NAME = new Map(SEARCHABLE_FIELD_BUILDERS.map((builder) => [builder.name, builder]));
|
|
815
|
+
function escapeRegExpLiteral(value) {
|
|
816
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Wrap every case-insensitive occurrence of any query token in `text` with the
|
|
820
|
+
* {@link HIGHLIGHT_OPEN}/{@link HIGHLIGHT_CLOSE} markers. Token boundaries follow
|
|
821
|
+
* the same substring semantics as the lexical scorer (a token highlights inside
|
|
822
|
+
* longer words, matching how it scored), and overlapping matches are coalesced
|
|
823
|
+
* by the single combined alternation so markers never nest.
|
|
824
|
+
*/
|
|
825
|
+
function markTokenRuns(text, tokens) {
|
|
826
|
+
// Sort by length descending before building the alternation so a longer token
|
|
827
|
+
// wins over a shorter token that is its prefix (regex alternation is greedy
|
|
828
|
+
// left-to-right, so `auth|authority` would mark «auth»ority instead of
|
|
829
|
+
// «authority»). `.filter` already returns a fresh array, so the in-place sort
|
|
830
|
+
// never mutates the caller's token list.
|
|
831
|
+
const escaped = tokens
|
|
832
|
+
.filter((token) => token.length > 0)
|
|
833
|
+
.sort((left, right) => right.length - left.length)
|
|
834
|
+
.map(escapeRegExpLiteral);
|
|
835
|
+
if (escaped.length === 0) {
|
|
836
|
+
return text;
|
|
837
|
+
}
|
|
838
|
+
const pattern = new RegExp(escaped.join("|"), "gi");
|
|
839
|
+
return text.replace(pattern, (match) => `${HIGHLIGHT_OPEN}${match}${HIGHLIGHT_CLOSE}`);
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Build a single highlighted snippet for one field value (GH-157), or null when
|
|
843
|
+
* no token matches. The snippet is a window of {@link HIGHLIGHT_SNIPPET_RADIUS}
|
|
844
|
+
* characters on each side of the first match with the matching token runs
|
|
845
|
+
* wrapped in markers, prefixed/suffixed with an ellipsis when the field text was
|
|
846
|
+
* trimmed. Returning null lets the caller skip fields with no concrete textual
|
|
847
|
+
* match (e.g. a field flagged purely by the phrase-coverage bonus).
|
|
848
|
+
*/
|
|
849
|
+
function highlightFieldSnippet(text, tokens) {
|
|
850
|
+
if (text.length === 0) {
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
const lowerText = text.toLowerCase();
|
|
854
|
+
let firstMatchIndex = -1;
|
|
855
|
+
let firstMatchLength = 0;
|
|
856
|
+
for (const token of tokens) {
|
|
857
|
+
if (token.length === 0) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
const index = lowerText.indexOf(token);
|
|
861
|
+
if (index >= 0 && (firstMatchIndex < 0 || index < firstMatchIndex)) {
|
|
862
|
+
firstMatchIndex = index;
|
|
863
|
+
firstMatchLength = token.length;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (firstMatchIndex < 0) {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
const windowStart = Math.max(0, firstMatchIndex - HIGHLIGHT_SNIPPET_RADIUS);
|
|
870
|
+
const windowEnd = Math.min(text.length, firstMatchIndex + firstMatchLength + HIGHLIGHT_SNIPPET_RADIUS);
|
|
871
|
+
const marked = markTokenRuns(text.slice(windowStart, windowEnd), tokens);
|
|
872
|
+
const prefix = windowStart > 0 ? "…" : "";
|
|
873
|
+
const suffix = windowEnd < text.length ? "…" : "";
|
|
874
|
+
return `${prefix}${marked}${suffix}`;
|
|
875
|
+
}
|
|
876
|
+
/** Produce per-field matched-text snippets for a hit (GH-157, `--highlight`). Iterates the hit's already-sorted `matched_fields`, retains only the document-derived searchable fields (skipping synthetic markers like `id`, `semantic`, `rerank`, and `linked_content`), and emits a snippet for each field that still contains a concrete token match. */
|
|
877
|
+
function buildHitHighlights(document, matchedFields, tokens) {
|
|
878
|
+
const highlights = [];
|
|
879
|
+
for (const field of matchedFields) {
|
|
880
|
+
const builder = SEARCHABLE_FIELD_BUILDER_BY_NAME.get(field);
|
|
881
|
+
if (builder === undefined) {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const snippet = highlightFieldSnippet(builder.value(document), tokens);
|
|
885
|
+
if (snippet !== null) {
|
|
886
|
+
highlights.push({ field, snippet });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return highlights;
|
|
890
|
+
}
|
|
891
|
+
function buildSearchableScoringFields(document, linkedCorpus, tuning) {
|
|
892
|
+
return [
|
|
893
|
+
...SEARCHABLE_FIELD_BUILDERS.map((builder) => ({
|
|
894
|
+
name: builder.name,
|
|
895
|
+
value: builder.value(document),
|
|
896
|
+
weight: tuning[builder.weightKey],
|
|
897
|
+
})),
|
|
898
|
+
{
|
|
899
|
+
name: "linked_content",
|
|
900
|
+
value: linkedCorpus,
|
|
901
|
+
weight: tuning.linked_content_weight,
|
|
902
|
+
},
|
|
903
|
+
];
|
|
904
|
+
}
|
|
905
|
+
function buildExactIdSearchHit(item, normalizedQuery, idPrefix) {
|
|
906
|
+
const normalizedId = normalizeSearchPhrase(item.id);
|
|
907
|
+
const normalizedIdPrefix = typeof idPrefix === "string" ? idPrefix.trim().toLowerCase() : "";
|
|
908
|
+
const normalizedIdPrefixPhrase = normalizeSearchPhrase(normalizedIdPrefix);
|
|
909
|
+
const normalizedShortId = normalizedIdPrefixPhrase.length > 0 &&
|
|
910
|
+
normalizedId.startsWith(normalizedIdPrefixPhrase)
|
|
911
|
+
? normalizedId.slice(normalizedIdPrefixPhrase.length).trim()
|
|
912
|
+
: normalizedId;
|
|
913
|
+
if (normalizedQuery !== normalizedId &&
|
|
914
|
+
normalizedQuery !== normalizedShortId) {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
item,
|
|
919
|
+
score: normalizedQuery === normalizedId
|
|
920
|
+
? EXACT_ID_MATCH_SCORE
|
|
921
|
+
: SHORT_ID_MATCH_SCORE,
|
|
922
|
+
matched_fields: ["id"],
|
|
923
|
+
matched_all_terms: true,
|
|
924
|
+
exact_id_match: true,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function scoreTokenMatches(tokens, titleTokenCounts, searchableFields, tuning, state) {
|
|
928
|
+
for (const token of tokens) {
|
|
929
|
+
const exactTitleMatches = titleTokenCounts.get(token) ?? 0;
|
|
930
|
+
if (exactTitleMatches > 0) {
|
|
931
|
+
state.score += exactTitleMatches * tuning.title_exact_bonus;
|
|
932
|
+
state.matched.add("title");
|
|
933
|
+
state.matchedTokens.add(token);
|
|
934
|
+
}
|
|
935
|
+
for (const field of searchableFields) {
|
|
936
|
+
const occurrences = countOccurrences(field.value.toLowerCase(), token);
|
|
937
|
+
if (occurrences > 0) {
|
|
938
|
+
state.score += occurrences * field.weight;
|
|
939
|
+
state.matched.add(field.name);
|
|
940
|
+
state.matchedTokens.add(token);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function scorePhraseMatches(item, normalizedQuery, searchableFields, state) {
|
|
946
|
+
const normalizedTitle = normalizeSearchPhrase(item.title);
|
|
947
|
+
if (normalizedTitle === normalizedQuery) {
|
|
948
|
+
state.score += LONG_QUERY_TITLE_EXACT_BONUS;
|
|
949
|
+
state.matched.add("title");
|
|
950
|
+
}
|
|
951
|
+
for (const field of searchableFields) {
|
|
952
|
+
const phraseOccurrences = countOccurrences(normalizeSearchPhrase(field.value), normalizedQuery);
|
|
953
|
+
if (phraseOccurrences > 0) {
|
|
954
|
+
state.score +=
|
|
955
|
+
phraseOccurrences * field.weight * LONG_QUERY_PHRASE_MULTIPLIER;
|
|
956
|
+
state.matched.add(field.name);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function scoreDocument(document, tokens, normalizedQuery, linkedCorpus, tuning, idPrefix, applyCoverageBonus = false) {
|
|
961
|
+
const item = document.metadata;
|
|
962
|
+
const exactIdHit = buildExactIdSearchHit(item, normalizedQuery, idPrefix);
|
|
963
|
+
if (exactIdHit) {
|
|
964
|
+
return exactIdHit;
|
|
965
|
+
}
|
|
966
|
+
const titleTokenCounts = new Map();
|
|
967
|
+
for (const token of tokenizeForExactTokenMatch(item.title)) {
|
|
968
|
+
titleTokenCounts.set(token, (titleTokenCounts.get(token) ?? 0) + 1);
|
|
969
|
+
}
|
|
970
|
+
const searchableFields = buildSearchableScoringFields(document, linkedCorpus, tuning);
|
|
971
|
+
const state = {
|
|
972
|
+
score: 0,
|
|
973
|
+
matched: new Set(),
|
|
974
|
+
matchedTokens: new Set(),
|
|
975
|
+
};
|
|
976
|
+
scoreTokenMatches(tokens, titleTokenCounts, searchableFields, tuning, state);
|
|
977
|
+
const distinctTokens = new Set(tokens);
|
|
978
|
+
// matchedTokens only ever holds entries drawn from `tokens`, so it is always a
|
|
979
|
+
// subset of distinctTokens — exact size equality means every distinct term matched.
|
|
980
|
+
const matchedAllTerms = distinctTokens.size > 0 && state.matchedTokens.size === distinctTokens.size;
|
|
981
|
+
// GH-181 default-mode all-terms ranking bonus: only meaningful for multi-token
|
|
982
|
+
// queries where every distinct token matched somewhere.
|
|
983
|
+
if (applyCoverageBonus && distinctTokens.size > 1 && matchedAllTerms) {
|
|
984
|
+
state.score += ALL_TERMS_COVERAGE_BONUS;
|
|
985
|
+
}
|
|
986
|
+
const isLongPhraseQuery = tokens.length >= LONG_QUERY_TOKEN_THRESHOLD &&
|
|
987
|
+
normalizedQuery.includes(" ");
|
|
988
|
+
if (isLongPhraseQuery) {
|
|
989
|
+
scorePhraseMatches(item, normalizedQuery, searchableFields, state);
|
|
990
|
+
}
|
|
991
|
+
if (state.score <= 0) {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
item,
|
|
996
|
+
score: state.score,
|
|
997
|
+
matched_fields: [...state.matched].sort((a, b) => a.localeCompare(b)),
|
|
998
|
+
matched_all_terms: matchedAllTerms,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function sortHits(items, statusRegistry) {
|
|
1002
|
+
return [...items].sort((a, b) => {
|
|
1003
|
+
const byScore = b.score - a.score;
|
|
1004
|
+
if (byScore !== 0)
|
|
1005
|
+
return byScore;
|
|
1006
|
+
const aTerminal = isTerminalStatus(a.item.status, statusRegistry);
|
|
1007
|
+
const bTerminal = isTerminalStatus(b.item.status, statusRegistry);
|
|
1008
|
+
if (aTerminal !== bTerminal) {
|
|
1009
|
+
return aTerminal ? 1 : -1;
|
|
1010
|
+
}
|
|
1011
|
+
const byPriority = a.item.priority - b.item.priority;
|
|
1012
|
+
if (byPriority !== 0)
|
|
1013
|
+
return byPriority;
|
|
1014
|
+
const byUpdated = compareTimestampStrings(b.item.updated_at, a.item.updated_at);
|
|
1015
|
+
if (byUpdated !== 0)
|
|
1016
|
+
return byUpdated;
|
|
1017
|
+
return a.item.id.localeCompare(b.item.id);
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
function buildHybridLexicalScore(document, tokens, normalizedQuery, includeLinked, linkedCorpusById, tuning, idPrefix, applyCoverageBonus = false) {
|
|
1021
|
+
/* c8 ignore start -- linked corpus presence branch is covered by keyword/hybrid integration query tests */
|
|
1022
|
+
return scoreDocument(document, tokens, normalizedQuery, includeLinked ? (linkedCorpusById.get(document.metadata.id) ?? "") : "", tuning, idPrefix, applyCoverageBonus);
|
|
1023
|
+
/* c8 ignore stop */
|
|
1024
|
+
}
|
|
1025
|
+
function normalizeScoreMap(scoreById) {
|
|
1026
|
+
if (scoreById.size === 0) {
|
|
1027
|
+
return new Map();
|
|
1028
|
+
}
|
|
1029
|
+
const values = [...scoreById.values()];
|
|
1030
|
+
const minScore = Math.min(...values);
|
|
1031
|
+
const maxScore = Math.max(...values);
|
|
1032
|
+
if (maxScore === minScore) {
|
|
1033
|
+
return new Map([...scoreById.keys()].map((id) => [id, 1]));
|
|
1034
|
+
}
|
|
1035
|
+
const normalized = new Map();
|
|
1036
|
+
for (const [id, score] of scoreById) {
|
|
1037
|
+
normalized.set(id, (score - minScore) / (maxScore - minScore));
|
|
1038
|
+
}
|
|
1039
|
+
return normalized;
|
|
1040
|
+
}
|
|
1041
|
+
/** Implements resolve search max results for the public runtime surface of this module. */
|
|
1042
|
+
export function resolveSearchMaxResults(settings) {
|
|
1043
|
+
const candidate = settings.search
|
|
1044
|
+
?.max_results;
|
|
1045
|
+
if (typeof candidate === "number" &&
|
|
1046
|
+
Number.isFinite(candidate) &&
|
|
1047
|
+
candidate > 0) {
|
|
1048
|
+
return Math.floor(candidate);
|
|
1049
|
+
}
|
|
1050
|
+
return 50;
|
|
1051
|
+
}
|
|
1052
|
+
/** Implements resolve search score threshold for the public runtime surface of this module. */
|
|
1053
|
+
export function resolveSearchScoreThreshold(settings) {
|
|
1054
|
+
const candidate = settings
|
|
1055
|
+
.search?.score_threshold;
|
|
1056
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
1057
|
+
return candidate;
|
|
1058
|
+
}
|
|
1059
|
+
return 0;
|
|
1060
|
+
}
|
|
1061
|
+
/** Implements resolve hybrid semantic weight for the public runtime surface of this module. */
|
|
1062
|
+
export function resolveHybridSemanticWeight(settings) {
|
|
1063
|
+
const candidate = settings.search?.hybrid_semantic_weight;
|
|
1064
|
+
if (typeof candidate === "number" &&
|
|
1065
|
+
Number.isFinite(candidate) &&
|
|
1066
|
+
candidate >= 0 &&
|
|
1067
|
+
candidate <= 1) {
|
|
1068
|
+
return candidate;
|
|
1069
|
+
}
|
|
1070
|
+
return 0.7;
|
|
1071
|
+
}
|
|
1072
|
+
/** Implements resolve search tuning for the public runtime surface of this module. */
|
|
1073
|
+
export function resolveSearchTuning(settings) {
|
|
1074
|
+
const defaults = {
|
|
1075
|
+
title_exact_bonus: 10,
|
|
1076
|
+
title_weight: 8,
|
|
1077
|
+
description_weight: 5,
|
|
1078
|
+
tags_weight: 6,
|
|
1079
|
+
status_weight: 2,
|
|
1080
|
+
body_weight: 1,
|
|
1081
|
+
comments_weight: 1,
|
|
1082
|
+
notes_weight: 1,
|
|
1083
|
+
learnings_weight: 1,
|
|
1084
|
+
reminders_weight: 2,
|
|
1085
|
+
events_weight: 2,
|
|
1086
|
+
dependencies_weight: 3,
|
|
1087
|
+
linked_content_weight: 1,
|
|
1088
|
+
};
|
|
1089
|
+
const tuning = settings
|
|
1090
|
+
.search?.tuning;
|
|
1091
|
+
if (!tuning)
|
|
1092
|
+
return defaults;
|
|
1093
|
+
const resolveWeight = (candidate, fallback) => {
|
|
1094
|
+
if (typeof candidate === "number" &&
|
|
1095
|
+
Number.isFinite(candidate) &&
|
|
1096
|
+
candidate >= 0) {
|
|
1097
|
+
return candidate;
|
|
1098
|
+
}
|
|
1099
|
+
return fallback;
|
|
1100
|
+
};
|
|
1101
|
+
return {
|
|
1102
|
+
title_exact_bonus: resolveWeight(tuning.title_exact_bonus, defaults.title_exact_bonus),
|
|
1103
|
+
title_weight: resolveWeight(tuning.title_weight, defaults.title_weight),
|
|
1104
|
+
description_weight: resolveWeight(tuning.description_weight, defaults.description_weight),
|
|
1105
|
+
tags_weight: resolveWeight(tuning.tags_weight, defaults.tags_weight),
|
|
1106
|
+
status_weight: resolveWeight(tuning.status_weight, defaults.status_weight),
|
|
1107
|
+
body_weight: resolveWeight(tuning.body_weight, defaults.body_weight),
|
|
1108
|
+
comments_weight: resolveWeight(tuning.comments_weight, defaults.comments_weight),
|
|
1109
|
+
notes_weight: resolveWeight(tuning.notes_weight, defaults.notes_weight),
|
|
1110
|
+
learnings_weight: resolveWeight(tuning.learnings_weight, defaults.learnings_weight),
|
|
1111
|
+
reminders_weight: resolveWeight(tuning.reminders_weight, defaults.reminders_weight),
|
|
1112
|
+
events_weight: resolveWeight(tuning.events_weight, defaults.events_weight),
|
|
1113
|
+
dependencies_weight: resolveWeight(tuning.dependencies_weight, defaults.dependencies_weight),
|
|
1114
|
+
linked_content_weight: resolveWeight(tuning.linked_content_weight, defaults.linked_content_weight),
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
/* c8 ignore start -- empty-result projection/count/warnings shape matrix is validated by integration response-contract tests */
|
|
1118
|
+
function emptySearchResult(query, mode, matchMode, options, includeLinked, scoreThreshold, hybridSemanticWeight, queryExpansion, rerank, projection, warnings, runtimeFieldFilters, countOnly = false) {
|
|
1119
|
+
// --count consistency: a count-only query that matches nothing must still
|
|
1120
|
+
// carry the same { count_only: true, total } shape as the non-empty path.
|
|
1121
|
+
const countExtras = countOnly ? { total: 0, count_only: true } : {};
|
|
1122
|
+
const compactSummaryMode = projection.mode === "compact" && options.compact === true;
|
|
1123
|
+
if (compactSummaryMode) {
|
|
1124
|
+
const compactFilters = buildCompactSearchFilterSummary({
|
|
1125
|
+
mode,
|
|
1126
|
+
matchMode,
|
|
1127
|
+
options,
|
|
1128
|
+
includeLinked,
|
|
1129
|
+
titleExact: options.titleExact === true,
|
|
1130
|
+
phraseExact: options.phraseExact === true,
|
|
1131
|
+
scoreThreshold,
|
|
1132
|
+
hybridSemanticWeight,
|
|
1133
|
+
runtimeFieldFilters,
|
|
1134
|
+
});
|
|
1135
|
+
return {
|
|
1136
|
+
query: query.trim(),
|
|
1137
|
+
mode,
|
|
1138
|
+
items: [],
|
|
1139
|
+
count: 0,
|
|
1140
|
+
...countExtras,
|
|
1141
|
+
filters: compactFilters,
|
|
1142
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const projectionFields = projection.mode === "full" ? null : [...projection.fields];
|
|
1146
|
+
return {
|
|
1147
|
+
query: query.trim(),
|
|
1148
|
+
mode,
|
|
1149
|
+
items: [],
|
|
1150
|
+
count: 0,
|
|
1151
|
+
...countExtras,
|
|
1152
|
+
filters: buildVerboseSearchFilters({
|
|
1153
|
+
effectiveMode: mode,
|
|
1154
|
+
matchMode,
|
|
1155
|
+
options,
|
|
1156
|
+
includeLinked,
|
|
1157
|
+
titleExact: options.titleExact === true,
|
|
1158
|
+
phraseExact: options.phraseExact === true,
|
|
1159
|
+
scoreThreshold,
|
|
1160
|
+
hybridSemanticWeight,
|
|
1161
|
+
queryExpansion,
|
|
1162
|
+
rerank,
|
|
1163
|
+
runtimeFieldFilters: runtimeFieldFilters ?? {},
|
|
1164
|
+
}),
|
|
1165
|
+
projection: {
|
|
1166
|
+
mode: projection.mode,
|
|
1167
|
+
fields: projectionFields,
|
|
1168
|
+
},
|
|
1169
|
+
now: nowIso(),
|
|
1170
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
/* c8 ignore stop */
|
|
1174
|
+
function requireSemanticDependencies(requestedMode, providerResolution, vectorResolution, hasExtensionVectorQuery) {
|
|
1175
|
+
if (!providerResolution.active) {
|
|
1176
|
+
throw new PmCliError(`Search mode '${requestedMode}' requires a configured embedding provider in settings.providers.openai or settings.providers.ollama`, EXIT_CODE.USAGE);
|
|
1177
|
+
}
|
|
1178
|
+
if (!vectorResolution.active && !hasExtensionVectorQuery) {
|
|
1179
|
+
throw new PmCliError(`Search mode '${requestedMode}' requires a configured vector store in settings.vector_store.qdrant/settings.vector_store.lancedb or an extension adapter selected by settings.vector_store.adapter`, EXIT_CODE.USAGE);
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
provider: providerResolution.active,
|
|
1183
|
+
vectorStore: vectorResolution.active ?? null,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
const toOptionalNonEmptyString = toNonEmptyStringOrUndefined;
|
|
1187
|
+
/* c8 ignore start -- extension search-provider registration permutations are covered by extension integration suites */
|
|
1188
|
+
function resolveExtensionSearchProviderByName(providerName) {
|
|
1189
|
+
const registrations = getActiveExtensionRegistrations();
|
|
1190
|
+
const resolved = resolveRegisteredSearchProvider(registrations, providerName);
|
|
1191
|
+
if (!resolved) {
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
const runtimeDefinition = resolved.runtime_definition ?? resolved.definition;
|
|
1195
|
+
const query = runtimeDefinition.query;
|
|
1196
|
+
const queryExpansion = runtimeDefinition.queryExpansion ??
|
|
1197
|
+
runtimeDefinition.query_expansion;
|
|
1198
|
+
const rerank = runtimeDefinition.rerank;
|
|
1199
|
+
const registeredName = toOptionalNonEmptyString(runtimeDefinition.name) ??
|
|
1200
|
+
toOptionalNonEmptyString(resolved.definition.name) ??
|
|
1201
|
+
providerName;
|
|
1202
|
+
/* c8 ignore next 2 -- providerName is required for lookup and remains a non-empty fallback */
|
|
1203
|
+
if (!registeredName) {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
const hooks = {
|
|
1207
|
+
providerName: registeredName,
|
|
1208
|
+
...(typeof query === "function"
|
|
1209
|
+
? { query: query }
|
|
1210
|
+
: {}),
|
|
1211
|
+
...(typeof queryExpansion === "function"
|
|
1212
|
+
? {
|
|
1213
|
+
queryExpansion: queryExpansion,
|
|
1214
|
+
}
|
|
1215
|
+
: {}),
|
|
1216
|
+
...(typeof rerank === "function"
|
|
1217
|
+
? { rerank: rerank }
|
|
1218
|
+
: {}),
|
|
1219
|
+
};
|
|
1220
|
+
if (!hooks.query && !hooks.queryExpansion && !hooks.rerank) {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
return hooks;
|
|
1224
|
+
}
|
|
1225
|
+
/* c8 ignore stop */
|
|
1226
|
+
function resolveExtensionSearchProvider(settings) {
|
|
1227
|
+
const providerName = toOptionalNonEmptyString(settings.search?.provider);
|
|
1228
|
+
const resolved = resolveExtensionSearchProviderByName(providerName);
|
|
1229
|
+
if (!resolved?.query) {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
providerName: resolved.providerName,
|
|
1234
|
+
query: resolved.query,
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
/* c8 ignore start -- vector adapter runtime definition permutations are covered by extension integration suites */
|
|
1238
|
+
function resolveExtensionVectorAdapter(settings) {
|
|
1239
|
+
const registrations = getActiveExtensionRegistrations();
|
|
1240
|
+
const adapterName = toOptionalNonEmptyString(settings.vector_store?.adapter);
|
|
1241
|
+
const resolved = resolveRegisteredVectorStoreAdapter(registrations, adapterName);
|
|
1242
|
+
if (!resolved) {
|
|
1243
|
+
return null;
|
|
1244
|
+
}
|
|
1245
|
+
const runtimeDefinition = resolved.runtime_definition ?? resolved.definition;
|
|
1246
|
+
const query = runtimeDefinition.query;
|
|
1247
|
+
if (typeof query !== "function") {
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
const upsert = runtimeDefinition.upsert;
|
|
1251
|
+
const runtimeAdapterName = toOptionalNonEmptyString(runtimeDefinition.name) ??
|
|
1252
|
+
toOptionalNonEmptyString(resolved.definition.name) ??
|
|
1253
|
+
adapterName ??
|
|
1254
|
+
"extension";
|
|
1255
|
+
return {
|
|
1256
|
+
adapterName: runtimeAdapterName,
|
|
1257
|
+
query: query,
|
|
1258
|
+
...(typeof upsert === "function"
|
|
1259
|
+
? { upsert: upsert }
|
|
1260
|
+
: {}),
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
/* c8 ignore stop */
|
|
1264
|
+
function normalizeExtensionProviderHits(providerName, raw, filteredById) {
|
|
1265
|
+
const rawHits = Array.isArray(raw)
|
|
1266
|
+
? raw
|
|
1267
|
+
: raw?.hits;
|
|
1268
|
+
if (!Array.isArray(rawHits)) {
|
|
1269
|
+
throw new PmCliError(`Extension search provider "${providerName}" must return an array of hits or { hits: [...] }`, EXIT_CODE.GENERIC_FAILURE);
|
|
1270
|
+
}
|
|
1271
|
+
const seen = new Set();
|
|
1272
|
+
const hits = [];
|
|
1273
|
+
for (const rawHit of rawHits) {
|
|
1274
|
+
if (typeof rawHit !== "object" || rawHit === null) {
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
const id = toOptionalNonEmptyString(rawHit.id);
|
|
1278
|
+
const score = rawHit.score;
|
|
1279
|
+
if (!id ||
|
|
1280
|
+
typeof score !== "number" ||
|
|
1281
|
+
!Number.isFinite(score) ||
|
|
1282
|
+
seen.has(id)) {
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
const document = filteredById.get(id);
|
|
1286
|
+
if (!document) {
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const matchedFieldsRaw = rawHit
|
|
1290
|
+
.matched_fields;
|
|
1291
|
+
const matchedFields = Array.isArray(matchedFieldsRaw) &&
|
|
1292
|
+
matchedFieldsRaw.every((entry) => typeof entry === "string")
|
|
1293
|
+
? [
|
|
1294
|
+
...new Set(matchedFieldsRaw
|
|
1295
|
+
.map((entry) => entry.trim())
|
|
1296
|
+
.filter((entry) => entry.length > 0)),
|
|
1297
|
+
].sort((a, b) => a.localeCompare(b))
|
|
1298
|
+
: [`provider:${providerName}`];
|
|
1299
|
+
seen.add(id);
|
|
1300
|
+
hits.push({
|
|
1301
|
+
item: document.metadata,
|
|
1302
|
+
score,
|
|
1303
|
+
matched_fields: matchedFields,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
return hits;
|
|
1307
|
+
}
|
|
1308
|
+
function buildSemanticHits(vectorHits, filteredById) {
|
|
1309
|
+
const semanticHits = [];
|
|
1310
|
+
const semanticScores = new Map();
|
|
1311
|
+
for (const vectorHit of vectorHits) {
|
|
1312
|
+
if (semanticScores.has(vectorHit.id)) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const document = filteredById.get(vectorHit.id);
|
|
1316
|
+
if (!document) {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
semanticScores.set(vectorHit.id, vectorHit.score);
|
|
1320
|
+
semanticHits.push({
|
|
1321
|
+
item: document.metadata,
|
|
1322
|
+
score: vectorHit.score,
|
|
1323
|
+
matched_fields: ["semantic"],
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
return {
|
|
1327
|
+
semanticHits,
|
|
1328
|
+
semanticScores,
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
// GH-281: in semantic & hybrid mode the blended/vector scores live on an
|
|
1332
|
+
// arbitrary scale (blended hybrid scores are [0,1]; raw vector similarities are
|
|
1333
|
+
// provider-defined), so an exact full-ID or short-ID keyword match can be
|
|
1334
|
+
// out-ranked by a high-semantic body mention. This reattaches the exact-ID
|
|
1335
|
+
// keyword hit(s) to the top of the ranked set in a reserved band ABOVE every
|
|
1336
|
+
// other hit, preserving matched_fields:["id"] and keeping full-ID above
|
|
1337
|
+
// short-ID. The reserved scores are derived from the current max so the
|
|
1338
|
+
// guarantee holds no matter what scale the backend returns, and the hit is
|
|
1339
|
+
// re-inserted even if it was dropped from `rankedHits` (e.g. it had no semantic
|
|
1340
|
+
// vector match) so it is never lost to the threshold or the result-limit slice.
|
|
1341
|
+
function forceExactIdHitsToTop(rankedHits, keywordHits) {
|
|
1342
|
+
const exactIdHits = keywordHits.filter((hit) => hit.exact_id_match === true);
|
|
1343
|
+
if (exactIdHits.length === 0) {
|
|
1344
|
+
return rankedHits;
|
|
1345
|
+
}
|
|
1346
|
+
const exactIdIds = new Set(exactIdHits.map((hit) => hit.item.id));
|
|
1347
|
+
const remaining = rankedHits.filter((hit) => !exactIdIds.has(hit.item.id));
|
|
1348
|
+
// reduce (not Math.max(...spread)) so a very large pre-truncation candidate
|
|
1349
|
+
// set can never overflow the call stack.
|
|
1350
|
+
const maxRemainingScore = remaining.reduce((max, hit) => Math.max(max, hit.score), 0);
|
|
1351
|
+
// Full-ID hits (score 1000) must rank above short-ID hits (score 900): rank
|
|
1352
|
+
// within the exact-ID band by the original keyword score (descending) so a
|
|
1353
|
+
// full-ID match always precedes a short-ID match for the same query, then
|
|
1354
|
+
// offset the whole band above every remaining hit. ids are unique, so two
|
|
1355
|
+
// exact-ID hits can never tie on score — sorting on score alone is total.
|
|
1356
|
+
// exactIdHits is already a fresh array from keywordHits.filter(...), so sort
|
|
1357
|
+
// it in place — no defensive copy needed.
|
|
1358
|
+
const orderedExactHits = exactIdHits.sort((left, right) => right.score - left.score);
|
|
1359
|
+
const bandBase = maxRemainingScore + orderedExactHits.length + 1;
|
|
1360
|
+
const promoted = orderedExactHits.map((hit, index) => ({
|
|
1361
|
+
// Spread the original keyword hit so flags like matched_all_terms /
|
|
1362
|
+
// exact_id_match are preserved; only the band-slot score and the id-only
|
|
1363
|
+
// matched_fields are overridden.
|
|
1364
|
+
...hit,
|
|
1365
|
+
// Higher band slot for earlier (higher original keyword score) hits.
|
|
1366
|
+
score: bandBase - index,
|
|
1367
|
+
matched_fields: ["id"],
|
|
1368
|
+
}));
|
|
1369
|
+
return [...promoted, ...remaining];
|
|
1370
|
+
}
|
|
1371
|
+
function combineHybridHits(filteredById, semanticScores, keywordHits, hybridSemanticWeight,
|
|
1372
|
+
// Matched-field label for the dense-ranking component. Defaults to "semantic"
|
|
1373
|
+
// (vector retrieval); the offline BM25 hybrid path passes "bm25" so hits carry
|
|
1374
|
+
// an honest provenance marker instead of implying a vector match (pm-75k9).
|
|
1375
|
+
semanticFieldLabel = "semantic") {
|
|
1376
|
+
const keywordScores = new Map(keywordHits.map((entry) => [entry.item.id, entry.score]));
|
|
1377
|
+
const keywordMatches = new Map(keywordHits.map((entry) => [entry.item.id, entry.matched_fields]));
|
|
1378
|
+
const normalizedSemantic = normalizeScoreMap(semanticScores);
|
|
1379
|
+
const normalizedKeyword = normalizeScoreMap(keywordScores);
|
|
1380
|
+
const candidateIds = new Set([
|
|
1381
|
+
...semanticScores.keys(),
|
|
1382
|
+
...keywordScores.keys(),
|
|
1383
|
+
]);
|
|
1384
|
+
const keywordWeight = 1 - hybridSemanticWeight;
|
|
1385
|
+
return [...candidateIds]
|
|
1386
|
+
.map((id) => {
|
|
1387
|
+
const document = filteredById.get(id);
|
|
1388
|
+
const semanticScore = normalizedSemantic.get(id) ?? 0;
|
|
1389
|
+
const keywordScore = normalizedKeyword.get(id) ?? 0;
|
|
1390
|
+
const combinedScore = semanticScore * hybridSemanticWeight + keywordScore * keywordWeight;
|
|
1391
|
+
if (combinedScore <= 0) {
|
|
1392
|
+
return null;
|
|
1393
|
+
}
|
|
1394
|
+
const matchedFields = new Set();
|
|
1395
|
+
if (semanticScores.has(id)) {
|
|
1396
|
+
matchedFields.add(semanticFieldLabel);
|
|
1397
|
+
}
|
|
1398
|
+
for (const field of keywordMatches.get(id) ?? []) {
|
|
1399
|
+
matchedFields.add(field);
|
|
1400
|
+
}
|
|
1401
|
+
return {
|
|
1402
|
+
item: document.metadata,
|
|
1403
|
+
score: combinedScore,
|
|
1404
|
+
matched_fields: [...matchedFields].sort((a, b) => a.localeCompare(b)),
|
|
1405
|
+
};
|
|
1406
|
+
})
|
|
1407
|
+
.filter((entry) => entry !== null);
|
|
1408
|
+
}
|
|
1409
|
+
function resolveBuiltInBm25Mode(params) {
|
|
1410
|
+
const normalized = params.configuredProvider?.trim().toLowerCase();
|
|
1411
|
+
if (normalized === "bm25") {
|
|
1412
|
+
return "explicit";
|
|
1413
|
+
}
|
|
1414
|
+
if (normalized === "auto" &&
|
|
1415
|
+
!params.hasEmbeddingProvider &&
|
|
1416
|
+
!params.hasExtensionSearchProvider) {
|
|
1417
|
+
return "auto-fallback";
|
|
1418
|
+
}
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Rank the filtered corpus with the offline BM25 provider (pm-75k9). Builds a
|
|
1423
|
+
* BM25 index over each document's resolved search corpus (honoring
|
|
1424
|
+
* `search.corpus_fields`), scores the query, and shapes hits for the requested
|
|
1425
|
+
* mode: semantic mode returns pure BM25-ranked hits tagged
|
|
1426
|
+
* `matched_fields: ["bm25"]`, while hybrid mode blends the BM25 scores with the
|
|
1427
|
+
* locally computed keyword scores via {@link combineHybridHits}. The exact
|
|
1428
|
+
* full-ID / short-ID guarantee is preserved in both modes via
|
|
1429
|
+
* {@link forceExactIdHitsToTop}. No network, embedding service, or vector store
|
|
1430
|
+
* is involved, so this path cannot fail on a backend error.
|
|
1431
|
+
*/
|
|
1432
|
+
function computeBuiltInBm25Hits(params) {
|
|
1433
|
+
const bm25Documents = params.filteredDocuments.map((document) => ({
|
|
1434
|
+
id: document.metadata.id,
|
|
1435
|
+
text: flattenSearchCorpusText(buildSearchCorpus(document, { fields: params.corpusFields })),
|
|
1436
|
+
}));
|
|
1437
|
+
const index = buildBm25Index(bm25Documents);
|
|
1438
|
+
const bm25Scores = scoreBm25Query(index, tokenizeBm25(params.query), params.bm25Params);
|
|
1439
|
+
const filteredById = new Map(params.filteredDocuments.map((document) => [
|
|
1440
|
+
document.metadata.id,
|
|
1441
|
+
document,
|
|
1442
|
+
]));
|
|
1443
|
+
if (params.requestedMode === "semantic") {
|
|
1444
|
+
const semanticHits = [...bm25Scores].map(([id, score]) => ({
|
|
1445
|
+
item: filteredById.get(id).metadata,
|
|
1446
|
+
score,
|
|
1447
|
+
matched_fields: ["bm25"],
|
|
1448
|
+
}));
|
|
1449
|
+
return forceExactIdHitsToTop(semanticHits, params.keywordHits);
|
|
1450
|
+
}
|
|
1451
|
+
const hybridHits = combineHybridHits(filteredById, bm25Scores, params.keywordHits, params.hybridSemanticWeight, "bm25");
|
|
1452
|
+
return forceExactIdHitsToTop(hybridHits, params.keywordHits);
|
|
1453
|
+
}
|
|
1454
|
+
function mergeVectorHitsById(vectorHitGroups) {
|
|
1455
|
+
const bestById = new Map();
|
|
1456
|
+
for (const group of vectorHitGroups) {
|
|
1457
|
+
for (const hit of group) {
|
|
1458
|
+
const existing = bestById.get(hit.id);
|
|
1459
|
+
if (!existing || hit.score > existing.score) {
|
|
1460
|
+
bestById.set(hit.id, hit);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
const merged = [...bestById.values()];
|
|
1465
|
+
merged.sort((left, right) => {
|
|
1466
|
+
if (left.score !== right.score) {
|
|
1467
|
+
return right.score - left.score;
|
|
1468
|
+
}
|
|
1469
|
+
return left.id.localeCompare(right.id);
|
|
1470
|
+
});
|
|
1471
|
+
return merged;
|
|
1472
|
+
}
|
|
1473
|
+
/* c8 ignore start -- rerank corpus metadata-shape permutations are covered by semantic integration tests */
|
|
1474
|
+
function buildRerankCorpus(document) {
|
|
1475
|
+
const metadata = document
|
|
1476
|
+
.metadata;
|
|
1477
|
+
const tags = Array.isArray(metadata?.tags) ? metadata.tags.join(" ") : "";
|
|
1478
|
+
return [
|
|
1479
|
+
metadata?.title,
|
|
1480
|
+
metadata?.description,
|
|
1481
|
+
metadata?.type,
|
|
1482
|
+
metadata?.status,
|
|
1483
|
+
tags,
|
|
1484
|
+
document.body,
|
|
1485
|
+
]
|
|
1486
|
+
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
1487
|
+
.filter((entry) => entry.length > 0)
|
|
1488
|
+
.join("\n");
|
|
1489
|
+
}
|
|
1490
|
+
/* c8 ignore stop */
|
|
1491
|
+
/* c8 ignore start -- semantic expansion/rerank fallback matrices are covered by end-to-end semantic search tests */
|
|
1492
|
+
async function resolveExpandedSemanticQueries(context, queryTrimmed) {
|
|
1493
|
+
const baseExpandedQueries = context.queryExpansion.enabled
|
|
1494
|
+
? buildDeterministicQueryExpansions(queryTrimmed, context.queryExpansion.max_queries)
|
|
1495
|
+
: [queryTrimmed];
|
|
1496
|
+
const expandedQueries = baseExpandedQueries.length > 0 ? baseExpandedQueries : [queryTrimmed];
|
|
1497
|
+
if (!context.queryExpansion.enabled) {
|
|
1498
|
+
return expandedQueries;
|
|
1499
|
+
}
|
|
1500
|
+
if (context.queryExpansionExtension?.expand) {
|
|
1501
|
+
try {
|
|
1502
|
+
const rawExpansion = await Promise.resolve(context.queryExpansionExtension.expand({
|
|
1503
|
+
query: queryTrimmed,
|
|
1504
|
+
mode: context.requestedMode,
|
|
1505
|
+
settings: context.settings,
|
|
1506
|
+
}));
|
|
1507
|
+
return mergeQueryExpansions(expandedQueries, normalizeQueryExpansionOutput(rawExpansion), context.queryExpansion.max_queries);
|
|
1508
|
+
}
|
|
1509
|
+
catch {
|
|
1510
|
+
context.warnings.push(`search_query_expansion_provider_failed:${context.queryExpansionExtension.providerName}:using_builtin`);
|
|
1511
|
+
return expandedQueries;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (context.queryExpansion.provider &&
|
|
1515
|
+
context.queryExpansion.provider !== "openai" &&
|
|
1516
|
+
context.queryExpansion.provider !== "ollama") {
|
|
1517
|
+
context.warnings.push(`search_query_expansion_provider_unavailable:${context.queryExpansion.provider}:using_builtin`);
|
|
1518
|
+
}
|
|
1519
|
+
return expandedQueries;
|
|
1520
|
+
}
|
|
1521
|
+
async function executeSemanticVectorQuery(context, semanticVector, semanticLimit, vectorQueryOptions) {
|
|
1522
|
+
if (context.extensionVectorAdapter?.query) {
|
|
1523
|
+
try {
|
|
1524
|
+
return await Promise.resolve(context.extensionVectorAdapter.query({
|
|
1525
|
+
vector: semanticVector,
|
|
1526
|
+
limit: semanticLimit,
|
|
1527
|
+
settings: context.settings,
|
|
1528
|
+
}));
|
|
1529
|
+
}
|
|
1530
|
+
catch (error) {
|
|
1531
|
+
if (!context.vectorStore) {
|
|
1532
|
+
throw new PmCliError(`Extension vector adapter query failed and no built-in fallback store is configured (${error instanceof Error ? error.message : String(error)})`, EXIT_CODE.GENERIC_FAILURE);
|
|
1533
|
+
}
|
|
1534
|
+
context.warnings.push(`search_vector_adapter_failed:${context.extensionVectorAdapter.adapterName}:using_builtin`);
|
|
1535
|
+
return await executeVectorQuery(context.vectorStore, semanticVector, semanticLimit, vectorQueryOptions);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
if (context.vectorStore) {
|
|
1539
|
+
return await executeVectorQuery(context.vectorStore, semanticVector, semanticLimit, vectorQueryOptions);
|
|
1540
|
+
}
|
|
1541
|
+
throw new PmCliError("Semantic search requires either a configured vector store or an extension vector adapter query handler", EXIT_CODE.USAGE);
|
|
1542
|
+
}
|
|
1543
|
+
function buildRerankCandidateContexts(hybridHits, filteredById, topK) {
|
|
1544
|
+
const sortedForCandidates = [...hybridHits].sort((left, right) => {
|
|
1545
|
+
if (left.score !== right.score) {
|
|
1546
|
+
return right.score - left.score;
|
|
1547
|
+
}
|
|
1548
|
+
return left.item.id.localeCompare(right.item.id);
|
|
1549
|
+
});
|
|
1550
|
+
return sortedForCandidates
|
|
1551
|
+
.slice(0, topK)
|
|
1552
|
+
.map((hit) => {
|
|
1553
|
+
const document = filteredById.get(hit.item.id);
|
|
1554
|
+
return document ? { hit, text: buildRerankCorpus(document) } : null;
|
|
1555
|
+
})
|
|
1556
|
+
.filter((entry) => entry !== null);
|
|
1557
|
+
}
|
|
1558
|
+
async function resolveExtensionRerankScores(context, queryTrimmed, candidateContexts) {
|
|
1559
|
+
if (!context.rerankExtension?.rerank) {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
try {
|
|
1563
|
+
const rawRerank = await Promise.resolve(context.rerankExtension.rerank({
|
|
1564
|
+
query: queryTrimmed,
|
|
1565
|
+
mode: "hybrid",
|
|
1566
|
+
model: context.rerank.model,
|
|
1567
|
+
top_k: context.rerank.top_k,
|
|
1568
|
+
settings: context.settings,
|
|
1569
|
+
candidates: candidateContexts.map((entry) => ({
|
|
1570
|
+
id: entry.hit.item.id,
|
|
1571
|
+
text: entry.text,
|
|
1572
|
+
score: entry.hit.score,
|
|
1573
|
+
})),
|
|
1574
|
+
}));
|
|
1575
|
+
const normalizedRerank = normalizeRerankOutput(rawRerank);
|
|
1576
|
+
if (normalizedRerank.length > 0) {
|
|
1577
|
+
return new Map(normalizedRerank.map((entry) => [entry.id, entry.score]));
|
|
1578
|
+
}
|
|
1579
|
+
context.warnings.push(`search_rerank_provider_invalid_response:${context.rerankExtension.providerName}:using_builtin`);
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
catch {
|
|
1583
|
+
context.warnings.push(`search_rerank_provider_failed:${context.rerankExtension.providerName}:using_builtin`);
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
async function resolveRerankScores(context, queryTrimmed, candidateContexts) {
|
|
1588
|
+
const extensionScores = await resolveExtensionRerankScores(context, queryTrimmed, candidateContexts);
|
|
1589
|
+
if (extensionScores) {
|
|
1590
|
+
return extensionScores;
|
|
1591
|
+
}
|
|
1592
|
+
const rerankCandidates = candidateContexts.map((entry) => ({
|
|
1593
|
+
id: entry.hit.item.id,
|
|
1594
|
+
text: entry.text,
|
|
1595
|
+
}));
|
|
1596
|
+
try {
|
|
1597
|
+
return await rerankCandidatesWithEmbeddings(context.provider, context.rerank.model, queryTrimmed, rerankCandidates, context.embeddingTimeoutMs);
|
|
1598
|
+
}
|
|
1599
|
+
catch {
|
|
1600
|
+
context.warnings.push("search_rerank_failed:using_hybrid_scores");
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
function applyRerankScores(hybridHits, rerankScores) {
|
|
1605
|
+
const rerankedIds = new Set(rerankScores.keys());
|
|
1606
|
+
const rerankedHits = hybridHits.map((hit) => {
|
|
1607
|
+
const rerankScore = rerankScores.get(hit.item.id);
|
|
1608
|
+
if (rerankScore === undefined) {
|
|
1609
|
+
return hit;
|
|
1610
|
+
}
|
|
1611
|
+
const matchedFields = new Set(hit.matched_fields);
|
|
1612
|
+
matchedFields.add("rerank");
|
|
1613
|
+
return {
|
|
1614
|
+
...hit,
|
|
1615
|
+
score: rerankScore,
|
|
1616
|
+
matched_fields: [...matchedFields].sort((left, right) => left.localeCompare(right)),
|
|
1617
|
+
};
|
|
1618
|
+
});
|
|
1619
|
+
rerankedHits.sort((left, right) => {
|
|
1620
|
+
const leftWasReranked = rerankedIds.has(left.item.id);
|
|
1621
|
+
const rightWasReranked = rerankedIds.has(right.item.id);
|
|
1622
|
+
if (leftWasReranked !== rightWasReranked) {
|
|
1623
|
+
return leftWasReranked ? -1 : 1;
|
|
1624
|
+
}
|
|
1625
|
+
if (left.score !== right.score) {
|
|
1626
|
+
return right.score - left.score;
|
|
1627
|
+
}
|
|
1628
|
+
return left.item.id.localeCompare(right.item.id);
|
|
1629
|
+
});
|
|
1630
|
+
return rerankedHits;
|
|
1631
|
+
}
|
|
1632
|
+
async function applyHybridRerank(context, queryTrimmed, hybridHits, filteredById) {
|
|
1633
|
+
if (!context.rerank.enabled || hybridHits.length <= 1) {
|
|
1634
|
+
return hybridHits;
|
|
1635
|
+
}
|
|
1636
|
+
const candidateContexts = buildRerankCandidateContexts(hybridHits, filteredById, context.rerank.top_k);
|
|
1637
|
+
const rerankScores = await resolveRerankScores(context, queryTrimmed, candidateContexts);
|
|
1638
|
+
return rerankScores && rerankScores.size > 0
|
|
1639
|
+
? applyRerankScores(hybridHits, rerankScores)
|
|
1640
|
+
: hybridHits;
|
|
1641
|
+
}
|
|
1642
|
+
async function computeSemanticOrHybridHits(context) {
|
|
1643
|
+
const semanticLimit = Math.max(context.limit ?? context.maxResults, context.maxResults);
|
|
1644
|
+
const embeddingOptions = context.embeddingTimeoutMs !== undefined
|
|
1645
|
+
? { timeout_ms: context.embeddingTimeoutMs }
|
|
1646
|
+
: {};
|
|
1647
|
+
const vectorQueryOptions = context.vectorQueryTimeoutMs !== undefined
|
|
1648
|
+
? { timeout_ms: context.vectorQueryTimeoutMs }
|
|
1649
|
+
: {};
|
|
1650
|
+
const queryTrimmed = context.query.trim();
|
|
1651
|
+
const expandedQueries = await resolveExpandedSemanticQueries(context, queryTrimmed);
|
|
1652
|
+
const queryVectors = await executeEmbeddingRequest(context.provider, expandedQueries, embeddingOptions);
|
|
1653
|
+
const queryVectorGroups = await Promise.all(queryVectors.map(async (semanticVector) => await executeSemanticVectorQuery(context, semanticVector, semanticLimit, vectorQueryOptions)));
|
|
1654
|
+
const vectorHits = mergeVectorHitsById(queryVectorGroups);
|
|
1655
|
+
const filteredById = new Map(context.filteredDocuments.map((document) => [
|
|
1656
|
+
document.metadata.id,
|
|
1657
|
+
document,
|
|
1658
|
+
]));
|
|
1659
|
+
const { semanticHits, semanticScores } = buildSemanticHits(vectorHits, filteredById);
|
|
1660
|
+
const vectorMatchCount = semanticScores.size;
|
|
1661
|
+
if (context.requestedMode === "semantic") {
|
|
1662
|
+
// GH-281: guarantee an exact full-ID / short-ID match ranks #1 even in pure
|
|
1663
|
+
// semantic mode, where it otherwise carries no vector hit at all.
|
|
1664
|
+
return {
|
|
1665
|
+
hits: forceExactIdHitsToTop(semanticHits, context.keywordHits),
|
|
1666
|
+
vectorMatchCount,
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
const hybridHits = await applyHybridRerank(context, queryTrimmed, combineHybridHits(filteredById, semanticScores, context.keywordHits, context.hybridSemanticWeight), filteredById);
|
|
1670
|
+
return {
|
|
1671
|
+
// GH-281: applied LAST (after any rerank reordering) so an exact full-ID /
|
|
1672
|
+
// short-ID keyword match always ranks #1 in hybrid mode regardless of the
|
|
1673
|
+
// semantic blend weight or rerank scores.
|
|
1674
|
+
hits: forceExactIdHitsToTop(hybridHits, context.keywordHits),
|
|
1675
|
+
vectorMatchCount,
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
/* c8 ignore stop */
|
|
1679
|
+
/* c8 ignore start -- item body fallback read-path combinations are covered by document-cache integration tests */
|
|
1680
|
+
async function loadDocuments(pmRoot, itemFormat, typeToFolder, schema) {
|
|
1681
|
+
const extensionFieldNames = collectRegisteredItemFieldNames(getActiveExtensionRegistrations());
|
|
1682
|
+
const readDocumentBody = async (metadata, preferredPath, preferredFormat) => {
|
|
1683
|
+
const tryRead = async (targetPath, format) => {
|
|
1684
|
+
await runActiveOnReadHooks({ path: targetPath, scope: "project" });
|
|
1685
|
+
const raw = await fs.readFile(targetPath, "utf8");
|
|
1686
|
+
const parsed = parseItemDocument(raw, {
|
|
1687
|
+
format,
|
|
1688
|
+
schema,
|
|
1689
|
+
extensionFieldNames,
|
|
1690
|
+
onWarning: (warning) => listWarnings.push(warning),
|
|
1691
|
+
});
|
|
1692
|
+
return parsed.body;
|
|
1693
|
+
};
|
|
1694
|
+
try {
|
|
1695
|
+
return await tryRead(preferredPath, preferredFormat);
|
|
1696
|
+
}
|
|
1697
|
+
catch {
|
|
1698
|
+
const alternateFormat = preferredFormat === "toon" ? "json_markdown" : "toon";
|
|
1699
|
+
const alternatePath = getItemPath(pmRoot, metadata.type, metadata.id, alternateFormat, typeToFolder);
|
|
1700
|
+
try {
|
|
1701
|
+
return await tryRead(alternatePath, alternateFormat);
|
|
1702
|
+
}
|
|
1703
|
+
catch {
|
|
1704
|
+
listWarnings.push(`item_list_item_read_failed:${path.relative(pmRoot, alternatePath)}`);
|
|
1705
|
+
return "";
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
const listWarnings = [];
|
|
1710
|
+
const cachedDocuments = await listAllDocumentCandidatesCached(pmRoot, itemFormat, typeToFolder, listWarnings, schema);
|
|
1711
|
+
const documents = [];
|
|
1712
|
+
if (cachedDocuments.length === 0) {
|
|
1713
|
+
const itemMetadataDocuments = await listAllItemMetadata(pmRoot, itemFormat, typeToFolder, listWarnings, schema);
|
|
1714
|
+
for (const metadata of itemMetadataDocuments) {
|
|
1715
|
+
const preferredPath = getItemPath(pmRoot, metadata.type, metadata.id, itemFormat, typeToFolder);
|
|
1716
|
+
const body = await readDocumentBody(metadata, preferredPath, itemFormat);
|
|
1717
|
+
documents.push({ metadata, body });
|
|
1718
|
+
}
|
|
1719
|
+
return {
|
|
1720
|
+
documents,
|
|
1721
|
+
warnings: [...new Set(listWarnings)].sort((left, right) => left.localeCompare(right)),
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
for (const cachedDocument of cachedDocuments) {
|
|
1725
|
+
if (typeof cachedDocument.body === "string") {
|
|
1726
|
+
documents.push({
|
|
1727
|
+
metadata: cachedDocument.metadata,
|
|
1728
|
+
body: cachedDocument.body,
|
|
1729
|
+
});
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
const body = await readDocumentBody(cachedDocument.metadata, cachedDocument.item_path, cachedDocument.item_format);
|
|
1733
|
+
documents.push({
|
|
1734
|
+
metadata: cachedDocument.metadata,
|
|
1735
|
+
body,
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
return {
|
|
1739
|
+
documents,
|
|
1740
|
+
warnings: [...new Set(listWarnings)].sort((left, right) => left.localeCompare(right)),
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
/* c8 ignore stop */
|
|
1744
|
+
/* c8 ignore start -- field projection lookup precedence is validated by output-shaping integration tests */
|
|
1745
|
+
function readSearchFieldValue(hit, field) {
|
|
1746
|
+
const normalized = field.trim();
|
|
1747
|
+
if (normalized.length === 0) {
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
if (normalized === "score") {
|
|
1751
|
+
return hit.score;
|
|
1752
|
+
}
|
|
1753
|
+
if (normalized === "matched_fields") {
|
|
1754
|
+
return hit.matched_fields;
|
|
1755
|
+
}
|
|
1756
|
+
if (normalized.startsWith("item.")) {
|
|
1757
|
+
const itemKey = normalized.slice("item.".length);
|
|
1758
|
+
if (itemKey.length === 0) {
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
const itemRecord = toItemRecord(hit.item);
|
|
1762
|
+
return itemRecord[itemKey] ?? null;
|
|
1763
|
+
}
|
|
1764
|
+
const hitRecord = hit;
|
|
1765
|
+
const itemRecord = toItemRecord(hit.item);
|
|
1766
|
+
if (Object.prototype.hasOwnProperty.call(itemRecord, normalized)) {
|
|
1767
|
+
return itemRecord[normalized] ?? null;
|
|
1768
|
+
}
|
|
1769
|
+
if (Object.prototype.hasOwnProperty.call(hitRecord, normalized)) {
|
|
1770
|
+
return hitRecord[normalized] ?? null;
|
|
1771
|
+
}
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
/* c8 ignore stop */
|
|
1775
|
+
function projectSearchHits(hits, projection) {
|
|
1776
|
+
if (projection.mode === "full") {
|
|
1777
|
+
// matched_all_terms is an internal ranking signal (GH-181); strip it from
|
|
1778
|
+
// full-mode output rows so the public hit shape stays { item, score, matched_fields }.
|
|
1779
|
+
return hits.map(({ matched_all_terms: _matchedAllTerms, exact_id_match: _exactIdMatch, ...hit }) => hit);
|
|
1780
|
+
}
|
|
1781
|
+
return hits.map((hit) => {
|
|
1782
|
+
const projected = {};
|
|
1783
|
+
for (const field of projection.fields) {
|
|
1784
|
+
projected[field] = readSearchFieldValue(hit, field);
|
|
1785
|
+
}
|
|
1786
|
+
return projected;
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
function resolveQueryExpansionExtension(queryExpansion) {
|
|
1790
|
+
const provider = resolveExtensionSearchProviderByName(queryExpansion.provider ?? undefined);
|
|
1791
|
+
return provider?.queryExpansion
|
|
1792
|
+
? { providerName: provider.providerName, expand: provider.queryExpansion }
|
|
1793
|
+
: null;
|
|
1794
|
+
}
|
|
1795
|
+
function resolveRerankExtension(settings) {
|
|
1796
|
+
const provider = resolveExtensionSearchProviderByName(toOptionalNonEmptyString(settings.search?.provider));
|
|
1797
|
+
return provider?.rerank
|
|
1798
|
+
? { providerName: provider.providerName, rerank: provider.rerank }
|
|
1799
|
+
: null;
|
|
1800
|
+
}
|
|
1801
|
+
function prepareSearchInput(rawQuery, rawOptions) {
|
|
1802
|
+
const inlineWarnings = [];
|
|
1803
|
+
const inlineParse = parseInlineQueryFilters(rawQuery);
|
|
1804
|
+
const hasInlineFilters = Object.keys(inlineParse.inlineFilters).length > 0;
|
|
1805
|
+
if (hasInlineFilters && inlineParse.residualQuery.trim().length === 0) {
|
|
1806
|
+
throw new PmCliError("Inline field:value tokens consumed the entire query, leaving no search terms.", EXIT_CODE.USAGE, {
|
|
1807
|
+
examples: [
|
|
1808
|
+
"pm search auth tag:area:search",
|
|
1809
|
+
"pm list --tag area:search --status open",
|
|
1810
|
+
],
|
|
1811
|
+
nextSteps: [
|
|
1812
|
+
"Add keyword terms alongside the inline filters, or use pm list for pure tag/status/type/priority filtering.",
|
|
1813
|
+
],
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
const query = hasInlineFilters ? inlineParse.residualQuery : rawQuery;
|
|
1817
|
+
const options = applyInlineQueryFilters(rawOptions, inlineParse.inlineFilters, inlineWarnings);
|
|
1818
|
+
return {
|
|
1819
|
+
query,
|
|
1820
|
+
options,
|
|
1821
|
+
inlineWarnings,
|
|
1822
|
+
highlight: options.highlight === true,
|
|
1823
|
+
includeLinked: parseIncludeLinked(options.includeLinked),
|
|
1824
|
+
titleExact: parseTitleExact(options.titleExact),
|
|
1825
|
+
phraseExact: parsePhraseExact(options.phraseExact),
|
|
1826
|
+
matchMode: parseMatchMode(typeof options.matchMode === "string" ? options.matchMode : undefined),
|
|
1827
|
+
minScoreOverride: parseMinScoreOverride(options.minScore),
|
|
1828
|
+
countOnly: options.count === true,
|
|
1829
|
+
tokens: parseTokens(query),
|
|
1830
|
+
normalizedQuery: normalizeSearchPhrase(query),
|
|
1831
|
+
limit: parseLimit(options.limit),
|
|
1832
|
+
projection: parseProjectionConfig(options),
|
|
1833
|
+
modeWasExplicit: typeof options.mode === "string" && options.mode.trim().length > 0,
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
async function resolveSearchRuntimeContext(prepared, global) {
|
|
1837
|
+
const pmRoot = resolvePmRoot(process.cwd(), global.path);
|
|
1838
|
+
if (!(await pathExists(getSettingsPath(pmRoot)))) {
|
|
1839
|
+
throw new PmCliError(`Tracker is not initialized at ${pmRoot}. Run pm init first.`, EXIT_CODE.NOT_FOUND);
|
|
1840
|
+
}
|
|
1841
|
+
const storedSettings = await readSettings(pmRoot);
|
|
1842
|
+
const settings = resolveSettingsWithSemanticRuntimeDefaults(storedSettings).settings;
|
|
1843
|
+
const statusRegistry = resolveRuntimeStatusRegistry(settings.schema);
|
|
1844
|
+
const runtimeFieldRegistry = resolveRuntimeFieldRegistry(settings.schema);
|
|
1845
|
+
validateSearchProjectionFields(prepared.projection, runtimeFieldRegistry);
|
|
1846
|
+
const runtimeFieldFilters = collectRuntimeFilterValues(prepared.options, runtimeFieldRegistry, "search");
|
|
1847
|
+
const typeRegistry = resolveItemTypeRegistry(settings, getActiveExtensionRegistrations());
|
|
1848
|
+
const providerResolution = resolveEmbeddingProviders(settings);
|
|
1849
|
+
const vectorResolution = resolveVectorStores(settings, pmRoot);
|
|
1850
|
+
const extensionSearchProvider = resolveExtensionSearchProvider(settings);
|
|
1851
|
+
const extensionVectorAdapter = resolveExtensionVectorAdapter(settings);
|
|
1852
|
+
const semanticWeightOverride = parseSemanticWeightOverride(prepared.options.semanticWeight);
|
|
1853
|
+
const queryExpansion = resolveQueryExpansionConfig(settings, providerResolution.active?.name ?? null);
|
|
1854
|
+
const rerank = resolveRerankConfig(settings, providerResolution.active?.model ??
|
|
1855
|
+
toOptionalNonEmptyString(settings.search?.embedding_model) ??
|
|
1856
|
+
"text-embedding-3-small");
|
|
1857
|
+
return {
|
|
1858
|
+
pmRoot,
|
|
1859
|
+
settings,
|
|
1860
|
+
statusRegistry,
|
|
1861
|
+
runtimeFieldRegistry,
|
|
1862
|
+
runtimeFieldFilters,
|
|
1863
|
+
typeRegistry,
|
|
1864
|
+
maxResults: resolveSearchMaxResults(settings),
|
|
1865
|
+
scoreThreshold: prepared.minScoreOverride ?? resolveSearchScoreThreshold(settings),
|
|
1866
|
+
semanticWeightProvided: prepared.options.semanticWeight !== undefined,
|
|
1867
|
+
semanticWeightOverride,
|
|
1868
|
+
hybridSemanticWeight: semanticWeightOverride ?? resolveHybridSemanticWeight(settings),
|
|
1869
|
+
tuning: resolveSearchTuning(settings),
|
|
1870
|
+
providerResolution,
|
|
1871
|
+
vectorResolution,
|
|
1872
|
+
extensionSearchProvider,
|
|
1873
|
+
extensionVectorAdapter,
|
|
1874
|
+
bm25Mode: resolveBuiltInBm25Mode({
|
|
1875
|
+
configuredProvider: toOptionalNonEmptyString(settings.search?.provider),
|
|
1876
|
+
hasEmbeddingProvider: providerResolution.active !== null,
|
|
1877
|
+
hasExtensionSearchProvider: extensionSearchProvider !== null,
|
|
1878
|
+
}),
|
|
1879
|
+
queryExpansion,
|
|
1880
|
+
queryExpansionExtension: resolveQueryExpansionExtension(queryExpansion),
|
|
1881
|
+
rerank,
|
|
1882
|
+
rerankExtension: resolveRerankExtension(settings),
|
|
1883
|
+
effectiveMode: parseMode(prepared.options.mode, {
|
|
1884
|
+
hasProvider: providerResolution.active !== null || extensionSearchProvider !== null,
|
|
1885
|
+
hasVectorStore: vectorResolution.active !== null || extensionVectorAdapter !== null,
|
|
1886
|
+
}),
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
async function loadFilteredSearchCorpus(prepared, runtime) {
|
|
1890
|
+
const loadedDocuments = await loadDocuments(runtime.pmRoot, runtime.settings.item_format ?? "toon", runtime.typeRegistry.type_to_folder, runtime.settings.schema);
|
|
1891
|
+
const statusFilter = parseStatusFilterCsv(typeof prepared.options.status === "string"
|
|
1892
|
+
? prepared.options.status
|
|
1893
|
+
: undefined, runtime.statusRegistry, { strict: true });
|
|
1894
|
+
const lifecycleClassifier = lifecycleClassifierFromStatusRegistry(runtime.statusRegistry);
|
|
1895
|
+
const metadataFilteredDocuments = applyFilters(loadedDocuments.documents, prepared.options, runtime.typeRegistry, runtime.runtimeFieldFilters, statusFilter, lifecycleClassifier);
|
|
1896
|
+
return {
|
|
1897
|
+
warnings: loadedDocuments.warnings,
|
|
1898
|
+
allDocuments: loadedDocuments.documents,
|
|
1899
|
+
filteredDocuments: applyExactQueryFilters(metadataFilteredDocuments, prepared.normalizedQuery, {
|
|
1900
|
+
titleExact: prepared.titleExact,
|
|
1901
|
+
phraseExact: prepared.phraseExact || prepared.matchMode === "exact",
|
|
1902
|
+
}),
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
async function collectLinkedCorpusById(includeLinked, effectiveMode, filteredDocuments) {
|
|
1906
|
+
const linkedCorpusById = new Map();
|
|
1907
|
+
if (!includeLinked ||
|
|
1908
|
+
(effectiveMode !== "keyword" && effectiveMode !== "hybrid")) {
|
|
1909
|
+
return linkedCorpusById;
|
|
1910
|
+
}
|
|
1911
|
+
const projectRoot = process.cwd();
|
|
1912
|
+
const linkedCorpusRoots = await resolveLinkedCorpusRoots(projectRoot, resolveGlobalPmRoot(projectRoot));
|
|
1913
|
+
const linkedCorpusEntries = await Promise.all(filteredDocuments.map(async (document) => [
|
|
1914
|
+
document.metadata.id,
|
|
1915
|
+
await loadLinkedCorpus(document, linkedCorpusRoots),
|
|
1916
|
+
]));
|
|
1917
|
+
for (const [id, corpus] of linkedCorpusEntries) {
|
|
1918
|
+
linkedCorpusById.set(id, corpus);
|
|
1919
|
+
}
|
|
1920
|
+
return linkedCorpusById;
|
|
1921
|
+
}
|
|
1922
|
+
async function computeKeywordSearchHits(prepared, runtime, filteredDocuments) {
|
|
1923
|
+
const linkedCorpusById = await collectLinkedCorpusById(prepared.includeLinked, runtime.effectiveMode, filteredDocuments);
|
|
1924
|
+
const applyCoverageBonus = prepared.matchMode === "or";
|
|
1925
|
+
return filteredDocuments
|
|
1926
|
+
.map((document) => buildHybridLexicalScore(document, prepared.tokens, prepared.normalizedQuery, prepared.includeLinked, linkedCorpusById, runtime.tuning, runtime.settings.id_prefix, applyCoverageBonus))
|
|
1927
|
+
.filter((entry) => entry !== null)
|
|
1928
|
+
.filter((entry) => prepared.matchMode !== "and" || entry.matched_all_terms === true);
|
|
1929
|
+
}
|
|
1930
|
+
function buildEmptySearchResultFromContext(response, countOnly) {
|
|
1931
|
+
return emptySearchResult(response.query, response.effectiveMode, response.matchMode, response.options, response.includeLinked, response.scoreThreshold, response.hybridSemanticWeight, response.queryExpansion, response.rerank, response.projection, response.warnings, response.runtimeFieldFilters, countOnly);
|
|
1932
|
+
}
|
|
1933
|
+
async function executeBuiltInBm25Search(prepared, runtime, filteredDocuments, keywordHits, warnings) {
|
|
1934
|
+
if (runtime.bm25Mode === null || runtime.effectiveMode === "keyword") {
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
const hits = computeBuiltInBm25Hits({
|
|
1938
|
+
requestedMode: runtime.effectiveMode,
|
|
1939
|
+
query: prepared.query,
|
|
1940
|
+
filteredDocuments,
|
|
1941
|
+
keywordHits,
|
|
1942
|
+
corpusFields: resolveSearchCorpusFields(runtime.settings),
|
|
1943
|
+
bm25Params: resolveBm25Params(runtime.settings),
|
|
1944
|
+
hybridSemanticWeight: runtime.hybridSemanticWeight,
|
|
1945
|
+
});
|
|
1946
|
+
if (runtime.bm25Mode === "auto-fallback") {
|
|
1947
|
+
warnings.push(`search_${runtime.effectiveMode}_offline_bm25:no_embedding_provider:using_lexical_bm25`);
|
|
1948
|
+
}
|
|
1949
|
+
return hits;
|
|
1950
|
+
}
|
|
1951
|
+
async function executeExtensionSearchProvider(prepared, runtime, filteredDocuments, filteredById) {
|
|
1952
|
+
if (!runtime.extensionSearchProvider) {
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
const canUseBuiltInSemantic = runtime.providerResolution.active !== null &&
|
|
1956
|
+
(runtime.vectorResolution.active !== null ||
|
|
1957
|
+
runtime.extensionVectorAdapter !== null);
|
|
1958
|
+
try {
|
|
1959
|
+
const providerResponse = await Promise.resolve(runtime.extensionSearchProvider.query({
|
|
1960
|
+
query: prepared.query,
|
|
1961
|
+
mode: runtime.effectiveMode,
|
|
1962
|
+
tokens: prepared.tokens,
|
|
1963
|
+
options: prepared.options,
|
|
1964
|
+
settings: runtime.settings,
|
|
1965
|
+
documents: filteredDocuments,
|
|
1966
|
+
}));
|
|
1967
|
+
return normalizeExtensionProviderHits(runtime.extensionSearchProvider.providerName, providerResponse, filteredById);
|
|
1968
|
+
}
|
|
1969
|
+
catch (error) {
|
|
1970
|
+
if (!canUseBuiltInSemantic) {
|
|
1971
|
+
throw new PmCliError(`Extension search provider "${runtime.extensionSearchProvider.providerName}" failed: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODE.GENERIC_FAILURE);
|
|
1972
|
+
}
|
|
1973
|
+
return null;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
function maybeApplyNoVectorFallback(effectiveMode, semanticResult, keywordHits, warnings) {
|
|
1977
|
+
if (semanticResult.vectorMatchCount > 0) {
|
|
1978
|
+
return semanticResult.hits;
|
|
1979
|
+
}
|
|
1980
|
+
warnings.push(`search_${effectiveMode}_degraded:no_vector_matches:results_are_lexical`);
|
|
1981
|
+
return effectiveMode === "semantic" ? keywordHits : semanticResult.hits;
|
|
1982
|
+
}
|
|
1983
|
+
async function prepareBuiltInSemanticSearch(runtime, effectiveMode, filteredDocuments, warnings, modeWasExplicit) {
|
|
1984
|
+
const builtInSemanticWillRun = !runtime.extensionSearchProvider &&
|
|
1985
|
+
runtime.providerResolution.active !== null &&
|
|
1986
|
+
(runtime.vectorResolution.active !== null ||
|
|
1987
|
+
runtime.extensionVectorAdapter !== null);
|
|
1988
|
+
if (modeWasExplicit && builtInSemanticWillRun) {
|
|
1989
|
+
await maybeEmitVectorIndexStaleWarning(runtime.pmRoot, filteredDocuments, warnings);
|
|
1990
|
+
}
|
|
1991
|
+
if (!runtime.extensionSearchProvider) {
|
|
1992
|
+
requireSemanticDependencies(effectiveMode, runtime.providerResolution, runtime.vectorResolution, runtime.extensionVectorAdapter !== null);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
async function executeSemanticSearch(input) {
|
|
1996
|
+
const { prepared, runtime, filteredDocuments, keywordHits, warnings, modeWasExplicit, } = input;
|
|
1997
|
+
let hits = keywordHits;
|
|
1998
|
+
const bm25Hits = await executeBuiltInBm25Search(prepared, runtime, filteredDocuments, keywordHits, warnings);
|
|
1999
|
+
const semanticMode = runtime.effectiveMode;
|
|
2000
|
+
if (bm25Hits !== null || semanticMode === "keyword") {
|
|
2001
|
+
return { effectiveMode: runtime.effectiveMode, hits: bm25Hits ?? hits };
|
|
2002
|
+
}
|
|
2003
|
+
try {
|
|
2004
|
+
await prepareBuiltInSemanticSearch(runtime, semanticMode, filteredDocuments, warnings, modeWasExplicit);
|
|
2005
|
+
if (filteredDocuments.length === 0 || prepared.limit === 0) {
|
|
2006
|
+
return { effectiveMode: runtime.effectiveMode, hits: [] };
|
|
2007
|
+
}
|
|
2008
|
+
const filteredById = new Map(filteredDocuments.map((document) => [document.metadata.id, document]));
|
|
2009
|
+
hits =
|
|
2010
|
+
(await executeExtensionSearchProvider(prepared, runtime, filteredDocuments, filteredById)) ?? hits;
|
|
2011
|
+
if (hits === keywordHits) {
|
|
2012
|
+
const { provider, vectorStore } = requireSemanticDependencies(semanticMode, runtime.providerResolution, runtime.vectorResolution, runtime.extensionVectorAdapter !== null);
|
|
2013
|
+
const semanticResult = await computeSemanticOrHybridHits({
|
|
2014
|
+
requestedMode: semanticMode,
|
|
2015
|
+
query: prepared.query,
|
|
2016
|
+
filteredDocuments,
|
|
2017
|
+
keywordHits,
|
|
2018
|
+
hybridSemanticWeight: runtime.hybridSemanticWeight,
|
|
2019
|
+
limit: prepared.limit,
|
|
2020
|
+
maxResults: runtime.maxResults,
|
|
2021
|
+
provider,
|
|
2022
|
+
vectorStore,
|
|
2023
|
+
extensionVectorAdapter: runtime.extensionVectorAdapter,
|
|
2024
|
+
queryExpansion: runtime.queryExpansion,
|
|
2025
|
+
queryExpansionExtension: runtime.queryExpansionExtension,
|
|
2026
|
+
rerank: runtime.rerank,
|
|
2027
|
+
rerankExtension: runtime.rerankExtension,
|
|
2028
|
+
warnings,
|
|
2029
|
+
settings: runtime.settings,
|
|
2030
|
+
});
|
|
2031
|
+
hits = maybeApplyNoVectorFallback(semanticMode, semanticResult, keywordHits, warnings);
|
|
2032
|
+
}
|
|
2033
|
+
return { effectiveMode: runtime.effectiveMode, hits };
|
|
2034
|
+
}
|
|
2035
|
+
catch (error) {
|
|
2036
|
+
warnings.push(buildExplicitSemanticFallbackWarning(runtime.effectiveMode, error));
|
|
2037
|
+
return { effectiveMode: "keyword", hits: keywordHits };
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
function buildCountOnlySearchResult(response, total) {
|
|
2041
|
+
if (response.projection.mode === "compact" &&
|
|
2042
|
+
response.options.compact === true) {
|
|
2043
|
+
return withSearchWarnings({
|
|
2044
|
+
query: response.query.trim(),
|
|
2045
|
+
mode: response.effectiveMode,
|
|
2046
|
+
items: [],
|
|
2047
|
+
count: total,
|
|
2048
|
+
total,
|
|
2049
|
+
count_only: true,
|
|
2050
|
+
filters: buildCompactSearchFilterSummary({
|
|
2051
|
+
mode: response.effectiveMode,
|
|
2052
|
+
matchMode: response.matchMode,
|
|
2053
|
+
options: response.options,
|
|
2054
|
+
includeLinked: response.includeLinked,
|
|
2055
|
+
titleExact: response.titleExact,
|
|
2056
|
+
phraseExact: response.phraseExact,
|
|
2057
|
+
scoreThreshold: response.scoreThreshold,
|
|
2058
|
+
hybridSemanticWeight: response.hybridSemanticWeight,
|
|
2059
|
+
runtimeFieldFilters: response.runtimeFieldFilters,
|
|
2060
|
+
}),
|
|
2061
|
+
}, response.warnings);
|
|
2062
|
+
}
|
|
2063
|
+
const projectionFields = response.projection.mode === "full"
|
|
2064
|
+
? null
|
|
2065
|
+
: [...response.projection.fields];
|
|
2066
|
+
return withSearchWarnings({
|
|
2067
|
+
query: response.query.trim(),
|
|
2068
|
+
mode: response.effectiveMode,
|
|
2069
|
+
items: [],
|
|
2070
|
+
count: total,
|
|
2071
|
+
total,
|
|
2072
|
+
count_only: true,
|
|
2073
|
+
filters: buildVerboseSearchFilters({
|
|
2074
|
+
effectiveMode: response.effectiveMode,
|
|
2075
|
+
matchMode: response.matchMode,
|
|
2076
|
+
options: response.options,
|
|
2077
|
+
includeLinked: response.includeLinked,
|
|
2078
|
+
titleExact: response.titleExact,
|
|
2079
|
+
phraseExact: response.phraseExact,
|
|
2080
|
+
scoreThreshold: response.scoreThreshold,
|
|
2081
|
+
hybridSemanticWeight: response.hybridSemanticWeight,
|
|
2082
|
+
queryExpansion: response.queryExpansion,
|
|
2083
|
+
rerank: response.rerank,
|
|
2084
|
+
runtimeFieldFilters: response.runtimeFieldFilters,
|
|
2085
|
+
}),
|
|
2086
|
+
projection: {
|
|
2087
|
+
mode: response.projection.mode,
|
|
2088
|
+
fields: projectionFields,
|
|
2089
|
+
},
|
|
2090
|
+
now: nowIso(),
|
|
2091
|
+
}, response.warnings);
|
|
2092
|
+
}
|
|
2093
|
+
function withSearchWarnings(result, warnings) {
|
|
2094
|
+
if (warnings.length === 0) {
|
|
2095
|
+
return result;
|
|
2096
|
+
}
|
|
2097
|
+
return { ...result, warnings };
|
|
2098
|
+
}
|
|
2099
|
+
function attachSearchHighlights(prepared, filteredDocuments, limited) {
|
|
2100
|
+
if (!prepared.highlight) {
|
|
2101
|
+
return { hits: limited, projection: prepared.projection };
|
|
2102
|
+
}
|
|
2103
|
+
const documentById = new Map(filteredDocuments.map((document) => [document.metadata.id, document]));
|
|
2104
|
+
const hits = limited.map((hit) => ({
|
|
2105
|
+
...hit,
|
|
2106
|
+
highlights: buildHitHighlights(documentById.get(hit.item.id), hit.matched_fields, prepared.tokens),
|
|
2107
|
+
}));
|
|
2108
|
+
if (prepared.projection.mode === "full" ||
|
|
2109
|
+
prepared.projection.fields.includes("highlights")) {
|
|
2110
|
+
return { hits, projection: prepared.projection };
|
|
2111
|
+
}
|
|
2112
|
+
return {
|
|
2113
|
+
hits,
|
|
2114
|
+
projection: {
|
|
2115
|
+
mode: prepared.projection.mode,
|
|
2116
|
+
fields: [...prepared.projection.fields, "highlights"],
|
|
2117
|
+
},
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
function buildSearchResultForHits(response, projectedItems, total, limitedCount, effectiveProjection, pageExtras) {
|
|
2121
|
+
const truncationExtras = limitedCount < total ? { total } : {};
|
|
2122
|
+
if (response.projection.mode === "compact" &&
|
|
2123
|
+
response.options.compact === true) {
|
|
2124
|
+
return withSearchWarnings({
|
|
2125
|
+
query: response.query.trim(),
|
|
2126
|
+
mode: response.effectiveMode,
|
|
2127
|
+
items: projectedItems,
|
|
2128
|
+
count: projectedItems.length,
|
|
2129
|
+
...truncationExtras,
|
|
2130
|
+
...pageExtras,
|
|
2131
|
+
filters: buildCompactSearchFilterSummary({
|
|
2132
|
+
mode: response.effectiveMode,
|
|
2133
|
+
matchMode: response.matchMode,
|
|
2134
|
+
options: response.options,
|
|
2135
|
+
includeLinked: response.includeLinked,
|
|
2136
|
+
titleExact: response.titleExact,
|
|
2137
|
+
phraseExact: response.phraseExact,
|
|
2138
|
+
scoreThreshold: response.scoreThreshold,
|
|
2139
|
+
hybridSemanticWeight: response.hybridSemanticWeight,
|
|
2140
|
+
runtimeFieldFilters: response.runtimeFieldFilters,
|
|
2141
|
+
}),
|
|
2142
|
+
}, response.warnings);
|
|
2143
|
+
}
|
|
2144
|
+
const finalProjectionFields = effectiveProjection.mode === "full"
|
|
2145
|
+
? null
|
|
2146
|
+
: [...effectiveProjection.fields];
|
|
2147
|
+
return {
|
|
2148
|
+
query: response.query.trim(),
|
|
2149
|
+
mode: response.effectiveMode,
|
|
2150
|
+
items: projectedItems,
|
|
2151
|
+
count: projectedItems.length,
|
|
2152
|
+
...truncationExtras,
|
|
2153
|
+
...pageExtras,
|
|
2154
|
+
filters: buildVerboseSearchFilters({
|
|
2155
|
+
effectiveMode: response.effectiveMode,
|
|
2156
|
+
matchMode: response.matchMode,
|
|
2157
|
+
options: response.options,
|
|
2158
|
+
includeLinked: response.includeLinked,
|
|
2159
|
+
titleExact: response.titleExact,
|
|
2160
|
+
phraseExact: response.phraseExact,
|
|
2161
|
+
scoreThreshold: response.scoreThreshold,
|
|
2162
|
+
hybridSemanticWeight: response.hybridSemanticWeight,
|
|
2163
|
+
queryExpansion: response.queryExpansion,
|
|
2164
|
+
rerank: response.rerank,
|
|
2165
|
+
runtimeFieldFilters: response.runtimeFieldFilters,
|
|
2166
|
+
}),
|
|
2167
|
+
projection: {
|
|
2168
|
+
mode: effectiveProjection.mode,
|
|
2169
|
+
fields: finalProjectionFields,
|
|
2170
|
+
},
|
|
2171
|
+
now: nowIso(),
|
|
2172
|
+
...(response.warnings.length > 0 ? { warnings: response.warnings } : {}),
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
function assertSearchPagingOptions(options) {
|
|
2176
|
+
if (options.after !== undefined && options.count === true) {
|
|
2177
|
+
throw new PmCliError("Search --after cannot be combined with --count.", EXIT_CODE.USAGE);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
/** Implements run search for the public runtime surface of this module. */
|
|
2181
|
+
export async function runSearch(rawQuery, rawOptions, global) {
|
|
2182
|
+
assertSearchPagingOptions(rawOptions);
|
|
2183
|
+
const prepared = prepareSearchInput(rawQuery, rawOptions);
|
|
2184
|
+
const runtime = await resolveSearchRuntimeContext(prepared, global);
|
|
2185
|
+
const corpus = await loadFilteredSearchCorpus(prepared, runtime);
|
|
2186
|
+
const warnings = corpus.warnings;
|
|
2187
|
+
warnings.push(...prepared.inlineWarnings);
|
|
2188
|
+
if (runtime.effectiveMode === "hybrid" &&
|
|
2189
|
+
runtime.semanticWeightProvided &&
|
|
2190
|
+
runtime.semanticWeightOverride === undefined) {
|
|
2191
|
+
warnings.push("search_hybrid_semantic_weight_override_invalid:using_settings_default");
|
|
2192
|
+
}
|
|
2193
|
+
const responseBase = {
|
|
2194
|
+
query: prepared.query,
|
|
2195
|
+
matchMode: prepared.matchMode,
|
|
2196
|
+
options: prepared.options,
|
|
2197
|
+
includeLinked: prepared.includeLinked,
|
|
2198
|
+
titleExact: prepared.titleExact,
|
|
2199
|
+
phraseExact: prepared.phraseExact,
|
|
2200
|
+
scoreThreshold: runtime.scoreThreshold,
|
|
2201
|
+
hybridSemanticWeight: runtime.hybridSemanticWeight,
|
|
2202
|
+
queryExpansion: runtime.queryExpansion,
|
|
2203
|
+
rerank: runtime.rerank,
|
|
2204
|
+
projection: prepared.projection,
|
|
2205
|
+
warnings,
|
|
2206
|
+
runtimeFieldFilters: runtime.runtimeFieldFilters,
|
|
2207
|
+
};
|
|
2208
|
+
if (runtime.effectiveMode === "keyword" &&
|
|
2209
|
+
(corpus.filteredDocuments.length === 0 ||
|
|
2210
|
+
(prepared.limit === 0 && !prepared.countOnly))) {
|
|
2211
|
+
return buildEmptySearchResultFromContext({ ...responseBase, effectiveMode: runtime.effectiveMode }, prepared.countOnly);
|
|
2212
|
+
}
|
|
2213
|
+
const keywordHits = await computeKeywordSearchHits(prepared, runtime, corpus.filteredDocuments);
|
|
2214
|
+
const modeResult = await executeSemanticSearch({
|
|
2215
|
+
prepared,
|
|
2216
|
+
runtime,
|
|
2217
|
+
filteredDocuments: corpus.filteredDocuments,
|
|
2218
|
+
keywordHits,
|
|
2219
|
+
warnings,
|
|
2220
|
+
modeWasExplicit: prepared.modeWasExplicit,
|
|
2221
|
+
});
|
|
2222
|
+
if (modeResult.effectiveMode !== "keyword" &&
|
|
2223
|
+
modeResult.hits.length === 0 &&
|
|
2224
|
+
(corpus.filteredDocuments.length === 0 ||
|
|
2225
|
+
(prepared.limit === 0 && !prepared.countOnly))) {
|
|
2226
|
+
return buildEmptySearchResultFromContext({ ...responseBase, effectiveMode: modeResult.effectiveMode }, prepared.countOnly);
|
|
2227
|
+
}
|
|
2228
|
+
const thresholded = modeResult.hits.filter((entry) => entry.exact_id_match === true || entry.score >= runtime.scoreThreshold);
|
|
2229
|
+
const sorted = sortHits(thresholded, runtime.statusRegistry);
|
|
2230
|
+
const total = sorted.length;
|
|
2231
|
+
const page = resolveSearchPage({
|
|
2232
|
+
sorted,
|
|
2233
|
+
query: prepared.query,
|
|
2234
|
+
mode: modeResult.effectiveMode,
|
|
2235
|
+
searchOptions: prepared.options,
|
|
2236
|
+
limit: prepared.limit ?? runtime.maxResults,
|
|
2237
|
+
});
|
|
2238
|
+
const response = { ...responseBase, effectiveMode: modeResult.effectiveMode };
|
|
2239
|
+
if (prepared.countOnly) {
|
|
2240
|
+
return buildCountOnlySearchResult(response, total);
|
|
2241
|
+
}
|
|
2242
|
+
const { hits: projectedHits, projection: effectiveProjection } = attachSearchHighlights(prepared, corpus.filteredDocuments, page.limited);
|
|
2243
|
+
const projectedItems = projectSearchHits(projectedHits, effectiveProjection);
|
|
2244
|
+
return buildSearchResultForHits(response, projectedItems, total, page.limited.length, effectiveProjection, page.pageExtras);
|
|
2245
|
+
}
|
|
2246
|
+
//# sourceMappingURL=search.js.map
|
|
2247
|
+
//# debugId=7d6721eb-1e28-58ea-a5fc-4494f249275c
|