Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
9925fb1
Do morph type filtering server-side
myieye Jan 16, 2026
1b7dbca
temp
myieye Jan 16, 2026
1c68ec7
Add Delayed component to wrap VList items
myieye Jan 16, 2026
bf08d97
Move SortMenu and extract options
myieye Jan 16, 2026
9b4af17
Temp virtual scrolling entry loader plan
myieye Jan 16, 2026
5b18db1
Add entry loader service
myieye Jan 16, 2026
17d8573
Use entry loader service in entries-list
myieye Jan 16, 2026
a6d782d
Make agent instructions leaner
myieye Jan 19, 2026
a3c1115
Add virtual scrolling tests
myieye Jan 19, 2026
1a8bd73
AI feedback
myieye Jan 19, 2026
c05461d
Remove old service
myieye Jan 19, 2026
7300a78
Add entry-loader-service tests
myieye Jan 19, 2026
d999fbb
Prepare backend API for querying entry index
myieye Jan 20, 2026
e09faf4
v2 test improvements with added event handling
myieye Jan 21, 2026
889cdc9
Add more failing event handling test cases
myieye Jan 20, 2026
a0df9ac
Frontend: Add entry index lookup, versions and invalidation
myieye Jan 20, 2026
f91dda4
Format
myieye Jan 20, 2026
33b1232
Tweak demo api
myieye Jan 20, 2026
8958896
Refactor
myieye Jan 20, 2026
a46f7fd
Flesh out entry-loader-service tests
myieye Jan 20, 2026
b633016
Simplify entry-loader by making api required
myieye Jan 21, 2026
983adf4
Use quiet reset for all events
myieye Jan 21, 2026
ed428da
Debounce quiet resets
myieye Jan 21, 2026
bafd37d
Fix sqlite contains function not always registered
myieye Jan 21, 2026
7358f3a
Handle restoring deleted entries
myieye Jan 22, 2026
2e9014b
Fix stale results and flicker
myieye Jan 22, 2026
46c66a7
Fix gitignore
myieye Jan 22, 2026
33e75fd
Fix and extend e2e coverage
myieye Jan 22, 2026
f937cb5
Fix tracking selected entry
myieye Jan 22, 2026
bd165f3
Finish types and backend
myieye Jan 22, 2026
a1c977d
Fix scroll flag
myieye Jan 22, 2026
fc775cb
Fix tests
myieye Jan 22, 2026
38d1f97
Remove entry version tracking - generation is enough
myieye Jan 22, 2026
d67dc98
Basic update event optimizations
myieye Jan 22, 2026
160e63e
Optimize and test GetEntryIndex
myieye Jan 23, 2026
c87d4b0
AI feedback
myieye Jan 23, 2026
1f5779a
Fix linting errors: floating promises and unused variables
myieye Jan 23, 2026
7c4f461
Extract i18n strings: Filter, This was deleted
myieye Jan 23, 2026
51cbd15
PR feedback
myieye Jan 23, 2026
9adcf02
Fix boolean logic error
myieye Jan 23, 2026
d89fb41
i18n:extract and ai enhance
myieye Jan 23, 2026
7125a25
fix(viewer): stabilize project view snapshots by waiting for rows
myieye Jan 23, 2026
33719d5
Fix screenshot tests
myieye Jan 26, 2026
405e228
Fix path
myieye Jan 27, 2026
6bda4cc
Fix design doc API names
myieye Jan 27, 2026
35d76b4
Don't rely on potentially stale indexes.
myieye Jan 27, 2026
b7461cc
Fix race condition
myieye Jan 27, 2026
7f8a5f3
Fix race condition v2
myieye Jan 27, 2026
8e67834
Cleanup entry-loader service
myieye Jan 28, 2026
2d9d562
Fix self-referencing derived in entryLoader
myieye Jan 29, 2026
b7d7d17
Cleanup
myieye Jan 30, 2026
794dbbd
Improve morphtype tests
myieye Jan 30, 2026
6d319bc
Refactor entries-list tests
myieye Jan 30, 2026
3eab410
Refactor entry-edit-persists tests and page and component models
myieye Jan 30, 2026
5e36d36
Refactor EntryLoaderService for clarity and maintainability
claude Jan 31, 2026
f2177d4
Refactor EntryLoaderService to use unified reset queue
claude Feb 2, 2026
2de10c0
Refactor ResetQueue to use promise-based debouncing
claude Feb 2, 2026
3cc3829
Add Debouncer utility and simplify EntryLoaderService
claude Feb 2, 2026
2be7818
Remove verbose comments from EntryLoaderService and Debouncer
claude Feb 2, 2026
2154e18
Use runed's useDebounce instead of custom Debouncer class
claude Feb 2, 2026
0b1dd60
Fix race conditions in reset logic
claude Feb 2, 2026
e6908f3
Queue event resets that occur during filter reset
claude Feb 3, 2026
ec40f8b
Add reset flow diagram and skip initial debounce
claude Feb 3, 2026
5465d7e
Move reset flow diagram to Mermaid markdown file
claude Feb 3, 2026
98a95fa
Cleanup
myieye Feb 3, 2026
9f7fe15
Fix test
myieye Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 0 additions & 60 deletions .github/copilot-instructions.md

This file was deleted.

91 changes: 38 additions & 53 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,14 @@ This is a monorepo containing:
- **FwLite** - A lightweight FieldWorks application (MAUI desktop app)
- **FwHeadless** - A headless service for FieldWorks data processing

## Tech Stack
### Tech Stack

- **Backend**: .NET 9, C#, Entity Framework Core, GraphQL (Hot Chocolate)
- **Frontend**: SvelteKit, TypeScript
- **Database**: PostgreSQL
- **Infrastructure**: Docker, Kubernetes, Skaffold, Tilt

## Development Commands

```bash
# Backend
dotnet build backend/LexBoxApi/LexBoxApi.csproj
dotnet test

# FwLite (Windows)
dotnet build backend/FwLite/FwLiteMaui/FwLiteMaui.csproj --framework net9.0-windows10.0.19041.0

# Frontend
cd frontend && pnpm dev
```

## Project Structure
### Structure

```text
languageforge-lexbox/
Expand All @@ -39,68 +25,67 @@ languageforge-lexbox/
│ ├── FwLite/ # FwLite MAUI app
│ ├── FwHeadless/ # Headless FW service
│ └── Testing/ # Test projects
├── frontend/ # SvelteKit web app
├── frontend/ # Lexbox SvelteKit web app
├── frontend/viewer/ # FieldWorks Lite frontend Svelte code
└── deployment/ # K8s/Docker configs
```

**IMPORTANT: Testing Policy**
- ❌ **Do NOT run integration tests** (`dotnet test`) unless the user explicitly asks
- Integration tests require full test infrastructure (database, services) and take significant time
- Only run unit tests locally when verifying critical business logic
- User must explicitly request test runs before executing them

### Checking GitHub Issues and PRs

When asked to check GitHub issues or PRs, use the `gh` CLI instead of browser tools:

```bash
# List open issues
gh issue list --limit 30

# List open PRs
gh pr list --limit 30

# View specific issue or PR
gh issue view <number>
gh pr view <number>
```

- If the user asks about "the" PR, but does not explicitly name a PR or branch, assume they mean the PR associated with the current branch.

Provide an in-conversation summary highlighting:
- Urgent/critical issues (regressions, bugs, broken builds)
- Common themes or patterns
- Items needing immediate attention

**Why CLI over browser**: Faster, less tokens, easier to scan and discuss.

### Important Files

Key documentation for this project:
- `README.md` - Project overview and setup
- `AGENTS.md` - You are here! Agent instructions
- `.github/copilot-instructions.md` - GitHub Copilot auto-loaded instructions
- `.github/AGENTS.md` - **CI/CD and deployment guide** (workflows, K8s, Docker)
- `docs/DEVELOPER-win.md` - Windows development setup
- `docs/DEVELOPER-linux.md` - Linux development setup
- `docs/DEVELOPER-osx.md` - macOS development setup
- `backend/README.md` - Backend architecture
- `backend/FwLite/AGENTS.md` - **FwLite/CRDT critical code guide** (data loss risks!)
- `backend/FwHeadless/AGENTS.md` - **FwHeadless sync guide**
- `backend/AGENTS.md` - General backend guidelines
- `backend/LexBoxApi/AGENTS.md` - API & GraphQL specific rules
- `backend/FwLite/AGENTS.md` - **FwLite/CRDT** (Critical code! Data loss risks!)
- `backend/FwHeadless/AGENTS.md` - **FwHeadless guide** (Critical code! Data loss risks! Mercurial sync, FwData processing)
- `frontend/AGENTS.md` - General frontend/SvelteKit rules
- `frontend/viewer/AGENTS.md` - **FwLite Viewer** (Specific frontend rules)
- `deployment/README.md` - Deployment and infrastructure

## Guidelines

### Testing

- ❌ **Do NOT run dotnet INTEGRATION tests** unless the user explicitly asks. They require full test infrastructure (database, services) which usually isn't available.
- ✅ **DO run unit tests locally** and filter to the tests that are relevant to the changes you are making. Use IDE testing tools over the cli.

### Questions?

- Check existing issues: `gh issue list --limit 30`
- Look at recent commits: `git log --oneline -20`
- Read the docs in `docs/` directory
- Create a GitHub issue if unsure
- Ask the user to clarify

### Pre-Flight Check

Before implementing any change that will touch many files or is in a 🔴 **Critical** area (FwLite sync, FwHeadless) do a "Pre-Flight Check" and list every component in the chain that will be touched (e.g., MiniLcm -> LcmCrdt -> FwDataBridge -> SyncHelper).

### Important Rules

- ✅ Use GitHub Issues for task tracking
- ✅ **ALWAYS read local `AGENTS.md` files** in the directories you are working in (and their parents) before starting.
- ✅ **ALWAYS review relevant code paths** before asking clarification questions.
- ✅ New instructions in AGENTS.md files should be SUCCINCT.
- ✅ Use `gh` CLI for GitHub issues/PRs, not browser tools
- ✅ When pulling PR comments with `gh` use `api`. It's the only thing that returns review comments.
- ✅ If the user asks about "the" PR, but does not explicitly name a PR or branch, assume they mean the PR associated with the current branch.
- ✅ Use **Mermaid diagrams** for flowcharts and architecture (not ASCII art)
- ✅ Prefer IDE diagnostics (compiler/lint errors) over CLI tools for identifying issues. Fixing these diagnostics is part of completing any instruction.
- ✅ Do NOT run integration tests unless user explicitly requests
- ❌ Do NOT use ASCII art for diagrams (use Mermaid instead)
- ✅ When handling a user prompt ALWAYS ask for clarification if there are details to clarify, important decisions that must be made first or the plan sounds unwise
- ❌ Do NOT git commit or git push without explicit user approval

### 🛡️ VIGILANCE

- ❌ **NEVER "fix" a failure** by removing assertions, commenting out code, or changing data to match a broken implementation.
- ✅ **ALWAYS fix the root cause** when a test or check fails.
- ✅ **ALWAYS double-check** that your "fix" hasn't made a check or test meaningless (e.g., asserting `expect(true).toBe(true)`).
- ✅ **Assert that E2E test user actions** e.g. (scroll, click, etc.) actually have the expected effect before proceeding further.

If you are struggling, explain the difficulty to the user instead of cheating. **Integrity is non-negotiable.**
11 changes: 11 additions & 0 deletions backend/FwLite/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ dotnet test FwLiteOnly.slnf
dotnet build FwLiteMaui/FwLiteMaui.csproj --framework net9.0-windows10.0.19041.0
```

## Generated Types (TypeScript)

The frontend viewer uses TypeScript types and API interfaces generated from .NET using **Reinforced.Typings**. These are automatically updated when you build the **FwLiteShared** project (or any project that depends on it like `FwLiteMaui` or `FwLiteWeb`).

```bash
# To manually update generated types:
dotnet build backend/FwLite/FwLiteShared/FwLiteShared.csproj
```

The configuration for this lives in `FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs` and `FwLiteShared/Reinforced.Typings.settings.xml`.

## Project Structure

| Directory | Priority | Purpose |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using FwDataMiniLcmBridge.Tests.Fixtures;

namespace FwDataMiniLcmBridge.Tests.MiniLcmTests;

[Collection(ProjectLoaderFixture.Name)]
public class EntryIndexTests(ProjectLoaderFixture fixture) : EntryIndexTestsBase
{
protected override Task<IMiniLcmApi> NewApi()
{
return Task.FromResult<IMiniLcmApi>(fixture.NewProjectApi("entry-index-test", "en", "en"));
}
}
38 changes: 30 additions & 8 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -916,23 +916,27 @@ public IAsyncEnumerable<Entry> GetEntries(
Func<ILexEntry, bool>? predicate, QueryOptions? options = null, string? query = null)
{
options ??= QueryOptions.Default;
var entries = GetLexEntries(predicate, options);

entries = ApplySorting(options, entries, query);
var entries = GetFilteredAndSortedEntries(predicate, options, options.Order, query);
entries = options.ApplyPaging(entries);

return entries.ToAsyncEnumerable().Select(FromLexEntry);
}

private IEnumerable<ILexEntry> ApplySorting(QueryOptions options, IEnumerable<ILexEntry> entries, string? query)
private IEnumerable<ILexEntry> GetFilteredAndSortedEntries(Func<ILexEntry, bool>? predicate, FilterQueryOptions? filterOptions, SortOptions order, string? query)
{
var entries = GetLexEntries(predicate, filterOptions);
return ApplySorting(order, entries, query);
}

private IEnumerable<ILexEntry> ApplySorting(SortOptions order, IEnumerable<ILexEntry> entries, string? query)
{
var sortWs = GetWritingSystemHandle(options.Order.WritingSystem, WritingSystemType.Vernacular);
if (options.Order.Field == SortField.SearchRelevance)
var sortWs = GetWritingSystemHandle(order.WritingSystem, WritingSystemType.Vernacular);
if (order.Field == SortField.SearchRelevance)
{
return entries.ApplyRoughBestMatchOrder(options.Order, sortWs, query);
return entries.ApplyRoughBestMatchOrder(order, sortWs, query);
}

return options.ApplyOrder(entries, e => e.LexEntryHeadword(sortWs));
return order.ApplyOrder(entries, e => e.LexEntryHeadword(sortWs));
}

public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options = null)
Expand All @@ -955,6 +959,24 @@ public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options
return Task.FromResult(lexEntry is null ? null : FromLexEntry(lexEntry));
}

public Task<int> GetEntryIndex(Guid entryId, string? query = null, IndexQueryOptions? options = null)
{
var predicate = EntrySearchPredicate(query);
var entries = GetFilteredAndSortedEntries(predicate, options, options?.Order ?? SortOptions.Default, query);

var rowIndex = 0;
foreach (var entry in entries)
{
if (entry.Guid == entryId)
{
return Task.FromResult(rowIndex);
}
rowIndex++;
}

return Task.FromResult(-1);
}

public async Task<Entry> CreateEntry(Entry entry, CreateEntryOptions? options = null)
{
options ??= CreateEntryOptions.Everything;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class LexEntryFilterMapProvider : EntryFilterMapProvider<ILexEntry>

public override Expression<Func<ILexEntry, string, object>> EntryCitationForm => (entry, ws) => entry.PickText(entry.CitationForm, ws);
public override Expression<Func<ILexEntry, string, object>> EntryLiteralMeaning => (entry, ws) => entry.PickText(entry.LiteralMeaning, ws);
public override Expression<Func<ILexEntry, object?>> EntryMorphType => e => LcmHelpers.FromLcmMorphType(e.PrimaryMorphType);
public override Expression<Func<ILexEntry, object?>> EntryComplexFormTypes => e => EmptyToNull(e.ComplexFormEntryRefs.SelectMany(r => r.ComplexEntryTypesRS));
public override Func<string, object>? EntryComplexFormTypesConverter => EntryFilter.NormalizeEmptyToNull<ILexEntryType>;
}
6 changes: 6 additions & 0 deletions backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ public Task<int> CountEntries(string? query, FilterQueryOptions? options)
return Task.Run(() => _wrappedApi.CountEntries(query, options));
}

[JSInvokable]
public Task<int> GetEntryIndex(Guid id, string? query, IndexQueryOptions? options)
{
return Task.Run(() => _wrappedApi.GetEntryIndex(id, query, options));
}

[JSInvokable]
public Task<Entry[]> GetEntries(QueryOptions? options = null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ private static void ConfigureMiniLcmTypes(ConfigurationBuilder builder)
builder.ExportAsInterfaces([
typeof(QueryOptions),
typeof(FilterQueryOptions),
typeof(IndexQueryOptions),
typeof(SortOptions),
typeof(ExemplarOptions),
typeof(EntryFilter),
Expand Down
23 changes: 23 additions & 0 deletions backend/FwLite/FwLiteWeb/Routes/MiniLcmRoutes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ await projectProvider.OpenProject(project, context.HttpContext.RequestServices)
api.MapGet("/entries", MiniLcm.GetEntries);
api.MapGet("/entries/{search}", MiniLcm.SearchEntries);
api.MapGet("/entry/{id:Guid}", MiniLcm.GetEntry);
api.MapGet("/entry/{id:Guid}/index", MiniLcm.GetEntryIndex);
api.MapGet("/parts-of-speech", MiniLcm.GetPartsOfSpeech);
api.MapGet("/semantic-domains", MiniLcm.GetSemanticDomains);
api.MapGet("/publications", MiniLcm.GetPublications);
Expand Down Expand Up @@ -137,6 +138,15 @@ public static IAsyncEnumerable<Entry> SearchEntries([FromServices] MiniLcmHolder
return api.GetEntry(id);
}

public static Task<int> GetEntryIndex(
Guid id,
[AsParameters] MiniLcmQueryOptions options,
[FromServices] MiniLcmHolder holder)
{
var api = holder.MiniLcmApi;
return api.GetEntryIndex(id, null, options.ToIndexQueryOptions());
}

public static IAsyncEnumerable<PartOfSpeech> GetPartsOfSpeech([FromServices] MiniLcmHolder holder)
{
var api = holder.MiniLcmApi;
Expand Down Expand Up @@ -185,6 +195,19 @@ public QueryOptions ToQueryOptions()
string.IsNullOrEmpty(GridifyFilter) ? null : new EntryFilter {GridifyFilter = GridifyFilter});
}

public IndexQueryOptions ToIndexQueryOptions()
{
ExemplarOptions? exemplarOptions = string.IsNullOrEmpty(ExemplarValue) || ExemplarWritingSystem is null
? null
: new(ExemplarValue, ExemplarWritingSystem);
var sortField = SortField ?? SortOptions.Default.Field;
return new IndexQueryOptions(new SortOptions(sortField,
SortWritingSystem ?? SortOptions.Default.WritingSystem,
Ascending ?? SortOptions.Default.Ascending),
exemplarOptions,
string.IsNullOrEmpty(GridifyFilter) ? null : new EntryFilter {GridifyFilter = GridifyFilter});
}

public SortField? SortField { get; set; } = SortOptions.Default.Field;

[DefaultValue(SortOptions.DefaultWritingSystem)]
Expand Down
26 changes: 26 additions & 0 deletions backend/FwLite/LcmCrdt.Tests/MiniLcmTests/EntryIndexTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Xunit.Abstractions;

namespace LcmCrdt.Tests.MiniLcmTests;

public class EntryIndexTests(ITestOutputHelper output) : EntryIndexTestsBase
{
private readonly MiniLcmApiFixture _fixture = new();

public override async Task InitializeAsync()
{
_fixture.LogTo(output);
await _fixture.InitializeAsync();
await base.InitializeAsync();
}

protected override Task<IMiniLcmApi> NewApi()
{
return Task.FromResult<IMiniLcmApi>(_fixture.Api);
}

public override async Task DisposeAsync()
{
await base.DisposeAsync();
await _fixture.DisposeAsync();
}
}
Loading
Loading