Review: Port block parser from Python to Ruby
Verdict: NEEDS_REFINEMENT
Template Completeness
- [x] Type -- Feature
- [x] Lineage -- Spike #75 (PR #76)
- [x] Repo -- ldraney/paldocs
- [x] User Story -- MCP cutover operator story
- [x] Context -- explains gap clearly
- [x] File Targets -- 4 targets listed
- [x] Feature Flag -- none (correct, internal infrastructure)
- [x] Acceptance Criteria -- 6 criteria
- [x] Test Expectations -- 4 expectations with run command
- [x] Constraints -- 3 constraints listed
- [x] Checklist -- standard
- [x] Related -- 3 references
Traceability
- [x] story:consolidation label -- absorb pal-e-docs into Rails monolith
- [x] story note verified -- story-paldocs-consolidation found in project-paldocs user-stories section
- [x] arch:rails-api label -- Rails API component
- [ ] arch note MISSING -- [SCOPE] Create architecture note arch-rails-api for the Rails API component. Search returned zero results.
- [x] Forgejo issue -- https://forgejo.tail5b443a.ts.net/ldraney/paldocs/issues/78, state: open
File Targets
- [x]
app/services/block_parser.rb(new) -- target directoryapp/services/does not exist yet but is standard Rails convention. Will need to be created. OK. - [x]
app/services/block_compiler.rb(new) -- same as above. OK. - [x]
app/controllers/api/notes_controller.rb-- verified at HEAD (commit 311f4a4). Hascreate(line 85) andupdate(line 167). Neither currently calls any block parsing. Confirmed gap. - [x] Reference:
pal-e-docs/src/pal_e_docs/blocks/sync.py(112 lines),parser.py(316 lines),compiler.py(148 lines) -- all exist and verified. - [ ] MISSING target:
Api::NotesController#from_template(line 125) also creates notes with html_content but is not listed. [BODY] Addfrom_templateto File Targets and AC.
Repo Placement
OK -- issue filed on ldraney/paldocs, all file targets are in the paldocs repo. Reference files in pal-e-docs are read-only source for the port.
Dependencies
- Upstream (done): Issue #57 (API namespace + Notes controller) -- merged as commit 311f4a4. Provides the controller to modify.
- Upstream (done): Issue #52 (Embedding pipeline) -- merged as commit 7d63900. Provides
EmbedBlockJobandBlock#after_save :enqueue_embeddingcallback. When this ticket creates Block rows via ActiveRecord, embeddings will be enqueued automatically. - Upstream (done): Issue #49 (Schema ownership) -- blocks table owned by Rails.
- Downstream: Issue #61 (Hybrid search) depends on embeddings working, which depends on blocks being created. This ticket unblocks #61.
- No blockers: No in_progress items block this ticket. Sprint:2 items (#56, #22, #75) are independent.
Acceptance Criteria
- [x] AC 1:
BlockParser.call(html_content)returns block hashes -- testable, clear. - [x] AC 2:
BlockCompiler.call(blocks)reconstructs HTML -- testable, clear. - [x] AC 3:
Api::NotesController#createstores blocks -- testable, clear. - [x] AC 4:
Api::NotesController#updatere-parses on content change -- testable, clear. Controller already trackscontent_changedboolean. - [ ] AC 5: "Postgres NOTIFY fires on new block rows (existing trigger)" -- MISLEADING. The existing Postgres trigger (
blocks_embedding_triggerfrom pal-e-docs Alembic migration) does fire pg_notify on INSERT, but the Rails embedding path uses ActiveRecordafter_savecallback on Block model to enqueueEmbedBlockJobvia Solid Queue. Both paths coexist. [BODY] Reword AC 5: "Block rows created via ActiveRecord triggerafter_saveembedding enqueue (existing Block model callback)" or split into two: one for the Rails path (Solid Queue) and one noting NOTIFY coexistence. - [x] AC 6: Embedding worker picks up blocks -- will work automatically via existing
Block#after_save :enqueue_embeddingcallback. - [ ] MISSING AC:
from_templateaction also creates notes with html_content and needs block parsing. [BODY] Add AC: "Api::NotesController#from_template stores Block rows via parser".
Blast Radius
- sync.py recompile pattern: The Python
parse_and_store_blocks()callsrecompile()which updates acompiled_pagestable and resetsnotes.html_contentfrom compiled output. The issue does not mentioncompiled_pages. If paldocs does not usecompiled_pages, this is fine. If it does, the compiler must also update that table. Need to verify whethercompiled_pagestable exists in Rails schema. - parser.py line count: Issue says "~200 lines" but actual parser.py is 316 lines. The port scope is larger than estimated. compiler.py is 148 lines and sync.py is 112 lines. Total reference code: 576 lines.
- BeautifulSoup vs Nokogiri: Parser uses bs4 (BeautifulSoup). Constraint says "Use Nokogiri" which is correct -- Nokogiri is the Ruby equivalent. Nokogiri is available as a transitive dependency via Rails (loofah), confirmed in Gemfile.lock. However, it is NOT in the explicit Gemfile. [BODY] Add constraint: "Add nokogiri to Gemfile explicitly if needed for direct require, or confirm transitive dependency suffices."
- Destroy action: The existing
Api::NotesController#destroyalready doesBlock.where(note_id: note.id).delete_all-- this bypasses ActiveRecord callbacks (usesdelete_allnotdestroy_all). Not a concern for this ticket but worth noting.
Decomposition Assessment
Borderline -- no decomposition needed, but monitor.
- File targets: 4 (2 new + 1 modified + references). All in 1 repo. Under threshold.
- AC count: 6 + 1 missing = 7. Over the 5 threshold, but all are tightly coupled to a single concern (block parsing on note write).
- Estimated agent time: ~10 minutes. The 576 lines of Python reference code must be understood and ported, plus tests. This exceeds the 5-minute rule.
- Recommendation: The 8-point estimate is appropriate for the scope. The work is cohesive (single service extraction pattern). Decomposition into sub-tickets would create artificial coupling overhead. However, the agent implementing this should be given clear phasing guidance: (1) port parser + compiler as services with unit tests, (2) wire into controller + integration tests.
Recommendation
[BODY]Fix parser.py line count: "~200 lines" should be "~316 lines" (total reference: 576 lines across parser.py, compiler.py, sync.py).[BODY]Addfrom_templateto File Targets and add AC: "Api::NotesController#from_template stores Block rows via parser".[BODY]Reword AC 5 about NOTIFY -- the Rails embedding path is via ActiveRecord after_save callback + Solid Queue, not Postgres NOTIFY. Both coexist on the shared DB but the agent should target the Rails path.[BODY]Add note about compiled_pages table -- clarify whether the Ruby port needs to update compiled_pages or if that table is pal-e-docs-only.[SCOPE]Create architecture notearch-rails-apifor the Rails API component. Multiple board items reference this label.