feat: add note_type, status, parent_note_id, position columns (#60) #61
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/pal-e-api!61
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "60-feat-add-note-type-status-parent-note-id"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements Phase 2 of the Note Decomposition plan (
plan-2026-03-01-note-decomposition). Adds four new columns to thenotestable that enable parent-child note relationships, structured note typing, and per-type status tracking. This is the schema foundation for decomposing monolithic plan notes into composable, individually-queryable phase records.Changes
Alembic Migration (
d4e5f6a7b8c9)note_type— VARCHAR, nullablestatus— VARCHAR, nullableparent_note_id— INTEGER, nullable, FK tonotes.id, ondelete=RESTRICTposition— INTEGER, nullable(parent_note_id, position)SQLAlchemy Model (
models.py)Noteparent/childrenwithback_populateschildrenordered byNote.position, withpassive_deletes=Trueto let DB enforce RESTRICTparent_slugproperty for Pydantic serializationPydantic Schemas (
schemas.py)NoteTypeLiteral with 11 values: plan, phase, sop, convention, issue, todo, template, project-page, skill, agent, docNoteCreateandNoteUpdategainnote_type,status,parent_slug,positionNoteOutandNoteSummarygain same fields in responseRoute Validation (
routes/notes.py)VALID_STATUSESdict — maps each note_type to its valid statuses (~30 lines)ALLOWED_PARENT_TYPESdict —{"phase": ["plan"]}(extensible)_validate_status()— rejects status without note_type, rejects invalid status for type_resolve_parent()— resolves parent_slug to parent_note_id, validates parent-type map, self-parent checklist_notes— new query params:note_type,status,parent_slugcreate_note— accepts and validates all new fieldsupdate_note— same validation, supports status-only updates without rewriting html_contentTests (
test_note_decomposition.py)Test Plan
ruff checkpassesruff format --checkpassesReview Checklist
Related Notes
plan-2026-03-01-note-decomposition— Phase 2note-conventions— the spec defining note_type enum and status-per-typedoc-pal-e-docs-schema— ERD referenceSelf-Review (Dev Agent)
Reviewed the full diff. No blocking issues found.
Checklist
passive_deletes=Trueon children relationship ensures DB-level RESTRICT enforcement (not ORM cascade)parent_slugproperty resolves correctly for Pydanticfrom_attributesserializationNoteTypeLiteral with 11 values, all 4 fields on Create/Update/Out/Summary_validate_statuscovers status-without-type, invalid-status-for-type, unknown-type_resolve_parentcovers type-not-in-map, parent-not-found, self-parent, parent-type-mismatcheffective_note_typein update handles both new-type and existing-type caseslist_notesjoinedload(Note.parent) for parent_slug resolution + 3 new filtersNotes
list_notesquery addsjoinedload(Note.parent)alongsidesubqueryload(Note.tags). Verified no duplicate rows when combined with project join filter.Ready for QA agent review.
PR #61 Review
BLOCKERS
None found. The implementation is correct and complete against the spec.
NITS
create_notemissing eager load ofparentrelationship (src/pal_e_docs/routes/notes.py, line 222)db.refresh(note), theparentrelationship is resolved via lazy load when Pydantic accessesnote.parent_slug. This works within the active session, but bothget_note(line 171) andupdate_note(line 232) explicitly usejoinedload(Note.parent).DetachedInstanceErrorif session handling ever changes), consider re-querying the note withjoinedload(Note.parent)before returning, or adding a line to explicitly accessnote.parentafter refresh to trigger the lazy load.Inconsistent 404 vs empty result on invalid filter values (
src/pal_e_docs/routes/notes.py, lines 152-162)list_notesreturns 404 whenparent_slugreferences a nonexistent note, but silently returns empty results whennote_typeorstatusdon't match anything. This is a reasonable design choice (parent is a FK lookup, the others are string filters), but worth documenting or being aware of.list_notesdoes not order bypositionwhen filtering byparent_slug(src/pal_e_docs/routes/notes.py, line 164)parent_slugis specified, callers likely expect children returned in position order. Currently they're returned inupdated_at descorder. The test at line 526 acknowledges this explicitly ("list_notes orders by updated_at desc, not position -- that's expected").order_by(Note.position)whenparent_slugis set as a follow-up improvement.No API mechanism to unset
parent_slugNoteUpdate.parent_slugisstr | NonewhereNonemeans "don't change". There's no way to remove an existing parent (setparent_note_idback to NULL). This is a known limitation of the current update API pattern (same issue exists for other nullable fields likeproject_slug). Not blocking for Phase 2 -- could be addressed in a follow-up.Test coverage gap: only 3 of 11 note_types have status validation tests
plan,phase, anddocexplicitly. The other 8 types (sop, convention, issue, todo, template, project-page, skill, agent) rely on the sameVALID_STATUSESdict lookup, so the code path is identical. Not blocking because the validation mechanism is tested; the parametric coverage is nice-to-have.VERIFICATION CHECKLIST
c3d4e5f6a7b8->d4e5f6a7b8c9parent_note_idFK ondelete=RESTRICT -- matches spec(parent_note_id, position)-- matches specparent_slugpropertyNoteTypeLiteral has all 11 values matchingnote-conventionsspec exactlyVALID_STATUSESdict matchesnote-conventionsspec for all 11 typesALLOWED_PARENT_TYPES:{"phase": ["plan"]}-- matches spec_validate_status: rejects status-without-type (422), rejects invalid status for type (422)_resolve_parent: validates parent-type map, self-parent check (422), parent-not-found (404)NoteCreate,NoteUpdate,NoteOut,NoteSummaryall updated with 4 new fieldslist_notesacceptsnote_type,status,parent_slugquery paramscreate_noteandupdate_noteaccept and validate all new fieldsupdate_notecorrectly useseffective_note_type(body value or existing) for validationSOP COMPLIANCE
60-feat-add-note-type-status-parent-note-idreferences issue #60)template-pr-body(## Summary, ## Changes, ## Test Plan, ## Review Checklist, ## Related Notes)plan-2026-03-01-note-decomposition)test_note_decomposition.py)VERDICT: APPROVED
Clean implementation. All 12 acceptance criteria from the issue are met. The VALID_STATUSES dict and NoteType Literal match the
note-conventionsspec exactly. Validation logic is thorough with proper error codes. Tests cover all critical paths. Migration is SQLite-safe. Fully backward compatible.The 5 nits above are all non-blocking improvements that could be addressed in follow-up work.
QA Nit Fixes (Round 1)
All 5 non-blocking nits from QA review have been addressed in commit
0d42be6:joinedload(Note.parent)instead ofdb.refresh()in bothcreate_noteandupdate_notetest_create_note_response_includes_parent_slug,test_update_note_response_includes_parent_slug[]instead of raising 404, consistent with tags/project filterstest_list_notes_filter_parent_slug_not_found_returns_empty.order_by(Note.position.asc().nullslast())when parent_slug is activetest_children_position_ordering(now asserts strict[1, 2, 3]order)parent_slug=""setsparent_note_id=None, skips validationtest_unset_parent_via_empty_string@pytest.mark.parametrizewith 30 valid pairs + 11 invalid pairs across all 11 note_typestest_valid_status_accepted_per_type[*],test_invalid_status_rejected_per_type[*]Results: 71 decomposition tests, 240 total tests. All pass. Ruff clean.
PR #61 Review (Round 2)
BLOCKERS
None.
NITS
1. Changing
note_typedoes not re-validate existingstatus(data integrity gap)In
update_note(routes/notes.py lines 270-280), if a caller sends{"note_type": "issue"}on a note that currently hasnote_type=plan, status=active, the note_type is updated toissuebut the existingstatus=activeis NOT re-validated against the new type. Sinceactiveis not a valid status forissue, this leaves the database in an invalid state.The status validation only fires when
body.status is not None:This is consistent with partial-update semantics (omitting a field means "don't touch it"), so I am not calling it a blocker. But it is a gap worth noting. A future improvement could either (a) re-validate existing status when note_type changes, or (b) clear status to None when note_type changes.
Non-blocking because:
ROUND 1 NIT VERIFICATION
All 5 nits from the round 1 review are confirmed fixed:
joinedload(Note.parent)after commit (lines 226-231 and 311-317). Tests at lines 705 and 726 verifyparent_slugis populated in the response.[]when parent not found (line 160-162). Test at line 435 verifies.q.order_by(Note.position.asc().nullslast())at line 167. Test at line 487 creates children out of order and verifies sorted return.parent_slug=""on update -- Empty string check at line 284-286 setsparent_note_id = None. Test at line 670 verifies._valid_pairs(27 combos) and_invalid_pairs(11 combos) at lines 754-819, using@pytest.mark.parametrize.CORRECTNESS
(parent_note_id, position). Downgrade drops in correct order. Chain correct (c3d4e5f6a7b8->d4e5f6a7b8c9). SQLite-compatible (follows same pattern as existingc3d4e5f6a7b8migration which is deployed).remote_side=[id],foreign_keys=[parent_note_id],back_populates. Children ordered byNote.positionwithpassive_deletes=True.parent_slugcomputed property.NoteTypeLiteral with all 11 values matching note-conventions spec.NoteCreate,NoteUpdate,NoteOut,NoteSummaryall include the 4 new fields.{"phase": ["plan"]}matches spec._validate_statusrejects status without note_type, rejects invalid status for type._resolve_parentvalidates parent-type map, self-parent check, parent existence.note_type,status,parent_slug. Returns empty list for nonexistent parent. Orders by position when filtering by parent.parent_slugtoparent_note_id.test_delete_parent_with_children_failsconfirms 409.conftest.pyanddatabase.pyboth enablePRAGMA foreign_keys=ON.SECURITY
note_type,status,parent_slug) are used via SQLAlchemy ORM filters, which are parameterized. No injection risk.TEST COVERAGE
35 test functions in
test_note_decomposition.py, expanding to ~71 test cases with parametrization:All existing tests (test_crud.py etc.) should pass unchanged since all new columns are nullable.
SOP COMPLIANCE
60-feat-add-note-type-status-parent-note-id)template-pr-body(Summary, Changes, Test Plan, Review Checklist, Related Notes)plan-2026-03-01-note-decomposition)VERDICT: APPROVED
Clean implementation that matches the spec exactly. All round 1 nits fixed. One non-blocking data integrity nit noted for future consideration (note_type change without status re-validation). No blockers.