Skip to content

Make the boilerplate resolve the packages it claims, and rename in one command - #13

Open
anilcancakir wants to merge 8 commits into
mainfrom
feature/fork-ready-boilerplate
Open

Make the boilerplate resolve the packages it claims, and rename in one command#13
anilcancakir wants to merge 8 commits into
mainfrom
feature/fork-ready-boilerplate

Conversation

@anilcancakir

Copy link
Copy Markdown
Contributor

What was wrong

A fork of this repository was already resolving the wrong packages, silently.

pubspec.yaml pinned magic_notifications: ^0.0.2, which in Dart's caret semantics means
>=0.0.2 <0.1.0. magic_starter alpha.26 requires ^0.2.0. The two ranges do not intersect,
so pub did not fail: it walked the starter back to alpha.24 and exited 0. Every fork got a
build that resolved, ran, and was two releases stale, and nothing in CI could see it.

Most of the rest follows from looking closely at that:

  • alpha.24 introduced 20 magic_starter.titles.* translation keys this app never shipped,
    while route_service_provider.dart:29 registers the routes that need them unconditionally.
    Those titles have been rendering as raw key paths.
  • LocalizationServiceProvider and NotificationServiceProvider were both registered with no
    config file and no factory in main.dart.
  • supported_locales claimed ['en', 'tr'] while assets/lang/ held only en.json.
  • .claude/agents/component-visual-reviewer.md pointed three commands at
    <workspace>/lib/ui/components, missing this repo's own directory segment. That path does
    not exist, so every find and grep in its procedure returned nothing and reported a clean
    review.

What this changes

The graph a fork gets, and proof of it. Nine hosted constraints bumped, with
magic_starter and magic_notifications moved as one edit because a partial bump does not
solve. A new published CI job resolves hosted-only and asserts the resolved version out of
the lock, because pub get exits 0 on a downgrade and only the lock knows which version won.
pubspec.lock is now tracked as the hosted resolution, gated by run_lockfile in bin/check
plus a CI check that runs before pub get can rewrite the evidence.

app:rename. The fork checklist was nine manual steps that never mentioned macOS, Windows,
Linux, web, Info.plist, main.dart, dependabot.yml, or the Kotlin package directory move.
One command now rewrites 24 sites plus that directory move, derives the current identity from
the tree so a second run is a no-op, and refuses on a dirty worktree only when it is actually
writing. --dry-run is proven inert by hashing all 158 files in scope before and after.

Two documented patterns that had no instance. A controller implementing
SessionScopedController, so the SessionScopeSync.attach() call that has always run finally
has something to reset, and a status token family in the supplement shape DESIGN.md
describes.

file_picker is no longer pinned here. Nothing in this app imports it; it arrives through
magic's Pick facade. The pin was a second opinion on a transitive dependency, and when
magic's master moved to ^12.2.0 ahead of a release the two ranges stopped intersecting, so
local resolution failed outright and bin/check could not run at all. It now resolves from
whichever magic is in play: 12.2.0 locally, 11.0.3 hosted, which is what a fork was already
getting.

Verification

bin/check green, eight jobs. CHECK_ALLOW_HOSTED=1 bin/check green too, so the suite passes
against the packages a fork actually downloads and not just the local checkouts. app:rename
was run for real in a throwaway worktree: pub get left it clean, the rename rewrote 24 files
and moved the Kotlin package, flutter analyze came back clean against the hosted graph, and a
second identical run reported unchanged (24) with an empty diff. Tests went from 10 to 26.

Two gates were negative-controlled rather than assumed: the lockfile gate fires on a genuinely
path-poisoned lock with every other job green, and the hardened design-tokens scanner catches
a raw colour literal that the previous version passed clean.

Not verified

The dusk-driven pass at 1440px and 390px did not run. bin/fsa deadlocks on its own lock: it
acquires it, installs a trap ... EXIT to release it, then execs into the compiled binary at
bin/fsa:110, and exec replaces the shell so the trap never fires. Short commands self-heal
when the process exits; the long-running mcp:serve this project spawns never releases. The
dispatcher route works but repoints a shared ~/.artisan/state.json that other live sessions
were holding.

Static cover was built instead: every magic_starter.titles.* key the starter's source
references was compared against what this app ships, an exact 20-to-20 match, and a new
locale_parity_test.dart holds en and tr to identical key sets. That covers the cause of a
raw route title, not a widget-level key or a layout break at 390px.

The bin/fsa lock bug is not fixed here, since no part of this change owns that script, but
every fork inherits it.

magic_starter alpha.26 needs magic_notifications ^0.2.0, and this app pinned
^0.0.2, which means >=0.0.2 <0.1.0. The two do not intersect, so pub quietly
walked the starter back to alpha.24 instead of failing: a fork got a build that
resolved, ran, and was two releases stale. The two constraints move together or
not at all.

alpha.24 also introduced 20 magic_starter.titles.* keys that en.json never
gained, and route_service_provider.dart:29 registers the notification routes
ungated, so those titles have been rendering as raw keys.

The two generated files come along because they are functions of the resolved
graph rather than edits: flutter pub get performs a one-time SDK migration on
analysis_options.yaml, and the macOS registrant gains purchases_flutter now that
magic_payments arrives through the starter. The registrant records the LOCAL
graph, which is what keeps the working tree clean. Unlike pubspec.lock, no fork
ever consumes this file: any build regenerates it from whatever that fork
resolves, so the committed value is transient for them and permanent dirt here
would be the only lasting cost.
config/app.dart registers LocalizationServiceProvider and
NotificationServiceProvider, but neither had a config file and neither appeared
in main.dart's factory list, so both fell through to package defaults. The
notifications feature flag was off besides, which meant a dependency this app
pays for shipped undemonstrated.

supported_locales already claimed en and tr while assets/lang held only en.json.
That claim is now true. The parity between the two files is exact rather than
best effort because magic's Translator replaces its sentence map on load instead
of merging it with the fallback: a key missing from tr.json renders the raw key
path on screen, not the English string. locale_parity_test.dart is what keeps
that from drifting back.

auto_detect_locale stays off. A device set to a locale this boilerplate does not
ship has no tested path here, and the replace-not-merge behaviour above is
exactly what turns that into raw keys on every screen. A fork flips it once its
supported_locales covers the locales its users actually have.
Four independent things, all of which were wrong or missing today.

CI gains a published job. Resolving is not the check: pub exits 0 on a
downgrade as long as SOME version in range resolves, which is exactly how the
alpha.24 walk-back stayed green. The job reads the resolved version out of the
lock and asserts it, verified against a synthetic alpha.24 lock to confirm it
fails on the input it exists to catch.

The component-visual-reviewer agent pointed at /Users/anilcan/Code/fluttersdk/
lib/ui/components, missing this repo's own directory segment. That path does not
exist, so every find and grep in its procedure returned nothing and reported a
clean review. It was also the only absolute path in a repository whose whole
purpose is to be cloned somewhere else.

design.md described components as living in the magic_starter generic library on
one line and scoped the WindRecipe import rule to magic_starter files on another.
Both meant this app's own lib/ui/components/. The import rule itself was right
and is unchanged.

design-tokens rescanned raw on one trigger, an unterminated block comment at end
of file. That catches the OPEN case, which flutter analyze already makes
impossible in real Dart. The CLOSED case is the live one: a /* inside a
triple-quoted string that some later ordinary */ closes leaves the scanner
balanced at EOF, so nothing fires and the region between them is dropped in
silence. Measured against the pre-edit script, a raw literal placed that way
passed clean.
The lock was gitignored because one generated with pubspec_overrides.yaml
active encodes sibling paths that exist on one machine. That diagnosis was
right and the conclusion was not: the fix for a lock generated the wrong way is
to generate it the right way. Untracked, a fork's first build resolves whatever
the caret ranges happen to solve to on the day it clones, which is not what CI
proved green.

run_lockfile is what makes tracking safe. It reads the STAGED lock, not the
working tree, and fails on a source: path entry. That split is deliberate and
permanent: a local pub get puts sibling paths back in the lock every time, so
the working copy stays dirty and unstaged while the index holds the hosted
resolution. require_local_siblings reads the opposite state (the overrides file
must be PRESENT), and the two cannot wedge because regenerating hosted-only
moves that file aside and back rather than removing it.

Verified by staging the real local-path lock, 11 path entries, and confirming
the job fails with every other job green. An earlier probe that appended a
malformed line proved nothing: pub refused to parse it and the run aborted
before the gate executed.
app:rename rewrites 24 sites plus the Kotlin package directory: the Dart
package name and its import sites, the Android namespace and applicationId, the
iOS and macOS bundle identifiers, the three desktop build systems, the web
manifest and title, .env, lib/main.dart, DESIGN.md and dependabot.yml. It
derives the CURRENT identity from the tree rather than hardcoding it, which is
what makes a second run a no-op. --dry-run is proven inert by hashing every file
in scope before and after, not by reading git status, which cannot tell a
sibling's staged file from a write.

Rewrite rules scan for their anchor instead of visiting recorded line numbers.
That is why the command handles the sixth PRODUCT_BUNDLE_IDENTIFIER line in the
iOS project file, which the research inventory had missed.

Two things the repo documented and never demonstrated now have exactly one
instance each: a controller implementing SessionScopedController, so the
SessionScopeSync.attach() call that has always run finally has something to
reset, and a status token family in the supplement shape DESIGN.md describes.

The tracked macOS plugin registrant switches to the hosted variant. A fork
clones it, runs pub get against the published graph, and gets no diff, so
app:rename does not refuse on its first run. The local variant is what made it
refuse. The tracked lock already follows this rule for the same reason: what is
committed is the graph a fork resolves, not the one this machine does.
…aths

The status tokens asserted WCAG AA in their comments and missed it on four
of six pairs, measured: white on #16A34A is 3.30, near-black on #B45309 is
3.53, white on #0EA5E9 is 2.77, white on #0284C7 is 4.10. The cause was
assuming one foreground per role works in both modes, but the generated fills
do not hold lightness across modes and they move in opposite directions:
bg-success goes darker to lighter, bg-warning lighter to darker. So each
foreground flips by mode now, no generated fill changed, and every pair is
recomputed in the docblock: 5.93, 6.40, 5.02, 5.38, 5.57, 5.02.

Two paths could act without the operator seeing it. The identity read back out
of the tree was never validated, only the flags were, so a pubspec.yaml
carrying a traversal in its name reached a directory move; it is now held to
the same patterns. And the Kotlin package move skipped every non-File entity
and then deleted the source recursively, which would take a fork's subpackages
with it, silently, in a case this repo has no fixture for.

--display now rejects # and tab. They break nothing syntactically, they get
eaten: flutter_dotenv strips a trailing #comment, so --display='Acme #1' left
the app reading 'Acme' while the command's own re-read saw the whole string,
which quietly breaks idempotency.

TEST_HOST on macOS is rewritten after all. The constraint was to touch the
Xcode project only through anchored matches, and this rule anchors on the
setting name and the current value exactly like the bundle-id rule beside it,
so it is an instance of that constraint rather than an exception to it. Left
alone it pointed a renamed fork's RunnerTests target at a bundle that is no
longer produced.

The lockfile gate did not run in CI at all, because CI never invokes bin/check;
it now checks the committed lock directly, before pub get rewrites the
evidence. The dashboard card was duplicated between its loading and loaded
branches, differing only in indentation, and its two strings were hardcoded
English on the one screen the rules name as the pattern to copy.
Nothing in this app imports file_picker. It arrives through magic's Pick
facade, which makes the pin here a second opinion on a version this app has no
stake in, and second opinions on a transitive dependency only matter when they
disagree with the first one.

They did. magic's master moved to ^12.2.0 ahead of a release while this app
pinned ^11.0.2 to match published magic 0.0.9. Those ranges do not intersect,
so version solving failed outright against local sibling checkouts and
bin/check could not run at all, which is the state this repository was already
in before any of this work started.

Removing the pin resolves file_picker from whichever magic is in play:
transitive 12.2.0 against the path-bound local checkout, transitive 11.0.3
against published magic 0.0.9, which is the same version a fork was getting
before. Both graphs verified green with the full suite, and the day magic
publishes its file_picker 12 port nothing here needs editing.

The comment the pin carried argued a fork has no dependency_overrides safety
net so the constraint should be repeated here. That was true and still
insufficient: magic 0.0.9 already caps file_picker below 12.0.0-0, so the pin
never protected anything the framework was not already protecting.
@kodizm

kodizm Bot commented Sep 6, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

The dependency fix and its CI proof hold up under hosted resolution, and the localisation work checks out exactly as claimed; one latent correctness bug in app:rename that this repo's own tree cannot trigger but a fork can.

Major

lib/app/commands/app_rename_command.dart:306 — correctness. _buildPlan appends _identityRewrites and _dartImportRewrites into the same changed list without merging by path, and both compute their content from the same on-disk source. _apply (line 668) then writes them in order, so when one path appears twice the second write discards the first. lib/main.dart is the only file in both generators: this repo's copy uses relative imports so the collision never fires, but a fork whose lib/main.dart self-imports package:<name>/... (exactly the case _dartImportRewrites exists to scan for, per its docblock at line 581) gets the import rewrite last and silently loses the MagicApplication(title: '...') display-name rewrite. The report prints lib/main.dart twice with plausible line counts, so it reads as done. Not covered: the fixture at test/app/commands/app_rename_command_test.dart:15 copies this repo's main.dart, which has no package: self-import.

Minor

.github/workflows/ci.yml:171 — maintainability. The published job hard-asserts 0.0.1-alpha.26, and nothing links that literal to pubspec.yaml. The next magic_starter bump turns the job red with "expected magic_starter 0.0.1-alpha.26" on a PR whose author has no reason to look in ci.yml. Deriving the expected version from pubspec.yaml's constraint, or asserting "not older than the constraint floor", would keep the same guarantee without the second edit site.

lib/app/commands/_index.g.dart:6AGENTS.md lists this file as generated by commands:refresh and never hand-edited, but the committed content is not dart format clean (a stray trailing blank line after ];), which suggests it was written by hand rather than regenerated. Harmless today (no CI format gate), but the next commands:refresh will produce a diff nobody expects. dart format --output=none --set-exit-if-changed lib/Changed lib/app/commands/_index.g.dart.

Tests

Good coverage for the new surface: 16 cases for app:rename (dry-run inertness by snapshot, idempotency, the pbxproj anchoring, the Kotlin move, five refusal paths) and locale_parity_test.dart for en/tr key parity. The gap is the one in the Major above. DashboardController itself has no unit test; widget_test.dart only proves the app boots.

Checks I ran

  • flutter pub get (no pubspec_overrides.yaml present, so hosted resolution) → Got dependencies!, git status --porcelain pubspec.lock empty, grep -cE 'source: (path|git)' pubspec.lock → 0, magic_starter resolved to "0.0.1-alpha.26". All three published-job assertions pass locally.
  • flutter analyzeNo issues found! (ran in 18.4s).
  • flutter test+26: All tests passed!.
  • bin/design-tokensno raw color literals found outside the allowlist.
  • bin/sync-instructions --checkmirrors are up to date.
  • Verified the PR's own claims: all 20 magic_starter.titles.* keys referenced in magic_starter alpha.26's source are present in assets/lang/en.json (0 missing); the only starter trans() keys still absent are billing ones, and billing is false; the six WCAG ratios in example_status_tokens.dart recompute to exactly the values documented (5.93 / 6.40 / 5.02 / 5.38 / 5.57 / 5.02) against the fills in wind_theme.g.dart; tr.json is genuinely translated (4 of 292 strings identical to en, all placeholders); notifications.push.app_id: '' is safe, notification_service_provider.dart:183 returns early on an empty id.
  • Did not run: the dusk pass (the author documents why), the backend/pint half, and bin/check itself (it requires pubspec_overrides.yaml, absent in this checkout).
  • Not reviewed: README.md, AGENTS.md, docs/verification-loop.md, the .github/ instruction mirrors (verified in sync, not read), assets/lang/tr.json and pubspec.lock (checked programmatically, not line by line).

_buildPlan spread the identity rewrites and the Dart-import scan into one list
without merging by path, and the loop re-reads each path from disk. So a file
claimed by both generators produced two planned writes from the same original
source, and _apply performed them in order: the second discarded the first
while the report counted the file as changed twice, with plausible line counts.

lib/main.dart is the only path both can claim. It carries the MagicApplication
title AND is scanned for a `package:` self-import. This repository never
reaches the collision because its own main.dart imports relatively, which is
exactly why the fix has to be structural rather than left to a fixture that
mirrors this tree: a fork that self-imports loses its display name silently.
The test introduces the self-import to reach the case, and fails on the old
code with "the display-name rewrite was discarded by the import rewrite".

The published job now reads the expected version floor out of pubspec.yaml
instead of carrying a literal. A hard-coded alpha.26 would turn the job red on
the next routine bump, in a file the bumping author has no reason to open, and
name a version nobody typed. The comparison is a floor rather than an equality
so a legitimately newer release still passes, verified against alpha.24
(rejected), alpha.26, alpha.27 and 0.0.2.
@anilcancakir
anilcancakir force-pushed the feature/fork-ready-boilerplate branch from 7bd5af2 to 257db66 Compare September 6, 2026 23:49
@anilcancakir

Copy link
Copy Markdown
Contributor Author

Thanks, the Major is real and now fixed. Two notes where I landed somewhere different.

Major, _buildPlan duplicate paths. Confirmed and fixed in 257db66. Verified the mechanism
rather than the symptom: the two generators spread into one list with no merge, and the planning
loop calls _read(rewrite.path) fresh each iteration, so the second entry starts from the
original source and _apply writes it last. lib/main.dart is indeed the only path both can
claim.

The plan now merges rules per path before planning. The test introduces the package: self-import
into the fixture's main.dart to reach the case this tree cannot, and asserts both rewrites
survive. Negative-controlled by restoring the old loop, where it fails with exactly the reason it
was written to produce:

Expected: contains 'MagicApplication(title: \'Acme App\''
  Actual: 'import \'package:acme_app/config/app.dart\';\n'
  the display-name rewrite was discarded by the import rewrite

Minor, the hard-coded alpha.26. Fair, and mine. The job now reads the floor out of
pubspec.yaml and compares as a floor rather than an equality, so a legitimately newer release
still passes. Checked against alpha.24 (rejected), alpha.26, alpha.27 and 0.0.2. It also refuses
when either value comes back empty, since the comparison would otherwise pass vacuously.

Minor, _index.g.dart formatting. Half right, and the inference does not hold. The file
genuinely is not dart format clean, but it was not hand-edited: running commands:refresh
reproduces the committed bytes exactly. Three other files fail the same check, including the
sibling generated lib/_previews.g.dart, and none of them is touched by this PR:

Changed lib/_previews.g.dart
Changed lib/app/commands/_index.g.dart
Changed lib/config/view.dart
Changed lib/ui/components/tag/tag.recipe.dart

So this is a property of the artisan generators rather than of this change, and it predates the
PR on three of four counts. Acting on it here would mean hand-editing the one file AGENTS.md
marks as never hand-edited. Left alone deliberately; the generator side belongs in the artisan
repo.

On DashboardController having no unit test: agreed, and worth saying why it is thin. Its
only real logic is resetForSession clearing before refetching, which matters across an identity
change that no unit test here can stage. It is on the list for the dusk pass that is documented
as not run.

@kodizm

kodizm Bot commented Sep 6, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental re-review of 257db66 only: the duplicate-path Major is genuinely fixed at the structural level and the CI floor is now derived rather than typed; one residual edge in the sort -V comparison.

Since my last review: _buildPlan merges rewrite rules per path before planning, a test introduces the package: self-import the fixture previously could not reach, and the published job reads the floor from pubspec.yaml and compares as a floor. The _index.g.dart formatting note is settled by the reply (generator output, reproduced byte-for-byte, three untouched files share it) and I am not repeating it.

Minor

.github/workflows/ci.yml:174 — correctness in the gate itself. sort -V orders a stable release below its own prereleases, the opposite of semver, so with the floor at ^0.0.1-alpha.26 a resolve onto the stable 0.0.1 reads as a downgrade and the job goes red. That version is inside the constraint (^0.0.1-alpha.26 is >=0.0.1-alpha.26 <0.1.0), so pub can pick it the day magic_starter cuts 0.0.1, and the failure would name a version nobody typed — the same class of surprise this rewrite set out to remove. Every other pair I checked behaves: sort --versionsort (GNU coreutils) 9.4; floor/resolved extraction against the real files both print 0.0.1-alpha.26; printf '0.0.1-alpha.26\n0.0.1-alpha.30\n' | sort -V | head -10.0.1-alpha.26 (pass), …\n0.0.2\n0.0.1-alpha.26 (pass), …\n0.0.1\n0.0.1 (false fail), and the alpha.24 walk-back this job exists for sorts below the floor and fails correctly.

Tests

The new case is the right one: it writes the package:magic_example/ self-import into the fixture's main.dart — the state this tree cannot reach — and asserts both the import and the MagicApplication(title:) rewrite survive, with isNot(contains('magic_example')) closing the gap. 17 cases in that file now.

Checks I ran

  • flutter test test/app/commands/app_rename_command_test.dart+17: All tests passed!
  • flutter test+27: All tests passed! (was 26)
  • flutter analyzeNo issues found! (ran in 17.7s)
  • Read _buildPlan, _identityRewrites, _dartImportRewrites, _apply in full: lib/main.dart is still the only path both generators can claim, merged rules apply in order (identity title, then import) against a single read, and each path now lands in changed/unchanged exactly once.
  • Reproduced the CI shell's floor and resolved extraction against the real pubspec.yaml / pubspec.lock (single magic_starter: line, no pubspec_overrides.yaml in this checkout, so this is the hosted graph) plus the sort -V pairs above. I could not run the full scripted job end to end in a scratch directory; the ordering evidence is direct.
  • Not re-run: the backend/pint half, bin/check (needs pubspec_overrides.yaml), the dusk pass. Nothing outside these three files was re-read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant