Phase 3: Gapped integer positions for block inserts #188

Closed
opened 2026-03-17 02:42:32 +00:00 by forgejo_admin · 0 comments

Lineage

plan-2026-03-16-knowledge-architecture → Phase 3 (Gapped Integer Positions)

Repo

forgejo_admin/pal-e-docs

User Story

As a session manager using create_block to insert content into existing notes,
I want to insert a block between two existing blocks without shifting every block after the insertion point,
so that inserts are O(1) instead of O(n) and don't touch rows I didn't intend to modify.

Context

Inserting a heading at position 20 in a note with 40 blocks currently requires the API to shift all blocks at positions 20+ up by 1 — that's 20 unnecessary writes. Similarly, deleting any block triggers _reindex_positions which rewrites ALL block positions to sequential 0,1,2,3...

The fix is gapped integers: blocks get positions 0, 1000, 2000, 3000... Inserting between 1000 and 2000 uses position 1500 with zero other writes. Deleting a block just leaves a gap — gaps are normal. A rebalance endpoint exists for the rare case where gaps exhaust.

This only applies to Block positions. Note child positions and board item positions are out of scope (they already tolerate gaps/collisions).

File Targets

Files to modify:

  • src/pal_e_docs/blocks/parser.py lines 63-64 — change position = len(blocks) to position = len(blocks) * 1000 (gapped spacing). Also update the bare-text paragraph path around line 53 to use pos * 1000.
  • src/pal_e_docs/routes/blocks.py lines 49-54 (_reindex_positions) — keep the function but do NOT call it from delete_block. It will be used only by the new rebalance endpoint (with gapped spacing: i * 1000 instead of i).
  • src/pal_e_docs/routes/blocks.py lines 235-243 (create_block shift logic) — change to only shift when there's an exact position collision (existing block at the exact same position), not all blocks at position >= target. When shifting for a collision, shift just the colliding block by +1.
  • src/pal_e_docs/routes/blocks.py line 286 (delete_block) — remove the _reindex_positions call. Gaps after delete are normal.
  • src/pal_e_docs/routes/blocks.py (new endpoint) — add POST /notes/{slug}/blocks/rebalance that renumbers all blocks to gapped positions (0, 1000, 2000...) preserving current order.

Files NOT to touch:

  • src/pal_e_docs/blocks/sync.pyparse_and_store_blocks takes positions from parser dict, which will already have gapped values. No changes needed.
  • src/pal_e_docs/blocks/compiler.py — compiles from block list, position-agnostic.
  • src/pal_e_docs/routes/notes.py — note-level endpoints unaffected.
  • src/pal_e_docs/models.py — Block.position is already an Integer column, no schema changes.
  • Any SDK or MCP files — position is already an int param, gaps are transparent.

Acceptance Criteria

  • When parse_and_store_blocks creates blocks from HTML, positions are 0, 1000, 2000, 3000... (gap of 1000)
  • When create_block(position=1500) is called and no block exists at 1500, the block is inserted with zero other position changes
  • When create_block(position=1000) is called and a block already exists at 1000, the existing block at 1000 shifts to 1001 (backward compat)
  • When delete_block is called, remaining blocks keep their positions — no reindexing to sequential
  • get_section still works — section retrieval uses Block.position > range queries, which work with gaps
  • POST /notes/{slug}/blocks/rebalance renumbers all blocks to gapped positions (0, 1000, 2000...) preserving order
  • Existing notes with sequential positions (0,1,2,3...) still render and query correctly — no migration required
  • All existing tests pass (pytest tests/)

Test Expectations

  • New test: parse_html_to_blocks produces gapped positions (0, 1000, 2000...)
  • New test: create_block between gapped positions (e.g., position=1500 between 1000 and 2000) — verify no other blocks shift
  • New test: create_block at occupied position — verify single shift of colliding block
  • New test: delete_block — verify remaining positions unchanged (no reindex to sequential)
  • New test: rebalance endpoint — verify positions renumbered to 0, 1000, 2000...
  • Existing tests all pass: pytest tests/
  • Run command: pytest tests/ -x -q

Constraints

  • Match existing route style in routes/blocks.py (FastAPI, Depends(get_db), response_model pattern)
  • The gap constant (1000) should be a module-level constant (e.g., POSITION_GAP = 1000) defined once and imported where needed
  • Do not add an Alembic migration — this is a behavioral change, not a schema change
  • The _reindex_positions helper should be updated to use gapped spacing (i * POSITION_GAP) and renamed to _rebalance_positions for clarity, but only called from the rebalance endpoint
  • Existing sequential positions (0,1,2,3...) must continue to work — ordering is by position value, gaps don't break anything

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • project-pal-e-docs — project this affects
  • plan-2026-03-16-knowledge-architecture — parent plan
### Lineage `plan-2026-03-16-knowledge-architecture` → Phase 3 (Gapped Integer Positions) ### Repo `forgejo_admin/pal-e-docs` ### User Story As a session manager using `create_block` to insert content into existing notes, I want to insert a block between two existing blocks without shifting every block after the insertion point, so that inserts are O(1) instead of O(n) and don't touch rows I didn't intend to modify. ### Context Inserting a heading at position 20 in a note with 40 blocks currently requires the API to shift all blocks at positions 20+ up by 1 — that's 20 unnecessary writes. Similarly, deleting any block triggers `_reindex_positions` which rewrites ALL block positions to sequential 0,1,2,3... The fix is gapped integers: blocks get positions 0, 1000, 2000, 3000... Inserting between 1000 and 2000 uses position 1500 with zero other writes. Deleting a block just leaves a gap — gaps are normal. A rebalance endpoint exists for the rare case where gaps exhaust. This only applies to Block positions. Note child positions and board item positions are out of scope (they already tolerate gaps/collisions). ### File Targets Files to modify: - `src/pal_e_docs/blocks/parser.py` lines 63-64 — change `position = len(blocks)` to `position = len(blocks) * 1000` (gapped spacing). Also update the bare-text paragraph path around line 53 to use `pos * 1000`. - `src/pal_e_docs/routes/blocks.py` lines 49-54 (`_reindex_positions`) — keep the function but do NOT call it from `delete_block`. It will be used only by the new rebalance endpoint (with gapped spacing: `i * 1000` instead of `i`). - `src/pal_e_docs/routes/blocks.py` lines 235-243 (create_block shift logic) — change to only shift when there's an exact position collision (existing block at the exact same position), not all blocks at `position >= target`. When shifting for a collision, shift just the colliding block by +1. - `src/pal_e_docs/routes/blocks.py` line 286 (delete_block) — remove the `_reindex_positions` call. Gaps after delete are normal. - `src/pal_e_docs/routes/blocks.py` (new endpoint) — add `POST /notes/{slug}/blocks/rebalance` that renumbers all blocks to gapped positions (0, 1000, 2000...) preserving current order. Files NOT to touch: - `src/pal_e_docs/blocks/sync.py` — `parse_and_store_blocks` takes positions from parser dict, which will already have gapped values. No changes needed. - `src/pal_e_docs/blocks/compiler.py` — compiles from block list, position-agnostic. - `src/pal_e_docs/routes/notes.py` — note-level endpoints unaffected. - `src/pal_e_docs/models.py` — Block.position is already an Integer column, no schema changes. - Any SDK or MCP files — position is already an int param, gaps are transparent. ### Acceptance Criteria - [ ] When `parse_and_store_blocks` creates blocks from HTML, positions are 0, 1000, 2000, 3000... (gap of 1000) - [ ] When `create_block(position=1500)` is called and no block exists at 1500, the block is inserted with zero other position changes - [ ] When `create_block(position=1000)` is called and a block already exists at 1000, the existing block at 1000 shifts to 1001 (backward compat) - [ ] When `delete_block` is called, remaining blocks keep their positions — no reindexing to sequential - [ ] `get_section` still works — section retrieval uses `Block.position >` range queries, which work with gaps - [ ] `POST /notes/{slug}/blocks/rebalance` renumbers all blocks to gapped positions (0, 1000, 2000...) preserving order - [ ] Existing notes with sequential positions (0,1,2,3...) still render and query correctly — no migration required - [ ] All existing tests pass (`pytest tests/`) ### Test Expectations - [ ] New test: parse_html_to_blocks produces gapped positions (0, 1000, 2000...) - [ ] New test: create_block between gapped positions (e.g., position=1500 between 1000 and 2000) — verify no other blocks shift - [ ] New test: create_block at occupied position — verify single shift of colliding block - [ ] New test: delete_block — verify remaining positions unchanged (no reindex to sequential) - [ ] New test: rebalance endpoint — verify positions renumbered to 0, 1000, 2000... - [ ] Existing tests all pass: `pytest tests/` - Run command: `pytest tests/ -x -q` ### Constraints - Match existing route style in `routes/blocks.py` (FastAPI, Depends(get_db), response_model pattern) - The gap constant (1000) should be a module-level constant (e.g., `POSITION_GAP = 1000`) defined once and imported where needed - Do not add an Alembic migration — this is a behavioral change, not a schema change - The `_reindex_positions` helper should be updated to use gapped spacing (`i * POSITION_GAP`) and renamed to `_rebalance_positions` for clarity, but only called from the rebalance endpoint - Existing sequential positions (0,1,2,3...) must continue to work — ordering is by position value, gaps don't break anything ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes ### Related - `project-pal-e-docs` — project this affects - `plan-2026-03-16-knowledge-architecture` — parent plan
forgejo_admin 2026-03-17 02:55:42 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/pal-e-api#188
No description provided.