Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8e82fe8
Do morph type filtering server-side
myieye Jan 16, 2026
22859d2
temp
myieye Jan 16, 2026
c19b768
Add Delayed component to wrap VList items
myieye Jan 16, 2026
8ea5a96
Move SortMenu and extract options
myieye Jan 16, 2026
486cb63
Temp virtual scrolling entry loader plan
myieye Jan 16, 2026
512faf2
Add entry loader service
myieye Jan 16, 2026
a24be28
Use entry loader service in entries-list
myieye Jan 16, 2026
b0154ce
Make agent instructions leaner
myieye Jan 19, 2026
a4ec425
Add virtual scrolling tests
myieye Jan 19, 2026
61524df
AI feedback
myieye Jan 19, 2026
ca3f582
Remove old service
myieye Jan 19, 2026
85b76c5
Add entry-loader-service tests
myieye Jan 19, 2026
7fc9515
Prepare backend API for querying entry index
myieye Jan 20, 2026
8e27a54
v2 test improvements with added event handling
myieye Jan 21, 2026
baa5843
Add more failing event handling test cases
myieye Jan 20, 2026
0daa812
Frontend: Add entry index lookup, versions and invalidation
myieye Jan 20, 2026
dcf2852
Format
myieye Jan 20, 2026
e11c9e0
Tweak demo api
myieye Jan 20, 2026
050fdec
Refactor
myieye Jan 20, 2026
0b5c073
Flesh out entry-loader-service tests
myieye Jan 20, 2026
330aeaa
Simplify entry-loader by making api required
myieye Jan 21, 2026
d1e99a6
Use quiet reset for all events
myieye Jan 21, 2026
949312c
Debounce quiet resets
myieye Jan 21, 2026
d32d44f
Fix sqlite contains function not always registered
myieye Jan 21, 2026
8ddd1d4
Handle restoring deleted entries
myieye Jan 22, 2026
41461b0
Fix stale results and flicker
myieye Jan 22, 2026
0e6b7e7
Fix gitignore
myieye Jan 22, 2026
93bcb80
Fix and extend e2e coverage
myieye Jan 22, 2026
ab606b3
Fix tracking selected entry
myieye Jan 22, 2026
7369263
Finish types and backend
myieye Jan 22, 2026
2f969cb
Fix scroll flag
myieye Jan 22, 2026
ee00c87
Fix tests
myieye Jan 22, 2026
9ecc968
Remove entry version tracking - generation is enough
myieye Jan 22, 2026
cfcdbbd
Basic update event optimizations
myieye Jan 22, 2026
b0417a4
Optimize and test GetEntryIndex
myieye Jan 23, 2026
490046b
AI feedback
myieye Jan 23, 2026
32d497d
Fix linting errors: floating promises and unused variables
myieye Jan 23, 2026
c1efd87
Extract i18n strings: Filter, This was deleted
myieye Jan 23, 2026
20937c0
PR feedback
myieye Jan 23, 2026
0f45998
Fix boolean logic error
myieye Jan 23, 2026
06a2534
i18n:extract and ai enhance
myieye Jan 23, 2026
340f407
fix(viewer): stabilize project view snapshots by waiting for rows
myieye Jan 23, 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.

88 changes: 37 additions & 51 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,66 +25,66 @@ 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>
```

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.
- ✅ 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"));
}
}
31 changes: 25 additions & 6 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -918,21 +918,21 @@ public IAsyncEnumerable<Entry> GetEntries(
options ??= QueryOptions.Default;
var entries = GetLexEntries(predicate, options);

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

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

private IEnumerable<ILexEntry> ApplySorting(QueryOptions options, IEnumerable<ILexEntry> entries, string? 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 +955,25 @@ 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 = GetLexEntries(predicate, options);
entries = ApplySorting(options?.Order ?? SortOptions.Default, entries, 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();
}
}
6 changes: 6 additions & 0 deletions backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,12 @@ public async IAsyncEnumerable<Entry> SearchEntries(string? query, QueryOptions?
return await repo.GetEntry(id);
}

public async Task<int> GetEntryIndex(Guid entryId, string? query = null, IndexQueryOptions? options = null)
{
await using var repo = await repoFactory.CreateRepoAsync();
return await repo.GetEntryIndex(entryId, query, options);
}

public async Task BulkCreateEntries(IAsyncEnumerable<Entry> entries)
{
await using var repo = await repoFactory.CreateRepoAsync();
Expand Down
Loading
Loading