no longer rely on json
This commit is contained in:
@@ -17,5 +17,9 @@ coverage/
|
|||||||
# Git (Vercel prefers no git dir for CLI deploys)
|
# Git (Vercel prefers no git dir for CLI deploys)
|
||||||
.git/
|
.git/
|
||||||
|
|
||||||
|
# Scripts and task files (build-time only)
|
||||||
|
scripts/
|
||||||
|
tasks/
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const TARGET_HEALTHY = 400;
|
|||||||
* Each disease is I/O-bound (HTTP requests), so high concurrency is safe.
|
* Each disease is I/O-bound (HTTP requests), so high concurrency is safe.
|
||||||
* The global DDG rate limiter prevents us from overwhelming DuckDuckGo.
|
* The global DDG rate limiter prevents us from overwhelming DuckDuckGo.
|
||||||
*/
|
*/
|
||||||
const DISEASE_CONCURRENCY = 30;
|
const DISEASE_CONCURRENCY = 60;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Max DDG requests per second (shared across all concurrent diseases).
|
* Max DDG requests per second (shared across all concurrent diseases).
|
||||||
@@ -68,10 +68,10 @@ const DISEASE_CONCURRENCY = 30;
|
|||||||
* parallel pages = 9 parallel DDG requests per disease at peak.
|
* parallel pages = 9 parallel DDG requests per disease at peak.
|
||||||
* The rate limiter serializes this so we don't get banned.
|
* The rate limiter serializes this so we don't get banned.
|
||||||
*/
|
*/
|
||||||
const DDG_RATE_LIMIT_RPS = 15;
|
const DDG_RATE_LIMIT_RPS = 3;
|
||||||
|
|
||||||
/** Max concurrent image downloads per disease */
|
/** Max concurrent image downloads per disease */
|
||||||
const CONCURRENT_DOWNLOADS = 30;
|
const CONCURRENT_DOWNLOADS = 3;
|
||||||
|
|
||||||
/** Minimum image size in bytes to accept */
|
/** Minimum image size in bytes to accept */
|
||||||
const MIN_IMAGE_SIZE = 10_000; // 10KB
|
const MIN_IMAGE_SIZE = 10_000; // 10KB
|
||||||
@@ -84,7 +84,7 @@ const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp"];
|
|||||||
|
|
||||||
/** User agent for requests */
|
/** User agent for requests */
|
||||||
const UA =
|
const UA =
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1";
|
||||||
|
|
||||||
/** Healthy class directory name */
|
/** Healthy class directory name */
|
||||||
const HEALTHY_CLASS = "healthy";
|
const HEALTHY_CLASS = "healthy";
|
||||||
|
|||||||
@@ -1,20 +1,101 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
/**
|
||||||
|
* Data integrity tests for the plant disease knowledge base.
|
||||||
|
*
|
||||||
|
* These tests validate the seed data directly from the JSON source files.
|
||||||
|
* They ensure every plant and disease entry meets minimum quality standards:
|
||||||
|
* required fields, valid enum values, minimum content counts, and
|
||||||
|
* valid cross-references between plants, diseases, and lookalike IDs.
|
||||||
|
*
|
||||||
|
* The JSON seed data is what populates the Turso/libSQL database.
|
||||||
|
*/
|
||||||
|
|
||||||
import {
|
import { describe, it, expect } from "vitest";
|
||||||
getPlantById,
|
import type { CausalAgentType, Disease, Plant, Severity, Prevalence } from "@/lib/types";
|
||||||
getDiseaseById,
|
|
||||||
getDiseasesByPlantId,
|
// Import seed data directly for validation
|
||||||
getPlantWithDiseases,
|
import rawPlants from "@/data/plants.json";
|
||||||
getDiseaseWithPlant,
|
import rawDiseases from "@/data/diseases.json";
|
||||||
getLookalikeDiseases,
|
|
||||||
searchPlants,
|
const plants = rawPlants as Plant[];
|
||||||
searchDiseases,
|
const diseases = rawDiseases as Disease[];
|
||||||
listPlants,
|
|
||||||
listDiseases,
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
validateKnowledgeBase,
|
|
||||||
plants,
|
function validateKnowledgeBase(): string[] {
|
||||||
diseases,
|
const errors: string[] = [];
|
||||||
} from "@/lib/api/diseases";
|
const validCausalAgentTypes: CausalAgentType[] = [
|
||||||
|
"fungal",
|
||||||
|
"bacterial",
|
||||||
|
"viral",
|
||||||
|
"environmental",
|
||||||
|
];
|
||||||
|
const validSeverities: Severity[] = ["low", "moderate", "high", "critical"];
|
||||||
|
|
||||||
|
const plantIds = new Set(plants.map((p) => p.id));
|
||||||
|
const diseaseIds = new Set(diseases.map((d) => d.id));
|
||||||
|
|
||||||
|
// Duplicate check
|
||||||
|
const seenPlantIds = new Set<string>();
|
||||||
|
for (const plant of plants) {
|
||||||
|
if (seenPlantIds.has(plant.id)) {
|
||||||
|
errors.push(`Duplicate plant ID: ${plant.id}`);
|
||||||
|
}
|
||||||
|
seenPlantIds.add(plant.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seenDiseaseIds = new Set<string>();
|
||||||
|
for (const disease of diseases) {
|
||||||
|
if (seenDiseaseIds.has(disease.id)) {
|
||||||
|
errors.push(`Duplicate disease ID: ${disease.id}`);
|
||||||
|
}
|
||||||
|
seenDiseaseIds.add(disease.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const d of diseases) {
|
||||||
|
if (!plantIds.has(d.plantId)) {
|
||||||
|
errors.push(`Disease "${d.id}" references unknown plant ID: ${d.plantId}`);
|
||||||
|
}
|
||||||
|
if (!validCausalAgentTypes.includes(d.causalAgentType)) {
|
||||||
|
errors.push(`Disease "${d.id}" has invalid causalAgentType: ${d.causalAgentType}`);
|
||||||
|
}
|
||||||
|
if (!validSeverities.includes(d.severity)) {
|
||||||
|
errors.push(`Disease "${d.id}" has invalid severity: ${d.severity}`);
|
||||||
|
}
|
||||||
|
if (d.symptoms.length < 3) {
|
||||||
|
errors.push(`Disease "${d.id}" has fewer than 3 symptoms (${d.symptoms.length})`);
|
||||||
|
}
|
||||||
|
if (d.causes.length < 2) {
|
||||||
|
errors.push(`Disease "${d.id}" has fewer than 2 causes (${d.causes.length})`);
|
||||||
|
}
|
||||||
|
if (d.treatment.length < 3) {
|
||||||
|
errors.push(`Disease "${d.id}" has fewer than 3 treatment steps (${d.treatment.length})`);
|
||||||
|
}
|
||||||
|
if (d.prevention.length < 2) {
|
||||||
|
errors.push(`Disease "${d.id}" has fewer than 2 prevention tips (${d.prevention.length})`);
|
||||||
|
}
|
||||||
|
for (const lookalikeId of d.lookalikeDiseaseIds) {
|
||||||
|
if (!diseaseIds.has(lookalikeId)) {
|
||||||
|
errors.push(`Disease "${d.id}" references unknown lookalike: ${lookalikeId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bidirectionality check
|
||||||
|
for (const d of diseases) {
|
||||||
|
for (const lookalikeId of d.lookalikeDiseaseIds) {
|
||||||
|
const lookalike = diseases.find((ld) => ld.id === lookalikeId);
|
||||||
|
if (lookalike && !lookalike.lookalikeDiseaseIds.includes(d.id)) {
|
||||||
|
errors.push(
|
||||||
|
`Lookalike reference not bidirectional: "${d.id}" references "${lookalikeId}" but not vice versa`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("Knowledge Base Data", () => {
|
describe("Knowledge Base Data", () => {
|
||||||
it("has ≥20 plants", () => {
|
it("has ≥20 plants", () => {
|
||||||
@@ -31,305 +112,6 @@ describe("Knowledge Base Data", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getPlantById", () => {
|
|
||||||
it("returns plant for known ID", () => {
|
|
||||||
const plant = getPlantById("tomato");
|
|
||||||
expect(plant).toBeDefined();
|
|
||||||
expect(plant!.commonName).toBe("Tomato");
|
|
||||||
expect(plant!.scientificName).toBe("Solanum lycopersicum");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns undefined for unknown ID", () => {
|
|
||||||
expect(getPlantById("nonexistent")).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("is case-insensitive", () => {
|
|
||||||
const plant = getPlantById("TOMATO");
|
|
||||||
expect(plant).toBeDefined();
|
|
||||||
expect(plant!.commonName).toBe("Tomato");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getDiseaseById", () => {
|
|
||||||
it("returns disease for known ID", () => {
|
|
||||||
const disease = getDiseaseById("early-blight");
|
|
||||||
expect(disease).toBeDefined();
|
|
||||||
expect(disease!.name).toBe("Early Blight");
|
|
||||||
expect(disease!.plantId).toBe("tomato");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns undefined for unknown ID", () => {
|
|
||||||
expect(getDiseaseById("nonexistent")).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getDiseasesByPlantId", () => {
|
|
||||||
it("returns diseases for tomato", () => {
|
|
||||||
const diseases = getDiseasesByPlantId("tomato");
|
|
||||||
expect(diseases.length).toBeGreaterThanOrEqual(3);
|
|
||||||
expect(diseases.every((d) => d.plantId === "tomato")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty array for plant with no diseases", () => {
|
|
||||||
const diseases = getDiseasesByPlantId("nonexistent");
|
|
||||||
expect(diseases).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getPlantWithDiseases", () => {
|
|
||||||
it("returns plant with diseases for known ID", () => {
|
|
||||||
const result = getPlantWithDiseases("tomato");
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result!.plant.id).toBe("tomato");
|
|
||||||
expect(result!.diseases.length).toBeGreaterThanOrEqual(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns undefined for unknown ID", () => {
|
|
||||||
expect(getPlantWithDiseases("nonexistent")).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getDiseaseWithPlant", () => {
|
|
||||||
it("returns disease with plant for known ID", () => {
|
|
||||||
const result = getDiseaseWithPlant("early-blight");
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result!.disease.id).toBe("early-blight");
|
|
||||||
expect(result!.plant.id).toBe("tomato");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns undefined for unknown ID", () => {
|
|
||||||
expect(getDiseaseWithPlant("nonexistent")).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getLookalikeDiseases", () => {
|
|
||||||
it("returns lookalike diseases for early blight", () => {
|
|
||||||
const lookalikes = getLookalikeDiseases("early-blight");
|
|
||||||
expect(lookalikes.length).toBeGreaterThan(0);
|
|
||||||
// Early blight should reference septoria-leaf-spot and late-blight
|
|
||||||
const lookalikeIds = lookalikes.map((d) => d.id);
|
|
||||||
expect(lookalikeIds).toContain("septoria-leaf-spot");
|
|
||||||
expect(lookalikeIds).toContain("late-blight");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty array for disease with no lookalikes", () => {
|
|
||||||
const lookalikes = getLookalikeDiseases("tomato-powdery-mildew");
|
|
||||||
expect(lookalikes).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("searchPlants", () => {
|
|
||||||
it("returns all plants for empty search", () => {
|
|
||||||
const results = searchPlants("");
|
|
||||||
expect(results).toEqual(plants);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds tomato by common name", () => {
|
|
||||||
const results = searchPlants("tomato");
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
expect(results.some((p) => p.id === "tomato")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds plants by scientific name", () => {
|
|
||||||
const results = searchPlants("Solanum");
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
expect(results.every((p) => p.scientificName.includes("Solanum"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds plants by family", () => {
|
|
||||||
const results = searchPlants("Lamiaceae");
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
expect(results.every((p) => p.family === "Lamiaceae")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds plants by category", () => {
|
|
||||||
const results = searchPlants("houseplant");
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
expect(results.every((p) => p.category === "houseplant")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty array for no matches", () => {
|
|
||||||
const results = searchPlants("xyznonexistent123");
|
|
||||||
expect(results).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("searchDiseases", () => {
|
|
||||||
it("returns all diseases for empty search", () => {
|
|
||||||
const results = searchDiseases("");
|
|
||||||
expect(results).toEqual(diseases);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds diseases by name", () => {
|
|
||||||
const results = searchDiseases("blight");
|
|
||||||
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds diseases by scientific name", () => {
|
|
||||||
const results = searchDiseases("Alternaria");
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds diseases by description content", () => {
|
|
||||||
const results = searchDiseases("calcium");
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("finds diseases by symptom text", () => {
|
|
||||||
const results = searchDiseases("powdery");
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns empty array for no matches", () => {
|
|
||||||
const results = searchDiseases("xyznonexistent123");
|
|
||||||
expect(results).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("listPlants", () => {
|
|
||||||
it("returns all plants with no filters", () => {
|
|
||||||
const results = listPlants();
|
|
||||||
expect(results).toEqual(plants);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters by category", () => {
|
|
||||||
const results = listPlants({ category: "vegetable" });
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
expect(results.every((p) => p.category === "vegetable")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("combines search and category filter", () => {
|
|
||||||
const results = listPlants({ search: "leaf", category: "houseplant" });
|
|
||||||
expect(results.every((p) => p.category === "houseplant")).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("listDiseases", () => {
|
|
||||||
it("returns all diseases with no filters", () => {
|
|
||||||
const results = listDiseases();
|
|
||||||
expect(results).toEqual(diseases);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters by plantId", () => {
|
|
||||||
const results = listDiseases({ plantId: "tomato" });
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
expect(results.every((d) => d.plantId === "tomato")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters by causalAgentType", () => {
|
|
||||||
const results = listDiseases({ causalAgentType: "fungal" });
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
expect(results.every((d) => d.causalAgentType === "fungal")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters by severity", () => {
|
|
||||||
const results = listDiseases({ severity: "critical" });
|
|
||||||
expect(results.length).toBeGreaterThan(0);
|
|
||||||
expect(results.every((d) => d.severity === "critical")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("combines plantId and search filters", () => {
|
|
||||||
const results = listDiseases({ plantId: "tomato", search: "blight" });
|
|
||||||
expect(results.every((d) => d.plantId === "tomato")).toBe(true);
|
|
||||||
expect(results.every((d) => d.name.toLowerCase().includes("blight") || d.description.toLowerCase().includes("blight") || d.symptoms.some((s) => s.toLowerCase().includes("blight")))).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("validateKnowledgeBase", () => {
|
|
||||||
it("returns no errors for valid data", () => {
|
|
||||||
const errors = validateKnowledgeBase();
|
|
||||||
expect(errors).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects invalid plant references", () => {
|
|
||||||
// Temporarily modify a disease to have invalid plantId
|
|
||||||
const original = diseases[0].plantId;
|
|
||||||
diseases[0].plantId = "nonexistent-plant";
|
|
||||||
const errors = validateKnowledgeBase();
|
|
||||||
diseases[0].plantId = original;
|
|
||||||
expect(errors.length).toBeGreaterThan(0);
|
|
||||||
expect(errors.some((e) => e.includes("nonexistent-plant"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects invalid causalAgentType", () => {
|
|
||||||
const original = diseases[0].causalAgentType;
|
|
||||||
(diseases[0] as any).causalAgentType = "invalid-type";
|
|
||||||
const errors = validateKnowledgeBase();
|
|
||||||
diseases[0].causalAgentType = original;
|
|
||||||
expect(errors.length).toBeGreaterThan(0);
|
|
||||||
expect(errors.some((e) => e.includes("invalid-type"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects invalid severity", () => {
|
|
||||||
const original = diseases[0].severity;
|
|
||||||
(diseases[0] as any).severity = "invalid-severity";
|
|
||||||
const errors = validateKnowledgeBase();
|
|
||||||
diseases[0].severity = original;
|
|
||||||
expect(errors.length).toBeGreaterThan(0);
|
|
||||||
expect(errors.some((e) => e.includes("invalid-severity"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects too few symptoms", () => {
|
|
||||||
const original = [...diseases[0].symptoms];
|
|
||||||
diseases[0].symptoms = ["only one"];
|
|
||||||
const errors = validateKnowledgeBase();
|
|
||||||
diseases[0].symptoms = original;
|
|
||||||
expect(errors.length).toBeGreaterThan(0);
|
|
||||||
expect(errors.some((e) => e.includes("fewer than 3 symptoms"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects too few causes", () => {
|
|
||||||
const original = [...diseases[0].causes];
|
|
||||||
diseases[0].causes = ["only one"];
|
|
||||||
const errors = validateKnowledgeBase();
|
|
||||||
diseases[0].causes = original;
|
|
||||||
expect(errors.length).toBeGreaterThan(0);
|
|
||||||
expect(errors.some((e) => e.includes("fewer than 2 causes"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects too few treatments", () => {
|
|
||||||
const original = [...diseases[0].treatment];
|
|
||||||
diseases[0].treatment = ["one", "two"];
|
|
||||||
const errors = validateKnowledgeBase();
|
|
||||||
diseases[0].treatment = original;
|
|
||||||
expect(errors.length).toBeGreaterThan(0);
|
|
||||||
expect(errors.some((e) => e.includes("fewer than 3 treatment"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects too few prevention tips", () => {
|
|
||||||
const original = [...diseases[0].prevention];
|
|
||||||
diseases[0].prevention = ["only one"];
|
|
||||||
const errors = validateKnowledgeBase();
|
|
||||||
diseases[0].prevention = original;
|
|
||||||
expect(errors.length).toBeGreaterThan(0);
|
|
||||||
expect(errors.some((e) => e.includes("fewer than 2 prevention"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects invalid lookalike references", () => {
|
|
||||||
const original = [...diseases[0].lookalikeDiseaseIds];
|
|
||||||
diseases[0].lookalikeDiseaseIds = ["nonexistent-disease"];
|
|
||||||
const errors = validateKnowledgeBase();
|
|
||||||
diseases[0].lookalikeDiseaseIds = original;
|
|
||||||
expect(errors.length).toBeGreaterThan(0);
|
|
||||||
expect(errors.some((e) => e.includes("nonexistent-disease"))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects non-bidirectional lookalike references", () => {
|
|
||||||
// early-blight references septoria-leaf-spot and late-blight
|
|
||||||
// If we remove early-blight from septoria's lookalikes, it should flag
|
|
||||||
const septoria = diseases.find((d) => d.id === "septoria-leaf-spot");
|
|
||||||
if (septoria) {
|
|
||||||
const original = [...septoria.lookalikeDiseaseIds];
|
|
||||||
septoria.lookalikeDiseaseIds = septoria.lookalikeDiseaseIds.filter(
|
|
||||||
(id) => id !== "early-blight"
|
|
||||||
);
|
|
||||||
const errors = validateKnowledgeBase();
|
|
||||||
septoria.lookalikeDiseaseIds = original;
|
|
||||||
expect(errors.some((e) => e.includes("not bidirectional"))).toBe(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Data quality checks", () => {
|
describe("Data quality checks", () => {
|
||||||
it("every disease has ≥3 symptoms", () => {
|
it("every disease has ≥3 symptoms", () => {
|
||||||
for (const d of diseases) {
|
for (const d of diseases) {
|
||||||
@@ -397,4 +179,23 @@ describe("Data quality checks", () => {
|
|||||||
const plantIds = new Set(diseases.map((d) => d.plantId));
|
const plantIds = new Set(diseases.map((d) => d.plantId));
|
||||||
expect(plantIds.size).toBeGreaterThanOrEqual(20);
|
expect(plantIds.size).toBeGreaterThanOrEqual(20);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("every disease has valid prevalence enum value", () => {
|
||||||
|
const validPrevalences: Prevalence[] = ["common", "uncommon", "rare", "very_rare"];
|
||||||
|
for (const d of diseases) {
|
||||||
|
if (d.prevalence !== undefined) {
|
||||||
|
expect(validPrevalences).toContain(d.prevalence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("every plant has required fields", () => {
|
||||||
|
for (const p of plants) {
|
||||||
|
expect(p.id).toBeTruthy();
|
||||||
|
expect(p.commonName).toBeTruthy();
|
||||||
|
expect(p.scientificName).toBeTruthy();
|
||||||
|
expect(p.family).toBeTruthy();
|
||||||
|
expect(p.category).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
// @vitest-environment node
|
// @vitest-environment node
|
||||||
|
|
||||||
import { describe, it, expect, beforeAll } from "vitest";
|
import { describe, it, expect, beforeAll } from "vitest";
|
||||||
import { getDiseaseById } from "@/lib/api/diseases";
|
import { getDiseaseById } from "@/lib/api/diseases-db";
|
||||||
|
|
||||||
const BASE_URL = process.env.TEST_BASE_URL || "http://localhost:3000";
|
const BASE_URL = process.env.TEST_BASE_URL || "http://localhost:3000";
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ describe("POST /api/identify", () => {
|
|||||||
const { data } = await callIdentify(imageId);
|
const { data } = await callIdentify(imageId);
|
||||||
|
|
||||||
for (const pred of data.predictions) {
|
for (const pred of data.predictions) {
|
||||||
const disease = getDiseaseById(pred.diseaseId);
|
const disease = await getDiseaseById(pred.diseaseId);
|
||||||
expect(disease).toBeDefined();
|
expect(disease).toBeDefined();
|
||||||
expect(disease!.id).toBe(pred.diseaseId);
|
expect(disease!.id).toBe(pred.diseaseId);
|
||||||
expect(disease!.name).toBe(pred.disease.name);
|
expect(disease!.name).toBe(pred.disease.name);
|
||||||
@@ -184,7 +184,7 @@ describe("POST /api/identify", () => {
|
|||||||
|
|
||||||
for (let i = 0; i < data.predictions.length - 1; i++) {
|
for (let i = 0; i < data.predictions.length - 1; i++) {
|
||||||
expect(data.predictions[i].confidence.adjusted).toBeGreaterThanOrEqual(
|
expect(data.predictions[i].confidence.adjusted).toBeGreaterThanOrEqual(
|
||||||
data.predictions[i + 1].confidence.adjusted
|
data.predictions[i + 1].confidence.adjusted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
@@ -194,7 +194,7 @@ describe("POST /api/identify", () => {
|
|||||||
|
|
||||||
for (const pred of data.predictions) {
|
for (const pred of data.predictions) {
|
||||||
for (const lookalikeId of pred.lookalikes) {
|
for (const lookalikeId of pred.lookalikes) {
|
||||||
const lookalike = getDiseaseById(lookalikeId);
|
const lookalike = await getDiseaseById(lookalikeId);
|
||||||
expect(lookalike).toBeDefined();
|
expect(lookalike).toBeDefined();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { runInference } from "@/lib/ml/inference";
|
|||||||
import { calibrateConfidence } from "@/lib/ml/confidence";
|
import { calibrateConfidence } from "@/lib/ml/confidence";
|
||||||
import { getDiseaseIdForIndex } from "@/lib/ml/labels";
|
import { getDiseaseIdForIndex } from "@/lib/ml/labels";
|
||||||
import { getModel } from "@/lib/ml/model-loader";
|
import { getModel } from "@/lib/ml/model-loader";
|
||||||
import { getDiseaseById, getPlantById } from "@/lib/api/diseases-db";
|
import { getDiseaseById, getPlantById, getLookalikeDiseases } from "@/lib/api/diseases-db";
|
||||||
import type { IdentifyRequest, IdentifyResponse, PredictionResult } from "@/lib/types";
|
import type { IdentifyRequest, IdentifyResponse, PredictionResult } from "@/lib/types";
|
||||||
|
|
||||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||||
@@ -121,7 +121,7 @@ async function preprocessImageBuffer(buffer: Buffer): Promise<Float32Array> {
|
|||||||
* For each prediction:
|
* For each prediction:
|
||||||
* - Look up disease by ID in knowledge base
|
* - Look up disease by ID in knowledge base
|
||||||
* - Calibrate confidence score
|
* - Calibrate confidence score
|
||||||
* - Include lookalike disease cross-references
|
* - Include lookalike disease cross-references (IDs and full objects)
|
||||||
*
|
*
|
||||||
* @param topPredictions - Top-K raw predictions from inference
|
* @param topPredictions - Top-K raw predictions from inference
|
||||||
* @returns Enriched prediction results
|
* @returns Enriched prediction results
|
||||||
@@ -149,8 +149,10 @@ async function enrichPredictions(
|
|||||||
// Calibrate confidence
|
// Calibrate confidence
|
||||||
const confidence = calibrateConfidence(pred.probability);
|
const confidence = calibrateConfidence(pred.probability);
|
||||||
|
|
||||||
// Get lookalike diseases
|
// Pre-resolve lookalike disease objects server-side so the client
|
||||||
|
// doesn't need sync access to JSON files
|
||||||
const lookalikes = disease.lookalikeDiseaseIds;
|
const lookalikes = disease.lookalikeDiseaseIds;
|
||||||
|
const lookalikeDiseases = await getLookalikeDiseases(diseaseId);
|
||||||
|
|
||||||
// Look up the plant for client convenience
|
// Look up the plant for client convenience
|
||||||
const plant = await getPlantById(disease.plantId).catch(() => null);
|
const plant = await getPlantById(disease.plantId).catch(() => null);
|
||||||
@@ -160,6 +162,7 @@ async function enrichPredictions(
|
|||||||
disease,
|
disease,
|
||||||
confidence,
|
confidence,
|
||||||
lookalikes,
|
lookalikes,
|
||||||
|
lookalikeDiseases,
|
||||||
plant: plant ?? null,
|
plant: plant ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import SymptomChecker from "@/components/SymptomChecker";
|
|||||||
import TreatmentTimeline, { treatmentStepsWithUrgency } from "@/components/TreatmentTimeline";
|
import TreatmentTimeline, { treatmentStepsWithUrgency } from "@/components/TreatmentTimeline";
|
||||||
import LookalikeWarning from "@/components/LookalikeWarning";
|
import LookalikeWarning from "@/components/LookalikeWarning";
|
||||||
import FlagButton from "@/components/FlagButton";
|
import FlagButton from "@/components/FlagButton";
|
||||||
import { getLookalikeDiseases } from "@/lib/api/diseases";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Individual disease result card with expandable sections.
|
* Individual disease result card with expandable sections.
|
||||||
@@ -31,9 +30,9 @@ export default function DiseaseCard({
|
|||||||
const [expanded, setExpanded] = useState(isPrimary);
|
const [expanded, setExpanded] = useState(isPrimary);
|
||||||
const [feedback, setFeedback] = useState<"yes" | "no" | null>(null);
|
const [feedback, setFeedback] = useState<"yes" | "no" | null>(null);
|
||||||
|
|
||||||
const { disease, confidence } = prediction;
|
const { disease, confidence, lookalikeDiseases } = prediction;
|
||||||
const colors = getConfidenceColors(confidence.label);
|
const colors = getConfidenceColors(confidence.label);
|
||||||
const lookalikes = getLookalikeDiseases(disease.id);
|
const lookalikes = lookalikeDiseases ?? [];
|
||||||
|
|
||||||
const toggleExpand = useCallback(() => {
|
const toggleExpand = useCallback(() => {
|
||||||
setExpanded((e) => !e);
|
setExpanded((e) => !e);
|
||||||
|
|||||||
@@ -1,315 +0,0 @@
|
|||||||
/**
|
|
||||||
* Typed helpers to query the plant disease knowledge base.
|
|
||||||
* All functions operate on the JSON seed data files.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {
|
|
||||||
CausalAgentType,
|
|
||||||
Disease,
|
|
||||||
DiseaseListParams,
|
|
||||||
DiseaseWithPlant,
|
|
||||||
Plant,
|
|
||||||
PlantListParams,
|
|
||||||
PlantWithDiseases,
|
|
||||||
Severity,
|
|
||||||
} from "@/lib/types";
|
|
||||||
|
|
||||||
import rawPlants from "@/data/plants.json";
|
|
||||||
import rawDiseases from "@/data/diseases.json";
|
|
||||||
|
|
||||||
// Cast JSON imports to typed arrays
|
|
||||||
const plants: Plant[] = rawPlants as Plant[];
|
|
||||||
const diseases: Disease[] = rawDiseases as Disease[];
|
|
||||||
|
|
||||||
// Re-export raw data for direct access if needed
|
|
||||||
export { plants, diseases };
|
|
||||||
|
|
||||||
// Lookup maps for O(1) access
|
|
||||||
const plantMap = new Map(plants.map((p) => [p.id, p]));
|
|
||||||
const diseaseMap = new Map(diseases.map((d) => [d.id, d]));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a plant by its ID.
|
|
||||||
* @returns The plant or undefined if not found.
|
|
||||||
*/
|
|
||||||
export function getPlantById(id: string): Plant | undefined {
|
|
||||||
return plantMap.get(id.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a disease by its ID.
|
|
||||||
* @returns The disease or undefined if not found.
|
|
||||||
*/
|
|
||||||
export function getDiseaseById(id: string): Disease | undefined {
|
|
||||||
return diseaseMap.get(id.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all diseases for a specific plant.
|
|
||||||
* @returns Array of diseases for the plant.
|
|
||||||
*/
|
|
||||||
export function getDiseasesByPlantId(plantId: string): Disease[] {
|
|
||||||
return diseases.filter(
|
|
||||||
(d) => d.plantId.toLowerCase() === plantId.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a plant with all its associated diseases.
|
|
||||||
* @returns PlantWithDiseases or undefined if plant not found.
|
|
||||||
*/
|
|
||||||
export function getPlantWithDiseases(
|
|
||||||
plantId: string
|
|
||||||
): PlantWithDiseases | undefined {
|
|
||||||
const plant = getPlantById(plantId);
|
|
||||||
if (!plant) return undefined;
|
|
||||||
return {
|
|
||||||
plant,
|
|
||||||
diseases: getDiseasesByPlantId(plantId),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a disease with its associated plant.
|
|
||||||
* @returns DiseaseWithPlant or undefined if disease not found.
|
|
||||||
*/
|
|
||||||
export function getDiseaseWithPlant(
|
|
||||||
diseaseId: string
|
|
||||||
): DiseaseWithPlant | undefined {
|
|
||||||
const disease = getDiseaseById(diseaseId);
|
|
||||||
if (!disease) return undefined;
|
|
||||||
const plant = getPlantById(disease.plantId);
|
|
||||||
if (!plant) return undefined;
|
|
||||||
return { disease, plant };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve lookalike disease IDs to full disease objects.
|
|
||||||
* @returns Array of lookalike diseases.
|
|
||||||
*/
|
|
||||||
export function getLookalikeDiseases(diseaseId: string): Disease[] {
|
|
||||||
const disease = getDiseaseById(diseaseId);
|
|
||||||
if (!disease || !disease.lookalikeDiseaseIds.length) return [];
|
|
||||||
return disease.lookalikeDiseaseIds
|
|
||||||
.map((id) => getDiseaseById(id))
|
|
||||||
.filter((d): d is Disease => d !== undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search plants by term (matches common name, scientific name, family, category).
|
|
||||||
* @param term - Search term (case-insensitive).
|
|
||||||
* @returns Matching plants.
|
|
||||||
*/
|
|
||||||
export function searchPlants(term: string): Plant[] {
|
|
||||||
const lower = term.toLowerCase().trim();
|
|
||||||
if (!lower) return plants;
|
|
||||||
return plants.filter(
|
|
||||||
(p) =>
|
|
||||||
p.commonName.toLowerCase().includes(lower) ||
|
|
||||||
p.scientificName.toLowerCase().includes(lower) ||
|
|
||||||
p.family.toLowerCase().includes(lower) ||
|
|
||||||
p.category.toLowerCase().includes(lower)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search diseases by term (matches name, scientific name, description, symptoms).
|
|
||||||
* @param term - Search term (case-insensitive).
|
|
||||||
* @returns Matching diseases.
|
|
||||||
*/
|
|
||||||
export function searchDiseases(term: string): Disease[] {
|
|
||||||
const lower = term.toLowerCase().trim();
|
|
||||||
if (!lower) return diseases;
|
|
||||||
return diseases.filter(
|
|
||||||
(d) =>
|
|
||||||
d.name.toLowerCase().includes(lower) ||
|
|
||||||
d.scientificName.toLowerCase().includes(lower) ||
|
|
||||||
d.description.toLowerCase().includes(lower) ||
|
|
||||||
d.symptoms.some((s) => s.toLowerCase().includes(lower))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List plants with optional search and category filters.
|
|
||||||
*/
|
|
||||||
export function listPlants(params: PlantListParams = {}): Plant[] {
|
|
||||||
let result = plants;
|
|
||||||
if (params.category) {
|
|
||||||
result = result.filter(
|
|
||||||
(p) => p.category === params.category
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (params.search) {
|
|
||||||
const lower = params.search.toLowerCase().trim();
|
|
||||||
result = result.filter(
|
|
||||||
(p) =>
|
|
||||||
p.commonName.toLowerCase().includes(lower) ||
|
|
||||||
p.scientificName.toLowerCase().includes(lower) ||
|
|
||||||
p.family.toLowerCase().includes(lower) ||
|
|
||||||
p.category.toLowerCase().includes(lower)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List diseases with optional filters.
|
|
||||||
*/
|
|
||||||
export function listDiseases(params: DiseaseListParams = {}): Disease[] {
|
|
||||||
let result = diseases;
|
|
||||||
if (params.plantId) {
|
|
||||||
result = result.filter(
|
|
||||||
(d) => d.plantId.toLowerCase() === params.plantId!.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (params.causalAgentType) {
|
|
||||||
result = result.filter(
|
|
||||||
(d) => d.causalAgentType === params.causalAgentType
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (params.severity) {
|
|
||||||
result = result.filter((d) => d.severity === params.severity);
|
|
||||||
}
|
|
||||||
if (params.search) {
|
|
||||||
const lower = params.search.toLowerCase().trim();
|
|
||||||
result = result.filter(
|
|
||||||
(d) =>
|
|
||||||
d.name.toLowerCase().includes(lower) ||
|
|
||||||
d.scientificName.toLowerCase().includes(lower) ||
|
|
||||||
d.description.toLowerCase().includes(lower) ||
|
|
||||||
d.symptoms.some((s) => s.toLowerCase().includes(lower))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all unique plant IDs that have diseases.
|
|
||||||
*/
|
|
||||||
export function getPlantIdsWithDiseases(): string[] {
|
|
||||||
return [...new Set(diseases.map((d) => d.plantId))];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all unique disease IDs referenced as lookalikes.
|
|
||||||
*/
|
|
||||||
export function getReferencedLookalikeIds(): Set<string> {
|
|
||||||
const ids = new Set<string>();
|
|
||||||
for (const disease of diseases) {
|
|
||||||
for (const lookalikeId of disease.lookalikeDiseaseIds) {
|
|
||||||
ids.add(lookalikeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate knowledge base data integrity.
|
|
||||||
* @returns Array of validation errors (empty = valid).
|
|
||||||
*/
|
|
||||||
export function validateKnowledgeBase(): string[] {
|
|
||||||
const errors: string[] = [];
|
|
||||||
const validCausalAgentTypes: CausalAgentType[] = [
|
|
||||||
"fungal",
|
|
||||||
"bacterial",
|
|
||||||
"viral",
|
|
||||||
"environmental",
|
|
||||||
];
|
|
||||||
const validSeverities: Severity[] = ["low", "moderate", "high", "critical"];
|
|
||||||
|
|
||||||
// Check all plant IDs are unique
|
|
||||||
const plantIds = new Set<string>();
|
|
||||||
for (const plant of plants) {
|
|
||||||
if (plantIds.has(plant.id)) {
|
|
||||||
errors.push(`Duplicate plant ID: ${plant.id}`);
|
|
||||||
}
|
|
||||||
plantIds.add(plant.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check all disease IDs are unique
|
|
||||||
const diseaseIds = new Set<string>();
|
|
||||||
for (const disease of diseases) {
|
|
||||||
if (diseaseIds.has(disease.id)) {
|
|
||||||
errors.push(`Duplicate disease ID: ${disease.id}`);
|
|
||||||
}
|
|
||||||
diseaseIds.add(disease.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check each disease
|
|
||||||
for (const disease of diseases) {
|
|
||||||
// Valid plant reference
|
|
||||||
if (!plantIds.has(disease.plantId)) {
|
|
||||||
errors.push(
|
|
||||||
`Disease "${disease.id}" references unknown plant ID: ${disease.plantId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid causal agent type
|
|
||||||
if (!validCausalAgentTypes.includes(disease.causalAgentType)) {
|
|
||||||
errors.push(
|
|
||||||
`Disease "${disease.id}" has invalid causalAgentType: ${disease.causalAgentType}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid severity
|
|
||||||
if (!validSeverities.includes(disease.severity)) {
|
|
||||||
errors.push(
|
|
||||||
`Disease "${disease.id}" has invalid severity: ${disease.severity}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimum symptom count
|
|
||||||
if (disease.symptoms.length < 3) {
|
|
||||||
errors.push(
|
|
||||||
`Disease "${disease.id}" has fewer than 3 symptoms (${disease.symptoms.length})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimum cause count
|
|
||||||
if (disease.causes.length < 2) {
|
|
||||||
errors.push(
|
|
||||||
`Disease "${disease.id}" has fewer than 2 causes (${disease.causes.length})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimum treatment count
|
|
||||||
if (disease.treatment.length < 3) {
|
|
||||||
errors.push(
|
|
||||||
`Disease "${disease.id}" has fewer than 3 treatment steps (${disease.treatment.length})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimum prevention count
|
|
||||||
if (disease.prevention.length < 2) {
|
|
||||||
errors.push(
|
|
||||||
`Disease "${disease.id}" has fewer than 2 prevention tips (${disease.prevention.length})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid lookalike references
|
|
||||||
for (const lookalikeId of disease.lookalikeDiseaseIds) {
|
|
||||||
if (!diseaseIds.has(lookalikeId)) {
|
|
||||||
errors.push(
|
|
||||||
`Disease "${disease.id}" references unknown lookalike: ${lookalikeId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check lookalike bidirectionality (optional warning, not error)
|
|
||||||
for (const disease of diseases) {
|
|
||||||
for (const lookalikeId of disease.lookalikeDiseaseIds) {
|
|
||||||
const lookalike = getDiseaseById(lookalikeId);
|
|
||||||
if (
|
|
||||||
lookalike &&
|
|
||||||
!lookalike.lookalikeDiseaseIds.includes(disease.id)
|
|
||||||
) {
|
|
||||||
errors.push(
|
|
||||||
`Lookalike reference not bidirectional: "${disease.id}" references "${lookalikeId}" but not vice versa`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
@@ -150,6 +150,8 @@ export interface PredictionResult {
|
|||||||
confidence: ConfidenceResult;
|
confidence: ConfidenceResult;
|
||||||
/** IDs of lookalike diseases that could be confused with this one */
|
/** IDs of lookalike diseases that could be confused with this one */
|
||||||
lookalikes: string[];
|
lookalikes: string[];
|
||||||
|
/** Full disease objects for lookalikes, pre-resolved server-side */
|
||||||
|
lookalikeDiseases: Disease[];
|
||||||
/** The plant this disease affects (included for client convenience) */
|
/** The plant this disease affects (included for client convenience) */
|
||||||
plant: Plant | null;
|
plant: Plant | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,5 +31,14 @@
|
|||||||
".next/dev/types/**/*.ts",
|
".next/dev/types/**/*.ts",
|
||||||
"**/*.mts"
|
"**/*.mts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx", "src/test/**"]
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"scripts",
|
||||||
|
"tasks",
|
||||||
|
"coverage",
|
||||||
|
"data",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"src/test/**"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user