Skip to content

Phase 1: make a green make verify mean something - #276

Merged
entelecheia merged 42 commits into
mainfrom
gsd/phase-1-trustworthy-verify-signal
Aug 22, 2026
Merged

Phase 1: make a green make verify mean something#276
entelecheia merged 42 commits into
mainfrom
gsd/phase-1-trustworthy-verify-signal

Conversation

@entelecheia

Copy link
Copy Markdown
Member

make verify gains four gates it did not have: a Rust lint gate, a React hook-dependency
gate, typecheck coverage for e2e/ and scripts/, and a pinned Rust toolchain. Plus a
Playwright trace that actually gets written on a CI failure, and two pieces of cleanup the
gates made possible.

Nothing user-visible changes. That is the point. Milestone 1's remaining phases are all
behavior-preserving refactors — collapsing duplicated Rust invariants, typing the IPC error
contract, moving src/App.tsx pane state onto module stores — and a green gate is their only
proof. This phase is first because the proof has to be trustworthy before the work starts.

The gates

Before After
Rust lint none cargo clippy -- -D warnings (lib scope), 75 pre-existing violations fixed, zero allow escapes
Rust format none cargo fmt --check
Rust toolchain rust-version = "1.77.2" floor rust-toolchain.toml pinning 1.98.0
JS/TS lint none at all ESLint 10 flat config, four correctness rules, src/ + e2e/
Typecheck scope ["src"] four projects: src, node, e2e/, scripts/
CI trace on e2e failure configured but never written written, downloaded, verified

Every gate proven, not asserted

The phase held itself to one standard: break it, watch it go red, revert. Config inspection
was not accepted as proof for anything.

  • GATE-03, both halves. A deliberate TS2322 in e2e/smoke.spec.ts and a deliberate
    TS2304 in scripts/build-macos-passkeys.mjs each drove pnpm typecheck to exit 2.
    Before this, a type error in a Playwright spec survived until the spec ran.
  • GATE-04. A temporarily-failing spec was pushed, CI run, the playwright-report
    artifact downloaded, and trace.zip confirmed present and non-trivial. Then reverted
    byte-identical. Done twice — once for the original config, again after the trace was
    narrowed, because the second change invalidated the first proof.
  • GATE-02. A wrong dep array and an unused symbol each failed make lint by name.
  • GATE-01. Proven red-then-green locally and, more usefully, by catching a real bug (below).

The gate earned its keep on day one

make clippy passed on macOS and failed on CI with nine dead_code errors.
browser_passkeys.rs and site_view.rs hold macOS-only functionality whose call sites
compile out on ubuntu-22.04, orphaning the helpers. Fixed with #[cfg(target_os = "macos")]
at the natural boundaries — sixteen attributes, no allow suppressions.

The fix also had to reach four #[cfg(test)] functions that cargo clippy never compiles but
cargo test --lib does, which would have turned a clippy failure into a Linux test-compile
failure. That is a class of defect nobody in this repo could have seen before this phase.

What this cost, measured

retain-on-failure records a trace for every test and discards the passes, unlike
on-first-retry with retries: 0, which records nothing. Turning it on took CI e2e from a
steady 5.5m / 203 passed to 7.4m / 2 failed — the added time tipped two wall-clock .poll()
specs into flaking. Narrowing the trace to the action timeline and stacks restored 5.4m with
zero failures.

The remaining trace is 123 KB across 6 entries, against 1.75 MB across 14 with the full
config. It keeps the action timeline, the failing stack, and source. It does not keep DOM
snapshots, screenshots, or the network log — snapshots: false disables network capture too,
which the config comment now says explicitly after an earlier version of it got this wrong.
For a selector that stops matching or a re-render that drops content, a DOM snapshot is the
most useful diagnostic and it is not there. Phases 4-5 produce exactly that failure class, so
re-enabling snapshots is a two-minutes-of-CI decision they may want to make.

What these gates still cannot catch

Recorded in REQUIREMENTS.md for Phase 3 rather than left implicit.

None of the seven gates can catch a serde mismatch at the Rust-TypeScript IPC boundary.
Verified concretely against this phase's own reshape of SkillDispatchBackgroundArgs:
cargo test --lib constructs the struct directly and never deserializes JSON, and
make test-e2e serves plain vite rather than tauri-dev, so window.__TAURI_INTERNALS__
never exists and neither reshaped command is exercised by any e2e spec. A camelCase drift
there passes typecheck, unit tests, e2e, and clippy, and fails only in the built app.

This is the pre-disclosed native-tauri-e2e-runner-missing gap, deliberately kept open and
deferred. Phase 3 adds more boundary structs of that exact kind, so it inherits the blind spot
— with a suggested mitigation far cheaper than the deferred native runner: one
serde_json::from_value round-trip test per new struct.

Scope kept

no-console is not enabled; the 35 calls in non-test src/ are untouched. The rule set is
four rules, not a preset. scripts/ is typechecked but not linted. The 45 exhaustive-deps
disable comments added all carry a concrete reason and are meant to be deleted by Phases 4-5
as each pane is touched — they are a worklist, not a settlement.

Eight bare disable directives from July survive elsewhere in src/, and graph.spec.ts has
carried its own per-spec retries since before this phase. Neither was introduced here, and the
phase artifacts now say so precisely rather than claiming a blanket property.

Verification

CI green at HEAD: make verify passes on ubuntu-22.04 with all seven gates wired in; Rust
1192 passed; e2e 202 passed / 0 failed / 1 flaky (the pre-existing WebGL retry) in 5.5m.

Full goal-backward verification in
.planning/phases/01-trustworthy-verify-signal/01-VERIFICATION.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_0133xHs4682BJrwz8NHpK76p

- Add rust-toolchain.toml at repo root pinning rustc 1.98.0 (current
  stable per D-11), with clippy+rustfmt components
- Add Makefile fmt-check target (cargo fmt --check, no ICON_PATH prereq)
- Add fmt-check to the verify prerequisite list
The dispatch-isolation sentinel is written per run to tell the isolation guard
hooks which mode the phase resolved to. It is machine-local and must not be
committed.
- Re-measured on the pinned rustc 1.98.0 toolchain: 75 violations,
  matching RESEARCH.md's count measured on 1.96.0 exactly
- cargo clippy --fix --allow-dirty --allow-staged cleared 36 of them
  (needless_borrow, map_or->is_some_and, derivable_impls Default,
  io::Error::other, sort_by_key, last->next_back, eta-reductions)
- cargo fmt normalizes the two files where --fix left non-idiomatic
  formatting (kordoc_lite.rs match guards, export/dispatch.rs Err arm)
- cargo test --lib: 1199 passed, 0 failed, 3 ignored - no test fn
  touched by the diff, behavior preserved
- 39 violations remain for Task 2's hand fixes
Hand-fixed the violation categories cargo clippy --fix could not touch
automatically:

- Genuinely dead test-only helpers in maru_dir.rs marked #[cfg(test)]
  rather than deleted
- Two is_none_or usages (agents.rs, kakao_relay.rs, pre-dating this
  plan) rewritten to map_or(true, ...) - is_none_or needs rustc 1.82,
  the crate's declared MSRV (Cargo.toml, untouched by this plan) is
  1.77.2
- manual_clamp, manual_strip, drop_non_drop, field_reassign_with_default,
  explicit_counter_loop, doc_lazy_continuation, filter().next_back(): one
  mechanical rewrite each, no behavior change
- large_enum_variant / result_large_err: boxed the oversized variant/Err
  payload (inbox.rs, terminal/mod.rs, web_actions.rs); construction and
  match sites updated, Debug/Serialize output unchanged
- too_many_arguments (9 functions): bundled related parameters into a
  small local struct per function. Two are #[tauri::command] IPC
  boundaries (skills_dispatch_background, terminal_spawn) - their
  paired frontend invoke() call was updated in the same commit to nest
  the same fields under one new key, so the wire values and all other
  callers are unchanged

cargo clippy --offline -- -D warnings: 0 errors (was 75 at the start
of this plan). cargo test --lib --offline: 1199 passed, 0 failed, 3
ignored - identical to the pre-fix baseline. cargo fmt --check: clean.
pnpm typecheck: clean. Zero allow(clippy::...) or crate-level
allow/deny/warn attributes introduced.
Add a clippy target (cargo clippy -- -D warnings, lib scope, $(ICON_PATH)
prerequisite since it compiles the crate like test-rust) and add it to
verify's prerequisite list after fmt-check. Proven red-then-green with a
deliberate needless_return break in src-tauri/src/maru_dir.rs, reverted
with git checkout -- with no residue (working tree byte-identical to
before the break).

Completes GATE-01 together with plan 01-01's fmt-check: make verify now
fails on either a Rust format violation or a clippy warning.
Switch use.trace from on-first-retry to retain-on-failure so a first-attempt
e2e failure in CI writes trace.zip under test-results/, which CI already
uploads on failure. No retries key added (D-12): the suite keeps its
zero-retry, no-flake signal from v0.4.58.
…dger

The entry's premise (README naming inbox-processor/lint/hwpx-fill instead of
the current inbox-process/vault-lint/hwpx skill names) no longer holds; the
shipped ledger should not carry a stale claim into every e2e artifact.

Add a one-line doc comment above the TODO_LEDGER declaration stating it is
hand-maintained, not derived from README or REQUIREMENTS, following the
sites.ts:267 comment convention.
…tion

Proves success criterion 3 empirically per D-13: a failing e2e in CI must leave
a downloadable Playwright trace in the uploaded artifacts, and must do so on the
first failure with no retry (D-12 adds `trace: "retain-on-failure"` and
deliberately does NOT add `retries`).

This commit is reverted immediately after the CI run is observed. It is not
intended to survive on the branch.
The clippy gate added in 01-02 passes on macOS but failed on CI
(ubuntu-22.04) with 9 dead_code errors: browser_passkeys.rs and
site_view.rs hold macOS-only functionality whose call sites compile
out on Linux, leaving the helpers unreferenced, and -D warnings
promotes dead_code to an error.

Gate each item with #[cfg(target_os = "macos")], matching the
convention both files already use. No allow escapes (D-08).

Found by CI run 32558565444, the first real run of the new gate.
- Add @types/node@^22.20.1 as a devDependency (approved at the blocking-human
  legitimacy gate), required for tsconfig.e2e.json/tsconfig.scripts.json to
  resolve types: ["node"]
- Remove @types/dompurify from dependencies; dompurify@3.4.1 ships its own
  type declarations (GATE-06)
- Fix: retype ScratchpadPane.tsx's two window.setTimeout-backed refs as
  number instead of ReturnType<typeof window.setTimeout>. Once @types/node's
  ambient globals become visible to tsconfig.app.json (which has no explicit
  types restriction), that type query resolves to NodeJS.Timeout instead of
  DOM's number, breaking these two browser-only timer refs. Type-only fix,
  no behavior change.
…te off

Add tsconfig.e2e.json at the repo root (strict, ES2022 + DOM + DOM.Iterable,
types: ["node"], include: ["e2e"]) and drive `pnpm exec tsc -p
tsconfig.e2e.json` to zero errors. Not yet added to tsconfig.json's
references array, so make verify stays green while this lands.

Re-measured against RESEARCH.md's "6 errors in 2 files" claim rather than
trusting it: confirmed exactly 6, in exactly those 2 files.

- e2e/drafts.spec.ts: give the in-page `drafts` seed array an explicit
  DraftEntry type (importance/confidence/promotedTo all nullable) instead of
  letting it narrow from the two seed literals, which didn't cover the
  nullable values drafts_promote/drafts_create assign later
- e2e/helpers/todayFixtures.ts: merge a duplicate `taskId` field in
  applyMutation's inline parameter type (TS2300/TS2717); narrow `event` to
  { ts?: string } before property access in read_task_events, matching the
  existing `(row as {...})` narrowing convention used a few lines above for
  taskRows

Deviation from the plan's literal instruction: left `composite: true` off
tsconfig.e2e.json. Adding it surfaced TS6307 project-boundary errors because
e2e/workbench-layout.spec.ts imports DEFAULT_MARU_SETTINGS from
src/lib/settings.ts, which composite's stricter file-list enforcement
rejects across the e2e/app project boundary. Verified empirically that
`tsc -b` accepts tsconfig.e2e.json as a solution-file reference without
composite (the plan's own note flagged this as untested), and every actual
acceptance criterion in Task 3/4 is satisfied without it. Fixing the
boundary "properly" would mean adding composite+declaration emission to
tsconfig.app.json, which is out of this plan's scope.
…ATE-03

Add { "path": "./tsconfig.e2e.json" } to tsconfig.json's references array.
typecheck is already a verify prerequisite (Makefile:309) and pnpm typecheck
is tsc -b, so this one line is GATE-03's e2e half; no Makefile change needed.

Break-and-revert proof (D-13): added `const gate03Probe: number = "not a
number";` to e2e/smoke.spec.ts, confirmed
`e2e/smoke.spec.ts(4,7): error TS2322: Type 'string' is not assignable to
type 'number'.` with tsc -b exiting 2, then reverted and confirmed
`pnpm typecheck` exits 0 with e2e/ clean.
…ipts/ errors

- allowJs + checkJs true, strict false per D-10, lib ES2022+DOM for
  perf-startup-profile.mjs's page.evaluate/window callbacks
- own tsBuildInfoFile, not yet referenced from tsconfig.json
- re-measured against RESEARCH.md's 44/9: found 42 errors across 8 files
- releaseVersion.mjs, provisioningProfile.mjs, updaterManifest.mjs,
  publish-updater-manifest.mjs: add JSDoc @PARAM types for destructured
  options objects that checkJs could not infer past their default-bearing
  properties (42 of 42 errors traced to this one shape)
- perf-startup-profile.mjs: inline @type cast on window inside Playwright
  page.evaluate/waitForFunction callbacks, matching e2e/startup.spec.ts's
  existing (window as Window & {...}) pattern for the same global
- updaterManifest.test.mjs: tag signatureEntries()'s return as
  Array<[string, string]> so new Map(...) resolves to a tuple overload;
  drop a dead first 'release' key always shadowed by the later spread
  (TS1117), no behavior change
- all fixes type-only: pnpm test 1853/1853 unchanged, lint:i18n/
  check:select-chrome/icons:check unchanged output
…ip GATE-03

- tsconfig.json references gains { path: ./tsconfig.scripts.json }; 4 leaf
  projects live under tsc -b now (app, node, e2e, scripts)
- no Makefile edit: typecheck was already a verify prerequisite
- break-and-revert proof: a deliberate number-for-string tag in
  scripts/check-release-version.mjs failed tsc -b with TS2322, naming the
  file; reverted, tsc -b green again, git status clean
- make verify run end to end: typecheck, lint gates, vitest 1853/1853,
  cargo test 1199/1199, fmt-check, clippy -D warnings, and the frontend
  build/bundle-budget check all green with all four project references live
- eslint@10.9.0, typescript-eslint@8.67.0, eslint-plugin-react-hooks@7.1.1
  added as devDependencies (approved at the blocking-human legitimacy gate)
- eslint.config.js: ESM flat config, src/**/*.{ts,tsx} scoped to
  tsconfig.app.json and e2e/**/*.ts scoped to tsconfig.e2e.json; exactly
  react-hooks/rules-of-hooks, react-hooks/exhaustive-deps,
  @typescript-eslint/no-unused-vars (with ^_ ignore patterns),
  @typescript-eslint/no-floating-promises, no recommended preset extended,
  no-console left off (D-07)
- package.json scripts.lint: eslint src e2e --max-warnings 0
- pnpm exec eslint src e2e measured 74 errors + 8 warnings across src/
  (e2e/ clean), matching RESEARCH.md's prior measurement; not wired into
  make verify yet (01-07's job)
- 12 no-unused-vars fixes: 6 dead icon-import removals, 1 dead prop-import
  removal, 2 dead useState bindings (outlookDecisions/setOutlookDecisions,
  telegramDecisions/setTelegramDecisions), 1 renamed to _gmailDecisions
  (setter still live), 1 dead useMemo (activeWorkspaceWriteReason), and
  the entirely-dead openBinaryWorkspaceFile callback plus its now-orphaned
  imports (binaryViewerClassify, binaryViewerPrepareAsset, insertBinaryTab,
  usesAssetProtocol, tabIdForWorkspaceFile) — deleting that dead callback
  also removed its own exhaustive-deps violation, so the final split is
  12 no-unused-vars + 8 exhaustive-deps, not the inventoried 13+9
- 8 new eslint-disable-next-line react-hooks/exhaustive-deps comments,
  each naming the rule and carrying a same-line reason; no dependency
  array's contents were changed (confirmed via diff read), only comments
  added/edited or dead code removed
- stale directive at (pre-edit) App.tsx:6974 removed; the two remaining
  pre-existing directives (boot-once-on-mount, kg-focus-reset) kept live
  and given same-line reasons to satisfy the same acceptance criterion
- pnpm exec eslint src/App.tsx --max-warnings 0 exits 0; pnpm typecheck
  exits 0; pnpm test 1853/1853 unchanged
- 24 no-unused-vars: dead imports/bindings deleted, positional args
  (favoriteIds, settings, warnings, hdbg, headerBg) renamed with a
  leading underscore where the call site still needs the arity
- 27 exhaustive-deps: every pre-existing violation annotated with a
  named, reasoned eslint-disable-next-line; no dependency array's
  contents changed
- 1 no-floating-promises: void-marked the un-awaited Promise.all in
  WritingGuidelineSidebar
- 7 dead no-console eslint-disable directives deleted (no-console
  stays off per D-07); all 35 console. calls in non-test src/
  untouched

pnpm exec eslint src --max-warnings 0 exits 0; pnpm typecheck exits 0;
pnpm test 1853/1853 unchanged.
Add `make lint` (node_modules prerequisite, runs `pnpm lint`) and add it
to the `verify` prerequisite list immediately after `typecheck`, per D-04.
Rewrites the `verify` `##` gloss to cover all three gates this phase
adds to it (lint, clippy, fmt-check).

Deliberate-break proof (see SUMMARY for full output):
- Wrong hook dependency list -> make lint fails naming
  react-hooks/exhaustive-deps; reverted, green again.
- Unused symbol without a leading underscore -> make lint fails naming
  no-unused-vars; reverted, green again.

make lint exits 0 on the reverted tree. Full make verify could not be
run to a clean exit on this shared checkout (see SUMMARY); CI is the
authoritative composite check per the team lead.
`retain-on-failure` records a trace for every test and discards the passes, so
unlike the previous `on-first-retry` (which recorded nothing at all when
`retries` is 0) it imposes real per-test cost. Measured against three
pre-phase CI runs, the e2e suite went from a consistent 203 passed in
5.3-5.6m to 201 passed / 2 failed in 7.4m — roughly 33% slower, and enough to
tip `select-audit.spec.ts` and `today.spec.ts:449` (both wall-clock `.poll()`
waits) into flaking.

DOM snapshots and screenshots are the bulk of that cost, so both are off. The
network log and stacks are retained, which is what makes a CI failure
diagnosable — the GATE-04 proof run downloaded a trace whose 471 KB
`0-trace.network` and `0-trace.stacks` carried the useful signal.

D-12 is unchanged: no `retries` key is added. The point of choosing
`retain-on-failure` was to keep the suite's no-retry property, and buying
trace capture at the cost of manufacturing flake would have defeated that.
The trace config was narrowed in a064994 (snapshots and screenshots off) after
the only empirical trace.zip proof was captured, and no e2e failure has run
since. Playwright documents retain-on-failure as writing trace.zip independent
of those sub-flags, but 'documented to work' is a weaker bar than the D-13
empirical standard this phase held everywhere else.

Reverted immediately after the CI artifact is inspected.
The comment added in a064994 said the narrowed trace retains the network log.
That is wrong. Playwright's `snapshots: false` disables network capture along
with DOM snapshots, so `0-trace.network` is 0 bytes.

Measured from a CI probe against the narrowed config (run 32569215249): the
trace.zip is 123,399 bytes across 6 entries — action timeline, failing stack,
and source — versus 1,752,382 bytes across 14 entries with a 471 KB network
log under the full configuration.

GATE-04 itself still holds: a failing e2e in CI does produce a downloadable,
non-empty trace.zip, re-proven under the config that actually ships. The
correction is to what the trace contains, not to whether it exists. The
comment now also names the failure class this trade-off is weakest against,
since Phases 4-5 are the ones most likely to hit it.
Copilot AI lite review requested due to automatic review settings August 22, 2026 11:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0b79945cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Makefile
Comment thread src-tauri/src/browser_passkeys.rs Outdated
The `verify` target grew four gates this phase but README still described the
old sequence, so the project's documented contract no longer matched what CI
enforces. Raised by Codex review on #276.

- The command list gains `pnpm lint`, `make fmt-check`, and `make clippy`.
- Both descriptions of what `make verify` runs now name ESLint and the Rust
  fmt/clippy gates.
- A new paragraph records the three things a reader cannot infer from the
  target list: typecheck now spans four TypeScript projects including `e2e/`
  and `scripts/`, `rust-toolchain.toml` makes the Rust gates resolve the same
  compiler everywhere, and the CI trace artifact keeps the action timeline and
  stack but not DOM snapshots, screenshots, or the network log.

Staged from HEAD plus these edits only: a concurrent session has an unrelated
uncommitted README section in the working tree, which is deliberately left out
of this commit and left intact on disk.
The previous fix (489aa6b) gated 4 test functions behind
#[cfg(target_os = "macos")] because they called helpers that had to
be gated to fix Linux dead_code errors. But cargo test --lib is a
Linux-only CI step, so gating the tests to macOS meant they ran on
no CI run at all -- silently dropped from the safety net despite
being pure logic (integer-to-enum mapping, URL scheme filtering)
with no macOS API dependency.

Widen the gate on the helpers themselves from
#[cfg(target_os = "macos")] to #[cfg(any(target_os = "macos", test))],
so they compile whenever cfg(test) is set (any platform's `cargo
test`) or on macOS proper, then drop the macOS-only gate from the
four tests. `cargo clippy` never enables cfg(test), so the Linux
dead_code fix still holds; `cargo test --lib` does, so the tests
run again on Linux CI.

Left EVENT_OPEN_REQUESTED and queue_opened_urls untouched -- no test
references them, so widening would be unjustified.

Found via Codex review on PR #276.
@entelecheia
entelecheia merged commit 2d2e866 into main Aug 22, 2026
1 check passed
@entelecheia
entelecheia deleted the gsd/phase-1-trustworthy-verify-signal branch August 22, 2026 12:16
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.

2 participants