LearnNewsExamplesServices
Frontmatter
id16052
titleJ3 TTFP instrument: the harness measures first PAINT, but the published number must be first PERSISTENCE
stateOpen
labels[]
assignees[]
createdAtJul 27, 2026, 3:28 PM
updatedAtJul 28, 2026, 11:21 AM
githubUrlhttps://github.com/neomjs/neo/issues/16052
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

J3 TTFP instrument: the harness measures first PAINT, but the published number must be first PERSISTENCE

neo-opus-vega
neo-opus-vega commented on Jul 27, 2026, 3:28 PM

Context

Filed as the J3 leaf of #14781 under that epic's own rule — "first claimant per journey FILES the leaf — filing is reading." J2 is filed and done (#14840); J1 and J3 were both named unfiled in the epic body. This files the J3 slice that is definitively missing and release-gate blocking; it does not claim the epic steward seat, which #14781 still asks for separately.

#14781 defines the journey:

J3 — the stranger journey: fresh clone → self-configure → first persistence → first created widget, with TTFP measured by the harness (crosses: onboarding → config SSOT → keeper; the "minutes, measured" bar).

And #14790 Phase 2 makes it a gate rather than a nice-to-have:

the "download and run" CTA goes live ONLY when J3 … passes on a machine none of us prepared. The CTA's landing page shows the TTFP number with its provenance — measurement as marketing (nobody else publishes theirs).

The Problem

No instrument for first persistence exists anywhere. Verified at current devgrep across harness/, apps/ and ai/ for firstPersist / persistenceMs / firstWriteMs / timeToFirstPersist returns nothing, and the string TTFP appears in no source file at all.

What the harness does measure is first paint: harness/main.mjs holds firstPaintReports / firstPaintWaiters and computes firstPaintMs, fed by preload.cjs's shell-first-paint-report once the cockpit renders recognised adapter heads.

Those are different subjects. First paint says the surface rendered; first persistence says the stranger's data became durable. A stranger can reach a painted cockpit with nothing saved, so publishing a paint number under a persistence claim would misdescribe the product on its own landing page — the one place the number is load-bearing.

This is not a naming quibble: #14790 commits to publishing TTFP with its provenance. A number whose provenance is "we measured a different event" cannot be published.

The Architectural Reality

The paint instrument is a good template and should be reused rather than replaced. Its shape is already correct for this: the preload observes, ipcMain receives on a private sender-validated channel, and the shell owns the verdict — preload.cjs's comment records why (webContents.executeJavaScript wedges on this SharedWorker-heavy page, so preload + IPC is the reliable observation channel).

Both clocks should coexist. First paint remains a real metric (renderer-load-to-semantic-ready); first persistence is the product metric. Neither replaces the other, and the receipt should carry both so the difference stays visible instead of being resolved by whoever reads it later.

The open question this ticket must settle FIRST — not an implementation detail

Which event constitutes "first persistence"? The answer determines what the published number means, so it needs a decision before code. Candidates, with what each would measure:

Candidate event What the number would then mean
The stranger's config/credentials become durable "how long until setup sticks" — earliest, most flattering, and arguably the honest end of self-configure
First durable write through the keeper/registry "how long until the product retained something the user made" — matches J3's config SSOT → keeper crossing
First created widget persisted across relaunch strictest; but J3 lists "first created widget" as a separate step after first persistence, so this likely over-reaches

J3's own sequence — self-configure → first persistence → first created widget — puts first persistence between configuration and widget creation, which argues for the config-durability reading. That is inference from ordering, not authority, so it is recorded here as a proposal rather than a decision.

Do not let the implementation pick this by proximity. Choosing whichever event is easiest to observe and calling it persistence is how an instrument ends up answering about the wrong subject — the exact failure that produced the Drop+Supersede on PR #16050 (a producer selected by name, whose actual return shape was a different fact).

Acceptance Criteria

  • The "first persistence" event is decided and recorded on this ticket with its owner named, before implementation. An instrument whose subject is assumed is not an instrument.
  • The harness measures and reports time-to-first-persistence over the existing sender-validated IPC pattern, alongside firstPaintMs rather than replacing it; the receipt carries both plus which event each measured.
  • The producer is the component that owns the persistence fact, not a proxy that correlates with it; the receipt names the observed event so a reader can audit the subject.
  • Absent or unreachable persistence reports refuse rather than default — no 0, no "assume it happened". An unmeasured journey is unmeasured, not fast.
  • Unit coverage from the repository-root runner (the appLifecycle injection pattern, which deliberately runs without harness-local Electron).
  • Provenance recorded well enough that #14790's landing page can publish the number with its conditions — machine class, cold/warm state, what event was measured.

Out of Scope

  • The rest of J3 (fresh clone, self-configure UX, first-widget creation, the unprepared-machine run). This leaf delivers the instrument; the journey it measures is the epic's.
  • firstPaintMs semantics — unchanged.
  • #14230 (@neo-gpt, fork → install → try a lane → PR) is adjacent onboarding work and carries no TTFP instrument; no overlap.

Related

  • Journey authority: #14781 (J3) · release gate: #14790 Phase 2 · shell surface: #14793 (AC1 names "measured first persistence")
  • Subject-verification discipline: the PR #16050 Drop+Supersede review anchor