@elalfner/relevo 0.5.0 → 0.6.0

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "relevo",
3
3
  "displayName": "Relevo",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "description": "An automatic issue-to-merge workflow where an agent builds and people verify.",
6
6
  "author": {
7
7
  "name": "Elias Alfaro"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elalfner/relevo",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Relevo: an automatic issue-to-merge workflow where an agent builds and people verify.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ // The worktree of an interactive `go` in Claude Code. Usage:
3
+ // node scripts/go-worktree.mjs create <issue> [--config relevo.yml] prints {"worktree","base"} as JSON
4
+ // node scripts/go-worktree.mjs remove <issue>
5
+ // exit 0: done 1: git failed 2: bad usage 3: the worktree of the issue already exists 4: already in a linked worktree
6
+ // The skill reads exit 3 as "stop" and exit 4 as "build where you are".
7
+
8
+ import { execFileSync } from 'node:child_process';
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { resolve } from 'node:path';
11
+ import { loadConfig } from './lib/config.mjs';
12
+ import { createWorktree, EXIT, removeWorktree } from './lib/go-worktree.mjs';
13
+
14
+ const args = process.argv.slice(2);
15
+ const [command, issue] = args;
16
+ const configAt = args.indexOf('--config');
17
+ const fail = (message, code = EXIT.usage) => {
18
+ console.error(message);
19
+ process.exit(code);
20
+ };
21
+
22
+ const runner = {
23
+ capture: (cmd, cmdArgs, options) => execFileSync(cmd, cmdArgs, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], cwd: options.cwd }),
24
+ };
25
+
26
+ let result;
27
+ if (command === 'create') {
28
+ const configPath = resolve(configAt === -1 ? 'relevo.yml' : args[configAt + 1]);
29
+ if (!existsSync(configPath)) fail(`relevo.yml not found at ${configPath}. Run /relevo:init to create it.`);
30
+ const loaded = loadConfig(readFileSync(configPath, 'utf8'));
31
+ if (!loaded.ok) fail(loaded.errors.map((e) => `error: ${e.message}`).join('\n'));
32
+ result = createWorktree({ issue, config: loaded.config, runner, exists: existsSync });
33
+ if (result.status === EXIT.ok) console.log(JSON.stringify({ worktree: result.worktree, base: result.base }));
34
+ } else if (command === 'remove') {
35
+ result = removeWorktree({ issue, runner, exists: existsSync });
36
+ } else {
37
+ fail('usage: go-worktree.mjs create|remove <issue>');
38
+ }
39
+ result.messages.forEach((m) => console.error(m));
40
+ process.exit(result.status);
@@ -13,7 +13,7 @@ import { loadConfig } from './config.mjs';
13
13
  import { labelDefinitions } from './labels.mjs';
14
14
  import { credentialMode, cloudBuilds, CLOUD_VARIABLE } from './execution-mode.mjs';
15
15
  import { SKILL_DIRS, SIZE_BUDGET_BYTES, folderHash, lockDifferences, resolveRoleSkills, skillBudget } from './role-skills.mjs';
16
- import { ROLES, profileLocations, profileLookups, resolveProfile } from './stack-profile.mjs';
16
+ import { ROLES, profileLocations, profileLookups, repositoryProfilePath, resolveProfile } from './stack-profile.mjs';
17
17
 
18
18
  export const REQUIRED_SKILLS = ['openspec-explore', 'grill-with-docs', 'grilling', 'domain-modeling'];
19
19
  export const SKILL_INSTALL = {
@@ -338,6 +338,14 @@ function gatherPlatforms(f, p, tryRun) {
338
338
  const tool = resolved.profile.targets[target] ?? '';
339
339
  return { target, tool, ok: tool === '' ? true : available(tool) };
340
340
  });
341
+ // The cloud and a fresh clone read the repository's layer from git, not from this working copy. `null` means
342
+ // git could not say (no git, or not a repository).
343
+ if (resolved.profile.layers.includes('repository')) {
344
+ const path = repositoryProfilePath(platform.stack);
345
+ if (!f.git) entry.repositoryLayerTracked = null;
346
+ else if (tryRun(() => { p.tool('git', ['ls-files', '--error-unmatch', path]); return true; }, false)) entry.repositoryLayerTracked = true;
347
+ else entry.repositoryLayerTracked = tryRun(() => { p.tool('git', ['rev-parse', '--is-inside-work-tree']); return true; }, false) ? false : null;
348
+ }
341
349
  }
342
350
  f.platformFacts.push(entry);
343
351
  });
@@ -474,6 +482,11 @@ function platformChecks(f) {
474
482
  }
475
483
  const layers = e.profile.layers.map((l) => (l === 'plugin' ? 'default of the plugin' : `.relevo/stacks/${e.stack}.md`)).join(' + ');
476
484
  r.push(pass(`platform.${e.name}.profile`, `platform ${e.name} (${e.stack}, ${e.path}): profile = ${layers}`));
485
+ if (e.repositoryLayerTracked === false) {
486
+ const path = `.relevo/stacks/${e.stack}.md`;
487
+ const effect = e.profile.layers.includes('plugin') ? `use only the plugin's profile for ${e.stack}, without your rules` : `have no profile for ${e.stack}`;
488
+ r.push(warn(`platform.${e.name}.profile.tracked`, `platform ${e.name}: ${path} is not tracked by git, so the cloud run and a fresh clone ${effect}`, `commit ${path} and push it with the rest of the repository's Relevo files`));
489
+ }
477
490
  (e.profile.ignored ?? []).forEach((x) => {
478
491
  r.push(warn(`platform.${e.name}.ignored.${x.key}`, `platform ${e.name}: the repository profile sets ${x.key} = "${x.value}", which is ignored (the plugin keeps "${x.kept}")`, `a command is replaced with platforms.${e.name}.checks in relevo.yml, not in the profile`));
479
492
  });
@@ -6,12 +6,13 @@
6
6
  // stream(cmd, args, { cwd, env }) -> exit status (the output goes to the terminal); throws with code ENOENT when missing
7
7
 
8
8
  import { tmpdir } from 'node:os';
9
- import { basename, dirname, join } from 'node:path';
9
+ import { basename, join } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { cloudBuilds } from './execution-mode.mjs';
12
12
  import { runFlags } from './run-flags.mjs';
13
+ import { WORKTREES_FOLDER, worktreePath } from './worktree-path.mjs';
13
14
 
14
- export const WORKTREES_FOLDER = '.relevo-worktrees';
15
+ export { WORKTREES_FOLDER };
15
16
  // The guard is part of the plugin, not of the project that is built, so it is found next to this file.
16
17
  const GUARD = fileURLToPath(new URL('../agent-guard.mjs', import.meta.url));
17
18
  // The same time limit as the cloud step of the Antigravity agent.
@@ -20,7 +21,7 @@ export const GUARD_TIMEOUT_MINUTES = 50;
20
21
  /** What a local run will do, without doing it: the worktree and the `claude` or `agy` command. Pure. */
21
22
  export function planLocalRun({ issue, repoRoot, config }) {
22
23
  const base = config.branches.base;
23
- const worktree = join(dirname(repoRoot), WORKTREES_FOLDER, `${basename(repoRoot)}-${issue}`);
24
+ const worktree = worktreePath(repoRoot, issue);
24
25
  const isAntigravity = config.agent?.build === 'antigravity';
25
26
  const command = isAntigravity
26
27
  ? ['node', [
@@ -0,0 +1,85 @@
1
+ // The worktree of an interactive `go` in Claude Code. Implements "An interactive Claude Code run builds in its own
2
+ // worktree" of the execution-modes specification: the skill makes the worktree here, enters it with EnterWorktree, and
3
+ // asks for its removal here when the run ends cleanly.
4
+ //
5
+ // Every process goes through an injected runner so the logic is tested without git or a network:
6
+ // capture(cmd, args, { cwd }) -> stdout (throws when the command fails)
7
+
8
+ import { worktreePath } from './worktree-path.mjs';
9
+
10
+ /** Exit codes of `go-worktree.mjs`. `exists` and `linked` are answers, not failures: the skill reads them. */
11
+ export const EXIT = { ok: 0, failed: 1, usage: 2, exists: 3, linked: 4 };
12
+
13
+ const fail = (status, message) => ({ status, worktree: null, messages: [message] });
14
+ // git prints progress ("Preparing worktree ...") before the reason, so the reason is the `fatal:` or `error:` line, or the last one.
15
+ function firstLine(err) {
16
+ const lines = String(err.stderr ?? err.message).trim().split('\n').map((l) => l.trim()).filter(Boolean);
17
+ return lines.find((l) => /^(fatal|error):/.test(l)) ?? lines.at(-1) ?? 'git failed';
18
+ }
19
+
20
+ function context(issue, runner) {
21
+ if (!/^\d+$/.test(String(issue))) return { early: fail(EXIT.usage, 'give the issue number: go-worktree.mjs create|remove <issue>') };
22
+ try {
23
+ const repoRoot = runner.capture('git', ['rev-parse', '--show-toplevel'], {}).trim();
24
+ const [gitDir, commonDir] = runner
25
+ .capture('git', ['rev-parse', '--path-format=absolute', '--git-dir', '--git-common-dir'], { cwd: repoRoot })
26
+ .trim()
27
+ .split('\n');
28
+ return { repoRoot, linked: gitDir !== commonDir };
29
+ } catch {
30
+ return { early: fail(EXIT.usage, 'not inside a git repository (or git is older than 2.31)') };
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Make the worktree of an issue from `origin/<base>`. `linked` when the session is already in a linked worktree (the
36
+ * build runs where it is), `exists` when the worktree of the issue is already there (the same issue twice).
37
+ */
38
+ export function createWorktree({ issue, config, runner, exists = () => false }) {
39
+ const ctx = context(issue, runner);
40
+ if (ctx.early) return ctx.early;
41
+ if (ctx.linked) {
42
+ return fail(EXIT.linked, 'this session is already in a linked worktree: build here, no worktree is made');
43
+ }
44
+ const base = config.branches.base;
45
+ const worktree = worktreePath(ctx.repoRoot, issue);
46
+ if (exists(worktree)) {
47
+ return {
48
+ status: EXIT.exists,
49
+ worktree,
50
+ messages: [`${worktree} already exists: another run is building #${issue}, or a failed one left it. Finish it or remove it first (git worktree remove --force "${worktree}")`],
51
+ };
52
+ }
53
+ try {
54
+ // A folder deleted by hand leaves a registration that makes `worktree add` fail; prune only drops those.
55
+ runner.capture('git', ['worktree', 'prune'], { cwd: ctx.repoRoot });
56
+ runner.capture('git', ['fetch', 'origin', base], { cwd: ctx.repoRoot });
57
+ runner.capture('git', ['worktree', 'add', '--detach', worktree, `origin/${base}`], { cwd: ctx.repoRoot });
58
+ } catch (err) {
59
+ return fail(EXIT.failed, `could not prepare the worktree: ${firstLine(err)}`);
60
+ }
61
+ return { status: EXIT.ok, worktree, base, messages: [] };
62
+ }
63
+
64
+ /** Remove the worktree of an issue, after a clean end. Run it from the main working tree, not from inside. */
65
+ export function removeWorktree({ issue, runner, exists = () => true }) {
66
+ const ctx = context(issue, runner);
67
+ if (ctx.early) return ctx.early;
68
+ if (ctx.linked) return fail(EXIT.linked, 'leave the worktree first (ExitWorktree with keep), then remove it from the main working tree');
69
+ const worktree = worktreePath(ctx.repoRoot, issue);
70
+ if (!exists(worktree)) {
71
+ // The folder may be gone while git still lists it: drop that registration so the issue can be built again.
72
+ try {
73
+ runner.capture('git', ['worktree', 'prune'], { cwd: ctx.repoRoot });
74
+ } catch (err) {
75
+ return { status: EXIT.failed, worktree, messages: [`could not prune the worktrees: ${firstLine(err)}; run git worktree prune`] };
76
+ }
77
+ return { status: EXIT.ok, worktree, messages: [`${worktree} is not there: nothing to remove`] };
78
+ }
79
+ try {
80
+ runner.capture('git', ['worktree', 'remove', '--force', worktree], { cwd: ctx.repoRoot });
81
+ } catch (err) {
82
+ return { status: EXIT.failed, worktree, messages: [`could not remove ${worktree}: ${firstLine(err)}; run git worktree remove --force "${worktree}"`] };
83
+ }
84
+ return { status: EXIT.ok, worktree, messages: [] };
85
+ }
@@ -208,11 +208,11 @@ export function planInit({ facts, answers, templates, current, admin }) {
208
208
  if (facts.configText !== null) items.push(item('config', facts.config ? 'ok' : 'differs', facts.config ? 'relevo.yml exists and is valid' : `relevo.yml exists but is invalid: ${facts.configErrors.join('; ')}`));
209
209
  else items.push(item('config', 'todo', 'Create relevo.yml from the answers', { content: renderConfig(templates.config, { base, release, ...answers }) }));
210
210
 
211
- // optional stack profiles of the repository (created only when asked for, and never committed by init)
211
+ // optional stack profiles of the repository (created only when asked for; init never commits them, the person does)
212
212
  (answers.stackProfiles ?? []).forEach((stack) => {
213
213
  const path = `.relevo/stacks/${stack}.md`;
214
214
  if ((current.stackProfiles ?? {})[stack]) items.push(item(`stack-profile.${stack}`, 'ok', `${path} exists`));
215
- else items.push(item(`stack-profile.${stack}`, 'todo', `Create ${path}, a commented profile that adds to the plugin's (not committed)`, { path, content: stackProfileTemplate(stack) }));
215
+ else items.push(item(`stack-profile.${stack}`, 'todo', `Create ${path}, a commented profile that adds to the plugin's (left for you to commit)`, { path, content: stackProfileTemplate(stack) }));
216
216
  });
217
217
 
218
218
  // labels
@@ -0,0 +1,11 @@
1
+ // Where the worktree of an issue lives: next to the repository, never inside it. Shared by `go-local` and
2
+ // `go-worktree` so that the two cannot name it differently.
3
+
4
+ import { basename, dirname, join } from 'node:path';
5
+
6
+ export const WORKTREES_FOLDER = '.relevo-worktrees';
7
+
8
+ /** The worktree of an issue: `<parent of the repository>/.relevo-worktrees/<repository>-<issue>`. Pure. */
9
+ export function worktreePath(repoRoot, issue) {
10
+ return join(dirname(repoRoot), WORKTREES_FOLDER, `${basename(repoRoot)}-${issue}`);
11
+ }
@@ -27,10 +27,22 @@ Launch `relevo:dev` and `relevo:qa` in the foreground and wait for each report b
27
27
 
28
28
  ## Where it runs
29
29
 
30
- In the cloud the workflow starts this skill with the decision already made. A person can also start it on their machine: `node ${CLAUDE_PLUGIN_ROOT}/scripts/go-local.mjs <issue>` runs it in a separate git worktree, with the same limits and tool lists as the workflow, and removes the worktree when it ends cleanly. Started by hand without that script, it needs a clean working tree (the preflight below stops otherwise).
30
+ In the cloud the workflow starts this skill with the decision already made. A person can also start it on their machine: `node ${CLAUDE_PLUGIN_ROOT}/scripts/go-local.mjs <issue>` runs it in a separate git worktree, with the same limits and tool lists as the workflow, and removes the worktree when it ends cleanly.
31
31
 
32
32
  **When `$ARGUMENTS` has `--local`**, do not build in this session: run `node ${CLAUDE_PLUGIN_ROOT}/scripts/go-local.mjs <issue>`, show what it prints, and stop. The build happens in the process that script starts, in its own worktree and with the closed tool lists, which a plain session does not have.
33
33
 
34
+ **In an interactive session** (started by hand without `--decision` and without `--local`), do not build in the folder you were started in: another session may be building another issue there, and the person may have changes of their own. Before step 0, make a worktree for the issue:
35
+
36
+ ```bash
37
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/go-worktree.mjs create <issue>
38
+ ```
39
+
40
+ - Exit 0: it prints `{"worktree": "<path>", "base": "<base>"}`. Enter it with `EnterWorktree` and its `path` (never its `name`, which would branch from the default branch and not from the base of the project). From then on every step, command and agent works in that folder.
41
+ - Exit 4: the session is already in a linked worktree (a run of `go-local.mjs`, or one the person made). Build where you are, make no worktree, and skip the exit below.
42
+ - Any other exit (for example 3, the worktree of this issue already exists because another run is building it or a failed one left it): show its message and stop without changing anything.
43
+
44
+ At the end of the run, before the final report, leave the worktree: call `ExitWorktree` with `keep`. Then, from the folder you came from, remove it with `node ${CLAUDE_PLUGIN_ROOT}/scripts/go-worktree.mjs remove <issue>` when the outcome is `built` or a gate decision that stopped before step 2 (nothing was built, so nothing is lost). After the failure path, or if the run stopped for any other reason, leave the worktree on disk and say its path in the report.
45
+
34
46
  ## OpenSpec commands
35
47
 
36
48
  Use these OpenSpec commands and no others:
@@ -36,7 +36,7 @@ If `relevo.yml` already exists and is valid, skip this step. Otherwise ask, in s
36
36
  - the platforms: for each one a name, its stack (look at the project to propose one: the profiles in `${CLAUDE_PLUGIN_ROOT}/stacks/` are the stacks Relevo ships with: `flutter`, `vue`, `laravel` and `supabase`), the folder where it lives (`.` for a repository with one platform, `apps/web` in a monorepo) and, for a stack that has targets, which ones (for Flutter: `android`, `ios`, `web`; for Vue: `web`; Laravel and Supabase have none). Several platforms in one repository are written down, but `go` builds only a repository with one for now;
37
37
  - where `go` builds: in the cloud, on each person's machine, or both (the default), and, for the cloud, with an API key from the Anthropic Console (the default) or with a subscription token. A subscription's quota is shared by every run and by the interactive use of the account, so `init` sets `limits.concurrent_agents` to 1 for it. Nothing about this goes in `relevo.yml` apart from that number: the installed workflows, the secret that exists and the repository variable `RELEVO_CLOUD_GO` (`off` pauses the cloud at once, with no pull request) decide it;
38
38
  - the repositories this one depends on, if any (`owner/repo`, for example the back end of a front end): they are written to `links`, the closed list that `Depends-on: owner/repo#N` is checked against;
39
- - whether to create a commented `.relevo/stacks/<stack>.md` for each stack, where the project can add its own hygiene to the plugin's profile (optional; it is not committed);
39
+ - whether to create a commented `.relevo/stacks/<stack>.md` for each stack, where the project can add its own hygiene to the plugin's profile (optional; `init` creates it but does not commit it: it is left for the person to commit, because the cloud and teammates read it from git);
40
40
  - which agent builds in the cloud, only when the person says they use Antigravity: `claude` (the default) or `antigravity`. With `antigravity` the model key is `GEMINI_API_KEY`, a key made only for Relevo in its own Google Cloud project with a quota, and the limit asked below is a number of tokens (`limits.run_max_tokens`) because that agent has no cost limit; `docs/security.md` says what else it cannot enforce;
41
41
  - the cost limit per unattended run in US dollars: the template's 5 is a placeholder the person must accept or change (with `antigravity`, the token limit instead).
42
42
 
@@ -72,4 +72,4 @@ Run `node ${CLAUDE_PLUGIN_ROOT}/scripts/doctor.mjs` and show its output.
72
72
 
73
73
  ## Report
74
74
 
75
- Finish with: what was created, what was left alone, what the person must still do by hand (always the model credential in the repository secrets, with all whitespace removed from a subscription token, and committing and pushing the new files to the default branch, because a workflow only runs from there), and the doctor result.
75
+ Finish with: what was created, what was left alone, what the person must still do by hand (always the model credential in the repository secrets, with all whitespace removed from a subscription token, and committing and pushing the new files to the default branch, because a workflow only runs from there; name each `.relevo/stacks/<stack>.md` it created among them), and the doctor result.
package/stacks/flutter.md CHANGED
@@ -4,7 +4,7 @@ commands:
4
4
  analyze: flutter analyze --no-pub
5
5
  test: flutter test --reporter=compact --no-pub
6
6
  format: dart format
7
- generated: ['*.g.dart', '*.freezed.dart']
7
+ generated: ['*.g.dart', '*.freezed.dart', '*.mocks.dart']
8
8
  tools: [flutter]
9
9
  requires_skills: []
10
10
  skills:
@@ -22,29 +22,67 @@ targets:
22
22
 
23
23
  ## Verification
24
24
 
25
- Prefer the Dart tooling server's tools when they are available in this session (analysis, running tests, formatting). Otherwise use the commands of this profile: `analyze`, `test` and `format`.
25
+ Run the commands of this profile from the folder of the platform: `analyze`, `test` and `format`. Run `format` on the files you changed, never on generated files.
26
+
27
+ When the Dart and Flutter MCP server is connected, you may use the tools that write nothing: `analyze_files` for analysis, `lsp` and `read_package_uris` to check an API or a symbol instead of guessing it, and `run_tests` when the server enables it (it is off by default). When a tool is missing, use the command of this profile. No agent uses `dart_format`, `dart_fix`, `pub`, `hot_reload`, `hot_restart` or `launch_app` during a run: they write files or drive an app, and `dart_format` formats the whole project, generated files included.
28
+
29
+ When annotated models or mocks changed, run `dart run build_runner build --delete-conflicting-outputs` before the tests.
26
30
 
27
31
  Do not claim the code compiles, analyses clean or passes tests unless you ran the tools and saw it. If neither the tooling server nor Flutter is available, say so plainly in your report.
28
32
 
33
+ ## Architecture
34
+
35
+ Follow the architecture that the project's rules declare (MVVM, bloc, riverpod or another). When they declare none, write new code with the structure of Flutter's architecture guide: UI and data in separate layers; data reached through repositories, which call stateless services that wrap each API or plugin; dependencies passed by constructor and kept private; no data logic in widgets. Command and Result objects, and any state-management package, are not required by this profile; use them when the project's rules do.
36
+
29
37
  ## Hygiene for the developer
30
38
 
39
+ Lifecycle and state:
40
+
31
41
  - Check `mounted` before using a `BuildContext` or calling `setState` after an `await`.
32
- - Dispose every controller, stream subscription and listener you create.
42
+ - Dispose every controller, stream subscription and listener you create. A `ChangeNotifierProvider(create:)` disposes its model itself.
43
+ - Never call an API, create a future or show a dialog inside `build()`; start them in `initState` or in an event handler.
33
44
  - Avoid the null-assertion operator unless a prior check makes it provably safe.
34
45
  - Give every screen loading, empty and error states.
35
46
  - Prevent double submission of actions.
36
- - Keep layouts from overflowing on small screens.
47
+
48
+ Layout:
49
+
50
+ - Keep layouts from overflowing on small screens: wrap a long `Text` or other unconstrained child of a `Row` or `Column` in `Expanded` or `Flexible`.
51
+ - Give a `ListView` or `GridView` inside a `Column` a bound (`Expanded` or a fixed height), and a `TextField` inside a `Row` a width (`Expanded` or `SizedBox`).
52
+ - Put `Expanded` and `Flexible` directly in a `Row`, `Column` or `Flex`, and `Positioned` directly in a `Stack`.
53
+ - Give each scroll view its own `ScrollController`.
54
+
55
+ Performance:
56
+
57
+ - Build long lists and grids with the builder constructors, not with a list of children.
58
+ - Keep `build()` cheap, and call `setState` as low in the tree as the change allows.
59
+ - Split large widgets into smaller widget classes rather than helper methods that return widgets.
60
+ - Keep `Opacity` and clipping out of animations; use `AnimatedOpacity` or a semi-transparent colour.
61
+ - Move heavy work, such as decoding a large JSON response, off the UI isolate with `Isolate.run`. On the web it still runs on the main thread.
62
+
63
+ Web:
64
+
65
+ - Use `package:web` and `dart:js_interop`; `dart:html` and `package:js` prevent the Wasm build.
66
+ - Do not use `dart:io` or `Platform` in code that runs on the web; check `kIsWeb` or use a conditional import.
67
+
68
+ Tests:
69
+
37
70
  - Write unit and widget tests for the scenarios the platform allows. A scenario that can only be seen on a device or in another app is covered by correct code, and is verified later in the on-device review.
71
+ - Make fakes of repositories and services for tests; a mock is also acceptable. A plugin throws `MissingPluginException` in unit and widget tests, so put it behind the project's own class and fake that class.
38
72
 
39
73
  ## What the auditor looks for in the diff
40
74
 
41
- - **Async and lifecycle**: a `BuildContext` use or `setState` after an `await` without a `mounted` check.
75
+ - **Async and lifecycle**: a `BuildContext` use or `setState` after an `await` without a `mounted` check; a network call, a new future, `setState` or `showDialog` inside `build()`.
42
76
  - **Leaks**: a controller, stream subscription or listener created and never disposed.
43
77
  - **Null safety**: an unchecked null-assertion operator that can throw at runtime.
44
- - **Layout**: unconstrained content that can overflow on a small screen.
78
+ - **Layout**: unconstrained content that can overflow on a small screen; a `ListView` or `GridView` in a `Column` without a bound; a `TextField` in a `Row` without a width; `Expanded`, `Flexible` or `Positioned` outside a compatible parent; one `ScrollController` attached to several scroll views.
45
79
  - **States**: missing loading, empty or error states; an action that can be triggered twice.
80
+ - **Performance**: a long list built from a list of children; costly work in `build()`; `setState` high in the tree for a local change; `Opacity` or clipping inside an animation; a large JSON decode on the UI isolate.
81
+ - **Web**: an import of `dart:html` or `package:js`; `dart:io` or `Platform` in code that runs on the web.
82
+ - **Tests**: a test that calls a real plugin, network or service instead of a fake or a mock.
83
+ - **Architecture**: report only what breaks a rule the project wrote down. Whatever the project declares, an HTTP or GraphQL client called from a widget is a finding.
46
84
  - **Android specifics**: an external-app launch with no handling for the app being missing, permission use without handling denial, behaviour on the back button.
47
- - **Generated code**: leave generated files (`*.g.dart`, `*.freezed.dart`) out of the review.
85
+ - **Generated code**: leave generated files (`*.g.dart`, `*.freezed.dart`, `*.mocks.dart`) out of the review.
48
86
 
49
87
  ## Targets
50
88
 
@@ -9,7 +9,7 @@ name: relevo-digest
9
9
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
10
10
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
11
11
  env:
12
- RELEVO_VERSION: '0.5.0'
12
+ RELEVO_VERSION: '0.6.0'
13
13
 
14
14
  on:
15
15
  schedule:
@@ -15,7 +15,7 @@ name: relevo-go
15
15
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
16
16
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
17
17
  env:
18
- RELEVO_VERSION: '0.5.0'
18
+ RELEVO_VERSION: '0.6.0'
19
19
 
20
20
  on:
21
21
  issues:
@@ -10,7 +10,7 @@ name: relevo-queue
10
10
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
11
11
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
12
12
  env:
13
- RELEVO_VERSION: '0.5.0'
13
+ RELEVO_VERSION: '0.6.0'
14
14
 
15
15
  on:
16
16
  issues:
@@ -10,7 +10,7 @@ name: relevo-review
10
10
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
11
11
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
12
12
  env:
13
- RELEVO_VERSION: '0.5.0'
13
+ RELEVO_VERSION: '0.6.0'
14
14
 
15
15
  on:
16
16
  pull_request_review:
@@ -8,7 +8,7 @@ name: relevo-sla
8
8
  # The version of @elalfner/relevo whose scripts this workflow runs. The plugin's templates carry the version they
9
9
  # were released with, and `/relevo:init` reports this file as different when it is older; change it by hand to update.
10
10
  env:
11
- RELEVO_VERSION: '0.5.0'
11
+ RELEVO_VERSION: '0.6.0'
12
12
 
13
13
  on:
14
14
  schedule: