use Illuminate\Support\Facades\DB; // Insert
after each sentence-ending period in question_text + solutions, // for the ingested past-paper set (qa_status ai_generated / unreviewed). // Safe: never touches math ($..$, $$..$$, \(..\), \[..\]) or html tags; keeps // decimals/money (no space after the dot) and common abbreviations intact. // Idempotent: ".
" has no trailing space so it never re-matches. // // Set $DRY = true to preview only (no writes). false = backup + update. $DRY = false; $LIMIT_SAMPLES = 10; $ABBR = ['mr','mrs','ms','mdm','dr','prof','sr','jr','st','vs','etc','no','fig','approx','e.g','i.e']; $sb = function($s) use ($ABBR) { if ($s === null || $s === '') return $s; // single pass: group 1 = math/tag (kept verbatim); else groups 2/3 = a // token + sentence-ending period + following spaces. $rx = '/(\$\$.*?\$\$|\$[^\$\n]+\$|\\\\\(.*?\\\\\)|\\\\\[.*?\\\\\]|<[^>]+>)|([A-Za-z0-9.]*)\.([ \t]+)/su'; $out = preg_replace_callback($rx, function($m) use ($ABBR) { if (isset($m[1]) && $m[1] !== '') return $m[1]; // math / tag: leave alone $w = strtolower($m[2] ?? ''); if (in_array($w, $ABBR, true)) return $m[0]; // abbreviation: no break return ($m[2] ?? '') . '.
'; }, $s); if ($out === null) return $s; // regex failure: no-op return preg_replace('/(?:
\s*){2,}/', '
', $out); // collapse doubled breaks }; $ids = DB::table('questions')->whereIn('qa_status',['ai_generated','unreviewed'])->pluck('id')->toArray(); echo "scope questions: ".count($ids)."\n"; if ($DRY) { $shown = 0; foreach (DB::table('questions')->whereIn('id',$ids) ->where('question','like','% %. %')->inRandomOrder()->limit(200)->get(['id','question']) as $q) { $nq = $sb($q->question); if ($nq !== $q->question) { echo "--- Q#{$q->id} ---\nBEFORE: ".mb_substr($q->question,0,200)."\nAFTER : ".mb_substr($nq,0,220)."\n"; if (++$shown >= $LIMIT_SAMPLES) break; } } // count how many WOULD change $qc=0; foreach (array_chunk($ids,1000) as $c){ foreach (DB::table('questions')->whereIn('id',$c)->get(['question','explanation']) as $q){ if ($sb($q->question)!==$q->question || $sb($q->explanation)!==$q->explanation) $qc++; } } echo "\nWOULD update questions: $qc (dry-run, no writes)\n"; return; } // real run $ts = date('Ymd_His'); $bk = fopen(storage_path("app/_sbreak_backup_$ts.jsonl"), 'w'); $qn=0; $sn=0; foreach (array_chunk($ids,500) as $chunk) { foreach (DB::table('questions')->whereIn('id',$chunk)->get(['id','question','explanation']) as $q) { $nq=$sb($q->question); $ne=$sb($q->explanation); if ($nq!==$q->question || $ne!==$q->explanation) { fwrite($bk, json_encode(['t'=>'q','id'=>$q->id,'q'=>$q->question,'e'=>$q->explanation])."\n"); DB::table('questions')->where('id',$q->id)->update(['question'=>$nq,'explanation'=>$ne]); $qn++; } } foreach (DB::table('solutions')->whereIn('question_id',$chunk)->get(['id','solution']) as $s) { $ns=$sb($s->solution); if ($ns!==$s->solution) { fwrite($bk, json_encode(['t'=>'s','id'=>$s->id,'s'=>$s->solution])."\n"); DB::table('solutions')->where('id',$s->id)->update(['solution'=>$ns]); $sn++; } } } fclose($bk); echo "DONE questions_updated=$qn solutions_updated=$sn backup=storage/app/_sbreak_backup_$ts.jsonl\n";