Skip to content

Latest commit

 

History

History
297 lines (214 loc) · 28.9 KB

AGENTS.md

File metadata and controls

297 lines (214 loc) · 28.9 KB

AGENTS.md — lsFusion platform

General engineering and process notes for AI coding agents (Codex, Cursor, Claude Code, Gemini CLI, GitHub Copilot). Feature-level lore stays in commit messages, code, and per-agent memory — this file is for durable cross-cutting process knowledge.

For documentation-only tasks under docs/, additional rules apply — see docs/AGENTS.md. That file complements the process rules here (commit conventions, issue policy, risk discipline still apply to docs commits); it does not replace them.

Workspace assumption. Several sections reference sibling directories (../plugin-idea/, ../docusaurus/, ../mcp/, application projects like ../mycompany/). They assume the canonical lsfusion-aggregate/ super-workspace where platform/ sits next to those repos. In a standalone clone the sibling-path instructions won't resolve — adjust paths or skip those steps as needed.

Repo layout

Maven multi-module project. Top-level modules: api/, ai/, build/, desktop-client/, server/, web-client/, tests/, docs/.

  • server/ — Java server, core platform logic.
    • server/src/main/java/lsfusion/server/... — Java sources.
    • server/src/main/lsfusion/ — system .lsf modules shipped with the platform (Service.lsf, SystemEvents.lsf, Chat.lsf, etc.).
    • server/src/main/antlr3/lsfusion/server/language/LsfLogics.g — ANTLR3 grammar source. Generated parser/lexer is written into src/main/java (path configured in server/pom.xml); never hand-edit generated files.
  • web-client/ — GWT web client.
  • desktop-client/ — Swing/Java desktop client.
  • tests/ — the platform's own compile corpus and server behaviour tests, see Tests.
  • docs/<type>/{en,ru}/ — primary documentation, type-first (type ∈ language, paradigm, how-to, brief, rules). The sibling ../docusaurus/ consumes it — see the Documentation section for the rules.

Sister repository

The IntelliJ plugin lives next door at ../plugin-idea/. Grammar changes in LsfLogics.g typically require parallel updates to the plugin's LSF.bnf (and sometimes LSF.flex if new keywords are introduced). Coordinate commits across the two repos (cross-link in commit messages, or close the same GitHub issue from both sides).

Build and run

The platform is a framework — runtime needs to be paired with an lsFusion application from a sibling repo (../mycompany/, ../erp/, …).

Build commands

# Build (skip tests). Add -am to also rebuild upstream deps.
mvn -pl server install -DskipTests              # server only (deps already built)
mvn -pl server -am clean install -DskipTests    # server + ai/api/build/base
mvn -pl web-client gwt:compile -q               # GWT client only
# Append -o for offline mode (deps must already be cached).

# Run JUnit tests (live in <module>/src/test/java, skipped unless asked for - see Tests).
mvn -pl <module> test -Dmaven.test.skip=false    # or -am test to rebuild deps first
mvn -pl web-client test -Dmaven.test.skip=false -Dgwt.skipCompilation   # web-client without its GWT compile

-DskipTests is a build verification, not a test run — don't claim "tests passed" in a commit body unless a test target actually ran (and say which one).

ANTLR3 sources regenerate during generate-sources via antlr3-maven-plugin. A clean build (or just generate-sources) is required after LsfLogics.g changes before new keywords reach downstream code; no regen is needed for .lsf-only edits.

When to rebuild and restart

A "verification" with the wrong restart verifies the old code. Use this matrix:

Change Rebuild Restart
Server Java mvn -pl server -am install -DskipTests BLB
ANTLR grammar (LsfLogics.g) mvn -pl server -am clean install -DskipTests BLB
Platform's own .lsf under server/src/main/lsfusion/ mvn -pl server resources:resources (or a full server rebuild) BLB
Application .lsf in a sibling app (../mycompany/, ../erp/, …) none BLB
Web-client Java / GWT mvn -pl web-client gwt:compile Jetty (or page reload if hot-deploy is on)
api/ protocol rebuild api/ + dependent server/client modules together BLB and Jetty
Docs only (docs/) none none

Topology

Three cooperating processes:

  • BLB (lsfusion.server.logics.BusinessLogicsBootstrap) — lsFusion server. Loads .lsf modules from the application project (e.g. ../mycompany/, also its working directory), connects to Postgres, exposes RMI. Classpath built from server/target/; needs --add-opens=java.base/java.{util,lang}=ALL-UNNAMED.
  • Jetty — serves the GWT web client, proxies HTTP/RMI to BLB. Working dir platform/; listens on http://localhost:8080/main.
  • Postgres — local; compose.yaml defines the canonical docker stack.

Normally started from IDE run configurations. For headless automation, capture each command line once via cat /proc/<pid>/cmdline | tr '\0' '\n' and re-exec; shape is stable across sessions. Redirect stdout/stderr to logs and wait for the readiness markers below.

Smoke tests

  • BLB ready: tail its log for Application '<name>' is ready / Server started / Ready for connections.
  • Jetty ready: curl -sI http://localhost:8080/main returns a 2xx/3xx status — a 3xx to a login URL is normal. A 200 alone does not confirm the lsFusion UI loaded; that requires the browser/CDP check below.
  • Poll with a bounded loop (e.g. 30 iterations × 5s sleep) until both signals show up before driving the UI.

UI / visual verification (headless Chrome via CDP)

For GWT-rendered forms, popovers, screenshots, layout — anything a log message can't confirm — run Chrome headlessly:

google-chrome --headless=new --remote-debugging-port=9333 \
  --user-data-dir=/tmp/chrome-profile-headless --window-size=1280,800 &

Persistent --user-data-dir keeps a one-time login across sessions. Open a tab (modern Chrome requires PUT):

curl -s -X PUT "http://localhost:9333/json/new?http://localhost:8080/main"

Or attach to an existing tab via GET /json/list and drive Page.navigate. The response carries a per-tab webSocketDebuggerUrl; drive JS evaluation, clicks, and screenshots over that websocket via CDP (Runtime.evaluate, Page.captureScreenshot, or in-page html-to-image / html2canvas).

A check must produce evidence: a saved screenshot, a DOM assertion (expected element with expected text), a console-error count, or a confirmed visible form name. Opening a tab without an obvious error is not verification — the page may show a login screen, stale cache, or a calm-looking error component. Record evidence (or its absence) in the commit body. Use only when the change is genuinely UI-shaped; for backend-only changes, log-based smoke tests are enough.

Tests

build/base skips tests platform-wide (maven.test.skip), and tests/ — an ordinary lsFusion logics that is never deployed — is the module that turns them back on. The unit tests of the other modules stay off in a local build and run when asked for with -Dmaven.test.skip=false, as Jenkins asks for them: a failing one stops the deploy, so run those of a module you change. Where a new test goes:

  • unit — plain JUnit in the module's own src/test/java, for what needs no server. The web client's GWT tests (GWTTestCase) are unit tests too: web-client's surefire configuration runs them compiled to JavaScript in HtmlUnit. The custom-view registry's own JS has a node test, run by hand: node server/src/test/js/lsfusion-custom-registry-test.js;
  • syntax — tests/compile/ok/<theme>/X.lsf has to compile, tests/compile/fail/<theme>/X.lsf has to fail with every non-blank line of its X.expected somewhere in the output. Run with dryRun, which compiles the logic and exits before touching the database: all of ok in one run, one run per fail module (LsfCompileTest, surefire);
  • server behaviour — an action named test*, without parameters, at the top level of a module under tests/src/main/lsfusion/<theme>/ (LsfServerIT, failsafe). Nothing is registered: modules come off the classpath, and every top-level name beginning with test is taken for a test and called, so a helper in a test module goes by another name. Each is called through the external HTTP /exec, so it runs in a session of its own and its changes are dropped afterwards — a test that needs them kept has to APPLY, and what it applies stays in the database for the tests after it. Assertions live in TestUtils.lsf and end in fail(message), which is what the report shows. /exec has no client: the runner starts the server with openFormsWithoutClient, with which SHOW ... NOWAIT opens the form - its ON INIT and the SHOW's init block run -, reads it once, as a client gets its first changes - the events that reading fires run (ON ORDER, ON FILTER, ...) -, and closes it right away, so that is where a test runs the operators of an open form (ORDER, FILTER, ...); a SHOW that waits for the form still fails. All the test modules are one logics, and a script EVAL runs sees every one of them: a name two modules declare is ambiguous there, so a test module prefixes its names;
  • browser — for what only the web client shows: a class Ui<Theme>IT extends UiTest in tests/src/ui/java/lsfusion/tests/, with the forms and actions it works on in tests/src/main/lsfusion/ui/Ui<Theme>.lsf (failsafe, the ui profile). UiTest starts a server of its own, the web client's war in a jetty and a browser driven by Playwright, once for all the browser tests of a run, and opens /main for each test in a browser context of its own. The test opens its form through the navigator (open), finds what it works on with form, cell and row, types with edit, and calls the module's actions through /exec: exec seeds its data, check runs an assertion, retried for 10 s, since the server gets what the browser sent a moment after the click. The module prefixes its names and begins none with test, which LsfServerIT would call as a test; its forms sit in a navigator folder of its own, each under an element named as the form. A test seeds its data in the test method, under names no other attempt has (System.nanoTime()): what a failed attempt saved stays in the database for the rerun.
mvn -pl tests -am test                        # the compile corpus only
mvn -pl tests -am verify                      # and the behaviour tests, which need postgres
mvn -pl tests -am verify -Dlsf.tests=forms/*  # one directory of them
mvn -pl tests -am verify -DskipITs            # skip them, e.g. in a full build without a database
mvn -pl tests -am verify -Pui                 # and the browser tests, see below

-am puts the server of the working tree into the run. Without it the tests get whichever lsfusion-server snapshot is newest — your last install, or one Jenkins has deployed since — rather than the server change under test. A run takes one to a few minutes, most of it building and starting the server, which is longer than some agent shells let a command run by default (Claude Code's stops one after two minutes): give it a longer timeout. After a test class or a test module is deleted or renamed, run mvn -pl tests clean: failsafe runs every class left in target/test-classes, and the server loads every module left in target/classes.

Postgres defaults to localhost / postgres / no password (-Ddb.server=, -Ddb.user=, -Ddb.password=), and tests/compose.yaml brings up a matching one for CI. A run creates its own lsfusion_test_<pid> database and drops it afterwards (-Dlsf.keepDb=true keeps it), and takes free ports, so a server of your own can stay up. With JIT on, as PGDG's Linux packages and the postgres image have it, the tests still pass, only slower: the server cancels and reruns long queries that compiling takes past its time limit, and the browser tests' page loads stall for seconds. tests/compose.yaml turns it off; for a Postgres of your own that is optional.

A failed test runs once more, and one that passes then is reported as a flake rather than failing the build; for a server test that is a new server on a new database. -Dfailsafe.rerunFailingTestsCount=0 turns that off, so that a test being written fails once rather than twice. tests/quarantine.txt names tests as <theme>/<Module>.<action> (a server test) or <Class>.<method> (a browser one): they still run, but a failure is reported as skipped rather than failing the build.

The browser tests run only with -Pui, and take the web client's war from web-client/target rather than building it: build it first, and again after every change to the client. The war and then one test class, on a JDK without pack200 (14 and later; on 11 the three skips are not needed):

mvn -pl web-client -am package -P-sign-desktop-jar -Pgwt-dev -Dmaven.antrun.skip=true -Dbuildhelper.skipAttach=true -Dmdep.skip=true
mvn -pl tests -am verify -Pui -Dit.test=UiGridIT -Dtest=none -Dsurefire.failIfNoSpecifiedTests=false

-Pgwt-dev builds the war about twice as fast, compiling only the permutation Chrome and Edge take, in English and in draft mode. -Dit.test picks the classes, 'Ui*IT' all the browser ones, and leaves out the server behaviour tests; -Dtest=none -Dsurefire.failIfNoSpecifiedTests=false leaves out the compile corpus. -Dui.war=<path> takes the war from elsewhere, e.g. from another worktree, so that a fresh one need not build its own. -Dui.browser is a Playwright channel of an installed browser, chrome by default or msedge, or docker, Playwright's image: Jenkins runs that one, and it needs Docker on Linux, where its container on the host's network reaches the jetty on localhost. After tests/pom.xml moves to another Playwright version, remove the old one's image from the Jenkins box by hand (2.5 GB, docker rmi mcr.microsoft.com/playwright:v<old version>). -Dui.headless=false shows the browser. The server's and jetty's logs are in tests/target/ui, and a failed test leaves a screenshot, the page and a Playwright trace in tests/target/ui/failures/<Class>.<method>.png|html|zip, its message listing the errors the page logged. The trace steps through the test action by action: drop the zip onto https://trace.playwright.dev, which opens it in the browser.

The web client under Playwright has habits of its own, which the helpers of UiTest keep to. A locator looks inside the form's root, [lsfusion-form=<Module.form>]: a hidden tab keeps its DOM, with the same names in it. A mouse down on a cell of another row only selects the row, so a cell's editor and a tree node open on a double click. Text goes in by fill into the editor's <input>: a key typed before the editor is up goes to the grid, where ctrl+a selects every row. And a key goes only into a field that comes with the server's answer: while a request is in flight the client drops input, and Playwright holds a click back until the glass the client shows then is gone, but sends a key at once.

A bug fix always comes with a test of that bug: one that fails on the code before the fix and passes after it, of whichever kind above reaches the code. Where none reaches it yet, the fix extends a harness until one does.

A test written because of a fix says so in a comment, and names the issue rather than the commit: a backport gives one fix several hashes, and the issue is what the changelog already points at. Names of files and directories stay descriptive either way; with no issue behind it, the comment says what the test holds.

A check worth keeping goes into tests/, not into a scratch directory, and lands in the same commit as the fix it guards. A fix made on a vN branch that has no tests/ (v6 and older) is the exception: its test lands on master in a commit of its own, after the merge-up has brought the fix there.

Commits

  • Short imperative subject in English, ≤72 chars. Suffix (closes #NNNN) when the commit fully resolves a tracked issue.
  • Body explains why — motivation, design constraints, alternatives rejected. The diff already shows what.
  • Multi-paragraph bodies are normal for non-trivial changes.
  • Never amend or force-push commits that may have been pulled by others. Always create a new commit on top.
  • Never pass --no-verify or otherwise skip hooks unless the user explicitly asks. If a pre-commit hook fails, fix the underlying issue and create a new commit — not --amend, because the failed commit never landed.
  • When the change is AI-assisted, include a Co-Authored-By: trailer naming the model.
  • Stage new files (git add <path>) at creation, not at commit time. Pending work then shows as A/M in git status instead of ??, and can't silently fall out of the commit.
  • Before each commit, run git status, git diff, and git diff --cached. Stage only files relevant to the change; never sweep up unrelated working-tree edits.
  • For non-trivial changes, record in the commit body which verifications ran (compile, smoke test, CDP render, click-through) and which were skipped and why. Be precise: install -DskipTests is a build verification, not a test run — don't write "tests passed" unless the suite actually executed.

Use a HEREDOC so newlines and trailers are preserved:

git commit -m "$(cat <<'EOF'
Subject line (closes #NNNN)

Body paragraph explaining the why.

Co-Authored-By: <model name> <agent-noreply-email>
EOF
)"

Branches and backports

  • master is the active development line.
  • The two vN branches immediately preceding master are the supported LTS maintenance branches; older vN branches are end-of-life.
  • A bugfix is a backport candidate — check it alongside plugin and docs impact. Just as a change prompts you to ask whether the plugin (LSF.bnf/LSF.flex) or docs/ need a parallel update, a fix to a defect prompts you to ask whether it should land on the supported vN maintenance branches too, not just master. If the bug predates the current development line and is present in one or more maintained releases, name the oldest still-supported branch it affects and suggest backporting from there. Surface it as a recommendation, not an action — a backport changes what ships in a maintenance release, so confirm with the user before touching a vN branch. New features, syntax additions, and intentional behavior changes normally stay on master only.
  • Backport pattern is merge-up: cherry-pick or land the fix on the oldest still-supported version branch, then merge that branch forward through each successive vN up to master. Don't apply the same commit independently to multiple branches; that creates divergent histories that bite during the next merge.
  • Never force-push to master or to any vN branch.

GitHub issues

Significant, user-visible platform changes get a GitHub issue on lsfusion/platform before the commit lands. The commit that resolves it ends its subject with (closes #NNNN) — release notes are generated from these closing references, so missing or vague issues directly degrade the changelog.

A closes #NNNN (or fixes / resolves) reference in a commit message makes GitHub close the issue automatically once the commit lands on the default branch (master) — it happens on push, so don't close the issue by hand; just land the commit with the reference. Keep the resolving change and its documentation in that one commit (carry the closes #NNNN there) rather than splitting the docs into a separate follow-up commit.

Issue creation is a remote publishing action — it requires explicit user authorization (see Risk discipline). When publishing isn't authorized but the change warrants an issue, prepare the github_issue*.md draft locally and either commit the change without a closes #NNNN reference or leave a TODO for the user to wire it up post-publish. Never fabricate closes #NNNN against an issue that hasn't actually been opened — the changelog generator will follow it to a broken link.

Create an issue when the change is one of:

  • a new language operator, keyword, option, or syntax form;
  • a behavioral change visible to lsFusion application developers (semantics, defaults, deprecations);
  • a server-side feature with user-facing impact (logging, scheduling, integrations, security, API);
  • a non-trivial fix to externally observable behavior (wrong query results, broken form rendering, race conditions affecting users).

Don't create an issue for:

  • internal refactors that don't change behavior;
  • typo fixes, comment edits, dependency bumps without behavior change;
  • build/devtool tweaks invisible to platform users;
  • migrating the platform's own system .lsf modules to a new spelling of a syntax already covered by an earlier issue.

Issues are drafted as local github_issue*.md files before posting. Standard format:

### Title
<short imperative title>

### Description
<context, developer-facing behavior or syntax spec, with a short example>

### Reason
<motivation: why this matters, what it unblocks or fixes>

Bug reports may add a ### Fix section, but stated in developer-facing terms (what observable behavior changes) — not the affected files, classes, or methods.

Audience and content

The issue is read by application developers using the platform, not by maintainers debugging internals. Lead with the developer-facing surface (new syntax, option, default, behavior). By default an issue carries no implementation specifics — file/class/method names, internal grammar-rule or collection names, refactoring shape — those belong only in the commit body, never in the issue.

  • Include a short .lsf example (3–10 lines, fenced) whenever the change introduces, modifies, or deprecates syntax. For deprecations show old vs new side-by-side; for bug fixes show the minimal reproducer.
  • Use developer-facing vocabulary (operator, form, property, action, session) — not platform internals (aspect, post-process visitor, ImMap).

Pull requests

Workflow is master-direct (commits push to master after review). When a PR is opened, title and body follow the same conventions as commits: short imperative title, body explaining the why, and closes #NNNN to link the driving issue.

Documentation and MCP guidance

A user-visible platform change is not done when the code compiles — the documentation under docs/ must reflect the new behavior, either in the same commit or in an immediate follow-up before the change is published. As a rule of thumb: if the change warrants a GitHub issue (see above), it warrants a docs update too.

  • The docs layout is type-first: docs/<type>/{en,ru}/ (type ∈ language, paradigm, how-to, brief, rules). Update both language versions of a page in lockstep — they are translations of the same content, not separate documents.
  • docs/sidebars.js (in this repo) is the navigation source of truth; update it when adding pages or restructuring sections. The sibling ../docusaurus/ is a derived copy — don't edit it; its own sidebars.js is just a loader for docs/sidebars.js.
  • Docs-specific structural rules live in docs/AGENTS.md, and per-type rules in docs/<type>/AGENTS.md.
  • Migration notes (root MIGRATION.md): when a change carries a non-obvious regression risk — typically a non-local change near the core (query compilation and planning, apply/materialization semantics, locking) whose effects may surface only on production data volumes or in rare query/data shapes — add a section under the target version describing the symptoms an upgrading application may see (errors, performance degradations) and the quickest way back: the backward-compatibility setting when one exists (also document it in docs/paradigm/{en,ru}/Working_parameters.md), or the revert/workaround otherwise. Rules of thumb: if the change warranted a ...BackwardCompatibility setting, or the commit body says something like "changes plans globally", MIGRATION.md needs a note; a local fix with an obvious blast radius doesn't.
  • What's new (docs/paradigm/{en,ru}/Whats_new.md, both locales in lockstep): what the major version being developed on master adds against the previous one, grouped by topic — what someone choosing a version or upgrading to it would want to know. It is not the per-issue changelog (CHANGELOG.md is generated at release from the closes #NNNN references) and does not repeat MIGRATION.md. Touch it in the commit that lands a change on master which gives application developers or administrators something new or changes how they write, deploy or run an application: a new operator, option or syntax form, a new subsystem or integration, a changed default, a removal or deprecation, a library upgrade that breaks custom Java code. Bug fixes, performance tuning, internal refactoring and docs-only changes stay out, and so does anything merged up from a vN branch — it is not new in this version. Prefer extending an existing item over adding a neighbour: an item is one to three sentences naming the feature by its syntax, with the issue and docs links; a change that also has a MIGRATION.md note gets a short line in the "Moving to version N" section pointing there. Before a release, or when asked, audit the article against git log --no-merges origin/vN..master and the issues it closes, and add what the per-commit rule missed (commits made without this file in view).

The AI guidance is published as documentation pages — docs/brief/{en,ru}/Brief.md (the concise capability map) and docs/rules/{en,ru}/Rules.md (the task rules) — and the MCP lsfusion_get_guidance tool serves them by fetching the published pages (../mcp/ no longer ships separate brief.md/rules.md). Touch these pages only when a platform change materially alters how assistants or developers reason about, structure, or write .lsf — a new core operator/paradigm with broad applicability, a deprecation of something the guidance recommends, or a default change that breaks existing examples. Niche options, isolated bug fixes, internal plumbing, anything confined to a subsystem most app developers never touch — leave alone. When in doubt, leave alone; the guidance is deliberately compact and noise hurts more than missing edges. The guidance distills from the docs, not the reverse.

Code conventions

  • Don't add error handling, fallbacks, or validation for conditions that can't happen. Trust internal contracts; validate only at system boundaries (user input, external APIs).
  • Don't introduce abstractions beyond what the task requires. Three similar lines is fine — a premature helper is not.
  • Minimize call-tree depth and inter-function coupling. A helper called from only one place, doing one obvious thing, is usually clearer inlined; delegation chains where each level just forwards arguments make code harder to follow than one self-contained function. Exception: genuinely primitive or broadly-reused utilities (collection operations, formatting, well-known infrastructure helpers) — coupling to those is cheap and welcome.
  • Don't keep backwards-compatibility shims for unreleased changes. Don't rename unused identifiers to _x — just delete the dead code.
  • Don't comment what the code already says. Comments belong only where why is non-obvious: a hidden constraint, a workaround, a subtle invariant.
  • No emojis in source, commits, or comments unless the user explicitly asks.
  • Prefer editing existing files to creating new ones.
  • Module-specific code/process rules live in the module's own AGENTS.md, auto-loaded when working there: language-syntax deprecation and the system .lsf deprecation-warning policy in server/AGENTS.md; API-version bumping in api/AGENTS.md.

Risk discipline

Treat the following as requiring explicit user authorization each time, even if a similar action was authorized earlier in the session:

  • git push (especially to remote branches); force-pushes are never automatic.
  • Destructive operations: git reset --hard, git clean -fd, branch deletion, dropping database tables, rm -rf of working trees.
  • Anything that publishes outside the local machine: opening or commenting on GitHub PRs and issues, releases, deployments, messages to external services.

Local, reversible work — editing files, running tests, regenerating parsers, creating local commits — doesn't need per-action confirmation.

Per-agent extension files

  • CLAUDE.md, GEMINI.md at the repo root are thin wrappers that import this file (Claude Code and Gemini CLI don't auto-read AGENTS.md out of the box).
  • Module subdirs with their own scoped rules carry a local AGENTS.md plus CLAUDE.md/GEMINI.md wrappers, auto-loaded by ancestry when working in that subtree: api/ (API-version bumping), server/ (language-syntax deprecation, system .lsf policy), docs/ (documentation rules, with its own per-type split).
  • .github/copilot-instructions.md — if added, GitHub Copilot in IDEs that don't read AGENTS.md will pick it up. Keep canonical guidance here, and let that file be a short reference or a generated mirror.
  • .cursor/rules/*.mdc — if added, lets Cursor apply scoped rules. Cursor also reads AGENTS.md natively, so most guidance can stay here.