issues/completed/8-019-implement-unique-poem-index-system.md
Issue 8-019: Implement Unique poem_index System
Current Behavior
The poem identification system uses id fields derived from source filenames, which creates collisions across categories:
fediverse/0002.txt → id: 2, category: "fediverse"
messages/0002.txt → id: 2, category: "messages" ← COLLISION!
notes/0002.txt → id: 2, category: "notes" ← COLLISION!
This causes problems in the embedding pipeline:
- Generation stores embeddings at array index:
embeddings[5731] = {id: 2, ...} - Loading (incremental mode) stores by
emb.id:existing_embeddings[2] = {...} - Lookup uses array index:
existing_embeddings[5731]→ nil!
Result: Incremental embedding mode fails for all non-fediverse poems because array index ≠ source file ID.
Discovery Context
While debugging embedding generation failures (2025-12-25), traced the root cause to:
similarity-engine.lua:336-339stores loaded embeddings byemb.idsimilarity-engine.lua:368looks up by array indexi- These only match for fediverse poems where source ID ≈ array position
Intended Behavior
Add a new poem_index field that provides a unique, stable, array-aligned identifier for each poem:
{
"poems": [
{
"poem_index": 1, // Unique global identifier (1-indexed)
"id": 1, // Original source file ID (preserved for display)
"category": "fediverse",
"filepath": "fediverse/0001.txt"
},
{
"poem_index": 5731, // Unique! Different from id
"id": 2, // Same numeric ID, different poem
"category": "messages",
"filepath": "messages/0002.txt"
}
]
}
Benefits
| Aspect | Current (id) | Proposed (poem_index) |
|---|---|---|
| Uniqueness | Collisions across categories | Globally unique |
| Array alignment | Mismatched after fediverse | Always matches array position |
| Stability | Depends on extraction order | Stable once assigned |
| Display | Used for filenames | Keep id for display, poem_index for internals |
Implementation Steps (Completed 2025-12-25)
Phase A: Add poem_index to Extraction ✅
File: src/poem-extractor.lua
- [x] During poem extraction, assign
poem_index = array_position - [x] Ensure poem_index is 1-indexed (Lua convention)
- [x] Add poem_index to each poem's metadata
- [x] Bumped extraction_version to 2.1
Implementation: Added loop after sorting to assign poem_index = i for each poem.
Phase B: Update Embedding System ✅
File: src/similarity-engine.lua
- [x] Update
generate_all_embeddings()to store bypoem_indexif available, fallback to array index - [x] Update incremental loading to store by
poem_index - [x] Update lookup to use
poem.poem_index - [x] Add
poem_indexfield to each embedding record for future incremental loads - [x] Backward compatibility maintained via
poem.poem_index or ifallback
Phase C: Update HTML Generation ✅
Files: src/flat-html-generator.lua, scripts/generate-html-parallel, src/centroid-html-generator.lua
- [x] Use category prefix for output filenames:
similar/{category}-{id}.html(e.g.,fediverse-0002.html) - [x] Update navigation links to use category prefix
- [x] Keep
id+categoryfor display (e.g., "fediverse #2") - [x] Update all file existence checks in incremental mode
- [x] Fix embedding lookup in parallel generator to use
poem.poem_index
Note: User preferred category prefix over poem_index for file naming (more readable URLs).
Phase D: Update Similarity Matrix ✅
Files: src/similarity-engine-parallel.lua, src/html-generator/similarity-engine.lua
- [x] Update
get_poem_similarity_file()to usepoem_indexfor file naming - [x] Files now named
poem_index_{N}.jsoninstead ofpoem_{id}.json - [x] Add backward compatibility loading for old
poem_{id}.jsonfiles - [x] Update similarity data to include both
poem_idandpoem_indexin metadata
Phase E: Migration ✅
- [x] Regenerated poems.json with
poem_indexfield - [x] Existing embeddings will work via fallback mechanism
- [x] Full regeneration recommended for clean state
Verification
$ jq '.poems[] | select(.id == 2) | {poem_index, id, category}' assets/poems.json
{
"poem_index": 2,
"id": 2,
"category": "fediverse"
}
{
"poem_index": 6437,
"id": 2,
"category": "messages"
}
{
"poem_index": 7518,
"id": 2,
"category": "notes"
}
Three poems with id: 2 now have unique poem_index values, preventing collisions.
Technical Notes
Why Not Just Fix the Lookup?
We could patch similarity-engine.lua to store/lookup by array index consistently. However:
- Array index is implicit - Not stored in the data, computed at runtime
- Fragile - Any change to poems.json order breaks embeddings
- No audit trail - Can't verify which poem an embedding belongs to
poem_index makes the identifier explicit and persistent.
Relationship to Other IDs
| Field | Purpose | Scope | Example |
|---|---|---|---|
poem_index | Internal processing, embeddings, similarity | Global | 5731 |
id | Source file reference, display | Per-category | 2 |
filepath | Unique source identification | Global | "messages/0002.txt" |
Chronological Sorting
poem_index is assigned during extraction (which may be chronological). For display sorting:
- Use
created_atfor chronological views - Use
poem_indexfor stable internal references - These are orthogonal concerns
Files to Modify
| File | Changes |
|---|---|
src/poem-extractor.lua | Assign poem_index during extraction |
src/similarity-engine.lua | Use poem_index for embedding storage/lookup |
src/similarity-engine-parallel.lua | Same as above |
src/flat-html-generator.lua | Use poem_index for output files |
scripts/generate-html-parallel | Use poem_index for output files |
libs/utils.lua | Add poem_index validation helper |
Testing Strategy
- [ ] Extract poems and verify poem_index assigned correctly
- [ ] Generate embeddings and verify storage by poem_index
- [ ] Run incremental mode and verify correct skip detection
- [ ] Generate HTML and verify file naming
- [ ] Full pipeline test: extract → embed → similarity → HTML
Related Issues
- Issue 4-004: Verify and Resolve Cross-Category ID Mapping (identified the collision)
- Issue 8-018: Fix embedding directory case inconsistency (discovered during same debugging session)
- Issue 9-003: Optimize centroid calculation (uses embeddings, affected by this)
Risk Assessment
- Low: poem_index is additive, doesn't break existing
idusage - Medium: Requires regeneration of embeddings after implementation
- Low: HTML output filenames may change (similar/5731.html vs similar/2.html)
Phase: 8 (Website Completion)
Priority: High (blocking correct incremental embedding behavior)
Created: 2025-12-25
Completed: 2025-12-25
Status: Complete
Estimated Effort: Medium (touches multiple files but straightforward logic)
Files Modified:
src/poem-extractor.lua- Assign poem_index after sortingsrc/similarity-engine.lua- Use poem_index for embedding storage/lookupsrc/similarity-engine-parallel.lua- Use poem_index for similarity file namingsrc/flat-html-generator.lua- Use category prefix for HTML file namingsrc/centroid-html-generator.lua- Use category prefix for navigation linkssrc/html-generator/similarity-engine.lua- Load similarities by poem_indexscripts/generate-html-parallel- Update workers and file checks for category prefix