#!/usr/bin/env node "use strict"; const fs = require("node:fs"); const path = require("node:path"); const ROOT = process.cwd(); const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, ""); const OUTPUT_DIR = path.join(ROOT, "tmp", "task787-weknora-default-lightrag-legacy-smoke"); const RESULT_PATH = path.join(OUTPUT_DIR, "result.json"); const FAILURE_PATH = path.join(OUTPUT_DIR, "failure.json"); const REQUEST_TIMEOUT_MS = Number(process.env.MNOTE_TASK787_STATUS_TIMEOUT_MS || 15_000); const REQUIRED_STATIC_FILES = [ "rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js", "rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js", "scripts/TESTING_REFERENCE.md", ]; const TASK_SMOKE_PATTERN = /^task(?:53|54|76|78)\d.*\.js$/; const SELF_PATH = "scripts/task787-weknora-default-lightrag-legacy-smoke.js"; function writeJson(filePath, payload) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); } function listTaskSmokeFiles() { return fs.readdirSync(path.join(ROOT, "scripts"), { withFileTypes: true }) .filter((entry) => entry.isFile() && TASK_SMOKE_PATTERN.test(entry.name)) .map((entry) => `scripts/${entry.name}`) .sort((left, right) => left.localeCompare(right)); } function uniqueSorted(values) { return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right)); } function extractWindow(line, start, length) { const from = Math.max(0, start - 90); const to = Math.min(line.length, start + length + 90); return line.slice(from, to).trim(); } function allowedLegacyContext(contextBlock, occurrenceWindow) { const lowerBlock = contextBlock.toLowerCase(); const lowerWindow = occurrenceWindow.toLowerCase(); const legacyMarkers = [ "legacy", "fallback", "history", "historical", "debug", "compat", "retired", "assertnotcontains", "assertnotincludes", "forbidden", "notcontains", "notincludes", "not contain", "does not include", "no lightrag", "nolightrag", "nodefaultlightrag", "without lightrag", "bypass", "bypasses", "skip", "旧", "历史", "兼容", "调试", "退役", "软归档", "不应包含", "不作为", "不再", "不直接", "不删除", "不走", "禁止", "绕过", "跳过", "移除", "缺少", ]; return legacyMarkers.some((marker) => lowerWindow.includes(marker) || lowerBlock.includes(marker)); } function ignoredTechnicalField(line) { return /\blightRag(?:DocId|FilePath|Status)\b/.test(line) || /lightrag-source-registry\.json/.test(line); } function ignoredLegacyAssetName(line) { return /task\d+-lightrag-[\w-]+/.test(line); } function ignoredNegativeAssertionName(line) { return /\b(?:uiNoDefault|ingestNo|deleteNo|sectionContextNo|noDefault)LightRag[A-Za-z0-9_]*\b/.test(line); } function classifyActiveLightRagContext(contextBlock, occurrenceWindow) { const normalized = `${contextBlock}\n${occurrenceWindow}`.toLowerCase(); const reasons = []; const activeDefaultPatterns = [ /默认\s*provider/i, /唯一默认/i, /主线\s*provider/i, /default\s*provider/i, /primary\s*provider/i, /mainline\s*provider/i, /only\s*default/i, ]; if (activeDefaultPatterns.some((pattern) => pattern.test(contextBlock) || pattern.test(occurrenceWindow))) { reasons.push("forbidden_default_or_mainline_provider_copy"); } const activeSurfacePatterns = [ /资料库/, /知识库/, /问答/, /检索/, /搜索/, /引用/, /入库/, /dashboard/, /health/, /status/, /source[-_ ]?registry/, /search\s*provider/, /references?/, /retrieval/, /smoke/, ]; if (activeSurfacePatterns.some((pattern) => pattern.test(normalized))) { reasons.push("active_lightrag_surface_without_legacy_marker"); } if (reasons.length === 0) { reasons.push("unqualified_lightrag_reference_without_legacy_marker"); } return reasons; } function scanStaticFiles(files) { const violations = []; const scannedFiles = []; for (const relativePath of files) { const absolutePath = path.join(ROOT, relativePath); if (!fs.existsSync(absolutePath)) { violations.push({ file: relativePath, line: 0, reason: "static_file_missing", context: "required static/smoke baseline file is missing", }); continue; } scannedFiles.push(relativePath); const content = fs.readFileSync(absolutePath, "utf8"); const lines = content.split(/\r?\n/); lines.forEach((line, index) => { const matches = Array.from(line.matchAll(/lightrag/gi)); for (const match of matches) { if (ignoredTechnicalField(line) || ignoredLegacyAssetName(line) || ignoredNegativeAssertionName(line)) { continue; } const occurrenceWindow = extractWindow(line, match.index || 0, match[0].length); const contextBlock = lines.slice(Math.max(0, index - 8), Math.min(lines.length, index + 5)).join("\n"); if (allowedLegacyContext(contextBlock, occurrenceWindow)) { continue; } violations.push({ file: relativePath, line: index + 1, reason: classifyActiveLightRagContext(contextBlock, occurrenceWindow), context: occurrenceWindow, }); } }); } return { scannedFiles, violations }; } async function fetchStatusProviderConfig() { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); const url = `${BASE_URL}/api/knowledge-rag/status`; try { const response = await fetch(url, { headers: { accept: "application/json" }, signal: controller.signal, }); const text = await response.text(); let payload = null; try { payload = text ? JSON.parse(text) : null; } catch (error) { return { ok: false, layer: "status_api_invalid_json", url, status: response.status, error: error.message, rawText: text.slice(0, 1000), providerConfig: null, }; } const providerConfig = payload && typeof payload.providerConfig === "object" ? payload.providerConfig : null; const active = providerConfig && providerConfig.active; const defaultProvider = providerConfig && providerConfig.default; const provider = payload && payload.provider; const ok = response.ok && providerConfig && active === "weknora" && defaultProvider === "weknora"; return { ok, layer: response.ok ? (ok ? "status_provider_config_ok" : "status_provider_config_mismatch") : "status_api_http_error", url, status: response.status, provider, providerConfig, responseSample: ok ? undefined : payload, }; } catch (error) { return { ok: false, layer: error && error.name === "AbortError" ? "status_api_timeout" : "status_api_unavailable", url, error: error && (error.stack || error.message) ? (error.stack || error.message) : String(error), providerConfig: null, }; } finally { clearTimeout(timeout); } } async function main() { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); const staticFiles = uniqueSorted([...REQUIRED_STATIC_FILES, ...listTaskSmokeFiles()]); const staticScan = scanStaticFiles(staticFiles.filter((relativePath) => relativePath !== SELF_PATH)); const statusCheck = await fetchStatusProviderConfig(); const ok = staticScan.violations.length === 0 && statusCheck.ok; const payload = { ok, task: "task787-weknora-default-lightrag-legacy-smoke", baseUrl: BASE_URL, checkedAt: new Date().toISOString(), scannedFiles: staticScan.scannedFiles, violations: staticScan.violations, statusProviderConfig: statusCheck, }; if (ok) { writeJson(RESULT_PATH, payload); console.log(JSON.stringify(payload, null, 2)); return; } writeJson(FAILURE_PATH, payload); console.error(JSON.stringify(payload, null, 2)); process.exit(1); } main().catch((error) => { const payload = { ok: false, task: "task787-weknora-default-lightrag-legacy-smoke", layer: "unexpected", error: error && (error.stack || error.message) ? (error.stack || error.message) : String(error), baseUrl: BASE_URL, }; writeJson(FAILURE_PATH, payload); console.error(JSON.stringify(payload, null, 2)); process.exit(1); });