Set-field column headers (E-d): the unit leaves the field, the gate proves the budget - #237
Conversation
…ement harness Commit 1 of set-field-column-headers.md §9. SetRowGeometry is D3's single source (index minimum + the lifted WEIGHT_COLUMN_FLEX its own KDoc invited; resolveIndexColumnWidth measures the widest index label through the real text stack so the column survives fontScale). SetColumnHeader is D2's one AnnotatedString in one Text: name textSecondary, unit a textDim tail span, so TextOverflow eats (КГ) before ВЕС structurally — proven by the sweep in SetColumnHeaderTest (unit-cut-name-intact at 150/130/110/90px), not by reasoning. OverflowGateSdk is the R2 measurement harness: PaparazziSdk with a discarding frame consumer — no snapshot handler, no PNGs, no liveness interaction. No consumer changes: zero golden movement by construction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
…the gate that proves it Commit 2 of set-field-column-headers.md §9. The suffix is gone from all six set-row call sites; SetColumnHeader sits above the rows in SetsColumn and CardBody with the index width resolved once for header and rows (D3) and the trailing gutter built from component-owned widths (AppCheckmarkButton now exposes its touch size; the chip slot re-exposed via SetRowGeometry). AppNumberInput trades .height for .heightIn(min) — pixel-neutral at fontScale 1.0, proven against 100 goldens before wiring — and its stale width-budget KDoc now carries the measured figures (R8). The overflow gate lands green at the fontScale-1.0 band: closed-loop per R1 (slot widths captured from the rendered production rows through valueSlotProbe — the flashAlphaOverride move), measurement-only per R2 (no snapshot handler, no PNGs, non-golden packages). Proven both ways before the fix: known-positive red on unmodified rows with the exact predicted cells (reps 2-glyph overflowed by 2.0px of the measured 1.8), and SIZE_TITLE_SP mutations moved the failure count 33→41 (26→40) and 33→22 (26→12, clearing every title-rung cell). New golden setTwoDigitReps (R7 fixture 1) — first recording. The 36 intentionally-moved existing goldens are NOT re-recorded: their verify stays red pending the Gate-2 decision; the recorded moved-set matched the prediction exactly (SessionState 20, SessionScreen 2, PastSession 14). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
…ed at last Commit 3 of set-field-column-headers.md §9, reduced to the R7 fixture: the bodyweight branch itself collapsed into commits 1-2 (SetColumnHeader's isWeighted branch is the single implementation point, so there was no third-commit code left to write). exerciseBodyweightRu is a first recording: the 71dp full-word label had zero golden coverage in any locale while it lived in the field as a suffix, and the fixture carries two-digit reps — the value class the corpus never held. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
Commit 4 of set-field-column-headers.md §9 (D6). The visible unit now lives
in a header no accessibility service associates with the fields, so
AppNumberInput gains accessibilityLabel — the AppTextField semantics
template, alias-before-block — and both rows pass the spelled-out unit
("вес, килограммы" / "повторения"; new a11y strings, both locales).
LiveSetRowSemanticsTest asserts the announcement through the repo's
Robolectric runComposeUiTest pattern: one @test, one composition holding
both branches. Deliberately NOT added: error() semantics — correct
finding, vetoed for this PR (R5), registered in the spec's blocker table.
Pixel-neutral, proven: the record-diff mover set is the same 36 files,
and the two new fixtures re-recorded byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
… now a ledger Commit 5 of set-field-column-headers.md §9 (D5). AppNumberInput resolves the value's rung by measuring its single-line advance against the slot the parent flex gave it — first fit on the [26sp, 19sp] ladder, floor pinned by contrast (below ~18.66sp bold the record molten and pending textTertiary cannot pay 4.5:1). MAX_GLYPHS_AT_FULL_SIZE is deleted: it was open-loop in both directions, never firing on the two-digit case that clipped and force-stepping five-glyph values whose boxes fit them. Acyclic by construction — the slot is the parent's decision, the style cannot move it. Ripples, all gated: plan-editor's 16 goldens byte-identical; the feature mover set is the same 36; the new readOnlyFiveGlyphWeight golden (R3) pins "102.5" back at 26sp in PlanSetCard's roomy box. MEASURED AND STOPPED ON: the full 64-cell matrix run shows 8 residual cells that exceed even the 19sp floor (spec §7's new ledger — reps×5-glyph reds the R4 hard band at 1.3/1.6). That conflicts with the band wording, so the gates stay asserted at the proven 1.0 band and the extension awaits the ruling; nothing is carved out silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
…tter; the dead suffix string leaves Commit 6 of set-field-column-headers.md §9. exerciseTenSets is R7 fixture 3 and D3's proof by picture: at ten sets the resolved index width grows the gutter for the header AND every row from the single SetRowGeometry source, so the columns stay aligned where a static header would have drifted 3dp. Strings audit closes the plan: unit_kg and unit_reps keep their consumers (PlanSetCard, PersonalRecordHero, LiveWorkoutMapper, the header's unit); unit_reps_full lost both of its consumers to the header and is removed in both locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9d28e35421
ℹ️ 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".
Unit Test Results2 039 tests 2 039 ✅ 11m 17s ⏱️ Results for commit cd6427d. ♻️ This comment has been updated with latest results. |
…lue budget Commit 7, rulings R9-R12 (spec §7a). AppNumberInput measures its incoming field width and drops Space.md insets to Space.sm under 105dp — a boundary between measured populations (in-app reps fields at 92.15/103.06dp fire; the ten-set card's 106.36dp and PlanSetCard's 109.1dp do not), costing neither typography nor contrast. The 64-cell matrix re-ran: 57/64 pass, the lever rescued live reps×3@2.0, and every remaining red is 5-glyph — five are five-digit rep counts ledgered "resolved by domain cap, follow-up PR" (R10; the missing input bound is blocker B-8), two are the 19sp-contrast-floor weight cells the bands permit at 2.0 (R11). The gates now assert the FULL matrix with the ledger inverted: a ledgered cell that starts fitting fails the gate. R12's sub-19sp rung is not taken. The mover set stays exactly the Gate-2-approved 36; plan-editor and all four new fixtures re-recorded byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
|
Rulings R9–R12 landed as commit 88a0fe2:
Gate 2 contact sheet (old | new | diff, all 36, final branch state): https://claude.ai/code/artifact/0b875cc4-bf60-406c-b643-c8a0f6a8a8b9 🤖 Generated with Claude Code |
…asserted, not photographed Commit 8, rulings R13-R16 (spec §7a). AppNumberInput.fieldInset replaces the 105dp width line — a boundary calibrated to a 3.3dp gap in today's geometry, a tripwire, not a property. Set rows pass SetRowGeometry.compactFieldInset uniformly; the header's label inset reads the same source, which also closes a real 4dp label/value drift the threshold had introduced. The matrix re-ran: 58/64, past weight×5@2.0 LEFT the ledger (caught by the inverted assertion demanding its removal — R16's rot detector in action), live weight×5@2.0 improved to +20px. The five reps×5 entries are restated as DEBT per R15: red in production until B-8 ships, void the moment it does. R14: SetColumnAlignmentGateTest asserts header gutter == row index column from the rendered layoutlib tree at 1 and 10 sets, through two flashAlphaOverride-style size probes. Proven both ways: hardcoding the gutter to 12dp reds at 10 sets (33px vs 40px); and the first Robolectric cut of this test is deliberately DELETED — measured, that stack lays a 3-digit mono index under the 12dp minimum, cannot reproduce the drift, and passed vacuously; the growth precondition now makes any such stack fail loudly instead. Baseline mover set re-verified: exactly the Gate-2-approved 36; plan-editor byte-identical. The three PR-local first-recordings (setTwoDigitReps, exerciseTenSets, exerciseBodyweightRu) are amended in-commit — the parameter removes the width-conditionality that had kept golden frames on insets production never rendered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
|
Rulings R13–R16 landed as commit 390cee3:
Contact sheet regenerated from the final state (same URL): https://claude.ai/code/artifact/0b875cc4-bf60-406c-b643-c8a0f6a8a8b9 — the 36 approved movers plus an appendix with the three PR-local first-recordings R13 amended in-commit ( Holding the re-record until Ilya clears the sheet. On clearance it is one record run over the two feature modules + one commit containing exactly the 36 re-recorded PNGs. 🤖 Generated with Claude Code |
…7's falsification honored Commit 9, rulings R17-R20 (spec §7a). The gutter-equality assert derived edge alignment from "shared tokens" — a derivation this PR itself falsified: the R9 threshold moved the field inset 4dp while the header label's stayed put, and every width stayed equal through the drift. The gate now captures the header label's and the value slot's rendered LEFT EDGES (two onGloballyPositioned probes, the flashAlphaOverride pattern) and asserts them equal at 1 and 10 sets; the gutter assert stays as the necessary-not-sufficient culprit-namer, and the golden is no longer cited as an alignment guarantee. Known-negative proven: a divergent header inset reds all four edge asserts (11px) with gutters green. R18: the Robolectric finding is recorded as an instrument defect, the second confirmed Robolectric false-negative of this PR — 12.5sp × 0.6em arithmetic puts "100" at 22.5dp, layoutlib agrees, Robolectric laid it 10.5dp under; the drift scenario is reachable in production and is exactly what the gate defends. R20: the baseline-vs-PR-local snapshot distinction is written into the spec. Probes proven pixel-inert: the record-diff mover set is unchanged at exactly 36. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
|
Rulings R17–R20 landed as commit c46110a:
Probes are pixel-inert, proven by record-diff: the mover set is unchanged at exactly the approved 36; plan-editor and the fixtures byte-identical — the contact sheet Ilya is reviewing remains valid as-is (same URL, no regeneration needed). Still holding the re-record for his clearance. 🤖 Generated with Claude Code |
|
R21 — D6 proven both ways. Known-negative run: stripping the Mutation reverted, test green again, tree clean. (Semantics-tree assertions read composition data, not font metrics — the class of Robolectric defect this PR hit twice does not apply here, and the known-negative now proves it empirically rather than by argument.) R22 — probe cost, stated plainly: the position probes are attached UNCONDITIONALLY with null-checked callbacks. Inventory of what rides production:
So: pixel-inert, yes; free, no. The 🤖 Generated with Claude Code |
|
Ruling R23 landed as commits ab516a5 + 5f18346 — step 1 taken, no fallback needed:
R24 acknowledged: this closes the technical rounds. Standing state: 11 commits, all gates green (full ledgered matrix, edge+gutter alignment, D6 both ways), zero suppressions, mover set exactly 36, sheet valid at the same URL. Remaining sequence, on Ilya's sheet clearance: one 🤖 Generated with Claude Code |
…ds the semantics tree Commit 10, ruling R23, step 1 taken (no fallback needed): layoutlib serves semantics bounds through the same public path Paparazzi's own accessibility extension uses — ComposeView's child as ViewRootForTest, semanticsOwner, getAllSemanticsNodes(false), boundsInRoot. The alignment gate now renders a caller-owned host view (OverflowGateSdk.renderView, reading during the frame window where the composition is guaranteed live) and addresses the label by its text and the field by its accessibility label — zero production trace. The onGloballyPositioned position probes (R22's per-frame cost class) are DELETED from AppNumberInput, LiveSetRow and SetColumnHeader; the onSizeChanged gutter/index probes stay (fire on size change only, never per frame). Re-proven per R23: the semantics path returns the probe path's exact numbers (88.0/414.0px at 1 set, 95.0/417.0px at 10); the R17 known-negative still reds all four edge asserts (11px) through the new path; the record-diff mover set is unchanged at exactly the approved 36. Includes the import-ordering fix this change first shipped without: the KitR alias must sort after kotlin.math.abs. That detekt red escaped because the verification pipeline piped gradle through `tail`, certifying the pipe's exit code rather than gradle's — the lesson is recorded here rather than in a separate fixup, so every commit on this branch is independently green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
5f18346 to
6a48eea
Compare
|
R25 landed — Record corrected in the description above: the "bisect-green" claim is withdrawn. Each commit is green for unit tests, both overflow gates, the alignment gate, detekt and lint — but R26 battery, re-run clean on the final state with gradle's exit codes captured directly (the earlier
Nothing previously reported green came back red. Gates proved non-vacuous in the same run: 32 cells each on the two overflow matrices, both alignment samples with real index growth (33→40px) and label/field edges equal to the pixel, 7-width ellipsis sweep, D6 green. And R2's claim now has direct evidence: live-workout 60 committed PNGs / 60 executed Nothing else moves until Ilya clears the contact sheet. 🤖 Generated with Claude Code |
…yout Gate 2's re-record, cleared by Ilya after contact-sheet review. Exactly the 36 PNGs predicted in Phase 0 and approved at the gate — SessionState 20, SessionScreen 2, PastSession 14 — and nothing else: the record run's changed set was diffed against the approved list before this commit was written. The other 410 baseline snapshots verify byte-identical. What moved, and why: the unit glyphs left the fields for the column header, so every value box widens and the direct-row frames shift; the expanded-card frames additionally grow by the header row (+61px canvas under SHRINK); "102.5" returns to 26sp where its box fits it, per the measured stepdown. verifyPaparazziDebug over the full corpus: exit 0, 2186 testcases, 0 red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
Review round 1 — classification (posted before the fixes, per AGENTS.md § "Merge flow")Two open threads, both from 1. 2.
The remaining five-digit cells are deferred, not resolved — registered as blocker B-8 in the spec and tagged in both gates' Fixes for (1) push next; CI result and thread replies follow. 🤖 Generated with Claude Code |
…8 applied Review finding, accepted: a KDoc paragraph recounted what existed before SetRowGeometry and why the prior duplication failed. The finding is systemic rather than local — a sweep of the comment text this branch adds found category-3 HISTORY in sixteen files, so all of them are treated here rather than the one flagged line. Deleted: ruling ids, round numbers, "the first cut", "replaces the old X", "predates", accounts of what a value used to be and of a test that was tried and removed. Collapsed to conclusion plus citation: the width budget, the ledger, the contrast arithmetic — all of which already live in documentation/feature-specs/set-field-column-headers.md and are now cited rather than restated. Kept, in the present tense, every guard that stops a specific wrong edit: that a fixed index width breaks the grown column, that splitting the header's AnnotatedString breaks the truncation order, that no rung may go below the contrast floor, that the field's own layout result cannot report its clipping, that an onGloballyPositioned node here would cost a dispatch per scroll frame, that the harness must never gain a snapshot handler. Comments only: every changed line in the diff is comment or KDoc text. Repo-wide testDebugUnitTest exit 0 (1732 tests), detekt exit 0 with zero findings and zero suppressions, lint exit 0, verifyPaparazziDebug exit 0 over the full corpus — the sweep moves no pixels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
|
@codex review |
|
@claude review |
|
Claude finished @stslex's task in 2m 22s —— View job Review complete
This is a thoroughly-instrumented change, and the two prior One real, if minor, finding:
internal fun buildSetColumnHeaderLabel(name: String, unit: String?, unitColor: Color): AnnotatedString =
buildAnnotatedString {
append(name.uppercase())
...
append("(${unit.uppercase()})")Kotlin's no-arg Everything else checked out:
No blocking issues found. The one finding above is a wording nit in a comment, not a functional defect — your call on whether it's worth a follow-up commit or can ride with the rest of the branch's known, disclosed state (R25/R26). |
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Review finding, accepted: the header's casing KDoc called the no-arg String.uppercase() "locale-aware". It is the opposite — the overload maps through Locale.ROOT, which is why Kotlin 1.5 introduced it to replace the locale-sensitive toUpperCase(). The behaviour was always right; the comment described it backwards, which is worse than silence for the next reader. Reworded in the two KDoc sites and the two spec sentences, and the invariance is now stated as the property this component WANTS: a label whose casing shifted with the device locale would let a golden and a user's screen disagree. A locale-sensitive pass would also be wrong for Turkish in the other direction — noted in the review thread, not papered over here. Comments only; tests, detekt and the full golden corpus all exit 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
6fdf09e to
3a67291
Compare
Review round 2 — 2 reviewers, 1 finding, classified and fixed@codex — "Didn't find any major issues 👍" on @claude — Reproduced and confirmed: Kotlin's no-arg Fixed in all four sites the branch owns — two KDoc blocks in On the Turkish half of the note: agreed that One neighbouring instance, flagged not touched: Also disclosing a mistake of mine in this round: the fix commit initially swept in Verification, exit codes read directly (no pipes): kit + live-workout tests Re-requesting both reviewers on the new head once CI reports. 🤖 Generated with Claude Code Correction to this comment: it first cited |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
@codex review |
|
@claude review |
|
Claude finished @stslex's task in 52s —— View job Review complete
ResultHEAD is still Confirming the round-1 finding was fixed correctly. My earlier note was that
No new findings. Everything else stands from the round-1 review: Nothing blocking. The two open items remaining are process/product decisions for @stslex, not code defects: the R4 band ruling (spec §7, five-digit reps debt/B-8) and the Gate 2 contact-sheet clearance for the 36-golden re-record. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a67291f2e
ℹ️ 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".
…he columns Review finding, reproduced and accepted: PersonalRecordTag and AppSetTypeChip share a 34dp MINIMUM, not a width. Measured in layoutlib, the tag renders 94px at fontScale 1.0/1.3/1.6 and 101px at 2.0 — its label outgrows the minimum. A header gutter and a sibling row pinned to 34dp therefore under-reserve for a record row at large text scales: the gate now records the reps label at 417px against its value at 413px, a 4px drift, on the exact case every fixture missed because both components sit at the minimum at fontScale 1.0. SetRowGeometry.resolveTrailingSlotWidth measures the tag's own label, style and padding (resolved in PersonalRecordTag, beside the values it measures, so the two cannot drift) and takes the max with the shared minimum. Both rows size their trailing slot from it and both headers reserve it, so chip rows, tag rows and the header agree at every scale — the leading-side fix of D3 applied to the trailing side. Gate extended along the two axes that were blind: font scale, and WHICH trailing component the row draws. Proven both ways — red before the fix on the record@2.0 case only, green after, with fontScale-1.0 geometry byte-identical. recordPaparazziDebug over all three snapshot modules reports ZERO changed PNGs: the goldens render at 1.0, where the measured slot equals the old minimum. Repo-wide tests 1732/0, detekt 0 findings, lint 0. Spec §7's ledger figures re-measured for the narrowed 2.0 slots. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i
1316232 to
cd6427d
Compare
|
@codex review |
|
@claude review |
|
Claude finished @stslex's task in 1m 47s —— View job Review complete
ResultOne new commit since my last review: The fix is correct and closes the loop properly:
Minor, non-blocking observation: No blocking issues. This closes out the second and last open reviewer finding on the branch (the codex history-KDoc finding and the "locale-aware" wording finding were already resolved in earlier rounds). The two remaining items are the process/product decisions already on record for @stslex: the R4 band ruling (spec §7, five-digit-reps DEBT/B-8) and Gate 2 contact-sheet clearance for the 36-golden re-record. |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
…teDriver (#239) **KMP phase 6, increment D: `core:data:database` becomes a KMP module, on `AndroidSQLiteDriver`.** The centrepiece of phase 6's second half (#232 carried the spec; #235 was this PR's step 1). The port only — the `BundledSQLiteDriver` flip is deliberately NOT here; it follows as its own PR with its own gate, so a SQLite regression and a source-set regression stay distinguishable under bisect. **The schema did not move: the regenerated `6.json` is byte-identical, `identityHash 9a848458f02ef4c4ace9841b9f2e3d42`.** ## The shape — five commits, bisect-green each 1. **`epochToIso` leaves `java.time`** — `kotlin.time.Instant.fromEpochMilliseconds(ms).toString()`, the zero-new-dependency stdlib replacement, measured byte-identical against `java.time` for whole-second / zero / fractional / negative epochs. Pinned twice: new literal-string tests, and `DatabaseJsonExporterImplTest` keeps `java.time` on the test side **deliberately** as a cross-library oracle. Mutation M-ISO1 (`fromEpochSeconds`) reds 4 tests — the epoch-zero pin alone survives, since 0 s and 0 ms name the same instant; reverted. This makes `WorkoutExportMapper` commonMain-eligible: androidMain ends at 4 files, not the spec's 5. 2. **Fifteen reflective builder sites take the generic form** — `Room.databaseBuilder<AppDatabase>(context, name)` etc., while the module is still Android-only (the reified overload falls back to reflection until `@ConstructedBy` exists — so this commit is pure mechanics, bisectable on its own). The 16th site was already the Room 3 KClass form; the spec's total said 15, measured 16. Phase-7 note: with `Class<T>` gone, a bare `ApplicationProvider.getApplicationContext()` argument deadlocks inference against the builder's factory overloads — pin as `getApplicationContext<Application>()`. 3. **The device-test component and its two CI names** — `withDeviceTest` unconditional in the KMP convention (classic-AGP parity: every library carries the component whether or not sources exist) plus `assembleDebugAndroidTest → assembleAndroidDeviceTest` **and** `connectedDebugAndroidTest → connectedAndroidDeviceTest`. **The second alias is a finding, not a plan item**: `ui_tests.yml` invokes `connectedDebugAndroidTest` literally, so the pre-diagnosed assemble alias alone would build a device-test APK the weekly suite never runs — the **fifth** silent-vanish instance of this arc. Proven both directions: on the parent commit both repo-wide graphs contain **0** `*AndroidDeviceTest` tasks; with the commit, both are fully wired for every KMP module, and an **empty** KMP device-test APK runs green on-device under the Regression annotation filter (the exact ui_tests.yml invocation), so the three source-less KMP modules cost the weekly suite nothing. `RoomLibraryConventionPlugin` keeps one plugin id and branches (KMP: room bundle + `paging-common` in commonMain, `sqlite-framework` in androidMain, `kspAndroid`/`kspIosSimulatorArm64`, room-testing on the device suite). 4. **The conversion** — 51 files commonMain / 4 androidMain (`buildAppDatabase`, `snapshot/`×3: Context, framework SQLite, `java.io.File`), 26 test classes → `androidHostTest`, 5 → `src/androidDeviceTest/kotlin`; `@ConstructedBy` + a **bare** `expect object AppDatabaseConstructor` (see below); detekt.yml gains `**/androidDeviceTest/**` beside every host-test glob, in this commit because that is when the sources exist (spec §3.4). 5. **The spec records what PR D measured** — new §9 in `kmp-phase-6-data-layer.md`; two of the spec's own §3 claims dated in place. ## What the first instrumented conversion surfaced — each measured, each fixed at the root - **Room-KMP puts no schemas on the device-test APK.** All 7 `MigrationTestHelper` tests failed on device, loudly: `FileNotFoundException … Missing file: …/5.json`. The classic integration's androidTest-assets copy has no KMP counterpart, so the Room convention's KMP branch wires `schemas/` as a static device-test asset via the variant API — **which is null until `androidResources.enable = true`**: AGP-KMP defaults the entire asset pipeline off (measured: `sources.assets == null` before, six schemas inside the APK after). - **`checkAndroidDeviceTestAarMetadata` fails on the first KMP device APK that depends on an Android-convention AAR** ("requires core library desugaring"). The KMP convention now mirrors `configureKotlinAndroid`'s repo-wide desugaring (flag + `desugar_jdk_libs`). - **Lint does not classify KSP output as generated on KMP.** `checkGeneratedSources = false` is already set and honored on classic modules; here a consumer with `checkDependencies = true` red **1087 RestrictedApi + 6 SyntheticAccessor — 100% under `build/generated`, zero in handwritten code**. Encoded in `lint.xml` as per-issue `**/build/generated/**` ignores. Mutation M-LINT1 (break the path) brings back exactly 1093; reverted. - **The repo's own gates policed this PR, twice.** `NoActualForExpectSuppressionRule` rejected the `NO_ACTUAL_FOR_EXPECT` suppression I first wrote on the expect object — and it is right: the metadata compilation never demands an actual, the platform compilations get Room's generated ones, and without the suppression a codegen failure reds the build instead of vanishing. And detekt, now seeing device tests through the KMP source-set model, surfaced two pre-existing `Wrapping` violations in `AtomicRollbackDeviceTest` — **`src/androidTest` was never full-detekt-checked on classic modules** (default sources are `src/main` + `src/test`), the same shape as #235's testFixtures hole. - **Room 3 schema export is write-on-diff** — the processor writes into the plugin's `schemaOutput` only when the generated schema differs from `schemaInput` (= the committed `schemas/`), so a production build can never silently rewrite the schema directory. The §6 gate ran as prescribed: schemaInput redirected to an empty dir via an out-of-tree init script, fresh `6.json` exported, byte-identical. - **`setQueryCoroutineContext` deliberately NOT added** (deviation from spec §3, argued): the Android builder runs without it today, adding it would change which context Room queries use — not "the port only" — and nothing constructs a database off-Android until phase 7's iOS composition root, which is where it belongs, next to iOS's driver choice. - One commonMain-ism: `String.toByteArray(Charsets.UTF_8)` is JVM-only → `encodeToByteArray()` (one site). ## A counting correction, in the open The repo-wide unit-test invariant number (2352 at #235) was produced by a `test-results/testDebugUnitTest` glob — which **never saw KMP modules' `testAndroidHostTest` results** (core:core 32, dataStore 5). Execution was never affected (CI's alias runs them); only the counting was blind, on both sides of every comparison, so past deltas stayed honest. From this PR on the number is reported in both scopes. ## Gates All `--rerun-tasks --no-build-cache --no-configuration-cache`; baseline measured on dev@c81c4490 the same way. | Gate | Result | |---|---| | full forced gate (assembleDebug testDebugUnitTest verifyPaparazziDebug lintDebug assembleDebugAndroidTest) | ✅ `3248 actionable tasks: 3248 executed`, 0 failed (dev baseline: 3080/3080; the +168 is the KMP module's task set + device-test components) | | detekt (separate invocation) | ✅ `56 actionable tasks: 56 executed` | | unit tests | ✅ **2230 tests / 230 classes** (`testDebugUnitTest` scope) + **165 / 33** (`testAndroidHostTest` scope) = **2395**, 0 failures, 0 skipped, 0 stale XMLs. Accounting vs 2354 baseline: +4 new epochToIso pins, +37 previously uncounted KMP host tests, −0 lost; database's 128 moved scope, not existence | | device suite | ✅ **`Starting 28 tests on Pixel_6_API_34(AVD)`, 28/28 green** — via the `connectedDebugAndroidTest` alias under the exact ui_tests.yml Regression annotation filter; red direction observed live (7 migration failures while schemas were missing from assets) | | instrumented suite gate | ✅ `:core:data:database: 5 instrumented source files, 206 classpath entries scanned, 0 missing` — resolved through `androidDeviceTestRuntimeClasspath` | | schema identityHash | ✅ regenerated-from-empty-input `6.json` **byte-identical** to committed; `git status schemas/` empty | | iOS | ✅ `compileKotlinIosSimulatorArm64` + `kspKotlinIosSimulatorArm64` + klib in the same repo-wide graph (the `assembleDebug` alias pulls them) | | aliases both directions | ✅ parent commit: 0 `*AndroidDeviceTest` tasks in either repo-wide graph; this branch: fully wired | | goldens | ✅ **456 on disk, 0 touched** (446 in older docs is stale — #236/#237 added 10 on dev before this branch) | Stacked work: #240 (the `BundledSQLiteDriver` flip) sits on top of this branch; PR E (`core:data:exercise` → KMP) will stack on #240. Waiting on review here does not block that work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_011kNU4XxzXp6NpCnVUQA6yH
Implements set-field-column-headers.md (spec on
dev,b366ee8c). Ten commits; every gradle proof ran with--rerun-tasks --no-build-cache --no-configuration-cache, detekt as separate serial invocations.Correction: the branch is NOT bisect-green, and this PR no longer claims it is
An earlier revision of this description said "bisect-green commits". That is wrong and is withdrawn. Each commit is green for unit tests, both overflow gates, the alignment gate, detekt and lint — but
verifyPaparazziDebugexits 1 on every commit from the suffix removal (56370b5) onward — seven commits, not one — because the code that moves the render was deliberately sequenced before the goldens that record it (Gate 2 exists precisely so the executor never re-records unilaterally). Making the branch genuinely bisect-green would mean folding the 36 PNGs into the commit that moved the render: a full-history rewrite, not the R25 squash that landed. Under a squash-merge,devnever sees those commits and the invariant holds by construction; under a merge-commit it does not. Ilya decides at merge.What changed
SetColumnHeader(kit): oneAnnotatedStringin oneText— nametextSecondary, unit atextDimtail span, locale-awareuppercase()in the component; ellipsis order proven by test (unit cut, name intact at 150/130/110/90px).SetRowGeometry(kit): D3's single source — index minimum, the liftedWEIGHT_COLUMN_FLEX, the chip-slot reference, andresolveIndexColumnWidth(measured through the real text stack, survives fontScale). Containers resolve once; header and rows grow together at 10+ sets (exerciseTenSetscanary).SetsColumn+CardBody;.height(48)→.heightIn(min = 48)(proven pixel-neutral against 100 goldens before wiring).AppNumberInput.accessibilityLabel→semantics { contentDescription }(theAppTextFieldtemplate); rows announce "вес, килограммы" / "повторения"; RobolectricrunComposeUiTestasserts it.isError → error()deliberately NOT added (R5 veto, spec blocker B-2).MAX_GLYPHS_AT_FULL_SIZEdeleted. Acyclic (slot is flex-driven). PlanSetCard ripple pinned by the newreadOnlyFiveGlyphWeightgolden (R3); its 16 existing PNGs byte-identical.AppNumberInputwidth-budget KDoc now carries the measured figures (18.2dp/digit, 81.12dp for "102.5", 68.38/35.75dp boxes).unit_reps_fullremoved (its only two consumers became the header).The gate (R1/R2, proven both ways)
OverflowGateSdkdrivesPaparazziSdkwith a discarding frame consumer — no snapshot handler, zero PNGs, non-golden packages;assertGoldenLivenessnever sees it (full corpus run confirms). Slot widths are captured from the rendered production rows viavalueSlotProbe(theflashAlphaOverridemove).SIZE_TITLE_SP26→40 grew failures 33→41; 26→12 cleared every title-rung cell (33→22; the residue is the section rung's own).CHIP_MIN_WIDTH34→60 from the green baseline: gate red (reps slot 187→155px, 5-glyph +25px) — the loop is closed.⛔ STOP GATE 2 — 36 moved goldens, prediction matched exactly
Full corpus (
verifyPaparazziDebug, all 13 modules, rerun flags): 36 failing golden verifications, exactly the predicted set, zero divergence in either direction:SessionStateGoldenTest20/30 — direct rows (suffix glyphs gone, boxes widen; "102.5" returns to 26sp) + 3 expanded-card cases (header row inserts)SessionScreenGoldenTest2/2 — expanded pe-2 card in framePastSessionGoldenTest14/30 — expanded cards + direct rowsAll other 410 PNGs verified byte-identical (incl. every
PlanSetCardconsumer and the kit corpus). Not re-recorded — your call. Reproduce:./gradlew :feature:live-workout:recordPaparazziDebug :feature:past-session:recordPaparazziDebugand read the image diff; the six new snapshots in this PR are first recordings, not re-records.⛔ R4 band ruling needed — the ladder floor loses 8 cells (spec §7 ledger)
With the stepdown live, the measured 64-cell matrix passes 56/64. Every failure exceeds even the contrast-pinned 19sp floor (which the non-linear converter scales ×~1.695 at fontScale 2.0): reps×5-glyph fails the hard band at 1.3/1.6 (live) and 1.6 (past); at 2.0, four cells clip beyond the single sanctioned weight/5 limit (incl. live reps×3 by 2px). Options in spec §7: (a) cap reps input length, (b) sub-19sp rung for non-record/non-pending states, (c) extend the known-limit ledger to all 8 cells, (d) Phase-7 row re-layout. The gates assert the proven fontScale-1.0 band until you rule; nothing was carved out silently.
Commit-plan deviation
Planned commit 3 (bodyweight branch) had no code left —
SetColumnHeader'sisWeightedbranch was the single implementation point — so it shipped as the RUПОВТОРЕНИЙfixture alone.🤖 Generated with Claude Code
https://claude.ai/code/session_018TGniHCT2ZMpUw1fy2ku2i