[split-required] Split 58 monoliths across Python, Go, TypeScript (Phases 1-3)

Phase 1 — Python (klausur-service): 5 monoliths → 36 files
- dsfa_corpus_ingestion.py (1,828 LOC → 5 files)
- cv_ocr_engines.py (2,102 LOC → 7 files)
- cv_layout.py (3,653 LOC → 10 files)
- vocab_worksheet_api.py (2,783 LOC → 8 files)
- grid_build_core.py (1,958 LOC → 6 files)

Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files
- staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3)
- policy_handlers.go (700 → 2), repository.go (684 → 2)
- search.go (592 → 2), ai_extraction_handlers.go (554 → 2)
- seed_data.go (591 → 2), grade_service.go (646 → 2)

Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files
- sdk/types.ts (2,108 → 16 domain files)
- ai/rag/page.tsx (2,686 → 14 files)
- 22 page.tsx files split into _components/ + _hooks/
- 11 component files split into sub-components
- 10 SDK data catalogs added to loc-exceptions
- Deleted dead backup index_original.ts (4,899 LOC)

All original public APIs preserved via re-export facades.
Zero new errors: Python imports verified, Go builds clean,
TypeScript tsc --noEmit shows only pre-existing errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-24 17:28:57 +02:00
parent 9ba420fa91
commit b681ddb131
251 changed files with 30016 additions and 25037 deletions

View File

@@ -0,0 +1,322 @@
import { useCallback, useState } from 'react'
import type { StructuredGrid, GridZone } from './types'
interface ActionDeps {
grid: StructuredGrid | null
setGrid: React.Dispatch<React.SetStateAction<StructuredGrid | null>>
setDirty: React.Dispatch<React.SetStateAction<boolean>>
pushUndo: (zones: GridZone[]) => void
}
export function useGridEditorActions(deps: ActionDeps) {
const { grid, setGrid, setDirty, pushUndo } = deps
// ------------------------------------------------------------------
// Multi-select state
// ------------------------------------------------------------------
const [selectedCells, setSelectedCells] = useState<Set<string>>(new Set())
const toggleCellSelection = useCallback(
(cellId: string) => {
setSelectedCells((prev) => {
const next = new Set(prev)
if (next.has(cellId)) next.delete(cellId)
else next.add(cellId)
return next
})
},
[],
)
const clearCellSelection = useCallback(() => {
setSelectedCells(new Set())
}, [])
// ------------------------------------------------------------------
// Cell editing
// ------------------------------------------------------------------
const updateCellText = useCallback(
(cellId: string, newText: string) => {
if (!grid) return
pushUndo(grid.zones)
setGrid((prev) => {
if (!prev) return prev
return {
...prev,
zones: prev.zones.map((zone) => {
// Check if cell exists
const existing = zone.cells.find((c) => c.cell_id === cellId)
if (existing) {
return {
...zone,
cells: zone.cells.map((cell) =>
cell.cell_id === cellId ? { ...cell, text: newText } : cell,
),
}
}
// Cell doesn't exist — create it if the cellId belongs to this zone
// cellId format: Z{zone}_R{row}_C{col}
const match = cellId.match(/^Z(\d+)_R(\d+)_C(\d+)$/)
if (!match || parseInt(match[1]) !== zone.zone_index) return zone
const rowIndex = parseInt(match[2])
const colIndex = parseInt(match[3])
const col = zone.columns.find((c) => c.index === colIndex)
const newCell = {
cell_id: cellId,
zone_index: zone.zone_index,
row_index: rowIndex,
col_index: colIndex,
col_type: col?.label ?? '',
text: newText,
confidence: 0,
bbox_px: { x: 0, y: 0, w: 0, h: 0 },
bbox_pct: { x: 0, y: 0, w: 0, h: 0 },
word_boxes: [],
ocr_engine: 'manual',
is_bold: false,
}
return { ...zone, cells: [...zone.cells, newCell] }
}),
}
})
setDirty(true)
},
[grid, pushUndo, setGrid, setDirty],
)
// ------------------------------------------------------------------
// Cell color
// ------------------------------------------------------------------
/**
* Set a manual color override on a cell.
* - hex string (e.g. "#dc2626"): force text color
* - null: force no color (clear bar)
* - undefined: remove override, restore OCR-detected color
*/
const setCellColor = useCallback(
(cellId: string, color: string | null | undefined) => {
if (!grid) return
pushUndo(grid.zones)
setGrid((prev) => {
if (!prev) return prev
return {
...prev,
zones: prev.zones.map((zone) => ({
...zone,
cells: zone.cells.map((cell) => {
if (cell.cell_id !== cellId) return cell
if (color === undefined) {
// Remove override entirely — restore OCR behavior
const { color_override: _, ...rest } = cell
return rest
}
return { ...cell, color_override: color }
}),
})),
}
})
setDirty(true)
},
[grid, pushUndo, setGrid, setDirty],
)
// ------------------------------------------------------------------
// Column formatting
// ------------------------------------------------------------------
const toggleColumnBold = useCallback(
(zoneIndex: number, colIndex: number) => {
if (!grid) return
pushUndo(grid.zones)
setGrid((prev) => {
if (!prev) return prev
return {
...prev,
zones: prev.zones.map((zone) => {
if (zone.zone_index !== zoneIndex) return zone
const col = zone.columns.find((c) => c.index === colIndex)
const newBold = col ? !col.bold : true
return {
...zone,
columns: zone.columns.map((c) =>
c.index === colIndex ? { ...c, bold: newBold } : c,
),
cells: zone.cells.map((cell) =>
cell.col_index === colIndex
? { ...cell, is_bold: newBold }
: cell,
),
}
}),
}
})
setDirty(true)
},
[grid, pushUndo, setGrid, setDirty],
)
// ------------------------------------------------------------------
// Row formatting
// ------------------------------------------------------------------
const toggleRowHeader = useCallback(
(zoneIndex: number, rowIndex: number) => {
if (!grid) return
pushUndo(grid.zones)
// Cycle: normal -> header -> footer -> normal
setGrid((prev) => {
if (!prev) return prev
return {
...prev,
zones: prev.zones.map((zone) => {
if (zone.zone_index !== zoneIndex) return zone
return {
...zone,
rows: zone.rows.map((r) => {
if (r.index !== rowIndex) return r
if (!r.is_header && !r.is_footer) {
return { ...r, is_header: true, is_footer: false }
} else if (r.is_header) {
return { ...r, is_header: false, is_footer: true }
} else {
return { ...r, is_header: false, is_footer: false }
}
}),
}
}),
}
})
setDirty(true)
},
[grid, pushUndo, setGrid, setDirty],
)
// ------------------------------------------------------------------
// Column pattern auto-correction
// ------------------------------------------------------------------
/**
* Detect dominant prefix+number patterns per column and complete
* partial matches. E.g. if 3+ cells read "p.70", "p.71", etc.,
* a cell reading ".65" is corrected to "p.65".
* Returns the number of corrections made.
*/
const autoCorrectColumnPatterns = useCallback(() => {
if (!grid) return 0
pushUndo(grid.zones)
let totalFixed = 0
const numberPattern = /^(.+?)(\d+)\s*$/
setGrid((prev) => {
if (!prev) return prev
return {
...prev,
zones: prev.zones.map((zone) => {
// Group cells by column
const cellsByCol = new Map<number, { cell: (typeof zone.cells)[0]; idx: number }[]>()
zone.cells.forEach((cell, idx) => {
const arr = cellsByCol.get(cell.col_index) || []
arr.push({ cell, idx })
cellsByCol.set(cell.col_index, arr)
})
const newCells = [...zone.cells]
for (const [, colEntries] of cellsByCol) {
// Count prefix occurrences
const prefixCounts = new Map<string, number>()
for (const { cell } of colEntries) {
const m = cell.text.trim().match(numberPattern)
if (m) {
prefixCounts.set(m[1], (prefixCounts.get(m[1]) || 0) + 1)
}
}
// Find dominant prefix (>= 3 occurrences)
let dominantPrefix = ''
let maxCount = 0
for (const [prefix, count] of prefixCounts) {
if (count >= 3 && count > maxCount) {
dominantPrefix = prefix
maxCount = count
}
}
if (!dominantPrefix) continue
// Fix partial matches — entries that are just [.?\s*]NUMBER
for (const { cell, idx } of colEntries) {
const text = cell.text.trim()
if (!text || text.startsWith(dominantPrefix)) continue
const numMatch = text.match(/^[.\s]*(\d+)\s*$/)
if (numMatch) {
newCells[idx] = { ...newCells[idx], text: `${dominantPrefix}${numMatch[1]}` }
totalFixed++
}
}
}
return { ...zone, cells: newCells }
}),
}
})
if (totalFixed > 0) setDirty(true)
return totalFixed
}, [grid, pushUndo, setGrid, setDirty])
// ------------------------------------------------------------------
// Multi-select bulk formatting
// ------------------------------------------------------------------
/** Toggle bold on all selected cells (and their columns). */
const toggleSelectedBold = useCallback(() => {
if (!grid || selectedCells.size === 0) return
pushUndo(grid.zones)
// Determine if we're turning bold on or off (majority rule)
const cells = grid.zones.flatMap((z) => z.cells)
const selectedArr = cells.filter((c) => selectedCells.has(c.cell_id))
const boldCount = selectedArr.filter((c) => c.is_bold).length
const newBold = boldCount < selectedArr.length / 2
setGrid((prev) => {
if (!prev) return prev
return {
...prev,
zones: prev.zones.map((zone) => ({
...zone,
cells: zone.cells.map((cell) =>
selectedCells.has(cell.cell_id) ? { ...cell, is_bold: newBold } : cell,
),
})),
}
})
setDirty(true)
setSelectedCells(new Set())
}, [grid, selectedCells, pushUndo, setGrid, setDirty])
return {
// Cell editing
updateCellText,
setCellColor,
// Column formatting
toggleColumnBold,
// Row formatting
toggleRowHeader,
// Pattern correction
autoCorrectColumnPatterns,
// Multi-select
selectedCells,
toggleCellSelection,
clearCellSelection,
toggleSelectedBold,
}
}