Skip to content

Latest commit

 

History

History
260 lines (208 loc) · 12.2 KB

File metadata and controls

260 lines (208 loc) · 12.2 KB

Working in this repository

DPIP is a Taiwan disaster-prevention app: a clean Flutter 3.47 rewrite, feature-first architecture.

Where things are written down

Each topic has exactly one home. Nothing below is repeated elsewhere — the other files point here, and this file points at them.

File Owns
ARCHITECTURE.md Folder layout, layer rules, and every subsystem contract: logging, state, networking, data & errors, realtime, calibrated time, async-state UI, push, LoRa mesh, persistence
DESIGN.md Design tokens, colour, spacing, motion, typography, icons, localization, shared components
api.md API endpoints, the region map, and which tier each one lives on
commit.md Commit format, in full (中文)
README.md What DPIP is, for people who do not work on it
this file Toolchain, running, verification, versions

Toolchain

mise is required. Not preferred — required. Without it the scripts refuse to run and tell you how to install it, because there is nothing to pin against and a build off the wrong SDK looks exactly like a build off the right one.

Flutter and Dart are pinned in mise.toml, and every workflow has a script under tool/. Never type the toolchain yourself — not flutter, not dart, and not mise exec. A shell's PATH is resolved once and mise activate caches it, so a toolchain bump leaves the old SDK on PATH until the session is replaced, and a run against the wrong SDK announces nothing: it builds, it runs, its tests pass.

Four things enforce it, none of which relies on anybody remembering:

Where What it refuses
require_mise in tool/dev/_lib.sh no mise, no mise.toml, or a flutter that resolves outside mise's own installs — the last one is the dangerous case, because mise exec will happily forward to a system SDK
tool/check/tooling.sh a bare toolchain command in the docs or CI; and, for every script in tool/, one that does not parse, is not executable, has no shebang, or reaches flutter / dart / mise exec without going through pinned
tool/internal/apply_sdk_patches.sh an SDK that is missing a patch this repo owns, or a patch that no longer applies to the pinned version — see SDK patches
tool/run.sh runs them before it starts anything (0.34 s)
tool/dev/analyze.sh
Do this Run
Start the app tool/run.sh (see Running)
Run the tests tool/dev/test.sh
Test coverage, into coverage/ tool/dev/coverage.sh (see Coverage)
Format + analyze tool/dev/analyze.sh
Reformat in place tool/dev/format.sh
Resolve dependencies tool/dev/deps.sh (--offline when pub.dev stalls)
Regenerate after @freezed / @JsonSerializable edits tool/dev/codegen.sh
Regenerate l10n by hand (a build does it anyway) tool/dev/l10n.sh
Throw away the build output tool/dev/clean.sh
Release build tool/dev/build.sh {android|bundle|ios}
Everything CI runs tool/check.sh (content-hash cached: ~1 s when nothing changed)
Before writing any commit tool/commit.sh — and .githooks/pre-commit runs it whether you do or not (see commit.md)
One-time git-hook setup tool/dev/setup.sh (tool/run.sh does it for you)

tool/ is organised by what a script is for: dev/ daily workflows, check/ the CI gates, release/ versioning and notes, gen/ asset and code generators, internal/ pieces other scripts call and nobody runs by hand.

SDK patches

The pinned Flutter SDK is not stock. tool/patches/*.patch is applied to it by require_mise, so every script here — and every CI job, which reaches the toolchain the same way — runs a patched framework. Each patch names its upstream issue in its own header; read that before touching one.

This is the one place the repo deliberately does what the rest of this section exists to prevent, so it is held to the same standard. The patch lives in the repo rather than in an install, it is applied at the single gate every toolchain call already passes, and it says so the first time it changes anything. A patch applied on one laptop and forgotten on a runner would be exactly the works-here-fails-there the pin was bought to stop.

Two things follow, and both matter:

  • A patch that no longer applies is a hard stop, not a skip. That is almost always a Flutter bump. Somebody has to decide whether upstream fixed the bug — delete the patch — or whether it has to be re-cut against the new source.
  • Every patch carries a test that fails without it. The test, not a check script, is what proves the patch is really in; it is also what stays behind once the patch is deleted, to say the upstream fix is real.

Running

tool/run.sh -d "iPhone 17 Pro"

On Windows, tool\run.ps1 -d "Pixel 9" — or bash tool/run.sh under Git Bash or WSL, which is the one that colours the log. run.ps1 deliberately does not pipe: $LASTEXITCODE is unreliable when a native command feeds a cmdlet (PowerShell/PowerShell#19848), and a wrapper that reports a failed build as a success is worse than an uncoloured one.

This is the only supported way to start the app. Every other way of starting it is wrong in a way nothing tells you about, so a debug build started any other way refuses to run and prints the command to use instead.

Arguments pass through untouched, and hot reload still works: the tool reads supportsColor from stdout and its keystrokes from stdin, and a pipe only touches the first.

Select the device with -d <name|id>. A bare flutter run ios treats ios as a target Dart file and fails with Target file "ios" not found.

  • tool/run.sh runs the pinned flutter run and pipes it through tool/internal/colorize_logs.sh. Colour is added by the pipe, not by the app: on iOS an escape sequence cannot survive the trip, because the platform's log path escapes the escape character and even a terminal that supports ANSI then prints it (flutter/flutter#20663). dart:developer's log does deliver them, but truncates past ~128 characters — which is where the diagnostic lines are. The pipe has neither problem, and drops the flutter: prefix as well.

  • If a launch stalls at Downloading packages, resolve from the local cache first with tool/dev/deps.sh --offline, then re-run.

  • The visible simulator window in Xcode 26+ is DeviceHub.app — it replaced Simulator.app, and open -a Simulator no longer works. flutter run boots the simulator headless, so open it separately to see or touch anything:

    open "$(xcode-select -p)/../Applications/DeviceHub.app"

Coverage

tool/dev/coverage.sh

It runs the test suite with coverage and writes coverage/lcov.info, plus coverage/html/ where genhtml is installed (brew install lcov). Every library in lib/ is counted, including the ones no test imports; leaving those out used to make the total read higher than the truth.

Generated code is left out by the standard // coverage:ignore-file comment, which flutter test itself honours, so every way of measuring leaves out the same files. freezed writes it, build.yaml has source_gen add it to every .g.dart, and l10n.yaml has gen-l10n add it to the localizations. test/tool/generated_code_coverage_test.dart fails when a generated file comes out without it.

Nobody has to remember to run it:

  • CI runs it as the test step and uploads the lcov to Codecov, which is where the README's badge and sunburst graph, the per-file view and the pull-request comment all come from. codecov.yml holds its settings; the upload signs with an OIDC token, so there is no secret to keep.
  • tool/check.sh runs it too, because it is what CI runs.

In VS Code, the Dart extension runs it from the Testing view: Run Tests with Coverage puts a bar on every folder and file under Test Coverage, a percentage beside each file in the Explorer, and the lines that ran in the editor. That is the extension's own run, so its numbers are close to this script's rather than equal to them: it leaves out the libraries no test imports, and it also counts the lines its branch data names.

Commits

The full specification, with examples, is commit.md. The parts worth knowing before writing one:

<type>(<scope>): <English summary>

New(zh-Hant): <一行,使用者感覺得到的事>
New(en-US): <the same, in English>
  • Each Category(locale): line is one changelog entry, extracted by tool/release/notes.sh with a single regular expression. Categories are New / Optimization / Fix; zh-Hant and en-US are required and the app's other locales are optional.
  • There is no prose body. Why it was done, what was tried, what bit you — all of it goes in a code comment, where the next person to touch the code will see it. Nobody reading a changelog can use any of it.
  • feat / fix / perf need at least one entry line; everything else needs none and simply does not appear in a note.
  • The category is declared, not inferred from the type — so a user-visible fix that lives in a chore: commit still reaches the changelog, which the old type-derived mapping silently dropped.
  • Rebase, never merge, and never leave the branch behind. CI refuses both: a merge commit is invisible to the gate (--no-merges), so anything arriving through one is never judged, and a branch that is behind was tested against a main that no longer exists. git rebase origin/main and force-with-lease.
  • One thing per commit. No gate can check this — whether two changes are the same thing is a judgement — so it is on you and on review.
  • Never add a Co-Authored-By: trailer, Generated with …, 🤖, a model name or an agent's name. A commit is authored by a person; an agent that writes itself into the record makes the history lie about who is accountable, and that record is what someone reads years later to ask why.

Before pushing

Everything CI runs, in order. All of it must be clean:

tool/check.sh

That is the whole list, and it is the same list .github/workflows/ci.yml runs — CI calls these scripts rather than naming the commands itself, so the two cannot drift.

CI judges the commits on the branch, never the pull request's title or description — which is safe because rebase is the only merge this repository allows (Settings → Pull requests). What lands on main is what was judged. See commit.md. Individually, if you want to fail faster:

tool/check/commits.sh origin/main..HEAD
tool/check/layering.sh
tool/check/l10n.sh
tool/check/storage.sh
tool/check/pubspec_lock.sh
tool/check/notification_sounds.sh
tool/check/tooling.sh
tool/dev/analyze.sh
tool/dev/codegen.sh          # then git diff --exit-code
tool/dev/test.sh

The bash gates need only bash and python3, so they fail fast without the toolchain. .github/workflows/ci.yml must stay green; android.yml / ios.yml build artifacts and review.yml adds an automated PR review.

Safety-critical seismic maths is pinned by golden tests (test/features/earthquake/eew_estimator_test.dart). If you change the EEW estimator, update those goldens deliberately.

Versions

Nobody edits a version by hand. tool/release/version.sh derives all three values from git state and CI passes them to the build:

label what a human sees 26.1 · 26w33a
train what Apple is told 26.1
code what both stores sort by 426000298

Every commit on main publishes a snapshot; a v* tag publishes a release. pubspec.yaml's version: is a placeholder for local runs only.

Read the header of tool/release/version.sh before changing any of it. Every constant there is a fact about what has already shipped to a store, and a store refuses, permanently, any build whose ordinal is not above the last it accepted — deleting the build does not release the number.

Reporting

Say what was actually done. If a test fails, show the output; if a step was skipped, say so. Do not report work as finished until it is verified — the list under Before pushing is what "verified" means here.