crewforth 3.0.4 → 3.1.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +200 -0
- package/README.md +3 -3
- package/VERSION +1 -1
- package/adopt.sh +86 -34
- package/kit/CLAUDE.md +1 -1
- package/kit/README.md +1 -1
- package/kit/agents/crew-commit-agent.md +7 -3
- package/kit/agents/crew-frontend-expert.md +4 -4
- package/kit/agents/crew-performance-expert.md +1 -1
- package/kit/agents/crew-review-agent.md +1 -1
- package/kit/eval/doctor.sh +57 -3
- package/kit/eval/golden-routing.txt +13 -0
- package/kit/eval/lib/git-bash.sh +11 -2
- package/kit/eval/parser-conformance.sh +1 -0
- package/kit/eval/smoke-test.sh +2572 -373
- package/kit/hooks/commit-msg +1 -1
- package/kit/hooks/context-usage.sh +7 -1
- package/kit/hooks/guard-bash.sh +1545 -104
- package/kit/hooks/guard-commit-scan.sh +75 -19
- package/kit/hooks/guard-powershell.sh +65 -15
- package/kit/hooks/guard-write.sh +74 -22
- package/kit/hooks/no-bash-guard.ps1 +71 -0
- package/kit/hooks/pre-commit +5 -2
- package/kit/hooks/prompt-approval.sh +484 -0
- package/kit/hooks/route-hint.sh +13 -5
- package/kit/hooks/session-rehydrate.sh +18 -0
- package/kit/hooks/skill-trust.sh +17 -2
- package/kit/owned-blobs.tsv +43 -0
- package/kit/settings.json +20 -4
- package/kit/skills/a11y/SKILL.md +1 -0
- package/kit/skills/crew-doctor/SKILL.md +2 -2
- package/kit/skills/frontend/SKILL.md +3 -3
- package/kit/skills/frontend-flutter/SKILL.md +124 -0
- package/kit/skills/frontend-flutter/references/adaptive-and-accessible.md +81 -0
- package/kit/skills/frontend-flutter/references/platform-async-release.md +100 -0
- package/kit/skills/frontend-flutter/references/rebuilds-and-performance.md +36 -0
- package/kit/skills/frontend-flutter/references/sensitive-data.md +54 -0
- package/kit/studio/server/hooks/studio-gate.sh +35 -1
- package/kit/studio/server/index.js +14 -4
- package/kit/studio/server/lib/graph.js +64 -3
- package/kit/studio/server/lib/kit.js +16 -2
- package/kit/studio/server/lib/permissions.js +77 -2
- package/kit/studio/server/lib/projects.js +60 -7
- package/kit/studio/server/lib/session.js +16 -2
- package/kit/studio/web/app.js +1765 -573
- package/kit/studio/web/approvals.js +124 -0
- package/kit/studio/web/canvas.js +932 -885
- package/kit/studio/web/chat.js +322 -145
- package/kit/studio/web/convo.js +174 -0
- package/kit/studio/web/dock.js +221 -0
- package/kit/studio/web/graph-plan.js +457 -0
- package/kit/studio/web/icons/builtin.svg +1 -0
- package/kit/studio/web/index.html +116 -50
- package/kit/studio/web/inspect.js +110 -0
- package/kit/studio/web/list-plan.js +112 -0
- package/kit/studio/web/list.js +157 -0
- package/kit/studio/web/liveness.js +66 -0
- package/kit/studio/web/nav.js +237 -0
- package/kit/studio/web/newsession.js +235 -0
- package/kit/studio/web/press.js +65 -0
- package/kit/studio/web/style.css +1281 -1014
- package/kit/studio/web/theme.js +43 -0
- package/kit/studio/web/timeline-plan.js +291 -0
- package/kit/studio/web/timeline.js +400 -0
- package/kit/studio/web/tokens.css +131 -0
- package/kit/studio/web/toolbar-fit.js +35 -0
- package/package.json +5 -4
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
# bare command has no word to take the language from. An English install adds nothing (English is the default).
|
|
19
19
|
# - CLAUDE.md itself reloads on /compact and /clear on its own, so this hook does NOT re-inject the discipline —
|
|
20
20
|
# only the session-specific state the reload can't recover.
|
|
21
|
+
# - THE GIT HOOKS LINE. Where the hooks are installed but git is not pointed at them, the session is told (below).
|
|
21
22
|
# - Fails OPEN and SILENT: no handover file, no output. It never blocks the session (always exits 0).
|
|
22
23
|
set -uo pipefail
|
|
23
24
|
IN="$(cat 2>/dev/null || true)"
|
|
@@ -50,6 +51,23 @@ if [ "$SRC" != startup ] && [ -s "$STATE" ]; then
|
|
|
50
51
|
MSG="A session handover from before this context boundary exists at docs/SESSION_STATE.md. If the user's request continues that work, read it first — it holds the in-progress task state, open decisions, and the intended next step — and do not restart from scratch. If the request is about something else, do not bring it up."
|
|
51
52
|
fi
|
|
52
53
|
[ -n "$LANGMSG" ] && MSG="${MSG:+$MSG }$LANGMSG"
|
|
54
|
+
|
|
55
|
+
# THE GIT HOOKS ARE NOT CONNECTED. pre-commit and commit-msg run only when core.hooksPath points at them (or at
|
|
56
|
+
# .claude/git-shim, which runs them and then the project's own). An install made before `git init`, and every clone of
|
|
57
|
+
# a repository that shares .claude/, starts without that setting (measured: a field project, and v3.0.0 again in three
|
|
58
|
+
# orders). The tool-level gates still judge a commit made through Claude Code; a commit typed in a terminal is scanned
|
|
59
|
+
# by nothing. Crewforth does not change git's configuration by itself, so the session is told, once at each start,
|
|
60
|
+
# and the user decides. Only where the hooks are installed and this is a repository; two git calls, at session start.
|
|
61
|
+
HOOKMSG=""
|
|
62
|
+
if [ -f "$ROOT/.claude/hooks/pre-commit" ] && git -C "$ROOT" rev-parse --git-dir >/dev/null 2>&1; then
|
|
63
|
+
_hp="$(git -C "$ROOT" config --get core.hooksPath 2>/dev/null || true)"; _hp="${_hp%$'\r'}"; _hp="${_hp//\\//}"
|
|
64
|
+
case "${_hp%/}" in
|
|
65
|
+
.claude/hooks|*/.claude/hooks|.claude/git-shim|*/.claude/git-shim) ;;
|
|
66
|
+
'') HOOKMSG="Crewforth's git hooks are not connected in this repository: core.hooksPath is not set, so a commit made outside Claude Code is not scanned for traces, secrets or size. Tell the user once, in one line. The fix is theirs to run in their own terminal, not yours: git config core.hooksPath .claude/hooks, or the Crewforth update when the repository has hooks of its own. /crew-doctor shows the state." ;;
|
|
67
|
+
*) HOOKMSG="Crewforth's git hooks are not connected in this repository: core.hooksPath points at another hook directory, so a commit made outside Claude Code is not scanned for traces, secrets or size. Tell the user once, in one line. The fix is theirs to run in their own terminal, not yours: the Crewforth update keeps that directory's hooks and runs Crewforth's before them. /crew-doctor shows the state." ;;
|
|
68
|
+
esac
|
|
69
|
+
fi
|
|
70
|
+
[ -n "$HOOKMSG" ] && MSG="${MSG:+$MSG }$HOOKMSG"
|
|
53
71
|
[ -n "$MSG" ] || exit 0 # no handover to offer and nothing to say about language -> stay silent
|
|
54
72
|
|
|
55
73
|
# hookSpecificOutput.additionalContext is the documented channel that injects text into the model's context.
|
package/kit/hooks/skill-trust.sh
CHANGED
|
@@ -72,6 +72,17 @@ digest(){
|
|
|
72
72
|
else cksum "$1" 2>/dev/null | tr -s ' ' | cut -d' ' -f1,2 | tr ' ' '-'; fi
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
# THE ANSWER IS CONFIRMED, AND FROM THE FILE. Both commands printed nothing on success, so a session that had just run
|
|
76
|
+
# one could not say the answer was recorded: in the field (two machines) it said it had not checked the record. One
|
|
77
|
+
# line now, and only after the component's line is READ BACK from the file it was written to; when it is not there
|
|
78
|
+
# the command fails and says so, instead of confirming a write that did not land.
|
|
79
|
+
_recorded(){ # $1 = file -> 0 when it holds this component's line (`<digest> <name>`), CR aside
|
|
80
|
+
local l; [ -f "$1" ] || return 1
|
|
81
|
+
while IFS= read -r l || [ -n "$l" ]; do [ "${l%$'\r'}" = "$dg $ONE" ] && return 0; done < "$1"
|
|
82
|
+
return 1; }
|
|
83
|
+
_confirm(){ # $1 = file, $2 = declined | trusted, $3 = what follows from it
|
|
84
|
+
_recorded "$1" || { echo "skill-trust: $ONE was NOT recorded — $1 does not hold its line after the write" >&2; exit 1; }
|
|
85
|
+
printf 'skill-trust: %s %s — recorded in .claude/%s (digest %s). %s\n' "$2" "$ONE" "${1##*/}" "${dg:0:12}" "$3"; }
|
|
75
86
|
if [ "$MODE" = one ] || [ "$MODE" = decline ]; then
|
|
76
87
|
_flag=trust; [ "$MODE" = decline ] && _flag=decline
|
|
77
88
|
# Every refusal says why on stderr: the updater shows it. It used to exit 1 silently, and the updater discarded
|
|
@@ -89,6 +100,8 @@ if [ "$MODE" = one ] || [ "$MODE" = decline ]; then
|
|
|
89
100
|
fi
|
|
90
101
|
drop_name "$TRUST" "$ONE" 2>/dev/null || { echo "skill-trust: cannot write $TRUST" >&2; exit 1; }
|
|
91
102
|
{ drop_name "$DECL" "$ONE" && printf '%s %s\n' "$dg" "$ONE" >> "$DECL"; } 2>/dev/null || { echo "skill-trust: cannot write $DECL" >&2; exit 1; }
|
|
103
|
+
_recorded "$TRUST" && { echo "skill-trust: $ONE is still in $TRUST after the decline — the earlier yes was not dropped" >&2; exit 1; }
|
|
104
|
+
_confirm "$DECL" declined "It will not be used, and not asked about again. To trust it later: bash .claude/hooks/skill-trust.sh --trust-one $ONE (Bash tool, not PowerShell)"
|
|
92
105
|
exit 0
|
|
93
106
|
fi
|
|
94
107
|
drop_name "$DECL" "$ONE" 2>/dev/null || { echo "skill-trust: cannot write $DECL" >&2; exit 1; }
|
|
@@ -96,8 +109,8 @@ if [ "$MODE" = one ] || [ "$MODE" = decline ]; then
|
|
|
96
109
|
printf '# Components reviewed and accepted by the user. Regenerate with: bash skill-trust.sh --trust\n' > "$TRUST" 2>/dev/null \
|
|
97
110
|
|| { echo "skill-trust: cannot create $TRUST" >&2; exit 1; }
|
|
98
111
|
fi
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
_recorded "$TRUST" || printf '%s %s\n' "$dg" "$ONE" >> "$TRUST" 2>/dev/null || { echo "skill-trust: cannot write $TRUST" >&2; exit 1; }
|
|
113
|
+
_confirm "$TRUST" trusted "A later change to it is reported again."
|
|
101
114
|
exit 0
|
|
102
115
|
fi
|
|
103
116
|
|
|
@@ -203,4 +216,6 @@ printf 'whether to trust it — one by one. Until they answer, do not use these
|
|
|
203
216
|
printf 'On a yes for a component, run the trust command listed under it (Bash tool, not PowerShell); it records that\n'
|
|
204
217
|
printf 'one component only, and re-flags it if it is edited later. On a no, run the decline command listed under it: the\n'
|
|
205
218
|
printf 'answer is then recorded, and neither a later session nor a Crewforth update asks or decides again.\n'
|
|
219
|
+
printf 'Either command prints one line that starts with "skill-trust: trusted" or "skill-trust: declined" once the record\n'
|
|
220
|
+
printf 'is read back from its file: pass that line to the user. No such line means the answer was NOT recorded.\n'
|
|
206
221
|
exit 0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.0.4 (below VERSION 3.1.0) — do not edit by hand.
|
|
2
|
+
# The flat files Crewforth owns: a copy whose bytes (CR aside) are one of these ids was never edited.
|
|
3
|
+
# install_path blob_sha1 first_tag last_tag
|
|
4
|
+
.claude/AGENT_TEMPLATE.md 075e2c26706c06f56af01652cd24c36918513bee v3.0.0 v3.0.4
|
|
5
|
+
.claude/AGENT_TEMPLATE.md 3fdc551d221ee3665b21ef8cc3563de861a9abe6 v1.4.0 v1.6.3
|
|
6
|
+
.claude/AGENT_TEMPLATE.md 686fed306c873b45c245c77a95a8e6de6ee383d5 v1.7.0 v2.11.0
|
|
7
|
+
.claude/AGENT_TEMPLATE.md 981df9ea27b6f1c5ab5cb509d40875a07603858d v2.12.0 v2.13.0
|
|
8
|
+
.claude/AGENT_TEMPLATE.md b38b9c9b799156e07280ca886230524563acc75c v1.0.5 v1.3.0
|
|
9
|
+
.claude/AGENT_TEMPLATE.md faec09e45543b75763c43bb24114a7b6355649ec v1.0.0 v1.0.4
|
|
10
|
+
.claude/DISCIPLINE.md 06e44a8767c991dcf91a689ae5be2514538b9006 v2.7.1 v2.7.1
|
|
11
|
+
.claude/DISCIPLINE.md 10b3da6ac62bcbb3d32879913efe07164141ff09 v1.8.0 v1.9.0
|
|
12
|
+
.claude/DISCIPLINE.md 228798df2cb7c43085bf9553ca92e7f4e7018fc4 v1.10.1 v1.10.1
|
|
13
|
+
.claude/DISCIPLINE.md 237f6ffb4a82eb9349a3dfa2ae1550aaac5ec7db v1.1.0 v1.1.0
|
|
14
|
+
.claude/DISCIPLINE.md 2a1b7f2962b1c164d85f427a4c299c125fac4bc8 v2.10.0 v2.10.1
|
|
15
|
+
.claude/DISCIPLINE.md 2e05ff78a133cdf5d24c826e0dc729bb4bdc0460 v1.1.9 v1.3.0
|
|
16
|
+
.claude/DISCIPLINE.md 2fe0b8b325352c8f5488f7e9efbe6dbc0ff1418b v2.7.0 v2.7.0
|
|
17
|
+
.claude/DISCIPLINE.md 316e6e68311448f04f17fdb34f4f737b713b91f9 v1.7.0 v1.7.0
|
|
18
|
+
.claude/DISCIPLINE.md 4216e55e0d7f4392c40f5cf41266f109f435384f v1.6.0 v1.6.1
|
|
19
|
+
.claude/DISCIPLINE.md 46d76b00ea065b8715ac5c38baf9be4401077b89 v2.2.0 v2.2.2
|
|
20
|
+
.claude/DISCIPLINE.md 4afcfe88e5e1c1f79824a211d7bccabd82b36bdc v1.5.0 v1.5.1
|
|
21
|
+
.claude/DISCIPLINE.md 4b3ed1d5ac6c016368f64c805bca7ea968dc98eb v2.11.0 v2.11.0
|
|
22
|
+
.claude/DISCIPLINE.md 5cfb98a0328f33da75fb1f97a7a140a72e59ddc9 v1.1.1 v1.1.2
|
|
23
|
+
.claude/DISCIPLINE.md 604b3c4036dbb539f6d53a264769a9b0738f6183 v2.3.0 v2.4.0
|
|
24
|
+
.claude/DISCIPLINE.md 718b98b9caa74c0406962cb11d829bcdc20f2dcf v3.0.1 v3.0.4
|
|
25
|
+
.claude/DISCIPLINE.md 76d73a4e25c559ea361618862cde8480b1a6d8c2 v3.0.0 v3.0.0
|
|
26
|
+
.claude/DISCIPLINE.md b1ab7549b571dd2fc80d5777e0ae3185d29bd7b6 v2.13.0 v2.13.0
|
|
27
|
+
.claude/DISCIPLINE.md b7e1066451a8590e0a2607ad0a6fe80b21599c53 v1.1.3 v1.1.8
|
|
28
|
+
.claude/DISCIPLINE.md bf062c2b51a9cf3cc4d0caac389c148cf6fdc977 v2.12.0 v2.12.0
|
|
29
|
+
.claude/DISCIPLINE.md c693f1bd5832ce570058c6eedcacb5dff13d89e8 v1.10.0 v1.10.0
|
|
30
|
+
.claude/DISCIPLINE.md d63a01a183c9a80d739c840a511d338dc2765820 v1.4.0 v1.4.4
|
|
31
|
+
.claude/DISCIPLINE.md dc1c41baa3a8b7d637214bc78553a78d94d14ac9 v1.6.2 v1.6.3
|
|
32
|
+
.claude/DISCIPLINE.md e42aefc9c1ffaeace8e5712d92df780959212280 v2.0.0 v2.1.0
|
|
33
|
+
.claude/DISCIPLINE.md e5c7363dba8601cf2bb0ba929ceb4a64ea8bfbb2 v2.7.2 v2.9.0
|
|
34
|
+
.claude/DISCIPLINE.md f5080a758d638c9591b516ce1cfb5cea9876e301 v2.5.0 v2.6.0
|
|
35
|
+
.claude/README.md 1ab816124a93301a587156abefc4a8d977db9dcf v2.0.0 v2.11.0
|
|
36
|
+
.claude/README.md 3251925270a9939e10c51b3b948554fe0d4ec1b4 v2.12.0 v2.13.0
|
|
37
|
+
.claude/README.md 391a4c90cd6f3465071891c388a7d9f522691d30 v1.8.0 v1.10.1
|
|
38
|
+
.claude/README.md 3bbffe0eefcc5576edbd25dba9bb5545e80691ae v1.0.5 v1.0.9
|
|
39
|
+
.claude/README.md 62a78576391c05e2193ebdd5b90196070684866e v3.0.0 v3.0.0
|
|
40
|
+
.claude/README.md 7d02d40b8639448f311879873c7b0b532d54b40f v1.1.0 v1.1.0
|
|
41
|
+
.claude/README.md 97d4dd1f7f1695052cc1d06cc43f5bcdc603c989 v1.0.0 v1.0.4
|
|
42
|
+
.claude/README.md a3419891cfda7385564008e4befc1007bc5364cb v1.1.1 v1.7.0
|
|
43
|
+
.claude/README.md a3b76e0fa31267610d28b742c122a962f6db59c4 v3.0.1 v3.0.4
|
package/kit/settings.json
CHANGED
|
@@ -38,6 +38,16 @@
|
|
|
38
38
|
},
|
|
39
39
|
"hooks": {
|
|
40
40
|
"PreToolUse": [
|
|
41
|
+
{
|
|
42
|
+
"matcher": "Bash|PowerShell|Write|Edit|NotebookEdit",
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "echo --% >/dev/null;: ' | out-null\n<#'\nexit 0\n#> try { Invoke-Expression (Get-Content -Raw -LiteralPath (Join-Path $env:CLAUDE_PROJECT_DIR '.claude/hooks/no-bash-guard.ps1') -ErrorAction Stop) } catch { [Console]::Error.WriteLine(\"GUARD: Crewforth's gate for a machine without Git Bash could not be loaded, so this tool call is stopped: $_\"); exit 2 }",
|
|
47
|
+
"timeout": 600
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
41
51
|
{
|
|
42
52
|
"matcher": "Bash|PowerShell",
|
|
43
53
|
"hooks": [
|
|
@@ -45,13 +55,13 @@
|
|
|
45
55
|
"type": "command",
|
|
46
56
|
"shell": "bash",
|
|
47
57
|
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/guard-bash.sh",
|
|
48
|
-
"timeout":
|
|
58
|
+
"timeout": 600
|
|
49
59
|
},
|
|
50
60
|
{
|
|
51
61
|
"type": "command",
|
|
52
62
|
"shell": "bash",
|
|
53
63
|
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/guard-commit-scan.sh",
|
|
54
|
-
"timeout":
|
|
64
|
+
"timeout": 600
|
|
55
65
|
}
|
|
56
66
|
]
|
|
57
67
|
},
|
|
@@ -62,7 +72,7 @@
|
|
|
62
72
|
"type": "command",
|
|
63
73
|
"shell": "bash",
|
|
64
74
|
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/guard-powershell.sh",
|
|
65
|
-
"timeout":
|
|
75
|
+
"timeout": 600
|
|
66
76
|
}
|
|
67
77
|
]
|
|
68
78
|
},
|
|
@@ -73,7 +83,7 @@
|
|
|
73
83
|
"type": "command",
|
|
74
84
|
"shell": "bash",
|
|
75
85
|
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/guard-write.sh",
|
|
76
|
-
"timeout":
|
|
86
|
+
"timeout": 600
|
|
77
87
|
}
|
|
78
88
|
]
|
|
79
89
|
}
|
|
@@ -81,6 +91,12 @@
|
|
|
81
91
|
"UserPromptSubmit": [
|
|
82
92
|
{
|
|
83
93
|
"hooks": [
|
|
94
|
+
{
|
|
95
|
+
"type": "command",
|
|
96
|
+
"shell": "bash",
|
|
97
|
+
"command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/prompt-approval.sh",
|
|
98
|
+
"timeout": 60
|
|
99
|
+
},
|
|
84
100
|
{
|
|
85
101
|
"type": "command",
|
|
86
102
|
"shell": "bash",
|
package/kit/skills/a11y/SKILL.md
CHANGED
|
@@ -35,6 +35,7 @@ Stack-agnostic (web/React/RN); do a web search when needed for framework-specifi
|
|
|
35
35
|
## React / RN note
|
|
36
36
|
- Web React: semantic element in JSX + `htmlFor`/`aria-*`; use `button` instead of a clickable `div`.
|
|
37
37
|
- React Native: `accessible`, `accessibilityLabel`, `accessibilityRole`, `accessibilityState` (coordinate with `frontend-rn-expo`).
|
|
38
|
+
- Flutter: the guideline test (`meetsGuideline` with the tap-target, label and contrast guidelines) on every new screen (coordinate with `frontend-flutter`).
|
|
38
39
|
|
|
39
40
|
## Invariant rules
|
|
40
41
|
1. **Semantics first, ARIA second** — wrong ARIA does harm.
|
|
@@ -9,7 +9,7 @@ Verify Crewforth is actually *active* in this project (not just present on disk)
|
|
|
9
9
|
1. Run `bash .claude/eval/doctor.sh` (Bash tool, not PowerShell).
|
|
10
10
|
2. Read its report. It checks: VERSION present · every hook executable · the required git hooks (pre-commit,
|
|
11
11
|
commit-msg) present · **guard-bash actually blocks a force-push** (catches a hook that is present but neutered) ·
|
|
12
|
-
`core.hooksPath` points at `.claude/hooks` (else the §4.1/§4.2 commit trace + secret/bloat scan never runs) ·
|
|
12
|
+
`core.hooksPath` points at `.claude/hooks`, or at `.claude/git-shim` when the project has hooks of its own (else the §4.1/§4.2 commit trace + secret/bloat scan never runs) ·
|
|
13
13
|
`settings.json` valid and wiring the PreToolUse / UserPromptSubmit / Stop gates to **non-empty** hook arrays
|
|
14
14
|
(an empty `[]` wires nothing); SessionStart (rehydration) is reported as a warning if absent · **`./CLAUDE.md`
|
|
15
15
|
actually imports `.claude/DISCIPLINE.md`** — without that line the discipline sits on disk and never loads, which
|
|
@@ -44,7 +44,7 @@ PowerShell and Crewforth's gates do not run. The full install's doctor runs the
|
|
|
44
44
|
`bash "${CLAUDE_PLUGIN_ROOT}/eval/lib/git-bash.sh"` (Bash tool, not PowerShell). Pass its lines to the user verbatim;
|
|
45
45
|
a ❌ is a failure with its fix. If this session has **no Bash tool at all**, that is the finding itself — Claude Code
|
|
46
46
|
found no Git Bash: tell the user the gates do not run and that the fix is Git for Windows in its default folder, or
|
|
47
|
-
`CLAUDE_CODE_GIT_BASH_PATH` set to its `bin\bash.exe
|
|
47
|
+
`CLAUDE_CODE_GIT_BASH_PATH` set to its `bin\bash.exe`, and then the terminal and Claude Code closed and opened again (an open terminal keeps the old value). Do not run the script through PowerShell.
|
|
48
48
|
|
|
49
49
|
**The eval scripts are installer-only, by decision.** `eval/` — `doctor.sh`, `smoke-test.sh`, `routing-eval.sh`,
|
|
50
50
|
`scan-skill.sh`, `utilization.sh` — ships with `start.sh` and `adopt.sh` and NOT with the plugin edition; `eval/lib/`
|
|
@@ -29,13 +29,13 @@ There is no default client stack, and none is suggested: the user chooses. **Mod
|
|
|
29
29
|
4. **Still open (a new project) → ask, once.** A few questions at most (which platforms · which client stack), the
|
|
30
30
|
options listed plainly — **no option marked recommended and no "Decide for me"**: the stack is the user's call.
|
|
31
31
|
This is the one decision where the discipline's "give a clear recommendation" does NOT apply: state each option's
|
|
32
|
-
trade-off and stop there. That Crewforth ships
|
|
33
|
-
favour
|
|
32
|
+
trade-off and stop there. That Crewforth ships layers for two stacks (`frontend-rn-expo`, `frontend-flutter`) is
|
|
33
|
+
not a reason to favour either (measured: the first version of this step still got "React Native + Expo (recommended)", because of it).
|
|
34
34
|
No one to ask (headless) → write the questions as text and stop; do not start client code on a guessed stack.
|
|
35
35
|
|
|
36
36
|
**Record it** in the `Client:` line of `CLAUDE.md ## Stack`, plus an ADR (`adr` skill). **Never ask twice** — a
|
|
37
37
|
recorded client stack changes only when the user asks. Then apply this skill on that stack; an RN/Expo project
|
|
38
|
-
also applies `frontend-rn-expo`.
|
|
38
|
+
also applies `frontend-rn-expo`, a Flutter project `frontend-flutter`.
|
|
39
39
|
|
|
40
40
|
## Architecture
|
|
41
41
|
- **Presentation / logic separation:** component/view is pure and thin; business logic lives in the hook/composable/service layer.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-flutter
|
|
3
|
+
description: |
|
|
4
|
+
OPTIONAL, stack-specific: Flutter. Flutter projects only; the generic principles live in the `frontend` skill.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Flutter (stack-specific layer)
|
|
8
|
+
|
|
9
|
+
**Apply only when the project IS Flutter** — its `pubspec.yaml` depends on the SDK (`flutter: sdk: flutter`).
|
|
10
|
+
Otherwise do not use this skill: `frontend` and the project's own stack apply (see its Client stack order).
|
|
11
|
+
|
|
12
|
+
<!-- routing-eval reads the next line; why it sits in the body: AGENT_TEMPLATE.md -->
|
|
13
|
+
Trigger phrases: "riverpod", "go_router", "pubspec", "widget test", "analysis_options", "flutter_test", "golden test", "platform channel", "dart isolate", "gen-l10n"
|
|
14
|
+
|
|
15
|
+
For generic frontend discipline, applies the `frontend` skill; this file only covers what Flutter adds. Every rule
|
|
16
|
+
below was checked against Flutter's own documentation (docs.flutter.dev, dart.dev) or the page of the package or
|
|
17
|
+
platform it names; the depth and the sources are in `references/`, which also say what was not checked. Not used in a project on another stack (delete it if needed).
|
|
18
|
+
|
|
19
|
+
## The project decides, not this file
|
|
20
|
+
- **Read before you write:** `pubspec.yaml` (the packages already chosen), `analysis_options.yaml` (the lint set and
|
|
21
|
+
formatter width), `pubspec.lock`, and how `lib/` is already laid out. Follow them.
|
|
22
|
+
- **State management, routing, code generation and the lint set are the project's choice.** Flutter's own guidance
|
|
23
|
+
names no single state approach ("the best choice … depends on the app's complexity, your team's preferences");
|
|
24
|
+
built in are `setState`, `ValueNotifier`/`ChangeNotifier` and `InheritedWidget`. A package is added only when the
|
|
25
|
+
user agrees, and recorded (`adr`). Never introduce a second state or routing approach beside the one in use.
|
|
26
|
+
- **A new project** asks once, the same way `frontend`'s Client stack step does: options and trade-offs, none marked
|
|
27
|
+
recommended.
|
|
28
|
+
|
|
29
|
+
## Architecture (Flutter's architecture guide; its "strongly recommend" items)
|
|
30
|
+
- Two layers at least: **UI** (views + view models) and **data** (repositories + services). A repository is behind
|
|
31
|
+
an abstract class, so tests can use a fake. **No logic in widgets** — a widget renders state and forwards events.
|
|
32
|
+
- **Unidirectional data flow** and **immutable models**; a model that crosses a layer is not mutated in place.
|
|
33
|
+
- **Dependencies are injected** into view models and repositories, not looked up from inside them. A domain layer
|
|
34
|
+
only when the logic needs one (the guide marks it conditional). **Where there is one it is plain Dart: no file in
|
|
35
|
+
it imports `package:flutter`**, and an architecture test reads its files and fails on such an import.
|
|
36
|
+
- Feature folders (`features/<name>/`) with view, view model and tests together, as `frontend` says.
|
|
37
|
+
|
|
38
|
+
## Widgets and rebuilds — the cost model
|
|
39
|
+
- **`const` constructors wherever possible**; Flutter short-circuits the rebuild of a `const` subtree.
|
|
40
|
+
- **Split by what changes:** keep `setState` (or the provider read) at the smallest subtree that needs it. A reusable
|
|
41
|
+
piece of UI is a `StatelessWidget`, not a helper function returning a widget.
|
|
42
|
+
- **`build()` stays cheap:** no I/O, parsing or allocation-heavy work in it — it runs on every ancestor rebuild.
|
|
43
|
+
- **Long lists and grids use the lazy builders** (`ListView.builder` and friends), never a `Column` of every item.
|
|
44
|
+
- Animations: no `Opacity` inside an animation (`AnimatedOpacity`/`FadeInImage`), no clipping while animating, and no
|
|
45
|
+
widget that forces a `saveLayer` without a reason. Detail: **`references/rebuilds-and-performance.md`**.
|
|
46
|
+
|
|
47
|
+
## Layout that adapts to the window
|
|
48
|
+
- **Decide by the window, never by the device type** ("phone"/"tablet" checks are wrong on foldables, split screen
|
|
49
|
+
and desktop). Measure with **`MediaQuery.sizeOf`** (the whole window; cheaper than `MediaQuery.of`) or
|
|
50
|
+
**`LayoutBuilder`** (the space this widget is given). Breakpoints come from the project's design system; the
|
|
51
|
+
Material window classes are compact < 600 · medium 600–839 · expanded 840–1199 · large 1200–1599 · ≥ 1600 dp.
|
|
52
|
+
- **List and detail:** two panes side by side in an expanded window, a push to the detail in a compact one. **What
|
|
53
|
+
the user selected and what they typed survive a change of window class** (a fold, a rotation, a resized window):
|
|
54
|
+
that state lives above the two layouts, not inside either.
|
|
55
|
+
- **A foldable:** when `MediaQuery.displayFeatures` holds a hinge that separates the window, the panes split at the
|
|
56
|
+
hinge and no content is laid out on it.
|
|
57
|
+
- **Do not lock orientation** — it is an accessibility problem. Keep scroll position across a layout change
|
|
58
|
+
(`PageStorageKey`), and support mouse, trackpad and keyboard where the app runs on desktop or a large screen.
|
|
59
|
+
- Accessibility: tap targets 48×48 (Android) / 44×44 (iOS), every tappable labelled, text contrast checked, the UI
|
|
60
|
+
usable at large text scales — tested, not eyeballed (`meetsGuideline`, below). Detail and the test:
|
|
61
|
+
**`references/adaptive-and-accessible.md`**. The `a11y` skill holds the rules for every stack.
|
|
62
|
+
|
|
63
|
+
## Off the main isolate, across the platform boundary
|
|
64
|
+
- **Work that would take longer than a frame** (decoding a large file, heavy parsing, image work) goes to an isolate:
|
|
65
|
+
`Isolate.run` / `compute` for one-off work, `Isolate.spawn` + ports for a long-lived worker. Spawned isolates have
|
|
66
|
+
no `rootBundle` and no UI. On the web there are no isolates: `compute` keeps it compiling and runs on the main thread.
|
|
67
|
+
- **Platform channels** (`MethodChannel`, `EventChannel`) are asynchronous; the Dart side handles
|
|
68
|
+
`PlatformException` and `MissingPluginException` explicitly, and the native handler runs on the platform's main
|
|
69
|
+
thread. Prefer generated, typed messages (`pigeon`) over string-matched method names. Detail:
|
|
70
|
+
**`references/platform-async-release.md`**.
|
|
71
|
+
|
|
72
|
+
## Navigation, text, data
|
|
73
|
+
- Navigation: the project's router. Named routes are not recommended by Flutter (no custom deep-link handling, no
|
|
74
|
+
browser forward button); a page-backed route is deep-linkable, a pageless one is not.
|
|
75
|
+
- **No user-facing text is written inline in a widget, in any app.** An app in more than one language uses
|
|
76
|
+
`gen-l10n` (ARB files, `AppLocalizations.of(context)`), plurals and placeholders in ICU form — never concatenated;
|
|
77
|
+
the `i18n-integrity` skill checks the files. An app in one language may keep every text in one central file instead.
|
|
78
|
+
- **Changing case follows the language.** Dart's `toUpperCase()` / `toLowerCase()` use the language-independent
|
|
79
|
+
mapping (Turkish `i` becomes `I`, not `İ`), so text is cased through one helper that takes the locale.
|
|
80
|
+
- Money is never a `double`; dates carry their time zone or are date-only on purpose (the `frontend` rules apply).
|
|
81
|
+
**Amounts and other numbers that line up are drawn with `FontFeature.tabularFigures()`**, so digits keep one width.
|
|
82
|
+
|
|
83
|
+
## Tests (the `testing` skill owns the strategy)
|
|
84
|
+
- Many **unit** and **widget** tests (`flutter_test`), and enough **integration** tests (`integration_test/`, the
|
|
85
|
+
binding initialised) for the important flows. Tests use fakes of the repositories and services (the guide's
|
|
86
|
+
recommendation), which is why those sit behind abstract classes.
|
|
87
|
+
- **Golden tests** (`matchesGoldenFile`) only where pixels are the contract, fonts loaded first. **They are generated
|
|
88
|
+
and compared in one place, a pinned container in CI**; `flutter test --update-goldens` is not run on a developer's
|
|
89
|
+
machine, whose rendering differs.
|
|
90
|
+
- **Every new screen reached from the app's navigation gets a widget test at one width of each window class** the
|
|
91
|
+
app supports (for example 320, 360, 700 and 1280 dp), **and a test that crosses a class boundary and finds the
|
|
92
|
+
selection and the typed input still there.** Plus the a11y guideline test.
|
|
93
|
+
|
|
94
|
+
## If the app holds sensitive data (apply only then)
|
|
95
|
+
Financial, health or identity data, or anything the user would not want read off a lost phone or a backup:
|
|
96
|
+
- **The local database is encrypted**, and its **key lives in the platform's secure store** (Keychain, Keystore),
|
|
97
|
+
never in the database's own directory, in preferences or in the code.
|
|
98
|
+
- **The database and the key's files are kept out of the operating system's backup and device transfer**, on both
|
|
99
|
+
platforms.
|
|
100
|
+
- **Every network call goes through a transport bound to its own allowed host or hosts.** A call that leaves that
|
|
101
|
+
list is refused: it throws before anything is sent, and a test makes such a call and expects the throw. One
|
|
102
|
+
transport for the whole app or one per service are both this rule; a call made outside any transport is not.
|
|
103
|
+
- **In an app that shows ads, no ad is drawn in a window where a sensitive screen is visible** — in a two-pane
|
|
104
|
+
layout that is the whole window, not the pane.
|
|
105
|
+
How, and what was checked: **`references/sensitive-data.md`**.
|
|
106
|
+
|
|
107
|
+
## Performance and release
|
|
108
|
+
- **Measure in profile mode on a real device** (`flutter run --profile`); debug mode is not indicative and profile
|
|
109
|
+
mode does not run on an emulator or simulator. Frame budget: about 16 ms at 60 Hz, 8 ms at 120 Hz.
|
|
110
|
+
- Release: `--obfuscate --split-debug-info=<dir>` where the target supports it, the symbols kept for `flutter
|
|
111
|
+
symbolize`. Obfuscation is not encryption: **no secret ships in the app**. An app commits its `pubspec.lock`.
|
|
112
|
+
- **A feature closed by a build flag stays closed in the store build:** the store build fails when it is handed
|
|
113
|
+
that flag, on Android and on iOS. Detail: **`references/platform-async-release.md`**.
|
|
114
|
+
- Logging: `debugPrint` prints in release mode too unless it sits behind a debug check or an assert; log through the
|
|
115
|
+
project's logger (the `observability` skill).
|
|
116
|
+
|
|
117
|
+
## DoD (in addition to the generic `frontend` DoD)
|
|
118
|
+
- `dart format --set-exit-if-changed .` exits 0 · `flutter analyze` clean (0 issues, the project's lint set) ·
|
|
119
|
+
`flutter test` green — one run after the last edit, reported with its exit code and counts.
|
|
120
|
+
- No widget holds business logic; no device-type or orientation check decides a layout.
|
|
121
|
+
- The a11y guideline test passes on every new screen; each window class the app supports is tested at one width,
|
|
122
|
+
and state survives a change of class.
|
|
123
|
+
- No user-facing text inline; the domain layer (if any) imports no `package:flutter`.
|
|
124
|
+
- Works across the project's target platform matrix (Android / iOS / web / desktop as declared).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Adaptive layout and accessibility (Flutter)
|
|
2
|
+
|
|
3
|
+
Checked against https://docs.flutter.dev/ui/adaptive-responsive/general,
|
|
4
|
+
https://docs.flutter.dev/ui/adaptive-responsive/best-practices,
|
|
5
|
+
https://docs.flutter.dev/ui/accessibility-and-internationalization/accessibility,
|
|
6
|
+
https://docs.flutter.dev/ui/accessibility/accessibility-testing and the Material window size classes as Android's
|
|
7
|
+
documentation states them (2026-10-02); the foldable part against
|
|
8
|
+
https://api.flutter.dev/flutter/widgets/MediaQueryData/displayFeatures.html (2026-10-07).
|
|
9
|
+
|
|
10
|
+
## The three steps
|
|
11
|
+
1. **Abstract** the widgets that change with the space, and the data they share.
|
|
12
|
+
2. **Measure:** `MediaQuery.sizeOf(context)` for the whole window — cheaper than `MediaQuery.of`, which rebuilds on
|
|
13
|
+
any change of the whole `MediaQueryData` — or `LayoutBuilder` for the space this widget is given (it hands you
|
|
14
|
+
`BoxConstraints`, a range, not a size).
|
|
15
|
+
3. **Branch** on breakpoints. The project's design system names them; the Material window size classes are:
|
|
16
|
+
|
|
17
|
+
| Class | Width |
|
|
18
|
+
|:--|:--|
|
|
19
|
+
| compact | < 600 dp |
|
|
20
|
+
| medium | 600 – 839 dp |
|
|
21
|
+
| expanded | 840 – 1199 dp |
|
|
22
|
+
| large | 1200 – 1599 dp |
|
|
23
|
+
| extra-large | ≥ 1600 dp |
|
|
24
|
+
|
|
25
|
+
Material's own example: a bottom navigation bar below 600 dp, a navigation rail from 600 dp.
|
|
26
|
+
|
|
27
|
+
## What not to do
|
|
28
|
+
- **No device-type checks** ("is this a phone or a tablet") for a layout decision: a foldable, a split screen or a
|
|
29
|
+
resizable desktop window breaks them. The window's size decides.
|
|
30
|
+
- **No orientation lock.** It is an accessibility problem; if a lock is truly required, read the physical size from
|
|
31
|
+
the `Display` API, not `MediaQuery`, and support both portrait directions.
|
|
32
|
+
- Large widgets broken into smaller ones, so `const` instances can be reused across a resize.
|
|
33
|
+
- A list that keeps its layout across a rotation keeps its scroll position: `PageStorageKey`.
|
|
34
|
+
- Mouse, trackpad and keyboard shortcuts work where the app runs on desktop or a large screen.
|
|
35
|
+
|
|
36
|
+
## List and detail
|
|
37
|
+
- Expanded and wider: the list and the detail are two panes of one screen. Compact: the list is the screen and the
|
|
38
|
+
detail is pushed over it. Medium is the design system's call.
|
|
39
|
+
- **The selection and any typed input are kept across a change of window class.** Hold them above the two layouts
|
|
40
|
+
(the view model, or state owned by the widget that chooses the layout), so that going from two panes to one shows
|
|
41
|
+
the detail that was selected, with the form as it was, and going back does not clear it. A `GlobalKey` or a
|
|
42
|
+
`PageStorageKey` keeps a subtree's state only while its place in the tree allows it; state that must survive is
|
|
43
|
+
not left to that.
|
|
44
|
+
|
|
45
|
+
## A foldable
|
|
46
|
+
- `MediaQuery.displayFeatures` (and `MediaQueryData.displayFeatures`) lists the areas of the display that hardware
|
|
47
|
+
obstructs. The documentation says it is populated on Android only. A **hinge** and a **cutout** obstruct the
|
|
48
|
+
display; a **fold** is a crease and does not. A hinge and a fold carry a state that tells the device's posture.
|
|
49
|
+
- When a hinge separates the window into two areas, **the panes split at the hinge**: one pane on each side, and
|
|
50
|
+
nothing — no text, no control, no divider the user must touch — laid out on the hinge's bounds.
|
|
51
|
+
- A window with no such feature, or with a fold that obstructs nothing, is laid out by its width alone.
|
|
52
|
+
|
|
53
|
+
## Accessibility, tested
|
|
54
|
+
Rules (the `a11y` skill holds them for every stack): tap targets at least 48×48 on Android and 44×44 on iOS; a
|
|
55
|
+
label on every tappable; text contrast at least 4.5:1 (3:1 for large text, 18 pt and above); the UI legible and
|
|
56
|
+
usable at very large text and display scale.
|
|
57
|
+
|
|
58
|
+
The guideline test, in `flutter_test`:
|
|
59
|
+
|
|
60
|
+
```dart
|
|
61
|
+
testWidgets('meets the accessibility guidelines', (tester) async {
|
|
62
|
+
final SemanticsHandle handle = tester.ensureSemantics();
|
|
63
|
+
await tester.pumpWidget(const MyApp());
|
|
64
|
+
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
|
65
|
+
await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
|
|
66
|
+
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
|
67
|
+
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
|
68
|
+
handle.dispose();
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Run it for every new screen.
|
|
73
|
+
|
|
74
|
+
## Widths, tested
|
|
75
|
+
- Every new screen reached from the app's navigation has a widget test at **one width of each window class the app
|
|
76
|
+
supports**, for example 320 and 360 dp (compact, a small and a common phone), 700 dp (medium) and 1280 dp (large).
|
|
77
|
+
Set the size with `tester.view.physicalSize` and `devicePixelRatio`, and reset both afterwards.
|
|
78
|
+
- One test **crosses a class boundary**: select an item and type into a form at one width, change the width to
|
|
79
|
+
another class, and expect the same selection and the same input.
|
|
80
|
+
- The narrowest and the widest window alone are not enough: a layout can be right at both ends and wrong in a class
|
|
81
|
+
between them.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Isolates, platform channels, navigation, text and release (Flutter)
|
|
2
|
+
|
|
3
|
+
Checked against https://docs.flutter.dev/perf/isolates, https://docs.flutter.dev/platform-integration/platform-channels,
|
|
4
|
+
https://docs.flutter.dev/ui/navigation,
|
|
5
|
+
https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization,
|
|
6
|
+
https://docs.flutter.dev/testing/overview, https://docs.flutter.dev/testing/integration-tests,
|
|
7
|
+
https://api.flutter.dev/flutter/flutter_test/matchesGoldenFile.html, https://docs.flutter.dev/deployment/obfuscate,
|
|
8
|
+
https://dart.dev/tools/dart-format and https://dart.dev/tools/pub/private-files (2026-10-02).
|
|
9
|
+
|
|
10
|
+
## Isolates
|
|
11
|
+
- The one hard rule: use an isolate when a computation makes the UI jank — when it takes longer than the gap between
|
|
12
|
+
two frames. Typical: decoding or parsing a large file, image/audio/video work, a large local query.
|
|
13
|
+
- One-off work: `Isolate.run(() => …)` or `compute(fn, message)`. A long-lived worker: `Isolate.spawn` with a
|
|
14
|
+
`ReceivePort` / `SendPort` pair.
|
|
15
|
+
- A spawned isolate has no `rootBundle` and does no widget or UI work. Plugins can be used there through
|
|
16
|
+
`BackgroundIsolateBinaryMessenger.ensureInitialized(RootIsolateToken.instance!)` (sending works; unsolicited
|
|
17
|
+
messages from the host do not arrive).
|
|
18
|
+
- The web has no isolates. `compute` still compiles there and runs on the main thread.
|
|
19
|
+
|
|
20
|
+
## Platform channels
|
|
21
|
+
- `MethodChannel` (calls), `EventChannel` (streams), `BasicMessageChannel` (messages); all asynchronous, encoded by
|
|
22
|
+
`StandardMessageCodec` (booleans, numbers, strings, typed byte lists, lists, maps, null).
|
|
23
|
+
- The Dart side catches `PlatformException` (the platform returned an error) and `MissingPluginException` (no
|
|
24
|
+
implementation on this platform) and decides what the UI shows — the `frontend` rule of explicit error paths.
|
|
25
|
+
- The native handler runs on the platform's main thread (Android: the UI thread; iOS: the main thread).
|
|
26
|
+
- Prefer `pigeon`: the messages are generated and typed, so the two sides cannot drift on a method name.
|
|
27
|
+
- Render by capability: a feature a platform lacks is not offered there.
|
|
28
|
+
|
|
29
|
+
## Navigation
|
|
30
|
+
- `Navigator` push/pop for a simple stack. An app with deep links, the web, or several navigators uses a routing
|
|
31
|
+
package that parses the path (the project's own; `go_router` is the one Flutter's docs name).
|
|
32
|
+
- Named routes (`routes:` with `pushNamed`) are not recommended: their deep-link behaviour cannot be customised and
|
|
33
|
+
the browser's forward button does not work with them.
|
|
34
|
+
|
|
35
|
+
## Text and locales
|
|
36
|
+
- No user-facing text is written inline. An app in more than one language uses `gen-l10n` (below). An app in one
|
|
37
|
+
language may keep every text in one central file of constants instead: one place to read, review and later move
|
|
38
|
+
to ARB files.
|
|
39
|
+
- `String.toUpperCase()` "uses the language independent Unicode mapping and thus only works in some languages"
|
|
40
|
+
(api.dart.dev, 2026-10-07), and `toLowerCase()` likewise: under them Turkish `i` gives `I` and `I` gives `i`,
|
|
41
|
+
where the language has `İ` and `ı`. Text is cased by one helper of the project that takes the locale and handles
|
|
42
|
+
the letters its languages need; nothing calls the two methods on user-facing text directly.
|
|
43
|
+
- `FontFeature.tabularFigures()` (`TextStyle.fontFeatures`) asks the font for figures of one width, so amounts in a
|
|
44
|
+
column line up and a changing number does not shift the text beside it. It does nothing in a font without them.
|
|
45
|
+
- `flutter_localizations` and `intl` in `pubspec.yaml`, `generate: true` under `flutter:`, an `l10n.yaml` with
|
|
46
|
+
`arb-dir`, `template-arb-file`, `output-localization-file`. `flutter gen-l10n` (or `flutter pub get` / `run`)
|
|
47
|
+
generates `AppLocalizations`.
|
|
48
|
+
- Placeholders, plurals and selects are ICU messages in the ARB file (`{count, plural, =0{…} other{…}}`), never
|
|
49
|
+
strings glued together in Dart. `MaterialApp` gets `localizationsDelegates` and `supportedLocales`.
|
|
50
|
+
|
|
51
|
+
## Tests
|
|
52
|
+
- Unit (logic, dependencies faked), widget (one widget's look and interaction), integration (`integration_test/`, the
|
|
53
|
+
`integration_test` SDK package as a dev dependency, `IntegrationTestWidgetsFlutterBinding.ensureInitialized()`,
|
|
54
|
+
`flutter test integration_test`). Confidence and maintenance cost both rise from unit to integration; a well-tested
|
|
55
|
+
app has many unit and widget tests and enough integration tests for its important flows.
|
|
56
|
+
- Golden files: `matchesGoldenFile('goldens/x.png')`, refreshed with `flutter test --update-goldens`. The default
|
|
57
|
+
test font (Ahem) draws boxes; custom fonts render differently across platforms and Flutter versions, so goldens are
|
|
58
|
+
produced and compared on one platform, with fonts loaded first (`flutter_test_config.dart`).
|
|
59
|
+
- One place, in practice: a container image pinned by digest, with the Flutter version pinned, in CI. A golden that
|
|
60
|
+
changed is regenerated there (a CI job that uploads the new files, or the same image run by hand) and reviewed as
|
|
61
|
+
an image diff. `--update-goldens` on a developer's own machine produces files CI then fails.
|
|
62
|
+
|
|
63
|
+
## Release
|
|
64
|
+
- `flutter build <target> --obfuscate --split-debug-info=<dir>` on the targets that support it (Android, iOS, macOS,
|
|
65
|
+
Linux, Windows; not the web). Keep the symbols: `flutter symbolize -i <trace> -d <symbols>` reads a stack trace
|
|
66
|
+
back. Code that matches on type names (`runtimeType.toString()`) breaks under obfuscation.
|
|
67
|
+
- Obfuscation renames symbols; it does not encrypt and does not stop reverse engineering. **A secret does not ship in
|
|
68
|
+
the app**, whatever the build flags.
|
|
69
|
+
- An application commits `pubspec.lock` (a library package does not): transitive upgrades become visible changes.
|
|
70
|
+
- `dart format` rewrites files by default; in CI and in the DoD use `dart format --set-exit-if-changed .` (exit 1
|
|
71
|
+
when anything would change). The line width is the project's `formatter` setting in `analysis_options.yaml`.
|
|
72
|
+
|
|
73
|
+
## A feature closed by a build flag
|
|
74
|
+
A feature that must not reach the store yet (it waits for a review, a consent text, a legal check) is closed in code
|
|
75
|
+
by a compile-time flag: `const bool.fromEnvironment('NAME')`, set with `--dart-define=NAME=true`. The compiler drops
|
|
76
|
+
the closed branch. That keeps it out of an ordinary build; it does not keep it out of a store build that someone
|
|
77
|
+
runs with the flag. **The store build itself refuses the flag**, in the build system of each platform, before
|
|
78
|
+
anything is compiled:
|
|
79
|
+
|
|
80
|
+
- **Android (Gradle):** a task the release bundle depends on reads the project properties the Flutter Gradle plugin
|
|
81
|
+
is handed — `dart-defines` and `extra-front-end-options` — and fails the build when a closed flag is among them.
|
|
82
|
+
- **iOS (Xcode):** a Run Script build phase, placed first in the app target, reads the build settings `DART_DEFINES`
|
|
83
|
+
and `EXTRA_FRONT_END_OPTIONS` and fails a release archive the same way (a line that starts with `error:` shows
|
|
84
|
+
among Xcode's build errors).
|
|
85
|
+
|
|
86
|
+
The ways round a check that only looks for `NAME=true`, each of which the check closes:
|
|
87
|
+
- **The defines are encoded.** They arrive as one value: entries separated by commas, each the base64 of
|
|
88
|
+
`NAME=value`. The check decodes every entry; an entry it cannot decode fails the build.
|
|
89
|
+
- **Any value sets it.** The flag's name fails the build whatever follows the `=`, and so does the name anywhere
|
|
90
|
+
inside another entry.
|
|
91
|
+
- **The front end's own options.** A define can be passed to the Dart front end directly (`-DNAME=…`), and other
|
|
92
|
+
options replace what is compiled. A store build is given no front-end options at all; one that is, fails.
|
|
93
|
+
- **Only the store artifact is checked** — the release app bundle and the release archive. A debug or profile
|
|
94
|
+
build, and a release build for one's own device, may carry the flag.
|
|
95
|
+
- A test in the project pins the check: it runs it with the flag, with the flag encoded, with a front-end option,
|
|
96
|
+
and with none, and expects three failures and one pass.
|
|
97
|
+
|
|
98
|
+
Not from Flutter's documentation: the property and setting names above are what the Flutter tool hands to Gradle
|
|
99
|
+
and Xcode, read from a project that does this. They are not a documented contract, so the project's test also fails
|
|
100
|
+
when a Flutter upgrade renames them, instead of the check passing because it reads nothing.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Rebuilds and performance (Flutter)
|
|
2
|
+
|
|
3
|
+
Checked against https://docs.flutter.dev/perf/best-practices, https://docs.flutter.dev/testing/build-modes and
|
|
4
|
+
https://docs.flutter.dev/ui/adaptive-responsive/best-practices (2026-10-02).
|
|
5
|
+
|
|
6
|
+
## Where a rebuild stops
|
|
7
|
+
- `setState()` on a `State` rebuilds every descendant. The walk stops where it meets **the same child instance** as
|
|
8
|
+
the previous frame — which is what a `const` widget, or a child passed in from outside, gives it. So:
|
|
9
|
+
- put `setState` (or the narrowest provider/listenable read) on the smallest subtree that changes;
|
|
10
|
+
- pass a static subtree as `child` to `AnimatedBuilder` and friends, instead of building it inside the builder;
|
|
11
|
+
- make a reusable piece of UI a `StatelessWidget`: a helper method that returns widgets is rebuilt with its parent.
|
|
12
|
+
- `build()` can run on every ancestor rebuild. Nothing slow in it: no parsing, no I/O, no sorting of a large list.
|
|
13
|
+
- Do not override `operator ==` on a widget unless it is a leaf; it turns rebuild comparisons quadratic.
|
|
14
|
+
|
|
15
|
+
## Painting
|
|
16
|
+
- `saveLayer` allocates an off-screen buffer. Widgets that can trigger it: `ShaderMask`, `ColorFilter`, a `Chip`
|
|
17
|
+
with a non-opaque disabled colour, a `Text` with an overflow shader. Look for it with the "checkerboard off-screen
|
|
18
|
+
layers" option in DevTools before blaming anything else.
|
|
19
|
+
- `Opacity` only when needed: animate with `AnimatedOpacity`, fade images with `FadeInImage`, or paint with a
|
|
20
|
+
semi-transparent colour.
|
|
21
|
+
- Clipping is cheaper than `Opacity` (it does not call `saveLayer` unless `Clip.antiAliasWithSaveLayer` is asked for)
|
|
22
|
+
but still costs: prefer `borderRadius`, and clip before an animation rather than during it.
|
|
23
|
+
|
|
24
|
+
## Layout
|
|
25
|
+
- Long lists and grids: the lazy builders (`ListView.builder`, `GridView.builder`) build only what is visible.
|
|
26
|
+
A `Column` or a `ListView(children: …)` builds every child up front.
|
|
27
|
+
- Intrinsic passes (sizing every cell to the largest, for example) poll all children. Give cells a fixed size, or pick
|
|
28
|
+
an anchor cell. DevTools' "Track layouts" option labels them `'<type> intrinsics'`.
|
|
29
|
+
- Build strings in a loop with `StringBuffer`, not `+`.
|
|
30
|
+
|
|
31
|
+
## Measuring, not guessing
|
|
32
|
+
- Measure in **profile mode on a real device**: `flutter run --profile`. Debug mode is not indicative, and profile
|
|
33
|
+
mode does not run on an emulator or simulator.
|
|
34
|
+
- Budget: build and render run on separate threads, about 16 ms each per frame at 60 Hz; under 8 ms in total is the
|
|
35
|
+
target at 120 Hz. Going below the budget is still worth it: it saves battery and heat.
|
|
36
|
+
- A performance claim in a PR carries the profile-mode numbers it is based on (the `performance` skill).
|