chore: remove unused useMemo import and errorMessage variable #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI · DevOps + MLOps | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| env: | |
| NODE_VERSION: '20' | |
| jobs: | |
| # ────────────────────────────────────────────── | |
| # DevOps: Lint + Type-check + Build | |
| # ────────────────────────────────────────────── | |
| devops: | |
| name: DevOps · Lint, Types, Build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Lint | |
| run: npm run lint | |
| - name: Type-check | |
| run: npx tsc --noEmit | |
| - name: Build Next.js | |
| run: npm run build:next | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build | |
| path: | | |
| .next/ | |
| kb/index/ | |
| # ────────────────────────────────────────────── | |
| # MLOps: Corpus + Index Validation | |
| # ────────────────────────────────────────────── | |
| mlops: | |
| name: MLOps · Corpus & RAG Validation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| # --- Corpus health checks --- | |
| - name: Validate corpus exists | |
| run: | | |
| CORPUS_DIR="kb/sources/final" | |
| FILE_COUNT=$(find "$CORPUS_DIR" -name "*.md" | wc -l) | |
| echo "Corpus files found: $FILE_COUNT" | |
| if [ "$FILE_COUNT" -eq 0 ]; then | |
| echo "::error::No corpus files found in $CORPUS_DIR" | |
| exit 1 | |
| fi | |
| - name: Corpus size check | |
| run: | | |
| CORPUS_DIR="kb/sources/final" | |
| TOTAL_CHARS=$(cat "$CORPUS_DIR"/*.md | wc -c) | |
| TOTAL_LINES=$(cat "$CORPUS_DIR"/*.md | wc -l) | |
| echo "Total characters: $TOTAL_CHARS" | |
| echo "Total lines: $TOTAL_LINES" | |
| if [ "$TOTAL_CHARS" -lt 500 ]; then | |
| echo "::error::Corpus too small ($TOTAL_CHARS chars). Minimum 500 chars required for meaningful RAG." | |
| exit 1 | |
| fi | |
| echo "Corpus size OK" | |
| - name: Corpus section coverage | |
| run: | | |
| CORPUS="kb/sources/final/base_corpus.md" | |
| if [ ! -f "$CORPUS" ]; then | |
| echo "::warning::base_corpus.md not found, skipping section check" | |
| exit 0 | |
| fi | |
| SECTIONS=$(grep -c '^# ' "$CORPUS" || true) | |
| echo "Top-level sections found: $SECTIONS" | |
| if [ "$SECTIONS" -lt 3 ]; then | |
| echo "::warning::Corpus has only $SECTIONS top-level sections. Consider adding more for better RAG coverage." | |
| fi | |
| echo "Section coverage OK" | |
| # --- Vector index integrity --- | |
| - name: Validate vector index exists | |
| run: | | |
| INDEX="kb/index/vector-index.json" | |
| if [ ! -f "$INDEX" ]; then | |
| echo "::error::Vector index not found at $INDEX. Run 'npm run ingest' locally and commit the result." | |
| exit 1 | |
| fi | |
| echo "Vector index found" | |
| - name: Vector index integrity check | |
| run: | | |
| node -e " | |
| const data = require('./kb/index/vector-index.json'); | |
| const chunks = data.chunks || data; | |
| console.log('Version:', data.version || 'N/A'); | |
| console.log('Created:', data.created || 'N/A'); | |
| console.log('Chunks:', chunks.length); | |
| if (!chunks.length || chunks.length === 0) { | |
| console.error('ERROR: No chunks found in vector index'); | |
| process.exit(1); | |
| } | |
| console.log('Vector index structure OK'); | |
| " | |
| - name: Embedding dimension check | |
| run: | | |
| node -e " | |
| const data = require('./kb/index/vector-index.json'); | |
| const chunks = data.chunks || data; | |
| const dims = new Set(chunks.map(c => c.embedding?.length)); | |
| console.log('Chunk count:', chunks.length); | |
| console.log('Embedding dimensions:', [...dims]); | |
| if (dims.size !== 1) { | |
| console.error('ERROR: Inconsistent embedding dimensions:', [...dims]); | |
| process.exit(1); | |
| } | |
| const dim = [...dims][0]; | |
| if (dim !== 384) { | |
| console.error('ERROR: Expected 384-dim (MiniLM-L6-v2), got', dim); | |
| process.exit(1); | |
| } | |
| const nullEmbeddings = chunks.filter(c => !c.embedding || c.embedding.length === 0); | |
| if (nullEmbeddings.length > 0) { | |
| console.error('ERROR:', nullEmbeddings.length, 'chunks have null/empty embeddings'); | |
| process.exit(1); | |
| } | |
| console.log('All', chunks.length, 'chunks have valid 384-dim embeddings'); | |
| " | |
| - name: Corpus-index sync check | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const data = require('./kb/index/vector-index.json'); | |
| const chunks = data.chunks || data; | |
| const corpusDir = 'kb/sources/final'; | |
| const corpusFiles = fs.readdirSync(corpusDir).filter(f => f.endsWith('.md')); | |
| const indexSources = [...new Set(chunks.map(c => c.source || c.filename))]; | |
| console.log('Corpus files:', corpusFiles); | |
| console.log('Index sources:', indexSources); | |
| const missing = indexSources.filter(s => !corpusFiles.includes(s)); | |
| if (missing.length > 0) { | |
| console.error('WARNING: Index references files not in corpus:', missing); | |
| } | |
| console.log('Corpus-index sync OK'); | |
| " | |
| # --- RAG pipeline smoke test --- | |
| - name: RAG retrieval smoke test | |
| run: | | |
| node -e " | |
| const data = require('./kb/index/vector-index.json'); | |
| const chunks = data.chunks || data; | |
| function cosineSim(a, b) { | |
| let dot = 0, normA = 0, normB = 0; | |
| for (let i = 0; i < a.length; i++) { | |
| dot += a[i] * b[i]; | |
| normA += a[i] * a[i]; | |
| normB += b[i] * b[i]; | |
| } | |
| return dot / (Math.sqrt(normA) * Math.sqrt(normB)); | |
| } | |
| // Self-similarity should be 1.0 | |
| const selfSim = cosineSim(chunks[0].embedding, chunks[0].embedding); | |
| console.log('Self-similarity:', selfSim.toFixed(6)); | |
| if (Math.abs(selfSim - 1.0) > 0.001) { | |
| console.error('ERROR: Self-similarity check failed'); | |
| process.exit(1); | |
| } | |
| // Cross-chunk similarity should be < 1.0 | |
| if (chunks.length > 1) { | |
| const crossSim = cosineSim(chunks[0].embedding, chunks[1].embedding); | |
| console.log('Cross-chunk similarity:', crossSim.toFixed(6)); | |
| if (crossSim >= 1.0) { | |
| console.error('ERROR: Different chunks have identical embeddings'); | |
| process.exit(1); | |
| } | |
| } | |
| console.log('RAG retrieval smoke test PASSED'); | |
| " |