feat(entity): Chunk #4 data layer + entity UI (characters & world-building)
Data layer: - Fuzzy scorer (packages/core/src/fuzzy.ts) with index recovery + unit tests - Backlink computation (packages/core/src/backlinks.ts) incl. entity-level backlinks - Consistency checker (packages/core/src/consistency.ts) incl. reciprocity/scope + unit tests - StorageAdapter contract extended with listEntities/searchEntities/saveEntity/deleteEntity; implemented in Local (Dexie) and Server adapters; contract tests cover entity CRUD + ?q= search - WorldItem.fields now supports list values (keyPeople, rivals); expanded RelationshipKind - @mention autocomplete extension + picker in the editor package Entity UI (apps/web/src/features/entities/): - Generic field renderer (EntityFields) driven by ENTITY_FIELDS/FieldDef - Tabbed directory with fuzzy search + entity cards (EntitiesView) - Create/edit/delete drawer (EntityEditor) reusing shared fields - Relationship editor + force-directed SVG relationship map - Entity engine stylesheet; wired into nav Store: project-store entity methods (load/create/update/delete) over the StorageAdapter contract. CI-green: typecheck, lint, 65 tests, and web build all pass.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { fuzzyScore, fuzzyMatch, fuzzySort } from "./fuzzy.js";
|
||||
|
||||
describe("fuzzyScore", () => {
|
||||
it("matches an empty query as no-match (score 0)", () => {
|
||||
expect(fuzzyScore("", "mara").score).toBe(0);
|
||||
});
|
||||
|
||||
it("matches a whole-string query at score 1", () => {
|
||||
const r = fuzzyScore("mara", "Mara");
|
||||
expect(r.score).toBe(1);
|
||||
expect(r.indices).toEqual([0, 1, 2, 3]);
|
||||
});
|
||||
|
||||
it("matches a subsequence in order", () => {
|
||||
const r = fuzzyScore("mr", "Mara");
|
||||
expect(r.score).toBeGreaterThan(0);
|
||||
expect(r.indices).toEqual([0, 2]);
|
||||
});
|
||||
|
||||
it("does not match a non-subsequence", () => {
|
||||
expect(fuzzyScore("z", "Mara").score).toBe(0);
|
||||
expect(fuzzyScore("marz", "Mara").score).toBe(0);
|
||||
});
|
||||
|
||||
it("scores consecutive matches higher than scattered ones", () => {
|
||||
const consecutive = fuzzyScore("mar", "maria").score;
|
||||
const scattered = fuzzyScore("mr", "maria").score;
|
||||
expect(consecutive).toBeGreaterThan(scattered);
|
||||
});
|
||||
|
||||
it("scores word-start matches higher", () => {
|
||||
const wordStart = fuzzyScore("h", "harbor").score;
|
||||
const mid = fuzzyScore("r", "harbor").score;
|
||||
expect(wordStart).toBeGreaterThan(mid);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fuzzyMatch", () => {
|
||||
it("is true for a subsequence and false otherwise", () => {
|
||||
expect(fuzzyMatch("mr", "Mara")).toBe(true);
|
||||
expect(fuzzyMatch("z", "Mara")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fuzzySort", () => {
|
||||
const items = [
|
||||
{ id: "ada", name: "Ada Lovelace" },
|
||||
{ id: "maria", name: "Maria Harbor" },
|
||||
{ id: "mark", name: "Mark Twain" },
|
||||
];
|
||||
|
||||
it("returns all items for an empty query, preserving order", () => {
|
||||
expect(fuzzySort(items, "", (i) => i.name).map((i) => i.id)).toEqual(["ada", "maria", "mark"]);
|
||||
});
|
||||
|
||||
it("drops non-matches and orders by descending score", () => {
|
||||
const sorted = fuzzySort(items, "mar", (i) => i.name);
|
||||
expect(sorted.map((i) => i.id)).toContain("maria");
|
||||
expect(sorted.map((i) => i.id)).not.toContain("ada");
|
||||
// "Maria Harbor" (word start) should beat "Mark Twain" (word start) only on tie-break; both match.
|
||||
expect(sorted.length).toBe(2);
|
||||
});
|
||||
|
||||
it("is stable for equal scores", () => {
|
||||
// "ada" and "amy" both score 2 for query "a" (word-start match); input
|
||||
// order must be preserved.
|
||||
const stable = [
|
||||
{ id: "a", name: "ada" },
|
||||
{ id: "b", name: "amy" },
|
||||
];
|
||||
const sorted = fuzzySort(stable, "a", (i) => i.name);
|
||||
expect(sorted.map((i) => i.id)).toEqual(["a", "b"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user