The current arc of work. Updated when the arc changes, not every session. For session-by-session state, see HANDOFF.md.
Make the Data Differences Tool presentable enough to hand a CEO, CFO, or consultant a naked link with no explanation needed.
v1.0 is functionally complete. The gap is first-impression credibility: no favicon, no link preview, no branding, and the /improve audit is overdue. This is a 30–60 minute pass, not a feature arc.
- Add Lailara favicon (SVG 32×32 + ICO fallback)
- Add to
index.html<head> - Apple touch icon 180×180 PNG (needs conversion tool — deferred)
-
<title>: "Data Differences Tool — Lailara LLC" -
<meta name="description"> - OG tags (og:title, og:description, og:type=website, og:url)
- Twitter card (summary + title + description)
- "Built by Lailara LLC" text link in footer
- Links to lailarallc.com
- Subtle — warm-gray, underlined, centered
- Full click-through on desktop: upload CSV pair, XLSX pair, mixed
- Verify auto-detect copy is clear
- Verify download buttons work (Excel + CSV)
- Check mobile viewport (iPhone SE width minimum) — zones stack, no overflow at 375px
- Confirm fonts load (Playfair Display headings, Source Sans 3 body)
- Run /improve (full pass, 2026-07-27) + full code review + UI review
- Fix P1 findings (CSV formula injection)
- Fix P2 findings that were safe (perf, export errors, warnings, stale auto-detect, dead reordered data)
- Blank-key matching semantics: deliberately NOT changed — intentional + tested. See open question in Improvement History.
-
npm run deploy - Verify live at diff.lailarallc.com
- Spot-check OG preview (paste URL in Slack or https://metatags.io)
Added 2026-07-28 by the FIX-LIST cross-repo test sweep. it.fails is vitest's
strict xfail: the suite fails loudly the moment the fix lands and the marker
has to come off. Do not remove a marker without doing the fix.
- Identical verdict ignores column changes. Fixed 2026-07-28: the gate
now consults
columnChangesand leads with the column finding. Both un-pinned tests are live, plus three new cases.src/lib/summary-generator.ts:13— the identical-verdict gate checks only added/removed/modified row counts, so a file whose rows all match but which gained a column reports "Files are identical. No differences found." Addresult.columnChanges.length === 0to the gate and lead with the column finding ("No row values changed. 1 column added: discount."). The same sentence is written to Excel atsrc/lib/export.ts:60— fix both in one commit. Tests:tests/lib/summary-generator.test.ts—does not claim identical when a column was added but no row changed,leads with the column finding when rows match but columns changed.
Seven of eight findings fixed this session, one commit each.
- [Critical] Identical verdict ignores column changes (item 7 above).
- [Medium] "Files are identical" overclaims — now "No material differences after tolerant matching." See DECISIONS.md.
- [Medium]
wasNormalizedrendered nowhere — now tagged in the expanded row table and carried as aNormalizedcolumn in both exports. - [Medium]
dayjs(str)formatted the fallback date parse in local time — nowdayjs.utc(str). - [Medium] Test case for zero row changes with non-empty
columnChanges(plus three more). - [Low] Column type inferred from at most 100 rows — now every row.
- [Low] Footer used London-40; now London-35, the footer token.
- [Low] README test count, CLAUDE.md "Primary language: TBD".
- [Product gap] No figure is denominated in money and no column is flagged as financial. Summing the numeric delta on detected currency columns would close the largest CFO gap. NOT started — this is a feature, out of scope for the v1.1 polish arc. Candidate for the next arc.
Known since 2026-05-22, recorded only as a doc caveat until now. Filing it as work: a documented-but-untracked defect has the same half-life as a prevention rule that was written and never widened — which is exactly what this project just spent a session learning.
-
src/lib/normalizer.ts:61— SheetJS parses slash-format dates as local midnight. East of UTC, local midnight is the previous UTC day (midnight Jan 15 in Tokyo is 15:00 Jan 14 UTC), so theDate-object branch'sgetUTC*extraction returns the wrong calendar date there. Negative offsets are unaffected, which is why it survived: every test and every session has run in US Eastern. Both string branches are already UTC-safe (verified underTZ=Asia/Tokyo); this is the last branch that is not. The fix means not trusting SheetJS's timezone anchoring at all — likely reading the raw cell text rather than the parsedDate. Add aTZ=Asia/Tokyocase to the suite as part of it.
A ship date of 2024-02-10 displays as "Fri Feb 09 2024 19:00:00 GMT-0500" —
wrong day, in a finance-facing report. The differ stores the raw cell value
and both the UI and the exports call String() on it.
-
src/components/RowChanges.tsx:95(old/new value cells) andsrc/lib/export.ts:38— formatDatevalues asYYYY-MM-DDfor display. The normalizer already has the UTC extraction logic; the display path never used it. Add a test for the export row content. Done 2026-07-28. A third surface the entry did not name:src/lib/summary-generator.ts:153stringifies the same values to build the plain-English transition sentence, so the summary carried the bug too. All three now callformatCellValueinsrc/lib/display.ts; 9 tests added. Display is UTC-anchored and therefore inherits task 9's east-of-UTC exposure by design — when 9 lands, screen, exports and summary all move with it rather than disagreeing.
- Link preview in Slack/iMessage shows title + description (not blank)
- Favicon visible in browser tab
- Footer credits Lailara
- No visual regressions from v1.0
- /improve audit current
- New features (v1.1 is polish only)
- SheetJS replacement
- Analytics / tracking
- Auth or persistence
When an arc completes, archive its goal, completion date, and outcome here. Then start a new arc above. Provides continuity without bloating the active plan.
- Outcome: All 12 definition-of-done items complete. App live at https://diff.lailarallc.com/. First /improve pass completed (8 fixes). 70 tests, 297KB initial bundle.
- Tag: v1.0
Track when this project was reviewed and improved via /improve. Each entry records what was found, what was fixed, and when to check again.
- Trigger: User-initiated combined review. Goal: confirm the tool is CEO/CFO-ready, self-explanatory in 30s, and that recent Claude Code work is correct (user reported quality concerns lately).
- What was reviewed: 5 ce personas over src/ (correctness, maintainability, testing, TypeScript, security) + ui-review-skill against the live site. Baseline healthy: clean tree, 100 tests, tsc + build clean, in sync with remote.
- UI verdict: Passes the 30-second executive test — headline, tagline,
concrete use cases, and the example-result card make the job obvious. Live
site renders on-brand (warm canvas, Playfair, Lailara footer). Most ui-review
FAILs were noise (checker measured
<body>; credential/staleness hits were in minified dist/ bundles) — tightened review.yaml so future runs are clean. - What was fixed (11 commits):
- [correctness] Date-typed KEY columns rendered as raw JS Date strings on screen + both exports (RowChanges ×2, export.ts formatKey) — the same defect 16537dd fixed for cell values but scoped to the repro, not the surface. Now routed through formatCellValue. +test.
- [correctness]
wasNormalizedtagged genuine cased text changes as "formatting normalized" in case-insensitive mode. +test. - [correctness] Two all-blank columns scored as a rename (jaccard 0/0→1). +test.
- [correctness] Numeric comma-strip collapsed "1,2,3"→123 (false match); now strips only valid thousands separators, shared helper. +tests.
- [correctness] CSV lone-CR not quoted → row-boundary corruption. +tests.
- [type-safety] Removed 3
as anycasts in differ.ts (typed metadata as ColumnMetadata); modeled ColumnChange as a proper discriminated union. - [testing] exportToExcel had ZERO tests + two false-confidence CSV tests (asserted blob.size>0 only) — added real read-back assertions. 100→112.
- [security] Bounded decompression-bomb cell counts + long-header Levenshtein cost (both self-DoS, tab-only). +tests. Final: 115 tests.
- [UI] Darkened green/amber result tokens to WCAG AA (green 3.6→5.5:1, amber 2.3→4.7:1) using documented DS steps (HK-25, SG-35). Browser-verified.
- Security posture: Strong. No Critical/High. CSV formula-injection neutralization verified complete. No XSS/innerHTML/eval.
- Deferred / not touched: SheetJS xlsx@0.18.5 CVEs (accepted, client-side); east-of-UTC Date-object branch (PLAN task 9, still open); blank-key matching (intentional). NOT pushed/deployed — local commits only, awaiting user.
- Next review: 2026-08-30 (30 days; stable after this pass).
- Trigger: User-initiated combined review; /improve audit was overdue (due 2026-06-19)
- What was reviewed: Full src/ code review (background agent), security, UI/first-impression clarity, tests, git hygiene, deps. Baseline healthy: clean tree, 75 tests, TSC clean, no secrets, design system correct.
- What was fixed (9 items, 7 commits):
- [P1] CSV formula injection — exported report cells beginning with =,+,-,@ could execute in Excel; now neutralized with a leading apostrophe (plain negatives untouched). +6 tests.
- [P2] Rename-detection O(n²) — B column value-sets rebuilt per iteration; now precomputed once.
- [P2] Silent export failures — added catch handlers + inline error message.
- [P2] Missing key column in File B — now warns instead of silently collapsing all rows.
- [P2] Duplicate-row warning count — now reports rows dropped (agrees with excludedRowCount).
- [P2] Stale key auto-detect — ColumnPicker re-runs detection when files change.
- [cleanup] Reordered column change — removed misleading dead indices.
- [UI] Landing copy — now states audience + stakes for the 30-second test.
- [UI] Example result card + mobile stacking (verified no overflow at 375px).
- Deferred / not touched: SheetJS xlsx@0.18.5 CVEs (already an accepted, documented decision — client-side blast radius). Row-cap-after-parse (low value). Full real-file click-through QA (browser pane couldn't upload files this session).
- OPEN QUESTION — blank-key matching: The code review flagged that two unrelated all-blank-key rows (one in each file) get matched and reported as "modified." This is currently INTENTIONAL and encoded in differ.test.ts ("treats null/empty key values as empty-string key") with excludedRowCount as the honesty mitigation. NOT changed unilaterally. Decide next session: keep as-is, or make blank composite keys non-matchable (would rewrite that test).
- Next review: 2026-08-26 (30 days; project stable after this pass)
- Trigger: First-ever /improve run (project was overdue)
- What was reviewed: Security (automated), code quality, dependencies, tests, docs, git hygiene
- What was fixed:
- Fixed Excel serial date offset bug (off-by-one, was 25570 should be 25569)
- Added 50MB file size limit on uploads
- Added 200K row count cap to prevent UI freezes
- Added magic-byte validation for XLSX files
- Code-split SheetJS and ExcelJS into lazy-loaded chunks (1.5MB → 297KB initial bundle)
- Error boundary now shows generic message, logs internals to console
- Updated README from stub to full documentation
- Documented SheetJS vulnerability acceptance in DECISIONS.md
- Deferred: SheetJS replacement (significant refactor, low practical risk for client-side tool)
- Next review: 2026-06-19