- Changed temperature from 0/0.1 to 0.7 (Qwen3 non-thinking mode default)
- Added topK=20, topP=0.8 per Qwen3 docs
- Added repeatPenalty with presencePenalty=0.5 for query expansion
- Fixes infinite loop on acronyms like DHH, BFCM
Qwen3 docs explicitly warn: 'DO NOT use greedy decoding, as it can
lead to performance degradation and endless repetitions'
- List all HuggingFace repos in CLAUDE.md (model, gguf, sft, grpo, train)
- Update jobs scripts to use tobil/qmd-query-expansion-train (no -v2)
- Clarify rules: no versioned repos, update in place
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove versioned files (sft_v4.yaml, prepare_v4_dataset.py, train_v2/)
- Update configs to use local data/train/ directory
- Add glob pattern support to prepare_data.py and train.py
- Update .gitignore to properly ignore outputs/ and data/train*/
- Document data preparation step in CLAUDE.md
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add finetune/CLAUDE.md documenting the training pipeline
- Update configs to output to local outputs/ directory (gitignored)
- Document that all data/*.jsonl files are training data
- Document local CUDA training vs HuggingFace Jobs cloud training
- Enforce eval requirement before any model upload
- Single model repo (no -v1, -v2, -v4 versioning)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Brings in:
- /only: variants for single-type expansions
- LLM session management for lifecycle safety
- skills.sh integration for AI agent discovery
- Various bug fixes for vector search and embeddings
Merge conflicts resolved by keeping hyde-first format ordering
from finetune branch while accepting expanded templates and
new features from main.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move the hyde (hypothetical document) line to the beginning of the
output format, before lex and vec lines. This better reflects the
logical flow where the hypothetical document is generated first and
then informs the keyword/semantic expansions.
Also adds auto-download of eval_common.py in training scripts for
standalone HuggingFace Jobs execution.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- generate_only_variants.py: Creates training data where queries end with
'only: lex', 'only: vec', or 'only: hyde' and output contains ONLY that type
- reward.py: Updated scorer to handle 'only:' mode separately
- Penalizes presence of unwanted types
- Type-specific quality checks
- Filters templated low-quality hyde outputs
- 4,444 high-quality 'only:' variants from v2 + handcrafted data
Adds a session layer that prevents LLM contexts from being disposed
mid-operation during long-running tasks like batch embedding or
multi-step search workflows (expand → embed → rerank).
Key changes:
- Add LLMSessionManager with reference counting for active sessions
- Add LLMSession class for scoped access with automatic acquire/release
- Add withLLMSession() API for multi-step workflows
- Update idle timer to check canUnloadLLM() before disposing
- Wrap querySearch, vectorSearch, and embed command in sessions
- Add optional session parameter to searchVec and getEmbedding
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add query expansion model finetuning infrastructure
- Training scripts for Qwen3-0.6B and 1.7B models
- Dataset generation from s-emanuilov/query-expansion
- Evaluation scripts comparing finetuned vs baseline models
- GRPO RL training script (optional improvement)
- Export script for GGUF conversion
Results:
- 0.6B finetuned: 95% format compliance (lex/vec/hyde)
- Baseline: 0% format compliance
- Dataset: 5,157 examples on HuggingFace Hub
Models available at:
- tobil/qmd-query-expansion-0.6B (recommended)
- tobil/qmd-query-expansion-train (dataset)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix GRPO training script for TRL API compatibility
- Use max_completion_length instead of max_new_tokens
- Use processing_class instead of tokenizer
- Use args instead of config for GRPOTrainer
- Add __name__ attribute to reward function class
- Accept **kwargs in reward function for extra TRL args
- Add new LoRA adapter after merging SFT weights
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update README with final evaluation results
- 0.6B SFT: 95% format compliance (best)
- 0.6B GRPO: 0% (catastrophic forgetting from RL)
- 1.7B v2: training completed, evaluation pending
- Added GRPO evaluation results
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add comprehensive scoring system for query expansion
New scoring criteria (0-100 points):
- Format (30): Must have lex: and vec: prefixes
- Diversity (30): Multiple types, no echoing query, diverse expansions
- Hyde (20): Optional, concise, no newlines, no word repetition
- Quality (20): Lex=keywords, vec=natural language
See SCORING.md for full documentation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add HuggingFace login and comprehensive scoring to GRPO v2 training
- Add explicit HF_TOKEN login before training
- Use SCORING.md criteria as RL reward function
- Conservative training: LR 1e-6, LoRA rank 4
- Reward scores: good=0.94, bad=0.38
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor finetune folder: train/rl scripts with YAML configs
Major changes:
- train.py: Generic SFT training script using YAML config
- rl.py: Generic GRPO training script using YAML config
- configs/: YAML configs per training run (sft_v4.yaml, grpo_v4.yaml)
- dataset/: Data preparation scripts moved here
- tui.py: Interactive model testing interface
Training results:
- SFT v4: 98.8% avg score (all Excellent)
- GRPO v4: 0% (failed - model drifted to verbose explanations)
Removed per-model scripts (train_0.6B.py, train_1.7B.py, etc)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add named entity extraction to GRPO reward function
Key changes:
- Extract named entities (acronyms, proper nouns, technical terms)
- Heavy penalty (-30) when lex queries miss named entities
- Penalty (-15) for generic filler phrases like "find information about"
- Compound entity detection (TDS motorsports -> both words)
- Update GRPO config with KL regularization (beta=0.04)
- Lower learning rate (5e-7) and add max_steps (200)
Test results:
- "who is TDS motorsports" good: 1.00, bad: 0.30 (was 0.75)
- "how to use React hooks" good: 0.87, bad: 0.45 (was 0.75)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add chat template leakage detection to reward function
Zero reward for outputs containing:
- <|im_start|>, <|im_end|> tokens
- <think>, </think> tags (Qwen3 thinking mode)
- Role markers like \nassistant\n, \nuser\n
- <|endoftext|> token
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Strict format validation: every line must be lex:/vec:/hyde:
Any line that doesn't start with a valid prefix now returns 0.0
instead of just counting as a penalty. This prevents any prose,
explanations, bullet points, or other invalid content.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Clean up evaluation files
- Remove old versioned evaluation files (0.6B, 1.7B, baseline)
- Rename evaluation_v4.json -> evaluation_sft.json
- Rename evaluation_v4_grpo.json -> evaluation_grpo_failed.json
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor evals into separate run and score scripts
New structure:
- evals/run.py: Generate model outputs to JSONL
- evals/score.py: Score outputs with detailed breakdown
- evals/queries.txt: Test queries (26 total)
Features:
- Supports both HF Hub and local model paths
- Named entity preservation scoring
- Chat template leakage detection
- Strict format validation (every line must be lex:/vec:/hyde:)
- Generic phrase detection
Usage:
uv run evals/run.py --model tobil/qmd-query-expansion-0.6B-v4
uv run evals/score.py evals/results_*.jsonl
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix GRPO model loading to use SFT base first
The GRPO adapter was trained on merged SFT weights, so loading it
directly on the base model results in 0% score. Added --sft-model
parameter to evals/run.py to load SFT first, then apply GRPO adapter.
With correct loading: GRPO scores 89.7% (all 26 queries Excellent).
Updated README with correct GRPO score and loading instructions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix TUI to load GRPO models with SFT base first
GRPO adapters were trained on merged SFT weights, so they need SFT
loaded and merged first before applying the GRPO adapter.
Updated MODELS config to include sft_base path for GRPO models,
and load_model() now handles the SFT -> merge -> GRPO flow.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update README for unified model repository structure
All models (0.6B, 1.7B, 4B) with SFT and GRPO variants now go into
a single HuggingFace repo (tobil/qmd-query-expansion) with subfolders
for each size and training method.
Updated loading examples to show subfolder-based model loading.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update README with separate model repos
Changed from subfolder approach to separate repos per model since
trainer.push_to_hub() doesn't support subfolder argument.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add 1.7B and 4B GRPO training and GGUF conversion scripts
Training scripts for GRPO fine-tuning:
- train_1.7B_grpo.py: GRPO training for Qwen3-1.7B
- train_4B_grpo.py: GRPO training for Qwen3-4B
GGUF conversion scripts:
- convert_1.7B_gguf.py: Merge SFT+GRPO adapters and convert to GGUF
- convert_4B_gguf.py: Merge SFT+GRPO adapters and convert to GGUF
All scripts use PEP 723 inline dependencies for HuggingFace Jobs.
Models published:
- tobil/qmd-query-expansion-1.7B-sft
- tobil/qmd-query-expansion-1.7B-grpo
- tobil/qmd-query-expansion-1.7B-gguf
- tobil/qmd-query-expansion-4B-sft
- tobil/qmd-query-expansion-4B-grpo
- tobil/qmd-query-expansion-4B-gguf
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove beads issue tracking
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove beads reference from CLAUDE.md
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix GRPO reward function to handle think blocks and end tokens
- Strip <|im_end|> token from completions (model output includes it)
- Change think_penalty to skipped_think bonus (+20 for not using think)
- Adjust max_possible to account for bonus (120/140)
- Fix typo in chat template artifact check
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make TUI model list dynamic from HuggingFace Hub
- Fetch available qmd-query-expansion models from tobil/ on Hub
- Auto-detect model size (0.6B, 1.7B, 4B) and use correct base model
- Group models by type (SFT vs GRPO) in menu
- Skip GGUF repos in model listing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix GRPO training: apply chat template to prompts
The SFT model was trained with chat template format but GRPO was
passing raw prompts. Now prompts are formatted with tokenizer.apply_chat_template()
so the model sees the same format it learned during SFT.
Also update extract_query_from_prompt to strip chat template artifacts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Finetune 2.0: consolidate and simplify the entire training pipeline
Consolidate ~2,800 lines of duplicated code across 12 files into 5 clean,
well-documented files targeting Qwen3-1.7B end-to-end.
Key changes:
- Extract reward function into single source of truth (reward.py)
Previously duplicated 3x with divergent bugs across rl.py,
train_1.7B_grpo.py, and train_4B_grpo.py
- Unify training into one script with sft/grpo subcommands (train.py)
Replaces train.py + rl.py + train_1.7B_grpo.py + train_4B_grpo.py
- Merge eval generate+score into single eval.py
Replaces evals/run.py + evals/score.py
- Parameterize GGUF conversion by --size (convert_gguf.py)
Replaces convert_1.7B_gguf.py + convert_4B_gguf.py
- Fix critical bug: rl.py silently ignored beta/temperature from config,
causing the exact catastrophic drift its own comments warned about
- Fix prompt consistency: all files use /no_think chat template format
- Retarget configs from 0.6B to 1.7B
- Comprehensive README documenting the full pipeline
Removed: rl.py, train_1.7B_grpo.py, train_4B_grpo.py, convert_1.7B_gguf.py,
convert_4B_gguf.py, tui.py, evals/run.py, evals/score.py
Net: -3,429 lines, +382 lines
Co-Authored-By: Claude (claude-fudge-eap-cc) <noreply@anthropic.com>
* Add HF Jobs scripts, temporal query examples, and training results
- jobs/sft.py and jobs/grpo.py: self-contained scripts for
`hf jobs uv run` (no local GPU needed)
- 12 temporal/recency query examples in training data (e.g. "recent
news about Shopify" -> lex with years 2025/2026)
- 4 temporal test queries in evals/queries.txt
- README updated with HF Jobs workflow, training results, and
updated file structure
- Remove .beads tracking
SFT and GRPO successfully trained on A10G via HF Jobs:
SFT: eval loss 0.321, token accuracy 92.4%
GRPO: mean reward 0.757, 200 steps, KL 0.00048
Co-Authored-By: Claude (claude-fudge-eap-cc) <noreply@anthropic.com>
* Deploy fine-tuned GRPO model as default for query expansion
Switch from generic Qwen3-1.7B-Q8_0 (~2.2GB) to fine-tuned
qmd-query-expansion-1.7B-q4_k_m (~1.1GB). The fine-tuned Q4
scores 91.7% avg with 30/30 Excellent, outperforming the base Q8.
- Update default generate model in src/llm.ts
- Update README model table, architecture diagram, config block
- Add v2 training data, eval scripts, and quantize job
- Remove superseded v1 training data (5,742 → 1,000 examples)
- Update finetune README with v2 results and file structure
Co-Authored-By: Claude (claude-fudge-eap-cc) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Switch from generic Qwen3-1.7B-Q8_0 (~2.2GB) to fine-tuned
qmd-query-expansion-1.7B-q4_k_m (~1.1GB). The fine-tuned Q4
scores 91.7% avg with 30/30 Excellent, outperforming the base Q8.
- Update default generate model in src/llm.ts
- Update README model table, architecture diagram, config block
- Add v2 training data, eval scripts, and quantize job
- Remove superseded v1 training data (5,742 → 1,000 examples)
- Update finetune README with v2 results and file structure
Co-Authored-By: Claude (claude-fudge-eap-cc) <noreply@anthropic.com>
- jobs/sft.py and jobs/grpo.py: self-contained scripts for
`hf jobs uv run` (no local GPU needed)
- 12 temporal/recency query examples in training data (e.g. "recent
news about Shopify" -> lex with years 2025/2026)
- 4 temporal test queries in evals/queries.txt
- README updated with HF Jobs workflow, training results, and
updated file structure
- Remove .beads tracking
SFT and GRPO successfully trained on A10G via HF Jobs:
SFT: eval loss 0.321, token accuracy 92.4%
GRPO: mean reward 0.757, 200 steps, KL 0.00048
Co-Authored-By: Claude (claude-fudge-eap-cc) <noreply@anthropic.com>
Consolidate ~2,800 lines of duplicated code across 12 files into 5 clean,
well-documented files targeting Qwen3-1.7B end-to-end.
Key changes:
- Extract reward function into single source of truth (reward.py)
Previously duplicated 3x with divergent bugs across rl.py,
train_1.7B_grpo.py, and train_4B_grpo.py
- Unify training into one script with sft/grpo subcommands (train.py)
Replaces train.py + rl.py + train_1.7B_grpo.py + train_4B_grpo.py
- Merge eval generate+score into single eval.py
Replaces evals/run.py + evals/score.py
- Parameterize GGUF conversion by --size (convert_gguf.py)
Replaces convert_1.7B_gguf.py + convert_4B_gguf.py
- Fix critical bug: rl.py silently ignored beta/temperature from config,
causing the exact catastrophic drift its own comments warned about
- Fix prompt consistency: all files use /no_think chat template format
- Retarget configs from 0.6B to 1.7B
- Comprehensive README documenting the full pipeline
Removed: rl.py, train_1.7B_grpo.py, train_4B_grpo.py, convert_1.7B_gguf.py,
convert_4B_gguf.py, tui.py, evals/run.py, evals/score.py
Net: -3,429 lines, +382 lines
Co-Authored-By: Claude (claude-fudge-eap-cc) <noreply@anthropic.com>
* fix: add promise guard to ensureEmbedContext to prevent race condition
Root cause: ensureEmbedContext() was not thread-safe. When multiple parallel
embedding requests called ensureEmbedContext() simultaneously, all would see
embedContext === null and start creating new contexts. This race condition
caused 'Context is disposed' errors as contexts were overwritten/orphaned.
The fix adds a promise guard (embedContextCreatePromise) to ensure only one
context creation runs at a time - identical to the pattern already used in
ensureGenerateModel().
Changes:
- Add embedContextCreatePromise field to track in-progress context creation
- Modify ensureEmbedContext() to wait for existing creation if in progress
- Update test comment and timeout for CPU-only systems
Testing:
- Fresh model download + qmd embed: 28/28 chunks succeeded (was 14/27)
- All embedBatch tests pass
- No warmup hack needed - full parallel performance from the start
Environment tested:
- Ubuntu 24.04 LTS (x64), Bun 1.3.6, node-llama-cpp 3.14.5, no GPU
* test: improve race condition test to verify single context creation
The previous test only verified embeddings succeeded but didn't prove the fix
actually prevents multiple context creation. This improved test:
- Instruments createEmbeddingContext to count invocations
- Runs 5 concurrent embedBatch calls on a fresh LlamaCpp instance
- Asserts exactly 1 context is created (fails with 5 without the fix)
Verified locally:
- With fix: 1 context created (PASS)
- Without fix: 5 contexts created (FAIL)
* chore: clear embedContextCreatePromise in dispose() for consistency
Refactor extractTitle to use extension-based extractors:
- .md: preserves original markdown logic (Notes skip behavior)
- .org: extracts from #+TITLE: property or first * heading
Extensions are lowercased for case-insensitive matching.
Easy to add more file types in the future.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Override sqlite in devShell to enable extension loading for sqlite-vec
support when running tests. Only sets BREW_PREFIX if not already defined
to avoid overriding user's existing setup.
Package build remains unchanged.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The SFT model was trained with chat template format but GRPO was
passing raw prompts. Now prompts are formatted with tokenizer.apply_chat_template()
so the model sees the same format it learned during SFT.
Also update extract_query_from_prompt to strip chat template artifacts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fetch available qmd-query-expansion models from tobil/ on Hub
- Auto-detect model size (0.6B, 1.7B, 4B) and use correct base model
- Group models by type (SFT vs GRPO) in menu
- Skip GGUF repos in model listing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Strip <|im_end|> token from completions (model output includes it)
- Change think_penalty to skipped_think bonus (+20 for not using think)
- Adjust max_possible to account for bonus (120/140)
- Fix typo in chat template artifact check
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Training scripts for GRPO fine-tuning:
- train_1.7B_grpo.py: GRPO training for Qwen3-1.7B
- train_4B_grpo.py: GRPO training for Qwen3-4B
GGUF conversion scripts:
- convert_1.7B_gguf.py: Merge SFT+GRPO adapters and convert to GGUF
- convert_4B_gguf.py: Merge SFT+GRPO adapters and convert to GGUF
All scripts use PEP 723 inline dependencies for HuggingFace Jobs.
Models published:
- tobil/qmd-query-expansion-1.7B-sft
- tobil/qmd-query-expansion-1.7B-grpo
- tobil/qmd-query-expansion-1.7B-gguf
- tobil/qmd-query-expansion-4B-sft
- tobil/qmd-query-expansion-4B-grpo
- tobil/qmd-query-expansion-4B-gguf
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed from subfolder approach to separate repos per model since
trainer.push_to_hub() doesn't support subfolder argument.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
All models (0.6B, 1.7B, 4B) with SFT and GRPO variants now go into
a single HuggingFace repo (tobil/qmd-query-expansion) with subfolders
for each size and training method.
Updated loading examples to show subfolder-based model loading.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
GRPO adapters were trained on merged SFT weights, so they need SFT
loaded and merged first before applying the GRPO adapter.
Updated MODELS config to include sft_base path for GRPO models,
and load_model() now handles the SFT -> merge -> GRPO flow.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The GRPO adapter was trained on merged SFT weights, so loading it
directly on the base model results in 0% score. Added --sft-model
parameter to evals/run.py to load SFT first, then apply GRPO adapter.
With correct loading: GRPO scores 89.7% (all 26 queries Excellent).
Updated README with correct GRPO score and loading instructions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New structure:
- evals/run.py: Generate model outputs to JSONL
- evals/score.py: Score outputs with detailed breakdown
- evals/queries.txt: Test queries (26 total)
Features:
- Supports both HF Hub and local model paths
- Named entity preservation scoring
- Chat template leakage detection
- Strict format validation (every line must be lex:/vec:/hyde:)
- Generic phrase detection
Usage:
uv run evals/run.py --model tobil/qmd-query-expansion-0.6B-v4
uv run evals/score.py evals/results_*.jsonl
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Any line that doesn't start with a valid prefix now returns 0.0
instead of just counting as a penalty. This prevents any prose,
explanations, bullet points, or other invalid content.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Zero reward for outputs containing:
- <|im_start|>, <|im_end|> tokens
- <think>, </think> tags (Qwen3 thinking mode)
- Role markers like \nassistant\n, \nuser\n
- <|endoftext|> token
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Major changes:
- train.py: Generic SFT training script using YAML config
- rl.py: Generic GRPO training script using YAML config
- configs/: YAML configs per training run (sft_v4.yaml, grpo_v4.yaml)
- dataset/: Data preparation scripts moved here
- tui.py: Interactive model testing interface
Training results:
- SFT v4: 98.8% avg score (all Excellent)
- GRPO v4: 0% (failed - model drifted to verbose explanations)
Removed per-model scripts (train_0.6B.py, train_1.7B.py, etc)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add explicit HF_TOKEN login before training
- Use SCORING.md criteria as RL reward function
- Conservative training: LR 1e-6, LoRA rank 4
- Reward scores: good=0.94, bad=0.38
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New scoring criteria (0-100 points):
- Format (30): Must have lex: and vec: prefixes
- Diversity (30): Multiple types, no echoing query, diverse expansions
- Hyde (20): Optional, concise, no newlines, no word repetition
- Quality (20): Lex=keywords, vec=natural language
See SCORING.md for full documentation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use max_completion_length instead of max_new_tokens
- Use processing_class instead of tokenizer
- Use args instead of config for GRPOTrainer
- Add __name__ attribute to reward function class
- Accept **kwargs in reward function for extra TRL args
- Add new LoRA adapter after merging SFT weights
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add normalizeDocid() to strip quotes and # prefix
- Add isDocid() to detect docid patterns including quoted forms
- Update findDocumentByDocid, findDocument, getDocument to use new helpers
- All formats now work: #abc123, abc123, "#abc123", "abc123", '#abc123', 'abc123'
- Add 18 unit tests for normalizeDocid and isDocid
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>