FR MVVM Contract
Run every bundled Python entrypoint with uv run --script <path>. Add or
update its PEP 723 dependencies with uv add --script <path> <dependency>;
never invoke a bundled script with python, python3, or uv run python.
Mode Selection
- For a new project or
acdd_scaffold, read
references/acdd_scaffold.md and use scripts/acdd_scaffold.py. Do not run
the project profile resolver before the target project exists.
- For an existing Flutter project that must adopt the standard scaffold
structure, run:
uv run --script <skill-root>/scripts/resolve.py --task adapt_project
Follow the resolved inventory, mapping, approval, migration, and validation
workflow. Treat this skill's assets/acdd_scaffold/ templates and
references/acdd_scaffold.md boundaries as the standard. Never run
acdd_scaffold.py --apply against the existing project.
- To check what application-level configuration an existing Flutter project
still lacks compared with a common complete Android/iOS app, run:
uv run --script <skill-root>/scripts/resolve.py --task check_app_info
Read the resolved references/check_app_info.md instructions and report the
evidence for every required item. Treat developer accounts and certificates
that cannot be proven from repository files as externally unverified; do not
edit identifiers, signing, platform targets, icons, or push configuration
during the check.
- For contract work in an existing project, run:
uv run --script <skill-root>/scripts/resolve.py --task <gen_page|gen_component|extract_shared_ui|validate|validate_routes|validate_navigation_shell|audit_figma_fidelity|audit_figma_data|refresh|package_bff|generate_openapi>
Read the resolved instructions once per instructions_id.
- For backend OpenAPI-to-Retrofit generation, run the resolver with task
generate_openapi, read references/generate_openapi.md, and use
scripts/openapi_to_retrofit.py. Project-specific generic request and
response wrappers belong in .agents/skills-config/fr-mvvm-contract/config.yaml;
never infer or hard-code their non-generic fields in the reusable skill.
- For a screen whose implementation must be audited or repaired against Figma,
resolve
audit_figma_fidelity, read its resolved project profile, and run
the declared audit command before and after changes. Discover screen
participation from each primary .c.dart contract's Figma Fidelity:
disposition; never maintain a second page registry. Keep the viewport,
asset-lock path, and regression-test owner in .c.dart. Keep only exact
export identities, repository paths, and hashes in the referenced asset
lock under .agents/skills-config/fr-mvvm-contract/; never hard-code one
product screen in this reusable skill. Before implementing or approving the
screen, read references/figma_flutter_design_to_code.md to constrain the
mandatory Figma MCP acquisition skill to evidence collection, then follow
references/audit_figma_fidelity.md, run its SVG scan and safe normalization
pipeline for exported SVG assets, and preserve icon placement-box versus
visual-glyph dimensions and exact typography. The SVG pipeline never
auto-repairs geometry, and a structural audit pass is not visual approval.
- For a macOS window derived from a Figma Frame, establish whether the Frame
specifies the Flutter content viewport or the visible native window frame
before changing
MainFlutterWindow.swift. Preserve the content viewport
with setContentSize and contentMinSize/contentMaxSize. When the Frame
explicitly specifies the visible macOS window, use setFrame, minSize,
and maxSize instead; content-size constraints add the title-bar height and
make the outer window too large. Keep native title-bar behavior unless the
design expressly requires a borderless window, then verify with
fvm flutter build macos --debug.
- Before editing any Figma-bound Page or component, read
references/figma_release_management.md and run
scripts/resolve_figma_release.py for its .c.dart. When project config
declares releases, treat active_release as explicit authority; never infer
the latest release by sorting names. A stale result requires lightweight
structure discovery in the active file and touched-contract migration before
implementation. Migrate automatically only when route identity, business
responsibility, navigation context, primary Frame, and state Frames identify
one unambiguous successor. Block on missing, split, merged, or ambiguous
successors. Honor only a reasoned Figma Release Override; never create one
automatically to silence drift.
- Treat copy, formatter, runtime-data binding, and layout-constraint edits as
Figma-bound work whenever the owning
.c.dart declares Figma:. Resolve
audit_figma_fidelity and use
references/figma_flutter_design_to_code.md before choosing a runtime text
layout. Do not turn the width of one Figma sample value into a universal
runtime constraint.
- For two or more destinations that share a persistent bottom navigation,
resolve
validate_navigation_shell, read
references/navigation-shells.md, and run the declared validator before
changing Pages or routes. Classify the shared shell before auditing each
destination as a standalone screen.
- Do not propose command-line packaging or dependency-download optimizations
during ordinary project creation, adaptation, validation, or repair. Only
when the user explicitly requests build or packaging optimization, read
references/optional-build-packaging-optimizations.md, present the relevant
options, and obtain the authorization required by the target repository
before applying them.
Source-First Layout
Choose ownership and directory by reuse scope:
- Use feature directories only for grouping. Put every Page/Component module
in its own basename-matching leaf directory; never place different module
shells or
*.c.dart contracts in the same directory.
- Put a route-owned component under
lib/app/<feature>/<component-name>/ beside its optional page adapter.
- Put a component reused by multiple routes under
lib/components/<component-name>/. Do not duplicate it under each route.
- Keep every implementation artifact of a cross-route component in its own
lib/components/<component-name>/ directory. Do not place or leave
module-specific files in lib/core/.
- Keep a Widget used only inside one component private in that component's
.v.dart.
- Put a plain Widget reused inside one route under
lib/app/<route-segment>/widgets/.
- Put a plain Widget reused by multiple routes under
lib/widgets/.
- Do not give a plain presentation Widget a contract, Provider, Event, or
ViewModel. Promote it to a component when it encapsulates a reusable business
capability, app/page state integration, independent state, API, Event, or
ViewModel responsibility.
- In an existing project with an established equivalent root, preserve that
root unless the explicit
adapt_project workflow approves a move. Preserving
a root never permits several modules to share one leaf directory.
Shared UI Discovery And Extension
Before modifying a Page or route-owned component, discover reusable UI in this
order:
- Search
lib/components/ for a component whose Capabilities: matches the
requested capability.
- Search
lib/widgets/ for a shared Widget module whose Capabilities:
matches the requested capability.
- Reuse an existing public View or public Widget when it satisfies the
requirement.
- When an existing module owns the capability but has no suitable public
entry, add a semantic public View or Widget in that same module and update
its public list.
- Create a new component only when no component owns the required stateful or
business capability. Create a Widget module only for a cross-page pure
presentation need.
Run scripts/discover_ui_reuse.py --project-root <project-root> --capability "<requested capability>" before this decision. Treat its output as a catalog,
not an automatic ownership decision: resolve close semantic matches from the
module contract before creating a new module.
Extract Shared UI
Use extract_shared_ui when an existing route-owned View contains a UI entry
that must be reused by another route. Resolve this task, read
references/extract_shared_ui.md, and run extract_shared_ui.py in dry-run
mode before changing Dart sources. The tool produces a reviewable ownership
and file-migration manifest; --apply is permitted only after the manifest is
approved.
Classify by ownership, not by similar appearance:
- A presentation entry with inputs and callbacks only is a shared Widget under
lib/widgets/; it has no contract, Provider, Event, or ViewModel.
- An entry with independent state, API, Event, or ViewModel is a feature
component under
lib/components/<name>/ and follows the complete
gen_component contract workflow. Do not mechanically move it with the
Widget extractor.
- Similar widgets with different visual or interaction contracts stay
separate until an approved design decision defines a common public API.
After extraction, add Capabilities: and Public Widgets: to the shared
Widget provider, update consumers' Widget Tree: entries, rerun reuse
discovery, and validate formatting, tests, and analysis.
Capabilities: and public API lists are owned by the provider module:
- A component contract (
lib/components/<name>/<name>.c.dart) declares
Capabilities: and Public Views:.
- A shared Widget module public entry under
lib/widgets/ declares
Capabilities: and Public Widgets:.
- Every item in a public list uses a bracket reference and must be accessible
from that module's public entry.
Consumer Page contracts do not declare Components: or Shared Widgets:.
Their Widget Tree: records the key public Views and Widgets actually composed
by the Page. A Page owns placement and composition; the provider module owns
the reusable UI and its interaction.
- Group tightly related Pages into a cross-page module under one feature
directory, but keep every Page/Component module in a separate child leaf
directory. The feature's basename-matching module export must document
Pages: and Page Data Flow:. Read references/validate_routes.md before
creating or refactoring that boundary.
Persistent Navigation Shell Ownership
Before treating several Figma Frames or route roots as complete Pages, determine
whether they are destinations of one persistent navigation shell. Read
references/navigation-shells.md whenever two or more destinations share a
bottom bar.
- One shell owns the outer Scaffold, persistent top-region host, and one bottom
navigation instance.
- Branch Page adapters retain route inputs and page-scoped Providers; branch
Views contain content only.
- The bottom-navigation Widget accepts selection state and callbacks. It never
imports branch Page adapters, performs navigation, or owns guarded-entry
preflight.
- A root action with no preflight may navigate from the Shell View callback. A
root action requiring permission, validation, API, business policy, or async
preflight uses a Shell-owned component ViewModel and listener; branch,
bottom-navigation, and target Page ViewModels do not own its gate.
- Use
StatefulShellRoute.indexedStack by default for transition-free branch
switching with retained independent branch state.
- Retained branch state does not keep query data fresh. When a shell branch
owns a query API, detect an actual inactive-to-active transition and dispatch
its established load/refresh Event into the retained page ViewModel. Do not
duplicate the initial Startup Event, refresh on an ordinary rebuild, or
recreate the Provider. Make overlapping refreshes latest-result-safe.
- A zero-duration Page transition or
NoTransitionPage is not a persistent
shell repair.
- Root fullscreen routes and overlays cover the shell through the root
navigator/overlay; a branch ViewModel does not hide or recreate shell chrome.
A reusable feature component is one Dart library:
order_content/
order_content.dart
order_content.c.dart
order_content.v.dart
order_content.vm.dart # only for page- or component-owned state
order_content.srv.dart # SDK adapter over lib/api/gen
order_content.bff.md # required in BFF-JSON mode
order_content.freezed.dart # generated by Freezed
order_content.g.dart # generated by json_serializable
order_content.dart owns all imports and part declarations. Its .c.dart,
.v.dart, and optional .vm.dart files use part of 'order_content.dart';
and never declare imports. Write every contract section in .c.dart with
consecutive /// documentation comments; do not use /* ... */ contract
blocks.
Treat .c.dart as the source contract, not the View implementation file. Put
its contract comment before part of, followed only by stable contract types
such as Models, Events, DTOs, and business enums when they exist. Put every
public and private Widget/View declaration, constructor, build, FrView, and
visual composition in .v.dart. New drafts follow this layout; the reader may
still recognize one legacy View declared in .c.dart for migration.
A page is that component plus an optional, independent typed route adapter:
order_content.page.dart
The adapter imports order_content.dart, extends GoRouteData, owns the
page-scoped FrProvider, and builds OrderContentView below it. It is never a
Widget or a part of the component. Deleting the adapter removes the
page-owned lifecycle; app-owned and stateless components remain independently
usable by another page, sheet, tab, or dialog.
Naming And Ownership
XxxPage lives in xxx.page.dart, extends GoRouteData with the generated
$XxxPage mixin, owns route inputs as constructor fields, and expands them
into the page ViewModel factory and/or ordinary XxxView fields. It owns the
page-scoped Provider and dispatches a startup Event only when the contract
declares Startup Event: [XxxStarted]. It is not a Widget.
- Public
*View entries live in .v.dart and are listed authoritatively under
Public Views: in .c.dart. A component may expose multiple semantic Views.
Each consumes its upstream page/app state or ordinary inputs and does not
create a Provider by default.
- For every newly generated public Widget carrying
@FrAcddPage, read
references/widget-preview.md and put a fully configured Flutter SDK
@Preview(...) on its public constructor. Derive name from Figma.Frame,
group from the @FrAcddPage namespace, and size from the authoritative
viewport; require a public wrapper that supplies its preview runtime context.
XxxViewModel extends FrBlocViewModel<XxxEvent, XxxModel> lives in
.vm.dart only when this module owns page- or component-scoped state; all
external writes use add(event).
- Component fields, models, DTOs, Events, BFF/service declarations, and the
component contract belong to the component library.
- Name component state
XxxModel, BFF request and response boundaries
XxxBffReq and XxxBffRsp, and BFF-only nested data XxxDto. A project
request-data-envelope profile may explicitly allow a root XxxRequestDto
in place of XxxBffReq.
- A component Service imports the concrete SDK from
lib/api/gen and calls it
directly. Permit a semantic typedef for an SDK request type when the
ViewModel constructs that request. Keep response signatures in their original
generated SDK form by default. Every alias must preserve the exact SDK type,
fields, generics, and serialization shape.
- When a frontend
BFF-UI-API entry has the same HTTP method and path as a
backend-owned business API, it is the same direct backend boundary rather
than a second UI API. Its XxxBffReq reference must be an exact typedef of
the generated SDK request payload; never declare a replacement Freezed DTO
or copy, add, remove, or rename request fields. A multi-call UI aggregate must
use a distinct approved UI boundary, or BFF-UI-API: - when it is local
orchestration with no standalone UI HTTP endpoint.
- Assign interaction authority before declaring Events:
- Keep BuildContext, unconditional known typed Page navigation, overlays,
controllers, and other presentation-only operations in the View callback
when they do not change durable/business/API state. Such operations normally
need no Event and no
Interactions: Flow. Common examples are
non-exhaustive; do not add exhaustive operation syntax validation.
- Use a Bloc Event and document an
Interactions: Flow only when the
ViewModel owns model/business changes, API or service work, validation,
guard/concurrency policy, retry/recovery, or an observable state outcome.
- Treat picker, URL, share, and clipboard actions as boundary cases. Keep the
platform invocation in the View when presentation-only; move decisions and
results through the ViewModel when permissions, validation, persistence,
API work, business policy, or model changes are involved.
- When Page entry requires permission, validation, API, business policy, or
asynchronous/concurrent preflight, the click expresses intent only. Use
Trigger -> Event -> ViewModel preflight -> observable approved/blocked outcome + nullable semantic navigation signal -> View FrListener/FrConsumer -> typed Page navigation. Inject gateways into the ViewModel; never await a
gate in a StatefulWidget and navigate directly afterward. Default to
ignore-while-active. Emit each declared phase atomically through one
direct emit(state.copyWith(...)): Pending includes the signal reset,
approved Success includes both the real approved outcome and exact signal,
and blocked/exception outcomes never set the signal.
- Do not generate Intent or callback-output protocols as a second state
channel. Ordinary reusable Widgets may accept input callbacks.
- For business/API-result navigation, or a
Uses: local Flow whose ViewModel
owns a real state/business decision, use
Navigation: view-listener-on-success [Model].field = Enum.member. Give each
nullable semantic enum signal exactly one owning Flow. Reset it to null in
Pending State, set only the exact member in Success State, and never assign
its field: named argument from Failure, another Flow/handler, or undeclared
ViewModel code. A local Flow's separate Success outcome must not be an obvious
field = state.field no-op. A View FrListener/FrConsumer binds the exact
ViewModel and Model generic types and uses only an exact transition parent,
an exact equality early-return guard, or one exact transition-and-member &&
condition before navigating inside the exact enum-member braced branch.
The ViewModel must not own BuildContext, router types, Page objects, or router
calls. Retain Navigation: none when no navigation follows. Direct
presentation routing remains a View callback with no Event or Flow only when
entry is unconditional.
- Put a Provider at the state owner's lifecycle and at the lowest common
ancestor of all consumers:
app-owned [AppViewModel]: the root AppProviders owns it; the component
consumes it directly and declares no local VM, Event, Model, or Provider.
page-owned [XxxViewModel]: xxx.page.dart creates it and dispatches the
optional declared Startup Event; every Widget in that page subtree can
consume it.
none: the component uses inputs/callbacks or Widget-local ephemeral
state and declares no VM, Event, Model, or Provider.
component-owned [XxxViewModel]: explicit opt-in only for an independently
embeddable compound component that owns a distinct lifecycle and has
multiple descendants sharing its state.
- Cross-route shared components must not bind to a route-specific Page
ViewModel. Consume app-owned state when the state is genuinely global;
otherwise accept ordinary inputs/callbacks.
Page Route Inputs And Component Inputs
- Do not declare
XxxPageArgs. XxxPage is the single typed route input
model; declare path, query, and $extra inputs as its constructor fields.
- The component library (
xxx.dart and its parts) must never reference its own
sibling XxxPage, generated route mixin, GoRouterState, or import its own
xxx.page.dart. It may import another target route's .page.dart to use its
generated Page helper or target-owned PageExtra for typed navigation.
XxxPage.build consumes every route field in its ViewModel factory and/or
ordinary named fields on XxxView.
- Do not declare component input wrappers named
XxxArgs or XxxConfig.
- Pass only the route fields needed by
XxxViewModel from the Page's Provider
factory. Do not pass the Page route object into the component library.
- Use a route-owned
XxxPageExtra only when several non-URL values must travel
together through $extra. Declare it directly in the target xxx.page.dart,
never in an independent model file. Treat it only as a route transport model,
not domain data or ViewModel state. The target Page must expand it into
ordinary View fields. Declare it with @FrAcddFreezedJSON, a redirecting
const factory, and fromJson; add the page .freezed.dart part and retain
its shared generated .page.g.dart part. Configure one application-owned
GoRouter.extraCodec with tagged encode/decode cases for every PageExtra.
Preserve the approved PageExtra field contract during migration; serialization
requirements do not authorize changing field names, values, ownership, or
lifecycle. Read references/typed-routing.md for the required shape.
| Type |
File |
Consumers |
OrderContentPage.orderId |
order_content.page.dart |
Path input and route system |
OrderContentPage.entryPoint |
order_content.page.dart |
Query input and route system |
OrderContentView.orderId |
order_content.v.dart |
View only, when rendering needs it |
OrderContentView.entryPoint |
order_content.v.dart |
View only, when rendering needs it |
Use this standard conversion shape:
@TypedGoRoute<OrderContentPage>(path: '/orders/:orderId')
class OrderContentPage extends GoRouteData with $OrderContentPage {
const OrderContentPage({required this.orderId, required this.entryPoint});
final String orderId;
final String entryPoint;
@override
Widget build(BuildContext context, GoRouterState state) => FrProvider(
(context) => OrderContentViewModel(
orderId: orderId,
entryPoint: entryPoint,
),
onCreated: (context, vm) => vm.add(const OrderContentStarted()),
child: const OrderContentView(),
);
}
Page fields describe how the route enters and initialize page-owned state.
Ordinary View fields are reserved for values the View itself must render
without reading that state. Keep route types out of the component library.
Page Contract
xxx.page.dart declares one route-to-Provider-to-view adapter:
@TypedGoRoute<OrderContentPage>(path: '/orders/:orderId')
class OrderContentPage extends GoRouteData with $OrderContentPage {
/* route fields -> page ViewModel and/or View fields */
}
The route path is read from @TypedGoRoute, and the primary View is inferred
from XxxPage.build; do not duplicate either fact in documentation comments.
The primary View may compose any number of public/shared components and
Widgets recorded in its Widget Tree:; it does not limit a page to one
component. A page file may
declare additional typed Page variants for distinct URLs only when every
variant builds the same primary View; keep the basename-matching
XxxPage as the primary entry.
Typed Routing
- New scaffolds use
go_router_builder by default. Keep it and build_runner
in dev_dependencies; every independent xxx.page.dart generates its own
$appRoutes, and the root app_router.dart spreads those prefixed lists.
- Before adding or changing a route, read
references/typed-routing.md.
- For a cross-page module or PageExtra migration, resolve
validate_routes,
read references/validate_routes.md, and run its module validator.
- For persistent bottom-navigation destinations, resolve
validate_navigation_shell; branch switching uses the shell's goBranch
API and is not Page-to-Page typed navigation.
- Make
XxxPage the GoRouteData; do not create a separate XxxRoute or
XxxPageArgs. Its build creates the page scope and constructs the primary
XxxView below it.
- Navigate with generated route helpers when the destination is known in app
code. Keep raw URI navigation only at explicit external/dynamic URI
boundaries.
validate_routes rejects fixed context.go/push/replace
calls and AppRoutes.xxx indirection when the URI matches a typed Page;
document exceptional compatibility boundaries with the required reasoned
marker from references/validate_routes.md.
Contract-First Workflow
- Inspect Figma, run shared UI discovery, inspect the matching component and
Widget module catalogs, nearby usage, and API context. Record one outcome:
reuse an existing public entry; extend its owning module because no entry
fits; or create a module because no module owns the capability. When the
request supplies multiple Figma nodes or a Figma container node, first read
references/figma-screen-audit.md and account for every supplied URL as a
primary Frame, same-owner state, visual reference, or explicit exclusion.
Inspect a container's structure before requesting any full design context.
Find requested pages by their visible page-title text first, then resolve
each matching text node to its owning Frame. Treat the Frame name as a
candidate hint only because designers may leave it stale or unrelated.
Confirm the screen from its hierarchy, navigation context, distinctive
content, and render when needed; do not mistake a menu item or embedded
title for the page itself. Record the confirmed node-id, current Frame
name, and visible page title as one association. Select concrete Frames
rather than reading the container as though it were a page.
Present the resulting logical page/state ownership map before drafting.
When primary Frames share a bottom navigation, also present their shell
membership and classify shell roots, branch children, root fullscreen
routes, and overlays before selecting Page or Scaffold ownership;
never infer route or contract count from link count or visual similarity.
Page drafts default to BFF-JSON when no concrete API is supplied.
Shared component drafts default to local with State Ownership: none.
Read
references/api-contract-semantics.md; draw the cross-component data and
business flow before defining DTOs.
- For
gen_page, draft xxx.page.dart, xxx.dart, xxx.c.dart, and the
reviewable public-View stub in xxx.v.dart:
uv run --script <skill-root>/scripts/draft_contract.py \
--name order_content --dir lib/app/order_content \
--figma-url <url> --figma-frame <exact-current-frame-name> \
--figma-page-title <exact-visible-page-title|none> \
--mode bff-json --route <route> \
--preview-width <width> --preview-height <height> \
--preview-wrapper <public-wrapper> \
--theme <none|material|app-shared|component>
Read references/widget-preview.md before choosing the viewport and
wrapper. Pass --preview-wrapper-import <dart-uri> when the public wrapper
is declared outside the generated component library. The generator rejects
missing Preview size or wrapper inputs instead of inventing them.
For app-shared or component, also pass --theme-type <ThemeType>.
Repeat --extra-field name:DartType when the Page needs multiple $extra
values. The draft emits the required
@FrAcddFreezedJSON PageExtra, generated parts, typed $extra, and
PageExtra-to-ViewModel expansion. Register every emitted type in the
application route-extra codec before final validation.
Use lib/app/<route-segment>/ for a route-owned component and
lib/components/<component-name>/ --component-only for a component reused
across routes. --component-only defaults to --mode local --state-owner none, so it emits no VM, Model, Event, Provider, Freezed, JSON, or BFF
assets. Use --state-owner app --state-type <AppViewModel> when it consumes
an existing root-provided VM. Use --state-owner component only after
explicitly proving an independent component lifecycle; component-owned
API/BFF modes require that flag. Use an existing project's established
equivalent roots when they differ, unless an approved adaptation moves
them.
3. Bind the primary Figma Frame and every declared Figma States Frame back
to the generated Dart files before contract review. Never bind Figma References or Figma Excluded. Record the primary Frame's exact current
name, visible page title, and complete node-specific URL under Figma: so
node-id ↔ Frame name ↔ Page Title remains explicit. Use Page Title: none
only for an intentionally titleless page or component. Record each Figma States target as only its node-id, resolved against that primary design
file.
Never repeat the design URL in Figma States. For an approved screen, add
exactly one Figma Fidelity: section in this fixed shape:
/// Figma Fidelity:
/// - Viewport: 360 x 780
/// - Asset Lock: .agents/skills-config/fr-mvvm-contract/order-figma-assets.lock.json
/// - Regression Test: orderFigmaFidelity renders approved states
Use Asset Lock: none only when the authoritative Frame has no exported
assets. Do not put the Figma file/node, viewport, routes, copy, source
assertions, or test names in the lock. Do not write contract metadata,
shared plugin data, or visible cards into Figma.
Use the one-line Figma Fidelity: excluded | <reason> form instead when
the current implementation is explicitly outside the approved gate.
Prepare page contracts and target Frames one at a time; a page contract must
target its exact Figma Frame, never a Section containing several pages.
Execute the
emitted verifyCode in a second use_figma call and inspect its screenshot.
Do not continue if the primary URL lacks node-id, a page target is a
non-Frame,
either representation is missing or stale, the card is not above its page,
a path is not visibly rendered, or the independent readback differs.
4. Before writing a Figma-derived View, read references/figma_fill_data.md.
Classify every non-copy Figma fill in the .c.dart Figma Data: section.
A draft may retain a TODO(figma-data) marker or a Binding: pending
declaration; contract and final validation reject both. Do not use Figma sample values as production
defaults. Use scripts/figma_fill_data.py to report pending, invalid, and
legacy-unreviewed declarations across a project.
5. Internally classify each UI-facing BFF endpoint as query or command;
do not ask the user to choose a type or write an API-type field. Read
references/frontend-interactions.md. Use its request boundary as the
endpoint identity, write exactly one record under Behaviors: and one
endpoint-scoped Request Field Sources: record, then define structured
Interactions: Flows only for ViewModel-owned Trigger, Bloc Event, guard,
state phases, concurrency, and navigation-signal coordination. Do not turn
View-local callbacks into Events or Flows. The same component may own query and command
endpoints. Do not author SDK Calls, SDK Call Flow, backend method/path
annotations, or backend orchestration in .c.dart. Backend developers own
those facts and edit only the protected backend section of xxx.bff.md.
The skill may define and refresh the frontend UI data API, UI DTOs, state,
endpoint behavior, interaction Flows, structure, and integration mapping.
It must preserve the complete backend section byte-for-byte. Set BFF Service to the Dart SDK-adapter service class, such as
[OrderContentService]; every BFF-JSON contract requires runtime
integration. When a data boundary is unknown or the required evidence has
not been supplied, add Data Boundary: with a stable
TODO(data-boundary): <capability> — <missing authority/evidence> marker.
Do not turn that uncertainty into BFF-UI-API: -, sample-data behavior, or an
invented method/path. BFF-UI-API: - is allowed only after an explicit,
approved API-less/local-only decision. Search for
TODO(data-boundary) before approval; contract and final validation reject
unresolved markers. Never invent /bootstrap, nextRoute, proof, result,
or error placeholders.
Write descriptive contract values in the resolved Contract Description Language. This applies to Behavior values, interaction state/feedback
prose, Request Field Sources purpose prose, and Notes. Keep stable labels,
Flow IDs, code identifiers, types,
methods, paths, enum literals, and authoritative source expressions
unchanged.
Keep the remaining approval contract minimal: Figma, API/BFF, state ownership,
reusable UI in the Widget Tree, theme, Event and ViewModel
references, models, and concise notes. Page Support contains only route and
primary View facts.
Define Widget Tree as a concise hierarchy of key Widgets that lets a
reviewer understand the View from .c.dart alone. Include a Widget when it
is directly interactive, carries primary information, expresses an
important state, determines the functional structure, or is a shared
component developers must recognize. Prefer 4–8 key Widgets and keep even
complex Views to at most 12; fold larger trees into business-level regions,
use × N for repeated items, and mark conditional states briefly.
When Public Views: lists more than one entry, write one Widget Tree
bullet per public View in the same order. Do not add a discriminator enum
merely to force semantically different entries through one generic View.
Preserve only necessary hierarchy. Omit formulaic _XxxViewBody nodes,
FrProvider, FrConsumer, Builder, layout glue such as Padding,
SizedBox, Spacer, Align, Expanded, Flexible, and SafeArea, pure
decoration such as Divider and DecoratedBox, and component-internal
labels/icons/spacing already covered by the parent component. Omit Row,
Column, Stack, and Container unless one is essential to disambiguate
business structure. A semantic private Widget such as [_HomeHeader] may
remain.
Do not replace key Widget references with prose such as confirmation form.
Do not draw a UI diagram or reproduce the complete runtime Widget tree.
The provider module's public list is the dependency/reuse inventory; the
concise Widget Tree records the entries actually composed here. Replace the generated TODO with an informative,
concise tree before contract review and approval.
For every Behaviors: record, remove the unused query or command fields;
complete every Interactions: Flow, resolve every TODO(data-boundary) and
other pending marker, then define DTO fields and synchronize typed XxxPage
route fields to the final ordinary XxxView fields. The draft shell deliberately names not-yet-generated
parts, so this review state is not a compilation or analyzer gate.
5. Present each UI API method/path and Req/Rsp/Error, its endpoint Behavior,
endpoint-scoped field provenance, every frontend interaction Flow, and the
SDK-adapter service class. A non-BFF local component may explicitly declare
structured Interactions: only for ViewModel-owned local Flows; every such
Flow uses Uses: local and requires no endpoint, BFF Service, SDK, or
bff.md. Do not present, propose, or edit backend APIs or flow; backend
developers maintain them in xxx.bff.md.
6. Validate the approved source contract before deriving files. This phase
rejects semantic/API placeholders, missing or incomplete endpoint
Behaviors, untraceable request fields, missing interaction coverage,
invalid Event/Widget/Model/response references, UI-only command responses, invalid
typed Page route-field conversion, incomplete Theme declarations, and missing direct
dependencies, but does not require Freezed/JSON output yet:
uv run --script <skill-root>/scripts/validate_contract.py \
--page-file path/to/xxx.page.dart --phase contract
- For all non-contract work, read the contract through scripts rather than
manually deriving decisions from raw Dart:
uv run --script <skill-root>/scripts/read_contract.py \
--page-file path/to/xxx.page.dart
uv run --script <skill-root>/scripts/read_contract.py \
--component-file path/to/xxx.dart
- Prepare derived parts only from the approved reader output. The generator
preflights the complete contract, Theme target, dependencies, and BFF
extractor before committing any file. It prepares Theme changes, the BFF
artifact, then
.vm.dart and .v.dart stubs as one rollback-protected file
set. An extractor or Theme failure must leave every prior file unchanged:
uv run --script <skill-root>/scripts/generate_from_contract.py \
--page-file path/to/xxx.page.dart --write-stubs
Every BFF-JSON contract with runtime backend calls declares
BFF Service: [Type]. Use one Service per component. The Service imports the
required concrete SDK files from lib/api/gen, constructs configured generic
wrappers internally, and exposes semantic methods to its ViewModel. Do not
generate a frontend Retrofit client from the UI data API.
A project may require every XxxBffRsp to model a complete gateway response such as
{state, code, message, data}. In that case the original business response is
the value of its data field, not a replacement for the outer envelope.
Generated *.bff.md files begin with compact bff-md-meta/v9 YAML Front
Matter containing schema, @FrAcddPage namespace/version, the authoritative
UI design source, a namespaced mdq v2 API-record contract, and no derivable
contract-file path. They separate inline UI
API DTOs from OpenAPI-owned backend operations, backend call flow, frontend UI
data, integration mapping, and a generated API Query Records verification
projection. The UI Contract contains UI State, endpoint Behaviors, generated
前端交互逻辑, and UI Structure in that order. Render UI State exclusively as
one JSON5 code block: every field has consecutive Model, Dart type, and
Authority: Frontend comments. Do not use Markdown tables for UI State. Read
references/bff-dual-authority.md before changing artifact structure,
ownership, generation, parsing, or validation.
Render BFF artifacts in this fixed order: title, 后端业务流程与业务逻辑 API, and
前端 UI 数据接口. The backend domain contains only OpenAPI document references,
the business API annotations and business flow written by backend developers.
Each business API annotation retains only method/path, parameter names and
types, and response DTO type. Backend developers may add prose, JSON, DTO field
examples, schema excerpts, or code blocks elsewhere in this protected domain;
the parser preserves that material and excludes it from machine API records.
Backend developers alone create and edit this entire domain. AI may create only
UI-facing paths and XxxBffReq/XxxBffRsp DTOs from approved Figma/UI
requirements.
It must not treat a backend business API method/path as a new UI-facing path.
On an exact method/
…(truncated)
1---2name: fr-mvvm-contract3description: Create or adapt ACDD Flutter projects across Android, iOS, macOS, Web, Windows, and Linux; audit application identity, Android/iOS icons, developer signing evidence, minimum platform versions, and push configuration; create, validate, or evolve FlowR contracts, typed Pages, cross-page modules, navigation shells, endpoint Behaviors, and frontend interaction Flows; audit or repair project-configured Figma-bound screens, including runtime text, formatting, data binding, and layout; generate, query, package, or synchronize BFF contracts; and assess Flutter packaging optimizations when explicitly requested. Use for acdd_scaffold, existing-project adaptation, application-information or release-readiness checks, contract-first FlowR work, typed-route or bottom-navigation refactors, BFF frontend interaction logic, changes to Views whose contracts declare Figma, Figma fidelity repair, BFF inventory or delivery, and explicit Flutter build optimization requests.4---56# FR MVVM Contract78Run every bundled Python entrypoint with `uv run --script <path>`. Add or9update its PEP 723 dependencies with `uv add --script <path> <dependency>`;10never invoke a bundled script with `python`, `python3`, or `uv run python`.1112## Mode Selection1314- For a new project or `acdd_scaffold`, read15 `references/acdd_scaffold.md` and use `scripts/acdd_scaffold.py`. Do not run16 the project profile resolver before the target project exists.17- For an existing Flutter project that must adopt the standard scaffold18 structure, run:1920```bash21uv run --script <skill-root>/scripts/resolve.py --task adapt_project22```2324 Follow the resolved inventory, mapping, approval, migration, and validation25 workflow. Treat this skill's `assets/acdd_scaffold/` templates and26 `references/acdd_scaffold.md` boundaries as the standard. Never run27 `acdd_scaffold.py --apply` against the existing project.28- To check what application-level configuration an existing Flutter project29 still lacks compared with a common complete Android/iOS app, run:3031```bash32uv run --script <skill-root>/scripts/resolve.py --task check_app_info33```3435 Read the resolved `references/check_app_info.md` instructions and report the36 evidence for every required item. Treat developer accounts and certificates37 that cannot be proven from repository files as externally unverified; do not38 edit identifiers, signing, platform targets, icons, or push configuration39 during the check.40- For contract work in an existing project, run:4142```bash43uv run --script <skill-root>/scripts/resolve.py --task <gen_page|gen_component|extract_shared_ui|validate|validate_routes|validate_navigation_shell|audit_figma_fidelity|audit_figma_data|refresh|package_bff|generate_openapi>44```4546 Read the resolved instructions once per `instructions_id`.47- For backend OpenAPI-to-Retrofit generation, run the resolver with task48 `generate_openapi`, read `references/generate_openapi.md`, and use49 `scripts/openapi_to_retrofit.py`. Project-specific generic request and50 response wrappers belong in `.agents/skills-config/fr-mvvm-contract/config.yaml`;51 never infer or hard-code their non-generic fields in the reusable skill.52- For a screen whose implementation must be audited or repaired against Figma,53 resolve `audit_figma_fidelity`, read its resolved project profile, and run54 the declared audit command before and after changes. Discover screen55 participation from each primary `.c.dart` contract's `Figma Fidelity:`56 disposition; never maintain a second page registry. Keep the viewport,57 asset-lock path, and regression-test owner in `.c.dart`. Keep only exact58 export identities, repository paths, and hashes in the referenced asset59 lock under `.agents/skills-config/fr-mvvm-contract/`; never hard-code one60 product screen in this reusable skill. Before implementing or approving the61 screen, read `references/figma_flutter_design_to_code.md` to constrain the62 mandatory Figma MCP acquisition skill to evidence collection, then follow63 `references/audit_figma_fidelity.md`, run its SVG scan and safe normalization64 pipeline for exported SVG assets, and preserve icon placement-box versus65 visual-glyph dimensions and exact typography. The SVG pipeline never66 auto-repairs geometry, and a structural audit pass is not visual approval.67- For a macOS window derived from a Figma Frame, establish whether the Frame68 specifies the Flutter content viewport or the visible native window frame69 before changing `MainFlutterWindow.swift`. Preserve the content viewport70 with `setContentSize` and `contentMinSize`/`contentMaxSize`. When the Frame71 explicitly specifies the visible macOS window, use `setFrame`, `minSize`,72 and `maxSize` instead; content-size constraints add the title-bar height and73 make the outer window too large. Keep native title-bar behavior unless the74 design expressly requires a borderless window, then verify with75 `fvm flutter build macos --debug`.76- Before editing any Figma-bound Page or component, read77 `references/figma_release_management.md` and run78 `scripts/resolve_figma_release.py` for its `.c.dart`. When project config79 declares releases, treat `active_release` as explicit authority; never infer80 the latest release by sorting names. A `stale` result requires lightweight81 structure discovery in the active file and touched-contract migration before82 implementation. Migrate automatically only when route identity, business83 responsibility, navigation context, primary Frame, and state Frames identify84 one unambiguous successor. Block on missing, split, merged, or ambiguous85 successors. Honor only a reasoned `Figma Release Override`; never create one86 automatically to silence drift.87- Treat copy, formatter, runtime-data binding, and layout-constraint edits as88 Figma-bound work whenever the owning `.c.dart` declares `Figma:`. Resolve89 `audit_figma_fidelity` and use90 `references/figma_flutter_design_to_code.md` before choosing a runtime text91 layout. Do not turn the width of one Figma sample value into a universal92 runtime constraint.93- For two or more destinations that share a persistent bottom navigation,94 resolve `validate_navigation_shell`, read95 `references/navigation-shells.md`, and run the declared validator before96 changing Pages or routes. Classify the shared shell before auditing each97 destination as a standalone screen.98- Do not propose command-line packaging or dependency-download optimizations99 during ordinary project creation, adaptation, validation, or repair. Only100 when the user explicitly requests build or packaging optimization, read101 `references/optional-build-packaging-optimizations.md`, present the relevant102 options, and obtain the authorization required by the target repository103 before applying them.104105## Source-First Layout106107Choose ownership and directory by reuse scope:108109- Use feature directories only for grouping. Put every Page/Component module110 in its own basename-matching leaf directory; never place different module111 shells or `*.c.dart` contracts in the same directory.112- Put a route-owned component under113 `lib/app/<feature>/<component-name>/` beside its optional page adapter.114- Put a component reused by multiple routes under115 `lib/components/<component-name>/`. Do not duplicate it under each route.116- Keep every implementation artifact of a cross-route component in its own117 `lib/components/<component-name>/` directory. Do not place or leave118 module-specific files in `lib/core/`.119- Keep a Widget used only inside one component private in that component's120 `.v.dart`.121- Put a plain Widget reused inside one route under122 `lib/app/<route-segment>/widgets/`.123- Put a plain Widget reused by multiple routes under `lib/widgets/`.124- Do not give a plain presentation Widget a contract, Provider, Event, or125 ViewModel. Promote it to a component when it encapsulates a reusable business126 capability, app/page state integration, independent state, API, Event, or127 ViewModel responsibility.128- In an existing project with an established equivalent root, preserve that129 root unless the explicit `adapt_project` workflow approves a move. Preserving130 a root never permits several modules to share one leaf directory.131132## Shared UI Discovery And Extension133134Before modifying a Page or route-owned component, discover reusable UI in this135order:1361371. Search `lib/components/` for a component whose `Capabilities:` matches the138 requested capability.1392. Search `lib/widgets/` for a shared Widget module whose `Capabilities:`140 matches the requested capability.1413. Reuse an existing public View or public Widget when it satisfies the142 requirement.1434. When an existing module owns the capability but has no suitable public144 entry, add a semantic public View or Widget in that same module and update145 its public list.1465. Create a new component only when no component owns the required stateful or147 business capability. Create a Widget module only for a cross-page pure148 presentation need.149150Run `scripts/discover_ui_reuse.py --project-root <project-root> --capability151"<requested capability>"` before this decision. Treat its output as a catalog,152not an automatic ownership decision: resolve close semantic matches from the153module contract before creating a new module.154155## Extract Shared UI156157Use `extract_shared_ui` when an existing route-owned View contains a UI entry158that must be reused by another route. Resolve this task, read159`references/extract_shared_ui.md`, and run `extract_shared_ui.py` in dry-run160mode before changing Dart sources. The tool produces a reviewable ownership161and file-migration manifest; `--apply` is permitted only after the manifest is162approved.163164Classify by ownership, not by similar appearance:165166- A presentation entry with inputs and callbacks only is a shared Widget under167 `lib/widgets/`; it has no contract, Provider, Event, or ViewModel.168- An entry with independent state, API, Event, or ViewModel is a feature169 component under `lib/components/<name>/` and follows the complete170 `gen_component` contract workflow. Do not mechanically move it with the171 Widget extractor.172- Similar widgets with different visual or interaction contracts stay173 separate until an approved design decision defines a common public API.174175After extraction, add `Capabilities:` and `Public Widgets:` to the shared176Widget provider, update consumers' `Widget Tree:` entries, rerun reuse177discovery, and validate formatting, tests, and analysis.178179`Capabilities:` and public API lists are owned by the provider module:180181- A component contract (`lib/components/<name>/<name>.c.dart`) declares182 `Capabilities:` and `Public Views:`.183- A shared Widget module public entry under `lib/widgets/` declares184 `Capabilities:` and `Public Widgets:`.185- Every item in a public list uses a bracket reference and must be accessible186 from that module's public entry.187188Consumer Page contracts do not declare `Components:` or `Shared Widgets:`.189Their `Widget Tree:` records the key public Views and Widgets actually composed190by the Page. A Page owns placement and composition; the provider module owns191the reusable UI and its interaction.192- Group tightly related Pages into a cross-page module under one feature193 directory, but keep every Page/Component module in a separate child leaf194 directory. The feature's basename-matching module export must document195 `Pages:` and `Page Data Flow:`. Read `references/validate_routes.md` before196 creating or refactoring that boundary.197198## Persistent Navigation Shell Ownership199200Before treating several Figma Frames or route roots as complete Pages, determine201whether they are destinations of one persistent navigation shell. Read202`references/navigation-shells.md` whenever two or more destinations share a203bottom bar.204205- One shell owns the outer Scaffold, persistent top-region host, and one bottom206 navigation instance.207- Branch Page adapters retain route inputs and page-scoped Providers; branch208 Views contain content only.209- The bottom-navigation Widget accepts selection state and callbacks. It never210 imports branch Page adapters, performs navigation, or owns guarded-entry211 preflight.212- A root action with no preflight may navigate from the Shell View callback. A213 root action requiring permission, validation, API, business policy, or async214 preflight uses a Shell-owned component ViewModel and listener; branch,215 bottom-navigation, and target Page ViewModels do not own its gate.216- Use `StatefulShellRoute.indexedStack` by default for transition-free branch217 switching with retained independent branch state.218- Retained branch state does not keep query data fresh. When a shell branch219 owns a query API, detect an actual inactive-to-active transition and dispatch220 its established load/refresh Event into the retained page ViewModel. Do not221 duplicate the initial Startup Event, refresh on an ordinary rebuild, or222 recreate the Provider. Make overlapping refreshes latest-result-safe.223- A zero-duration Page transition or `NoTransitionPage` is not a persistent224 shell repair.225- Root fullscreen routes and overlays cover the shell through the root226 navigator/overlay; a branch ViewModel does not hide or recreate shell chrome.227228A reusable feature component is one Dart library:229230```text231order_content/232 order_content.dart233 order_content.c.dart234 order_content.v.dart235 order_content.vm.dart # only for page- or component-owned state236 order_content.srv.dart # SDK adapter over lib/api/gen237 order_content.bff.md # required in BFF-JSON mode238 order_content.freezed.dart # generated by Freezed239 order_content.g.dart # generated by json_serializable240```241242`order_content.dart` owns all imports and part declarations. Its `.c.dart`,243`.v.dart`, and optional `.vm.dart` files use `part of 'order_content.dart';`244and never declare imports. Write every contract section in `.c.dart` with245consecutive `///` documentation comments; do not use `/* ... */` contract246blocks.247248Treat `.c.dart` as the source contract, not the View implementation file. Put249its contract comment before `part of`, followed only by stable contract types250such as Models, Events, DTOs, and business enums when they exist. Put every251public and private Widget/View declaration, constructor, `build`, `FrView`, and252visual composition in `.v.dart`. New drafts follow this layout; the reader may253still recognize one legacy View declared in `.c.dart` for migration.254255A page is that component plus an optional, independent typed route adapter:256257```text258order_content.page.dart259```260261The adapter imports `order_content.dart`, extends `GoRouteData`, owns the262page-scoped `FrProvider`, and builds `OrderContentView` below it. It is never a263Widget or a `part` of the component. Deleting the adapter removes the264page-owned lifecycle; app-owned and stateless components remain independently265usable by another page, sheet, tab, or dialog.266267## Naming And Ownership268269- `XxxPage` lives in `xxx.page.dart`, extends `GoRouteData` with the generated270 `$XxxPage` mixin, owns route inputs as constructor fields, and expands them271 into the page ViewModel factory and/or ordinary `XxxView` fields. It owns the272 page-scoped Provider and dispatches a startup Event only when the contract273 declares `Startup Event: [XxxStarted]`. It is not a Widget.274- Public `*View` entries live in `.v.dart` and are listed authoritatively under275 `Public Views:` in `.c.dart`. A component may expose multiple semantic Views.276 Each consumes its upstream page/app state or ordinary inputs and does not277 create a Provider by default.278- For every newly generated public Widget carrying `@FrAcddPage`, read279 `references/widget-preview.md` and put a fully configured Flutter SDK280 `@Preview(...)` on its public constructor. Derive `name` from `Figma.Frame`,281 `group` from the `@FrAcddPage` namespace, and `size` from the authoritative282 viewport; require a public wrapper that supplies its preview runtime context.283- `XxxViewModel extends FrBlocViewModel<XxxEvent, XxxModel>` lives in284 `.vm.dart` only when this module owns page- or component-scoped state; all285 external writes use `add(event)`.286- Component fields, models, DTOs, Events, BFF/service declarations, and the287 component contract belong to the component library.288- Name component state `XxxModel`, BFF request and response boundaries289 `XxxBffReq` and `XxxBffRsp`, and BFF-only nested data `XxxDto`. A project290 request-data-envelope profile may explicitly allow a root `XxxRequestDto`291 in place of `XxxBffReq`.292- A component Service imports the concrete SDK from `lib/api/gen` and calls it293 directly. Permit a semantic `typedef` for an SDK request type when the294 ViewModel constructs that request. Keep response signatures in their original295 generated SDK form by default. Every alias must preserve the exact SDK type,296 fields, generics, and serialization shape.297- When a frontend `BFF-UI-API` entry has the same HTTP method and path as a298 backend-owned business API, it is the same direct backend boundary rather299 than a second UI API. Its `XxxBffReq` reference must be an exact `typedef` of300 the generated SDK request payload; never declare a replacement Freezed DTO301 or copy, add, remove, or rename request fields. A multi-call UI aggregate must302 use a distinct approved UI boundary, or `BFF-UI-API: -` when it is local303 orchestration with no standalone UI HTTP endpoint.304- Assign interaction authority before declaring Events:305 - Keep BuildContext, unconditional known typed Page navigation, overlays,306 controllers, and other presentation-only operations in the View callback307 when they do not change durable/business/API state. Such operations normally308 need no Event and no `Interactions:` Flow. Common examples are309 non-exhaustive; do not add exhaustive operation syntax validation.310 - Use a Bloc Event and document an `Interactions:` Flow only when the311 ViewModel owns model/business changes, API or service work, validation,312 guard/concurrency policy, retry/recovery, or an observable state outcome.313 - Treat picker, URL, share, and clipboard actions as boundary cases. Keep the314 platform invocation in the View when presentation-only; move decisions and315 results through the ViewModel when permissions, validation, persistence,316 API work, business policy, or model changes are involved.317 - When Page entry requires permission, validation, API, business policy, or318 asynchronous/concurrent preflight, the click expresses intent only. Use319 `Trigger -> Event -> ViewModel preflight -> observable approved/blocked320 outcome + nullable semantic navigation signal -> View FrListener/FrConsumer321 -> typed Page navigation`. Inject gateways into the ViewModel; never await a322 gate in a StatefulWidget and navigate directly afterward. Default to323 `ignore-while-active`. Emit each declared phase atomically through one324 direct `emit(state.copyWith(...))`: Pending includes the signal reset,325 approved Success includes both the real approved outcome and exact signal,326 and blocked/exception outcomes never set the signal.327 - Do not generate Intent or callback-output protocols as a second state328 channel. Ordinary reusable Widgets may accept input callbacks.329- For business/API-result navigation, or a `Uses: local` Flow whose ViewModel330 owns a real state/business decision, use331 `Navigation: view-listener-on-success [Model].field = Enum.member`. Give each332 nullable semantic enum signal exactly one owning Flow. Reset it to `null` in333 Pending State, set only the exact member in Success State, and never assign334 its `field:` named argument from Failure, another Flow/handler, or undeclared335 ViewModel code. A local Flow's separate Success outcome must not be an obvious336 `field = state.field` no-op. A View `FrListener`/`FrConsumer` binds the exact337 ViewModel and Model generic types and uses only an exact transition parent,338 an exact equality early-return guard, or one exact transition-and-member `&&`339 condition before navigating inside the exact enum-member braced branch.340 The ViewModel must not own BuildContext, router types, Page objects, or router341 calls. Retain `Navigation: none` when no navigation follows. Direct342 presentation routing remains a View callback with no Event or Flow only when343 entry is unconditional.344- Put a Provider at the state owner's lifecycle and at the lowest common345 ancestor of all consumers:346 - `app-owned [AppViewModel]`: the root `AppProviders` owns it; the component347 consumes it directly and declares no local VM, Event, Model, or Provider.348 - `page-owned [XxxViewModel]`: `xxx.page.dart` creates it and dispatches the349 optional declared Startup Event; every Widget in that page subtree can350 consume it.351 - `none`: the component uses inputs/callbacks or Widget-local ephemeral352 state and declares no VM, Event, Model, or Provider.353 - `component-owned [XxxViewModel]`: explicit opt-in only for an independently354 embeddable compound component that owns a distinct lifecycle and has355 multiple descendants sharing its state.356- Cross-route shared components must not bind to a route-specific Page357 ViewModel. Consume app-owned state when the state is genuinely global;358 otherwise accept ordinary inputs/callbacks.359360## Page Route Inputs And Component Inputs361362- Do not declare `XxxPageArgs`. `XxxPage` is the single typed route input363 model; declare path, query, and `$extra` inputs as its constructor fields.364- The component library (`xxx.dart` and its parts) must never reference its own365 sibling `XxxPage`, generated route mixin, `GoRouterState`, or import its own366 `xxx.page.dart`. It may import another target route's `.page.dart` to use its367 generated Page helper or target-owned PageExtra for typed navigation.368- `XxxPage.build` consumes every route field in its ViewModel factory and/or369 ordinary named fields on `XxxView`.370- Do not declare component input wrappers named `XxxArgs` or `XxxConfig`.371- Pass only the route fields needed by `XxxViewModel` from the Page's Provider372 factory. Do not pass the Page route object into the component library.373- Use a route-owned `XxxPageExtra` only when several non-URL values must travel374 together through `$extra`. Declare it directly in the target `xxx.page.dart`,375 never in an independent model file. Treat it only as a route transport model,376 not domain data or ViewModel state. The target Page must expand it into377 ordinary View fields. Declare it with `@FrAcddFreezedJSON`, a redirecting378 `const factory`, and `fromJson`; add the page `.freezed.dart` part and retain379 its shared generated `.page.g.dart` part. Configure one application-owned380 `GoRouter.extraCodec` with tagged encode/decode cases for every PageExtra.381 Preserve the approved PageExtra field contract during migration; serialization382 requirements do not authorize changing field names, values, ownership, or383 lifecycle. Read `references/typed-routing.md` for the required shape.384385| Type | File | Consumers |386|---|---|---|387| `OrderContentPage.orderId` | `order_content.page.dart` | Path input and route system |388| `OrderContentPage.entryPoint` | `order_content.page.dart` | Query input and route system |389| `OrderContentView.orderId` | `order_content.v.dart` | View only, when rendering needs it |390| `OrderContentView.entryPoint` | `order_content.v.dart` | View only, when rendering needs it |391392Use this standard conversion shape:393394```dart395@TypedGoRoute<OrderContentPage>(path: '/orders/:orderId')396class OrderContentPage extends GoRouteData with $OrderContentPage {397 const OrderContentPage({required this.orderId, required this.entryPoint});398399 final String orderId;400 final String entryPoint;401402 @override403 Widget build(BuildContext context, GoRouterState state) => FrProvider(404 (context) => OrderContentViewModel(405 orderId: orderId,406 entryPoint: entryPoint,407 ),408 onCreated: (context, vm) => vm.add(const OrderContentStarted()),409 child: const OrderContentView(),410 );411}412```413414Page fields describe how the route enters and initialize page-owned state.415Ordinary View fields are reserved for values the View itself must render416without reading that state. Keep route types out of the component library.417418## Page Contract419420`xxx.page.dart` declares one route-to-Provider-to-view adapter:421422```dart423@TypedGoRoute<OrderContentPage>(path: '/orders/:orderId')424class OrderContentPage extends GoRouteData with $OrderContentPage {425 /* route fields -> page ViewModel and/or View fields */426}427```428429The route path is read from `@TypedGoRoute`, and the primary View is inferred430from `XxxPage.build`; do not duplicate either fact in documentation comments.431The primary View may compose any number of public/shared components and432Widgets recorded in its `Widget Tree:`; it does not limit a page to one433component. A page file may434declare additional typed Page variants for distinct URLs only when every435variant builds the same primary View; keep the basename-matching436`XxxPage` as the primary entry.437438## Typed Routing439440- New scaffolds use `go_router_builder` by default. Keep it and `build_runner`441 in `dev_dependencies`; every independent `xxx.page.dart` generates its own442 `$appRoutes`, and the root `app_router.dart` spreads those prefixed lists.443- Before adding or changing a route, read `references/typed-routing.md`.444- For a cross-page module or PageExtra migration, resolve `validate_routes`,445 read `references/validate_routes.md`, and run its module validator.446- For persistent bottom-navigation destinations, resolve447 `validate_navigation_shell`; branch switching uses the shell's `goBranch`448 API and is not Page-to-Page typed navigation.449- Make `XxxPage` the `GoRouteData`; do not create a separate `XxxRoute` or450 `XxxPageArgs`. Its `build` creates the page scope and constructs the primary451 `XxxView` below it.452- Navigate with generated route helpers when the destination is known in app453 code. Keep raw URI navigation only at explicit external/dynamic URI454 boundaries. `validate_routes` rejects fixed `context.go`/`push`/`replace`455 calls and `AppRoutes.xxx` indirection when the URI matches a typed Page;456 document exceptional compatibility boundaries with the required reasoned457 marker from `references/validate_routes.md`.458459## Contract-First Workflow4604611. Inspect Figma, run shared UI discovery, inspect the matching component and462 Widget module catalogs, nearby usage, and API context. Record one outcome:463 reuse an existing public entry; extend its owning module because no entry464 fits; or create a module because no module owns the capability. When the465 request supplies multiple Figma nodes or a Figma container node, first read466 `references/figma-screen-audit.md` and account for every supplied URL as a467 primary Frame, same-owner state, visual reference, or explicit exclusion.468 Inspect a container's structure before requesting any full design context.469 Find requested pages by their visible page-title text first, then resolve470 each matching text node to its owning Frame. Treat the Frame name as a471 candidate hint only because designers may leave it stale or unrelated.472 Confirm the screen from its hierarchy, navigation context, distinctive473 content, and render when needed; do not mistake a menu item or embedded474 title for the page itself. Record the confirmed `node-id`, current Frame475 name, and visible page title as one association. Select concrete Frames476 rather than reading the container as though it were a page.477 Present the resulting logical page/state ownership map before drafting.478 When primary Frames share a bottom navigation, also present their shell479 membership and classify shell roots, branch children, root fullscreen480 routes, and overlays before selecting Page or Scaffold ownership;481 never infer route or contract count from link count or visual similarity.482 Page drafts default to `BFF-JSON` when no concrete API is supplied.483 Shared component drafts default to `local` with `State Ownership: none`.484 Read485 `references/api-contract-semantics.md`; draw the cross-component data and486 business flow before defining DTOs.4872. For `gen_page`, draft `xxx.page.dart`, `xxx.dart`, `xxx.c.dart`, and the488 reviewable public-View stub in `xxx.v.dart`:489490```bash491uv run --script <skill-root>/scripts/draft_contract.py \492 --name order_content --dir lib/app/order_content \493 --figma-url <url> --figma-frame <exact-current-frame-name> \494 --figma-page-title <exact-visible-page-title|none> \495 --mode bff-json --route <route> \496 --preview-width <width> --preview-height <height> \497 --preview-wrapper <public-wrapper> \498 --theme <none|material|app-shared|component>499```500501 Read `references/widget-preview.md` before choosing the viewport and502 wrapper. Pass `--preview-wrapper-import <dart-uri>` when the public wrapper503 is declared outside the generated component library. The generator rejects504 missing Preview size or wrapper inputs instead of inventing them.505 For `app-shared` or `component`, also pass `--theme-type <ThemeType>`.506 Repeat `--extra-field name:DartType` when the Page needs multiple `$extra`507 values. The draft emits the required508 `@FrAcddFreezedJSON` PageExtra, generated parts, typed `$extra`, and509 PageExtra-to-ViewModel expansion. Register every emitted type in the510 application route-extra codec before final validation.511512 Use `lib/app/<route-segment>/` for a route-owned component and513 `lib/components/<component-name>/ --component-only` for a component reused514 across routes. `--component-only` defaults to `--mode local --state-owner515 none`, so it emits no VM, Model, Event, Provider, Freezed, JSON, or BFF516 assets. Use `--state-owner app --state-type <AppViewModel>` when it consumes517 an existing root-provided VM. Use `--state-owner component` only after518 explicitly proving an independent component lifecycle; component-owned519 API/BFF modes require that flag. Use an existing project's established520 equivalent roots when they differ, unless an approved adaptation moves521 them.5223. Bind the primary Figma Frame and every declared `Figma States` Frame back523 to the generated Dart files before contract review. Never bind `Figma524 References` or `Figma Excluded`. Record the primary Frame's exact current525 name, visible page title, and complete node-specific URL under `Figma:` so526 `node-id ↔ Frame name ↔ Page Title` remains explicit. Use `Page Title: none`527 only for an intentionally titleless page or component. Record each `Figma528 States` target as only its `node-id`, resolved against that primary design529 file.530 Never repeat the design URL in `Figma States`. For an approved screen, add531 exactly one `Figma Fidelity:` section in this fixed shape:532533```dart534/// Figma Fidelity:535/// - Viewport: 360 x 780536/// - Asset Lock: .agents/skills-config/fr-mvvm-contract/order-figma-assets.lock.json537/// - Regression Test: orderFigmaFidelity renders approved states538```539540 Use `Asset Lock: none` only when the authoritative Frame has no exported541 assets. Do not put the Figma file/node, viewport, routes, copy, source542 assertions, or test names in the lock. Do not write contract metadata,543 shared plugin data, or visible cards into Figma.544 Use the one-line `Figma Fidelity: excluded | <reason>` form instead when545 the current implementation is explicitly outside the approved gate.546 Prepare page contracts and target Frames one at a time; a page contract must547 target its exact Figma Frame, never a Section containing several pages.548 Execute the549 emitted `verifyCode` in a second `use_figma` call and inspect its screenshot.550 Do not continue if the primary URL lacks `node-id`, a page target is a551 non-Frame,552 either representation is missing or stale, the card is not above its page,553 a path is not visibly rendered, or the independent readback differs.5544. Before writing a Figma-derived View, read `references/figma_fill_data.md`.555 Classify every non-copy Figma fill in the `.c.dart` `Figma Data:` section.556 A draft may retain a `TODO(figma-data)` marker or a `Binding: pending`557 declaration; contract and final validation reject both. Do not use Figma sample values as production558 defaults. Use `scripts/figma_fill_data.py` to report pending, invalid, and559 legacy-unreviewed declarations across a project.5605. Internally classify each UI-facing BFF endpoint as `query` or `command`;561 do not ask the user to choose a type or write an API-type field. Read562 `references/frontend-interactions.md`. Use its request boundary as the563 endpoint identity, write exactly one record under `Behaviors:` and one564 endpoint-scoped `Request Field Sources:` record, then define structured565 `Interactions:` Flows only for ViewModel-owned Trigger, Bloc Event, guard,566 state phases, concurrency, and navigation-signal coordination. Do not turn567 View-local callbacks into Events or Flows. The same component may own query and command568 endpoints. Do not author `SDK Calls`, `SDK Call Flow`, backend method/path569 annotations, or backend orchestration in `.c.dart`. Backend developers own570 those facts and edit only the protected backend section of `xxx.bff.md`.571 The skill may define and refresh the frontend UI data API, UI DTOs, state,572 endpoint behavior, interaction Flows, structure, and integration mapping.573 It must preserve the complete backend section byte-for-byte. Set `BFF574 Service` to the Dart SDK-adapter service class, such as575 `[OrderContentService]`; every BFF-JSON contract requires runtime576 integration. When a data boundary is unknown or the required evidence has577 not been supplied, add `Data Boundary:` with a stable578 `TODO(data-boundary): <capability> — <missing authority/evidence>` marker.579 Do not turn that uncertainty into `BFF-UI-API: -`, sample-data behavior, or an580 invented method/path. `BFF-UI-API: -` is allowed only after an explicit,581 approved API-less/local-only decision. Search for582 `TODO(data-boundary)` before approval; contract and final validation reject583 unresolved markers. Never invent `/bootstrap`, `nextRoute`, proof, result,584 or error placeholders.585586 Write descriptive contract values in the resolved `Contract Description587 Language`. This applies to Behavior values, interaction state/feedback588 prose, Request Field Sources purpose prose, and Notes. Keep stable labels,589 Flow IDs, code identifiers, types,590 methods, paths, enum literals, and authoritative source expressions591 unchanged.592593 Keep the remaining approval contract minimal: Figma, API/BFF, state ownership,594 reusable UI in the Widget Tree, theme, Event and ViewModel595 references, models, and concise notes. Page Support contains only route and596 primary View facts.597 Define `Widget Tree` as a concise hierarchy of key Widgets that lets a598 reviewer understand the View from `.c.dart` alone. Include a Widget when it599 is directly interactive, carries primary information, expresses an600 important state, determines the functional structure, or is a shared601 component developers must recognize. Prefer 4–8 key Widgets and keep even602 complex Views to at most 12; fold larger trees into business-level regions,603 use `× N` for repeated items, and mark conditional states briefly.604605 When `Public Views:` lists more than one entry, write one `Widget Tree`606 bullet per public View in the same order. Do not add a discriminator enum607 merely to force semantically different entries through one generic View.608609 Preserve only necessary hierarchy. Omit formulaic `_XxxViewBody` nodes,610 `FrProvider`, `FrConsumer`, `Builder`, layout glue such as `Padding`,611 `SizedBox`, `Spacer`, `Align`, `Expanded`, `Flexible`, and `SafeArea`, pure612 decoration such as `Divider` and `DecoratedBox`, and component-internal613 labels/icons/spacing already covered by the parent component. Omit `Row`,614 `Column`, `Stack`, and `Container` unless one is essential to disambiguate615 business structure. A semantic private Widget such as `[_HomeHeader]` may616 remain.617618 Do not replace key Widget references with prose such as `confirmation form`.619 Do not draw a UI diagram or reproduce the complete runtime Widget tree.620 The provider module's public list is the dependency/reuse inventory; the621 concise `Widget Tree` records the entries actually composed here. Replace the generated TODO with an informative,622 concise tree before contract review and approval.623 For every `Behaviors:` record, remove the unused query or command fields;624 complete every `Interactions:` Flow, resolve every `TODO(data-boundary)` and625 other pending marker, then define DTO fields and synchronize typed `XxxPage`626 route fields to the final ordinary `XxxView` fields. The draft shell deliberately names not-yet-generated627 parts, so this review state is not a compilation or analyzer gate.6285. Present each UI API method/path and Req/Rsp/Error, its endpoint Behavior,629 endpoint-scoped field provenance, every frontend interaction Flow, and the630 SDK-adapter service class. A non-BFF local component may explicitly declare631 structured `Interactions:` only for ViewModel-owned local Flows; every such632 Flow uses `Uses: local` and requires no endpoint, BFF Service, SDK, or633 `bff.md`. Do not present, propose, or edit backend APIs or flow; backend634 developers maintain them in `xxx.bff.md`.6356. Validate the approved source contract before deriving files. This phase636 rejects semantic/API placeholders, missing or incomplete endpoint637 Behaviors, untraceable request fields, missing interaction coverage,638 invalid Event/Widget/Model/response references, UI-only command responses, invalid639 typed Page route-field conversion, incomplete Theme declarations, and missing direct640 dependencies, but does not require Freezed/JSON output yet:641642```bash643uv run --script <skill-root>/scripts/validate_contract.py \644 --page-file path/to/xxx.page.dart --phase contract645```6466477. For all non-contract work, read the contract through scripts rather than648 manually deriving decisions from raw Dart:649650```bash651uv run --script <skill-root>/scripts/read_contract.py \652 --page-file path/to/xxx.page.dart653uv run --script <skill-root>/scripts/read_contract.py \654 --component-file path/to/xxx.dart655```6566578. Prepare derived parts only from the approved reader output. The generator658 preflights the complete contract, Theme target, dependencies, and BFF659 extractor before committing any file. It prepares Theme changes, the BFF660 artifact, then `.vm.dart` and `.v.dart` stubs as one rollback-protected file661 set. An extractor or Theme failure must leave every prior file unchanged:662663```bash664uv run --script <skill-root>/scripts/generate_from_contract.py \665 --page-file path/to/xxx.page.dart --write-stubs666```667668Every BFF-JSON contract with runtime backend calls declares669`BFF Service: [Type]`. Use one Service per component. The Service imports the670required concrete SDK files from `lib/api/gen`, constructs configured generic671wrappers internally, and exposes semantic methods to its ViewModel. Do not672generate a frontend Retrofit client from the UI data API.673674A project may require every `XxxBffRsp` to model a complete gateway response such as675`{state, code, message, data}`. In that case the original business response is676the value of its `data` field, not a replacement for the outer envelope.677678Generated `*.bff.md` files begin with compact `bff-md-meta/v9` YAML Front679Matter containing schema, `@FrAcddPage` namespace/version, the authoritative680UI design source, a namespaced mdq v2 API-record contract, and no derivable681contract-file path. They separate inline UI682API DTOs from OpenAPI-owned backend operations, backend call flow, frontend UI683data, integration mapping, and a generated `API Query Records` verification684projection. The UI Contract contains UI State, endpoint Behaviors, generated685`前端交互逻辑`, and UI Structure in that order. Render UI State exclusively as686one JSON5 code block: every field has consecutive Model, Dart type, and687`Authority: Frontend` comments. Do not use Markdown tables for UI State. Read688`references/bff-dual-authority.md` before changing artifact structure,689ownership, generation, parsing, or validation.690691Render BFF artifacts in this fixed order: title, `后端业务流程与业务逻辑 API`, and692`前端 UI 数据接口`. The backend domain contains only OpenAPI document references,693the business API annotations and business flow written by backend developers.694Each business API annotation retains only method/path, parameter names and695types, and response DTO type. Backend developers may add prose, JSON, DTO field696examples, schema excerpts, or code blocks elsewhere in this protected domain;697the parser preserves that material and excludes it from machine API records.698Backend developers alone create and edit this entire domain. AI may create only699UI-facing paths and `XxxBffReq`/`XxxBffRsp` DTOs from approved Figma/UI700requirements.701It must not treat a backend business API method/path as a new UI-facing path.702On an exact method/703704…(truncated)