Imported from WolfeLeo2/Zynk (
AGENTS.md). Install upstream withnpx skills add WolfeLeo2/Zynk. Copyright stays with the author.
AGENTS.md — Zynk Codebase Design Principles
This file defines the engineering standards for the Zynk codebase. All contributors (human and AI) must follow these rules.
Coding Conventions
- Do not use Supabase CLI for SQL migrations. Use Supabase MCP
- Move completed implementation plans from
docs/plans/todocs/completed-plans/when finished. - Always reference
Skillsorskillsbefore anything.
Dart / Flutter
json_serializablefor JSON parsing — no manualfromJson- All async operations use
AsyncValuefrom Riverpod — handle loading, data, and error in UI - Feature folders are self-contained with
data/,domain/,presentation/ - Shared widgets in
lib/shared/widgets/— never duplicate across features - File naming:
snake_case.dart - Class naming:
PascalCase - Never use
BuildContextacross async gaps ListView.builderonly — neverListViewwith children array- Loading states: shimmer skeleton loaders only — never
CircularProgressIndicator - Never show empty screens — always shimmer, error state, or empty state illustration
- Remote images:
cached_network_imagewith team color gradient placeholder - Complex list items: wrap in
RepaintBoundary - Do not use if blocks if it is a simple Null check. Dart is null aware. Use that to your advantage
- All screens need to be responsive
- Do not use my AppTokens raw. Use them from referencing colorScheme. Raw AppTokens don't mutate according to theme
Core Design Principles
DRY — Don't Repeat Yourself
Every piece of knowledge must have a single, unambiguous, authoritative representation.
In practice:
- Extract shared UI into reusable widgets (
BranchRequiredGuard,AuthPinPad,SkeletonWidget) - Shared business logic lives in services (
SalesService,AuthService) — not in screenbuild()methods - Database queries belong in
PowerSyncRepository— never inline SQL in providers or widgets - Theme values come from
AppTokens— never hardcode colors or spacing
Red flags:
// ❌ Same SQL in 3 different providers
final p1 = StreamProvider((ref) => ref.watch(repositoryProvider).db.watch('SELECT ...'));
final p2 = StreamProvider((ref) => ref.watch(repositoryProvider).db.watch('SELECT ...'));
// ✅ One method on the repository, called from providers
final p1 = StreamProvider((ref) => ref.watch(repositoryProvider).watchProducts());
KISS — Keep It Simple, Stupid
The simplest solution that works is the correct one.
In practice:
- Providers should do one thing — watch data OR derive data, not both + manage side effects
- Prefer
StreamProviderover complexAsyncNotifierwhen you just need a stream - Riverpod redirect functions must be pure functions — read state, return a path, nothing else
- Avoid over-engineering state: if a
boolworks, don't use asealed class
Red flags:
// ❌ Notifier.build() with ref.listen + side effects + async init + validation
class BranchNotifier extends Notifier<State> {
@override
State build() {
ref.listen(...); // ❌ fires during build
Future.microtask(() => _init()); // ❌ deferred side effect in build
return initialState;
}
}
// ✅ build() returns initial state only; side effects in separate providers
class BranchNotifier extends Notifier<State> {
@override
State build() => const State(isLoading: true); // pure
}
SOLID
S — Single Responsibility
Each class/file has one reason to change.
| Layer | Responsibility |
|---|---|
repository.dart |
Raw DB queries only |
*_service.dart |
Business logic (validation, orchestration) |
*_provider.dart |
State + derived data |
*_screen.dart |
UI layout and user events only |
*_widget.dart |
Reusable UI components |
O — Open/Closed
Extend behavior without modifying existing code.
- Use
Provider.familyfor parameterized data — don't add conditionals inside existing providers - Add new routes in
routes.dartwithout touching existing route definitions
L — Liskov Substitution
Not directly applicable to Flutter, but:
ConsumerWidgetandConsumerStatefulWidgetshould be interchangeable for the same use case- Prefer
ConsumerWidget(stateless) unless you needAnimationControllerorTextEditingController
I — Interface Segregation
- Don't add methods to
PowerSyncRepositorythat only one screen uses — create a focused service instead - Providers that only one widget uses should be scoped (
.autoDispose) or local
D — Dependency Inversion
- Screens depend on providers, not on concrete service classes
ref.watch(repositoryProvider)notPowerSyncRepository(db)inside a widget
YAGNI — You Aren't Gonna Need It
Don't build features until they are needed.
In practice:
- Don't add
refreshBranches()to a notifier when aStreamProvideralready handles freshness - Don't build a
CombinedListenableunless you actually combine multiple listenables - Don't add
role-based visibility to a feature before the role system is tested end-to-end - Delete dead code immediately — unused classes/providers are tech debt
Riverpod-Specific Rules
Notifier.build()must be pure — return initial state only. Noref.listen, noFuture.microtask, no async calls.- Side effects that react to streams belong in a dedicated
Provider(not aNotifier), watched by an always-alive widget (e.g.,AppShell). - Use
WidgetsBinding.addPostFrameCallback(notFuture.microtask) to defer state mutations that happen during provider initialization. StreamProvider.autoDisposefor screen-scoped data;StreamProvider(no autoDispose) for app-wide data (branches, auth).ref.readin callbacks,ref.watchin build — never the other way around.Provider.familyfor parameterized providers — never pass state through constructors to solve the same problem.
File Structure Rules
lib/
core/
models/ # Pure Dart data classes — no Flutter imports
providers/ # App-wide Riverpod providers
services/ # Business logic (no UI)
widgets/ # Truly reusable, app-wide UI components
theme/ # AppTokens, ThemeData
routes.dart # GoRouter config only
app_shell.dart # Navigation shell only
data/
local/ # PowerSync repository (SQL only)
features/
<feature>/
models/ # Feature-specific models
providers/ # Feature-specific providers
presentation/ # Screens + widgets
Rules:
- No cross-feature imports (feature A must not import from feature B's
presentation/) - Models never import Flutter — only
dart:core - Services never import widgets
- Providers never import
package:flutter/material.dart(usepackage:flutter/foundation.dartif needed)
Naming Conventions
| Thing | Convention | Example |
|---|---|---|
| Provider | camelCaseProvider |
currentBranchIdProvider |
| Notifier | PascalCaseNotifier |
BranchSelectionNotifier |
| Screen | PascalCaseScreen |
PosScreen |
| Widget file | snake_case.dart |
branch_required_guard.dart |
| Service | PascalCaseService |
SalesService |
| Model | PascalCase |
Branch, Sale |
AI Agent Guidelines
When making changes to this codebase:
- Read before writing — view the file and understand what exists before editing
- Analyze before claiming done — always run
dart analyzebefore saying a task is complete - One concern per PR/task — don't mix feature work with refactoring
- Delete dead code — if a class/provider/method is no longer referenced, remove it
- No inline SQL in widgets or providers — it belongs in
repository.dart - No business logic in
build()methods — delegate to services/providers - Prefer hot reload over full restart — use the DTD MCP tool when available
- Always reference
Skillsorskillsbefore anything.
Research -> Questions -> Plan Pipeline
When tackling medium-to-complex user requests, feature additions, or UI/UX overhauls, the Agent MUST follow this pipeline before making any code changes:
- Research First: Gather context by reviewing existing files, scraping provided web links (e.g., competitors, inspiration), or searching the web for concepts (e.g., "Zoho Item Groups").
- Ask Questions: Identify any ambiguities in the user's request. Formulate clear, concise questions regarding business logic, UI preferences, or database schema decisions.
- Draft an Implementation Plan: Using the
writing-plansskill or similar, create an implementation plan artifact (e.g.,implementation_plan.md) outlining database migrations, logic changes, and UI/UX designs. - Notify User: Present findings, ask the questions, and link the plan artifact for the user's approval.
- Wait for Approval: Do not edit code until the user approves the plan or answers the questions.
Mulch Context Pipeline (Mandatory)
Use Mulch to preserve project expertise across agent sessions.
1) Start every task by loading context
- Run
ml primefor full context, orml prime <domain>for targeted context - Run
ml query <domain>on the area you are about to change before writing code
2) During work, query what is already known
- Run
ml search "<topic>"whenever you hit uncertainty or design choices - Re-run
ml query <domain>before high-impact edits (schema, auth, security, API contracts)
3) End every task by recording learnings
- Record durable learnings with
ml record <domain> --type <type> ... - Use record types intentionally:
decision,convention,failure,pattern,reference,guide - Run
ml validatebefore finishing to keep expertise files healthy
4) Commit and share knowledge
- If a task changes product code and
.mulchlearnings from the same work, commit both together - If only
.mulchchanged, a knowledge-only commit is acceptable - Do not mix unrelated code changes with
.mulchupdates in the same commit - Use
ml syncin git-enabled environments to validate, stage, and commit.mulch/updates
