Skip to content

feat(config): publish env-sourced feature flags via GET /config/features (+ Repeated Word Check backend)#213

Merged
JEdward7777 merged 18 commits into
eten-tech-foundation:mainfrom
JEdward7777:jel-feature-flags-impl
Jul 7, 2026
Merged

feat(config): publish env-sourced feature flags via GET /config/features (+ Repeated Word Check backend)#213
JEdward7777 merged 18 commits into
eten-tech-foundation:mainfrom
JEdward7777:jel-feature-flags-impl

Conversation

@JEdward7777

@JEdward7777 JEdward7777 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What & why

Enables shipping AI-dependent front-end features hidden while fluent-ai is unhosted in dev/staging/prod, then flipping them on per environment (via env, no code redeploy). This is the fluent-api side: an env-sourced feature-flag map published at a new meta endpoint. The matching fluent-web consumer (useFeatureFlags hook + <FeatureGate>, diagnostics page) is a separate later PR.

This branch bundles the Repeated Word Check backend work (previously PR #203) plus the feature-flags enabler, because #203 was only being held back by the lack of a way to hide its UI. Keeping them together means #203's branch will auto-resolve as merged once this lands, rather than looking like a competing PR.

State: pending any change requests on the feature-flags proposal PR #211. The design was implemented ahead of formal proposal sign-off (the alternative was idling); if #211's review changes the design (endpoint shape, env-var naming, default derivation, etc.), those changes will be folded back into this branch before merge.

Endpoint

GET /config/features — unauthenticated, DB-less, sibling of /health. Returns the env-derived map of which optional features are on:

{ "features": { "repeatedWordCheck": true } }

It publishes state; it does not enforce it (publishing is decoupled from the AI request path).

Design highlights

  • One flat boolean env var per flag under an EN_FEATURE_* prefix, each declared explicitly in the Zod EnvSchema (a fixed z.object strips unknown keys, so the schema is the authoritative flag catalog).
  • EN_FEATURE_REPEATED_WORD_CHECK defaults OFF unless FLUENT_AI_URL + FLUENT_AI_KEY are wired — so forgetting the flag in an AI-less environment yields the safe (hidden) answer.
  • Parsed with a local envBool() (accepts true/false, 1/0, yes/no, on/off), not z.coerce.boolean() (which would read the string "false" as true and invert the safe default). Note: z.stringbool() — floated in the proposal — is only available on the zod/v4 subpath, not the classic z bound by @hono/zod-openapi, hence the local parser.
  • A typed FLAGS registry ties each env var to its camelCase wire key and its unset default; a flag lives in three synced places (env schema / registry / OpenAPI schema), kept honest by a compile-time satisfies and a runtime three-way drift test (both directions).

Verification

  • All gates green: lint / format:check / typecheck / test / build (129 tests).
  • End-to-end via the freshly-built platform container: curl localhost:9999/config/features{"features":{"repeatedWordCheck":true}} with AI wired; explicit EN_FEATURE_REPEATED_WORD_CHECK=false/offfalse.
  • Full fresh platform bring-up (freshupdb:init) confirmed migrations apply cleanly from scratch: the renumbered 0013_add_user_settings and main's 0012_add_pericope_sets coexist; no phantom-table drift.

Notes for reviewers

  • Includes the feature-flags proposal docs (docs/proposals/feature-flags/) also under review in docs(proposals): feature flags to ship AI-dependent UI hidden (config/features) #211.
  • Includes b011cde fix(db): correct 0012 pericope snapshot to match schema — a main-owned snapshot defect (phantom recordings table + a wrong index-uniqueness flag) that otherwise produces spurious drizzle-kit generate drift. Confirmed safe to remove with Kasey.

Relates to #211 (proposal) and supersedes #203 (checks-api impl).

Summary by CodeRabbit

  • New Features
    • Added a published, login-gated feature-flags endpoint (GET /config/features).
    • Added per-user global settings API (GET/PUT /self/settings).
    • Included target language codes in chapter-assignment progress responses.
    • Added an extra pericope-sets seeding step during container startup.
    • Added English to the seeded language list.
  • Bug Fixes
    • Improved startup resilience by retrying database bootstrap until the server is ready.
    • Improved handling of legacy/malformed settings data and validation (returns 422 on bad input).
  • Documentation
    • Added feature-flag rollout proposal and review summary documents.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a feature-flag config endpoint and docs, a new self-settings API with storage and migration support, chapter-assignment language-code propagation, and several seed and bootstrap updates.

Changes

Integrated API and schema updates

Layer / File(s) Summary
Feature flag schema and registry
src/env.ts, src/lib/features.ts, src/lib/features.test.ts
Adds strict env boolean parsing, the repeated-word feature flag, the published feature registry, and drift-guard tests.
Feature flag endpoint and docs
src/routes/config.route.ts, src/routes/config.route.test.ts, src/routes/doc.route.test.ts, src/app.ts, .env.example, docs/proposals/feature-flags/*
Registers /config/features, wires it into the app, adds route and doc tests, and documents the rollout.
Self settings schema and migration
src/db/schema.ts, src/db/schema.editor-state-extension.test.ts, src/db/migrations/0013_add_user_settings.sql, src/db/migrations/meta/*
Adds the user_settings table, schema transforms, compatibility tests, and migration metadata.
Self settings repository and types
src/domains/self/settings/self-settings.repository.ts, src/domains/self/settings/self-settings.types.ts
Adds repository read/upsert functions and the request/response types for settings.
Self settings service and route
src/domains/self/settings/self-settings.service.ts, src/domains/self/settings/self-settings.route.ts, src/domains/self/settings/self-settings.route.test.ts, src/app.ts
Maps repository rows to responses, validates writes, registers authenticated GET/PUT /self/settings, and adds route coverage.
Target language code in assignment data
src/domains/chapter-assignments/*, src/domains/projects/chapter-assignments/*, src/domains/users/chapter-assignments/*
Adds targetLangCode/targetLanguageCode through repository queries, schemas, services, and tests.
Seed and bootstrap updates
docker-entrypoint.sh, src/db/seeds/data/languages.json, src/db/seeds/dev-users.ts, src/db/migrations/meta/0012_snapshot.json, src/db/scripts/bootstrap.ts
Adds the pericope-sets seed step, an English language row, a second dev user, snapshot metadata updates, and DB readiness retries.

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

Possibly related PRs

Suggested reviewers: kaseywright, Joel-Joseph-George

🚥 Pre-merge checks | ✅ 4
✅ 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 describes the main change: adding env-sourced feature flags exposed through GET /config/features, with the Repeated Word Check backend as a secondary detail.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@JEdward7777
JEdward7777 force-pushed the jel-feature-flags-impl branch from 281bb2f to 756bfe1 Compare July 3, 2026 16:23
@JEdward7777

Copy link
Copy Markdown
Contributor Author

Slipped in a small unrelated fix while smoke-testing locally (commit 3350c5d): seed pericope sets in docker-entrypoint.sh.

The container seeds the DB via docker-entrypoint.sh, which inlines its own seed list and stopped after bible-texts — it never ran the pericope-sets seed that src/db/scripts/setup.ts includes as its final step ([10/10]). As a result, every container-based DB (re)generation left pericope_sets empty, so the Create Project page's Pericope Set dropdown had nothing to select and a project couldn't be created.

The added step runs after bible-texts (it depends on books, already seeded earlier), is idempotent (upsert set by name + replace verses, matching the other entrypoint seeds), and its data files (data/FCBH-All-Books.json, data/FIA-All-Books.json) are already baked into the image via COPY . .. Verified locally: entrypoint passes bash -n, the seed populates FCBH (16,910 verses) + FIA (16,870 verses), and GET /pericope-sets returns both.

Happy to split this into its own PR if you'd prefer it not ride along with the feature-flags change.

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/domains/projects/chapter-assignments/project-chapter-assignments.service.ts (1)

36-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate mapping logic across two functions.

Both mapping blocks are near-identical and now had to be updated in two places for one field. Consider extracting a shared mapper (e.g. toChapterAssignmentProgressResponse(info)) to avoid this kind of duplicated maintenance burden going forward.

♻️ Suggested consolidation
+function toChapterAssignmentProgressResponse(info: ChapterAssignmentProgressInfo) {
+  return {
+    assignmentId: info.assignmentId,
+    projectUnitId: info.projectUnitId,
+    status: info.status,
+    bookNameEng: info.bookNameEng,
+    chapterNumber: info.chapterNumber,
+    bibleId: info.bibleId,
+    bookId: info.bookId,
+    bookCode: info.bookCode,
+    sourceLangCode: info.sourceLangCode ?? '',
+    targetLanguageCode: info.targetLangCode ?? '',
+    assignedUser: info.assignedUserId
+      ? { id: info.assignedUserId, displayName: info.assignedUserDisplayName ?? '' }
+      : null,
+    peerChecker: info.peerCheckerId
+      ? { id: info.peerCheckerId, displayName: info.peerCheckerDisplayName ?? '' }
+      : null,
+    totalVerses: info.totalVerses,
+    completedVerses: info.completedVerses,
+    createdAt: info.createdAt,
+    updatedAt: info.updatedAt,
+    submittedTime: info.submittedTime,
+  };
+}

Then replace both .map((info) => ({...})) blocks with .map(toChapterAssignmentProgressResponse).

Also applies to: 175-197

🤖 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/domains/projects/chapter-assignments/project-chapter-assignments.service.ts`
around lines 36 - 58, The chapter assignment progress response mapping is
duplicated in two places, causing repeated maintenance when fields change.
Extract the shared object transformation into a single helper such as
toChapterAssignmentProgressResponse(info) in
project-chapter-assignments.service.ts, using the same field mapping for
assignmentId, assignedUser, peerChecker, and the rest, then replace both inline
.map((info) => ({ ... })) blocks with .map(toChapterAssignmentProgressResponse).
src/domains/projects/chapter-assignments/project-chapter-assignments.types.ts (1)

16-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent field naming: sourceLangCode vs targetLanguageCode.

Same response schema abbreviates one language code field but spells out the other. Minor readability nit; not blocking since it's already part of the public contract mirrored in users-chapter-assignments.types.ts.

🤖 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/domains/projects/chapter-assignments/project-chapter-assignments.types.ts`
around lines 16 - 27, The chapter assignment response schema has inconsistent
language-code field naming between sourceLangCode and targetLanguageCode. Update
the chapterAssignmentProgressResponseSchema in
project-chapter-assignments.types.ts to use a consistent naming convention for
both fields, and keep the mirrored type in users-chapter-assignments.types.ts
aligned so the public contract remains unchanged.
🤖 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 `@docs/proposals/feature-flags/feature-flags-suggestion.md`:
- Around line 105-112: The example in the feature-flags proposal uses
z.stringbool(), but that API requires Zod 4 while this repo is pinned to
zod@3.25.76. Update the guidance near EN_FEATURE_REPEATED_WORD_CHECK to
explicitly call out the Zod 4 minimum or switch the snippet/reference to zod/v4;
alternatively, replace it with a Zod 3-compatible boolean parsing approach so
the example matches the supported version.

In `@src/db/schema.editor-state-extension.test.ts`:
- Around line 56-70: The current tests only cover the tolerant
userSettingsSchema, so add a case for userSettingsWriteSchema to verify it
rejects malformed input instead of collapsing to {}. Extend the
userSettingsSchema test block in schema.editor-state-extension.test.ts with a
focused assertion on userSettingsWriteSchema, using its existing export name to
check that invalid shapes like a bad checkIgnoredWordPairs value throw or fail
validation as intended.

In `@src/domains/self/settings/self-settings.service.ts`:
- Around line 34-35: The schema-validation branch in self-settings.service
should not return INVALID_REFERENCE when safeParse fails. Update the handling
around userSettingsWriteSchema.safeParse in the self settings flow to return
VALIDATION_ERROR instead, so request body shape failures map to the correct
422-style error code. Use the existing ErrorCode enum and the parsed.success
check to locate the change.

In `@src/env.ts`:
- Around line 26-36: The envBool parser currently rejects empty strings, so
EN_FEATURE_REPEATED_WORD_CHECK= from dotenv still fails even with
envBool().optional(). Update envBool in src/env.ts to treat a blank or
whitespace-only value as unset/undefined before applying the truthy/falsy
checks, so EnvSchema.safeParse(process.env) succeeds when copying .env.example
verbatim. Keep the change localized to envBool and its optional handling.

---

Outside diff comments:
In
`@src/domains/projects/chapter-assignments/project-chapter-assignments.service.ts`:
- Around line 36-58: The chapter assignment progress response mapping is
duplicated in two places, causing repeated maintenance when fields change.
Extract the shared object transformation into a single helper such as
toChapterAssignmentProgressResponse(info) in
project-chapter-assignments.service.ts, using the same field mapping for
assignmentId, assignedUser, peerChecker, and the rest, then replace both inline
.map((info) => ({ ... })) blocks with .map(toChapterAssignmentProgressResponse).

In
`@src/domains/projects/chapter-assignments/project-chapter-assignments.types.ts`:
- Around line 16-27: The chapter assignment response schema has inconsistent
language-code field naming between sourceLangCode and targetLanguageCode. Update
the chapterAssignmentProgressResponseSchema in
project-chapter-assignments.types.ts to use a consistent naming convention for
both fields, and keep the mirrored type in users-chapter-assignments.types.ts
aligned so the public contract remains unchanged.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 56bbf19e-aa0c-4196-99eb-a8d39fffb71e

📥 Commits

Reviewing files that changed from the base of the PR and between cb105b7 and 3350c5d.

📒 Files selected for processing (30)
  • .env.example
  • docker-entrypoint.sh
  • docs/proposals/feature-flags/feature-flags-suggestion.md
  • docs/proposals/feature-flags/feature-flags-summary.md
  • src/app.ts
  • src/db/migrations/0013_add_user_settings.sql
  • src/db/migrations/meta/0012_snapshot.json
  • src/db/migrations/meta/0013_snapshot.json
  • src/db/migrations/meta/_journal.json
  • src/db/schema.editor-state-extension.test.ts
  • src/db/schema.ts
  • src/db/seeds/data/languages.json
  • src/db/seeds/dev-users.ts
  • src/domains/chapter-assignments/chapter-assignments.repository.ts
  • src/domains/chapter-assignments/chapter-assignments.types.ts
  • src/domains/projects/chapter-assignments/project-chapter-assignments.service.ts
  • src/domains/projects/chapter-assignments/project-chapter-assignments.types.ts
  • src/domains/self/settings/self-settings.repository.ts
  • src/domains/self/settings/self-settings.route.test.ts
  • src/domains/self/settings/self-settings.route.ts
  • src/domains/self/settings/self-settings.service.ts
  • src/domains/self/settings/self-settings.types.ts
  • src/domains/users/chapter-assignments/users-chapter-assignments.service.test.ts
  • src/domains/users/chapter-assignments/users-chapter-assignments.service.ts
  • src/domains/users/chapter-assignments/users-chapter-assignments.types.ts
  • src/env.ts
  • src/lib/features.test.ts
  • src/lib/features.ts
  • src/routes/config.route.test.ts
  • src/routes/config.route.ts

Comment thread docs/proposals/feature-flags/feature-flags-suggestion.md Outdated
Comment thread src/db/schema.editor-state-extension.test.ts
Comment thread src/domains/self/settings/self-settings.service.ts Outdated
Comment thread src/env.ts
@JEdward7777

Copy link
Copy Markdown
Contributor Author

Front-end counterpart: eten-tech-foundation/fluent-web#337eten-tech-foundation/fluent-web#337. It consumes the GET /config/features endpoint published by this PR to hide the Repeated Word Check where fluent-ai isn't wired (failing closed), and bundles the rebased Repeated Word Check UI. The two are meant to land together.

JEdward7777 pushed a commit to JEdward7777/fluent-api that referenced this pull request Jul 5, 2026
- rename wire field targetLanguageCode -> targetLangCode (match sourceLangCode)
- env: treat blank EN_FEATURE_* as unset so empty .env.example lines don't fail boot
- self-settings: return VALIDATION_ERROR (422) on write-schema violation
- docs: update feature-flags snippet from z.stringbool() to shipped envBool()
- tests: assert userSettingsWriteSchema rejects malformed writes
- refactor: extract shared toChapterAssignmentProgressResponse() mapper
@JEdward7777

Copy link
Copy Markdown
Contributor Author

Pushed 18ca571 addressing the CodeRabbit pass:

  • env blank-value boot failure (Major): envBool() now treats blank/whitespace-only as unset.
  • self-settings error code (Minor): write-schema violations return VALIDATION_ERROR (422); route test updated.
  • doc z.stringbool() (Minor): snippet updated to the shipped envBool() with a Zod-3-vs-4 note.
  • write-schema test coverage (Trivial): added 5 rejection cases.
  • duplicate mapping block (outside-diff): extracted a shared toChapterAssignmentProgressResponse() mapper.
  • sourceLangCode vs targetLanguageCode inconsistency (outside-diff): rather than just annotate it, the wire field was renamed targetLanguageCode -> targetLangCode to match sourceLangCode. This is a coordinated change with the paired web PR #337 (same rename on the consumer side); both are unmerged so the new key has not shipped to any consumer. The two PRs are meant to land together.

Gates green (141 tests).

Joshua Lansford and others added 16 commits July 7, 2026 08:19
Add a docs-only proposal for a lightweight, env-sourced feature-flag
mechanism to gate AI-dependent front-end features (starting with the
Repeated Word Check) so their PRs can merge and ship hidden while
fluent-ai is unhosted.

- EN_FEATURE_* env vars, declared in the Zod schema, swept into a map
- new unauthenticated meta route GET /config/features (sibling of /health)
- fluent-web consumes via a useFeatureFlags hook + FeatureGate (fail-closed)
- unlinked, login-gated diagnostics page for the flag view
- publishing only; no server-side enforcement (D5)

Includes -suggestion.md (design, D1-D8, Q1-Q4) and -summary.md.
The 0012_add_pericope_sets migration snapshot was out of sync with its
own schema/SQL:

- Removed a phantom `public.recordings` table that was never part of the
  migration (an accidental leak from experimental mobile DB work; absent
  from Dev/PROD and from schema.ts).
- Fixed `idx_pericope_verses_set_book_chapter_verse` to `isUnique: true`
  to match the CREATE UNIQUE INDEX in 0012_add_pericope_sets.sql.

No SQL or schema changes; snapshot-only correction so drizzle-kit
generate diffs cleanly against the actual schema.
Phase 1 of the repeated-word-check persistence backend.

- db/schema.ts: add user_settings table (user_id PK, settings jsonb,
  timestamps, FK to users ON DELETE CASCADE) with select/insert schemas;
  add userSettingsSchema (read, .catch({})) + strict userSettingsWriteSchema
  so malformed PUT bodies 400 instead of being silently coerced; extend
  editorStateResourcesSchema with optional activeLeftTab + checkOccurrenceRules.
- migration 0013_add_user_settings.sql (+meta). Renumbered from 0012 during
  rebase onto jel-feature-flags-proposal to avoid colliding with the pericope
  0012 migration that landed on main via eten-tech-foundation#205.
- domains/self/settings quartet: GET/PUT /self/settings, authenticateUser only,
  full-replace upsert, strict-validate -> 400, read-tolerant.
- register self domain in app.ts.
- tests: self-settings.route.test.ts (8) + schema.editor-state-extension.test.ts (8).

Relates to eten-tech-foundation#172, #277, #278
…ts (phase-04 BUG eten-tech-foundation#2)

The repeated-words check sends the target language's ISO 639-3 code to
greek-room as lang_code (greek-room keys its legitimate-duplicate
whitelist on the code, not the display name). The user chapter-assignment
response previously exposed only the human display name (targetLanguage),
so the web client had no code to send.

Plumb targetLangCode (targetLang.langCodeIso6393) through
findAssignmentsProgress -> ChapterAssignmentProgressInfo -> toResponse as
targetLanguageCode on UserChapterAssignmentResponse (schema + types).
Add a toResponse regression test. Found during the Phase 4 manual smoke.
Enables exercising the full project assign -> draft flow out-of-the-box:
- dev-users seeder now provisions a second translator (translator2,
  t2@fluent.local), so a project meets the >=2-translator requirement for
  chapter assignment (distinct Drafter + Peer Checker). Idempotent; honours
  SEED_TRANSLATOR2_EMAIL/SEED_TRANSLATOR2_PASSWORD overrides.
- languages seed now includes English (eng), so a draftable English target
  language is available without manual DB insertion.

Both seeders remain idempotent (existing rows skipped on re-run).
The repeated-words check sends greek-room a `lang_code` keyed on the ISO
639-3 target language code. greek-room's legitimate-duplicate whitelist
(e.g. "truly truly" for "eng") only matches when the code is correct.

The PM "open chapter" path builds its ProjectItem from
GET /projects/{id}/chapter-assignments/progress, but that endpoint never
serialized the target language code: the repository fetched targetLangCode
yet both service mappers (getChapterAssignmentProgressByProject and
assignSelectedChapters) dropped it, and the response schema didn't declare
it. As a result the PM-path check sent lang_code "<unknown>" and legitimate
duplicates were flagged.

Add `targetLanguageCode` to chapterAssignmentProgressResponseSchema and
populate it in both mappers (falling back to '' when the FK doesn't resolve,
matching the existing sourceLangCode handling).

Includes TEMP [RW-DIAG] logging used to pin down the bug; removed in the
following commit.
Removes the diagnostic logging added in the previous commit (inbound
repeated-words request log, fluent-ai response/error log, and the
toResponse targetLangCode log) now that the root cause is fixed and
verified.
- normalize stored settings JSONB through the tolerant read schema in
  toResponse (null-guarded) so a malformed/legacy blob degrades to {}
  instead of leaking through GET; add two read-path tests
- declare 403 FORBIDDEN on both GET and PUT route contracts (reachable
  via authenticateUser's inactive-account branch); add a 403 test to each
- tighten the OpenAPI response schema settings field to userSettingsSchema
  so the spec advertises the real keys/enums (response provably conforms
  after the read-path normalization); request schema left generic by design
Implements the fluent-api half of the feature-flags proposal (docs in
docs/proposals/feature-flags): an unauthenticated, DB-less meta route that
publishes which optional, AI-dependent features are enabled per environment,
so fluent-web can ship AI UI hidden while fluent-ai is unhosted.

- env.ts: declare EN_FEATURE_REPEATED_WORD_CHECK in the Zod EnvSchema via a
  local envBool() parser (not z.coerce.boolean(), which would read the string
  "false" as true and invert the safe-off default). Optional so an unset flag
  can derive its default.
- lib/features.ts: typed FLAGS registry tying each env var to its camelCase
  wire key + unset default (repeatedWordCheck defaults to whether FLUENT_AI_URL
  + FLUENT_AI_KEY are wired); buildFeatures() derives the published map; named
  OpenAPI featuresSchema guarded by a compile-time `satisfies`.
- routes/config.route.ts: GET /config/features -> { features: { ... } },
  sibling of /health; publishes state, does not enforce it (no gating of the
  AI request path).
- app.ts: register the route. .env.example: document the flag.
- Tests: buildFeatures behavior + a three-way drift guard (env schema / FLAGS /
  OpenAPI schema kept in sync, both directions) + route behavior.

Relates to the feature-flags proposal (fluent-api eten-tech-foundation#211).
The container seeds the DB via docker-entrypoint.sh, which inlined its own
seed list and stopped after bible-texts, never running the pericope-sets
seed that setup.ts includes as its final step. As a result every
container-based DB (re)generation left pericope_sets empty, so the Create
Project page's Pericope Set dropdown had nothing to select.

Add the pericope-sets seed after bible-texts (it depends on books, already
seeded earlier). The seed is idempotent (upsert set by name + replace
verses) and its data files are baked into the image, matching the other
entrypoint seeds.
- rename wire field targetLanguageCode -> targetLangCode (match sourceLangCode)
- env: treat blank EN_FEATURE_* as unset so empty .env.example lines don't fail boot
- self-settings: return VALIDATION_ERROR (422) on write-schema violation
- docs: update feature-flags snippet from z.stringbool() to shipped envBool()
- tests: assert userSettingsWriteSchema rejects malformed writes
- refactor: extract shared toChapterAssignmentProgressResponse() mapper
… spec

The OpenAPI document walk over every registered schema currently 500s
('Unknown zod object type') and nothing at the unit/type level catches it,
so it only surfaced at runtime. This test renders the real assembled
document via GET /doc and asserts a 200, so that class of break fails CI.

Currently RED (reproduces the live 500); the fix follows in the next commit.
…urface

GET /doc 500'd with 'Unknown zod object type' because the /self/settings
response schema embedded userSettingsSchema (= userSettingsObjectSchema.catch({})).
The .catch wrapper is a ZodCatch node that @asteasolutions/zod-to-openapi
(under @hono/zod-openapi) has no renderer for, so building the OpenAPI
document threw and took the whole /doc endpoint down.

Export the plain userSettingsObjectSchema and embed THAT in
userSettingsResponseSchema. The .catch only affects behaviour on invalid
input, so it adds/removes no fields: the documented shape and the response
bytes are unchanged. The fail-soft W8 read tolerance stays where it belongs
(the service read path, toResponse -> userSettingsSchema.parse), and the
strict write schema is untouched. No request/response contract change.

Turns the doc.route regression test green.
@JEdward7777
JEdward7777 force-pushed the jel-feature-flags-impl branch from 18ca571 to 1b803bd Compare July 7, 2026 13:16
kaseywright
kaseywright previously approved these changes Jul 7, 2026
Comment thread src/lib/features.ts
export const FLAGS = {
// Repeated Word Check — the one AI-dependent feature today. Defaults to
// whether AI is wired when EN_FEATURE_REPEATED_WORD_CHECK is unset (D2/§4.2).
repeatedWordCheck: { env: 'EN_FEATURE_REPEATED_WORD_CHECK', default: aiIsWired },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is an acceptable default for all AI features. In the proposal doc, I suggested that this not be the case. However, After seeing the implementation, I see the merit in this default. Let's keep this and make user it is standard for any feature flag.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed: the AI-wiring-derived unset-default stays, and we are treating it as the standard convention for feature flags going forward — an unset flag resolves to a safe-by-context default via its DefaultResolver, rather than a blanket static false. The FLAGS registry in lib/features.ts is built for exactly that (each flag ties its env var to a default resolver), so future AI-dependent flags will follow the same aiIsWired pattern. The proposal + summary docs (folded in f547988) now record Q4 as resolved this way.

…ation#211 Q1)

Per kaseywright's review on proposal eten-tech-foundation#211 (Q1), GET /config/features is now
login-gated via authenticateUser (no role required), matching the pattern used
by self-settings and pericopes-list. The endpoint still only publishes flags
(does not enforce them, D5 unchanged); only the read now requires a session.
Reversible later if we decide it should be public.

- config.route.ts: add authenticateUser middleware + UNAUTHORIZED (401) response,
  reword description (no longer 'unauthenticated like /health').
- config.route.test.ts: rewrite against the mocked-auth pattern — null session
  => 401; valid session => 200 with the features map.
kaseywright
kaseywright previously approved these changes Jul 7, 2026
@JEdward7777

Copy link
Copy Markdown
Contributor Author

The Q1 auth-gate from the proposal review (#211) is now in on this branch (0812a39): GET /config/features requires a session. With the read behind auth, the feature-gating no longer blocks this from going to production, so this is ready to merge. (The prior approval was on the pre-auth-gate revision, so a fresh look at 0812a39 is welcome.)

@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: 7

🤖 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 `@docs/proposals/feature-flags/feature-flags-suggestion.md`:
- Line 3: Update the proposal document so it matches the shipped auth behavior
of the feature-flags route: the current “Draft for review. Not yet implemented.”
status and Q1 open-question text are stale. In the feature-flags suggestion doc,
revise the Status line and the Q1/auth sections (including the mermaid diagram
and endpoint spec references) to state that GET /config/features is
authenticated via authenticateUser and that the auth decision is already
resolved, or point to the canonical decision record if that is where it now
lives.

In `@docs/proposals/feature-flags/feature-flags-summary.md`:
- Line 3: The feature-flags summary is stale and contradicts the shipped auth
behavior. Update the summary and related sections in the proposal so they match
the implemented route guarded by authenticateUser and reflect that auth is no
longer an open question; also revise the reviewer status text and any
“unauthenticated” wording to align with the confirmed decision. Use the
feature-flags summary doc headings and the route/auth references as the places
to sync the narrative with the code.

In `@src/db/schema.ts`:
- Around line 511-514: The comment on userSettingsWriteSchema is stale because
malformed write-body validation failures are not surfaced as 400; they map to
VALIDATION_ERROR and end up as 422. Update the inline comment near
userSettingsWriteSchema to match the behavior described in
self-settings.service.ts and safeParse handling, keeping the note aligned with
the actual status code returned for schema validation failures.

In `@src/domains/self/settings/self-settings.route.ts`:
- Around line 45-51: The GET handler in getSelfSettingsRoute hardcodes
INTERNAL_SERVER_ERROR instead of using the shared status-mapping pattern. Update
the error branch to derive the response status from result.error via
getHttpStatus, matching the PUT handler’s approach in self-settings.route and
keeping status handling consistent if getSettings adds new failure modes.
- Around line 65-95: The OpenAPI contract for saveSelfSettingsRoute is missing
the 422 validation-error response, which is why the return path in
server.openapi uses an unsafe as never cast. Update the responses map in
self-settings.route.ts to declare the validation failure status alongside
BAD_REQUEST, and then remove the cast by letting getHttpStatus(result.error)
flow through with the proper typed response handling. Keep the fix centered on
saveSelfSettingsRoute and the result/error handling branch so the spec matches
the actual selfSettingsService.upsertSettings behavior.

In `@src/lib/features.ts`:
- Around line 50-55: The aiIsWired default resolver in features.ts is
effectively unreachable at runtime because FLUENT_AI_URL and FLUENT_AI_KEY are
already required by the env parsing path. Update the boot/default logic used by
repeatedWordCheck so omitted AI-dependent flags do not resolve to true by
accident: either relax the env requirements in src/env.ts so the “AI not wired”
branch can occur, or change aiIsWired (and any related default resolver wiring)
to default to false when the AI vars are not intended to control the flag.
- Around line 90-99: Tighten the `FlagDefinition.env` typing used by
`buildFeatures` so only boolean-parsed env keys can be referenced. Update the
`FlagDefinition` type (and any related flag definitions in `FLAGS`) to constrain
`env` to keys of `Env` whose values are `boolean | undefined`, then remove the
unsafe cast in `buildFeatures` and let the compiler enforce that `e[def.env]` is
boolean-compatible.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f0a92cd0-6dbb-4074-8e31-f57683e8d7ce

📥 Commits

Reviewing files that changed from the base of the PR and between 18ca571 and 0812a39.

📒 Files selected for processing (31)
  • .env.example
  • docker-entrypoint.sh
  • docs/proposals/feature-flags/feature-flags-suggestion.md
  • docs/proposals/feature-flags/feature-flags-summary.md
  • src/app.ts
  • src/db/migrations/0013_add_user_settings.sql
  • src/db/migrations/meta/0012_snapshot.json
  • src/db/migrations/meta/0013_snapshot.json
  • src/db/migrations/meta/_journal.json
  • src/db/schema.editor-state-extension.test.ts
  • src/db/schema.ts
  • src/db/seeds/data/languages.json
  • src/db/seeds/dev-users.ts
  • src/domains/chapter-assignments/chapter-assignments.repository.ts
  • src/domains/chapter-assignments/chapter-assignments.types.ts
  • src/domains/projects/chapter-assignments/project-chapter-assignments.service.ts
  • src/domains/projects/chapter-assignments/project-chapter-assignments.types.ts
  • src/domains/self/settings/self-settings.repository.ts
  • src/domains/self/settings/self-settings.route.test.ts
  • src/domains/self/settings/self-settings.route.ts
  • src/domains/self/settings/self-settings.service.ts
  • src/domains/self/settings/self-settings.types.ts
  • src/domains/users/chapter-assignments/users-chapter-assignments.service.test.ts
  • src/domains/users/chapter-assignments/users-chapter-assignments.service.ts
  • src/domains/users/chapter-assignments/users-chapter-assignments.types.ts
  • src/env.ts
  • src/lib/features.test.ts
  • src/lib/features.ts
  • src/routes/config.route.test.ts
  • src/routes/config.route.ts
  • src/routes/doc.route.test.ts

Comment thread docs/proposals/feature-flags/feature-flags-suggestion.md Outdated
Comment thread docs/proposals/feature-flags/feature-flags-summary.md Outdated
Comment thread src/db/schema.ts
Comment thread src/domains/self/settings/self-settings.route.ts
Comment thread src/domains/self/settings/self-settings.route.ts
Comment thread src/lib/features.ts
Comment thread src/lib/features.ts
…+ harden db bootstrap

CodeRabbit re-review (auth-gate follow-up):
- self-settings.route: add 422 UNPROCESSABLE_ENTITY to PUT OpenAPI responses so
  the published contract matches upsertSettings validation behavior (R5)
- self-settings.route: GET error branch derives status via getHttpStatus(result.error)
  instead of hardcoding 500, consistent with PUT (R4)
- schema.ts: correct stale userSettingsWriteSchema comment (400 -> 422) (R3)
- lib/features: tighten FlagDefinition.env to boolean-valued EN_FEATURE_* keys and
  drop the boolean cast in buildFeatures (R7)
- docs/proposals/feature-flags: fold suggestion + summary to the shipped
  authenticated contract and Q1-Q4 resolutions (R1/R2)

Stack durability (unblocks local smoke testing):
- db bootstrap: poll for real query readiness before issuing DDL. The compose
  healthcheck (pg_isready) reports ready as soon as Postgres accepts connections,
  but during startup/WAL-recovery it rejects queries with 57P03; the entrypoint's
  set -e then killed the container (and its dependents) on restart. Retry transient
  startup/connection errors with bounded backoff (~60s ceiling); real errors still
  fail fast.

@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

🤖 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/db/scripts/bootstrap.ts`:
- Around line 91-111: The retry loop in waitForDatabase currently uses a fixed
2s delay, which can cause multiple replicas to synchronize and hammer Postgres
in lockstep. Update the retry delay logic in waitForDatabase to use exponential
backoff with jitter while keeping the existing transient-error handling and
maxAttempts behavior intact, and continue logging the attempt count and delay so
startup diagnostics remain clear.
🪄 Autofix (Beta)

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 32bea658-b8a6-4f3e-8b68-e63ab96566c2

📥 Commits

Reviewing files that changed from the base of the PR and between 0812a39 and f547988.

📒 Files selected for processing (6)
  • docs/proposals/feature-flags/feature-flags-suggestion.md
  • docs/proposals/feature-flags/feature-flags-summary.md
  • src/db/schema.ts
  • src/db/scripts/bootstrap.ts
  • src/domains/self/settings/self-settings.route.ts
  • src/lib/features.ts

Comment thread src/db/scripts/bootstrap.ts
@JEdward7777
JEdward7777 merged commit 47d5fb8 into eten-tech-foundation:main Jul 7, 2026
2 checks passed
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.

2 participants