Skip to content

CON-188: add per-post Notes entity (post_notes) - #103

Merged
grsmv merged 3 commits into
mainfrom
feature/con-188-post-notes-entity
Aug 6, 2026
Merged

CON-188: add per-post Notes entity (post_notes)#103
grsmv merged 3 commits into
mainfrom
feature/con-188-post-notes-entity

Conversation

@grsmv

@grsmv grsmv commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Adds a first-class Note attached to a Post — a small standalone record (title?, body, type, origin, created_by, timestamps) that captures ancillary content (draft theses, image prompts, side notes) outside the post body.

Closes CON-188.

Why

Today the content-plan flow dumps a bullet-point "draft thesis" straight into posts.content, and any assistant-produced artifact (e.g. an image prompt) either overwrites the body or is lost. Notes give these a proper home and let the assistant read/write them without touching the copy.

What's included

  • post_notes table (tenant-scoped, ON DELETE CASCADE from posts) + models.PostNote with PostNoteType {draft_thesis, image_prompt, note} (Go-validated so the set can grow without a migration) and PostNoteOrigin {manual, assistant, content_plan} (DB CHECK).
  • Shared notes.Service (validation + origin stamping) reused by the REST CRUD and the assistant tool, so the two entry points can't drift.
  • REST CRUD under /api/posts/:post_id/notes (PostNotesHandler, mirrors post_attachments): list/create/get/patch/delete, wrong-post → 404, validation → 400, CON-125 activity events.
  • Content-plan integration: generated posts are now created with an empty body; the thesis is stored as a draft_thesis note (origin=content_plan, author = campaign owner). Empty thesis → no note. A note-write failure is logged, not fatal (preserves the CON-66 "persisted work survives" guarantee).
  • Post-assistant integration: new createNote tool (image_prompt/note only — draft_thesis is reserved for content-plan), existing notes injected into the LLM context, response gains NotesCreated + a new Action="noted". Notes are additive — a single turn can edit the body and create notes; a notes-only turn is noted. Includes prompt "Notes mode", a note_created SSE event, prewarm tool-list update, and noteCount in the persisted turn history.
  • http-client/posts/notes.http + unit and integration tests.

Locked product decisions

Decision Choice
Post body when content-plan writes a thesis note Empty — thesis li` note
Track how a note was created origin enum {manual, assistant, content_plan}; created_by is always a real user
image_prompt notes Store text only — no image generation (CON-10sumer)

API

GET    /api/posts/:post_id/notes           # draft_thesis first, then created_at ASC
POST   /api/posts/:post_id/notes           # {type, title?, body} → 201; o
GET    /api/posts/:post_id/notes/:id
PATCH  /api/posts/:post_id/notes/:id        # partial: {type?|title?|body?
DELETE /api/posts/:post_id/notes/:id        # 204

Behavior change to call out

Content-plan drafts now render with an empty posts.content until auts in the draft_thesis note. Anything reading posts.content (posts list,quality assessment CON-184, publish-readiness) will see empty drafts. This is the intended new workflow, not a regression.

Testing

  • go build ./... and go vet ./src/... clean.
  • New unit tests pass: notes.Service validation, content-plan persistOn note, empty-thesis skip, nil-repo safety), assistant createNote` tool(origin/reserved-type/unavailable).
  • Integration test post_notes_test.go covers CRUD, ordering, validation,st — run with -tags integration against a Postgres instance.

Migration

20260806000001_post_notes.{up,down}.sql — additive, auto-discovered. `do

Summary by CodeRabbit

  • New Features
    • Added per-post notes with create, view, edit, and delete support.
    • Added draft thesis, image prompt, and general note types.
    • Post Assistant can create notes, display attached notes, and report note-created events.
    • Content-plan theses are saved as draft thesis notes.
    • Notes are ordered with draft theses first, followed by older notes.
  • Validation
    • Added validation for note types, required bodies, and title/body length limits.
  • Testing
    • Added coverage for note workflows, assistant note creation, content-plan behavior, and API validation.

A Note is a small standalone record attached to a Post (title?/body/type/
origin/created_by/timestamps) in a tenant-scoped post_notes table, so
ancillary content lives outside the post body.

- content-plan now leaves the post body empty and stores the generated
  bullet thesis as a draft_thesis note (origin=content_plan)
- post assistant gains a createNote tool (image_prompt/note), reads a post's
  existing notes as context, and supports a "noted" action — standalone or
  alongside an edit in the same turn
- REST CRUD under /api/posts/:post_id/notes backed by a shared notes.Service
  (also used by the assistant tool) with draft_thesis-first ordering
- http-client file + unit/integration tests
@linear-code

linear-code Bot commented Aug 6, 2026

Copy link
Copy Markdown

CON-188

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@grsmv, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a789a38-1a1b-438a-b9db-ad8cf28b840e

📥 Commits

Reviewing files that changed from the base of the PR and between 2546715 and 92d1b87.

📒 Files selected for processing (1)
  • src/genkit/flows/post_assistant/context.go

Walkthrough

Adds tenant-scoped post notes with shared persistence, authenticated CRUD endpoints, content-plan thesis storage, and post-assistant note creation and context support.

Changes

Post Notes

Layer / File(s) Summary
Note storage and service
src/database/migrations/*, src/models/post_note.go, src/notes/*, src/repository/post_notes.go
Adds the post_notes table, typed model, repository operations, validation, creation, updates, listing, and deletion.
Post-note REST API
src/handlers/post_notes.go, src/integration/post_notes_test.go, http-client/posts/notes.http
Adds authenticated nested CRUD endpoints with ownership checks, validation, activity events, integration tests, and HTTP examples.
Content-plan thesis notes
src/genkit/flows/content_plan/*
Stores non-empty generated thesis text as draft_thesis notes and keeps generated post bodies empty.
Post-assistant note flow
src/genkit/flows/post_assistant/*
Adds note context, the createNote tool, note-created events, note responses, notes-only actions, prompt guidance, and tests.
Server dependency wiring
src/server/*
Creates the shared note service and wires it into REST, content-plan, and post-assistant initialization.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PostNotesHandler
  participant NotesService
  participant PostNoteRepository
  participant Database
  Client->>PostNotesHandler: Submit authenticated note request
  PostNotesHandler->>NotesService: Validate and execute operation
  NotesService->>PostNoteRepository: Create, list, update, or delete
  PostNoteRepository->>Database: Read or write post_notes
  Database-->>PostNoteRepository: Return operation result
  PostNoteRepository-->>NotesService: Return note data or status
  NotesService-->>PostNotesHandler: Return service result
  PostNotesHandler-->>Client: Return HTTP response
Loading

Possibly related PRs

  • ogen-app/ogen#35: Extends the same post-assistant tools, responses, prompts, and server wiring.
  • ogen-app/ogen#41: Modifies the same post-assistant context, tools, responses, and server wiring.
  • ogen-app/ogen#68: Modifies the content-plan flow, including generate.go.

Suggested labels: to test

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the addition of the per-post Notes entity, which is the main change in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/con-188-post-notes-entity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/genkit/flows/content_plan/generate.go`:
- Around line 546-550: Update the DraftPost.Body JSON schema description in the
content-plan types and the associated content-plan prompt to request a concise
bullet-point thesis rather than complete platform-adapted post copy. Keep the
contract aligned with persistOne, which stores Body as draft_thesis while
leaving Post.Content empty.

In `@src/genkit/flows/post_assistant/context.go`:
- Around line 230-240: Bound aggregate note context in the note-summary
construction after repos.Notes.ListByPostID by enforcing a total note-count or
rune budget while iterating in repository order, stopping once the budget is
reached; retain per-body truncation and existing error handling. Add a boundary
test covering the limit and confirming ordering.
- Around line 220-242: Invalidate the cached context entry for the post
immediately after a successful note persistence in the createNote tool flow.
Update the contextCache entry keyed by post.ID after the Notes repository create
operation completes, while preserving existing error handling and leaving
buildNoteSummaries unchanged.

In `@src/genkit/flows/post_assistant/prompts/post_assistant.tmpl`:
- Around line 38-41: Update the notes-mode instructions in the post assistant
prompt and the response schema/tool guidance they conflict with: allow action
"noted" in the response schema and permit createNote for ordinary note requests.
Preserve the existing createNote type mapping, empty updatedContent requirement,
and edited-post-plus-note behavior.

In `@src/genkit/flows/post_assistant/run.go`:
- Around line 405-419: Update the action assignment in the note-results handling
around result.NotesCreated so the notes-only fallback executes only when
result.UpdatedContent is empty, in addition to the existing action and result
checks. Preserve non-empty UpdatedContent and avoid setting result.Action to
"noted" or clearing edited content for truncated combined edit-and-note
responses.

In `@src/handlers/post_notes.go`:
- Around line 49-59: Update PostNotesHandler.loadPostOrErr and the repository
lookup it uses to distinguish an absent post from a post belonging to another
tenant: preserve 404 for sql.ErrNoRows when the post does not exist, but
propagate a distinct tenant-authority error and map it to fiber.StatusForbidden
in List, Get, and Create. Add an integration test covering a note endpoint
request using another tenant’s post and assert a 403 response.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d36aa4f9-e527-4b04-baaf-648f0f0162ca

📥 Commits

Reviewing files that changed from the base of the PR and between 361b681 and ac24624.

📒 Files selected for processing (22)
  • http-client/posts/notes.http
  • src/database/migrations/20260806000001_post_notes.down.sql
  • src/database/migrations/20260806000001_post_notes.up.sql
  • src/genkit/flows/content_plan/flow.go
  • src/genkit/flows/content_plan/generate.go
  • src/genkit/flows/content_plan/generate_test.go
  • src/genkit/flows/post_assistant/context.go
  • src/genkit/flows/post_assistant/post_note_tool_test.go
  • src/genkit/flows/post_assistant/prewarm.go
  • src/genkit/flows/post_assistant/prompts/post_assistant.tmpl
  • src/genkit/flows/post_assistant/run.go
  • src/genkit/flows/post_assistant/tools.go
  • src/genkit/flows/post_assistant/types.go
  • src/handlers/post_notes.go
  • src/integration/post_notes_test.go
  • src/models/post_note.go
  • src/notes/service.go
  • src/notes/service_test.go
  • src/repository/post_notes.go
  • src/server/genkit_runtime.go
  • src/server/post_assistant.go
  • src/server/server.go

Comment on lines +546 to +550
//
// CON-188: the model's bullet-point thesis (dp.Body) is no longer written into
// the post body. The post is created with an empty body and the thesis is
// stored as a draft_thesis note, so the assistant can later expand it into copy.
func persistOne(ctx context.Context, dp DraftPost, campaign *models.Campaign, postRepo repository.PostRepository, noteRepo repository.PostNoteRepository, usedAssetIDs []string) (string, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align the generated-field contract.

persistOne now treats DraftPost.Body as a bullet-point thesis. src/genkit/flows/content_plan/types.go still instructs the model to generate “Complete post copy adapted to the platform”.

The model can generate publishable copy, but this flow stores it as draft_thesis and leaves Post.Content empty. Update the DraftPost.Body JSON schema description and the content-plan prompt to request a thesis consistently.

Proposed contract update
- Body string `json:"body" jsonschema:"description=Complete post copy adapted to the platform"`
+ Body string `json:"body" jsonschema:"description=Bullet-point thesis for a future post expansion"`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genkit/flows/content_plan/generate.go` around lines 546 - 550, Update the
DraftPost.Body JSON schema description in the content-plan types and the
associated content-plan prompt to request a concise bullet-point thesis rather
than complete platform-adapted post copy. Keep the contract aligned with
persistOne, which stores Body as draft_thesis while leaving Post.Content empty.

Comment thread src/genkit/flows/post_assistant/context.go
Comment thread src/genkit/flows/post_assistant/context.go Outdated
Comment thread src/genkit/flows/post_assistant/prompts/post_assistant.tmpl
Comment thread src/genkit/flows/post_assistant/run.go
Comment on lines +49 to +59
// loadPostOrErr fetches the parent post, returning 404 when it is missing or
// belongs to another tenant.
func (h *PostNotesHandler) loadPostOrErr(c *fiber.Ctx) (*models.Post, error) {
post, err := h.postRepo.GetByID(c.Context(), c.Params("post_id"))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fiber.NewError(fiber.StatusNotFound, "post not found")
}
return nil, err
}
return post, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect post lookup and tenant authorization behavior.
fd -e go . src/repository -x rg -n -C 5 'func \(.*\) GetByID\(' {}
rg -n -C 5 'TenantScoped|tenant_id|ErrNoRows|StatusForbidden' src/models src/repository src/handlers

Repository: ogen-app/ogen

Length of output: 4802


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)src/(handlers|models|repository).*\.go$|(^|/)src/.*\.go$' | sed -n '1,160p'

echo
echo "== post handlers and routes =="
fd -e go post_notes post router . src | sed -n '1,120p'
for f in $(fd -e go 'post_notes|notes' src/handlers); do
  echo "--- $f"
  wc -l "$f"
  sed -n '1,180p' "$f"
done

echo
echo "== repository getByID implementations mentioning posts =="
rg -n -C 12 'func \(.*\) GetByID\([^)]*\*\s*models\.Post|GetByID.*Post|scopeTenantRead|tenant_id|TenantScoped' src -g '*.go'

echo
echo "== route auth setup =="
rg -n -C 6 'RequireAuth|Register.*Notes|post_notes|PostNotesHandler|NewPostNotesHandler' src

Repository: ogen-app/ogen

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== targeted file list =="
git ls-files 'src/handlers/post_notes.go' 'src/repository/posts.go' 'src/models/*.go' 'src/*.go' | sed -n '1,120p'

echo
echo "== src/handlers/post_notes.go =="
wc -l src/handlers/post_notes.go
sed -n '1,180p' src/handlers/post_notes.go

echo
echo "== src/repository/posts.go focused =="
wc -l src/repository/posts.go
sed -n '80,135p' src/repository/posts.go

echo
echo "== models.TenantScoped / Post =="
rg -n -C 8 'type Post struct|TenantScoped|tenant_id' src/models -g '*.go'

echo
echo "== router/auth registration focused =="
rg -n -C 8 'RequireAuth\(|Register.*Notes|PostNotesHandler|NewPostNotesHandler|Notes' src -g '*.go'

Repository: ogen-app/ogen

Length of output: 50370


Return 403 for cross-tenant post lookups.

loadPostOrErr returns the same sql.ErrNoRows path for a missing post and a tenant-denied post, and List/Get/Create convert it to 404. Keep 404 only when the post is absent. Add a distinct tenant-authority failure and return 403 for it.

Also add an integration test that calls a note endpoint with a post from another tenant and expects 403.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/handlers/post_notes.go` around lines 49 - 59, Update
PostNotesHandler.loadPostOrErr and the repository lookup it uses to distinguish
an absent post from a post belonging to another tenant: preserve 404 for
sql.ErrNoRows when the post does not exist, but propagate a distinct
tenant-authority error and map it to fiber.StatusForbidden in List, Get, and
Create. Add an integration test covering a note endpoint request using another
tenant’s post and assert a 403 response.

Source: Path instructions

- content_plan: fix DraftPost.Body schema description to match the prompt and
  persistOne (a bullet-point thesis stored as a draft_thesis note, not copy)
- post_assistant context: bound the aggregate note section (count + rune
  budget) in repository order so draft_thesis is never the entry dropped;
  add a boundary/ordering test
- post_assistant: invalidate the context cache after createNote persists a
  note (notes aren't part of the cache fingerprint)
- post_assistant prompt: allow action "noted" in the response schema and
  scope the "don't call tools" note to the asset-retrieval tools so createNote
  is permitted for ordinary note requests
- post_assistant run: guard the notes-only fallback on empty updatedContent so
  a truncated combined edit-and-note turn keeps its edit

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/genkit/flows/post_assistant/post_note_tool_test.go (1)

85-102: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add a rune-budget boundary test.

This test only reaches maxNotesInContext. It cannot detect removal or regression of maxNotesContextRunes.

Add 20 notes with 800-rune bodies. Assert that buildNoteSummaries returns five entries and retains the first draft_thesis.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/genkit/flows/post_assistant/post_note_tool_test.go` around lines 85 -
102, Add a rune-budget boundary case to TestBuildNoteSummaries_LimitAndOrdering
using 20 notes whose bodies are 800 runes each, so the maxNotesContextRunes
limit is exercised independently of maxNotesInContext. Assert that
buildNoteSummaries returns exactly five entries and that the first entry remains
the draft_thesis with its pinned body.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/genkit/flows/post_assistant/context.go`:
- Around line 117-120: Add per-post generation tracking protected by
contextCacheMu around assembleContextCached: capture the current generation
before assembling and only write actx to contextCache if it is still unchanged
afterward. Update invalidateContextCache to increment the post’s generation
while deleting its cached entry, preventing in-flight stale assemblies from
repopulating the cache.

---

Nitpick comments:
In `@src/genkit/flows/post_assistant/post_note_tool_test.go`:
- Around line 85-102: Add a rune-budget boundary case to
TestBuildNoteSummaries_LimitAndOrdering using 20 notes whose bodies are 800
runes each, so the maxNotesContextRunes limit is exercised independently of
maxNotesInContext. Assert that buildNoteSummaries returns exactly five entries
and that the first entry remains the draft_thesis with its pinned body.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 571b8412-2697-4e07-b154-019fe4e97e41

📥 Commits

Reviewing files that changed from the base of the PR and between ac24624 and 2546715.

📒 Files selected for processing (6)
  • src/genkit/flows/content_plan/types.go
  • src/genkit/flows/post_assistant/context.go
  • src/genkit/flows/post_assistant/post_note_tool_test.go
  • src/genkit/flows/post_assistant/prompts/post_assistant.tmpl
  • src/genkit/flows/post_assistant/run.go
  • src/genkit/flows/post_assistant/tools.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/genkit/flows/post_assistant/run.go
  • src/genkit/flows/post_assistant/prompts/post_assistant.tmpl
  • src/genkit/flows/post_assistant/tools.go

Comment thread src/genkit/flows/post_assistant/context.go
Track a per-post generation bumped by invalidateContextCache. Capture it
before the unlocked assemble and only write the result back if unchanged,
so an invalidation racing an in-flight assembly can't be undone by the
stale result repopulating the cache (notes aren't in the fingerprint).
@grsmv
grsmv merged commit a3f0bcf into main Aug 6, 2026
6 checks passed
@grsmv
grsmv deleted the feature/con-188-post-notes-entity branch August 15, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant