Summary
Found by CodeAnt AI review while extracting the project-bootstrap effect into hooks/useProjectBootstrapEffect.ts (PR #530, fixing issue #527's race). Both are confirmed pre-existing — the extraction is behavior-preserving aside from adding the isInitialLoad guard; the original inline effect in App.tsx had the identical if (!isI18nReady || isPortalActive || !project) return; guard and identical repairProjectI18nFields call, verified via direct diff against origin/main. Neither gap was introduced by #530 or by v1.28.2.
Gap 1 — bootstrap effect is permanently blocked if the translation bundle fails to load
The effect's guard requires isI18nReady to be true. If the active locale bundle fails to fetch (offline, transient network failure), isI18nReady may never become true even though t() itself still works via English fallback (per useTranslation's own bootstrap/fallback design). Consequence: on an offline or transiently-failed first start, a blank project's raw-i18n-key fields never get repaired and a brand-new project never gets seeded — the effect just never runs.
Suggested direction (not implemented, needs its own design pass): allow the fallback-translation state to satisfy this gate (i.e., gate on "translations are usable," not literally "the network fetch for the active locale bundle succeeded"), or add a retry/timeout path so a failed bundle load doesn't block bootstrap forever.
Gap 2 — repair can silently overwrite a user's intentionally-cleared title/logline
repairProjectI18nFields treats any falsy (empty-string) title or logline as "needs repair" and replaces it with the localized initialProject.title/initialProject.logline default. This doesn't distinguish "this field was never set / holds a raw i18n key" (genuinely needs repair) from "the user deliberately cleared this field" (should stay empty, that's their choice). A returning user who intentionally blanks either field could have it silently replaced with the default on the next app start.
Suggested direction (not implemented, needs its own design pass): distinguish a raw-persisted-i18n-key value (e.g. literally "initialProject.title" stored as the title string — a real corruption case) from a genuinely-empty user-cleared value, and only auto-repair the former. Possibly restrict the repair path to newly-created projects specifically, if that's a state that can be tracked, rather than running on every app start for every project.
Gap 3 — startup repair dispatches multiple separate undo-history entries instead of one atomic update
Found by CodeAnt AI. repairProjectI18nFields's result is applied via up to three separate dispatch() calls (updateTitle, updateLogline, setManuscript), each landing as its own entry in project's redux-undo history (100-step history, per this repo's own architecture docs) instead of one atomic change. A user hitting Undo once after a startup repair only partially reverts it (e.g. undoes the seeded manuscript but leaves the repaired title/logline in place), which is confusing. Also pre-existing and byte-identical to the original App.tsx effect — verified via diff.
Suggested direction (not implemented, needs its own design pass): add a single reducer/action that atomically applies a ProjectI18nRepair payload (whichever of title/logline/manuscript are present) in one dispatch, replacing the three separate calls. resetProject is not a drop-in replacement — it also resets characters/worlds/binderNodes, which a repair must not touch.
Release relevance
Non-blocking, pre-existing on main before v1.28.2 and before #530. Not fixed here — #530 is scoped narrowly to closing the isPortalActive/isInitialLoad race (per explicit review-loop guidance to keep that fix minimal and not expand into a broader onboarding refactor). Tracked here for its own bounded design + fix pass.
Summary
Found by CodeAnt AI review while extracting the project-bootstrap effect into
hooks/useProjectBootstrapEffect.ts(PR #530, fixing issue #527's race). Both are confirmed pre-existing — the extraction is behavior-preserving aside from adding theisInitialLoadguard; the original inline effect inApp.tsxhad the identicalif (!isI18nReady || isPortalActive || !project) return;guard and identicalrepairProjectI18nFieldscall, verified via direct diff againstorigin/main. Neither gap was introduced by #530 or by v1.28.2.Gap 1 — bootstrap effect is permanently blocked if the translation bundle fails to load
The effect's guard requires
isI18nReadyto betrue. If the active locale bundle fails to fetch (offline, transient network failure),isI18nReadymay never becometrueeven thought()itself still works via English fallback (peruseTranslation's own bootstrap/fallback design). Consequence: on an offline or transiently-failed first start, a blank project's raw-i18n-key fields never get repaired and a brand-new project never gets seeded — the effect just never runs.Suggested direction (not implemented, needs its own design pass): allow the fallback-translation state to satisfy this gate (i.e., gate on "translations are usable," not literally "the network fetch for the active locale bundle succeeded"), or add a retry/timeout path so a failed bundle load doesn't block bootstrap forever.
Gap 2 — repair can silently overwrite a user's intentionally-cleared title/logline
repairProjectI18nFieldstreats any falsy (empty-string)titleorloglineas "needs repair" and replaces it with the localizedinitialProject.title/initialProject.loglinedefault. This doesn't distinguish "this field was never set / holds a raw i18n key" (genuinely needs repair) from "the user deliberately cleared this field" (should stay empty, that's their choice). A returning user who intentionally blanks either field could have it silently replaced with the default on the next app start.Suggested direction (not implemented, needs its own design pass): distinguish a raw-persisted-i18n-key value (e.g. literally
"initialProject.title"stored as the title string — a real corruption case) from a genuinely-empty user-cleared value, and only auto-repair the former. Possibly restrict the repair path to newly-created projects specifically, if that's a state that can be tracked, rather than running on every app start for every project.Gap 3 — startup repair dispatches multiple separate undo-history entries instead of one atomic update
Found by CodeAnt AI.
repairProjectI18nFields's result is applied via up to three separatedispatch()calls (updateTitle,updateLogline,setManuscript), each landing as its own entry inproject'sredux-undohistory (100-step history, per this repo's own architecture docs) instead of one atomic change. A user hitting Undo once after a startup repair only partially reverts it (e.g. undoes the seeded manuscript but leaves the repaired title/logline in place), which is confusing. Also pre-existing and byte-identical to the originalApp.tsxeffect — verified via diff.Suggested direction (not implemented, needs its own design pass): add a single reducer/action that atomically applies a
ProjectI18nRepairpayload (whichever oftitle/logline/manuscriptare present) in one dispatch, replacing the three separate calls.resetProjectis not a drop-in replacement — it also resets characters/worlds/binderNodes, which a repair must not touch.Release relevance
Non-blocking, pre-existing on
mainbefore v1.28.2 and before #530. Not fixed here — #530 is scoped narrowly to closing the isPortalActive/isInitialLoad race (per explicit review-loop guidance to keep that fix minimal and not expand into a broader onboarding refactor). Tracked here for its own bounded design + fix pass.