Commit graph

1810 commits

Author SHA1 Message Date
LLLin000
e7a030d057 refactor: remove ChromaDB dependency gating from status and preflight
Embed status no longer checks for chromadb import — vec0 runs in
paperforge.db and doesn't need the ChromaDB library. The preflight
check also no longer requires chromadb since vec0 backends do not
depend on it. The chroma_backend module remains for legacy test
compatibility but no longer gates any production build/status path.
2026-07-10 23:57:33 +08:00
LLLin000
51018943ad feat: implement 8 search domain states with keyboard shortcuts and a11y in dashboard
Adds _renderSearchState() rendering 8 distinguishable states:
idle, searching (M/@), results, empty, vectors-not-built,
backend-unavailable, timeout, model-changed.

Keyboard: / focuses search, Escape cancels, ArrowUp/Down navigates
results, Enter opens note, Ctrl+Enter forces deep mode.

Accessibility: role=alert on errors, aria-live=polite on results
and progress, role=listbox with aria-selected/aria-posinset on cards,
aria-setsize, tabindex=0 on result cards, focus management on
state transitions.

Focus management: Stop button on build start, first result on
search complete, error banner on failure.
2026-07-10 23:56:31 +08:00
LLLin000
6e4b446ca2 feat: add retrieval error code routing in classifyError
Maps 7 retrieval error codes (VECTOR_NOT_BUILT, VECTOR_CORRUPTED,
MODEL_CHANGED, BACKEND_UNAVAILABLE, TIMEOUT, INTERNAL_ERROR, NO_PYTHON)
to distinct UI states with type, message, recoverable flag, and action.

Adds 7 new test cases for the error classification routing.
2026-07-10 23:56:25 +08:00
LLLin000
6b53de8135 feat: add 33 retrieval_* i18n keys for error distinction and recovery UI
Adds all retrieval state labels, button text, warning messages, and
descriptive copy in English and Chinese under the retrieval_* prefix.

Prepares i18n foundation for search domain (8 states) and build domain
(10 states) per PRD #57 and issue #62.
2026-07-10 23:56:20 +08:00
LLLin000
ec810b10a9 feat: plugin reads embed status --json live, polls during build, stop without kill
getVectorRuntime now calls embed status --json synchronously with
a 2s time-based cache before falling back to the JSON snapshot.

During build, a 2s polling interval reads embed status --json for
live build state and detects remote stop.

Stop button calls embed stop --json only (no direct .kill() on the
child process), letting the CLI handle cooperative stop.

ISettingPlugin interface extended with _embedPollInterval and
_embedPolling properties.
2026-07-10 23:55:26 +08:00
LLLin000
0bc054e7b0 fix: cooperative stop with state settlement and build-loop cancellation
embed stop now sets status=stopping, waits for the build PID to exit
(8s timeout), force-kills as last resort, then settles to idle.

Build loop checks the cancellation flag (read_vector_build_state)
between papers in the main loop and drains in-flight work before
cleanly exiting without marking 'completed'.

Fixes pre-existing UnboundLocalError from a local import shadowing
the global mark_vector_build_state symbol in the run() function.
2026-07-10 23:55:21 +08:00
LLLin000
40326d6759 fix: add vec0 k-NN health probe to embed status
embed status now runs a zero-vector k-NN query on each vec0 table
that has companion meta rows. If the probe fails (table dropped,
extension unavailable, index corrupted), healthy=false and
corrupted=true are reported even when meta row counts > 0.

Includes integration test that drops vec_fulltext and verifies
healthy=false.
2026-07-10 23:55:16 +08:00
LLLin000
aa9f1de278 fix: add missing CREATE_VEC_FULLTEXT_META to vec0 schema
The vec_fulltext_meta table DDL was referenced by ensure_schema()
but never defined. ADD the missing CREATE_VEC_FULLTEXT_META variable
so the meta table is created alongside vec_body_meta and vec_objects_meta.
2026-07-10 23:55:11 +08:00
LLLin000
fc1dca429c Unify PFResult envelope: data.matches with consistent field names, --deep flag
- search.py: normalize FTS results to unified field set (score from rank,
  add text/heading/source keys). Result key unchanged (already 'matches').
- retrieve.py: rename data.chunks to data.matches in standard and deep paths.
  Map field names: paper_id->zotero_key, chunk_text->text, section_path->heading.
  Expand enrichment SQL to fetch journal and domain.
  --deep flag already wired in CLI parser.
- tests/cli/test_json_contracts.py: add contract tests verifying PFResult
  envelope shape, data.matches key presence, unified field names,
  --deep flag acceptance, and field type correctness.
2026-07-10 23:15:16 +08:00
LLLin000
32e69a6c7b Delete sql.js: remove db.ts, wasm, dependency, and all sql.js code paths
- Remove paperforge/plugin/src/services/db.ts (sql.js wrapper, initDatabase/searchMetadata/SearchResultItem)
- Remove paperforge/plugin/sql-wasm.wasm (644KB binary)
- Remove sql.js and @types/sql.js from package.json; npm install to update lockfile
- dashboard.ts: remove import of db.ts, remove _sqlJsInitialized/_sqlJsFailed state,
  remove the sql.js search path block entirely — all searches go straight to CLI spawn.
  Clean up stale comments.
- Bundle drops from 346KB to 167KB; 6 test files / 58 tests still pass
2026-07-10 23:07:26 +08:00
LLLin000
f52124bcf7 docs: retrieval recovery design — Approach A (Python CLI sole owner) recommended 2026-07-10 23:01:57 +08:00
LLLin000
e3c41cc07d chore: add sql.js WASM, DB service, and PRD document 2026-07-10 02:49:49 +08:00
LLLin000
bf29966a86 feat: real-time metadata search via sql.js with 200ms debounce
Replace CLI spawn for metadata search with direct sql.js FTS5 queries.
Input debounced at 200ms. Falls back to CLI when sql.js unavailable.
@ Deep Search stays on CLI.
2026-07-10 02:49:31 +08:00
LLLin000
d6c1d1539b chore: rebuild plugin main.js with sql.js dependency
Bundles sql.js for direct FTS5 access to paperforge.db.
2026-07-10 02:49:06 +08:00
LLLin000
65311fc1c5 fix(status): handle read-only DB during concurrent build
get_embed_status opens a read-only connection that cannot load the
vec extension when another process has the DB locked. ensure_vec_extension
failure is now non-fatal — the status query proceeds with vec0 meta
table reads, which work in read-only mode.
2026-07-10 02:48:51 +08:00
LLLin000
3f2e9b9d0a fix(embed): auto-recover from stale build state + fix _pid_alive crash
- Stale detection: instead of hard-exiting with 'Use --force', reset
  build_state to idle and continue rebuilding. User no longer gets
  stuck when a previous build was killed.
- _pid_alive: avoid UnicodeDecodeError on Windows tasklist output.
  Don't pass text=True to subprocess.run; manually decode stdout
  with errors='replace'.
2026-07-10 02:48:39 +08:00
LLLin000
6348869941 feat: runtime embedding dimension detection
dim_detect.detect_embedding_dim() calls the embedding API once to
determine the model's output dimension. ensure_vec_tables() drops
and recreates vec0 tables when the dimension changes, so the schema
always matches the user's model (1536 for ada-002, 2560 for Qwen,
3072 for text-embedding-3-large, etc.).
2026-07-10 02:48:26 +08:00
LLLin000
55e0c4c33c fix(schema): create vec0 tables unconditionally, support dynamic dim
- CREATE VIRTUAL TABLE IF NOT EXISTS runs unconditionally, not gated
  on schema version migration. Fixes case where vec0 tables were never
  created when sqlite-vec wasn't available during v5 migration.
- Add VEC_EMBEDDING_DIM and _vec_table_ddl() to support runtime
  dimension detection instead of hardcoded 1536.
2026-07-10 02:48:11 +08:00
LLLin000
745c3dd1e1 fix(db): use sqlite_vec.load(conn) instead of loadable_path()
loadable_path() returns a path without .dll extension on Windows,
causing vec0 extension loading to fail. Use sqlite_vec.load(conn)
which handles platform-specific paths correctly.
2026-07-10 02:47:58 +08:00
LLLin000
4ff165ae49 feat(#38, #40): version history panel + @ Deep Search
#38: File Recovery-style version history panel with i18n (en/zh).
- _renderVersionMode() with paper list + timeline + restore/compare
- version-history.ts service for scan/restore/compare
- Paper mode [版本历史] button
- i18n strings for all panel text

#40: @ Deep Search with query rewrite + hybrid retrieval.
- query_rewrite.py: abbreviation expansion (28 terms) + synonyms (12)
- hybrid_search(): BM25 FTS5 + vec0 k-NN + score fusion (0.3/0.7)
- retrieve --deep flag routes to hybrid_search
- Graceful vec0 fallback (FTS5-only)
- 17 new tests

Co-authored-by: VersionPanel <agent>
Co-authored-by: DeepSearch <agent>
2026-07-10 01:01:48 +08:00
LLLin000
27e45f9419 feat(#37): auto-backup fulltext before rebuild
backup_render_before_rebuild() copies render/fulltext.md (and
render-map.json, heading-events.json) to versions/v{N}/ before
rebuild overwrites them. Creates/updates versions/manifest.json
with version metadata. Idempotent: skips when no render exists.

Hook placed between phase 3 and phase 4 in _rebuild_one_paper.

6 new tests.
2026-07-10 00:34:44 +08:00
LLLin000
06fec04651 feat: Phase 0, 1, 3a — legacy OCR fix, ChromaDB migration, search bar UI
Phase 0 (#35): _needs_derived_rebuild returns (False, 'legacy_ocr') for
papers without structured blocks and derived_version. _can_rebuild adds
has_structured check. No misleading rebuild suggestions for legacy users.

Phase 1 (#36): migrate_chroma_to_vec0() copies existing ChromaDB vectors
to vec0 tables with zero API calls. Idempotent (skips by paper_id).
delete_paper_vectors() cleans both ChromaDB and vec0. CLI: embed migrate.
5 new tests.

Phase 3a (#39): Search bar in collection panel — two-mode input
(M metadata / @ deep) with card results display. JSON output parsed
from search/retrieve CLI. 289 lines TypeScript, 175 lines CSS.
Build passes clean.

Co-authored-by: Phase0LegacyOcr <agent>
Co-authored-by: ChromaMigration <agent>
Co-authored-by: SearchBarUI <agent>
2026-07-10 00:02:02 +08:00
LLLin000
8699733b09 feat(#21): show structured_content_hash in paperforge status output
Add structured_content_hash field to OCRMaintenanceRow for display.
- Terminal table: new 'Hash' column showing first 12 chars of hash
- JSON output: structured_content_hash included in to_dict()
- Empty hash displayed as '-'
2026-07-09 18:14:15 +08:00
LLLin000
d24c78c5dc feat(#27 #28 #29 #30 #32): migrate write/read/status/build_state from ChromaDB to sqlite-vec
#27: write_encoded_payload and delete_paper_vectors use sqlite-vec
  - builder.py: writes to vec0 tables + companion meta tables
  - _chroma.py: delete_paper_vectors deletes from vec0 + meta by paper_id
  - Stores body_units_hash, object_units_hash, retrieval_policy_version in meta tables

#28: merge_retrieve and retrieve_chunks use vec0 k-NN search
  - search.py: vec0 k-NN queries with companion meta JOINs
  - Same dedup/per-paper-cap logic, same result shapes

#29: build_state migrated from JSON file to SQLite build_state table

#30: E2E embed+retrieve test with sqlite-vec + FixedProvider
  - 3 tests: body roundtrip, source correctness, per-paper cap

#32: E2E OCR pipeline test with fixture PDFs

Additional changes:
- schema.py: bump to v6, add hash/policy columns to vec companion meta tables
- embed.py: _assert_collections_healthy uses sqlite-vec; resume hash checks read from meta tables
- status.py: chunk counts from SQL COUNT on companion tables
- Removed unused get_collection/embed_paper imports from embed.py
- All tests updated: 76 pass (was 72, +4 new E2E tests, +4 for migration)
- FixedFixedProvider to generate 1536-dim vectors (matching vec0 schema)
- Fixed health check tests for sqlite-vec path
2026-07-09 17:52:45 +08:00
LLLin000
49f84a1aad feat(#31): OpenAICompatibleProvider uses openai SDK with requests fallback
Rewrite embedding provider to use openai.OpenAI client instead of
raw requests.post. Old code preserved in requests_fallback.py,
selectable via VECTOR_DB_PROVIDER_TYPE env/setting.

Changes:
- paperforge/embedding/providers/openai_compatible.py: rewrite to SDK
- paperforge/embedding/providers/requests_fallback.py: NEW old impl
- paperforge/embedding/_config.py: add get_provider_type()
- tests/test_openai_compatible.py: 6 new tests (encode, timeout,
  provider_type switch, no-key error, custom base url)

feat(#33): E2E test fixtures — 3 synthetic PDFs + expected outputs

3 synthetic papers generated with PyMuPDF, each 3 pages:
- paper_a: The Effect of Machine Learning on Clinical Outcomes
- paper_b: A Randomized Trial of Remimazolam vs Propofol (with tables)
- paper_c: Deep Learning for Medical Image Segmentation (with figures)

Changes:
- tests/fixtures/papers/: 3 PDFs
- tests/fixtures/expected_outputs/: expected blocks + body units
- tests/conftest.py: e2e_fixture_dir + synthetic_paper_paths fixtures

feat(#26): sqlite-vec schema — vec0 virtual tables + build_state

Schema version bumped 4→5. Adds:
- 3 vec0 virtual tables: vec_fulltext, vec_body, vec_objects (1536-dim)
- 3 companion metadata tables: vec_*_meta (paper_id, chunk_index, text)
- build_state table (key/value/updated_at)
- ensure_vec_extension() in db.py for extension loading

Changes:
- paperforge/memory/schema.py: schema v5, new tables in ensure_schema()
- paperforge/memory/db.py: ensure_vec_extension() helper
- tests/test_vector_schema.py: 5 new tests (tables, virtual, idempotent, v5)
2026-07-09 17:15:19 +08:00
LLLin000
970f147d59 chore(deps, markers): add sqlite-vec dep, e2e_fast marker
- Add sqlite-vec>=0.1.0 to vector optional deps
- Add e2e_fast marker for quick E2E tests
2026-07-09 17:12:48 +08:00
LLLin000
2b23ad716f feat(#20): hash-based OCR change detection — two-tier mtime+xxhash staleness
Replace version-constant-only detection with two-tier content-hash
detection against blocks.structured.jsonl.

Tier 1 (stat): compare st_mtime + st_size of blocks.structured.jsonl
against stored values in meta.json. Microsecond skip for unchanged files.

Tier 2 (hash): compute xxhash of the file when stat changes. If the hash
matches stored structured_content_hash, update mtime/size only (false
alarm recovery). If it differs, mark paper for rebuild.

Changes:
- pyproject.toml: add xxhash>=3.4.0 dependency
- paperforge/worker/ocr_versions.py: add compute_structured_hash()
- paperforge/worker/ocr_rebuild.py: _apply_post_rebuild_version_flags()
  writes hash+mtime+size to meta.json when vault+key provided
- paperforge/commands/ocr.py: _needs_derived_rebuild() two-tier check
  before version-constant fallback
- New tests: hash compute (hex, determinism, missing file),
  stat skip, hash match (false alarm recovery), hash mismatch trigger

61 tests pass (14 existing + 3 hash rebuild + 4 hash compute + 40 other).
2026-07-09 17:09:28 +08:00
LLLin000
9d150d1d95 test: add regression assertion for health-probe get(limit=1) and guard against query() 2026-07-09 02:17:49 +08:00
LLLin000
1397063c30 fix: replace dim-dependent query probe with metadata get(limit=1) in health check
The 256-dim zero vector probe crashes on any collection with real
1536-dim embeddings (text-embedding-3-small). get(limit=1) has no
dimension dependency and still verifies collection accessibility.
2026-07-09 02:17:14 +08:00
LLLin000
a588bd884a fix(#18): atomic build_state writes with .tmp fallback recovery
test(#17): integration tests for embed pipeline against EphemeralChromaDB

- build_state: write to .tmp first, leave as backup; read falls back to
  .tmp if main file corrupt. Extracted default/fallback state helpers.
- integration: 10 tests covering payload prep, encode→write→retrieve
  round-trip for all 3 collections, per-paper cap, delete, and
  encode_paper_job — all against real ChromaDB EphemeralClient with
  mocked provider
2026-07-09 02:03:20 +08:00
LLLin000
cc064bd74a fix: address 5 PR9 embedding pipeline issues from triage
- #13: Critical — swap delete/write order in _complete_one (write new
  vectors before deleting old ones) to prevent data loss on write failure
- #12: Add logger.warning() to 3 resume-skip except blocks so silent
  re-embed fallbacks are observable
- #14: Add lightweight HNSW query probe to _assert_collections_healthy
- #11: Log collection query failures in merge_retrieve instead of silent
  continue
- #16: Reuse provider across payloads in encode_paper_job — one creation
  per worker per paper instead of per payload
2026-07-09 01:46:22 +08:00
LLLin000
45022c3e16 docs: archive superpowers specs/plans, commit PR9A-C plan files
- Archive 173 design/plan/analysis docs from docs/superpowers/ →
  docs/archive/superpowers/ (all features shipped)
- Commit 5 PR9A-C plan files to docs/plans/ (historical record)
- Rebuild plugin bundle (main.js: 2172→49 lines, minified)
- Fix settings.ts type assertion
2026-07-09 00:39:39 +08:00
LLLin000
077c190b60 docs: update project management for PR9A-C embedding pipeline work 2026-07-09 00:30:50 +08:00
LLLin000
d94096f4d8 fix: plugin shows total chunks across all collections
Previously only showed paperforge_fulltext chunk_count (345).
Now sums chunk_count + body_chunk_count (14338) + object_chunk_count (5972).
2026-07-07 02:18:04 +08:00
LLLin000
9c85b30d7f fix: write_vector_build_state fallback when file locked
Windows: tmp.replace(path) fails when Obsidian plugin is reading
vector-build-state.json. Fall back to direct write to avoid crashing
the embed build. Stale tmp files cleaned up lazily.
2026-07-07 02:12:27 +08:00
LLLin000
db2988d8f0 PR9C: Streaming embed pipeline with continuous progress
Replace two-phase (prepare all, then encode+write) with sliding-window
pipeline. Maintains bounded in-flight window (max_workers * 4 = 16),
emits EMBED_PROGRESS from the first paper completion (~2s vs ~30s).

Key changes:
- producer/consumer loop: prepare + submit one paper at a time
- wait(FIRST_COMPLETED) in main thread for encode results
- processed_count = skip + embedded, monotonic EMBED_PROGRESS
- resume skip and no-payload paths also advance processed_count
- encode failure fails closed (return 1, no silent skip)
- removed PR9B_BATCH_SIZE and _batched() (replaced by sliding window)
- 4 integration tests for progress counting, failure, and write cycle
2026-07-07 02:04:12 +08:00
LLLin000
7d5fffda16 fix: emit EMBED_PROGRESS during Phase 1 (PREPARE)
Previously Phase 1 processed all papers silently, then Phase 2+3
emitted EMBED_PROGRESS. The plugin's progress bar showed 0/729 for
~30s during preparation, then jumped, looking disconnected.

Now Phase 1 emits EMBED_PROGRESS for each prepared paper, so the
progress bar starts updating immediately. Phase 2+3 continues with
its own counter for actual encode/write progress.
2026-07-07 01:41:50 +08:00
LLLin000
2022785101 fix: add missing jobs list initialization
PaperEmbeddingJob list was used (jobs.append) but never initialized,
causing NameError on embed build. Added
before the PREPARE loop.
2026-07-07 01:35:38 +08:00
LLLin000
a71796536a fix: OpenAICompatibleProvider uses requests instead of openai client
The openai Python client (v2.44.0) has compatibility issues with
SiliconFlow API — calls hang indefinitely even with TCP keepalive.
Replace with plain requests, which is simpler and works reliably.

- Removed openai/httpx/socket dependencies
- Uses requests.post() directly, 60s timeout
- ~0.3s per batch vs 2.3s+ with openai client
2026-07-06 23:15:58 +08:00
LLLin000
6ff0f6d38b PR9B: Embed Parallel Encode
- Four new dataclasses: EmbeddingPayload, EncodedPayload, PaperEmbeddingJob, PaperEncodedBundle
- prepare_legacy/body/object_payload: extract prepare phase from legacy embed functions
- encode_payload / encode_paper_job: worker-thread-safe encode functions
- write_encoded_payload: ChromaDB serial write
- prepare_payloads_for_entry: routing to correct prepare function
- Existing embed_body_units/embed_paper/embed_object_units refactored as wrappers
- Embed main loop: three-phase pipeline (PREPARE -> ENCODE -> WRITE) with ThreadPoolExecutor
- Bounded batch processing (BATCH_SIZE=8, MAX_WORKERS=4)
- Encode-success-gated delete: old vectors only removed after encode succeeds
- 23 regression tests for new functions
2026-07-06 22:41:25 +08:00
LLLin000
89f5a3fce6 PR9A: Resume & Rebuild Correctness
- OCR rebuild --all: version/artifact-based selection via _needs_derived_rebuild()
- --status and explicit keys: manual override, no version filter
- Removed .done.{key} checkpoint markers from selection logic
- _apply_post_rebuild_version_flags now writes derived_version
- Embed resume entry: three-gate protection (stale state / missing DB / corrupt DB)
- _pid_alive() cross-platform PID health check
- _assert_collections_healthy() explicit ChromaDB probe
- 14 regression tests for selection logic, version flags, and resume guards
2026-07-06 22:33:08 +08:00
LLLin000
6afcdc1830 fix: PR8.1 — status healthy init + object_units rebuild + version bump
- status.py: healthy=True init before if exists: (was UnboundLocalError
  when vector DB doesn't exist)
- manifest.py: RETRIEVAL_POLICY_VERSION l4.body.v1 → l4.body.v2 to
  trigger object_units rebuild with PR7 fix (unit_id fallback)
- Object_units corrected from 20→311 after rebuild (was INSERT OR
  REPLACE overwrite from old empty unit_id)
- Coverage parity: 20 papers have body_units = 20 have object_units 
  (body=811 chunks, object=311 chunks, expected difference due to
   section-split vs 1:1 caption structure)
- Unit tests: 97 pass (subset)
2026-07-06 20:08:51 +08:00
LLLin000
0dfbf2a68d feat: PR 8 — Object Vector Retrieval (paperforge_objects collection)
New paperforge_objects collection in ChromaDB for figure/table caption
vector search, completing the memory layer retrieval triad:

  paperforge_fulltext → legacy_chunk
  paperforge_body     → body_unit
  paperforge_objects  → object_unit (NEW)

Changes:
- manifest.py: compute_object_units_hash(), manifest records both hashes
- _chroma.py: _COLLECTION_NAMES includes paperforge_objects
- builder.py: get_object_units_for_embedding() + embed_object_units()
- status.py: object_chunk_count, total_chunks
- state_snapshot.py: write_vector_runtime() extended with new fields
- search.py: 3rd collection, dict-based source mapping, object_kind/label
- __init__.py: exports for new functions
- commands/embed.py: _has_object_units_in_db, body+object structured
  path routing with dual-hash resume, extended counters
- Test fix: test_build_paper_manifest data complete for hash functions

Verification: 20 object vectors embedded, all per-paper counts match,
status total consistent (58266 = 57435 + 811 + 20),
object caption queries return object_unit results via merge_retrieve.
Unit tests: 153 pass, 1 skip.
2026-07-06 19:58:46 +08:00
LLLin000
e551f79a77 fix: PR 7 post-test hardening — object units, node_id, expanded tests
P0 fixes from test report review:
- object unit_id: fallback to f"{obj_type}:p{page}:{block_id}" when
  figure_id/table_id missing (prevents DB overwrite on INSERT OR REPLACE)
- object caption_key: page-qualified f"p{page}:{caption_bid}" to match
  page-qualified subtree_block_ids in find_owning_node
- object block_map: page-qualified keys for consistency with body_units
- structure node_id: changed from sec:{block_id} to sec:p{page}:{block_id}
  for stability (reduces vector ID churn from emitted_order changes)

New tests:
- Section 4b: Object units DB persistence (unit_id unique, non-empty labels,
  DB count == list count after upsert)
- Section 5b: Coverage gate (v2_tree / render_map / body_papers counts)

Layer A: 1669/1669 pass (+60 checks from 4b + 5b)
Unit tests: 153 pass, 1 skip
2026-07-06 19:34:05 +08:00
LLLin000
01461330bb fix: final hardening — policy revision + Layer A all green
- _body_unit_role_kind now excludes only reference_item/reference_heading
  (per policy: everything in fulltext except ref zone)
- backmatter_body unit_kind removed; all units are 'body'
- bounds_map in structure_tree uses page-qualified key (fixes collision
  when same block_id appears on different pages)
- Layer A test plan: emitted_ids uses page-qualified keys + handles
  block_id=0 (falsy value)
- Layer A: 1609 passed, 0 failed on real vault (22 papers)
- Unit tests: 152 passed, 1 pre-existing skip
2026-07-06 19:08:54 +08:00
LLLin000
77a6e0cfec fix: heading→emitted events, backmatter→heading events, page-qualified keys
- Section/subsection headings now appear in BOTH heading_events and
  emitted_block_events (emitted_as='heading') for complete event stream
- Backmatter headings (Funding/Acknowledgments/Data Availability/Conflicts)
  now generate heading_events → appear as structure tree sections with
  their own body_units
- Page-qualified block IDs (p{page}:{block_id}) in tree own/subtree_block_ids
  and body_units block_map: eliminates ambiguous block_id collisions when
  same block_id appears on multiple pages in real OCR data
- TC-9.2 wording fix, KEYS empty guard, global unit_id uniqueness check
2026-07-06 18:19:17 +08:00
LLLin000
897afdaecb fix: token cap recursive split + policy version check + test plan update
- _split_if_oversized: recursive _halve_text until every part <= 1000 tokens
- _incremental_units_only: also checks retrieval_policy_version match
- _rebuild_paper_units: use RETRIEVAL_POLICY_VERSION constant
- Functional test plan: Layer A (artifact) + Layer B (API), all 8 review
  fixes incorporated (persisted artifacts, no jump-assertion, stronger
  object/backmatter/abstract tests, proper embed flow)
2026-07-06 17:05:13 +08:00
LLLin000
1ef0899fec docs: comprehensive functional test plan for memory layer
10 sections covering Render Events, Structure Tree, Body Units,
Object Units, FTS, Content Discovery, Memory Builder, Embed Build,
Retrieve Merge, and Regression. Includes auto-test script skeleton.
2026-07-06 16:59:31 +08:00
LLLin000
9f79ac3482 fix: hardening — FTS per-paper, object_units keys, unit_id collision, resume hash
P0-1: FTS per-paper insert — move SELECT INTO body_units_fts inside the
per-paper loop with WHERE paper_id = ? to avoid re-inserting other papers'
rows during full rebuild. Fix sqlite3.DatabaseError on empty FTS table.

P0-2: object_units real role_index keys — read from 'captions'/'tables'
(what build_role_indexes actually outputs) with fallback to old key names.

P1-1: unit_id collision for mixed body/backmatter — include unit_kind
suffix (:backmatter_body) in unit_id. Also fix duplicate tree node_ids
(block_id reused on different pages) by appending order{emitted_order}.

P1-2: Embed resume body_units_hash — add compute_body_units_hash() to
manifest.py, write hash + retrieval_policy_version to Chroma metadata,
resume compares both before skipping.
2026-07-06 16:56:33 +08:00
LLLin000
cb845362de fix: block_map in build_body_units handles duplicate block_ids across pages
Real OCR output has the same block_id on multiple pages (e.g. running
headers). block_map[str(bid)] = b overwrites correct entries with
wrong-page duplicates. Fix: prefer blocks with non-empty text content
when multiple blocks share the same block_id.
2026-07-06 16:50:12 +08:00