Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/features/pericope-ai-suggestions/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Pericope AI suggestions

Supports [fluent-web#394](https://github.com/eten-tech-foundation/fluent-web/issues/394). Depends on the `markers.headings` storage contract in [fluent-api#320](https://github.com/eten-tech-foundation/fluent-api/pull/320).

The editor queues the active and next pericope by their source identifiers. The API resolves each identifier against the project's selected pericope set, source Bible, book, and chapter assignment. It queues individual source verses whose translation is absent or blank and whose suggestion is not already cached. Verse 1 and saved empty rows participate. Nonempty translations remain untouched.

Groups with a nonempty source title may also queue a heading-only job. A heading is omitted when the first target verse already has authored `markers.headings`, or when a title suggestion is cached. Title jobs use distinct singleton keys including the selected set and exact source range. Existing scripture job keys and payloads remain compatible.

## Public HTTP contract

Use the exact `pericopeNumber` returned by the chapter pericopes endpoint. Sets with sections use a compound identity such as `1_4a`; this keeps two sections that reuse the same raw pericope number separate.

- `POST /ai-suggestions/queue-pericopes`: `{projectUnitId,bibleId,bookCode,chapterNumber,pericopeNumbers:string[]}` → `{queued,thresholdMet}`. Accepts 1–2 unique identifiers, each 1–100 characters without commas. Chapter assignment AI enablement and the existing activation threshold control queuing. Invalid batches queue nothing. Queue submission failures return an error; singleton deduplication is successful submission.
- `GET /ai-suggestions/pericopes`: the same fields in the query; `pericopeNumbers=4a,4b` is a comma-separated string. Returns `{data:[{pericopeNumber,bibleTextId,suggestedText,modelInfo?}]}`. `bibleTextId` identifies the first source-backed verse in the chapter group. Groups without source titles or with authored headings return no title.
- `POST /ai-suggestions/pericopes/usage`: `{projectUnitId,bibleTextId,pericopeNumber,wasUsed}`. A matching persisted suggestion from the current set must exist. Exposure (`false`) and acceptance (`true`) are recorded separately from verse suggestion usage. Once accepted, a delayed exposure cannot change the record back to false.

All public endpoints reuse authenticated project access and `project:view` checks. Source verse IDs and title text are resolved on the server, never supplied by the browser.

## Worker HTTP contract

Existing trigger/context fields remain required. Optional `pericopeNumber` selects a heading-only job, and API-generated jobs also include `pericopeSetId`. Heading context validates the exact server-derived range and current set. The response adds `sectionHeading:{pericopeNumber,pericopeSetId,bibleTextId,sourceTitle}` or `null` when title generation no longer applies. `sourceVerses` is ordered and limited to exact pericope membership, including for sparse ranges. The worker treats `sectionHeading:null` as a successful no-op.

`POST /ai-suggestions/internal/results` continues accepting `{items:[...]}` for scripture. Heading jobs send `{items:[],heading:{projectUnitId,bibleTextId,pericopeNumber,pericopeSetId,suggestedText,modelInfo?}}`; mixed heading/scripture results are rejected. Heading text uses the same validator as authored headings: trimmed, 1–300 UTF-16 units, no backslashes or line breaks. A title result never writes scripture or markers. Results are cached once, scoped by project unit, first verse, selected set, and pericope identifier. Old-set results are rejected and old-set caches are never served for a new set.

Migration `0029_add_pericope_ai_suggestions` creates `ai_pericope_suggestions` and `ai_pericope_suggestion_usage`, with cascading references and uniqueness constraints. No existing translation data is rewritten.

## Validation

Unit and route tests cover gates, authorization, schema bounds, exact verse jobs, preservation, omitted titles, singleton behavior, submission failures, and heading context/results. The opt-in PostgreSQL suite applies the full migration history and checks real joins, source isolation, persistent cache uniqueness, monotonic usage, authored text preservation, and set changes.

Run the integration suite only with a disposable PostgreSQL database named `fluent394_api` bound to `127.0.0.1:55494`, supplying its URL in `PERICOPE_TEST_DATABASE_URL`. The suite refuses any other database target. It creates fixture data only inside that disposable database.
31 changes: 31 additions & 0 deletions src/db/migrations/0031_add_pericope_ai_suggestions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
CREATE TABLE "ai_pericope_suggestion_usage" (
"id" serial PRIMARY KEY NOT NULL,
"suggestion_id" integer NOT NULL,
"user_id" integer NOT NULL,
"was_used" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "ai_pericope_suggestions" (
"id" serial PRIMARY KEY NOT NULL,
"project_unit_id" integer NOT NULL,
"bible_id" integer NOT NULL,
"bible_text_id" integer NOT NULL,
"pericope_set_id" integer NOT NULL,
"book_id" integer NOT NULL,
"chapter_number" integer NOT NULL,
"pericope_number" varchar(100) NOT NULL,
"suggested_text" varchar(300) NOT NULL,
"model_info" varchar(100),
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "ai_pericope_suggestion_usage" ADD CONSTRAINT "ai_pericope_suggestion_usage_suggestion_id_ai_pericope_suggestions_id_fk" FOREIGN KEY ("suggestion_id") REFERENCES "public"."ai_pericope_suggestions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ai_pericope_suggestion_usage" ADD CONSTRAINT "ai_pericope_suggestion_usage_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ai_pericope_suggestions" ADD CONSTRAINT "ai_pericope_suggestions_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ai_pericope_suggestions" ADD CONSTRAINT "ai_pericope_suggestions_bible_id_bibles_id_fk" FOREIGN KEY ("bible_id") REFERENCES "public"."bibles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ai_pericope_suggestions" ADD CONSTRAINT "ai_pericope_suggestions_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ai_pericope_suggestions" ADD CONSTRAINT "ai_pericope_suggestions_pericope_set_id_pericope_sets_id_fk" FOREIGN KEY ("pericope_set_id") REFERENCES "public"."pericope_sets"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ai_pericope_suggestions" ADD CONSTRAINT "ai_pericope_suggestions_book_id_books_id_fk" FOREIGN KEY ("book_id") REFERENCES "public"."books"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "uq_ai_pericope_usage_user" ON "ai_pericope_suggestion_usage" USING btree ("suggestion_id","user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "uq_ai_pericope_suggestion" ON "ai_pericope_suggestions" USING btree ("project_unit_id","bible_id","pericope_set_id","book_id","chapter_number","pericope_number");
Loading