From d3518ae3e71c363a98500db035565c1b9ec43021 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 22 May 2026 23:13:15 +0800 Subject: [PATCH] chore: remove cli qmd support --- cli-e2e/spec/non_sync_cases.edn | 102 --- ...04-markdown-mirror-block-db-id-comments.md | 466 +--------- docs/agent-guide/logseq-cli/011-qmd-search.md | 76 -- .../012-qsearch-page-group-output.md | 435 --------- docs/cli/logseq-cli.md | 2 - src/main/frontend/worker/markdown_mirror.cljs | 17 +- src/main/logseq/cli/command/core.cljs | 5 +- src/main/logseq/cli/command/qmd.cljs | 532 ----------- src/main/logseq/cli/command/show.cljs | 9 - src/main/logseq/cli/commands.cljs | 59 +- src/main/logseq/cli/format.cljs | 149 ---- .../frontend/worker/markdown_mirror_test.cljs | 197 ++-- src/test/logseq/cli/command/qmd_test.cljs | 840 ------------------ src/test/logseq/cli/commands_test.cljs | 94 +- src/test/logseq/cli/format_test.cljs | 185 ---- 15 files changed, 120 insertions(+), 3048 deletions(-) delete mode 100644 docs/agent-guide/logseq-cli/011-qmd-search.md delete mode 100644 docs/agent-guide/logseq-cli/012-qsearch-page-group-output.md delete mode 100644 src/main/logseq/cli/command/qmd.cljs delete mode 100644 src/test/logseq/cli/command/qmd_test.cljs diff --git a/cli-e2e/spec/non_sync_cases.edn b/cli-e2e/spec/non_sync_cases.edn index bb60d1a195..d56d673ac0 100644 --- a/cli-e2e/spec/non_sync_cases.edn +++ b/cli-e2e/spec/non_sync_cases.edn @@ -610,108 +610,6 @@ :search ["--content"]}}, :tags [:upsert :search], :extends :non-sync/graph-json-env} - {:id "qmd-and-qsearch-json", - :setup - ["python3 - <<'PY'\nimport base64, pathlib\np = pathlib.Path('{{tmp-dir}}/fake-bin/qmd')\np.parent.mkdir(parents=True, exist_ok=True)\np.write_bytes(base64.b64decode('IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwppbXBvcnQganNvbgppbXBvcnQgb3MKaW1wb3J0IHBhdGhsaWIKaW1wb3J0IHJlCmltcG9ydCBzeXMKCmFyZ3MgPSBzeXMuYXJndlsxOl0Kc3RhdGUgPSBwYXRobGliLlBhdGgob3MuZW52aXJvblsnUU1EX0ZBS0VfU1RBVEUnXSkKCmlmIGFyZ3MgPT0gWyctLWhlbHAnXToKICAgIHByaW50KCdxbWQgaGVscCcpCiAgICBzeXMuZXhpdCgwKQoKaWYgYXJnc1s6Ml0gPT0gWydjb2xsZWN0aW9uJywgJ3Nob3cnXToKICAgIGlmIG5vdCBzdGF0ZS5leGlzdHMoKToKICAgICAgICBwcmludCgnQ29sbGVjdGlvbiBub3QgZm91bmQnLCBmaWxlPXN5cy5zdGRlcnIpCiAgICAgICAgc3lzLmV4aXQoMSkKICAgIHByaW50KCdDb2xsZWN0aW9uOiAnICsgYXJnc1syXSkKICAgIHByaW50KCcgIFBhdGg6ICAgICAnICsgc3RhdGUucmVhZF90ZXh0KCkuc3RyaXAoKSkKICAgIHByaW50KCcgIFBhdHRlcm46ICAqKi8qLm1kJykKICAgIHN5cy5leGl0KDApCgppZiBhcmdzWzoyXSA9PSBbJ2NvbGxlY3Rpb24nLCAnYWRkJ106CiAgICBzdGF0ZS53cml0ZV90ZXh0KGFyZ3NbMl0pCiAgICBwcmludCgnY3JlYXRlZCcpCiAgICBzeXMuZXhpdCgwKQoKaWYgYXJnc1s6MV0gPT0gWydlbWJlZCddOgogICAgcHJpbnQoJ2VtYmVkZGVkJykKICAgIHN5cy5leGl0KDApCgppZiBhcmdzWzoxXSA9PSBbJ3VwZGF0ZSddOgogICAgcHJpbnQoJ3VwZGF0ZWQnKQogICAgc3lzLmV4aXQoMCkKCmlmIGFyZ3NbOjFdID09IFsncXVlcnknXToKICAgIHJvb3QgPSBwYXRobGliLlBhdGgoc3RhdGUucmVhZF90ZXh0KCkuc3RyaXAoKSkKICAgIHJvd3MgPSBbXQogICAgZm9yIHBhdGggaW4gcm9vdC5yZ2xvYignKi5tZCcpOgogICAgICAgIHRleHQgPSBwYXRoLnJlYWRfdGV4dChlbmNvZGluZz0ndXRmOCcpCiAgICAgICAgbWF0Y2ggPSByZS5zZWFyY2gocic8IS0tIGlkOiAoXGQrKSAtLT4nLCB0ZXh0KQogICAgICAgIGlmIG1hdGNoOgogICAgICAgICAgICByb3dzLmFwcGVuZCh7CiAgICAgICAgICAgICAgICAnc2NvcmUnOiAxLAogICAgICAgICAgICAgICAgJ2ZpbGUnOiAncW1kOi8vY3VzdG9tLycgKyBwYXRoLm5hbWUsCiAgICAgICAgICAgICAgICAnc25pcHBldCc6ICctIHFtZCB0YXJnZXQgPCEtLSBpZDogJyArIG1hdGNoLmdyb3VwKDEpICsgJyAtLT4nLAogICAgICAgICAgICB9KQogICAgICAgICAgICBicmVhawogICAgcHJpbnQoanNvbi5kdW1wcyhyb3dzKSkKICAgIHN5cy5leGl0KDApCgpwcmludCgndW5leHBlY3RlZCBhcmdzOiAnICsgcmVwcihhcmdzKSwgZmlsZT1zeXMuc3RkZXJyKQpzeXMuZXhpdCgyKQo='))\np.chmod(0o755)\nPY" - "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page QmdSearchPage --content \"qmd target\" >/dev/null"], - :cmds - ["QMD_FAKE_STATE={{tmp-dir-arg}}/qmd-state PATH={{tmp-dir-arg}}/fake-bin:$PATH {{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json qmd --graph {{graph-arg}}" - "QMD_FAKE_STATE={{tmp-dir-arg}}/qmd-state PATH={{tmp-dir-arg}}/fake-bin:$PATH {{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json qsearch qmd target --graph {{graph-arg}} --no-rerank"], - :expect - {:exit 0, - :stdout-json-paths {[:status] "ok"}, - :stdout-contains ["qmd target"]}, - :covers - {:commands ["qmd" "qsearch"], - :options - {:global ["--config" "--graph" "--root-dir" "--output"], - :qmd [] - :qsearch ["--no-rerank"]}}, - :tags [:qmd :search], - :extends :non-sync/graph-json-env} - {:id "qmd-and-qsearch-human-page-groups", - :setup - ["python3 - <<'PY' -import pathlib -p = pathlib.Path('{{tmp-dir}}/fake-bin/qmd') -p.parent.mkdir(parents=True, exist_ok=True) -p.write_text(r'''#!/usr/bin/env python3 -import json -import os -import pathlib -import re -import sys - -args = sys.argv[1:] -state = pathlib.Path(os.environ['QMD_FAKE_STATE']) - -if args == ['--help']: - print('qmd help') - sys.exit(0) - -if args[:2] == ['collection', 'show']: - if not state.exists(): - print('Collection not found', file=sys.stderr) - sys.exit(1) - print('Collection: ' + args[2]) - print(' Path: ' + state.read_text().strip()) - print(' Pattern: **/*.md') - sys.exit(0) - -if args[:2] == ['collection', 'add']: - state.write_text(args[2]) - print('created') - sys.exit(0) - -if args[:1] == ['embed']: - print('embedded') - sys.exit(0) - -if args[:1] == ['update']: - print('updated') - sys.exit(0) - -if args[:1] == ['query']: - root = pathlib.Path(state.read_text().strip()) - rows = [] - for path in sorted(root.rglob('*.md')): - text = path.read_text(encoding='utf8') - match = re.search(r'', text) - if match: - rows.append({ - 'score': 1, - 'file': 'qmd://custom/' + path.name, - 'snippet': '- qmd target ', - }) - break - print(json.dumps(rows)) - sys.exit(0) - -print('unexpected args: ' + repr(args), file=sys.stderr) -sys.exit(2) -''') -p.chmod(0o755) -PY" - "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name QmdHumanTag >/dev/null" - "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert property --graph {{graph-arg}} --name qmd-human-prop --public true >/dev/null" - "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page QmdHumanPage --content \"qmd target\" --update-tags '[\"QmdHumanTag\"]' --update-properties '{:qmd-human-prop \"rich value\"}' >/dev/null"], - :cmds - ["QMD_FAKE_STATE={{tmp-dir-arg}}/qmd-human-state PATH={{tmp-dir-arg}}/fake-bin:$PATH {{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json qmd --graph {{graph-arg}} >/dev/null" - "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json graph switch --graph {{graph-arg}} >/dev/null" - "QMD_FAKE_STATE={{tmp-dir-arg}}/qmd-human-state PATH={{tmp-dir-arg}}/fake-bin:$PATH {{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output human qsearch qmd target --no-rerank"], - :expect - {:exit 0, - :stdout-contains ["QmdHumanPage" "qmd target #QmdHumanTag" "qmd-human-prop: rich value" "└──"], - :stdout-not-contains ["QmdHumanPage (1 match)" "PAGE-TITLE" "Count:"]}, - :covers - {:commands ["upsert tag" "upsert property" "qmd" "graph switch" "qsearch"], - :options - {:global ["--config" "--graph" "--root-dir" "--output"], - :upsert ["--name" "--target-page" "--content" "--update-tags" "--update-properties"], - :qmd [] - :qsearch ["--no-rerank"]}}, - :tags [:qmd :search], - :extends :non-sync/graph-json-env} {:id "search-page-json", :setup ["{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SearchPageTarget >/dev/null"], diff --git a/docs/agent-guide/db-worker/004-markdown-mirror-block-db-id-comments.md b/docs/agent-guide/db-worker/004-markdown-mirror-block-db-id-comments.md index 722520e845..386c62358a 100644 --- a/docs/agent-guide/db-worker/004-markdown-mirror-block-db-id-comments.md +++ b/docs/agent-guide/db-worker/004-markdown-mirror-block-db-id-comments.md @@ -1,459 +1,21 @@ -# Markdown Mirror Block db/id Comments Implementation Plan +# Markdown Mirror Block ID Comments -Goal: Add each mirrored non-code block's Datascript `:db/id` to the end of that block's first rendered Markdown line as an HTML comment such as ``. +Status: Superseded. -Architecture: Keep the behavior inside the existing markdown mirror renderer that already runs in the db-worker runtime. -Architecture: Reuse the current block-to-rendered-line alignment in `frontend.worker.markdown-mirror` and enrich the existing block line metadata with `:db/id`. -Architecture: Do not add a new `thread-api` unless implementation proves that the existing markdown mirror setting, flush, and regenerate APIs cannot cover this change. - -Tech Stack: ClojureScript, Datascript, Promesa, existing db-worker-node HTTP invoke bridge, existing markdown mirror worker module, existing `frontend.worker.platform` storage abstraction. - -Related: Builds on `docs/agent-guide/db-worker/001-db-worker-node-restart-on-version-mismatch.md`. -Related: Relates to `docs/agent-guide/db-worker/002-desktop-db-worker-request-cap-switch-graph.md`. -Related: Relates to `docs/agent-guide/db-worker/003-server-list-write-lock.md`. -Related: Relates to `docs/agent-guide/074-db-worker-node-invoke-main-thread-refactor.md`. - -## Problem statement - -Markdown mirror currently writes derived Markdown files under `/mirror/markdown` for DB graphs. - -The generated files include a page-level `id:: ` marker at the top of each mirrored page. - -The generated block lines do not expose the corresponding block entity id. - -The requested markdown mirror feature is to append a comment-format block database id to every non-code block's first line. - -Code blocks are excluded and must not receive generated db/id comments. - -The required comment format is ``. - -The required placement is at the end of the first physical rendered line for each block. - -For example, a block rendered as two physical lines should become: +Markdown Mirror files must not append generated block database ids to rendered +block lines. Keep mirrored Markdown clean and user-readable: ```markdown -- block line1 - block line2 ... +id:: 11111111-1111-4111-8111-111111111111 + +- hello +- world ``` -The continuation line should not receive another id comment. +The page-level `id:: ` line remains part of the mirror format because it +identifies the page without changing the friendly file name. Block-level +identity must not be encoded as HTML comments in the mirrored Markdown. -The page-level `id:: ` marker should remain unchanged because the request targets block first lines. - -## Testing Plan - -I will use @Test-Driven Development (TDD) before changing implementation behavior. - -I will add a focused unit test in `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs` that mirrors a page with two top-level blocks and asserts that each `- ...` line ends with ``. - -I will make that focused test fail on the current implementation because the generated block lines currently do not include db/id comments. - -I will add a unit test in `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs` for a multi-line block and assert that only the first rendered block line receives the id comment. - -I will add a unit test in `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs` for nested blocks and assert that parent and child first lines each get their own db/id comment while indentation remains unchanged. - -I will add a unit test in `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs` for code blocks and assert that code block fence lines do not receive db/id comments. - -I will add a unit test in `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs` for page and block property exports and assert that property value bullet lines are not treated as separate block first lines. - -I will update existing tests in `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs` whose expected mirrored content includes block lines. - -I will preserve tests that assert page-level `id:: ` markers because the page marker format is not part of this change. - -I will run focused tests before implementation with a command such as `bb dev:test -v frontend.worker.markdown-mirror-test/block-db-id-comments-are-written-to-each-block-first-line-test` and confirm the new test fails for the expected missing comment. - -I will run the focused markdown mirror namespace after implementation with `bb dev:test -v frontend.worker.markdown-mirror-test` if namespace-level targeting is supported by the local test runner. - -I will run each newly added focused test by full test var name if namespace-level targeting is not supported. - -I will run `bb dev:lint-and-test` after the focused tests pass. - -NOTE: I will write *all* tests before I add any implementation behavior. - -## Current implementation snapshot - -| Concern | Current location | Current behavior | Change impact | -|---------|------------------|------------------|---------------| -| Markdown mirror module | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs` | Owns mirror path derivation, page selection, rendering, queued tx handling, and file writes. | This is the primary implementation target. | -| Page render entry | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs` | `render-page-content` calls `common-file/block->content` and then `add-page-id-to-rendered-content`. | Add non-code block id comments after the existing content render step. | -| Page marker | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs` | `add-page-id-to-rendered-content` prepends `id:: `. | Keep unchanged. | -| Block line alignment | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs` | `rendered-block-line-infos` walks visible outline blocks in render order. | Extend each metadata map with `:db/id`. | -| Block decoration | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs` | `decorate-block-line` adds status markers and tags to Markdown block lines. | Append the db/id comment after existing status and tag decoration for non-code blocks only. | -| Property value protection | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs` | `property-value-line?` prevents property value lines from consuming block metadata. | Keep this behavior so property value list items do not get block comments. | -| Existing thread APIs | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs` | `:thread-api/markdown-mirror-set-enabled`, `:thread-api/markdown-mirror-flush`, and `:thread-api/markdown-mirror-regenerate` already exist. | No new `thread-api` should be needed. | -| db-worker-node invoke bridge | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_worker_node.cljs` | `/v1/invoke` proxies existing `thread-api` calls into the worker core. | No protocol or endpoint change should be needed. | -| Desktop setting sync | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/persist_db.cljs` | Pushes `:feature/markdown-mirror?` to the worker through the existing set-enabled API. | No Desktop IPC change should be needed. | -| Browser worker setting sync | `/Users/rcmerci/gh-repos/logseq/src/main/frontend/persist_db/browser.cljs` | Pushes `:feature/markdown-mirror?` to the in-browser worker through the same set-enabled API. | Existing tests may need expected content updates only if they assert mirror output. | -| Tests | `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs` | Contains fake platform storage tests for rendering, writes, deletes, regeneration, and tx handling. | Add behavior tests here. | - -## Target behavior - -Every rendered Logseq block first line in a markdown mirror file must end with exactly one generated db/id comment, except code blocks. - -Code blocks must not receive a generated db/id comment because appending an HTML comment to the fence line can break Markdown fence semantics. - -The comment format must be ``. - -The `` value must come from the block entity's numeric Datascript `:db/id`. - -The comment must be appended after the existing rendered block content, status marker, and tag decorations. - -The comment must be appended to the first physical line emitted for that block only. - -Continuation lines for multi-line block content must not receive id comments. - -Nested block indentation must remain unchanged. - -Property lines and property value lines must not consume block metadata and must not receive generated block db/id comments unless they are the first line of a real rendered block. - -The top-level page marker must remain `id:: `. - -Mirrored files must remain derived files under `mirror/markdown`, which is already ignored by graph scanning. - -The implementation must not add a new `thread-api` for this rendering-only change unless the existing APIs cannot trigger all required writes. - -## Proposed rendered examples - -A page with two top-level blocks should render like this. - -```markdown -id:: 33333333-3333-4333-8333-333333333333 - -- hello -- world -``` - -A multi-line block should render like this. - -```markdown -id:: 33333333-3333-4333-8333-333333333333 - -- block line1 - block line2 -``` - -A nested outline should render like this. - -```markdown -id:: 33333333-3333-4333-8333-333333333333 - -- parent - - child -``` - -A block with exported properties should render the comment on the block first line only. - -```markdown -id:: 33333333-3333-4333-8333-333333333333 - -- TODO body - * rating:: 5 - * notes:: - - property value bullet -``` - -## Design - -### 1. Keep the change inside `frontend.worker.markdown-mirror` - -Make the implementation in `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs`. - -Do not add a db-worker-node HTTP endpoint. - -Do not add Electron IPC. - -Do not add a new `thread-api`. - -The current thread APIs already cause markdown mirror writes through setting sync, transaction listeners, flush, and full regeneration. - -### 2. Add block db/id to rendered block metadata - -Update `block-line-info` in `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs` to include the block's `:db/id`. - -Keep the existing `:status-marker` and `:tag-tokens` entries. - -The metadata map should represent the real block entity that corresponds to one rendered Markdown block line. - -The metadata map should include enough information to identify code blocks, such as the block display type or code block class information already available on the block entity. - -The code can assume real outline blocks have `:db/id` because they come from Datascript entities in a controlled worker path. - -### 3. Add a small comment formatter - -Add a private helper in `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs` for rendering the generated comment. - -The helper should return the exact string ``. - -The helper should not use the page UUID. - -The helper should not use the block UUID. - -The helper should not localize or translate the comment. - -The helper should not add extra metadata fields unless requested later. - -### 4. Append the comment in block line decoration - -Update `decorate-block-line` in `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs`. - -The function already recognizes rendered block first lines with `markdown-block-re`. - -After it builds the decorated block content, append a single space and the db/id comment at the end of that line when the block is not a code block. - -This keeps the status marker and tag behavior stable because those transformations already happen inside `decorate-block-content`. - -For code block first lines, keep returning the decorated line without a db/id comment. - -For non-block lines, keep returning the line unchanged. - -Do not append comments in `add-page-id-to-rendered-content` directly because that function also tracks property lines and continuation lines. - -Keep `add-page-id-to-rendered-content` responsible for consuming block metadata only when it sees `markdown-block-re` and is not inside a property value. - -### 5. Preserve property value handling - -Do not change `property-line-indent`, `property-value-line?`, `markdown-property-line-re`, or `property-line-re` unless a test proves the current logic consumes block metadata incorrectly after the comment change. - -The current algorithm intentionally avoids consuming block metadata for property value lines. - -This matters because exported default property values can render as Markdown list items that look like blocks but are not outline blocks. - -### 6. Preserve db-worker-node contracts - -Do not change `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_worker_node.cljs` for this feature. - -Do not change `/Users/rcmerci/gh-repos/logseq/src/main/frontend/persist_db/remote.cljs` for this feature. - -Do not change `/Users/rcmerci/gh-repos/logseq/src/main/frontend/persist_db.cljs` for this feature unless tests show the existing mirror setting sync fails to trigger the updated renderer. - -Do not add a new `thread-api` in `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs`. - -The only expected `db_core.cljs` edits are test expectation updates if existing API enumeration tests depend on generated output or function order. - -## Implementation tasks - -### Task 1. Add failing top-level block id comment test - -Edit `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs`. - -Create a test named `block-db-id-comments-are-written-to-each-block-first-line-test`. - -Create a fake platform with `fake-platform`. - -Create a test DB with one page and two top-level blocks. - -Find the page and the block entities from the created DB. - -Call `markdown-mirror/ -->` comments. - -Run the focused test and confirm it fails because current output lacks comments. - -### Task 2. Add failing multi-line block test - -Edit `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs`. - -Create a test named `block-db-id-comment-is-only-written-to-first-rendered-line-test`. - -Use a block title containing a newline. - -Assert that the first rendered `- ...` line has the id comment. - -Assert that the continuation line does not have an id comment. - -Run the focused test and confirm it fails for the expected missing first-line comment. - -### Task 3. Add failing nested block test - -Edit `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs`. - -Create a test named `nested-block-db-id-comments-preserve-indent-test`. - -Use a parent block with a child block. - -Assert that both first lines get their own ids. - -Assert that the child line keeps its existing indentation before `-`. - -Run the focused test and confirm it fails for the expected missing comments. - -### Task 4. Add code block exclusion regression test - -Edit `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs`. - -Create a test named `code-blocks-do-not-receive-db-id-comments-test`. - -Use a code block with `:logseq.property.node/display-type :code` and a language property. - -Assert that the rendered code fence line does not include ``. - -Assert that a normal block in the same page still receives its db/id comment. - -Run the focused test and confirm it passes on the exclusion expectation only after the implementation can distinguish code blocks from normal blocks. - -### Task 5. Add or update property export coverage - -Edit `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs`. - -Prefer adding a focused test if existing property tests become too broad after expectation updates. - -Assert that a real non-code block line with properties receives the db/id comment. - -Assert that nested property value list items do not receive db/id comments and do not consume the next block's metadata. - -Run the focused test and confirm it fails for the expected missing block comment before implementation. - -### Task 6. Implement metadata enrichment - -Edit `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs`. - -Update `block-line-info` to include the block `:db/id`. - -Keep existing status and tag metadata untouched. - -Run the focused tests and confirm they still fail until the formatter is used. - -### Task 7. Implement the comment formatter and line append - -Edit `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/markdown_mirror.cljs`. - -Add a private helper for the exact comment format. - -Update `decorate-block-line` to append the comment to matched Markdown block first lines for non-code blocks only. - -Keep code block lines and non-block lines unchanged for comment purposes. - -Run all focused tests and confirm the new tests pass. - -### Task 8. Update existing markdown mirror expectations - -Edit `/Users/rcmerci/gh-repos/logseq/src/test/frontend/worker/markdown_mirror_test.cljs`. - -Update expected mirrored content strings that include block lines. - -Use actual block entity ids from the test DB rather than hard-coded guesses when possible. - -Do not update page marker expectations to use db/id because page markers should remain UUID-based. - -Run the full markdown mirror test namespace or each related focused test. - -### Task 9. Verify db-worker-node API stability - -Inspect the diff for `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs`. - -Confirm no new `def-thread-api` was added. - -Inspect the diff for `/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_worker_node.cljs`. - -Confirm no HTTP route, invoke contract, or repo validation behavior changed. - -Inspect the diff for `/Users/rcmerci/gh-repos/logseq/src/main/frontend/persist_db.cljs` and `/Users/rcmerci/gh-repos/logseq/src/main/frontend/persist_db/remote.cljs`. - -Confirm no Desktop runtime or SSE behavior changed. - -### Task 10. Run verification commands - -Run the newly added focused tests first. - -Run existing focused markdown mirror tests that were updated. - -Run `bb dev:lint-and-test` before final review. - -If `bb dev:lint-and-test` is too slow or blocked by unrelated local failures, record the exact command, failure, and the focused commands that passed. - -### Task 11. Review the implementation with @logseq-review-workflow - -After implementation is complete, invoke @logseq-review-workflow on the final diff. - -Apply the repository-wide review rules from `.agents/skills/logseq-review-workflow/rules/common.md`. - -Route the ClojureScript changes through the Clojure/CLJS library rules. - -Route the Datascript entity and query assumptions through the Datascript library rules. - -Route the markdown mirror output change through the import/export module rules. - -Route any db-worker-node or `thread-api` diff through the Logseq CLI, Electron main, or db-worker related rules only if those files changed. - -Fix any blocking or important findings before considering the implementation done. - -## Edge cases - -A block with an empty title should still receive a comment on the bullet line. - -A block with status markers should render the status marker before the db/id comment. - -A block with tags added by `decorate-block-content` should render tags before the db/id comment. - -A heading block should keep the heading marker and append the db/id comment after the rendered heading text. - -A quote block should append the db/id comment to the first quote block line only. - -A code block currently renders its first line as the block line that opens the fence. - -The implementation must not append the db/id comment to the code fence line. - -A multi-line code block should not add comments to the fence line or code body lines. - -Property value bullets should not receive generated block comments because they are derived property values, not outline blocks. - -Deleted pages and excluded pages should keep their current skip or delete behavior. - -Renamed pages should keep the current old-path deletion behavior. - -Full regeneration should write the same comment format as transaction-triggered mirroring. - -Repeated mirror writes should not accumulate duplicate comments because the mirror content is regenerated from the Datascript DB, not patched from the previous file content. - -## Non-goals - -Do not make markdown mirror files editable source files. - -Do not parse the generated `` comments back into the graph. - -Do not add a migration. - -Do not add a new built-in property. - -Do not change page `id:: ` markers. - -Do not change graph ignore rules for `mirror/markdown`. - -Do not add localized UI text. - -Do not add backward compatibility for older mirrored file formats because mirror files are derived artifacts. - -## Testing Details - -The primary tests should exercise behavior through `markdown-mirror/ -->` from `decorate-block-line` after existing content decoration for non-code blocks only. -- Keep code block output unchanged for db/id comments. -- Keep page-level `id:: ` output unchanged. -- Keep property value line handling unchanged unless a failing behavior test proves it must change. -- Avoid touching `db_worker_node.cljs`, `remote.cljs`, and `persist_db.cljs` unless a test proves an integration issue. -- Avoid adding new `thread-api` entries. -- Run focused markdown mirror tests before the broader lint and test command. -- Run @logseq-review-workflow after implementation and before final handoff. - -## Question - -No open questions. - -Code blocks are explicitly excluded from db/id comments. - ---- +Do not reintroduce block id comments for search integrations. If a future +integration needs block identity, add a non-visible mapping mechanism outside +the Markdown content and keep the mirror files free of implementation metadata. diff --git a/docs/agent-guide/logseq-cli/011-qmd-search.md b/docs/agent-guide/logseq-cli/011-qmd-search.md deleted file mode 100644 index fdc7c77769..0000000000 --- a/docs/agent-guide/logseq-cli/011-qmd-search.md +++ /dev/null @@ -1,76 +0,0 @@ -# QMD Search CLI Implementation Plan - -## Goal - -Add `logseq qmd` and `logseq qsearch` so CLI users can search DB graph Markdown Mirror files through QMD and map search hits back to Logseq block entities. - -## Architecture - -- Keep the QMD integration in the CLI process. -- Call `qmd` as an external executable through Node child process APIs with argument vectors. -- Reuse db-worker-node startup and transport helpers. -- Reuse the existing `:thread-api/markdown-mirror-regenerate` API for mirror generation. -- Reuse the existing `:thread-api/pull` API for entity lookup. -- Do not add a new thread API for this feature. - -## Public Interface - -`logseq qmd [--graph ]` - -- Verify that `qmd` is executable. -- Regenerate Markdown Mirror for the selected graph. -- Resolve the mirror directory as `/graphs//mirror/markdown`. -- Use the deterministic graph collection name computed by the Logseq CLI. -- Initialize the QMD collection with `qmd collection add --name --mask "**/*.md"`. -- If the collection already points at the same mirror directory, reuse it. -- If the collection name points at a different path, fail fast. -- Run `qmd embed`. -- Run `qmd update`. - -`logseq qsearch --graph [-n ] [--no-rerank]` - -- Run `qmd query --json -c -n `. -- Use the same deterministic graph collection name as `logseq qmd`. -- Parse noisy QMD stdout defensively. -- Extract block ids from Markdown Mirror comments matching ``. -- Pull entities through `:thread-api/pull`. -- Deduplicate ids in QMD rank order. -- Return Logseq list-style output plus `missing-ids` for stale QMD results. - -Collection names are internal and use `logseq--` to avoid collisions between graphs with similar display names. The CLI uses QMD's default index and does not expose manual collection or index options. - -## Data Flow - -```mermaid -flowchart LR - A["logseq qmd"] --> B["db-worker-node"] - B --> C["markdown-mirror-regenerate"] - C --> D["Markdown Mirror directory"] - D --> E["qmd collection add/show"] - E --> K["qmd embed"] - K --> L["qmd update"] - - F["logseq qsearch"] --> G["qmd query --json"] - G --> H["Extract block id comments"] - H --> I["thread-api/pull"] - I --> J["List-style CLI output"] -``` - -## Implementation Tasks - -- Add `src/main/logseq/cli/command/qmd.cljs` for command entries, action builders, QMD process execution, collection initialization, QMD JSON parsing, id extraction, and qsearch normalization. -- Wire `qmd` and `qsearch` into `src/main/logseq/cli/commands.cljs`. -- Show `qmd` in utility help and `qsearch` in graph inspect/search help. -- Format `:qmd` and `:qsearch` in `src/main/logseq/cli/format.cljs` for human, JSON, and EDN output. -- Add unit tests for command entries, graph requirements, default collection names, mirror path derivation, QMD executable checks, collection create/update/mismatch handling, noisy JSON parsing, block id extraction, dedupe order, missing ids, and entity pull calls. -- Add parser tests for `qmd`, `qsearch`, help output, positional query handling, and invalid option behavior. -- Add format tests for human table output and JSON/EDN payload stability. -- Add a CLI E2E case with a fake `qmd` executable on `PATH` so the test does not depend on local QMD models, embeddings, or network access. - -## Verification - -- Run focused unit tests for `logseq.cli.command.qmd-test`, `logseq.cli.commands-test/test-qmd-and-qsearch-parse`, and qsearch/qmd format tests. -- Run `bb dev:lint-and-test`. -- Run `bb -f cli-e2e/bb.edn build`. -- Run `bb -f cli-e2e/bb.edn test --skip-build`. -- Review the finished diff with `logseq-review-workflow`, applying common, Clojure/CLJS, promesa/Node process, babashka CLI, shadow-cljs Node, logseq-cli, and search-indexing rules. diff --git a/docs/agent-guide/logseq-cli/012-qsearch-page-group-output.md b/docs/agent-guide/logseq-cli/012-qsearch-page-group-output.md deleted file mode 100644 index 491a0db4cb..0000000000 --- a/docs/agent-guide/logseq-cli/012-qsearch-page-group-output.md +++ /dev/null @@ -1,435 +0,0 @@ -# Qsearch Page Group Output Implementation Plan - -Goal: Change `logseq qsearch` human output from a list-style table into page-grouped, show-like search output while keeping the existing QMD and db-worker-node data flow. - -Architecture: Keep QMD execution in the CLI process and continue using the existing `qmd query --json` integration from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/qmd.cljs`. -Architecture: Keep db-worker-node access on existing `:thread-api/pull` and `:thread-api/q` style calls, and do not add a new thread API unless implementation proves a concrete existing API cannot fetch required data. -Architecture: Extract or reuse the pure human tree renderer behind `logseq show` so qsearch can render grouped hits in the same visual family without copying renderer behavior into a separate ad hoc formatter. - -Tech Stack: ClojureScript, Logseq CLI, QMD JSON output, db-worker-node transport, Datascript pull selectors, Promesa, `string-width`, CLI formatter tests, command tests, and `cli-e2e` non-sync cases. - -Related: Builds on `/Users/rcmerci/gh-repos/logseq/docs/agent-guide/logseq-cli/011-qmd-search.md`, `/Users/rcmerci/gh-repos/logseq/docs/agent-guide/logseq-cli/003-cli-block-ref-rendering.md`, and the current `show` renderer in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs`. - -## Problem statement - -The current `logseq qsearch` implementation returns matched Logseq blocks in a generic list table. - -That table is useful for compact inspection, but it loses the page-first shape that users expect from QMD query output and from Logseq content review. - -The current human output columns are defined by `qsearch-columns` in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs`. - -The formatter then calls `format-list-dynamic`, which produces a table with `RANK`, `ID`, `TITLE`, `PAGE-ID`, `PAGE-TITLE`, `SCORE`, `FILE`, and a final `Count: N` line. - -The requested output should stop using the list formatter for human qsearch output. - -The requested output should group extracted block ids by page. - -Each page group should render the matched blocks with a tree-like layout similar to `logseq show`. - -The implementation should preserve the existing QMD search path and should avoid new db-worker-node APIs unless there is no viable way to fetch the necessary data through existing `pull` or `q` calls. - -## Current implementation snapshot - -The QMD command implementation lives in: - -```text -/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/qmd.cljs -``` - -`execute-qsearch` currently performs this flow: - -```text -ensure db-worker-node server - -> run qmd query --json -c - -> parse noisy QMD JSON stdout - -> extract block ids from Markdown Mirror comments - -> deduplicate ids in QMD result order - -> pull each block through :thread-api/pull - -> normalize flat items - -> return {:items ..., :missing-ids ..., :qmd ...} -``` - -The current selector is: - -```clojure -[:db/id :block/title :block/uuid - {:block/page [:db/id :block/title :block/name :block/uuid]}] -``` - -The current formatter lives in: - -```text -/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs -``` - -The current human `:qsearch` formatter uses: - -```text -format-qsearch - -> format-list-dynamic - -> qsearch-columns -``` - -The existing show renderer lives in: - -```text -/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs -``` - -`tree->text` already renders a root node plus child rows with ids, tree glyphs, multi-line text handling, block reference label replacement, tags, status text, and property lines when the required data is present. - -Most of the `show` namespace is data fetching and show-specific behavior. - -Only the pure rendering layer should be reused by qsearch. - -## Desired behavior - -`logseq qsearch [--graph ]` should render human output grouped by page instead of a table. - -The first page group should be the page that contains the first extracted hit in QMD rank order. - -Subsequent page groups should follow the first-hit order from QMD results. - -Within each page group, matched blocks should follow the extracted id order from QMD results after deduplication. - -Each group should show the page as the root row and the matched blocks as direct child rows. - -The group should not fetch or render unrelated siblings, descendants, linked references, or page content blocks that did not match the QMD result. - -Matched block rows should use the same visible block rendering details as `show`, including tags, status, visible user properties, and displayable built-in properties. - -After the final human text is rendered, qsearch should highlight query terms case-insensitively when colors are enabled. - -An example human output shape should be: - -```text -1 Home -3 ├── alpha target -4 └── beta target - -10 Projects -11 └── release target - -Missing ids: 5 -``` - -Blank lines between page-root trees provide the page boundary. Do not add a separate `page-title (N matches)` heading line. - -The page root row should use the page entity id when available. - -The page label should use `:block/title`, then `:block/name`, then `:block/uuid`, then `:db/id`. - -The matched block rows should use `:block/title` with the same visible block reference replacement behavior that the current qsearch normalization path already depends on through shared CLI helpers. - -QMD score and file path are useful metadata but should not dominate the primary human output. - -If the implementation keeps score and file metadata in human output, render it as a compact dim metadata line per page group or per hit rather than as table columns. - -If the implementation cannot make metadata readable without noise, keep score and file metadata in structured output only. - -## Output contract - -The primary behavioral change is human `qsearch` output. - -JSON and EDN output should remain machine-friendly. - -Prefer preserving the existing structured `data.items`, `data.missing-ids`, and `data.qmd` shape unless a grouped structured payload is explicitly needed. - -If grouped structured data is added, add it under a new key such as `data.groups` and keep `data.items` for existing consumers. - -Do not hide missing ids in human output. - -Do not treat stale QMD hits as successful invisible matches. - -Do not add a fallback query path that silently searches Logseq DB content when QMD output has no extractable ids. - -## Data model - -Normalize qsearch hits into page groups after entity lookup. - -Each group should contain: - -| Field | Source | Purpose | -| --- | --- | --- | -| `:page` | `:block/page` from pulled block entities | Root node for the rendered group. | -| `:items` | Pulled matched block entities | Direct child rows for the rendered group. | -| `:qmd/files` | Distinct QMD files for hits in the group | Optional metadata. | - -The flat item shape can continue to include: - -| Field | Purpose | -| --- | --- | -| `:db/id` | Stable block id for display and pipelines. | -| `:block/title` | Visible matched block text. | -| `:block/uuid` | Reference normalization and structured output. | -| `:block/page-id` | Existing structured page id field. | -| `:block/page-title` | Existing structured page title field. | -| `:qmd/rank` | Existing QMD result rank. | -| `:qmd/score` | Existing QMD score. | -| `:qmd/file` | Existing QMD file. | - -When a pulled block has no page entity, put it in an explicit `Unpaged` or `Unknown Page` group and keep the block visible. - -When multiple QMD snippets reference the same block id, keep the first occurrence and ignore later duplicates. - -When one QMD snippet references multiple block ids from the same page, render all of them in extracted id order. - -When one QMD snippet references multiple block ids from different pages, split them into their page groups while preserving global first-hit page order. - -## Rendering architecture - -Create a small pure renderer namespace if direct reuse of `show/tree->text` would create a command namespace dependency cycle. - -Recommended file: - -```text -/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/tree_text.cljs -``` - -Move the pure tree rendering helpers from `show.cljs` into this namespace. - -Keep data fetching, linked references, breadcrumbs, user property fetching, and show-specific option handling in `show.cljs`. - -Expose a function with a narrow signature such as: - -```clojure -(tree->text {:root root - :uuid->label uuid->label - :property-titles property-titles - :property-value-labels property-value-labels}) -``` - -Update `show.cljs` to call the extracted renderer and preserve existing output byte-for-byte where practical. - -Update `format.cljs` to render `:qsearch` human output with a qsearch-specific grouping function that calls the extracted renderer for each page group. - -The qsearch formatter should not perform transport calls. - -All data needed for qsearch human output must be available in the command result returned by `execute-qsearch`. - -## Implementation plan - -1. Read `@test-driven-development` and use RED-GREEN-REFACTOR for all code changes. - -2. Add failing formatter coverage for page-grouped qsearch human output in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs`. - -3. Assert that human qsearch output contains page root rows and show-like matched block rows. - -4. Assert that human qsearch output does not contain table headers such as `RANK`, `PAGE-TITLE`, or `SCORE`. - -5. Assert that human qsearch output does not contain the generic final `Count: N` line from list formatting. - -6. Assert that missing ids still render as `Missing ids: ...`. - -7. Add a failing formatter test for two pages where page group order follows first QMD hit order. - -8. Add a failing formatter test for repeated hits where duplicate block ids render only once. - -9. Add or update structured output tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs`. - -10. Preserve `data.items` in JSON and EDN output unless the implementation intentionally adds `data.groups`. - -11. Add failing command tests in `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/qmd_test.cljs` if grouping data is built in `execute-qsearch`. - -12. Verify command tests prove behavior, not just map shape. - -13. Run the focused failing tests and confirm they fail because qsearch still uses list output. - -14. Extract pure tree rendering helpers from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs` into `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/tree_text.cljs`. - -15. Keep function names and helper behavior close to the existing `show.cljs` implementation to minimize review risk. - -16. Update `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs` to require the new renderer namespace. - -17. Replace local calls to the moved pure renderer helpers with calls through the new namespace. - -18. Run focused show command and formatter tests to verify show output still passes. - -19. Update `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/qmd.cljs` only if the formatter needs richer grouped data than the current flat item payload can provide. - -20. Prefer extending the existing `qsearch-pull-selector` over adding a new thread API. - -21. Include only fields needed by the renderer and output contract. - -22. Do not fetch full page block trees for qsearch. - -23. Do not fetch linked references for qsearch. - -24. Do not fetch user properties for qsearch unless the final renderer contract explicitly includes property lines. - -25. Implement qsearch page grouping as a pure helper in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` or a small formatter-adjacent namespace. - -26. Build a synthetic root page node for each page group. - -27. Attach matched blocks as direct `:block/children` of that root node. - -28. Sort page groups by first matched `:qmd/rank`. - -29. Sort group items by first matched order inside that page. - -30. Render each group with the extracted tree renderer. - -31. Do not add a separate page heading above each rendered tree. - -32. Append the existing missing ids footer after all groups. - -33. Update `/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md` to describe qsearch human output as page-grouped rather than list-table output. - -34. Update the qsearch CLI e2e case in `/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn` so it checks for page-grouped human output in addition to the existing JSON case. - -35. Keep the fake `qmd` executable approach from the existing e2e case so the test does not require real embeddings, network access, or local QMD models. - -36. Run the focused tests again and confirm they pass. - -37. Run broader CLI tests and non-sync e2e tests. - -38. Add focused coverage for case-insensitive query-term highlighting after final human rendering. - -39. Review the finished diff with `@logseq-review-workflow`. - -40. Apply the common, Clojure/CLJS, promesa, babashka CLI, shadow-cljs Node, logseq-cli, and search-indexing review rules. - -41. Fix every blocking or important review finding before considering the implementation complete. - -## Testing Plan - -I will add formatter tests that call `logseq.cli.format/format-result` with a `:qsearch` payload containing multiple pages, multiple hits per page, duplicate hits, multiline block text, and missing ids. - -Those tests will verify the human output behavior by checking page group boundaries, show-like tree glyphs, block ids, visible titles, deduplication, missing id footer, and absence of the old list table columns. - -I will add command tests around `execute-qsearch` only if implementation builds grouped data before formatting. - -Those command tests will stub `qmd` JSON output and `transport/invoke`, then verify that pulled blocks from different pages retain QMD order and can be rendered into page groups. - -I will preserve or update structured output tests so JSON and EDN output continue to expose machine-readable qsearch results. - -I will add a CLI e2e case that creates two pages with qsearch target blocks, runs fake QMD, and verifies human stdout contains two page groups with show-like rows. - -I will keep the existing JSON qsearch e2e case so machine output coverage remains intact. - -NOTE: I will write *all* tests before I add any implementation behavior. - -## Edge cases - -QMD output may include warnings before or after the JSON array. - -QMD output may return no JSON array. - -QMD output may return snippets without Markdown Mirror block id comments. - -One QMD result may contain multiple block id comments. - -The same block id may appear in multiple QMD results. - -An extracted block id may no longer exist in the graph. - -A pulled block may have no `:block/page` data. - -A page title may be missing but `:block/name` may exist. - -A block title may contain multiple lines. - -A block title may contain serialized UUID block references. - -A page may contain CJK text or wide glyphs. - -The result set may contain many hits from the same page. - -The result set may contain many pages with one hit each. - -The QMD rank may not be the same as page sort order. - -The user may request JSON or EDN output and should not receive human tree text in structured mode. - -## Verification commands - -Run focused formatter tests: - -```bash -bb dev:test -v logseq.cli.format-test/test-human-output-qsearch -``` - -Run focused command tests if `qmd.cljs` behavior changes: - -```bash -bb dev:test -v logseq.cli.command.qmd-test -``` - -Run command parser tests if command metadata or examples change: - -```bash -bb dev:test -v logseq.cli.commands-test/test-qmd-and-qsearch-parse -``` - -Run the full repo lint and unit test command before final review: - -```bash -bb dev:lint-and-test -``` - -Build the CLI e2e target if needed: - -```bash -bb -f cli-e2e/bb.edn build -``` - -Run CLI non-sync e2e tests: - -```bash -bb -f cli-e2e/bb.edn test --skip-build -``` - -Run `@logseq-review-workflow` after implementation and before final handoff. - -## Acceptance criteria - -Human `logseq qsearch` output no longer uses `format-list-dynamic`. - -Human `logseq qsearch` output groups matched blocks by page. - -Each page group renders matched blocks with show-like id and tree layout. - -Page group order follows first QMD hit order. - -Block order inside a group follows first extracted QMD hit order. - -Duplicate extracted block ids render once. - -Missing ids remain visible in human output. - -Structured JSON and EDN output remain machine-readable and covered by tests. - -No new thread API is added unless the implementation notes a concrete blocker with existing `pull` or `q` APIs. - -The existing `show` command output remains covered after renderer extraction. - -The implementation is reviewed with `@logseq-review-workflow`. - -## Testing Details - -The formatter tests validate visible behavior by comparing the qsearch human output shape that users see instead of checking only intermediate data structures. - -The command tests validate behavior at the QMD-to-Logseq boundary by proving extracted ids are pulled, deduplicated, and retain ordering across pages. - -The e2e test validates the actual CLI path with a fake QMD binary, a real graph, db-worker-node, and command stdout. - -## Implementation Details - -- Keep qsearch search execution in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/qmd.cljs`. -- Keep qsearch human formatting in `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` or a formatter helper namespace. -- Extract pure tree rendering from `/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs` only if direct reuse would otherwise duplicate renderer logic. -- Keep db-worker-node calls on existing `:thread-api/pull` and `:thread-api/q`. -- Keep qsearch from rendering full page trees, linked references, breadcrumbs, or unrelated page content. -- Preserve `data.items` for structured output when possible. -- Use page titles from `:block/title` first and `:block/name` second. -- Preserve missing id reporting after grouped output. -- Update CLI docs after the output contract changes. -- Run `@logseq-review-workflow` after implementation. - -## Question - -Should qsearch human output show QMD score and file metadata at all, or should those remain structured-output-only fields. - -Should qsearch render matched blocks as direct children of the page root even when the matched blocks are deeply nested, or should it show ancestor context by adding parent rows. - ---- diff --git a/docs/cli/logseq-cli.md b/docs/cli/logseq-cli.md index f109046672..2c59d3a3b7 100644 --- a/docs/cli/logseq-cli.md +++ b/docs/cli/logseq-cli.md @@ -307,7 +307,6 @@ Inspect and edit commands: - `search page --content ` - search pages by `:block/name` (case-insensitive substring) - `search property --content ` - search properties by `:block/title` (Property entities only) - `search tag --content ` - search tags by `:block/title` (Tag entities only) -- `qsearch ` - search QMD Markdown Mirror output and render matched Logseq blocks grouped by page in human output; uses the current graph when `--graph` is omitted - `query --query [--inputs ]` - run a Datascript query against the graph - `query --name [--inputs ]` - run a named query (built-in or from `cli.edn`) - `query list` - list available named queries @@ -411,7 +410,6 @@ JSON key migration (flat -> namespaced): - `doctor-server-revision-mismatch`: one or more discovered servers use a different revision than the local CLI revision (warning). Follow the printed remediation command for each affected graph: `logseq server restart --graph `. - If bundled runtime startup fails with missing-module or missing-file errors, rebuild with `pnpm db-worker-node:release:bundle` and confirm `dist/db-worker-node.js` exists and every path listed in `dist/db-worker-node-assets.json` is present next to it. - `query` human output returns a plain string (the query result rendered via `pr-str`), which is convenient for pipelines like `logseq query ... | xargs logseq show --id`. -- `qsearch` human output is page grouped. Each group renders the page id/title as the tree root and the matched blocks below it with the same tree-style id column and block details used by `show`, including visible tags and properties. When colors are enabled, query terms are highlighted case-insensitively after the final human text is rendered. JSON and EDN output keep the structured `:items`, `:missing-ids`, and `:qmd` payload. - Built-in named queries currently include `block-search`, `task-search`, `recent-updated`, `list-status`, and `list-priority`. Use `query list` to see the full set for your config. - Show output resolves block reference UUIDs inside text, replacing `[[]]` with the referenced block content. Nested references are resolved recursively up to 10 levels to avoid excessive expansion. For example: `[[]]` → `[[some text [[]]]]` and then `` is also replaced. - When `show` targets an ordinary block (`--id` or `--uuid`), human output prepends one breadcrumb line (`page > ... > nearest parent`) above the root block line. Each segment is display-width truncated to `24` with `…`. diff --git a/src/main/frontend/worker/markdown_mirror.cljs b/src/main/frontend/worker/markdown_mirror.cljs index 81c9d5092d..d4cec60b66 100644 --- a/src/main/frontend/worker/markdown_mirror.cljs +++ b/src/main/frontend/worker/markdown_mirror.cljs @@ -324,17 +324,6 @@ (not (ldb/page? target))) target))) -(defn- block-id-comment - [db-id] - (str "")) - -(defn- append-block-id-comment - [content db-id] - (let [comment-text (block-id-comment db-id)] - (if (string/blank? content) - comment-text - (str content " " comment-text)))) - (defn- content-first-line [content] (-> (or content "") @@ -385,8 +374,7 @@ (defn- block-line-info [db block marker] - {:db/id (:db/id block) - :first-line-fragment (block-first-line-fragment block) + {:first-line-fragment (block-first-line-fragment block) :code-block? (code-block? block) :status-marker (when (seq (d/datoms db :eavt (:db/id block) :logseq.property/status)) (some-> (:logseq.property/status block) status-marker)) @@ -460,9 +448,6 @@ options {:initial-lines []})) (let [content (decorate-block-content block-info title) - content (cond-> content - (not (:code-block? block-info)) - (append-block-id-comment (:db/id block-info))) marker (or (:marker block-info) "-")] [(str spaces marker (when-not (string/blank? content) " ") diff --git a/src/main/logseq/cli/command/core.cljs b/src/main/logseq/cli/command/core.cljs index ad4102a2b3..89abe80e33 100644 --- a/src/main/logseq/cli/command/core.cljs +++ b/src/main/logseq/cli/command/core.cljs @@ -116,18 +116,17 @@ (defn top-level-summary [table] (let [groups [{:title "Graph Inspect and Edit" - :commands #{"list" "upsert" "remove" "query" "qsearch" "search" "show"}} + :commands #{"list" "upsert" "remove" "query" "search" "show"}} {:title "Graph Management" :commands #{"graph" "server" "doctor" "sync"}} {:title "Authentication" :commands #{"login" "logout"}} {:title "Utilities" - :commands #{"agent" "completion" "debug" "example" "qmd" "skill"} + :commands #{"agent" "completion" "debug" "example" "skill"} :top-level-only? true :desc-overrides {"agent" "Run task agent bridge" "debug" "Pull raw entity data for debugging" "example" "Show command examples" - "qmd" "Initialize and manage QMD search" "skill" "Show/install built-in logseq-cli skill"}}] to-top-level-entries (fn [entries commands desc-overrides] (->> commands diff --git a/src/main/logseq/cli/command/qmd.cljs b/src/main/logseq/cli/command/qmd.cljs deleted file mode 100644 index 4de8737965..0000000000 --- a/src/main/logseq/cli/command/qmd.cljs +++ /dev/null @@ -1,532 +0,0 @@ -(ns logseq.cli.command.qmd - "QMD-backed CLI search commands." - (:require ["child_process" :as child-process] - ["crypto" :as crypto] - ["fs" :as fs] - ["path" :as node-path] - [clojure.string :as string] - [logseq.cli.command.core :as core] - [logseq.cli.command.show :as show-command] - [logseq.cli.output-mode :as output-mode] - [logseq.cli.root-dir :as root-dir] - [logseq.cli.server :as cli-server] - [logseq.cli.transport :as transport] - [logseq.cli.uuid-refs :as uuid-refs] - [logseq.common.graph-dir :as graph-dir] - [promesa.core :as p])) - -(def ^:private markdown-glob "**/*.md") -(def ^:private block-id-comment-re #"") -(def ^:private block-line-re #"^(\s*)-\s?.*.*$") -(def ^:private qmd-hunk-header-re #"(?m)^@@\s+-([0-9]+)(?:,[0-9]+)?\s+\@\@.*$") -(def ^:private qsearch-context-lookback 50) -(def ^:private qsearch-context-lookahead 10) - -(def ^:private qmd-spec - {}) - -(def ^:private qsearch-spec - {:limit {:desc "Limit results" - :alias :n - :coerce :long} - :no-rerank {:desc "Skip QMD reranking" - :coerce :boolean}}) - -(def entries - [(core/command-entry ["qmd"] :qmd - "Initialize QMD for the graph Markdown Mirror" - qmd-spec - {:examples ["logseq qmd --graph my-graph"]}) - (core/command-entry ["qsearch"] :qsearch - "Search graph Markdown Mirror with QMD" - qsearch-spec - {:examples ["logseq qsearch \"markdown mirror\" --graph my-graph"]})]) - -(defn- sha1-prefix - [value length] - (subs (.digest (.update (.createHash crypto "sha1") (str value)) "hex") - 0 - length)) - -(defn- slug - [value] - (let [value (-> (str value) - string/lower-case - (string/replace #"[^a-z0-9]+" "-") - (string/replace #"^-+" "") - (string/replace #"-+$" ""))] - (if (seq value) value "graph"))) - -(defn default-collection-name - [repo] - (str "logseq-" - (slug (core/repo->graph repo)) - "-" - (sha1-prefix repo 8))) - -(defn mirror-dir - [config repo] - (node-path/join (root-dir/graphs-dir (:root-dir config)) - (graph-dir/repo->encoded-graph-dir-name repo) - "mirror" - "markdown")) - -(defn js args) - #js {:stdio #js ["ignore" "pipe" "pipe"]})] - (some-> (.-stdout child) - (.on "data" (fn [chunk] - (swap! stdout str (.toString chunk))))) - (some-> (.-stderr child) - (.on "data" (fn [chunk] - (swap! stderr str (.toString chunk))))) - (.on child "error" - (fn [error] - (when-not @settled? - (reset! settled? true) - (resolve {:exit 127 - :out @stdout - :err (or (.-message error) (str error)) - :error error - :args args})))) - (.on child "close" - (fn [code] - (when-not @settled? - (reset! settled? true) - (resolve {:exit (or code 0) - :out @stdout - :err @stderr - :args args})))))))) - -(defn- qmd-error - [code message result] - {:status :error - :error (cond-> {:code code - :message message} - (:err result) (assoc :stderr (:err result)) - (:out result) (assoc :stdout (:out result)))}) - -(defn- (re-find #"(?m)^\s*Path:\s+([^\r\n]+)\s*$" (or output "")) - second - string/trim)) - -(defn- normalize-path - [path] - (some-> path - node-path/resolve - (as-> resolved - (try - (.realpathSync fs resolved) - (catch :default _ resolved))))) - -(defn- same-path? - [left right] - (= (normalize-path left) - (normalize-path right))) - -(defn- qmd-command-failed - [message result] - (qmd-error :qmd-command-failed message result)) - -(defn build-action - [_options repo] - (if-not (seq repo) - {:ok? false - :error {:code :missing-repo - :message "repo is required for qmd"}} - {:ok? true - :action {:type :qmd - :repo repo - :graph (core/repo->graph repo) - :collection (default-collection-name repo)}})) - -(defn build-search-action - [options args repo] - (let [query (->> args - (map str) - (string/join " ") - string/trim)] - (cond - (not (seq repo)) - {:ok? false - :error {:code :missing-repo - :message "repo is required for qsearch"}} - - (not (seq query)) - {:ok? false - :error {:code :missing-query-text - :message "query text is required"}} - - :else - {:ok? true - :action {:type :qsearch - :repo repo - :graph (core/repo->graph repo) - :query query - :limit (:limit options) - :collection (default-collection-name repo) - :no-rerank (true? (:no-rerank options))}}))) - -(defn- collection-show-result->action - [action mirror-dir* show-result] - (if-let [existing-path (parse-collection-path (:out show-result))] - (if (same-path? existing-path mirror-dir*) - {:status :ok - :collection-action :existing} - {:status :error - :error {:code :qmd-collection-path-mismatch - :message "QMD collection exists for a different path" - :collection (:collection action) - :expected-path mirror-dir* - :actual-path existing-path}}) - {:status :error - :error {:code :qmd-collection-show-invalid - :message "Unable to read QMD collection path" - :collection (:collection action)}})) - -(defn- collection-add-result->action - [add-result] - (if (zero? (:exit add-result)) - {:status :ok - :collection-action :created} - (qmd-command-failed "qmd collection add failed" add-result))) - -(defn- action action mirror-dir* show-result) - (p/let [add-result (action add-result))))) - -(defn- clj parsed :keywordize-keys true) - (recur (string/index-of output "[" (inc start))))))))) - -(defn- qmd-json-parse-failed - [result] - (qmd-error :qmd-json-parse-failed - "Unable to parse QMD JSON output" - result)) - -(defn extract-block-ids - [results] - (->> (or results []) - (mapcat (fn [result] - (map (fn [[_ id]] - (js/parseInt id 10)) - (re-seq block-id-comment-re (or (:snippet result) ""))))) - (reduce (fn [acc id] - (if (some #{id} acc) - acc - (conj acc id))) - []))) - -(defn- positive-int - [value] - (let [parsed (cond - (number? value) value - (string? value) (js/parseInt value 10) - :else js/NaN)] - (when (and (number? parsed) - (not (js/isNaN parsed)) - (pos? parsed)) - parsed))) - -(defn- split-snippet-lines - [snippet] - (let [lines (string/split-lines (or snippet ""))] - (if (seq lines) (vec lines) [""]))) - -(defn- hunk-start-line - [snippet] - (some-> (re-find qmd-hunk-header-re (or snippet "")) - second - positive-int)) - -(defn- qmd-result-line - [result] - (or (positive-int (:line result)) - (hunk-start-line (:snippet result)))) - -(defn- find-subsequence-index - [lines candidate] - (let [lines (vec lines) - candidate (vec candidate) - candidate-count (count candidate) - last-start (- (count lines) candidate-count)] - (when (and (pos? candidate-count) - (not (neg? last-start))) - (loop [idx 0] - (cond - (> idx last-start) nil - (= candidate (subvec lines idx (+ idx candidate-count))) idx - :else (recur (inc idx))))))) - -(defn- leading-space-count - [line] - (count (second (re-find #"^(\s*)" (or line ""))))) - -(defn- block-line-info - [line] - (when-let [[_ indent] (re-matches block-line-re (or line ""))] - {:line line - :indent (count indent)})) - -(defn- enclosing-block-line - [context-lines snippet-lines snippet-start-index] - (let [first-line (first snippet-lines) - first-indent (leading-space-count first-line) - needs-parent-indent? (pos? first-indent)] - (loop [idx snippet-start-index] - (when (>= idx 0) - (if-let [{:keys [line indent]} (block-line-info (get context-lines idx))] - (if (and needs-parent-indent? - (not (< indent first-indent))) - (recur (dec idx)) - line) - (recur (dec idx))))))) - -(defn- expanded-snippet - [result context] - (let [snippet (:snippet result) - snippet-lines (split-snippet-lines snippet) - context-lines (vec (string/split-lines (or context ""))) - line (qmd-result-line result) - start-line (max 1 (- line qsearch-context-lookback)) - snippet-start (or (find-subsequence-index context-lines snippet-lines) - (let [idx (- line start-line)] - (when (<= 0 idx (dec (count context-lines))) - idx))) - enclosing-line (when snippet-start - (enclosing-block-line context-lines snippet-lines snippet-start))] - (if (and enclosing-line - (not (some #{enclosing-line} snippet-lines))) - (str enclosing-line "\n" snippet) - snippet))) - -(defn- qmd-get-args - [file start-line line-count] - ["get" (str file ":" start-line) "-l" (str line-count)]) - -(defn- {:db/id (:db/id entity) - :block/title (:block/title entity) - :qmd/rank (:qmd/rank qmd-result)} - (:block/uuid entity) (assoc :block/uuid (:block/uuid entity)) - (:score qmd-result) (assoc :qmd/score (:score qmd-result)) - (:file qmd-result) (assoc :qmd/file (:file qmd-result)) - (:db/id page) (assoc :block/page-id (:db/id page)) - (or (:block/title page) (:block/name page)) - (assoc :block/page-title (or (:block/title page) (:block/name page)))))) - -(defn- qsearch-entity-present? - [entity] - (and (map? entity) - (or (contains? entity :block/title) - (contains? entity :block/uuid) - (contains? entity :block/page)))) - -(defn- label (uuid-refs/fetch-uuid-labels config repo uuid-strings)] - (uuid-refs/normalize-item-string-fields items [:block/title :block/page-title] uuid->label)))) - -(defn- qsearch-ok-data - ([action result-count items missing-ids] - (qsearch-ok-data action result-count items missing-ids nil)) - ([action result-count items missing-ids human-data] - (cond-> {:status :ok - :data {:items items - :missing-ids missing-ids - :qmd {:collection (:collection action) - :result-count result-count}}} - human-data (assoc :human {:qsearch human-data})))) - -(defn- human-output? - [config] - (not (output-mode/structured? (output-mode/parse (:output-format config))))) - -(defn- label (:uuid->label tree-data)}) - (p/resolved {:query (:query action) - :items []})))) - -(defn- qsearch-args - [{:keys [query collection limit no-rerank]}] - (cond-> ["query" query "--json" "-c" collection] - limit (into ["-n" (str limit)]) - no-rerank (conj "--no-rerank"))) - -(defn execute-qsearch - [action config] - (p/let [cfg (cli-server/ensure-server! config (:repo action)) - qmd-result (]` and retry"}} - - :else - (let [result-by-id (qmd-result-by-id results)] - (p/let [entities (p/all - (map (fn [id] - (transport/invoke cfg :thread-api/pull - [(:repo action) qsearch-pull-selector id])) - ids)) - pairs (mapv vector ids entities) - items (->> pairs - (keep (fn [[id entity]] - (when (qsearch-entity-present? entity) - (normalize-qsearch-item entity - (get result-by-id id))))) - vec) - missing-ids (->> pairs - (keep (fn [[id entity]] - (when-not (qsearch-entity-present? entity) id))) - vec) - items ( (conj option-tokens token) - value (conj value)) - query-tokens)) - - (contains? qsearch-flag-options token) - (recur (subvec remaining 1) - (conj option-tokens token) - query-tokens) - - (string/starts-with? token "-") - (recur (subvec remaining 1) - (conj option-tokens token) - query-tokens) - - :else - (recur (subvec remaining 1) - option-tokens - (conj query-tokens token))) - (vec (concat ["qsearch"] option-tokens query-tokens)))) - args)) - (defn- unknown-command-message [{:keys [dispatch wrong-input]}] (string/join " " (cond-> (vec dispatch) @@ -338,14 +297,6 @@ (not (seq (some-> (:content opts) str string/trim)))) (assoc (missing-query-text-result summary) :command command) - (and (= :qsearch command) - (not (seq args))) - (assoc (missing-query-text-result summary) :command command) - - (and (= :qmd command) - (seq args)) - (command-core/invalid-options-result summary "qmd does not accept positional arguments") - :else nil)) @@ -545,7 +496,7 @@ [raw-args] (let [summary (command-core/top-level-summary table) {:keys [opts args]} (command-core/parse-leading-global-opts raw-args) - {:keys [args id-from-stdin?]} (inject-stdin-id-arg (normalize-qsearch-args (vec args))) + {:keys [args id-from-stdin?]} (inject-stdin-id-arg (vec args)) group-path (group-help-path args)] (cond (:version opts) @@ -671,12 +622,6 @@ (:search-block :search-page :search-property :search-tag) (search-command/build-action command options repo) - :qmd - (qmd-command/build-action options repo) - - :qsearch - (qmd-command/build-search-action options args repo) - :upsert-block (upsert-command/build-block-action options args repo) @@ -778,8 +723,6 @@ :search-page (search-command/execute-search-page action config) :search-property (search-command/execute-search-property action config) :search-tag (search-command/execute-search-tag action config) - :qmd (qmd-command/execute-qmd action config) - :qsearch (qmd-command/execute-qsearch action config) :upsert-block (upsert-command/execute-upsert-block action config) :upsert-page (upsert-command/execute-upsert-page action config) :upsert-task (upsert-command/execute-upsert-task action config) diff --git a/src/main/logseq/cli/format.cljs b/src/main/logseq/cli/format.cljs index 204bbc8860..617e11f177 100644 --- a/src/main/logseq/cli/format.cljs +++ b/src/main/logseq/cli/format.cljs @@ -7,7 +7,6 @@ [logseq.cli.humanize :as cli-humanize] [logseq.cli.output-mode :as output-mode] [logseq.cli.style :as style] - [logseq.cli.tree-text :as tree-text] [logseq.common.util :as common-util] ["string-width" :default string-width])) @@ -192,7 +191,6 @@ :search-page "Use: logseq search page --content " :search-property "Use: logseq search property --content " :search-tag "Use: logseq search tag --content " - :qsearch "Use: logseq qsearch [--graph ]" "Use: logseq search --content ")) (defn- error-hint @@ -204,7 +202,6 @@ :missing-tag-name "Use --name " :missing-query "Use --query " :missing-query-text (missing-search-query-hint command) - :qmd-no-block-ids "Run `logseq qmd [--graph ]` and retry" :unknown-query "Use `logseq query list` to see available queries" :ambiguous-tag-name "Retry with --id " :ambiguous-property-name "Retry with --id " @@ -424,150 +421,6 @@ (format-list-dynamic items now-ms list-node-columns {:title-max-display-width title-max-display-width :truncate-cell-max-lines list-human-cell-max-lines}))) -(defn- qsearch-item-id - [item] - (or (:db/id item) (:id item))) - -(defn- qsearch-page-entity - [item] - (let [page (:block/page item) - page-id (:block/page-id item) - page-title (:block/page-title item)] - (cond - (map? page) page - (or page-id page-title) - (cond-> {} - page-id (assoc :db/id page-id) - page-title (assoc :block/title page-title)) - :else - {:block/title "Unknown Page"}))) - -(defn- qsearch-page-label - [page] - (or (:block/title page) - (:block/name page) - (some-> (:block/uuid page) str) - (some-> (:db/id page) str) - "Unknown Page")) - -(defn- qsearch-page-key - [page] - (if-let [id (:db/id page)] - [:db/id id] - [:label (qsearch-page-label page)])) - -(defn- dedupe-qsearch-items - [items] - (second - (reduce (fn [[seen acc] item] - (let [id (qsearch-item-id item)] - (if (and id (contains? seen id)) - [seen acc] - [(cond-> seen id (conj id)) - (conj acc item)]))) - [#{} []] - (or items [])))) - -(defn- append-qsearch-group-item - [{:keys [groups by-key] :as state} item] - (let [page (qsearch-page-entity item) - page-key (qsearch-page-key page) - group-idx (get by-key page-key) - item* (cond-> item - (nil? (:db/id item)) (assoc :db/id (qsearch-item-id item)))] - (if (some? group-idx) - (update-in state [:groups group-idx :items] conj item*) - (let [group {:page page - :items [item*]} - group-idx (count groups)] - (-> state - (update :groups conj group) - (assoc-in [:by-key page-key] group-idx)))))) - -(defn- qsearch-page-groups - [items] - (:groups - (reduce append-qsearch-group-item - {:groups [] :by-key {}} - (dedupe-qsearch-items items)))) - -(defn- qsearch-group->text - [{:keys [property-titles property-value-labels uuid->label]} {:keys [page items]}] - (let [root (assoc page :block/children (vec items))] - (tree-text/tree->text {:root root - :property-titles property-titles - :property-value-labels property-value-labels - :uuid->label uuid->label}))) - -(def ^:private ansi-or-text-pattern - #"\u001b\[[0-9;]*m|[^\u001b]+") - -(def ^:private regex-special-chars - #{\. \* \+ \? \^ \$ \{ \} \( \) \| \[ \] \\}) - -(defn- escape-regex - [value] - (->> value - str - (map (fn [ch] - (if (contains? regex-special-chars ch) - (str "\\" ch) - (str ch)))) - (apply str))) - -(defn- qsearch-highlight-terms - [query] - (->> (string/split (or query "") #"\s+") - (map string/trim) - (remove string/blank?) - (sort-by count >) - (map escape-regex) - distinct - vec)) - -(defn- highlight-qsearch-query - [text query] - (let [terms (qsearch-highlight-terms query)] - (if (seq terms) - (let [pattern (js/RegExp. (string/join "|" terms) "gi") - highlight-segment (fn [segment] - (if (re-matches style/ansi-pattern segment) - segment - (string/replace segment pattern - (fn [match] - (style/yellow match)))))] - (->> (re-seq ansi-or-text-pattern text) - (map highlight-segment) - (apply str))) - text))) - -(defn- format-qsearch - [data human-data _now-ms _title-max-display-width] - (let [render-items (if (and (map? human-data) - (contains? human-data :items)) - (:items human-data) - (:items data)) - groups (qsearch-page-groups render-items) - body (if (seq groups) - (string/join "\n\n" (map #(qsearch-group->text human-data %) groups)) - "No matches") - missing-ids (vec (or (:missing-ids data) []))] - (highlight-qsearch-query - (cond-> body - (seq missing-ids) - (str "\nMissing ids: " (string/join ", " missing-ids))) - (:query human-data)))) - -(defn- format-qmd - [{:keys [collection mirror-dir collection-action embed] - update-status :update}] - (string/join "\n" - [(str "QMD ready: " (or collection "-")) - (str "Mirror: " (or mirror-dir "-")) - (str "Collection: " (name (or collection-action :unknown))) - (str "Embed: " (name (or embed :unknown))) - (str "Update: " (name (or update-status :unknown)))])) - (defn- normalize-asset-type [value] (cond @@ -1220,8 +1073,6 @@ :list-asset (format-list-asset (:items data) now-ms list-title-max-display-width) (:search-block :search-page :search-property :search-tag) (format-list-page (:items data) now-ms) - :qmd (format-qmd data) - :qsearch (format-qsearch data (get-in human [:qsearch]) now-ms list-title-max-display-width) :upsert-block (format-upsert-block context (:result data)) :upsert-page (format-upsert-page context (:result data)) :upsert-task (format-upsert-task context (:result data)) diff --git a/src/test/frontend/worker/markdown_mirror_test.cljs b/src/test/frontend/worker/markdown_mirror_test.cljs index d688ccba9d..6ba43ad5fb 100644 --- a/src/test/frontend/worker/markdown_mirror_test.cljs +++ b/src/test/frontend/worker/markdown_mirror_test.cljs @@ -48,9 +48,6 @@ (defn- first-block [page] (-> page :block/_page first)) -(defn- block-id-comment [block] - (str "")) - (defn- (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ quote line 1 " (block-id-comment quote-block) "\n" + "- ## Heading block\n" + "- > quote line 1\n" " > quote line 2\n" "- ```clojure\n" " (println \"hi\")\n" @@ -461,17 +460,14 @@ :build/children [{:block/title "child"}]} {:block/title "second" :build/properties {:logseq.property/order-list-type "number"}}]}]}) - page (db-test/find-page-by-title @conn "Ordered") - ordered-first (db-test/find-block-by-content @conn "first") - child (db-test/find-block-by-content @conn "child") - ordered-second (db-test/find-block-by-content @conn "second")] + page (db-test/find-page-by-title @conn "Ordered")] (-> (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/ (markdown-mirror/\"}]\n"] - (is (= [{:file "qmd://demo/pages/A.md" - :snippet "- hello "}] - (qmd-command/parse-qmd-json-output payload)))) - (let [payload "[WARN] embeddings pending\n[{\"file\":\"qmd://demo/pages/A.md\",\"snippet\":\"- hello \"}]\n"] - (is (= [{:file "qmd://demo/pages/A.md" - :snippet "- hello "}] - (qmd-command/parse-qmd-json-output payload))))) - -(deftest test-extract-block-ids-preserves-rank-and-dedupes - (let [results [{:snippet "- one \n- two "} - {:snippet "- duplicate "} - {:snippet "- missing id"} - {:snippet "- three "}]] - (is (= [7 8 9] - (qmd-command/extract-block-ids results))))) - -(deftest test-execute-qmd-checks-qmd-before-touching-graph - (async done - (let [calls (atom [])] - (-> (p/with-redefs [qmd-command/ (p/with-redefs [qmd-command/ (p/with-redefs [qmd-command/ (p/with-redefs [qmd-command/ (p/with-redefs [qmd-command/ (p/with-redefs [qmd-command/\\n- stale \"}," - "{\"score\":0.5,\"file\":\"qmd://custom/pages/B.md\"," - "\"snippet\":\"- duplicate \"}]") - :err ""})) - cli-server/ensure-server! (fn [config repo] - (assoc config :ensured-repo repo)) - transport/invoke (fn [_ method args] - (swap! calls conj {:invoke [method args]}) - (case method - :thread-api/pull - (let [[_repo _selector id] args] - (p/resolved - (case id - 3 {:db/id 3 - :block/title "alpha" - :block/page {:db/id 1 - :block/title "Home"}} - 5 {:db/id 5})))))] - (qmd-command/execute-qsearch - {:type :qsearch - :repo "logseq_db_demo" - :query "alpha" - :limit 10 - :collection "custom" - :no-rerank true} - {:output-format :json})) - (p/then (fn [result] - (is (= :ok (:status result))) - (is (= [{:db/id 3 - :block/title "alpha" - :block/page-id 1 - :block/page-title "Home" - :qmd/rank 1 - :qmd/score 1 - :qmd/file "qmd://custom/pages/A.md"}] - (get-in result [:data :items]))) - (is (= [5] (get-in result [:data :missing-ids]))) - (is (= ["query" "alpha" "--json" "-c" "custom" "-n" "10" "--no-rerank"] - (:qmd (first @calls)))) - (is (= [3 5] - (mapv (fn [{:keys [invoke]}] - (get-in invoke [1 2])) - (filter :invoke @calls)))))) - (p/catch (fn [e] (is false (str "unexpected error: " e)))) - (p/finally done))))) - -(deftest test-execute-qsearch-expands-property-snippet-with-qmd-get - (async done - (let [snippet (str " * GitHub URL:: https://github.com/logseq/logseq/commit/5a40a2e1618182984d778dfeb1066786b2bbe176\n" - " * Assignee:: [[Tienson]]\n" - " - nice ") - calls (atom [])] - (-> (p/with-redefs [qmd-command/\n" - snippet) - :err ""} - - :else - {:exit 99 :out "" :err (pr-str args)}))) - cli-server/ensure-server! (fn [config repo] - (assoc config :ensured-repo repo)) - transport/invoke (fn [_ method args] - (swap! calls conj {:invoke [method args]}) - (case method - :thread-api/pull - (let [[_repo _selector id] args] - (p/resolved - (case id - 6515 {:db/id 6515 - :block/title "DONE on web, app should be able to input immediately after cmd+e(quick add) #[[UX Enhancement]]" - :block/page {:db/id 100 - :block/title "DB release checklist"}} - 6533 {:db/id 6533 - :block/title "nice" - :block/page {:db/id 100 - :block/title "DB release checklist"}})))))] - (qmd-command/execute-qsearch - {:type :qsearch - :repo "logseq_db_demo" - :query "deadline" - :collection "logseq-demo-9d477851"} - {:output-format :json})) - (p/then (fn [result] - (is (= :ok (:status result))) - (is (= [6515 6533] - (mapv :db/id (get-in result [:data :items])))) - (is (= [6515 6533] - (mapv (fn [{:keys [invoke]}] - (get-in invoke [1 2])) - (filter :invoke @calls)))) - (is (= [["query" "deadline" "--json" "-c" "logseq-demo-9d477851"] - ["get" "qmd://custom/pages/db-release-checklist.md:1" "-l" "15"]] - (mapv :qmd (filter :qmd @calls)))))) - (p/catch (fn [e] (is false (str "unexpected error: " e)))) - (p/finally done))))) - -(deftest test-execute-qsearch-expands-nested-child-snippet-with-enclosing-parent - (async done - (let [snippet " - nice " - calls (atom [])] - (-> (p/with-redefs [qmd-command/\n" - snippet) - :err ""} - - :else - {:exit 99 :out "" :err (pr-str args)}))) - cli-server/ensure-server! (fn [config repo] - (assoc config :ensured-repo repo)) - transport/invoke (fn [_ method args] - (swap! calls conj {:invoke [method args]}) - (case method - :thread-api/pull - (let [[_repo _selector id] args] - (p/resolved - (case id - 6515 {:db/id 6515 - :block/title "DONE parent" - :block/page {:db/id 100 - :block/title "DB release checklist"}} - 6533 {:db/id 6533 - :block/title "nice" - :block/page {:db/id 100 - :block/title "DB release checklist"}})))))] - (qmd-command/execute-qsearch - {:type :qsearch - :repo "logseq_db_demo" - :query "nice" - :collection "custom"} - {:output-format :json})) - (p/then (fn [result] - (is (= :ok (:status result))) - (is (= [6515 6533] - (mapv :db/id (get-in result [:data :items])))) - (is (= [6515 6533] - (mapv (fn [{:keys [invoke]}] - (get-in invoke [1 2])) - (filter :invoke @calls)))))) - (p/catch (fn [e] (is false (str "unexpected error: " e)))) - (p/finally done))))) - -(deftest test-execute-qsearch-expands-qmd-hunk-snippet-without-line-field - (async done - (let [snippet (str "@@ -4,4 @@ (3 before, 19 after)\n" - " * GitHub URL:: https://github.com/logseq/logseq/commit/5a40a2e1618182984d778dfeb1066786b2bbe176\n" - " * Assignee:: [[Tienson]]\n" - " - nice \n" - "- on mobile side, I can’t see namespace pages(children pages) ") - calls (atom [])] - (-> (p/with-redefs [qmd-command/\n" - " * GitHub URL:: https://github.com/logseq/logseq/commit/5a40a2e1618182984d778dfeb1066786b2bbe176\n" - " * Assignee:: [[Tienson]]\n" - " - nice \n" - "- on mobile side, I can’t see namespace pages(children pages) ") - :err ""} - - :else - {:exit 99 :out "" :err (pr-str args)}))) - cli-server/ensure-server! (fn [config repo] - (assoc config :ensured-repo repo)) - transport/invoke (fn [_ method args] - (swap! calls conj {:invoke [method args]}) - (case method - :thread-api/pull - (let [[_repo _selector id] args] - (p/resolved - (case id - 6515 {:db/id 6515 - :block/title "DONE on web, app should be able to input immediately after cmd+e(quick add) #[[UX Enhancement]]" - :block/page {:db/id 6465 - :block/title "Jul 11th, 2025"}} - 6533 {:db/id 6533 - :block/title "nice" - :block/page {:db/id 6465 - :block/title "Jul 11th, 2025"}} - 6517 {:db/id 6517 - :block/title "on mobile side, I can’t see namespace pages(children pages)" - :block/page {:db/id 6465 - :block/title "Jul 11th, 2025"}})))))] - (qmd-command/execute-qsearch - {:type :qsearch - :repo "logseq_db_demo" - :query "tienson sync" - :collection "custom"} - {:output-format :json})) - (p/then (fn [result] - (is (= :ok (:status result))) - (is (= [6515 6533 6517] - (mapv :db/id (get-in result [:data :items])))) - (is (= [["query" "tienson sync" "--json" "-c" "custom"] - ["get" "qmd://custom/journals/2025-07-11.md:1" "-l" "18"]] - (mapv :qmd (filter :qmd @calls)))))) - (p/catch (fn [e] (is false (str "unexpected error: " e)))) - (p/finally done))))) - -(deftest test-execute-qsearch-does-not-duplicate-already-complete-snippet - (async done - (let [snippet "- DONE parent \n - nice " - calls (atom [])] - (-> (p/with-redefs [qmd-command/ (p/with-redefs [qmd-command/\"}]" - :err ""} - - (= ["get" "qmd://custom/pages/missing.md:1" "-l" "19"] args) - {:exit 1 :out "" :err "not found"} - - :else - {:exit 99 :out "" :err (pr-str args)}))) - cli-server/ensure-server! (fn [config repo] - (assoc config :ensured-repo repo)) - transport/invoke (fn [_ method args] - (swap! calls conj {:invoke [method args]}) - (case method - :thread-api/pull - (let [[_repo _selector id] args] - (p/resolved - (case id - 6533 {:db/id 6533 - :block/title "nice" - :block/page {:db/id 100 - :block/title "DB release checklist"}})))))] - (qmd-command/execute-qsearch - {:type :qsearch - :repo "logseq_db_demo" - :query "nice" - :collection "custom"} - {:output-format :json})) - (p/then (fn [result] - (is (= :ok (:status result))) - (is (= [6533] - (mapv :db/id (get-in result [:data :items])))) - (is (= [["query" "nice" "--json" "-c" "custom"] - ["get" "qmd://custom/pages/missing.md:1" "-l" "19"]] - (mapv :qmd (filter :qmd @calls)))))) - (p/catch (fn [e] (is false (str "unexpected error: " e)))) - (p/finally done))))) - -(deftest test-execute-qsearch-does-not-call-qmd-get-without-line - (async done - (let [calls (atom [])] - (-> (p/with-redefs [qmd-command/\"}]" - :err ""})) - cli-server/ensure-server! (fn [config repo] - (assoc config :ensured-repo repo)) - transport/invoke (fn [_ method args] - (swap! calls conj {:invoke [method args]}) - (case method - :thread-api/pull - (let [[_repo _selector id] args] - (p/resolved - (case id - 6533 {:db/id 6533 - :block/title "nice" - :block/page {:db/id 100 - :block/title "DB release checklist"}})))))] - (qmd-command/execute-qsearch - {:type :qsearch - :repo "logseq_db_demo" - :query "nice" - :collection "custom"} - {:output-format :json})) - (p/then (fn [result] - (is (= :ok (:status result))) - (is (= [6533] - (mapv :db/id (get-in result [:data :items])))) - (is (= [["query" "nice" "--json" "-c" "custom"]] - (mapv :qmd (filter :qmd @calls)))))) - (p/catch (fn [e] (is false (str "unexpected error: " e)))) - (p/finally done))))) - -(deftest test-execute-qsearch-normalizes-block-reference-labels - (async done - (let [ref-uuid #uuid "11111111-1111-1111-1111-111111111111" - calls (atom [])] - (-> (p/with-redefs [qmd-command/\"}]" - :err ""})) - cli-server/ensure-server! (fn [config repo] - (assoc config :ensured-repo repo)) - transport/invoke (fn [_ method args] - (swap! calls conj {:invoke [method args]}) - (case method - :thread-api/pull - (let [[_repo selector id] args] - (p/resolved - (cond - (= id 3) - {:db/id 3 - :block/title (str "alpha [[" ref-uuid "]]") - :block/page {:db/id 1 - :block/title "Home"}} - - (= id [:block/uuid ref-uuid]) - {:db/id 7 - :block/uuid ref-uuid - :block/title "Referenced block"} - - :else - (throw (ex-info "unexpected pull" - {:selector selector - :id id})))))))] - (qmd-command/execute-qsearch - {:type :qsearch - :repo "logseq_db_demo" - :query "alpha" - :collection "custom"} - {:output-format :json})) - (p/then (fn [result] - (is (= :ok (:status result))) - (is (= "alpha [[Referenced block]]" - (get-in result [:data :items 0 :block/title]))) - (is (not (string/includes? (get-in result [:data :items 0 :block/title]) - (str ref-uuid)))))) - (p/catch (fn [e] (is false (str "unexpected error: " e)))) - (p/finally done))))) - -(deftest test-execute-qsearch-renders-pulled-block-details-like-show - (async done - (let [query-call-count (atom 0)] - (-> (p/with-redefs [qmd-command/\"}]" - :err ""})) - cli-server/ensure-server! (fn [config repo] - (assoc config :ensured-repo repo)) - transport/invoke (fn [_ method args] - (case method - :thread-api/pull - (let [[_repo _selector target] args] - (p/resolved - (cond - (= target 3) - {:db/id 3 - :block/title "alpha target" - :block/page {:db/id 1 - :block/title "Home"} - :block/tags [{:db/id 20 - :block/title "Project"}]} - - (= target [:db/ident :user.property/priority]) - {:db/id 30 - :db/ident :user.property/priority - :block/title "Priority"} - - :else nil))) - - :thread-api/q - (let [idx (swap! query-call-count inc)] - (p/resolved - (case idx - 1 [[:user.property/priority :default]] - 2 [] - 3 [[3 :user.property/priority "P1"]] - [])))))] - (qmd-command/execute-qsearch - {:type :qsearch - :repo "logseq_db_demo" - :query "alpha" - :collection "custom"} - {:output-format nil})) - (p/then (fn [result] - (is (= :ok (:status result))) - (let [plain (-> result - (assoc :command :qsearch) - (format/format-result {:output-format nil}) - style/strip-ansi)] - (is (string/includes? plain "3 └── alpha target #Project")) - (is (string/includes? plain "Priority: P1")) - (is (not (string/includes? plain "PAGE-TITLE"))) - (is (not (string/includes? plain "Count: 1")))))) - (p/catch (fn [e] (is false (str "unexpected error: " e)))) - (p/finally done))))) - -(deftest test-execute-qsearch-empty-qmd-results-returns-no-matches - (async done - (-> (p/with-redefs [qmd-command/ (p/with-redefs [qmd-command/ (p/with-redefs [qmd-command/label {}}}} - {:output-format nil}))] - (is (string/includes? result "3 └── alpha target #Project")) - (is (string/includes? result "Priority: P1")) - (is (not (string/includes? result "PAGE-TITLE"))) - (is (not (string/includes? result "Count: 1"))))) - -(deftest test-human-output-qsearch-highlights-query - (let [result (binding [style/*color-enabled?* true] - (format/format-result {:status :ok - :command :qsearch - :data {:items [{:db/id 3 - :block/title "Alpha target" - :block/page-id 1 - :block/page-title "Home" - :qmd/rank 1}] - :missing-ids [] - :qmd {:collection "custom" - :result-count 1}} - :human {:qsearch {:query "alpha TARGET"}}} - {:output-format nil})) - highlighted-alpha (binding [style/*color-enabled?* true] - (style/yellow "Alpha")) - highlighted-target (binding [style/*color-enabled?* true] - (style/yellow "target")) - plain (style/strip-ansi result)] - (is (= (str "1 Home\n" - "3 └── Alpha target") - plain)) - (is (re-find style/ansi-pattern result)) - (is (string/includes? result highlighted-alpha)) - (is (string/includes? result highlighted-target)))) - -(deftest test-structured-output-qsearch - (let [payload {:status :ok - :command :qsearch - :data {:items [{:db/id 3 - :block/title "alpha" - :qmd/rank 1}] - :missing-ids [5] - :qmd {:collection "custom"}}} - json-result (format/format-result payload {:output-format :json}) - edn-result (format/format-result payload {:output-format :edn})] - (is (string/includes? json-result "\"qmd/rank\"")) - (is (string/includes? edn-result ":qmd/rank")))) - (deftest test-list-property-json-edn-cardinality-shape (testing "list property json keeps namespaced db/cardinality while edn stays unchanged" (let [base-result {:status :ok