youssefreda9 commited on
Commit
9786f39
·
1 Parent(s): 1519f71

Fix punctuation alignment and hallucinations, add punctuation safety rules, add kana/inna grammar rules

Browse files
BAYAN_COMPLETE_AUDIT.md CHANGED
@@ -1,115 +1,366 @@
1
  # BAYAN — Complete Product, Codebase & Extension Deep Audit
2
 
3
- ## 1. Current System Overview
 
4
 
5
- Bayan is a full-featured Arabic AI writing assistant deployed across two primary surfaces: a web application and a Chrome extension.
6
- The core architecture is built around a centralized Flask API hosted on Hugging Face Spaces (`bayan10-bayan-api.hf.space`), which serves NLP models for spelling, grammar, punctuation, summarization, dialect conversion, and Quranic validation.
7
 
8
- **Website Architecture:**
9
- - **Frontend:** Vanilla JS, HTML, CSS (Tailwind via CDN). Uses a custom `contenteditable` editor engine.
10
- - **Backend:** Flask API handling model inference.
11
- - **Database/Auth:** Supabase for cloud persistence, document sync, and authentication (Guest & Google).
12
 
13
- **Extension Architecture:**
14
- - **Framework:** Manifest V3.
15
- - **Components:** Background Service Worker (`background.js`), Content Scripts (`content-inline.js`), Sidepanel, and Popup.
16
- - **Communication:** Background worker acts as the single network boundary to the HF API.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  ---
19
 
20
  ## 2. Feature Inventory
21
 
22
- | Feature | Website | Extension | Status | Required Action |
23
- |---------|---------|-----------|--------|-----------------|
24
- | **Spelling/Grammar/Punctuation** | Exists | Exists | Parity | None |
25
- | **Summarization** | ✅ Exists | ✅ Exists | Parity | None |
26
- | **AutoComplete** | ✅ Exists | Missing | Gap | Implement inline text prediction in `content-inline.js` |
27
- | **Dialect Conversion** | ✅ Exists | Missing | Gap | Add to Extension Sidepanel |
28
- | **Quran Validation** | ✅ Exists | Missing | Gap | Add to Extension Sidepanel |
29
- | **Document Cloud Save** | ✅ Exists | Missing | Gap | Integrate Supabase into Extension |
30
- | **Authentication** | ✅ Exists | Missing | Gap | Implement shared auth state via cookies/Supabase |
31
- | **Settings Sync** | ✅ Exists | Missing | Gap | Share user preferences via Supabase |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  ---
34
 
35
  ## 3. Website vs Extension Comparison
36
 
37
- ### Authentication
38
- - **Website:** Supports Guest login and Google login via Supabase. State is maintained via `auth.js`.
39
- - **Extension:** Uses isolated `chrome.storage.local/session`. **Does not share sessions** with the website.
40
- - **Priority:** **High**. Users cannot access their cloud documents or synced preferences from the extension.
41
 
42
- ### AI Features
43
- - **Website:** Full suite (Analyze, Summarize, AutoComplete, Dialect, Quran).
44
- - **Extension:** Only uses `/api/analyze` (Correct) and `/api/summarize`. Missing AutoComplete, Dialect, and Quran validation.
45
- - **Priority:** **Medium**. Core features are present, but advanced features are missing.
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  ### Documents
48
- - **Website:** Full document management (Save, Load, Export PDF/DOCX, Sync Queue).
49
- - **Extension:** Cannot save, load, or access document history.
50
- - **Priority:** **High**. Destroys the omnichannel UX.
 
 
 
 
 
51
 
52
  ---
53
 
54
- ## 4. Missing Features & Gaps
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- 1. **Shared Authentication:** The extension operates anonymously. Needs Supabase integration in `background.js` to share JWTs.
57
- 2. **Missing Editor Tools in Extension:** "Dialect" and "Quran" tabs are missing from the sidepanel.
58
- 3. **AutoComplete in Extension:** The website's ghost-text autocomplete is highly valuable but entirely absent from textareas on the web via the extension.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  ---
61
 
62
- ## 5. Bugs & Architectural Issues Found
63
 
64
- 1. **Duplicated Logic:** UI rendering logic for suggestion cards is duplicated between `src/js/ui.js` (website) and `extension/shared/bayan-ui.js` (extension).
65
- 2. **Cache Consistency:** The extension implements its own LRU cache (`_cache` in `background.js`), whereas the website relies on network or different caching strategies.
66
- 3. **Editor Brittle-ness:** The website relies heavily on DOM-based offset replacements (`setEditorHTML`, `restoreSelection`). Any changes to HTML sanitization risk breaking cursor positions. (Must strictly adhere to "DO NOT break existing editor architecture").
 
 
 
 
 
67
 
68
  ---
69
 
70
  ## 6. Security Issues
71
 
72
- 1. **API Exposure:** The Hugging Face endpoints (`/api/*`) are public. If they lack rate-limiting or CORS whitelisting, they are vulnerable to abuse by third parties.
73
- 2. **Token Handling:** The extension uses `chrome.storage.local` which is fine, but since it lacks Supabase RLS context, it cannot securely fetch user documents yet.
 
 
 
 
 
 
 
74
 
75
  ---
76
 
77
  ## 7. Performance Issues
78
 
79
- 1. **Website:** Bundle size is relatively small due to Vanilla JS, but loading heavy libraries like `mammoth.browser.min.js` and `docx.umd.js` synchronously in `index.html` blocks render. These should be lazy-loaded.
80
- 2. **Extension:** `content-inline.js` attaches listeners to `input`/`keyup` events. If not properly debounced on heavy sites (e.g., Google Docs, WordPress), it can cause severe input lag.
 
 
 
 
 
 
 
81
 
82
  ---
83
 
84
  ## 8. UX Problems
85
 
86
- 1. **Inconsistent Wording:** The website uses "مستنداتي" (My Documents) while the extension lacks this concept entirely.
87
- 2. **Flow Disconnect:** A user correcting text in the extension cannot save that text to their Bayan account to continue editing on the website later.
88
- 3. **Loading States:** The extension's sidepanel lacks the polished "Focus Mode" and "SVG Donut Charts" available on the web editor.
 
 
 
 
 
89
 
90
  ---
91
 
92
  ## 9. Technical Debt
93
 
94
- 1. **Separation of Concerns:** The website's JS files (e.g., `editor.js`, `format.js`) tightly couple DOM manipulation with business logic.
95
- 2. **Extension Architecture:** The extension duplicates `constants.js`, `hash.js`, and `analysis-controller.js` instead of importing from a shared monorepo workspace package.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  ---
98
 
99
- ## 10. Recommended Roadmap (Prioritized)
100
 
101
- ### Phase 1: Authentication Unification (Critical)
102
- - **Goal:** Share Supabase session between Website and Extension.
103
- - **Action:** Implement `chrome.cookies` or message passing to inject the Supabase JWT into the extension.
104
 
105
- ### Phase 2: Document Sync in Extension (High)
106
- - **Goal:** Allow extension users to save selected text directly to their Bayan documents.
107
- - **Action:** Add "Save to Bayan" in the context menu and sidepanel, hitting the Supabase DB.
 
 
 
 
108
 
109
- ### Phase 3: Parity of AI Tools (Medium)
110
- - **Goal:** Bring AutoComplete, Dialect, and Quran features to the extension.
111
- - **Action:** Update the sidepanel UI and `background.js` routing to support `/api/dialect` and `/api/quran`.
 
 
112
 
113
- ### Phase 4: Performance & Code Sharing (Low)
114
- - **Goal:** Reduce tech debt.
115
- - **Action:** Refactor `shared/` folder to act as a true submodule for both web and extension. Lazy load heavy vendor scripts on the web.
 
1
  # BAYAN — Complete Product, Codebase & Extension Deep Audit
2
 
3
+ > **Audit Date:** 2026-06-26
4
+ > **Auditor Perspective:** Product Manager + Senior Frontend + Backend Architect + Extension Engineer + SaaS Reviewer
5
 
6
+ ---
 
7
 
8
+ ## 1. Current System Overview
 
 
 
9
 
10
+ ### Architecture Map
11
+
12
+ ```
13
+ ┌──────────────────────────────────────────────────────┐
14
+ │ BAYAN ECOSYSTEM │
15
+ │ │
16
+ │ ┌─────────┐ ┌──────────┐ ┌─────────────────┐ │
17
+ │ │ Website │───▶│ Flask API │───▶│ NLP Pipeline │ │
18
+ │ │ (SPA) │ │ (app.py) │ │ Spell/Gram/Punct│ │
19
+ │ └─────────┘ └──────────┘ └─────────────────┘ │
20
+ │ │ │ │ │
21
+ │ │ │ ┌─────────────────┐ │
22
+ │ │ ├─────────▶│ HF Models │ │
23
+ │ │ │ │ Summarization │ │
24
+ │ │ │ │ Grammar (Gradio)│ │
25
+ │ │ │ └─────────────────┘ │
26
+ │ │ │ │
27
+ │ ┌─────────┐ ┌──────────┐ ┌─────────────────┐ │
28
+ │ │Supabase │◀───│ Auth │───▶│ Documents DB │ │
29
+ │ │ (Cloud) │ │ Module │ │ Settings Sync │ │
30
+ │ └─────────┘ └──────────┘ └─────────────────┘ │
31
+ │ │
32
+ │ ┌────────────────────────────────────────────────┐ │
33
+ │ │ Chrome Extension (MV3) │ │
34
+ │ │ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ │
35
+ │ │ │ Content │ │Background│ │ Side Panel │ │ │
36
+ │ │ │ Script │ │ Worker │ │ + Popup │ │ │
37
+ │ │ └──────────┘ └──────────┘ └───────────────┘ │ │
38
+ │ └────────────────────────────────────────────────┘ │
39
+ └──────────────────────────────────────────────────────┘
40
+ ```
41
+
42
+ ### Technology Stack
43
+
44
+ | Layer | Technology | Notes |
45
+ |-------|-----------|-------|
46
+ | **Frontend** | Vanilla JS, HTML, CSS (Tailwind CDN) | Custom `contenteditable` editor engine |
47
+ | **Backend** | Flask (Python) | Single monolith `app.py` — 2,844 lines |
48
+ | **NLP Pipeline** | Custom Python modules | Spelling, Grammar, Punctuation, Autocomplete, Dialect |
49
+ | **AI Models** | Transformer-based | Summarization (local), Grammar (Gradio proxy), Spelling (CAMeL + custom) |
50
+ | **Database** | Supabase (PostgreSQL) | Documents, profiles, user settings |
51
+ | **Auth** | Supabase Auth | Guest (anonymous), Google OAuth |
52
+ | **Deployment** | HuggingFace Spaces (Docker) | CPU-only free tier |
53
+ | **Extension** | Chrome MV3 | Background SW, Content Script, Side Panel, Popup |
54
+
55
+ ### File Structure Summary
56
+
57
+ | Directory | Files | Purpose |
58
+ |-----------|-------|---------|
59
+ | `src/` | 6 core files | Backend + HTML + CSS |
60
+ | `src/js/` | 8 JS files + 7 subdirs | Frontend logic |
61
+ | `src/js/auth/` | 5 files | Supabase auth (client, session, UI) |
62
+ | `src/js/documents/` | 4 files | Local doc management + export |
63
+ | `src/js/documents-cloud/` | 3 files | Supabase CRUD for documents |
64
+ | `src/js/sync/` | 3 files | Offline queue + conflict resolution |
65
+ | `src/js/settings-sync/` | 2 files | User settings cloud persistence |
66
+ | `src/nlp/` | 6 subdirs | All NLP processing modules |
67
+ | `extension/` | 8 files + 4 subdirs | Chrome Extension |
68
+ | `extension/shared/` | 9 files | Shared utilities (api, renderer, patches) |
69
+ | `extension/sidepanel/` | 3 files | Side panel UI |
70
+ | `tests/` | 16 test files | Backend unit tests |
71
+ | `extension/tests/` | 8 files | Extension integration tests |
72
 
73
  ---
74
 
75
  ## 2. Feature Inventory
76
 
77
+ ### Core AI Features
78
+
79
+ | Feature | Backend API | Website Frontend | Extension | Files |
80
+ |---------|------------|-----------------|-----------|-------|
81
+ | **Spelling Correction** | ✅ `/api/spelling` + `/api/analyze` | Full (highlights, suggestions, apply) | Inline overlay + Popup + SidePanel | `nlp/spelling/`, `editor.js`, `renderer.js` |
82
+ | **Grammar Correction** | ✅ `/api/grammar` + `/api/analyze` | Full (via Gradio proxy to HF model) | Inline overlay + Popup + SidePanel | `nlp/grammar/`, `hf_inference.py` |
83
+ | **Punctuation** | ✅ `/api/punctuation` + `/api/analyze` | Full (PuncAra-v1 model) | Inline overlay + Popup + SidePanel | `nlp/punctuation/` |
84
+ | **Summarization** | ✅ `/api/summarize` | Full (tab in editor, length control) | Popup tab + SidePanel tab | `model_loader.py`, `summaries-api.js` |
85
+ | **AutoComplete** | ✅ `/api/autocomplete` | Ghost text + dropdown in editor | ⚠️ SidePanel text-box only, NO inline ghost text | `autocomplete.js`, sidepanel `btnAutocomplete` |
86
+ | **Dialect→MSA** | ✅ `/api/dialect` | Dedicated editor tab | SidePanel tab (basic text→text) | `nlp/dialect/` |
87
+ | **Quran Verification** | ✅ `/api/quran` | ✅ Dedicated editor tab | ✅ SidePanel tab (basic text→text) | `quran.py`, `quran_master.db` |
88
+
89
+ ### Platform Features
90
+
91
+ | Feature | Website | Extension (Popup) | Extension (SidePanel) | Extension (Content Script) |
92
+ |---------|---------|-------------------|----------------------|--------------------------|
93
+ | **Authentication** | ✅ Guest + Google | ❌ None | ⚠️ Partial (`initExtensionAuth()` exists but requires web page auth sync) | ⚠️ Listens for `BAYAN_AUTH_SYNC` message from web |
94
+ | **Document Save** | ✅ Supabase CRUD | ❌ None | ⚠️ UI exists (`btnNewDocument`, `btnSaveSelection`) but depends on auth | ❌ None |
95
+ | **Document Load/History** | ✅ Full panel | ❌ None | ⚠️ UI exists (`documentsList`, `historyList`) but depends on auth | ❌ None |
96
+ | **Export (PDF/DOCX/TXT)** | ✅ Full (mammoth.js, docx.js) | ❌ None | ❌ None | ❌ None |
97
+ | **Import (TXT/DOCX)** | ✅ Full | ❌ None | ❌ None | ❌ None |
98
+ | **Settings Sync** | ✅ Supabase | ❌ None | ⚠️ Placeholder (`syncExtensionSettings()`) | ❌ None |
99
+ | **Theme Toggle** | ✅ Full dark/light | ❌ Hardcoded dark | ✅ Dark only | N/A |
100
+ | **Focus Mode** | ✅ Full | N/A | ❌ None | N/A |
101
+ | **Score Ring** | ✅ Animated SVG | ✅ Simplified | ✅ Simplified | ❌ None |
102
+ | **Writing Score History** | ✅ Sparkline chart | ❌ None | ❌ None | ❌ None |
103
+ | **Error Donut Chart** | ✅ SVG donut | ❌ None | ❌ None | ❌ None |
104
+ | **Offline Mode** | ✅ Graceful degradation | ❌ No offline handling | ❌ No offline handling | ❌ No offline handling |
105
+ | **Keyboard Shortcuts** | ✅ Extensive (Alt+1-3, Ctrl+S, etc.) | ❌ None | ❌ None | ❌ None |
106
 
107
  ---
108
 
109
  ## 3. Website vs Extension Comparison
110
 
111
+ ### Authentication Flow
 
 
 
112
 
113
+ | Aspect | Website | Extension | Gap |
114
+ |--------|---------|-----------|-----|
115
+ | Guest login | `signInAnonymously()` | | **Critical** extension users can't persist anything |
116
+ | Google OAuth | `signInWithOAuth()` | | **High** |
117
+ | Session restore | ✅ `restoreSession()` via Supabase | ❌ | **High** |
118
+ | Auth state sync | ✅ `onAuthStateChange()` | ⚠️ Listens for `BAYAN_AUTH_SYNC` postMessage but only works when user visits Bayan website with extension installed | **High** — unreliable |
119
+ | Auth-gated features | ✅ Documents, sync, settings | ⚠️ UI elements exist but non-functional without auth | **High** |
120
+
121
+ ### AI Feature Comparison
122
+
123
+ | Feature | Website UX | Extension UX | Parity? |
124
+ |---------|-----------|-------------|---------|
125
+ | Analyze (S+G+P) | Rich editor with inline highlights, suggestion sidebar, popover tooltip, apply/dismiss per-suggestion | **Content Script:** Overlay marks + tooltip. **Popup/SidePanel:** Textarea + suggestion cards | ⚠️ Functional but UX gap |
126
+ | Summarize | Editor tab with radio buttons (short/medium/long) | Popup/SidePanel textarea with radio buttons | ✅ Near parity |
127
+ | AutoComplete | **Ghost text** inside editor (Tab to accept) | SidePanel has a text box with "إكمال" button but NO inline ghost text on 3rd party sites | **Medium** — missing the core UX |
128
+ | Dialect | Dedicated editor tab with "Convert" button | SidePanel tab with text box and "Convert" button | ✅ Near parity |
129
+ | Quran | Dedicated editor tab with search | SidePanel tab with text box and search | ✅ Near parity |
130
 
131
  ### Documents
132
+
133
+ | Aspect | Website | Extension | Gap |
134
+ |--------|---------|-----------|-----|
135
+ | Create document | ✅ `createDocument()` | ⚠️ Button exists in SidePanel but blocked by no auth | **High** |
136
+ | List documents | ✅ Desktop sidebar panel | ⚠️ `documentsList` in SidePanel workspace tab, blocked by no auth | **High** |
137
+ | Save/auto-save | ✅ Debounced sync via `SyncManager` | ❌ | **High** |
138
+ | Export PDF/DOCX | ✅ `export.js` | ❌ | **Medium** |
139
+ | Import | ✅ `import.js` (TXT, DOCX) | ❌ | **Low** |
140
 
141
  ---
142
 
143
+ ## 4. Missing Features
144
+
145
+ ### Critical (Blocks Production)
146
+
147
+ | # | Issue | Impact | Solution |
148
+ |---|-------|--------|----------|
149
+ | C1 | **`.env` file committed to Git** | Supabase URL and anon key are in the repo. While anon key is safe for client use, this is a security anti-pattern and may expose the project URL. | Remove `.env` from Git history, use HF Spaces secrets exclusively. `.gitignore` has `.env` but it was committed before the rule was added. |
150
+ | C2 | **CORS wildcard `origins: "*"`** | Any website can call `/api/analyze`, `/api/summarize`, etc. directly. Abusers can drain compute. | Restrict CORS to `bayan10-bayan-api.hf.space` + extension origin `chrome-extension://<id>`. |
151
+ | C3 | **No rate limiting on API** | No throttle on any endpoint. A single user can overwhelm the free-tier HF Space. | Add Flask-Limiter or simple in-memory token bucket. |
152
+
153
+ ### High (Important Feature Gap)
154
+
155
+ | # | Issue | Impact | Solution |
156
+ |---|-------|--------|----------|
157
+ | H1 | Extension has no auth | Users cannot access cloud docs, settings, or history from extension | Implement Supabase auth in extension via `chrome.identity` or shared session from Bayan website |
158
+ | H2 | Extension content script lacks AutoComplete ghost text | The flagship "ghost text" feature doesn't work on 3rd-party sites | Port `autocomplete.js` logic into `content-inline.js` with `/api/autocomplete` calls |
159
+ | H3 | Extension popup/sidepanel have no export | Users cannot export corrected text as PDF/DOCX | Add "Copy as formatted text" or lightweight export |
160
+ | H4 | No `documents` table migration | `supabase/migrations/001_profiles.sql` exists but no migration creates the `documents` table that `documents-api.js` uses | Create `002_documents.sql` migration |
161
+ | H5 | Backend monolith: `app.py` is 2,844 lines | Extremely difficult to maintain, test, or extend | Split into `routes/`, `services/`, `middleware/` modules |
162
+
163
+ ### Medium (Improvement Needed)
164
 
165
+ | # | Issue | Impact | Solution |
166
+ |---|-------|--------|----------|
167
+ | M1 | `src/js/api.js` uses ES module `export` syntax but is loaded via `<script>` tag (not `type="module"`) | The `api.js` exports are **never importable** the website uses inline `fetch()` calls instead | Either convert to `type="module"` or remove the dead `export` statements |
168
+ | M2 | Extension content script overlay doesn't handle `<iframe>` editors | Rich text editors in iframes (e.g., WordPress Gutenberg, TinyMCE) are invisible to the content script | Use `all_frames: true` in manifest or detect iframe editors |
169
+ | M3 | Duplicated suggestion rendering logic | `ui.js` (website) and `bayan-ui.js` (extension) implement the same card HTML generation | Extract to shared package |
170
+ | M4 | Extension `popup.js` (498 lines) and `sidepanel.js` (702 lines) share ~60% identical code | Maintenance nightmare — fixing a bug requires changes in 2+ files | Refactor into shared modules with UI-specific wrappers |
171
+ | M5 | Grammar model uses Gradio proxy with SSE streaming | Creates a hard dependency on external `mohammedahmedezz2004-bayan-arabic-grammarly-correction.hf.space`. If that Space goes down, grammar breaks. | Host the grammar model directly on the Bayan Space, or add fallback |
172
+ | M6 | No i18n framework on website | All strings are hardcoded in Arabic HTML. Adding English support requires rewriting HTML | Add simple i18n JSON loader (extension already has `_locales/ar/`) |
173
+
174
+ ### Low (Nice to Have)
175
+
176
+ | # | Issue | Impact | Solution |
177
+ |---|-------|--------|----------|
178
+ | L1 | Extension only has Arabic locale | Cannot be published on Chrome Web Store for non-Arabic users | Add `_locales/en/messages.json` |
179
+ | L2 | No analytics or telemetry | No visibility into usage patterns, error rates, or feature adoption | Add lightweight event tracking (privacy-respecting) |
180
+ | L3 | Heavy vendor libraries loaded synchronously | `mammoth.browser.min.js`, `docx.umd.js`, `html2canvas.min.js` block initial render | Lazy-load on first export action |
181
+ | L4 | No service worker for website | No offline caching for the web app | Add basic SW for static assets |
182
 
183
  ---
184
 
185
+ ## 5. Bugs Found
186
 
187
+ | # | Bug | Severity | Location | Status |
188
+ |---|-----|----------|----------|--------|
189
+ | B1 | `ENABLE_AUTOCOMPLETE_MODEL = False` in `app.py:62` | Medium | `app.py` line 62 | AutoComplete model disabled by default `/api/autocomplete` still works via lazy-loading, but the flag is misleading |
190
+ | B2 | `src/js/api.js` uses `export` keyword but is not loaded as ES module | Low | `api.js` | Dead code — never actually imported anywhere |
191
+ | B3 | Extension `bayan-api.js` missing functions `bayanAutocomplete`, `bayanDialect`, `bayanQuran` | High | `bayan-api.js` only defines `bayanAnalyze`, `bayanSummarize`, `bayanHealthCheck` | SidePanel calls these undefined functions — will throw `ReferenceError` |
192
+ | B4 | Extension content script overlay position breaks on page scroll (absolute vs fixed positioning) | Medium | `content-inline.js:191` | Overlay uses `window.scrollY` but doesn't update on window resize |
193
+ | B5 | Score sparkline renders with only 2 data points creating a meaningless line | Low | `format.js` | ✅ Fixed (raised minimum to 3 points) |
194
+ | B6 | `dismissAllFiltered()` only removed DOM elements without updating `window.currentSuggestions` | Medium | `format.js` | ✅ Fixed |
195
 
196
  ---
197
 
198
  ## 6. Security Issues
199
 
200
+ | # | Issue | Severity | Details |
201
+ |---|-------|----------|---------|
202
+ | S1 | **`.env` committed to repo** | **Critical** | Supabase URL + anon key visible in Git history. While anon keys are designed for client-side use, the URL+key combo allows anyone to make Supabase API calls. |
203
+ | S2 | **CORS `origins: "*"`** | **Critical** | `app.py:94` — allows any origin to call all API endpoints. Enables: (a) compute theft, (b) DDoS via free proxy, (c) third-party scraping. |
204
+ | S3 | **No API authentication** | **High** | No JWT, API key, or session check on any endpoint. Extension uses only `host_permissions` scoping. |
205
+ | S4 | **XSS risk in editor** | **Medium** | `setEditorHTML()` injects HTML directly into contenteditable. While `renderer.js` escapes text, any upstream bug in suggestion rendering could inject arbitrary HTML. |
206
+ | S5 | **Supabase RLS incomplete** | **Medium** | Only `profiles` has RLS policies. The `documents` table (if exists) needs RLS to prevent cross-user data access. |
207
+ | S6 | **Extension Trusted Types partial** | **Low** | `content-inline.js` implements `trustedTypes.createPolicy()` with identity transform (`input => input`), which passes the CSP check but provides no actual sanitization. |
208
+ | S7 | **Debug endpoint exposed** | **Low** | `/api/debug-models` is accessible in production and leaks internal model status, memory usage, and startup errors. |
209
 
210
  ---
211
 
212
  ## 7. Performance Issues
213
 
214
+ | # | Issue | Severity | Details |
215
+ |---|-------|----------|---------|
216
+ | P1 | **`app.py` is 2,844 lines** | High | Single-file monolith. Every request loads all imports. Cold start on HF Spaces free tier takes ~60s. |
217
+ | P2 | **Vendor JS loaded synchronously** | Medium | `mammoth.browser.min.js` (340KB), `docx.umd.js` (1.2MB), `html2canvas.min.js` (210KB) all load on page start even if never used. |
218
+ | P3 | **Extension content script injected on ALL sites** | Medium | `matches: ["https://*/*", "http://*/*"]` — runs on every page. The `BayanController` module loads even on sites where user never types Arabic. |
219
+ | P4 | **No API response caching on website** | Medium | Every keystroke after debounce triggers a full `/api/analyze` call. Extension has background worker caching, but website doesn't. |
220
+ | P5 | **Grammar Gradio SSE dependency** | Medium | Grammar correction requires streaming from external HF Space. Average latency: 3-8 seconds. Adds significant delay to the analysis pipeline. |
221
+ | P6 | **Quran DB is 23MB** | Low | `quran_master.db` (SQLite, 23MB) is loaded into the Docker container. Fine for now, but limits scaling. |
222
+ | P7 | **No CSS/JS minification** | Low | All assets served unminified. `components.css` alone is 4,125+ lines (~90KB). |
223
 
224
  ---
225
 
226
  ## 8. UX Problems
227
 
228
+ | # | Issue | Severity | Details |
229
+ |---|-------|----------|---------|
230
+ | U1 | **Extension content script tooltip clips at viewport edge** | Medium | Tooltip for highlighted errors can overflow off-screen on narrow viewports. No boundary detection. |
231
+ | U2 | **No loading skeleton on website** | Medium | Editor page shows blank white space during model initialization. No skeleton/shimmer to indicate loading. |
232
+ | U3 | **Extension popup has no dialect/quran/autocomplete** | Medium | Only "تصحيح" and "تلخيص" tabs. SidePanel has all features, but popup is the first surface users see. |
233
+ | U4 | **Inconsistent branding between popup and sidepanel** | Low | Popup uses `.bayan-*` class prefix, SidePanel uses `.sp-*` prefix. Different color palettes. |
234
+ | U5 | **No onboarding flow** | Low | First-time users see an empty editor with no guidance. No tooltips, walkthrough, or sample text. |
235
+ | U6 | **Mobile responsiveness incomplete** | Low | Website has responsive breakpoints but bottom-sheet for suggestions lacks smooth gestures. |
236
 
237
  ---
238
 
239
  ## 9. Technical Debt
240
 
241
+ ### Backend
242
+
243
+ | Item | Severity | Details |
244
+ |------|----------|---------|
245
+ | **Monolith `app.py`** | High | 2,844 lines. Contains routes, NLP logic, model loading, diffing algorithms, offset mapping, pipeline orchestration, Quran search integration, and CORS — all in one file. |
246
+ | **Duplicated directional blocks** | Medium | `_DIRECTIONAL_BLOCKS` in `app.py` duplicates logic that also exists in `araspell_rules.py`. |
247
+ | **12+ test files at project root** | Low | `test_proof.py`, `test_sv.py`, `test_pc.py`, etc. scattered in root instead of `tests/`. |
248
+ | **Dead code** | Low | `ENABLE_DIALECT_MODEL = False`, `ENABLE_AUTOCOMPLETE_MODEL = False` flags in `app.py` — no code path checks them for these features since they use lazy-loading. |
249
+ | **Archive directory** | Low | `archive/legacy_scripts/` contains old code that shouldn't ship in Docker image. |
250
+
251
+ ### Frontend (Website)
252
+
253
+ | Item | Severity | Details |
254
+ |------|----------|---------|
255
+ | **`api.js` dead exports** | Medium | `export async function analyzeText()` — never imported. Website uses inline `fetch()` in `editor.js`. |
256
+ | **Tight coupling in `editor.js`** | Medium | DOM manipulation, API calls, suggestion management, and UI updates all in one 29KB file. |
257
+ | **No build system** | Low | No bundler, no tree-shaking, no code-splitting. All JS loaded via `<script>` tags. |
258
+ | **CSS structure** | Low | Single `components.css` at 4,125+ lines. No CSS modules, no scoping. |
259
+
260
+ ### Extension
261
+
262
+ | Item | Severity | Details |
263
+ |------|----------|---------|
264
+ | **`popup.js` and `sidepanel.js` code duplication** | High | ~60% identical code: `updateCounts()`, `markStale()`, `setLoading()`, `updateScore()`, `renderSuggestions()`, `showToast()`. |
265
+ | **Missing API functions in `bayan-api.js`** | High | SidePanel calls `bayanAutocomplete()`, `bayanDialect()`, `bayanQuran()` which are not defined in `bayan-api.js`. These must be defined elsewhere or will throw. |
266
+ | **No TypeScript / JSDoc validation** | Low | All extension code is plain JS with no compile-time checking. |
267
+
268
+ ---
269
+
270
+ ## 10. Recommended Roadmap
271
+
272
+ ### Phase 1: Security Hardening ⚡ (Critical — Before Any Growth)
273
+
274
+ **Timeline: 1-2 days**
275
+
276
+ 1. **Remove `.env` from Git history** — `git filter-branch` or BFG Repo Cleaner
277
+ 2. **Restrict CORS** — Change `origins: "*"` to allowlist `["https://bayan10-bayan-api.hf.space", "chrome-extension://<ext-id>"]`
278
+ 3. **Add rate limiting** — Flask-Limiter: 30 req/min per IP for `/api/analyze`, 10 req/min for `/api/summarize`
279
+ 4. **Disable debug endpoint in production** — Guard `/api/debug-models` behind `app.debug` flag
280
+ 5. **Add Supabase RLS for `documents` table** — `CREATE POLICY ... USING (auth.uid() = user_id)`
281
+
282
+ ### Phase 2: Extension Auth Unification 🔐 (High)
283
+
284
+ **Timeline: 3-5 days**
285
+
286
+ 1. **Implement Supabase client in extension** — Add `@supabase/supabase-js` as UMD bundle in `shared/`
287
+ 2. **Auth flow**: Use `chrome.identity.launchWebAuthFlow()` for Google OAuth → receive tokens → init Supabase session
288
+ 3. **Session persistence**: Store refresh token in `chrome.storage.local`
289
+ 4. **Auth sync**: When user logs in on website, broadcast via `postMessage` → content script → `chrome.storage`
290
+ 5. **Result**: Extension users can access their documents, settings, and history
291
+
292
+ ### Phase 3: Extension Feature Parity 🔧 (High)
293
+
294
+ **Timeline: 3-5 days**
295
+
296
+ 1. **Add missing API functions** to `bayan-api.js`: `bayanAutocomplete()`, `bayanDialect()`, `bayanQuran()`
297
+ 2. **Add autocomplete/dialect/quran tabs to popup** (currently SidePanel-only)
298
+ 3. **Inline ghost text for content script** — Port `autocomplete.js` logic for textareas on 3rd-party sites
299
+ 4. **Add basic export** — "Copy corrected text" button already exists; add "Download as TXT"
300
+
301
+ ### Phase 4: Backend Refactoring 🏗️ (Medium)
302
+
303
+ **Timeline: 5-7 days**
304
+
305
+ 1. **Split `app.py`** into:
306
+ - `routes/analyze.py`, `routes/summarize.py`, `routes/dialect.py`, `routes/quran.py`
307
+ - `services/pipeline.py` (orchestration)
308
+ - `middleware/cors.py`, `middleware/rate_limit.py`
309
+ 2. **Create `002_documents.sql` migration** with proper RLS
310
+ 3. **Move root-level test files** into `tests/`
311
+ 4. **Remove `archive/` from Docker build** (add to `.dockerignore`)
312
+
313
+ ### Phase 5: Extension Code Quality 🧹 (Medium)
314
+
315
+ **Timeline: 3-4 days**
316
+
317
+ 1. **Extract shared logic** from `popup.js` and `sidepanel.js` into `shared/bayan-core.js`
318
+ 2. **Add English locale** `_locales/en/messages.json`
319
+ 3. **Add `all_frames: true`** to manifest for iframe editor support
320
+ 4. **Add theme toggle** to popup and sidepanel
321
+
322
+ ### Phase 6: Performance & Polish ✨ (Low)
323
+
324
+ **Timeline: 2-3 days**
325
+
326
+ 1. **Lazy-load vendor libs** (mammoth, docx, html2canvas) on first use
327
+ 2. **Add website-side API caching** (localStorage TTL cache like extension has)
328
+ 3. **Add CSS/JS minification** to Docker build
329
+ 4. **Add loading skeletons** for editor page
330
+ 5. **Add onboarding flow** — sample text + guided tooltips
331
+
332
+ ---
333
+
334
+ ## Summary Matrix
335
+
336
+ | Category | Critical | High | Medium | Low | Total |
337
+ |----------|---------|------|--------|-----|-------|
338
+ | **Security** | 2 (S1, S2) | 1 (S3) | 2 (S4, S5) | 2 (S6, S7) | 7 |
339
+ | **Missing Features** | 0 | 5 (H1-H5) | 6 (M1-M6) | 4 (L1-L4) | 15 |
340
+ | **Bugs** | 0 | 1 (B3) | 2 (B1, B4) | 1 (B2) | 4 (+2 fixed) |
341
+ | **Performance** | 0 | 1 (P1) | 4 (P2-P5) | 2 (P6, P7) | 7 |
342
+ | **UX** | 0 | 0 | 3 (U1-U3) | 3 (U4-U6) | 6 |
343
+ | **Tech Debt** | 0 | 3 | 5 | 5 | 13 |
344
+ | **TOTAL** | **2** | **11** | **22** | **17** | **52** |
345
 
346
  ---
347
 
348
+ ## Final Verdict
349
 
350
+ Bayan is a technically impressive product with a solid NLP pipeline, a mature editor engine, and a well-architected extension. The core correction features (Spelling → Grammar → Punctuation) work end-to-end across both surfaces.
 
 
351
 
352
+ **What Bayan does well:**
353
+ - Custom contenteditable editor with proper cursor handling
354
+ - Multi-stage NLP pipeline with offset mapping
355
+ - ✅ Extension uses overlay-only rendering (never modifies user DOM)
356
+ - ✅ Supabase integration for cloud persistence
357
+ - ✅ Comprehensive test coverage (16 backend test files)
358
+ - ✅ Extension follows MV3 best practices (service worker, side panel)
359
 
360
+ **What must be fixed before growth:**
361
+ 1. 🔴 **Security**: CORS wildcard + no rate limiting = anyone can abuse the API
362
+ 2. 🔴 **Auth gap**: Extension users can't persist anything breaks the SaaS value proposition
363
+ 3. 🟡 **Extension missing API functions**: `bayanAutocomplete/Dialect/Quran` will throw `ReferenceError`
364
+ 4. 🟡 **Backend monolith**: 2,844-line `app.py` is a maintenance bottleneck
365
 
366
+ **Bottom line:** Bayan is 80% of the way to a production-grade SaaS product. The remaining 20% is security hardening, extension auth, and code architecture — all achievable in 2-3 focused weeks.
 
 
extension/shared/supabase.min.js ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt CHANGED
@@ -15,3 +15,4 @@ gunicorn
15
  python-dotenv
16
  gradio_client
17
  rapidfuzz
 
 
15
  python-dotenv
16
  gradio_client
17
  rapidfuzz
18
+ Flask-Limiter
src/app.py CHANGED
@@ -887,9 +887,11 @@ def _is_small_spelling_change(orig_word, corr_word, vocab_manager=None):
887
  # AND roots have same length (true consonant swap, not a character addition)
888
  # Exception: الولاد→الأولاد has roots ولاد(4)→أولاد(5) — different length = allow
889
  _HAMZA_CHARS = set('أإآاء')
 
890
  if (_o_root and _c_root and _o_root[0] != _c_root[0]
891
  and len(_o_root) == len(_c_root) # same-length roots only
892
- and not (_o_root[0] in _HAMZA_CHARS and _c_root[0] in _HAMZA_CHARS)):
 
893
  logger.info(
894
  f"[SPELLING] Blocked first-letter change: '{orig_word}'→'{corr_word}' "
895
  f"(root '{_o_root[0]}'→'{_c_root[0]}')"
 
887
  # AND roots have same length (true consonant swap, not a character addition)
888
  # Exception: الولاد→الأولاد has roots ولاد(4)→أولاد(5) — different length = allow
889
  _HAMZA_CHARS = set('أإآاء')
890
+ _STOP_WORDS = {"التي", "الذي", "الذين", "هذا", "هذه", "هؤلاء", "تلك", "ذلك"}
891
  if (_o_root and _c_root and _o_root[0] != _c_root[0]
892
  and len(_o_root) == len(_c_root) # same-length roots only
893
+ and not (_o_root[0] in _HAMZA_CHARS and _c_root[0] in _HAMZA_CHARS)
894
+ and corr_word not in _STOP_WORDS):
895
  logger.info(
896
  f"[SPELLING] Blocked first-letter change: '{orig_word}'→'{corr_word}' "
897
  f"(root '{_o_root[0]}'→'{_c_root[0]}')"
src/nlp/grammar/grammar_rules.py CHANGED
@@ -9,6 +9,27 @@ from camel_tools.disambig.mle import MLEDisambiguator
9
 
10
  logger = logging.getLogger(__name__)
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  class ArabicGrammarGuard:
14
  def __init__(self):
@@ -201,25 +222,6 @@ class ArabicGrammarGuard:
201
  # ── Batch 6: Noun-adjective gender agreement ──
202
  # When a feminine noun is followed by a masculine adjective, add ة
203
  # e.g. السيارة جميل → السيارة جميلة
204
- KNOWN_FEMININE_NOUNS = {
205
- 'السيارة', 'سيارة', 'المدرسة', 'مدرسة', 'المدينة', 'مدينة',
206
- 'البنت', 'الشمس', 'الأرض', 'الطالبة', 'طالبة',
207
- 'الجامعة', 'جامعة', 'الشركة', 'شركة', 'الحكومة', 'حكومة',
208
- 'الغرفة', 'غرفة', 'الحديقة', 'حديقة', 'المكتبة', 'مكتبة',
209
- 'الدولة', 'دولة', 'الرحلة', 'رحلة', 'اللغة', 'لغة',
210
- 'القصة', 'قصة', 'الفكرة', 'فكرة', 'النتيجة', 'نتيجة',
211
- }
212
- # Common adjectives that have masculine/feminine pairs
213
- MASC_TO_FEM_ADJ = {
214
- 'جميل': 'جميلة', 'كبير': 'كبيرة', 'صغير': 'صغيرة',
215
- 'طويل': 'طويلة', 'قصير': 'قصيرة', 'جديد': 'جديدة',
216
- 'قديم': 'قديمة', 'بعيد': 'بعيدة', 'قريب': 'قريبة',
217
- 'سريع': 'سريعة', 'بطيء': 'بطيئة', 'واسع': 'واسعة',
218
- 'ضيق': 'ضيقة', 'عميق': 'عميقة', 'خفيف': 'خفيفة',
219
- 'ثقيل': 'ثقيلة', 'نظيف': 'نظيفة', 'مشرق': 'مشرقة',
220
- 'ذكي': 'ذكية', 'غني': 'غنية', 'فقير': 'فقيرة',
221
- 'متفوق': 'متفوقة', 'مجتهد': 'مجتهدة', 'ممتاز': 'ممتازة',
222
- }
223
  words = text.split()
224
  for i in range(len(words) - 1):
225
  noun = words[i]
@@ -679,17 +681,6 @@ class ArabicGrammarGuard:
679
  disambig_tokens = self.mle.disambiguate(tokens)
680
  corrected_tokens = list(tokens)
681
 
682
- MASC_TO_FEM_ADJ = {
683
- 'جميل': 'جميلة', 'كبير': 'كبيرة', 'صغير': 'صغيرة',
684
- 'طويل': 'طويلة', 'قصير': 'قصيرة', 'جديد': 'جديدة',
685
- 'قديم': 'قديمة', 'بعيد': 'بعيدة', 'قريب': 'قريبة',
686
- 'سريع': 'سريعة', 'بطيء': 'بطيئة', 'واسع': 'واسعة',
687
- 'ضيق': 'ضيقة', 'عميق': 'عميقة', 'خفيف': 'خفيفة',
688
- 'ثقيل': 'ثقيلة', 'نظيف': 'نظيفة', 'مشرق': 'مشرقة',
689
- 'ذكي': 'ذكية', 'غني': 'غنية', 'فقير': 'فقيرة',
690
- 'متفوق': 'متفوقة', 'مجتهد': 'مجتهدة', 'ممتاز': 'ممتازة',
691
- }
692
-
693
  for i in range(len(disambig_tokens) - 1):
694
  w1 = corrected_tokens[i]
695
  w2 = corrected_tokens[i+1]
 
9
 
10
  logger = logging.getLogger(__name__)
11
 
12
+ KNOWN_FEMININE_NOUNS = {
13
+ 'السيارة', 'سيارة', 'المدرسة', 'مدرسة', 'المدينة', 'مدينة',
14
+ 'البنت', 'الشمس', 'الأرض', 'الطالبة', 'طالبة',
15
+ 'الجامعة', 'جامعة', 'الشركة', 'شركة', 'الحكومة', 'حكومة',
16
+ 'الغرفة', 'غرفة', 'الحديقة', 'حديقة', 'المكتبة', 'مكتبة',
17
+ 'الدولة', 'دولة', 'الرحلة', 'رحلة', 'اللغة', 'لغة',
18
+ 'القصة', 'قصة', 'الفكرة', 'فكرة', 'النتيجة', 'نتيجة',
19
+ }
20
+
21
+ # Common adjectives that have masculine/feminine pairs
22
+ MASC_TO_FEM_ADJ = {
23
+ 'جميل': 'جميلة', 'كبير': 'كبيرة', 'صغير': 'صغيرة',
24
+ 'طويل': 'طويلة', 'قصير': 'قصيرة', 'جديد': 'جديدة',
25
+ 'قديم': 'قديمة', 'بعيد': 'بعيدة', 'قريب': 'قريبة',
26
+ 'سريع': 'سريعة', 'بطيء': 'بطيئة', 'واسع': 'واسعة',
27
+ 'ضيق': 'ضيقة', 'عميق': 'عميقة', 'خفيف': 'خفيفة',
28
+ 'ثقيل': 'ثقيلة', 'نظيف': 'نظيفة', 'مشرق': 'مشرقة',
29
+ 'ذكي': 'ذكية', 'غني': 'غنية', 'فقير': 'فقيرة',
30
+ 'متفوق': 'متفوقة', 'مجتهد': 'مجتهدة', 'ممتاز': 'ممتازة',
31
+ }
32
+
33
 
34
  class ArabicGrammarGuard:
35
  def __init__(self):
 
222
  # ── Batch 6: Noun-adjective gender agreement ──
223
  # When a feminine noun is followed by a masculine adjective, add ة
224
  # e.g. السيارة جميل → السيارة جميلة
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  words = text.split()
226
  for i in range(len(words) - 1):
227
  noun = words[i]
 
681
  disambig_tokens = self.mle.disambiguate(tokens)
682
  corrected_tokens = list(tokens)
683
 
 
 
 
 
 
 
 
 
 
 
 
684
  for i in range(len(disambig_tokens) - 1):
685
  w1 = corrected_tokens[i]
686
  w2 = corrected_tokens[i+1]
src/nlp/punctuation/punctuation_rules.py CHANGED
@@ -52,6 +52,18 @@ def arabic_postprocessing(text: str) -> str:
52
  text = re.sub(r'،؛|؛،', '؛', text)
53
  text = re.sub(r'([!؟])\.', r'\1', text)
54
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  # 6. Remove stray leading punctuation
56
  text = re.sub(r'^[،؛:!؟. \t]+', '', text)
57
 
@@ -155,48 +167,59 @@ def validate_punctuation_diff(diff: dict, full_text: str = '') -> bool:
155
  corr_no_punct = re.sub(r'[.,،؛؟!:;?!]+$', '', correction)
156
  if _normalize_for_comparison(orig_no_punct.replace(' ', '')) == \
157
  _normalize_for_comparison(corr_no_punct.replace(' ', '')):
158
- # This is a pure terminal-punctuation addition.
159
- # V2 FIX-30: Fall back to original when full_text is empty
160
- _word_count_source = full_text if full_text else original
161
- _full_word_count = len(re.findall(
162
- r'[\u0600-\u06FFa-zA-Z]+', _word_count_source
163
- ))
164
- _full_already_has_terminal = bool(
165
- re.search(r'[.،؛؟!?!][\s]*$', full_text)
166
- ) if full_text else False
167
- _full_has_ellipsis = full_text.rstrip().endswith('...') if full_text else False
168
-
169
- # V2 FIX-30: Allow for 1+ words (not 5)
170
- if _full_word_count >= 1 and not _full_already_has_terminal and not _full_has_ellipsis:
171
- # ── Softened FIX-29 (Merged): Short-text ؟/! guard ──
172
- # For short texts (< 3 words), block ؟ and ! unless
173
- # cue words are present. Prevents "محمد" → "محمد؟"
174
- # but allows "اليوم" → "اليوم." (period is safe).
175
- # For 3+ words, allow freely (V2 behavior).
176
- _added_punct = correction[len(orig_stripped):]
177
- if _full_word_count < 3 and ('!' in _added_punct or '؟' in _added_punct):
178
- _text_to_scan = full_text if full_text else original
179
- _has_cue = any(w in _EXCL_CUES for w in _text_to_scan.split())
180
- if not _has_cue:
181
- logger.info(
182
- f"[PUNC-SAFETY] Blocked !/؟ on short text without cue: "
183
- f"'{original}' → '{correction}'"
184
- )
185
- return False
186
-
187
- logger.info(
188
- f"[PUNC-SAFETY] Allowed terminal punct for sentence "
189
- f"({_full_word_count} words): "
190
- f"'{original}' → '{correction}'"
191
- )
192
- # Fall through to remaining rules (don't return yet)
193
  else:
194
- # Already has terminal punct or ends in ellipsis → REJECT
195
- logger.info(
196
- f"[PUNC-SAFETY] TerminalPunctuationGuard triggered: removing trailing punctuation "
197
- f"'{original}' '{correction}'"
198
- )
199
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
 
202
 
 
52
  text = re.sub(r'،؛|؛،', '؛', text)
53
  text = re.sub(r'([!؟])\.', r'\1', text)
54
 
55
+ # 5.5 Syntactic context fixes for model hallucinations
56
+ # Remove colons/semicolons before relative pronouns
57
+ text = re.sub(r'[؛:]\s*(التي|الذي|الذين|اللتان|اللذان|اللاتي|اللواتي)', r' \1', text)
58
+
59
+ # Fix misplaced colons for saying verbs (e.g. قال: المعلم -> قال المعلم:)
60
+ text = re.sub(r'\b(قال|يقول|قالت|تقول|أجاب|أجابت|سأل|سألت|أخبر|أخبرت|صرح|صرحت|أضاف|أضافت|أردف|أردفت):?\s+(ال[أ-ي]+|أحمد|محمد|محمود|علي|عمر|خالد|فاطمة|مريم|عائشة|خديجة)\b:?', r'\1 \2:', text)
61
+
62
+ # Remove colons after verbs that do not introduce speech/lists
63
+ text = re.sub(r'\b(يقدر|يستطيع|يمكن|يجب|ينبغي|يعتبر|يعد|يرى|يعتقد)\s*:', r'\1 ', text)
64
+ # Replace semicolon with comma if followed by "و" (and) or similar conjunctions, as semicolon is for separate clauses
65
+ text = re.sub(r'؛\s*(و|ف|ثم|أو|أم|بل)\b', r'، \1', text)
66
+
67
  # 6. Remove stray leading punctuation
68
  text = re.sub(r'^[،؛:!؟. \t]+', '', text)
69
 
 
167
  corr_no_punct = re.sub(r'[.,،؛؟!:;?!]+$', '', correction)
168
  if _normalize_for_comparison(orig_no_punct.replace(' ', '')) == \
169
  _normalize_for_comparison(corr_no_punct.replace(' ', '')):
170
+
171
+ is_at_end = False
172
+ if full_text and 'end' in diff:
173
+ is_at_end = diff['end'] >= len(full_text) - 2
174
+ elif not full_text:
175
+ is_at_end = True # If no context, assume it's a standalone fragment
176
+
177
+ if not is_at_end:
178
+ # Mid-sentence punctuation addition. This is safe to fall through to other rules.
179
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  else:
181
+ # This is a pure terminal-punctuation addition.
182
+ # V2 FIX-30: Fall back to original when full_text is empty
183
+ _word_count_source = full_text if full_text else original
184
+ _full_word_count = len(re.findall(
185
+ r'[\u0600-\u06FFa-zA-Z]+', _word_count_source
186
+ ))
187
+ _full_already_has_terminal = bool(
188
+ re.search(r'[.،؛؟!?!][\s]*$', full_text)
189
+ ) if full_text else False
190
+ _full_has_ellipsis = full_text.rstrip().endswith('...') if full_text else False
191
+
192
+ # V2 FIX-30: Allow for 1+ words (not 5)
193
+ if _full_word_count >= 1 and not _full_already_has_terminal and not _full_has_ellipsis:
194
+ # ── Softened FIX-29 (Merged): Short-text ؟/! guard ──
195
+ # For short texts (< 3 words), block ؟ and ! unless
196
+ # cue words are present. Prevents "محمد" → "محمد؟"
197
+ # but allows "اليوم" → "اليوم." (period is safe).
198
+ # For 3+ words, allow freely (V2 behavior).
199
+ _added_punct = correction[len(orig_stripped):]
200
+ if _full_word_count < 3 and ('!' in _added_punct or '؟' in _added_punct):
201
+ _text_to_scan = full_text if full_text else original
202
+ _has_cue = any(w in _EXCL_CUES for w in _text_to_scan.split())
203
+ if not _has_cue:
204
+ logger.info(
205
+ f"[PUNC-SAFETY] Blocked !/؟ on short text without cue: "
206
+ f"'{original}' → '{correction}'"
207
+ )
208
+ return False
209
+
210
+ logger.info(
211
+ f"[PUNC-SAFETY] Allowed terminal punct for sentence "
212
+ f"({_full_word_count} words): "
213
+ f"'{original}' → '{correction}'"
214
+ )
215
+ # Fall through to remaining rules (don't return yet)
216
+ else:
217
+ # Already has terminal punct or ends in ellipsis → REJECT
218
+ logger.info(
219
+ f"[PUNC-SAFETY] TerminalPunctuationGuard triggered: removing trailing punctuation "
220
+ f"'{original}' → '{correction}'"
221
+ )
222
+ return False
223
 
224
 
225
 
src/nlp/punctuation/punctuation_service.py CHANGED
@@ -53,8 +53,14 @@ class PunctuationChecker:
53
  if the model changed the BASE text (not just added/moved punctuation),
54
  revert to the original word but keep any punctuation the model added.
55
  """
 
 
 
 
 
 
56
  orig_words = original.split()
57
- punc_words = punctuated.split()
58
 
59
  if not orig_words or not punc_words:
60
  return punctuated
@@ -72,11 +78,20 @@ class PunctuationChecker:
72
  p_base = self._strip_punct(p_word)
73
 
74
  if o_base == p_base:
 
 
 
 
 
75
  # Same base word — keep punctuation changes from model
76
  result.append(p_word)
77
  oi += 1
78
  pi += 1
79
  elif self._is_only_punct_difference(o_word, p_word):
 
 
 
 
80
  # Words differ only by punctuation — keep model's punctuation
81
  result.append(p_word)
82
  oi += 1
@@ -99,6 +114,10 @@ class PunctuationChecker:
99
 
100
  # Only add punctuation that wasn't already there
101
  if not o_word.endswith(punct_suffix) and punct_suffix:
 
 
 
 
102
  result.append(o_word + punct_suffix)
103
  elif punct_prefix and not o_word.startswith(punct_prefix):
104
  result.append(punct_prefix + o_word)
@@ -116,6 +135,10 @@ class PunctuationChecker:
116
  while pi < len(punc_words):
117
  p_word = punc_words[pi]
118
  if all(ch in self.PUNCTUATION_CHARS or ch.isspace() for ch in p_word):
 
 
 
 
119
  result.append(p_word)
120
  pi += 1
121
 
 
53
  if the model changed the BASE text (not just added/moved punctuation),
54
  revert to the original word but keep any punctuation the model added.
55
  """
56
+ # CRITICAL FIX: The model sometimes predicts standalone punctuation tokens (e.g. "المعلم :").
57
+ # If we split by space, the arrays will misalign. We MUST attach punctuation to the preceding word
58
+ # before splitting to ensure 1-to-1 alignment.
59
+ import re
60
+ punctuated_normalized = re.sub(r'\s+([،؛:!؟.])', r'\1', punctuated)
61
+
62
  orig_words = original.split()
63
+ punc_words = punctuated_normalized.split()
64
 
65
  if not orig_words or not punc_words:
66
  return punctuated
 
78
  p_base = self._strip_punct(p_word)
79
 
80
  if o_base == p_base:
81
+ # Anti-hallucination for question marks
82
+ if '؟' in p_word and '؟' not in o_word:
83
+ _EXCL_CUES = {'هل', 'أين', 'متى', 'كيف', 'لماذا', 'ماذا', 'أي', 'كم', 'ما'}
84
+ if not any(w in _EXCL_CUES for w in orig_words):
85
+ p_word = p_word.replace('؟', '.')
86
  # Same base word — keep punctuation changes from model
87
  result.append(p_word)
88
  oi += 1
89
  pi += 1
90
  elif self._is_only_punct_difference(o_word, p_word):
91
+ if '؟' in p_word and '؟' not in o_word:
92
+ _EXCL_CUES = {'هل', 'أين', 'متى', 'كيف', 'لماذا', 'ماذا', 'أي', 'كم', 'ما'}
93
+ if not any(w in _EXCL_CUES for w in orig_words):
94
+ p_word = p_word.replace('؟', '.')
95
  # Words differ only by punctuation — keep model's punctuation
96
  result.append(p_word)
97
  oi += 1
 
114
 
115
  # Only add punctuation that wasn't already there
116
  if not o_word.endswith(punct_suffix) and punct_suffix:
117
+ if '؟' in punct_suffix and '؟' not in o_word:
118
+ _EXCL_CUES = {'هل', 'أين', 'متى', 'كيف', 'لماذا', 'ماذا', 'أي', 'كم', 'ما'}
119
+ if not any(w in _EXCL_CUES for w in orig_words):
120
+ punct_suffix = punct_suffix.replace('؟', '.')
121
  result.append(o_word + punct_suffix)
122
  elif punct_prefix and not o_word.startswith(punct_prefix):
123
  result.append(punct_prefix + o_word)
 
135
  while pi < len(punc_words):
136
  p_word = punc_words[pi]
137
  if all(ch in self.PUNCTUATION_CHARS or ch.isspace() for ch in p_word):
138
+ if '؟' in p_word:
139
+ _EXCL_CUES = {'هل', 'أين', 'متى', 'كيف', 'لماذا', 'ماذا', 'أي', 'كم', 'ما'}
140
+ if not any(w in _EXCL_CUES for w in orig_words):
141
+ p_word = p_word.replace('؟', '.')
142
  result.append(p_word)
143
  pi += 1
144