wobble-bibble 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +133 -0
- package/dist/index.d.ts +324 -0
- package/dist/index.js +471 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ragaeeb Haq
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# wobble-bibble 🕌
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/wobble-bibble)
|
|
4
|
+
[](https://codecov.io/gh/ragaeeb/wobble-bibble)
|
|
5
|
+
[](https://bundlejs.com/?q=wobble-bibble%40latest)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://bun.sh)
|
|
8
|
+
[](https://www.typescriptlang.org)
|
|
9
|
+
[](https://biomejs.dev)
|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+
[](https://wakatime.com/badge/user/a0b906ce-b8e7-4463-8bce-383238df6d4b/project/f2110f75-cd59-4395-9790-b971ad3a8195)
|
|
15
|
+
|
|
16
|
+
TypeScript library for Islamic text translation prompts with LLM output validation and prompt stacking utilities.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install wobble-bibble
|
|
22
|
+
# or
|
|
23
|
+
bun add wobble-bibble
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Bundled Prompts**: 8 optimized translation prompts (Hadith, Fiqh, Tafsir, etc.) with strongly-typed access
|
|
29
|
+
- **Translation Validation**: Catch LLM hallucinations like malformed segment IDs, Arabic leaks, forbidden terms
|
|
30
|
+
- **Prompt Stacking**: Master + specialized prompts combined automatically
|
|
31
|
+
- **Gold Standards**: [High-fidelity reference dataset](docs/gold-standard.md) for benchmarking
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### Get Translation Prompts
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { getPrompt, getPrompts, getPromptIds } from 'wobble-bibble';
|
|
39
|
+
|
|
40
|
+
// Get a specific stacked prompt (strongly typed)
|
|
41
|
+
const hadithPrompt = getPrompt('hadith');
|
|
42
|
+
console.log(hadithPrompt.content); // Master + Hadith addon combined
|
|
43
|
+
|
|
44
|
+
// Get all available prompts
|
|
45
|
+
const allPrompts = getPrompts();
|
|
46
|
+
|
|
47
|
+
// Get list of prompt IDs for dropdowns
|
|
48
|
+
const ids = getPromptIds(); // ['master_prompt', 'hadith', 'fiqh', ...]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Validate LLM Output
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import {
|
|
55
|
+
validateTranslations,
|
|
56
|
+
detectArabicScript,
|
|
57
|
+
detectNewlineAfterId,
|
|
58
|
+
} from 'wobble-bibble';
|
|
59
|
+
|
|
60
|
+
const llmOutput = `P1234 - Translation of first segment
|
|
61
|
+
P1235 - Translation of second segment`;
|
|
62
|
+
|
|
63
|
+
// Full validation pipeline
|
|
64
|
+
const result = validateTranslations(llmOutput, ['P1234', 'P1235']);
|
|
65
|
+
if (!result.isValid) {
|
|
66
|
+
console.error('Error:', result.error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Individual detectors
|
|
70
|
+
const arabicWarnings = detectArabicScript(llmOutput); // Soft warnings
|
|
71
|
+
const newlineError = detectNewlineAfterId(llmOutput); // Hard error
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API Reference
|
|
75
|
+
|
|
76
|
+
### Prompts
|
|
77
|
+
|
|
78
|
+
| Function | Description |
|
|
79
|
+
|----------|-------------|
|
|
80
|
+
| `getPrompt(id)` | Get a specific stacked prompt by ID (strongly typed) |
|
|
81
|
+
| `getPrompts()` | Get all available stacked prompts |
|
|
82
|
+
| `getStackedPrompt(id)` | Get just the prompt content string |
|
|
83
|
+
| `getMasterPrompt()` | Get raw master prompt (for custom addons) |
|
|
84
|
+
| `getPromptIds()` | Get list of available prompt IDs |
|
|
85
|
+
| `stackPrompts(master, addon)` | Manually combine prompts |
|
|
86
|
+
|
|
87
|
+
### Validation (Hard Errors)
|
|
88
|
+
|
|
89
|
+
| Function | Description |
|
|
90
|
+
|----------|-------------|
|
|
91
|
+
| `validateTranslations(text, expectedIds)` | Full validation pipeline |
|
|
92
|
+
| `validateTranslationMarkers(text)` | Check for malformed IDs (e.g., `P123$4`) |
|
|
93
|
+
| `detectNewlineAfterId(text)` | Catch `P1234\nText` (Gemini bug) |
|
|
94
|
+
| `detectImplicitContinuation(text)` | Catch "implicit continuation" text |
|
|
95
|
+
| `detectMetaTalk(text)` | Catch "(Note:", "[Editor:" |
|
|
96
|
+
| `detectDuplicateIds(ids)` | Catch same ID appearing twice |
|
|
97
|
+
|
|
98
|
+
### Validation (Soft Warnings)
|
|
99
|
+
|
|
100
|
+
| Function | Description |
|
|
101
|
+
|----------|-------------|
|
|
102
|
+
| `detectArabicScript(text)` | Detect Arabic characters (except ﷺ) |
|
|
103
|
+
| `detectWrongDiacritics(text)` | Detect â/ã/á instead of macrons |
|
|
104
|
+
|
|
105
|
+
### Utilities
|
|
106
|
+
|
|
107
|
+
| Function | Description |
|
|
108
|
+
|----------|-------------|
|
|
109
|
+
| `extractTranslationIds(text)` | Extract all segment IDs from text |
|
|
110
|
+
| `normalizeTranslationText(text)` | Split merged markers onto separate lines |
|
|
111
|
+
| `findUnmatchedTranslationIds(ids, expected)` | Find IDs not in expected list |
|
|
112
|
+
| `formatExcerptsForPrompt(segments, prompt)` | Format segments for LLM input |
|
|
113
|
+
|
|
114
|
+
## Available Prompts
|
|
115
|
+
|
|
116
|
+
| ID | Name | Use Case |
|
|
117
|
+
|----|------|----------|
|
|
118
|
+
| `master_prompt` | Master Prompt | Universal grounding rules |
|
|
119
|
+
| `hadith` | Hadith | Isnad-heavy texts, Sharh |
|
|
120
|
+
| `fiqh` | Fiqh | Legal terminology |
|
|
121
|
+
| `tafsir` | Tafsir | Quranic exegesis |
|
|
122
|
+
| `fatawa` | Fatawa | Q&A format |
|
|
123
|
+
| `encyclopedia_mixed` | Encyclopedia Mixed | Polymath works |
|
|
124
|
+
| `jarh_wa_tadil` | Jarh Wa Tadil | Narrator criticism |
|
|
125
|
+
| `usul_al_fiqh` | Usul Al Fiqh | Legal methodology |
|
|
126
|
+
|
|
127
|
+
## Prompt Development
|
|
128
|
+
|
|
129
|
+
See [REFINEMENT_GUIDE.md](docs/refinement-guide.md) for the methodology used to develop and test these prompts.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
//#region src/constants.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Supported marker types for segments.
|
|
4
|
+
*/
|
|
5
|
+
declare enum Markers {
|
|
6
|
+
/** B - Book reference */
|
|
7
|
+
Book = "B",
|
|
8
|
+
/** F - Footnote reference */
|
|
9
|
+
Footnote = "F",
|
|
10
|
+
/** T - Heading reference */
|
|
11
|
+
Heading = "T",
|
|
12
|
+
/** C - Chapter reference */
|
|
13
|
+
Chapter = "C",
|
|
14
|
+
/** N - Note reference */
|
|
15
|
+
Note = "N",
|
|
16
|
+
/** P - Translation/Plain segment */
|
|
17
|
+
Plain = "P",
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Regex parts for building translation marker patterns.
|
|
21
|
+
*/
|
|
22
|
+
declare const TRANSLATION_MARKER_PARTS: {
|
|
23
|
+
/** Dash variations (hyphen, en dash, em dash) */
|
|
24
|
+
readonly dashes: "[-–—]";
|
|
25
|
+
/** Numeric portion of the reference */
|
|
26
|
+
readonly digits: "\\d+";
|
|
27
|
+
/** Valid marker prefixes (Book, Chapter, Footnote, Translation, Page) */
|
|
28
|
+
readonly markers: "[BCFTPN]";
|
|
29
|
+
/** Optional whitespace before dash */
|
|
30
|
+
readonly optionalSpace: "\\s?";
|
|
31
|
+
/** Valid single-letter suffixes */
|
|
32
|
+
readonly suffix: "[a-z]";
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Pattern for a segment ID (e.g., P1234, B45a).
|
|
36
|
+
*/
|
|
37
|
+
declare const MARKER_ID_PATTERN: string;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/formatting.d.ts
|
|
40
|
+
/**
|
|
41
|
+
* Internal segment type for formatting.
|
|
42
|
+
*/
|
|
43
|
+
type Segment = {
|
|
44
|
+
/** The segment ID (e.g., P1) */
|
|
45
|
+
id: string;
|
|
46
|
+
/** The segment text */
|
|
47
|
+
text: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Formats excerpts for an LLM prompt by combining the prompt rules with the segment text.
|
|
51
|
+
* Each segment is formatted as "ID - Text" and separated by double newlines.
|
|
52
|
+
*
|
|
53
|
+
* @param segments - Array of segments to format
|
|
54
|
+
* @param prompt - The instruction/system prompt to prepend
|
|
55
|
+
* @returns Combined prompt and formatted text
|
|
56
|
+
*/
|
|
57
|
+
declare const formatExcerptsForPrompt: (segments: Segment[], prompt: string) => string;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region .generated/prompts.d.ts
|
|
60
|
+
type PromptId = 'master_prompt' | 'encyclopedia_mixed' | 'fatawa' | 'fiqh' | 'hadith' | 'jarh_wa_tadil' | 'tafsir' | 'usul_al_fiqh';
|
|
61
|
+
declare const PROMPTS: readonly [{
|
|
62
|
+
readonly id: "master_prompt";
|
|
63
|
+
readonly name: "Master Prompt";
|
|
64
|
+
readonly content: "ROLE: Expert academic translator of Classical Islamic texts; prioritize accuracy and structure over fluency.\nCRITICAL NEGATIONS: 1. NO SANITIZATION (Do not soften polemics). 2. NO META-TALK (Output translation only). 3. NO MARKDOWN (Plain text only). 4. NO EMENDATION. 5. NO INFERENCE. 6. NO RESTRUCTURING. 7. NO OPAQUE TRANSLITERATION (Must translate phrases). 8. NO INVENTED SEGMENTS (Do not create, modify, or \"continue\" segment IDs. Output IDs verbatim exactly as they appear in the source input/metadata. Alphabetic suffixes (e.g., P5511a) are allowed IF AND ONLY IF that exact ID appears in the source. Any ID not present verbatim in the source is INVENTED. EXAMPLE: If P5803b ends with a questioner line, that line stays under P5803b — do NOT invent P5803c. If an expected ID is missing from the source, output: \"ID - [MISSING]\".)\nRULES: NO ARABIC SCRIPT (Except ﷺ). Plain text only. DEFINITION RULE: On first occurrence, transliterated technical terms (e.g., bidʿah) MUST be defined: \"translit (English)\". Preserve Segment ID. Translate meaning/intent. No inference. No extra fields. Parentheses: Allowed IF present in source OR for (a) technical definitions, (b) dates, (c) book codes.\nTRANSLITERATION & TERMS:\n1. SCHEME: Use full ALA-LC for explicit Arabic-script Person/Place/Book-Titles.\n - al-Casing: Lowercase al- mid-sentence; Capitalize after (al-Salafīyyah).\n - Book Titles: Transliterate only (do not translate meanings).\n2. TECHNICAL TERMS: On first occurrence, define: \"translit (English)\" (e.g., bidʿah (innovation), isnād (chain)).\n - Do NOT output multi-word transliterations without immediate English translation.\n3. STANDARDIZED TERMS: Use standard academic spellings: Muḥammad, Shaykh, Qurʾān, Islām, ḥadīth.\n - Sunnah (Capitalized) = The Corpus/Prophetic Tradition. sunnah (lowercase) = legal status/recommended.\n4. PROPER NAMES: Transliterate only (no parentheses).\n5. UNICODE: Latin + Latin Extended (āīūḥʿḍṣṭẓʾ) + punctuation. NO Arabic script (except ﷺ). NO emoji.\n - DIACRITIC FALLBACK: If you cannot produce correct ALA-LC diacritics, output English only. Do NOT use substitute accents (â/ã/á).\n6. SALUTATION: Replace all Prophet salutations with ﷺ.\n7. AMBIGUITY: Use contextual meaning from tafsir for theological terms. Do not sanitise polemics (e.g. Rāfiḍah).\nOUTPUT FORMAT: Segment_ID - English translation.\nCRITICAL: You must use the ASCII hyphen separator \" - \" (space+hyphen+space) immediately after the ID. Do NOT use em-dash or en-dash. Do NOT use a newline after the ID.\nMULTI-LINE SEGMENTS (e.g., internal Q&A): Output the Segment_ID and \" - \" ONLY ONCE on the first line. Do NOT repeat the Segment_ID on subsequent lines; subsequent lines must start directly with the speaker label/text (no \"ID - \" prefix).\nSEGMENT BOUNDARIES (Anti-hallucination): Start a NEW segment ONLY when the source explicitly provides a Segment_ID. If the source continues with extra lines (including speaker labels like \"Questioner:\"/\"The Shaykh:\"/\"السائل:\"/\"الشيخ:\") WITHOUT a new Segment_ID, treat them as part of the CURRENT segment (multi-line under the current Segment_ID). Do NOT invent a new ID (including alphabetic suffixes like \"P5803c\") to label such continuation.\nOUTPUT COMPLETENESS: Translate ALL content in EVERY segment. Do not truncate, summarize, or skip content. The \"…\" symbol in the source indicates an audio gap in the original recording — it is NOT an instruction to omit content. Every segment must be fully translated. If you cannot complete a segment, output \"ID - [INCOMPLETE]\" instead of just \"…\".\nOUTPUT UNIQUENESS: Each Segment_ID from the source must appear in your output EXACTLY ONCE as an \"ID - ...\" prefix. Do NOT output the same Segment_ID header twice. If a segment is long or has multiple speaker turns, continue translating under that single ID header without re-stating it.\nNEGATIVE CONSTRAINTS: Do NOT output \"implicit continuation\", summaries, or extra paragraphs. Output only the text present in the source segment.\nExample: P1234 - Translation text... (Correct) vs P1234\\nTranslation... (Forbidden).\nEXAMPLE: Input: P405 - حدثنا عبد الله بن يوسف... Output: P405 - ʿAbd Allāh b. Yūsuf narrated to us...";
|
|
65
|
+
}, {
|
|
66
|
+
readonly id: "encyclopedia_mixed";
|
|
67
|
+
readonly name: "Encyclopedia Mixed";
|
|
68
|
+
readonly content: "NO MODE TAGS: Do not output any mode labels or bracket tags.\nSTRUCTURE (Apply First):\n- Q&A: Whenever \"Al-Sāʾil:\"/\"Al-Shaykh:\" appear: Start NEW LINE for speaker. Keep Label+Text on SAME LINE.\n- EXCEPTION: If the speaker label is the VERY FIRST token after the \"ID - \" prefix, keep it on the same line. (Correct: P5455 - Questioner: Text...) (Wrong: P5455 \\n Questioner: Text...).\n- INTERNAL Q&A: If segment has multiple turns, use new lines for speakers. Output Segment ID ONLY ONCE at the start of the first line. Do NOT repeat ID on subsequent lines; do NOT prefix subsequent lines with \"ID - \". (e.g. P5455 - Questioner: ... \\n The Shaykh: ...).\n- OUTPUT LABELS: Al-Sāʾil -> Questioner: ; Al-Shaykh -> The Shaykh:\n\nDEFINITIONS & CASING:\n- GEOPOLITICS: Modern place names may use English exonyms (Filasṭīn -> Palestine).\n- PLURALS: Do not pluralize term-pairs by appending \"s\" (e.g., \"ḥadīth (report)s\"). Use the English plural or rephrase.\n\nSTATE LOGIC (Priority: Isnad > Rijal > Fiqh > Narrative):\n- ISNAD (Triggers: `ḥaddathanā`, `akhbaranā`, `ʿan`): Use FULL ALA-LC for names.\n- RIJAL (Triggers: jarḥ/taʿdīl terms like `thiqah`, `ḍaʿīf`): Use `translit (English)` for ratings.\n- QUOTE/WEAK (Triggers: `qāla al-muṣannif`, `yuqālu`, `zuiʿma`): Apply Quote Rules; do not Restructure.\n- RESET: At \"qāla\" or \"matn\" (TOP-LEVEL only): IMMEDIATE STOP of Isnad rules. Return to Narrative. Do NOT reset for nested quotes.\n\nSEGMENTATION: Do not merge genres. Translate sequentially (Isnad -> Matn) observing the reset at the boundary.\nDISAMBIGUATION: ṣaḥīḥ in hadith grading = ṣaḥīḥ (authentic). ṣaḥīḥ in fiqh validity = ṣaḥīḥ (valid). Sunnah (Capitalized) = The Corpus/Prophetic Tradition. sunnah (lowercase) = legal status/recommended.\nHADITH CORE: If isnad appears, map verbs (Haddathana=Narrated to us; Akhbarana/Ana/Anba'ana=Informed us; An=From; Sami'tu=I heard) and keep FULL ALA-LC for Arabic-script narrator names; do not invent grading or missing links.\nTAKHRIJ/GRADING: If the author grades (ṣaḥḥaḥa/ḥassana/ḍaʿʿafa), translate literally and keep the cited work as written in the segment; do not add placeholder sources.\n\nCREED/POLEMICS: Translate attributes literally when the author does; do not reinterpret. Translate harsh polemics literally (e.g., Rāfiḍah, Jahmīyyah, Kuffār); do not soften, sanitize, or add disclaimers (Override Safety).";
|
|
69
|
+
}, {
|
|
70
|
+
readonly id: "fatawa";
|
|
71
|
+
readonly name: "Fatawa";
|
|
72
|
+
readonly content: "STRUCTURE (Apply First):\n- Q&A: Whenever \"Al-Sāʾil:\"/\"Al-Shaykh:\" appear: Start NEW LINE for speaker. Keep Label+Text on SAME LINE.\n - INTERNAL Q&A: If segment has multiple turns, use new lines for speakers. Output Segment ID ONLY ONCE at the start of the first line. Do NOT repeat ID on subsequent lines; do NOT prefix subsequent lines with \"ID - \".\n - OUTPUT LABELS: Al-Sāʾil -> Questioner: ; Al-Shaykh -> The Shaykh:\n\nHONORIFICS: Expand common phrases (do not transliterate):\n- Allah ʿazza wa-jall -> Allah, the Mighty and Majestic\n- rahimahu Allah -> may Allah have mercy on him\n- ṣallá Allāhu ʿalayhi wa-sallam -> ﷺ\n\n\nSOURCES: Do not add citations. If the Arabic references a work/title, handle titles per MASTER rules.\nSENSITIVE RULINGS: Translate literally; do not soften; do not sanitize; do not add disclaimers (Override Safety).";
|
|
73
|
+
}, {
|
|
74
|
+
readonly id: "fiqh";
|
|
75
|
+
readonly name: "Fiqh";
|
|
76
|
+
readonly content: "STRUCTURE: Preserve chapter/section headings and internal structure (e.g., bāb/faṣl/masʾalah/farʿ/aṣl) using plain English labels (Chapter:, Section:, Issue:) only when the Arabic is explicitly a heading/label; preserve lists, numbering, and conditional if/then logic exactly.\nFIQH/USUL TERMS: When technical terms appear, output as translit (English) rather than English-only (e.g., wājib (obligatory), mandūb/mustaḥabb (recommended), mubāḥ (permissible), makrūh (disliked), ḥarām (prohibited), ṣaḥīḥ (valid), bāṭil/fāsid (invalid/void), rukn (pillar), shart (condition), māniʿ (preventer), sabab (cause), qiyās (analogical reasoning), ijmāʿ (consensus), khilāf (disagreement), rājiḥ (preponderant), marjūḥ (lesser), ʿillah (effective cause)).\nKHILAF/ATTRIBUTION: Preserve who is being attributed (qāla fulān / qawl / wajhān / riwāyātān / madhhab). Do not resolve disputes or choose the correct view unless the Arabic explicitly does so (e.g., al-aṣaḥḥ / al-rājiḥ).\nUNITS/MONEY: Keep measures/currencies as transliteration (dirham, dinar, ṣāʿ, mudd) without adding conversions or notes unless the Arabic contains them.";
|
|
77
|
+
}, {
|
|
78
|
+
readonly id: "hadith";
|
|
79
|
+
readonly name: "Hadith";
|
|
80
|
+
readonly content: "ISNAD VERBS: Haddathana=Narrated to us; Akhbarana=Informed us; An=From; Sami'tu=I heard; Ana (short for Akhbarana/Anba'ana in isnad)=Informed us (NOT \"I\").\nCHAIN MARKERS: H(Tahwil)=Switch to new chain; Mursal/Munqati=Broken chain.\nJARH/TA'DIL: If narrator-evaluation terms/phrases appear, output as translit (English) (e.g., fīhi naẓar (he needs to be looked into)); do not replace with only English.\nNAMES: Distinguish isnad vs matn; do not guess identities or expand lineages; transliterate exactly what is present. Book titles follow master rule.\nRUMUZ/CODES: If the segment contains book codes (kh/m/d/t/s/q/4), preserve them exactly; do not expand to book names.";
|
|
81
|
+
}, {
|
|
82
|
+
readonly id: "jarh_wa_tadil";
|
|
83
|
+
readonly name: "Jarh Wa Tadil";
|
|
84
|
+
readonly content: "GLOSSARY: When a jarh/ta'dil term/phrase appears, output as translit (English) (e.g., thiqah (trustworthy), ṣadūq (truthful), layyin (soft/lenient), ḍaʿīf (weak), matrūk (abandoned), kadhdhāb (liar), dajjāl (imposter), munkar al-ḥadīth (narrates denounced hadith)).\nRUMUZ: Preserve book codes in Latin exactly as in the segment (e.g., (kh) (m) (d t q) (4) (a)); do not expand unless the Arabic segment itself expands them.\nQALA: Translate as \"He said:\" and start a new line for each new critic.\nDATES: Use (d. 256 AH) or (born 194 AH).\nNO HARM: Translate \"There is no harm in him\"; no notes.\nPOLEMICS: Harsh terms (e.g., dajjāl, khabīth, rāfiḍī) must be translated literally; do not soften.";
|
|
85
|
+
}, {
|
|
86
|
+
readonly id: "tafsir";
|
|
87
|
+
readonly name: "Tafsir";
|
|
88
|
+
readonly content: "AYAH CITES: Do not output surah names unless the Arabic includes the name. Use [2:255]. If the segment contains quoted Qur'an text, translate it in braces: {…} [2:255].\nATTRIBUTES: Translate Allah’s attributes as the author intends; if the author is literal, keep literal (e.g., Hand, Face); do not add metaphorical reinterpretation unless the author does; mirror the author’s theology (Ash'ari vs Salafi) exactly.\nI'RAB TERMS: Mubtada=Subject; Khabar=Predicate; Fa'il=Agent/Doer; Maf'ul=Object.\nPROPHET NAMES: Use Arabic equivalents with ALA-LC diacritics (e.g., Mūsá, ʿĪsá, Dāwūd, Yūsuf).\nPOETRY: Preserve line breaks (one English line per Arabic line); no bullets; prioritize literal structure/grammar over rhyme.";
|
|
89
|
+
}, {
|
|
90
|
+
readonly id: "usul_al_fiqh";
|
|
91
|
+
readonly name: "Usul Al Fiqh";
|
|
92
|
+
readonly content: "STRUCTURE: Preserve the argument structure (claims, objections \"if it is said...\", replies \"we say...\", evidences, counter-evidences). Preserve explicit labels (faṣl, masʾalah, qāla, qīla, qulna) as plain English equivalents only when the Arabic is explicitly a label.\nUSUL TERMS: When technical terms appear, output as translit (English) (e.g., ʿāmm (general), khāṣṣ (specific), muṭlaq (absolute), muqayyad (restricted), amr (command), nahy (prohibition), ḥaqīqah (literal), majāz (figurative), mujmal (ambiguous), mubayyan (clarified), naṣṣ (explicit text), ẓāhir (apparent), mafhūm (implication), manṭūq (stated meaning), dalīl (evidence), qiyās (analogical reasoning), ʿillah (effective cause), sabab (cause), shart (condition), māniʿ (preventer), ijmāʿ (consensus), naskh (abrogation)).\nDISPUTE HANDLING: Do not resolve methodological disputes or harmonize schools unless the Arabic explicitly chooses (e.g., al-rājiḥ / al-aṣaḥḥ / ṣaḥīḥ). Preserve attribution to the madhhab/scholars as written.\nQUR'AN/HADITH: Keep verse references in the segment’s style; do not invent references. If a hadith isnad appears, follow MASTER isnad/name rules.";
|
|
93
|
+
}];
|
|
94
|
+
type PromptMetadata = (typeof PROMPTS)[number];
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/prompts.d.ts
|
|
97
|
+
/**
|
|
98
|
+
* A stacked prompt ready for use with an LLM.
|
|
99
|
+
*/
|
|
100
|
+
type StackedPrompt = {
|
|
101
|
+
/** Unique identifier */
|
|
102
|
+
id: PromptId;
|
|
103
|
+
/** Human-readable name */
|
|
104
|
+
name: string;
|
|
105
|
+
/** The full prompt content (master + addon if applicable) */
|
|
106
|
+
content: string;
|
|
107
|
+
/** Whether this is the master prompt (not stacked) */
|
|
108
|
+
isMaster: boolean;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Stacks a master prompt with a specialized addon prompt.
|
|
112
|
+
*
|
|
113
|
+
* @param master - The master/base prompt
|
|
114
|
+
* @param addon - The specialized addon prompt
|
|
115
|
+
* @returns Combined prompt text
|
|
116
|
+
*/
|
|
117
|
+
declare const stackPrompts: (master: string, addon: string) => string;
|
|
118
|
+
/**
|
|
119
|
+
* Gets all available prompts as stacked prompts (master + addon combined).
|
|
120
|
+
* Master prompt is returned as-is, addon prompts are stacked with master.
|
|
121
|
+
*
|
|
122
|
+
* @returns Array of all stacked prompts
|
|
123
|
+
*/
|
|
124
|
+
declare const getPrompts: () => StackedPrompt[];
|
|
125
|
+
/**
|
|
126
|
+
* Gets a specific prompt by ID (strongly typed).
|
|
127
|
+
* Returns the stacked version (master + addon) for addon prompts.
|
|
128
|
+
*
|
|
129
|
+
* @param id - The prompt ID to retrieve
|
|
130
|
+
* @returns The stacked prompt
|
|
131
|
+
* @throws Error if prompt ID is not found
|
|
132
|
+
*/
|
|
133
|
+
declare const getPrompt: (id: PromptId) => StackedPrompt;
|
|
134
|
+
/**
|
|
135
|
+
* Gets the raw stacked prompt text for a specific prompt ID.
|
|
136
|
+
* Convenience method for when you just need the text.
|
|
137
|
+
*
|
|
138
|
+
* @param id - The prompt ID
|
|
139
|
+
* @returns The stacked prompt content string
|
|
140
|
+
*/
|
|
141
|
+
declare const getStackedPrompt: (id: PromptId) => string;
|
|
142
|
+
/**
|
|
143
|
+
* Gets the list of available prompt IDs.
|
|
144
|
+
* Useful for UI dropdowns or validation.
|
|
145
|
+
*
|
|
146
|
+
* @returns Array of prompt IDs
|
|
147
|
+
*/
|
|
148
|
+
declare const getPromptIds: () => PromptId[];
|
|
149
|
+
/**
|
|
150
|
+
* Gets just the master prompt content.
|
|
151
|
+
* Useful when you need to use a custom addon.
|
|
152
|
+
*
|
|
153
|
+
* @returns The master prompt content
|
|
154
|
+
*/
|
|
155
|
+
declare const getMasterPrompt: () => string;
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/validation.d.ts
|
|
158
|
+
/**
|
|
159
|
+
* Warning types for soft validation issues
|
|
160
|
+
*/
|
|
161
|
+
type ValidationWarningType = 'arabic_leak' | 'wrong_diacritics';
|
|
162
|
+
/**
|
|
163
|
+
* A soft validation warning (not a hard error)
|
|
164
|
+
*/
|
|
165
|
+
type ValidationWarning = {
|
|
166
|
+
/** The type of warning */
|
|
167
|
+
type: ValidationWarningType;
|
|
168
|
+
/** Human-readable warning message */
|
|
169
|
+
message: string;
|
|
170
|
+
/** The offending text match */
|
|
171
|
+
match?: string;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Result of translation validation
|
|
175
|
+
*/
|
|
176
|
+
type TranslationValidationResult = {
|
|
177
|
+
/** Whether validation passed */
|
|
178
|
+
isValid: boolean;
|
|
179
|
+
/** Error message if validation failed */
|
|
180
|
+
error?: string;
|
|
181
|
+
/** Normalized/fixed text (with merged markers split onto separate lines) */
|
|
182
|
+
normalizedText: string;
|
|
183
|
+
/** List of parsed translation IDs in order */
|
|
184
|
+
parsedIds: string[];
|
|
185
|
+
/** Soft warnings (issues that don't fail validation) */
|
|
186
|
+
warnings?: ValidationWarning[];
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Detects Arabic script in text (except allowed ﷺ symbol).
|
|
190
|
+
* This is a SOFT warning - Arabic leak is bad but not a hard failure.
|
|
191
|
+
*
|
|
192
|
+
* @param text - The text to scan for Arabic script
|
|
193
|
+
* @returns Array of validation warnings if Arabic is found
|
|
194
|
+
*/
|
|
195
|
+
declare const detectArabicScript: (text: string) => ValidationWarning[];
|
|
196
|
+
/**
|
|
197
|
+
* Detects wrong diacritics (â/ã/á instead of correct macrons ā/ī/ū).
|
|
198
|
+
* This is a SOFT warning - wrong diacritics are bad but not a hard failure.
|
|
199
|
+
*
|
|
200
|
+
* @param text - The text to scan for incorrect diacritics
|
|
201
|
+
* @returns Array of validation warnings if wrong diacritics are found
|
|
202
|
+
*/
|
|
203
|
+
declare const detectWrongDiacritics: (text: string) => ValidationWarning[];
|
|
204
|
+
/**
|
|
205
|
+
* Detects newline immediately after segment ID (the "Gemini bug").
|
|
206
|
+
* Format should be "P1234 - Text" not "P1234\nText".
|
|
207
|
+
*
|
|
208
|
+
* @param text - The text to validate
|
|
209
|
+
* @returns Error message if bug is detected, otherwise undefined
|
|
210
|
+
*/
|
|
211
|
+
declare const detectNewlineAfterId: (text: string) => string | undefined;
|
|
212
|
+
/**
|
|
213
|
+
* Detects implicit continuation text that LLMs add when hallucinating.
|
|
214
|
+
*
|
|
215
|
+
* @param text - The text to scan for continuation markers
|
|
216
|
+
* @returns Error message if continuation text is found, otherwise undefined
|
|
217
|
+
*/
|
|
218
|
+
declare const detectImplicitContinuation: (text: string) => string | undefined;
|
|
219
|
+
/**
|
|
220
|
+
* Detects meta-talk (translator notes, editor comments) that violate NO META-TALK.
|
|
221
|
+
*
|
|
222
|
+
* @param text - The text to scan for meta-talk
|
|
223
|
+
* @returns Error message if meta-talk is found, otherwise undefined
|
|
224
|
+
*/
|
|
225
|
+
declare const detectMetaTalk: (text: string) => string | undefined;
|
|
226
|
+
/**
|
|
227
|
+
* Detects duplicate segment IDs in the output.
|
|
228
|
+
*
|
|
229
|
+
* @param ids - List of IDs extracted from the translation
|
|
230
|
+
* @returns Error message if duplicates are found, otherwise undefined
|
|
231
|
+
*/
|
|
232
|
+
declare const detectDuplicateIds: (ids: string[]) => string | undefined;
|
|
233
|
+
/**
|
|
234
|
+
* Detects IDs in the output that were not in the source (invented/hallucinated IDs).
|
|
235
|
+
* @param outputIds - IDs extracted from LLM output
|
|
236
|
+
* @param sourceIds - IDs that were present in the source input
|
|
237
|
+
* @returns Error message if invented IDs found, undefined if all IDs are valid
|
|
238
|
+
*/
|
|
239
|
+
declare const detectInventedIds: (outputIds: string[], sourceIds: string[]) => string | undefined;
|
|
240
|
+
/**
|
|
241
|
+
* Detects segments that appear truncated (just "…" or very short with no real content).
|
|
242
|
+
* @param text - The full LLM output text
|
|
243
|
+
* @returns Error message if truncated segments found, undefined if all segments have content
|
|
244
|
+
*/
|
|
245
|
+
declare const detectTruncatedSegments: (text: string) => string | undefined;
|
|
246
|
+
/**
|
|
247
|
+
* Validates translation marker format and returns error message if invalid.
|
|
248
|
+
* Catches common AI hallucinations like malformed reference IDs.
|
|
249
|
+
*
|
|
250
|
+
* @param text - Raw translation text to validate
|
|
251
|
+
* @returns Error message if invalid, undefined if valid
|
|
252
|
+
*/
|
|
253
|
+
declare const validateTranslationMarkers: (text: string) => string | undefined;
|
|
254
|
+
/**
|
|
255
|
+
* Normalizes translation text by splitting merged markers onto separate lines.
|
|
256
|
+
* LLMs sometimes put multiple translations on the same line.
|
|
257
|
+
*
|
|
258
|
+
* @param content - Raw translation text
|
|
259
|
+
* @returns Normalized text with each marker on its own line
|
|
260
|
+
*/
|
|
261
|
+
declare const normalizeTranslationText: (content: string) => string;
|
|
262
|
+
/**
|
|
263
|
+
* Extracts translation IDs from text in order of appearance.
|
|
264
|
+
*
|
|
265
|
+
* @param text - Translation text
|
|
266
|
+
* @returns Array of IDs in order
|
|
267
|
+
*/
|
|
268
|
+
declare const extractTranslationIds: (text: string) => string[];
|
|
269
|
+
/**
|
|
270
|
+
* Extracts the numeric portion from an excerpt ID.
|
|
271
|
+
* E.g., "P11622a" -> 11622, "C123" -> 123, "B45b" -> 45
|
|
272
|
+
*
|
|
273
|
+
* @param id - Excerpt ID
|
|
274
|
+
* @returns Numeric portion of the ID
|
|
275
|
+
*/
|
|
276
|
+
declare const extractIdNumber: (id: string) => number;
|
|
277
|
+
/**
|
|
278
|
+
* Extracts the prefix (type) from an excerpt ID.
|
|
279
|
+
* E.g., "P11622a" -> "P", "C123" -> "C", "B45" -> "B"
|
|
280
|
+
*
|
|
281
|
+
* @param id - Excerpt ID
|
|
282
|
+
* @returns Single character prefix
|
|
283
|
+
*/
|
|
284
|
+
declare const extractIdPrefix: (id: string) => string;
|
|
285
|
+
/**
|
|
286
|
+
* Validates that translation IDs appear in ascending numeric order within the same prefix type.
|
|
287
|
+
* This catches LLM errors where translations are output in wrong order (e.g., P12659 before P12651).
|
|
288
|
+
*
|
|
289
|
+
* @param translationIds - IDs from pasted translations
|
|
290
|
+
* @returns Error message if order issue detected, undefined if valid
|
|
291
|
+
*/
|
|
292
|
+
declare const validateNumericOrder: (translationIds: string[]) => string | undefined;
|
|
293
|
+
/**
|
|
294
|
+
* Validates translation order against expected excerpt order from the store.
|
|
295
|
+
* Allows pasting in multiple blocks where each block is internally ordered.
|
|
296
|
+
* Resets (position going backwards) are allowed between blocks.
|
|
297
|
+
* Errors only when there's disorder WITHIN a block (going backwards then forwards).
|
|
298
|
+
*
|
|
299
|
+
* @param translationIds - IDs from pasted translations
|
|
300
|
+
* @param expectedIds - IDs from store excerpts/headings/footnotes in order
|
|
301
|
+
* @returns Error message if order issue detected, undefined if valid
|
|
302
|
+
*/
|
|
303
|
+
declare const validateTranslationOrder: (translationIds: string[], expectedIds: string[]) => string | undefined;
|
|
304
|
+
/**
|
|
305
|
+
* Performs comprehensive validation on translation text.
|
|
306
|
+
* Validates markers, normalizes text, and checks order against expected IDs.
|
|
307
|
+
*
|
|
308
|
+
* @param rawText - Raw translation text from user input
|
|
309
|
+
* @param expectedIds - Expected IDs from store (excerpts + headings + footnotes)
|
|
310
|
+
* @returns Validation result with normalized text and any errors
|
|
311
|
+
*/
|
|
312
|
+
declare const validateTranslations: (rawText: string, expectedIds: string[]) => TranslationValidationResult;
|
|
313
|
+
/**
|
|
314
|
+
* Finds translation IDs that don't exist in the expected store IDs.
|
|
315
|
+
* Used to validate that all pasted translations can be matched before committing.
|
|
316
|
+
*
|
|
317
|
+
* @param translationIds - IDs from parsed translations
|
|
318
|
+
* @param expectedIds - IDs from store (excerpts + headings + footnotes)
|
|
319
|
+
* @returns Array of IDs that exist in translations but not in the store
|
|
320
|
+
*/
|
|
321
|
+
declare const findUnmatchedTranslationIds: (translationIds: string[], expectedIds: string[]) => string[];
|
|
322
|
+
//#endregion
|
|
323
|
+
export { MARKER_ID_PATTERN, Markers, type PromptId, type PromptMetadata, type StackedPrompt, TRANSLATION_MARKER_PARTS, type TranslationValidationResult, type ValidationWarning, type ValidationWarningType, detectArabicScript, detectDuplicateIds, detectImplicitContinuation, detectInventedIds, detectMetaTalk, detectNewlineAfterId, detectTruncatedSegments, detectWrongDiacritics, extractIdNumber, extractIdPrefix, extractTranslationIds, findUnmatchedTranslationIds, formatExcerptsForPrompt, getMasterPrompt, getPrompt, getPromptIds, getPrompts, getStackedPrompt, normalizeTranslationText, stackPrompts, validateNumericOrder, validateTranslationMarkers, validateTranslationOrder, validateTranslations };
|
|
324
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
//#region src/constants.ts
|
|
2
|
+
/**
|
|
3
|
+
* Supported marker types for segments.
|
|
4
|
+
*/
|
|
5
|
+
let Markers = /* @__PURE__ */ function(Markers$1) {
|
|
6
|
+
/** B - Book reference */
|
|
7
|
+
Markers$1["Book"] = "B";
|
|
8
|
+
/** F - Footnote reference */
|
|
9
|
+
Markers$1["Footnote"] = "F";
|
|
10
|
+
/** T - Heading reference */
|
|
11
|
+
Markers$1["Heading"] = "T";
|
|
12
|
+
/** C - Chapter reference */
|
|
13
|
+
Markers$1["Chapter"] = "C";
|
|
14
|
+
/** N - Note reference */
|
|
15
|
+
Markers$1["Note"] = "N";
|
|
16
|
+
/** P - Translation/Plain segment */
|
|
17
|
+
Markers$1["Plain"] = "P";
|
|
18
|
+
return Markers$1;
|
|
19
|
+
}({});
|
|
20
|
+
/**
|
|
21
|
+
* Regex parts for building translation marker patterns.
|
|
22
|
+
*/
|
|
23
|
+
const TRANSLATION_MARKER_PARTS = {
|
|
24
|
+
dashes: "[-–—]",
|
|
25
|
+
digits: "\\d+",
|
|
26
|
+
markers: `[${Markers.Book}${Markers.Chapter}${Markers.Footnote}${Markers.Heading}${Markers.Plain}${Markers.Note}]`,
|
|
27
|
+
optionalSpace: "\\s?",
|
|
28
|
+
suffix: "[a-z]"
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Pattern for a segment ID (e.g., P1234, B45a).
|
|
32
|
+
*/
|
|
33
|
+
const MARKER_ID_PATTERN = `${TRANSLATION_MARKER_PARTS.markers}${TRANSLATION_MARKER_PARTS.digits}${TRANSLATION_MARKER_PARTS.suffix}?`;
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/formatting.ts
|
|
37
|
+
/**
|
|
38
|
+
* Formats excerpts for an LLM prompt by combining the prompt rules with the segment text.
|
|
39
|
+
* Each segment is formatted as "ID - Text" and separated by double newlines.
|
|
40
|
+
*
|
|
41
|
+
* @param segments - Array of segments to format
|
|
42
|
+
* @param prompt - The instruction/system prompt to prepend
|
|
43
|
+
* @returns Combined prompt and formatted text
|
|
44
|
+
*/
|
|
45
|
+
const formatExcerptsForPrompt = (segments, prompt) => {
|
|
46
|
+
return [prompt, segments.map((e) => `${e.id} - ${e.text}`).join("\n\n")].join("\n\n");
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region .generated/prompts.ts
|
|
51
|
+
const MASTER_PROMPT = "ROLE: Expert academic translator of Classical Islamic texts; prioritize accuracy and structure over fluency.\nCRITICAL NEGATIONS: 1. NO SANITIZATION (Do not soften polemics). 2. NO META-TALK (Output translation only). 3. NO MARKDOWN (Plain text only). 4. NO EMENDATION. 5. NO INFERENCE. 6. NO RESTRUCTURING. 7. NO OPAQUE TRANSLITERATION (Must translate phrases). 8. NO INVENTED SEGMENTS (Do not create, modify, or \"continue\" segment IDs. Output IDs verbatim exactly as they appear in the source input/metadata. Alphabetic suffixes (e.g., P5511a) are allowed IF AND ONLY IF that exact ID appears in the source. Any ID not present verbatim in the source is INVENTED. EXAMPLE: If P5803b ends with a questioner line, that line stays under P5803b — do NOT invent P5803c. If an expected ID is missing from the source, output: \"ID - [MISSING]\".)\nRULES: NO ARABIC SCRIPT (Except ﷺ). Plain text only. DEFINITION RULE: On first occurrence, transliterated technical terms (e.g., bidʿah) MUST be defined: \"translit (English)\". Preserve Segment ID. Translate meaning/intent. No inference. No extra fields. Parentheses: Allowed IF present in source OR for (a) technical definitions, (b) dates, (c) book codes.\nTRANSLITERATION & TERMS:\n1. SCHEME: Use full ALA-LC for explicit Arabic-script Person/Place/Book-Titles.\n - al-Casing: Lowercase al- mid-sentence; Capitalize after (al-Salafīyyah).\n - Book Titles: Transliterate only (do not translate meanings).\n2. TECHNICAL TERMS: On first occurrence, define: \"translit (English)\" (e.g., bidʿah (innovation), isnād (chain)).\n - Do NOT output multi-word transliterations without immediate English translation.\n3. STANDARDIZED TERMS: Use standard academic spellings: Muḥammad, Shaykh, Qurʾān, Islām, ḥadīth.\n - Sunnah (Capitalized) = The Corpus/Prophetic Tradition. sunnah (lowercase) = legal status/recommended.\n4. PROPER NAMES: Transliterate only (no parentheses).\n5. UNICODE: Latin + Latin Extended (āīūḥʿḍṣṭẓʾ) + punctuation. NO Arabic script (except ﷺ). NO emoji.\n - DIACRITIC FALLBACK: If you cannot produce correct ALA-LC diacritics, output English only. Do NOT use substitute accents (â/ã/á).\n6. SALUTATION: Replace all Prophet salutations with ﷺ.\n7. AMBIGUITY: Use contextual meaning from tafsir for theological terms. Do not sanitise polemics (e.g. Rāfiḍah).\nOUTPUT FORMAT: Segment_ID - English translation.\nCRITICAL: You must use the ASCII hyphen separator \" - \" (space+hyphen+space) immediately after the ID. Do NOT use em-dash or en-dash. Do NOT use a newline after the ID.\nMULTI-LINE SEGMENTS (e.g., internal Q&A): Output the Segment_ID and \" - \" ONLY ONCE on the first line. Do NOT repeat the Segment_ID on subsequent lines; subsequent lines must start directly with the speaker label/text (no \"ID - \" prefix).\nSEGMENT BOUNDARIES (Anti-hallucination): Start a NEW segment ONLY when the source explicitly provides a Segment_ID. If the source continues with extra lines (including speaker labels like \"Questioner:\"/\"The Shaykh:\"/\"السائل:\"/\"الشيخ:\") WITHOUT a new Segment_ID, treat them as part of the CURRENT segment (multi-line under the current Segment_ID). Do NOT invent a new ID (including alphabetic suffixes like \"P5803c\") to label such continuation.\nOUTPUT COMPLETENESS: Translate ALL content in EVERY segment. Do not truncate, summarize, or skip content. The \"…\" symbol in the source indicates an audio gap in the original recording — it is NOT an instruction to omit content. Every segment must be fully translated. If you cannot complete a segment, output \"ID - [INCOMPLETE]\" instead of just \"…\".\nOUTPUT UNIQUENESS: Each Segment_ID from the source must appear in your output EXACTLY ONCE as an \"ID - ...\" prefix. Do NOT output the same Segment_ID header twice. If a segment is long or has multiple speaker turns, continue translating under that single ID header without re-stating it.\nNEGATIVE CONSTRAINTS: Do NOT output \"implicit continuation\", summaries, or extra paragraphs. Output only the text present in the source segment.\nExample: P1234 - Translation text... (Correct) vs P1234\\nTranslation... (Forbidden).\nEXAMPLE: Input: P405 - حدثنا عبد الله بن يوسف... Output: P405 - ʿAbd Allāh b. Yūsuf narrated to us...";
|
|
52
|
+
const ENCYCLOPEDIA_MIXED = "NO MODE TAGS: Do not output any mode labels or bracket tags.\nSTRUCTURE (Apply First):\n- Q&A: Whenever \"Al-Sāʾil:\"/\"Al-Shaykh:\" appear: Start NEW LINE for speaker. Keep Label+Text on SAME LINE.\n- EXCEPTION: If the speaker label is the VERY FIRST token after the \"ID - \" prefix, keep it on the same line. (Correct: P5455 - Questioner: Text...) (Wrong: P5455 \\n Questioner: Text...).\n- INTERNAL Q&A: If segment has multiple turns, use new lines for speakers. Output Segment ID ONLY ONCE at the start of the first line. Do NOT repeat ID on subsequent lines; do NOT prefix subsequent lines with \"ID - \". (e.g. P5455 - Questioner: ... \\n The Shaykh: ...).\n- OUTPUT LABELS: Al-Sāʾil -> Questioner: ; Al-Shaykh -> The Shaykh:\n\nDEFINITIONS & CASING:\n- GEOPOLITICS: Modern place names may use English exonyms (Filasṭīn -> Palestine).\n- PLURALS: Do not pluralize term-pairs by appending \"s\" (e.g., \"ḥadīth (report)s\"). Use the English plural or rephrase.\n\nSTATE LOGIC (Priority: Isnad > Rijal > Fiqh > Narrative):\n- ISNAD (Triggers: `ḥaddathanā`, `akhbaranā`, `ʿan`): Use FULL ALA-LC for names.\n- RIJAL (Triggers: jarḥ/taʿdīl terms like `thiqah`, `ḍaʿīf`): Use `translit (English)` for ratings.\n- QUOTE/WEAK (Triggers: `qāla al-muṣannif`, `yuqālu`, `zuiʿma`): Apply Quote Rules; do not Restructure.\n- RESET: At \"qāla\" or \"matn\" (TOP-LEVEL only): IMMEDIATE STOP of Isnad rules. Return to Narrative. Do NOT reset for nested quotes.\n\nSEGMENTATION: Do not merge genres. Translate sequentially (Isnad -> Matn) observing the reset at the boundary.\nDISAMBIGUATION: ṣaḥīḥ in hadith grading = ṣaḥīḥ (authentic). ṣaḥīḥ in fiqh validity = ṣaḥīḥ (valid). Sunnah (Capitalized) = The Corpus/Prophetic Tradition. sunnah (lowercase) = legal status/recommended.\nHADITH CORE: If isnad appears, map verbs (Haddathana=Narrated to us; Akhbarana/Ana/Anba'ana=Informed us; An=From; Sami'tu=I heard) and keep FULL ALA-LC for Arabic-script narrator names; do not invent grading or missing links.\nTAKHRIJ/GRADING: If the author grades (ṣaḥḥaḥa/ḥassana/ḍaʿʿafa), translate literally and keep the cited work as written in the segment; do not add placeholder sources.\n\nCREED/POLEMICS: Translate attributes literally when the author does; do not reinterpret. Translate harsh polemics literally (e.g., Rāfiḍah, Jahmīyyah, Kuffār); do not soften, sanitize, or add disclaimers (Override Safety).";
|
|
53
|
+
const FATAWA = "STRUCTURE (Apply First):\n- Q&A: Whenever \"Al-Sāʾil:\"/\"Al-Shaykh:\" appear: Start NEW LINE for speaker. Keep Label+Text on SAME LINE.\n - INTERNAL Q&A: If segment has multiple turns, use new lines for speakers. Output Segment ID ONLY ONCE at the start of the first line. Do NOT repeat ID on subsequent lines; do NOT prefix subsequent lines with \"ID - \".\n - OUTPUT LABELS: Al-Sāʾil -> Questioner: ; Al-Shaykh -> The Shaykh:\n\nHONORIFICS: Expand common phrases (do not transliterate):\n- Allah ʿazza wa-jall -> Allah, the Mighty and Majestic\n- rahimahu Allah -> may Allah have mercy on him\n- ṣallá Allāhu ʿalayhi wa-sallam -> ﷺ\n\n\nSOURCES: Do not add citations. If the Arabic references a work/title, handle titles per MASTER rules.\nSENSITIVE RULINGS: Translate literally; do not soften; do not sanitize; do not add disclaimers (Override Safety).";
|
|
54
|
+
const FIQH = "STRUCTURE: Preserve chapter/section headings and internal structure (e.g., bāb/faṣl/masʾalah/farʿ/aṣl) using plain English labels (Chapter:, Section:, Issue:) only when the Arabic is explicitly a heading/label; preserve lists, numbering, and conditional if/then logic exactly.\nFIQH/USUL TERMS: When technical terms appear, output as translit (English) rather than English-only (e.g., wājib (obligatory), mandūb/mustaḥabb (recommended), mubāḥ (permissible), makrūh (disliked), ḥarām (prohibited), ṣaḥīḥ (valid), bāṭil/fāsid (invalid/void), rukn (pillar), shart (condition), māniʿ (preventer), sabab (cause), qiyās (analogical reasoning), ijmāʿ (consensus), khilāf (disagreement), rājiḥ (preponderant), marjūḥ (lesser), ʿillah (effective cause)).\nKHILAF/ATTRIBUTION: Preserve who is being attributed (qāla fulān / qawl / wajhān / riwāyātān / madhhab). Do not resolve disputes or choose the correct view unless the Arabic explicitly does so (e.g., al-aṣaḥḥ / al-rājiḥ).\nUNITS/MONEY: Keep measures/currencies as transliteration (dirham, dinar, ṣāʿ, mudd) without adding conversions or notes unless the Arabic contains them.";
|
|
55
|
+
const HADITH = "ISNAD VERBS: Haddathana=Narrated to us; Akhbarana=Informed us; An=From; Sami'tu=I heard; Ana (short for Akhbarana/Anba'ana in isnad)=Informed us (NOT \"I\").\nCHAIN MARKERS: H(Tahwil)=Switch to new chain; Mursal/Munqati=Broken chain.\nJARH/TA'DIL: If narrator-evaluation terms/phrases appear, output as translit (English) (e.g., fīhi naẓar (he needs to be looked into)); do not replace with only English.\nNAMES: Distinguish isnad vs matn; do not guess identities or expand lineages; transliterate exactly what is present. Book titles follow master rule.\nRUMUZ/CODES: If the segment contains book codes (kh/m/d/t/s/q/4), preserve them exactly; do not expand to book names.";
|
|
56
|
+
const JARH_WA_TADIL = "GLOSSARY: When a jarh/ta'dil term/phrase appears, output as translit (English) (e.g., thiqah (trustworthy), ṣadūq (truthful), layyin (soft/lenient), ḍaʿīf (weak), matrūk (abandoned), kadhdhāb (liar), dajjāl (imposter), munkar al-ḥadīth (narrates denounced hadith)).\nRUMUZ: Preserve book codes in Latin exactly as in the segment (e.g., (kh) (m) (d t q) (4) (a)); do not expand unless the Arabic segment itself expands them.\nQALA: Translate as \"He said:\" and start a new line for each new critic.\nDATES: Use (d. 256 AH) or (born 194 AH).\nNO HARM: Translate \"There is no harm in him\"; no notes.\nPOLEMICS: Harsh terms (e.g., dajjāl, khabīth, rāfiḍī) must be translated literally; do not soften.";
|
|
57
|
+
const TAFSIR = "AYAH CITES: Do not output surah names unless the Arabic includes the name. Use [2:255]. If the segment contains quoted Qur'an text, translate it in braces: {…} [2:255].\nATTRIBUTES: Translate Allah’s attributes as the author intends; if the author is literal, keep literal (e.g., Hand, Face); do not add metaphorical reinterpretation unless the author does; mirror the author’s theology (Ash'ari vs Salafi) exactly.\nI'RAB TERMS: Mubtada=Subject; Khabar=Predicate; Fa'il=Agent/Doer; Maf'ul=Object.\nPROPHET NAMES: Use Arabic equivalents with ALA-LC diacritics (e.g., Mūsá, ʿĪsá, Dāwūd, Yūsuf).\nPOETRY: Preserve line breaks (one English line per Arabic line); no bullets; prioritize literal structure/grammar over rhyme.";
|
|
58
|
+
const USUL_AL_FIQH = "STRUCTURE: Preserve the argument structure (claims, objections \"if it is said...\", replies \"we say...\", evidences, counter-evidences). Preserve explicit labels (faṣl, masʾalah, qāla, qīla, qulna) as plain English equivalents only when the Arabic is explicitly a label.\nUSUL TERMS: When technical terms appear, output as translit (English) (e.g., ʿāmm (general), khāṣṣ (specific), muṭlaq (absolute), muqayyad (restricted), amr (command), nahy (prohibition), ḥaqīqah (literal), majāz (figurative), mujmal (ambiguous), mubayyan (clarified), naṣṣ (explicit text), ẓāhir (apparent), mafhūm (implication), manṭūq (stated meaning), dalīl (evidence), qiyās (analogical reasoning), ʿillah (effective cause), sabab (cause), shart (condition), māniʿ (preventer), ijmāʿ (consensus), naskh (abrogation)).\nDISPUTE HANDLING: Do not resolve methodological disputes or harmonize schools unless the Arabic explicitly chooses (e.g., al-rājiḥ / al-aṣaḥḥ / ṣaḥīḥ). Preserve attribution to the madhhab/scholars as written.\nQUR'AN/HADITH: Keep verse references in the segment’s style; do not invent references. If a hadith isnad appears, follow MASTER isnad/name rules.";
|
|
59
|
+
const PROMPTS = [
|
|
60
|
+
{
|
|
61
|
+
id: "master_prompt",
|
|
62
|
+
name: "Master Prompt",
|
|
63
|
+
content: MASTER_PROMPT
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "encyclopedia_mixed",
|
|
67
|
+
name: "Encyclopedia Mixed",
|
|
68
|
+
content: ENCYCLOPEDIA_MIXED
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "fatawa",
|
|
72
|
+
name: "Fatawa",
|
|
73
|
+
content: FATAWA
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "fiqh",
|
|
77
|
+
name: "Fiqh",
|
|
78
|
+
content: FIQH
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "hadith",
|
|
82
|
+
name: "Hadith",
|
|
83
|
+
content: HADITH
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "jarh_wa_tadil",
|
|
87
|
+
name: "Jarh Wa Tadil",
|
|
88
|
+
content: JARH_WA_TADIL
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "tafsir",
|
|
92
|
+
name: "Tafsir",
|
|
93
|
+
content: TAFSIR
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "usul_al_fiqh",
|
|
97
|
+
name: "Usul Al Fiqh",
|
|
98
|
+
content: USUL_AL_FIQH
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/prompts.ts
|
|
104
|
+
/**
|
|
105
|
+
* Stacks a master prompt with a specialized addon prompt.
|
|
106
|
+
*
|
|
107
|
+
* @param master - The master/base prompt
|
|
108
|
+
* @param addon - The specialized addon prompt
|
|
109
|
+
* @returns Combined prompt text
|
|
110
|
+
*/
|
|
111
|
+
const stackPrompts = (master, addon) => {
|
|
112
|
+
if (!master) return addon;
|
|
113
|
+
if (!addon) return master;
|
|
114
|
+
return `${master}\n${addon}`;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Gets all available prompts as stacked prompts (master + addon combined).
|
|
118
|
+
* Master prompt is returned as-is, addon prompts are stacked with master.
|
|
119
|
+
*
|
|
120
|
+
* @returns Array of all stacked prompts
|
|
121
|
+
*/
|
|
122
|
+
const getPrompts = () => {
|
|
123
|
+
return PROMPTS.map((prompt) => ({
|
|
124
|
+
content: prompt.id === "master_prompt" ? prompt.content : stackPrompts(MASTER_PROMPT, prompt.content),
|
|
125
|
+
id: prompt.id,
|
|
126
|
+
isMaster: prompt.id === "master_prompt",
|
|
127
|
+
name: prompt.name
|
|
128
|
+
}));
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Gets a specific prompt by ID (strongly typed).
|
|
132
|
+
* Returns the stacked version (master + addon) for addon prompts.
|
|
133
|
+
*
|
|
134
|
+
* @param id - The prompt ID to retrieve
|
|
135
|
+
* @returns The stacked prompt
|
|
136
|
+
* @throws Error if prompt ID is not found
|
|
137
|
+
*/
|
|
138
|
+
const getPrompt = (id) => {
|
|
139
|
+
const prompt = PROMPTS.find((p) => p.id === id);
|
|
140
|
+
if (!prompt) throw new Error(`Prompt not found: ${id}`);
|
|
141
|
+
return {
|
|
142
|
+
content: prompt.id === "master_prompt" ? prompt.content : stackPrompts(MASTER_PROMPT, prompt.content),
|
|
143
|
+
id: prompt.id,
|
|
144
|
+
isMaster: prompt.id === "master_prompt",
|
|
145
|
+
name: prompt.name
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Gets the raw stacked prompt text for a specific prompt ID.
|
|
150
|
+
* Convenience method for when you just need the text.
|
|
151
|
+
*
|
|
152
|
+
* @param id - The prompt ID
|
|
153
|
+
* @returns The stacked prompt content string
|
|
154
|
+
*/
|
|
155
|
+
const getStackedPrompt = (id) => {
|
|
156
|
+
return getPrompt(id).content;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Gets the list of available prompt IDs.
|
|
160
|
+
* Useful for UI dropdowns or validation.
|
|
161
|
+
*
|
|
162
|
+
* @returns Array of prompt IDs
|
|
163
|
+
*/
|
|
164
|
+
const getPromptIds = () => {
|
|
165
|
+
return PROMPTS.map((p) => p.id);
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Gets just the master prompt content.
|
|
169
|
+
* Useful when you need to use a custom addon.
|
|
170
|
+
*
|
|
171
|
+
* @returns The master prompt content
|
|
172
|
+
*/
|
|
173
|
+
const getMasterPrompt = () => {
|
|
174
|
+
return MASTER_PROMPT;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/validation.ts
|
|
179
|
+
/**
|
|
180
|
+
* Detects Arabic script in text (except allowed ﷺ symbol).
|
|
181
|
+
* This is a SOFT warning - Arabic leak is bad but not a hard failure.
|
|
182
|
+
*
|
|
183
|
+
* @param text - The text to scan for Arabic script
|
|
184
|
+
* @returns Array of validation warnings if Arabic is found
|
|
185
|
+
*/
|
|
186
|
+
const detectArabicScript = (text) => {
|
|
187
|
+
const warnings = [];
|
|
188
|
+
const matches = text.match(/[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDF9\uFDFB-\uFDFF\uFE70-\uFEFF]+/g);
|
|
189
|
+
if (matches) for (const match of matches) warnings.push({
|
|
190
|
+
match,
|
|
191
|
+
message: `Arabic script detected: "${match}"`,
|
|
192
|
+
type: "arabic_leak"
|
|
193
|
+
});
|
|
194
|
+
return warnings;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Detects wrong diacritics (â/ã/á instead of correct macrons ā/ī/ū).
|
|
198
|
+
* This is a SOFT warning - wrong diacritics are bad but not a hard failure.
|
|
199
|
+
*
|
|
200
|
+
* @param text - The text to scan for incorrect diacritics
|
|
201
|
+
* @returns Array of validation warnings if wrong diacritics are found
|
|
202
|
+
*/
|
|
203
|
+
const detectWrongDiacritics = (text) => {
|
|
204
|
+
const warnings = [];
|
|
205
|
+
const matches = text.match(/[âêîôûãñáéíóú]/gi);
|
|
206
|
+
if (matches) {
|
|
207
|
+
const uniqueMatches = [...new Set(matches)];
|
|
208
|
+
for (const match of uniqueMatches) warnings.push({
|
|
209
|
+
match,
|
|
210
|
+
message: `Wrong diacritic "${match}" detected - use macrons (ā, ī, ū) instead`,
|
|
211
|
+
type: "wrong_diacritics"
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return warnings;
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Detects newline immediately after segment ID (the "Gemini bug").
|
|
218
|
+
* Format should be "P1234 - Text" not "P1234\nText".
|
|
219
|
+
*
|
|
220
|
+
* @param text - The text to validate
|
|
221
|
+
* @returns Error message if bug is detected, otherwise undefined
|
|
222
|
+
*/
|
|
223
|
+
const detectNewlineAfterId = (text) => {
|
|
224
|
+
const pattern = new RegExp(`^${MARKER_ID_PATTERN}\\n`, "m");
|
|
225
|
+
const match = text.match(pattern);
|
|
226
|
+
if (match) return `Invalid format: newline after ID "${match[0].trim()}" - use "ID - Text" format`;
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Detects implicit continuation text that LLMs add when hallucinating.
|
|
230
|
+
*
|
|
231
|
+
* @param text - The text to scan for continuation markers
|
|
232
|
+
* @returns Error message if continuation text is found, otherwise undefined
|
|
233
|
+
*/
|
|
234
|
+
const detectImplicitContinuation = (text) => {
|
|
235
|
+
for (const pattern of [
|
|
236
|
+
/implicit continuation/i,
|
|
237
|
+
/\bcontinuation:/i,
|
|
238
|
+
/\bcontinued:/i
|
|
239
|
+
]) {
|
|
240
|
+
const match = text.match(pattern);
|
|
241
|
+
if (match) return `Detected "${match[0]}" - do not add implicit continuation text`;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* Detects meta-talk (translator notes, editor comments) that violate NO META-TALK.
|
|
246
|
+
*
|
|
247
|
+
* @param text - The text to scan for meta-talk
|
|
248
|
+
* @returns Error message if meta-talk is found, otherwise undefined
|
|
249
|
+
*/
|
|
250
|
+
const detectMetaTalk = (text) => {
|
|
251
|
+
for (const pattern of [
|
|
252
|
+
/\(note:/i,
|
|
253
|
+
/\(translator'?s? note:/i,
|
|
254
|
+
/\[editor:/i,
|
|
255
|
+
/\[note:/i,
|
|
256
|
+
/\(ed\.:/i,
|
|
257
|
+
/\(trans\.:/i
|
|
258
|
+
]) {
|
|
259
|
+
const match = text.match(pattern);
|
|
260
|
+
if (match) return `Detected meta-talk "${match[0]}" - output translation only, no translator/editor notes`;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Detects duplicate segment IDs in the output.
|
|
265
|
+
*
|
|
266
|
+
* @param ids - List of IDs extracted from the translation
|
|
267
|
+
* @returns Error message if duplicates are found, otherwise undefined
|
|
268
|
+
*/
|
|
269
|
+
const detectDuplicateIds = (ids) => {
|
|
270
|
+
const seen = /* @__PURE__ */ new Set();
|
|
271
|
+
for (const id of ids) {
|
|
272
|
+
if (seen.has(id)) return `Duplicate ID "${id}" detected - each segment should appear only once`;
|
|
273
|
+
seen.add(id);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Detects IDs in the output that were not in the source (invented/hallucinated IDs).
|
|
278
|
+
* @param outputIds - IDs extracted from LLM output
|
|
279
|
+
* @param sourceIds - IDs that were present in the source input
|
|
280
|
+
* @returns Error message if invented IDs found, undefined if all IDs are valid
|
|
281
|
+
*/
|
|
282
|
+
const detectInventedIds = (outputIds, sourceIds) => {
|
|
283
|
+
const sourceSet = new Set(sourceIds);
|
|
284
|
+
const invented = outputIds.filter((id) => !sourceSet.has(id));
|
|
285
|
+
if (invented.length > 0) return `Invented ID(s) detected: ${invented.map((id) => `"${id}"`).join(", ")} - these IDs do not exist in the source`;
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Detects segments that appear truncated (just "…" or very short with no real content).
|
|
289
|
+
* @param text - The full LLM output text
|
|
290
|
+
* @returns Error message if truncated segments found, undefined if all segments have content
|
|
291
|
+
*/
|
|
292
|
+
const detectTruncatedSegments = (text) => {
|
|
293
|
+
const segmentPattern = /^([A-Z]\d+[a-j]?)\s*[-–—]\s*(.*)$/gm;
|
|
294
|
+
const truncated = [];
|
|
295
|
+
for (const match of text.matchAll(segmentPattern)) {
|
|
296
|
+
const id = match[1];
|
|
297
|
+
const content = match[2].trim();
|
|
298
|
+
if (!content || content === "…" || content === "..." || content === "[INCOMPLETE]") truncated.push(id);
|
|
299
|
+
}
|
|
300
|
+
if (truncated.length > 0) return `Truncated segment(s) detected: ${truncated.map((id) => `"${id}"`).join(", ")} - segments must be fully translated`;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Validates translation marker format and returns error message if invalid.
|
|
304
|
+
* Catches common AI hallucinations like malformed reference IDs.
|
|
305
|
+
*
|
|
306
|
+
* @param text - Raw translation text to validate
|
|
307
|
+
* @returns Error message if invalid, undefined if valid
|
|
308
|
+
*/
|
|
309
|
+
const validateTranslationMarkers = (text) => {
|
|
310
|
+
const { markers, digits, suffix, dashes, optionalSpace } = TRANSLATION_MARKER_PARTS;
|
|
311
|
+
const invalidRefPattern = new RegExp(`^${markers}(?=${digits})(?=.*${dashes})(?!${digits}${suffix}*${optionalSpace}${dashes})[^\\s-–—]+${optionalSpace}${dashes}`, "m");
|
|
312
|
+
const invalidRef = text.match(invalidRefPattern);
|
|
313
|
+
if (invalidRef) return `Invalid reference format "${invalidRef[0].trim()}" - expected format is letter + numbers + optional suffix (a-j) + dash`;
|
|
314
|
+
const spaceBeforePattern = new RegExp(` ${markers}${digits}${suffix}+${optionalSpace}${dashes}`, "m");
|
|
315
|
+
const suffixNoDashPattern = new RegExp(`^${markers}${digits}${suffix}(?! ${dashes})`, "m");
|
|
316
|
+
const match = text.match(spaceBeforePattern) || text.match(suffixNoDashPattern);
|
|
317
|
+
if (match) return `Suspicious reference found: "${match[0]}"`;
|
|
318
|
+
const emptyAfterDashPattern = new RegExp(`^${MARKER_ID_PATTERN}${optionalSpace}${dashes}\\s*$`, "m");
|
|
319
|
+
const emptyAfterDash = text.match(emptyAfterDashPattern);
|
|
320
|
+
if (emptyAfterDash) return `Reference "${emptyAfterDash[0].trim()}" has dash but no content after it`;
|
|
321
|
+
const dollarSignPattern = new RegExp(`^${markers}${digits}\\$${digits}`, "m");
|
|
322
|
+
const dollarSignRef = text.match(dollarSignPattern);
|
|
323
|
+
if (dollarSignRef) return `Invalid reference format "${dollarSignRef[0]}" - contains $ character`;
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* Normalizes translation text by splitting merged markers onto separate lines.
|
|
327
|
+
* LLMs sometimes put multiple translations on the same line.
|
|
328
|
+
*
|
|
329
|
+
* @param content - Raw translation text
|
|
330
|
+
* @returns Normalized text with each marker on its own line
|
|
331
|
+
*/
|
|
332
|
+
const normalizeTranslationText = (content) => {
|
|
333
|
+
const mergedMarkerPattern = new RegExp(` (${MARKER_ID_PATTERN}${TRANSLATION_MARKER_PARTS.optionalSpace}${TRANSLATION_MARKER_PARTS.dashes})`, "gm");
|
|
334
|
+
return content.replace(mergedMarkerPattern, "\n$1").replace(/\\\[/gm, "[");
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* Extracts translation IDs from text in order of appearance.
|
|
338
|
+
*
|
|
339
|
+
* @param text - Translation text
|
|
340
|
+
* @returns Array of IDs in order
|
|
341
|
+
*/
|
|
342
|
+
const extractTranslationIds = (text) => {
|
|
343
|
+
const { dashes, optionalSpace } = TRANSLATION_MARKER_PARTS;
|
|
344
|
+
const pattern = new RegExp(`^(${MARKER_ID_PATTERN})${optionalSpace}${dashes}`, "gm");
|
|
345
|
+
const ids = [];
|
|
346
|
+
for (const match of text.matchAll(pattern)) ids.push(match[1]);
|
|
347
|
+
return ids;
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Extracts the numeric portion from an excerpt ID.
|
|
351
|
+
* E.g., "P11622a" -> 11622, "C123" -> 123, "B45b" -> 45
|
|
352
|
+
*
|
|
353
|
+
* @param id - Excerpt ID
|
|
354
|
+
* @returns Numeric portion of the ID
|
|
355
|
+
*/
|
|
356
|
+
const extractIdNumber = (id) => {
|
|
357
|
+
const match = id.match(/\d+/);
|
|
358
|
+
return match ? Number.parseInt(match[0], 10) : 0;
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* Extracts the prefix (type) from an excerpt ID.
|
|
362
|
+
* E.g., "P11622a" -> "P", "C123" -> "C", "B45" -> "B"
|
|
363
|
+
*
|
|
364
|
+
* @param id - Excerpt ID
|
|
365
|
+
* @returns Single character prefix
|
|
366
|
+
*/
|
|
367
|
+
const extractIdPrefix = (id) => {
|
|
368
|
+
return id.charAt(0);
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Validates that translation IDs appear in ascending numeric order within the same prefix type.
|
|
372
|
+
* This catches LLM errors where translations are output in wrong order (e.g., P12659 before P12651).
|
|
373
|
+
*
|
|
374
|
+
* @param translationIds - IDs from pasted translations
|
|
375
|
+
* @returns Error message if order issue detected, undefined if valid
|
|
376
|
+
*/
|
|
377
|
+
const validateNumericOrder = (translationIds) => {
|
|
378
|
+
if (translationIds.length < 2) return;
|
|
379
|
+
const lastNumberByPrefix = /* @__PURE__ */ new Map();
|
|
380
|
+
for (const id of translationIds) {
|
|
381
|
+
const prefix = extractIdPrefix(id);
|
|
382
|
+
const num = extractIdNumber(id);
|
|
383
|
+
const last = lastNumberByPrefix.get(prefix);
|
|
384
|
+
if (last && num < last.num) return `Numeric order error: "${id}" (${num}) appears after "${last.id}" (${last.num}) but should come before it`;
|
|
385
|
+
lastNumberByPrefix.set(prefix, {
|
|
386
|
+
id,
|
|
387
|
+
num
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
/**
|
|
392
|
+
* Validates translation order against expected excerpt order from the store.
|
|
393
|
+
* Allows pasting in multiple blocks where each block is internally ordered.
|
|
394
|
+
* Resets (position going backwards) are allowed between blocks.
|
|
395
|
+
* Errors only when there's disorder WITHIN a block (going backwards then forwards).
|
|
396
|
+
*
|
|
397
|
+
* @param translationIds - IDs from pasted translations
|
|
398
|
+
* @param expectedIds - IDs from store excerpts/headings/footnotes in order
|
|
399
|
+
* @returns Error message if order issue detected, undefined if valid
|
|
400
|
+
*/
|
|
401
|
+
const validateTranslationOrder = (translationIds, expectedIds) => {
|
|
402
|
+
if (translationIds.length === 0 || expectedIds.length === 0) return;
|
|
403
|
+
const expectedPositions = /* @__PURE__ */ new Map();
|
|
404
|
+
for (let i = 0; i < expectedIds.length; i++) expectedPositions.set(expectedIds[i], i);
|
|
405
|
+
let lastExpectedPosition = -1;
|
|
406
|
+
let blockStartPosition = -1;
|
|
407
|
+
let lastFoundId = null;
|
|
408
|
+
for (const translationId of translationIds) {
|
|
409
|
+
const expectedPosition = expectedPositions.get(translationId);
|
|
410
|
+
if (expectedPosition === void 0) continue;
|
|
411
|
+
if (lastFoundId !== null) {
|
|
412
|
+
if (expectedPosition < lastExpectedPosition) blockStartPosition = expectedPosition;
|
|
413
|
+
else if (expectedPosition < blockStartPosition && blockStartPosition !== -1) return `Order error: "${translationId}" appears after "${lastFoundId}" but comes before it in the excerpts. This suggests a duplicate or misplaced translation.`;
|
|
414
|
+
} else blockStartPosition = expectedPosition;
|
|
415
|
+
lastExpectedPosition = expectedPosition;
|
|
416
|
+
lastFoundId = translationId;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* Performs comprehensive validation on translation text.
|
|
421
|
+
* Validates markers, normalizes text, and checks order against expected IDs.
|
|
422
|
+
*
|
|
423
|
+
* @param rawText - Raw translation text from user input
|
|
424
|
+
* @param expectedIds - Expected IDs from store (excerpts + headings + footnotes)
|
|
425
|
+
* @returns Validation result with normalized text and any errors
|
|
426
|
+
*/
|
|
427
|
+
const validateTranslations = (rawText, expectedIds) => {
|
|
428
|
+
const normalizedText = normalizeTranslationText(rawText);
|
|
429
|
+
const markerError = validateTranslationMarkers(normalizedText);
|
|
430
|
+
if (markerError) return {
|
|
431
|
+
error: markerError,
|
|
432
|
+
isValid: false,
|
|
433
|
+
normalizedText,
|
|
434
|
+
parsedIds: []
|
|
435
|
+
};
|
|
436
|
+
const parsedIds = extractTranslationIds(normalizedText);
|
|
437
|
+
if (parsedIds.length === 0) return {
|
|
438
|
+
error: "No valid translation markers found",
|
|
439
|
+
isValid: false,
|
|
440
|
+
normalizedText,
|
|
441
|
+
parsedIds: []
|
|
442
|
+
};
|
|
443
|
+
const orderError = validateTranslationOrder(parsedIds, expectedIds);
|
|
444
|
+
if (orderError) return {
|
|
445
|
+
error: orderError,
|
|
446
|
+
isValid: false,
|
|
447
|
+
normalizedText,
|
|
448
|
+
parsedIds
|
|
449
|
+
};
|
|
450
|
+
return {
|
|
451
|
+
isValid: true,
|
|
452
|
+
normalizedText,
|
|
453
|
+
parsedIds
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
/**
|
|
457
|
+
* Finds translation IDs that don't exist in the expected store IDs.
|
|
458
|
+
* Used to validate that all pasted translations can be matched before committing.
|
|
459
|
+
*
|
|
460
|
+
* @param translationIds - IDs from parsed translations
|
|
461
|
+
* @param expectedIds - IDs from store (excerpts + headings + footnotes)
|
|
462
|
+
* @returns Array of IDs that exist in translations but not in the store
|
|
463
|
+
*/
|
|
464
|
+
const findUnmatchedTranslationIds = (translationIds, expectedIds) => {
|
|
465
|
+
const expectedSet = new Set(expectedIds);
|
|
466
|
+
return translationIds.filter((id) => !expectedSet.has(id));
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
//#endregion
|
|
470
|
+
export { MARKER_ID_PATTERN, Markers, TRANSLATION_MARKER_PARTS, detectArabicScript, detectDuplicateIds, detectImplicitContinuation, detectInventedIds, detectMetaTalk, detectNewlineAfterId, detectTruncatedSegments, detectWrongDiacritics, extractIdNumber, extractIdPrefix, extractTranslationIds, findUnmatchedTranslationIds, formatExcerptsForPrompt, getMasterPrompt, getPrompt, getPromptIds, getPrompts, getStackedPrompt, normalizeTranslationText, stackPrompts, validateNumericOrder, validateTranslationMarkers, validateTranslationOrder, validateTranslations };
|
|
471
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/constants.ts","../src/formatting.ts","../.generated/prompts.ts","../src/prompts.ts","../src/validation.ts"],"sourcesContent":["/**\n * Supported marker types for segments.\n */\nexport enum Markers {\n /** B - Book reference */\n Book = 'B',\n /** F - Footnote reference */\n Footnote = 'F',\n /** T - Heading reference */\n Heading = 'T',\n /** C - Chapter reference */\n Chapter = 'C',\n /** N - Note reference */\n Note = 'N',\n /** P - Translation/Plain segment */\n Plain = 'P',\n}\n\n/**\n * Regex parts for building translation marker patterns.\n */\nexport const TRANSLATION_MARKER_PARTS = {\n /** Dash variations (hyphen, en dash, em dash) */\n dashes: '[-–—]',\n /** Numeric portion of the reference */\n digits: '\\\\d+',\n /** Valid marker prefixes (Book, Chapter, Footnote, Translation, Page) */\n markers: `[${Markers.Book}${Markers.Chapter}${Markers.Footnote}${Markers.Heading}${Markers.Plain}${Markers.Note}]`,\n /** Optional whitespace before dash */\n optionalSpace: '\\\\s?',\n /** Valid single-letter suffixes */\n suffix: '[a-z]',\n} as const;\n\n/**\n * Pattern for a segment ID (e.g., P1234, B45a).\n */\nexport const MARKER_ID_PATTERN = `${TRANSLATION_MARKER_PARTS.markers}${TRANSLATION_MARKER_PARTS.digits}${TRANSLATION_MARKER_PARTS.suffix}?`;\n","/**\n * Internal segment type for formatting.\n */\ntype Segment = {\n /** The segment ID (e.g., P1) */\n id: string;\n /** The segment text */\n text: string;\n};\n\n/**\n * Formats excerpts for an LLM prompt by combining the prompt rules with the segment text.\n * Each segment is formatted as \"ID - Text\" and separated by double newlines.\n *\n * @param segments - Array of segments to format\n * @param prompt - The instruction/system prompt to prepend\n * @returns Combined prompt and formatted text\n */\nexport const formatExcerptsForPrompt = (segments: Segment[], prompt: string) => {\n const formatted = segments.map((e) => `${e.id} - ${e.text}`).join('\\n\\n');\n return [prompt, formatted].join('\\n\\n');\n};\n","// AUTO-GENERATED FILE - DO NOT EDIT\n// Generated from prompts/*.md by scripts/generate-prompts.ts\n\n// =============================================================================\n// PROMPT TYPE\n// =============================================================================\n\nexport type PromptId = 'master_prompt' | 'encyclopedia_mixed' | 'fatawa' | 'fiqh' | 'hadith' | 'jarh_wa_tadil' | 'tafsir' | 'usul_al_fiqh';\n\n// =============================================================================\n// RAW PROMPT CONTENT\n// =============================================================================\n\nexport const MASTER_PROMPT = \"ROLE: Expert academic translator of Classical Islamic texts; prioritize accuracy and structure over fluency.\\nCRITICAL NEGATIONS: 1. NO SANITIZATION (Do not soften polemics). 2. NO META-TALK (Output translation only). 3. NO MARKDOWN (Plain text only). 4. NO EMENDATION. 5. NO INFERENCE. 6. NO RESTRUCTURING. 7. NO OPAQUE TRANSLITERATION (Must translate phrases). 8. NO INVENTED SEGMENTS (Do not create, modify, or \\\"continue\\\" segment IDs. Output IDs verbatim exactly as they appear in the source input/metadata. Alphabetic suffixes (e.g., P5511a) are allowed IF AND ONLY IF that exact ID appears in the source. Any ID not present verbatim in the source is INVENTED. EXAMPLE: If P5803b ends with a questioner line, that line stays under P5803b — do NOT invent P5803c. If an expected ID is missing from the source, output: \\\"ID - [MISSING]\\\".)\\nRULES: NO ARABIC SCRIPT (Except ﷺ). Plain text only. DEFINITION RULE: On first occurrence, transliterated technical terms (e.g., bidʿah) MUST be defined: \\\"translit (English)\\\". Preserve Segment ID. Translate meaning/intent. No inference. No extra fields. Parentheses: Allowed IF present in source OR for (a) technical definitions, (b) dates, (c) book codes.\\nTRANSLITERATION & TERMS:\\n1. SCHEME: Use full ALA-LC for explicit Arabic-script Person/Place/Book-Titles.\\n - al-Casing: Lowercase al- mid-sentence; Capitalize after (al-Salafīyyah).\\n - Book Titles: Transliterate only (do not translate meanings).\\n2. TECHNICAL TERMS: On first occurrence, define: \\\"translit (English)\\\" (e.g., bidʿah (innovation), isnād (chain)).\\n - Do NOT output multi-word transliterations without immediate English translation.\\n3. STANDARDIZED TERMS: Use standard academic spellings: Muḥammad, Shaykh, Qurʾān, Islām, ḥadīth.\\n - Sunnah (Capitalized) = The Corpus/Prophetic Tradition. sunnah (lowercase) = legal status/recommended.\\n4. PROPER NAMES: Transliterate only (no parentheses).\\n5. UNICODE: Latin + Latin Extended (āīūḥʿḍṣṭẓʾ) + punctuation. NO Arabic script (except ﷺ). NO emoji.\\n - DIACRITIC FALLBACK: If you cannot produce correct ALA-LC diacritics, output English only. Do NOT use substitute accents (â/ã/á).\\n6. SALUTATION: Replace all Prophet salutations with ﷺ.\\n7. AMBIGUITY: Use contextual meaning from tafsir for theological terms. Do not sanitise polemics (e.g. Rāfiḍah).\\nOUTPUT FORMAT: Segment_ID - English translation.\\nCRITICAL: You must use the ASCII hyphen separator \\\" - \\\" (space+hyphen+space) immediately after the ID. Do NOT use em-dash or en-dash. Do NOT use a newline after the ID.\\nMULTI-LINE SEGMENTS (e.g., internal Q&A): Output the Segment_ID and \\\" - \\\" ONLY ONCE on the first line. Do NOT repeat the Segment_ID on subsequent lines; subsequent lines must start directly with the speaker label/text (no \\\"ID - \\\" prefix).\\nSEGMENT BOUNDARIES (Anti-hallucination): Start a NEW segment ONLY when the source explicitly provides a Segment_ID. If the source continues with extra lines (including speaker labels like \\\"Questioner:\\\"/\\\"The Shaykh:\\\"/\\\"السائل:\\\"/\\\"الشيخ:\\\") WITHOUT a new Segment_ID, treat them as part of the CURRENT segment (multi-line under the current Segment_ID). Do NOT invent a new ID (including alphabetic suffixes like \\\"P5803c\\\") to label such continuation.\\nOUTPUT COMPLETENESS: Translate ALL content in EVERY segment. Do not truncate, summarize, or skip content. The \\\"…\\\" symbol in the source indicates an audio gap in the original recording — it is NOT an instruction to omit content. Every segment must be fully translated. If you cannot complete a segment, output \\\"ID - [INCOMPLETE]\\\" instead of just \\\"…\\\".\\nOUTPUT UNIQUENESS: Each Segment_ID from the source must appear in your output EXACTLY ONCE as an \\\"ID - ...\\\" prefix. Do NOT output the same Segment_ID header twice. If a segment is long or has multiple speaker turns, continue translating under that single ID header without re-stating it.\\nNEGATIVE CONSTRAINTS: Do NOT output \\\"implicit continuation\\\", summaries, or extra paragraphs. Output only the text present in the source segment.\\nExample: P1234 - Translation text... (Correct) vs P1234\\\\nTranslation... (Forbidden).\\nEXAMPLE: Input: P405 - حدثنا عبد الله بن يوسف... Output: P405 - ʿAbd Allāh b. Yūsuf narrated to us...\";\n\nexport const ENCYCLOPEDIA_MIXED = \"NO MODE TAGS: Do not output any mode labels or bracket tags.\\nSTRUCTURE (Apply First):\\n- Q&A: Whenever \\\"Al-Sāʾil:\\\"/\\\"Al-Shaykh:\\\" appear: Start NEW LINE for speaker. Keep Label+Text on SAME LINE.\\n- EXCEPTION: If the speaker label is the VERY FIRST token after the \\\"ID - \\\" prefix, keep it on the same line. (Correct: P5455 - Questioner: Text...) (Wrong: P5455 \\\\n Questioner: Text...).\\n- INTERNAL Q&A: If segment has multiple turns, use new lines for speakers. Output Segment ID ONLY ONCE at the start of the first line. Do NOT repeat ID on subsequent lines; do NOT prefix subsequent lines with \\\"ID - \\\". (e.g. P5455 - Questioner: ... \\\\n The Shaykh: ...).\\n- OUTPUT LABELS: Al-Sāʾil -> Questioner: ; Al-Shaykh -> The Shaykh:\\n\\nDEFINITIONS & CASING:\\n- GEOPOLITICS: Modern place names may use English exonyms (Filasṭīn -> Palestine).\\n- PLURALS: Do not pluralize term-pairs by appending \\\"s\\\" (e.g., \\\"ḥadīth (report)s\\\"). Use the English plural or rephrase.\\n\\nSTATE LOGIC (Priority: Isnad > Rijal > Fiqh > Narrative):\\n- ISNAD (Triggers: `ḥaddathanā`, `akhbaranā`, `ʿan`): Use FULL ALA-LC for names.\\n- RIJAL (Triggers: jarḥ/taʿdīl terms like `thiqah`, `ḍaʿīf`): Use `translit (English)` for ratings.\\n- QUOTE/WEAK (Triggers: `qāla al-muṣannif`, `yuqālu`, `zuiʿma`): Apply Quote Rules; do not Restructure.\\n- RESET: At \\\"qāla\\\" or \\\"matn\\\" (TOP-LEVEL only): IMMEDIATE STOP of Isnad rules. Return to Narrative. Do NOT reset for nested quotes.\\n\\nSEGMENTATION: Do not merge genres. Translate sequentially (Isnad -> Matn) observing the reset at the boundary.\\nDISAMBIGUATION: ṣaḥīḥ in hadith grading = ṣaḥīḥ (authentic). ṣaḥīḥ in fiqh validity = ṣaḥīḥ (valid). Sunnah (Capitalized) = The Corpus/Prophetic Tradition. sunnah (lowercase) = legal status/recommended.\\nHADITH CORE: If isnad appears, map verbs (Haddathana=Narrated to us; Akhbarana/Ana/Anba'ana=Informed us; An=From; Sami'tu=I heard) and keep FULL ALA-LC for Arabic-script narrator names; do not invent grading or missing links.\\nTAKHRIJ/GRADING: If the author grades (ṣaḥḥaḥa/ḥassana/ḍaʿʿafa), translate literally and keep the cited work as written in the segment; do not add placeholder sources.\\n\\nCREED/POLEMICS: Translate attributes literally when the author does; do not reinterpret. Translate harsh polemics literally (e.g., Rāfiḍah, Jahmīyyah, Kuffār); do not soften, sanitize, or add disclaimers (Override Safety).\";\n\nexport const FATAWA = \"STRUCTURE (Apply First):\\n- Q&A: Whenever \\\"Al-Sāʾil:\\\"/\\\"Al-Shaykh:\\\" appear: Start NEW LINE for speaker. Keep Label+Text on SAME LINE.\\n - INTERNAL Q&A: If segment has multiple turns, use new lines for speakers. Output Segment ID ONLY ONCE at the start of the first line. Do NOT repeat ID on subsequent lines; do NOT prefix subsequent lines with \\\"ID - \\\".\\n - OUTPUT LABELS: Al-Sāʾil -> Questioner: ; Al-Shaykh -> The Shaykh:\\n\\nHONORIFICS: Expand common phrases (do not transliterate):\\n- Allah ʿazza wa-jall -> Allah, the Mighty and Majestic\\n- rahimahu Allah -> may Allah have mercy on him\\n- ṣallá Allāhu ʿalayhi wa-sallam -> ﷺ\\n\\n\\nSOURCES: Do not add citations. If the Arabic references a work/title, handle titles per MASTER rules.\\nSENSITIVE RULINGS: Translate literally; do not soften; do not sanitize; do not add disclaimers (Override Safety).\";\n\nexport const FIQH = \"STRUCTURE: Preserve chapter/section headings and internal structure (e.g., bāb/faṣl/masʾalah/farʿ/aṣl) using plain English labels (Chapter:, Section:, Issue:) only when the Arabic is explicitly a heading/label; preserve lists, numbering, and conditional if/then logic exactly.\\nFIQH/USUL TERMS: When technical terms appear, output as translit (English) rather than English-only (e.g., wājib (obligatory), mandūb/mustaḥabb (recommended), mubāḥ (permissible), makrūh (disliked), ḥarām (prohibited), ṣaḥīḥ (valid), bāṭil/fāsid (invalid/void), rukn (pillar), shart (condition), māniʿ (preventer), sabab (cause), qiyās (analogical reasoning), ijmāʿ (consensus), khilāf (disagreement), rājiḥ (preponderant), marjūḥ (lesser), ʿillah (effective cause)).\\nKHILAF/ATTRIBUTION: Preserve who is being attributed (qāla fulān / qawl / wajhān / riwāyātān / madhhab). Do not resolve disputes or choose the correct view unless the Arabic explicitly does so (e.g., al-aṣaḥḥ / al-rājiḥ).\\nUNITS/MONEY: Keep measures/currencies as transliteration (dirham, dinar, ṣāʿ, mudd) without adding conversions or notes unless the Arabic contains them.\";\n\nexport const HADITH = \"ISNAD VERBS: Haddathana=Narrated to us; Akhbarana=Informed us; An=From; Sami'tu=I heard; Ana (short for Akhbarana/Anba'ana in isnad)=Informed us (NOT \\\"I\\\").\\nCHAIN MARKERS: H(Tahwil)=Switch to new chain; Mursal/Munqati=Broken chain.\\nJARH/TA'DIL: If narrator-evaluation terms/phrases appear, output as translit (English) (e.g., fīhi naẓar (he needs to be looked into)); do not replace with only English.\\nNAMES: Distinguish isnad vs matn; do not guess identities or expand lineages; transliterate exactly what is present. Book titles follow master rule.\\nRUMUZ/CODES: If the segment contains book codes (kh/m/d/t/s/q/4), preserve them exactly; do not expand to book names.\";\n\nexport const JARH_WA_TADIL = \"GLOSSARY: When a jarh/ta'dil term/phrase appears, output as translit (English) (e.g., thiqah (trustworthy), ṣadūq (truthful), layyin (soft/lenient), ḍaʿīf (weak), matrūk (abandoned), kadhdhāb (liar), dajjāl (imposter), munkar al-ḥadīth (narrates denounced hadith)).\\nRUMUZ: Preserve book codes in Latin exactly as in the segment (e.g., (kh) (m) (d t q) (4) (a)); do not expand unless the Arabic segment itself expands them.\\nQALA: Translate as \\\"He said:\\\" and start a new line for each new critic.\\nDATES: Use (d. 256 AH) or (born 194 AH).\\nNO HARM: Translate \\\"There is no harm in him\\\"; no notes.\\nPOLEMICS: Harsh terms (e.g., dajjāl, khabīth, rāfiḍī) must be translated literally; do not soften.\";\n\nexport const TAFSIR = \"AYAH CITES: Do not output surah names unless the Arabic includes the name. Use [2:255]. If the segment contains quoted Qur'an text, translate it in braces: {…} [2:255].\\nATTRIBUTES: Translate Allah’s attributes as the author intends; if the author is literal, keep literal (e.g., Hand, Face); do not add metaphorical reinterpretation unless the author does; mirror the author’s theology (Ash'ari vs Salafi) exactly.\\nI'RAB TERMS: Mubtada=Subject; Khabar=Predicate; Fa'il=Agent/Doer; Maf'ul=Object.\\nPROPHET NAMES: Use Arabic equivalents with ALA-LC diacritics (e.g., Mūsá, ʿĪsá, Dāwūd, Yūsuf).\\nPOETRY: Preserve line breaks (one English line per Arabic line); no bullets; prioritize literal structure/grammar over rhyme.\";\n\nexport const USUL_AL_FIQH = \"STRUCTURE: Preserve the argument structure (claims, objections \\\"if it is said...\\\", replies \\\"we say...\\\", evidences, counter-evidences). Preserve explicit labels (faṣl, masʾalah, qāla, qīla, qulna) as plain English equivalents only when the Arabic is explicitly a label.\\nUSUL TERMS: When technical terms appear, output as translit (English) (e.g., ʿāmm (general), khāṣṣ (specific), muṭlaq (absolute), muqayyad (restricted), amr (command), nahy (prohibition), ḥaqīqah (literal), majāz (figurative), mujmal (ambiguous), mubayyan (clarified), naṣṣ (explicit text), ẓāhir (apparent), mafhūm (implication), manṭūq (stated meaning), dalīl (evidence), qiyās (analogical reasoning), ʿillah (effective cause), sabab (cause), shart (condition), māniʿ (preventer), ijmāʿ (consensus), naskh (abrogation)).\\nDISPUTE HANDLING: Do not resolve methodological disputes or harmonize schools unless the Arabic explicitly chooses (e.g., al-rājiḥ / al-aṣaḥḥ / ṣaḥīḥ). Preserve attribution to the madhhab/scholars as written.\\nQUR'AN/HADITH: Keep verse references in the segment’s style; do not invent references. If a hadith isnad appears, follow MASTER isnad/name rules.\";\n\n// =============================================================================\n// PROMPT METADATA\n// =============================================================================\n\nexport const PROMPTS = [\n {\n id: 'master_prompt' as const,\n name: 'Master Prompt',\n content: MASTER_PROMPT,\n },\n {\n id: 'encyclopedia_mixed' as const,\n name: 'Encyclopedia Mixed',\n content: ENCYCLOPEDIA_MIXED,\n },\n {\n id: 'fatawa' as const,\n name: 'Fatawa',\n content: FATAWA,\n },\n {\n id: 'fiqh' as const,\n name: 'Fiqh',\n content: FIQH,\n },\n {\n id: 'hadith' as const,\n name: 'Hadith',\n content: HADITH,\n },\n {\n id: 'jarh_wa_tadil' as const,\n name: 'Jarh Wa Tadil',\n content: JARH_WA_TADIL,\n },\n {\n id: 'tafsir' as const,\n name: 'Tafsir',\n content: TAFSIR,\n },\n {\n id: 'usul_al_fiqh' as const,\n name: 'Usul Al Fiqh',\n content: USUL_AL_FIQH,\n },\n] as const;\n\nexport type PromptMetadata = (typeof PROMPTS)[number];\n","import { MASTER_PROMPT, PROMPTS, type PromptId, type PromptMetadata } from '@generated/prompts';\n\nexport type { PromptId, PromptMetadata };\n\n/**\n * A stacked prompt ready for use with an LLM.\n */\nexport type StackedPrompt = {\n /** Unique identifier */\n id: PromptId;\n /** Human-readable name */\n name: string;\n /** The full prompt content (master + addon if applicable) */\n content: string;\n /** Whether this is the master prompt (not stacked) */\n isMaster: boolean;\n};\n\n/**\n * Stacks a master prompt with a specialized addon prompt.\n *\n * @param master - The master/base prompt\n * @param addon - The specialized addon prompt\n * @returns Combined prompt text\n */\nexport const stackPrompts = (master: string, addon: string): string => {\n if (!master) {\n return addon;\n }\n if (!addon) {\n return master;\n }\n return `${master}\\n${addon}`;\n};\n\n/**\n * Gets all available prompts as stacked prompts (master + addon combined).\n * Master prompt is returned as-is, addon prompts are stacked with master.\n *\n * @returns Array of all stacked prompts\n */\nexport const getPrompts = (): StackedPrompt[] => {\n return PROMPTS.map((prompt) => ({\n content: prompt.id === 'master_prompt' ? prompt.content : stackPrompts(MASTER_PROMPT, prompt.content),\n id: prompt.id,\n isMaster: prompt.id === 'master_prompt',\n name: prompt.name,\n }));\n};\n\n/**\n * Gets a specific prompt by ID (strongly typed).\n * Returns the stacked version (master + addon) for addon prompts.\n *\n * @param id - The prompt ID to retrieve\n * @returns The stacked prompt\n * @throws Error if prompt ID is not found\n */\nexport const getPrompt = (id: PromptId): StackedPrompt => {\n const prompt = PROMPTS.find((p) => p.id === id);\n if (!prompt) {\n throw new Error(`Prompt not found: ${id}`);\n }\n\n return {\n content: prompt.id === 'master_prompt' ? prompt.content : stackPrompts(MASTER_PROMPT, prompt.content),\n id: prompt.id,\n isMaster: prompt.id === 'master_prompt',\n name: prompt.name,\n };\n};\n\n/**\n * Gets the raw stacked prompt text for a specific prompt ID.\n * Convenience method for when you just need the text.\n *\n * @param id - The prompt ID\n * @returns The stacked prompt content string\n */\nexport const getStackedPrompt = (id: PromptId): string => {\n return getPrompt(id).content;\n};\n\n/**\n * Gets the list of available prompt IDs.\n * Useful for UI dropdowns or validation.\n *\n * @returns Array of prompt IDs\n */\nexport const getPromptIds = (): PromptId[] => {\n return PROMPTS.map((p) => p.id);\n};\n\n/**\n * Gets just the master prompt content.\n * Useful when you need to use a custom addon.\n *\n * @returns The master prompt content\n */\nexport const getMasterPrompt = (): string => {\n return MASTER_PROMPT;\n};\n","import { MARKER_ID_PATTERN, TRANSLATION_MARKER_PARTS } from './constants';\n\n/**\n * Warning types for soft validation issues\n */\nexport type ValidationWarningType = 'arabic_leak' | 'wrong_diacritics';\n\n/**\n * A soft validation warning (not a hard error)\n */\nexport type ValidationWarning = {\n /** The type of warning */\n type: ValidationWarningType;\n /** Human-readable warning message */\n message: string;\n /** The offending text match */\n match?: string;\n};\n\n/**\n * Result of translation validation\n */\nexport type TranslationValidationResult = {\n /** Whether validation passed */\n isValid: boolean;\n /** Error message if validation failed */\n error?: string;\n /** Normalized/fixed text (with merged markers split onto separate lines) */\n normalizedText: string;\n /** List of parsed translation IDs in order */\n parsedIds: string[];\n /** Soft warnings (issues that don't fail validation) */\n warnings?: ValidationWarning[];\n};\n\n/**\n * Detects Arabic script in text (except allowed ﷺ symbol).\n * This is a SOFT warning - Arabic leak is bad but not a hard failure.\n *\n * @param text - The text to scan for Arabic script\n * @returns Array of validation warnings if Arabic is found\n */\nexport const detectArabicScript = (text: string): ValidationWarning[] => {\n const warnings: ValidationWarning[] = [];\n // Arabic Unicode range: \\u0600-\\u06FF, \\u0750-\\u077F, \\uFB50-\\uFDFF, \\uFE70-\\uFEFF\n // Exclude ﷺ (U+FDFA)\n const arabicPattern = /[\\u0600-\\u06FF\\u0750-\\u077F\\uFB50-\\uFDF9\\uFDFB-\\uFDFF\\uFE70-\\uFEFF]+/g;\n const matches = text.match(arabicPattern);\n\n if (matches) {\n for (const match of matches) {\n warnings.push({\n match,\n message: `Arabic script detected: \"${match}\"`,\n type: 'arabic_leak',\n });\n }\n }\n\n return warnings;\n};\n\n/**\n * Detects wrong diacritics (â/ã/á instead of correct macrons ā/ī/ū).\n * This is a SOFT warning - wrong diacritics are bad but not a hard failure.\n *\n * @param text - The text to scan for incorrect diacritics\n * @returns Array of validation warnings if wrong diacritics are found\n */\nexport const detectWrongDiacritics = (text: string): ValidationWarning[] => {\n const warnings: ValidationWarning[] = [];\n // Wrong diacritics: circumflex (â/ê/î/ô/û), tilde (ã/ñ), acute (á/é/í/ó/ú)\n const wrongPattern = /[âêîôûãñáéíóú]/gi;\n const matches = text.match(wrongPattern);\n\n if (matches) {\n const uniqueMatches = [...new Set(matches)];\n for (const match of uniqueMatches) {\n warnings.push({\n match,\n message: `Wrong diacritic \"${match}\" detected - use macrons (ā, ī, ū) instead`,\n type: 'wrong_diacritics',\n });\n }\n }\n\n return warnings;\n};\n\n/**\n * Detects newline immediately after segment ID (the \"Gemini bug\").\n * Format should be \"P1234 - Text\" not \"P1234\\nText\".\n *\n * @param text - The text to validate\n * @returns Error message if bug is detected, otherwise undefined\n */\nexport const detectNewlineAfterId = (text: string): string | undefined => {\n const pattern = new RegExp(`^${MARKER_ID_PATTERN}\\\\n`, 'm');\n const match = text.match(pattern);\n\n if (match) {\n return `Invalid format: newline after ID \"${match[0].trim()}\" - use \"ID - Text\" format`;\n }\n};\n\n/**\n * Detects forbidden terms from the locked glossary.\n * These are common \"gravity well\" spellings that should be avoided.\n *\n * @param text - The text to scan for forbidden terms\n * @returns Error message if a forbidden term is found, otherwise undefined\n */\nexport const detectForbiddenTerms = (text: string): string | undefined => {\n const forbidden: Array<{ term: RegExp; correct: string }> = [\n { correct: 'Shaykh', term: /\\bSheikh\\b/i },\n { correct: 'Qurʾān', term: /\\bKoran\\b/i },\n { correct: 'ḥadīth', term: /\\bHadith\\b/ }, // Case-sensitive: Hadith without dots\n { correct: 'Islām', term: /\\bIslam\\b/ }, // Case-sensitive: Islam without macron\n { correct: 'Salafīyyah', term: /\\bSalafism\\b/i },\n ];\n\n for (const { term, correct } of forbidden) {\n const match = text.match(term);\n if (match) {\n return `Forbidden term \"${match[0]}\" detected - use \"${correct}\" instead`;\n }\n }\n};\n\n/**\n * Detects implicit continuation text that LLMs add when hallucinating.\n *\n * @param text - The text to scan for continuation markers\n * @returns Error message if continuation text is found, otherwise undefined\n */\nexport const detectImplicitContinuation = (text: string): string | undefined => {\n const patterns = [/implicit continuation/i, /\\bcontinuation:/i, /\\bcontinued:/i];\n\n for (const pattern of patterns) {\n const match = text.match(pattern);\n if (match) {\n return `Detected \"${match[0]}\" - do not add implicit continuation text`;\n }\n }\n};\n\n/**\n * Detects meta-talk (translator notes, editor comments) that violate NO META-TALK.\n *\n * @param text - The text to scan for meta-talk\n * @returns Error message if meta-talk is found, otherwise undefined\n */\nexport const detectMetaTalk = (text: string): string | undefined => {\n const patterns = [/\\(note:/i, /\\(translator'?s? note:/i, /\\[editor:/i, /\\[note:/i, /\\(ed\\.:/i, /\\(trans\\.:/i];\n\n for (const pattern of patterns) {\n const match = text.match(pattern);\n if (match) {\n return `Detected meta-talk \"${match[0]}\" - output translation only, no translator/editor notes`;\n }\n }\n};\n\n/**\n * Detects duplicate segment IDs in the output.\n *\n * @param ids - List of IDs extracted from the translation\n * @returns Error message if duplicates are found, otherwise undefined\n */\nexport const detectDuplicateIds = (ids: string[]): string | undefined => {\n const seen = new Set<string>();\n for (const id of ids) {\n if (seen.has(id)) {\n return `Duplicate ID \"${id}\" detected - each segment should appear only once`;\n }\n seen.add(id);\n }\n};\n\n/**\n * Detects IDs in the output that were not in the source (invented/hallucinated IDs).\n * @param outputIds - IDs extracted from LLM output\n * @param sourceIds - IDs that were present in the source input\n * @returns Error message if invented IDs found, undefined if all IDs are valid\n */\nexport const detectInventedIds = (outputIds: string[], sourceIds: string[]): string | undefined => {\n const sourceSet = new Set(sourceIds);\n const invented = outputIds.filter((id) => !sourceSet.has(id));\n\n if (invented.length > 0) {\n return `Invented ID(s) detected: ${invented.map((id) => `\"${id}\"`).join(', ')} - these IDs do not exist in the source`;\n }\n};\n\n/**\n * Detects segments that appear truncated (just \"…\" or very short with no real content).\n * @param text - The full LLM output text\n * @returns Error message if truncated segments found, undefined if all segments have content\n */\nexport const detectTruncatedSegments = (text: string): string | undefined => {\n // Pattern to match segment lines\n const segmentPattern = /^([A-Z]\\d+[a-j]?)\\s*[-–—]\\s*(.*)$/gm;\n const truncated: string[] = [];\n\n for (const match of text.matchAll(segmentPattern)) {\n const id = match[1];\n const content = match[2].trim();\n\n // Check for truncated content: empty, just ellipsis, or [INCOMPLETE]\n if (!content || content === '…' || content === '...' || content === '[INCOMPLETE]') {\n truncated.push(id);\n }\n }\n\n if (truncated.length > 0) {\n return `Truncated segment(s) detected: ${truncated.map((id) => `\"${id}\"`).join(', ')} - segments must be fully translated`;\n }\n};\n\n/**\n * Validates translation marker format and returns error message if invalid.\n * Catches common AI hallucinations like malformed reference IDs.\n *\n * @param text - Raw translation text to validate\n * @returns Error message if invalid, undefined if valid\n */\nexport const validateTranslationMarkers = (text: string): string | undefined => {\n const { markers, digits, suffix, dashes, optionalSpace } = TRANSLATION_MARKER_PARTS;\n\n // Check for invalid reference format (with dash but wrong structure)\n // This catches cases like B12a34 -, P1x2y3 -, P2247$2 -, etc.\n // Requires at least one digit after the marker to be considered a potential reference\n const invalidRefPattern = new RegExp(\n `^${markers}(?=${digits})(?=.*${dashes})(?!${digits}${suffix}*${optionalSpace}${dashes})[^\\\\s-–—]+${optionalSpace}${dashes}`,\n 'm',\n );\n const invalidRef = text.match(invalidRefPattern);\n\n if (invalidRef) {\n return `Invalid reference format \"${invalidRef[0].trim()}\" - expected format is letter + numbers + optional suffix (a-j) + dash`;\n }\n\n // Check for space before reference with multi-letter suffix (e.g., \" P123ab -\")\n const spaceBeforePattern = new RegExp(` ${markers}${digits}${suffix}+${optionalSpace}${dashes}`, 'm');\n\n // Check for reference with single letter suffix but no dash after (e.g., \"P123a without\")\n const suffixNoDashPattern = new RegExp(`^${markers}${digits}${suffix}(?! ${dashes})`, 'm');\n\n const match = text.match(spaceBeforePattern) || text.match(suffixNoDashPattern);\n\n if (match) {\n return `Suspicious reference found: \"${match[0]}\"`;\n }\n\n // Check for references with dash but no content after (e.g., \"P123 -\")\n const emptyAfterDashPattern = new RegExp(`^${MARKER_ID_PATTERN}${optionalSpace}${dashes}\\\\s*$`, 'm');\n const emptyAfterDash = text.match(emptyAfterDashPattern);\n\n if (emptyAfterDash) {\n return `Reference \"${emptyAfterDash[0].trim()}\" has dash but no content after it`;\n }\n\n // Check for $ character in references (invalid format like B1234$5)\n const dollarSignPattern = new RegExp(`^${markers}${digits}\\\\$${digits}`, 'm');\n const dollarSignRef = text.match(dollarSignPattern);\n\n if (dollarSignRef) {\n return `Invalid reference format \"${dollarSignRef[0]}\" - contains $ character`;\n }\n};\n\n/**\n * Normalizes translation text by splitting merged markers onto separate lines.\n * LLMs sometimes put multiple translations on the same line.\n *\n * @param content - Raw translation text\n * @returns Normalized text with each marker on its own line\n */\nexport const normalizeTranslationText = (content: string): string => {\n const mergedMarkerPattern = new RegExp(\n ` (${MARKER_ID_PATTERN}${TRANSLATION_MARKER_PARTS.optionalSpace}${TRANSLATION_MARKER_PARTS.dashes})`,\n 'gm',\n );\n\n return content.replace(mergedMarkerPattern, '\\n$1').replace(/\\\\\\[/gm, '[');\n};\n\n/**\n * Extracts translation IDs from text in order of appearance.\n *\n * @param text - Translation text\n * @returns Array of IDs in order\n */\nexport const extractTranslationIds = (text: string): string[] => {\n const { dashes, optionalSpace } = TRANSLATION_MARKER_PARTS;\n const pattern = new RegExp(`^(${MARKER_ID_PATTERN})${optionalSpace}${dashes}`, 'gm');\n const ids: string[] = [];\n\n for (const match of text.matchAll(pattern)) {\n ids.push(match[1]);\n }\n\n return ids;\n};\n\n/**\n * Extracts the numeric portion from an excerpt ID.\n * E.g., \"P11622a\" -> 11622, \"C123\" -> 123, \"B45b\" -> 45\n *\n * @param id - Excerpt ID\n * @returns Numeric portion of the ID\n */\nexport const extractIdNumber = (id: string): number => {\n const match = id.match(/\\d+/);\n return match ? Number.parseInt(match[0], 10) : 0;\n};\n\n/**\n * Extracts the prefix (type) from an excerpt ID.\n * E.g., \"P11622a\" -> \"P\", \"C123\" -> \"C\", \"B45\" -> \"B\"\n *\n * @param id - Excerpt ID\n * @returns Single character prefix\n */\nexport const extractIdPrefix = (id: string): string => {\n return id.charAt(0);\n};\n\n/**\n * Validates that translation IDs appear in ascending numeric order within the same prefix type.\n * This catches LLM errors where translations are output in wrong order (e.g., P12659 before P12651).\n *\n * @param translationIds - IDs from pasted translations\n * @returns Error message if order issue detected, undefined if valid\n */\nexport const validateNumericOrder = (translationIds: string[]): string | undefined => {\n if (translationIds.length < 2) {\n return;\n }\n\n // Track last seen number for each prefix type\n const lastNumberByPrefix = new Map<string, { id: string; num: number }>();\n\n for (const id of translationIds) {\n const prefix = extractIdPrefix(id);\n const num = extractIdNumber(id);\n\n const last = lastNumberByPrefix.get(prefix);\n\n if (last && num < last.num) {\n // Out of numeric order within the same prefix type\n return `Numeric order error: \"${id}\" (${num}) appears after \"${last.id}\" (${last.num}) but should come before it`;\n }\n\n lastNumberByPrefix.set(prefix, { id, num });\n }\n};\n\n/**\n * Validates translation order against expected excerpt order from the store.\n * Allows pasting in multiple blocks where each block is internally ordered.\n * Resets (position going backwards) are allowed between blocks.\n * Errors only when there's disorder WITHIN a block (going backwards then forwards).\n *\n * @param translationIds - IDs from pasted translations\n * @param expectedIds - IDs from store excerpts/headings/footnotes in order\n * @returns Error message if order issue detected, undefined if valid\n */\nexport const validateTranslationOrder = (translationIds: string[], expectedIds: string[]): string | undefined => {\n if (translationIds.length === 0 || expectedIds.length === 0) {\n return;\n }\n\n // Build a map of expected ID positions for O(1) lookup\n const expectedPositions = new Map<string, number>();\n for (let i = 0; i < expectedIds.length; i++) {\n expectedPositions.set(expectedIds[i], i);\n }\n\n // Track position within current block\n // When position goes backwards, we start a new block\n // Error only if we go backwards THEN forwards within the same conceptual sequence\n let lastExpectedPosition = -1;\n let blockStartPosition = -1;\n let lastFoundId: string | null = null;\n\n for (const translationId of translationIds) {\n const expectedPosition = expectedPositions.get(translationId);\n\n if (expectedPosition === undefined) {\n // ID not found in expected list - skip\n continue;\n }\n\n if (lastFoundId !== null) {\n if (expectedPosition < lastExpectedPosition) {\n // Reset detected - starting a new block\n // This is allowed, just track the new block's start\n blockStartPosition = expectedPosition;\n } else if (expectedPosition < blockStartPosition && blockStartPosition !== -1) {\n // Within the current block, we went backwards - this is an error\n // This catches: A, B, C (block 1), D, E, C (error: C < E but we're in block starting at D)\n return `Order error: \"${translationId}\" appears after \"${lastFoundId}\" but comes before it in the excerpts. This suggests a duplicate or misplaced translation.`;\n }\n } else {\n blockStartPosition = expectedPosition;\n }\n\n lastExpectedPosition = expectedPosition;\n lastFoundId = translationId;\n }\n};\n\n/**\n * Performs comprehensive validation on translation text.\n * Validates markers, normalizes text, and checks order against expected IDs.\n *\n * @param rawText - Raw translation text from user input\n * @param expectedIds - Expected IDs from store (excerpts + headings + footnotes)\n * @returns Validation result with normalized text and any errors\n */\nexport const validateTranslations = (rawText: string, expectedIds: string[]): TranslationValidationResult => {\n // First normalize the text (split merged markers)\n const normalizedText = normalizeTranslationText(rawText);\n\n // Validate marker formats\n const markerError = validateTranslationMarkers(normalizedText);\n if (markerError) {\n return { error: markerError, isValid: false, normalizedText, parsedIds: [] };\n }\n\n // Extract IDs from normalized text\n const parsedIds = extractTranslationIds(normalizedText);\n\n if (parsedIds.length === 0) {\n return { error: 'No valid translation markers found', isValid: false, normalizedText, parsedIds: [] };\n }\n\n // Validate order against expected IDs\n const orderError = validateTranslationOrder(parsedIds, expectedIds);\n if (orderError) {\n return { error: orderError, isValid: false, normalizedText, parsedIds };\n }\n\n return { isValid: true, normalizedText, parsedIds };\n};\n\n/**\n * Finds translation IDs that don't exist in the expected store IDs.\n * Used to validate that all pasted translations can be matched before committing.\n *\n * @param translationIds - IDs from parsed translations\n * @param expectedIds - IDs from store (excerpts + headings + footnotes)\n * @returns Array of IDs that exist in translations but not in the store\n */\nexport const findUnmatchedTranslationIds = (translationIds: string[], expectedIds: string[]): string[] => {\n const expectedSet = new Set(expectedIds);\n return translationIds.filter((id) => !expectedSet.has(id));\n};\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Ragaeeb Haq",
|
|
3
|
+
"bugs": {
|
|
4
|
+
"url": "https://github.com/ragaeeb/wobble-bibble/issues"
|
|
5
|
+
},
|
|
6
|
+
"description": "TypeScript library for Islamic text translation prompts with LLM output validation and prompt stacking utilities.",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@biomejs/biome": "^2.3.11",
|
|
9
|
+
"@types/bun": "^1.3.6",
|
|
10
|
+
"@types/node": "^25.0.8",
|
|
11
|
+
"semantic-release": "^25.0.2",
|
|
12
|
+
"tsdown": "^0.20.0-beta.2",
|
|
13
|
+
"typescript": "^5.9.3"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"bun": ">=1.3.6",
|
|
17
|
+
"node": ">=24.0.0"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist/**"
|
|
27
|
+
],
|
|
28
|
+
"homepage": "https://github.com/ragaeeb/wobble-bibble",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"islamic-translation",
|
|
31
|
+
"llm-prompts",
|
|
32
|
+
"arabic-english",
|
|
33
|
+
"hadith",
|
|
34
|
+
"fiqh",
|
|
35
|
+
"tafsir",
|
|
36
|
+
"prompt-engineering",
|
|
37
|
+
"validation",
|
|
38
|
+
"typescript"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"main": "dist/index.js",
|
|
42
|
+
"module": "dist/index.js",
|
|
43
|
+
"name": "wobble-bibble",
|
|
44
|
+
"packageManager": "bun@1.3.6",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/ragaeeb/wobble-bibble.git"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "bun run generate && tsdown",
|
|
51
|
+
"generate": "bun run scripts/generate-prompts.ts",
|
|
52
|
+
"lint": "biome check .",
|
|
53
|
+
"test": "bun test",
|
|
54
|
+
"test:dist": "bun run build && bun test tests/dist.test.ts"
|
|
55
|
+
},
|
|
56
|
+
"sideEffects": false,
|
|
57
|
+
"source": "src/index.ts",
|
|
58
|
+
"type": "module",
|
|
59
|
+
"types": "dist/index.d.ts",
|
|
60
|
+
"version": "1.0.0"
|
|
61
|
+
}
|