Branch: flow-real-types → main (rebased onto post-#767 main, now mergeable)
Scope: 167 files changed, 7 commits
| Metric | Before (#767 merged) | After #769 |
|---|---|---|
flow check errors |
0 | 0 |
Suppressions ($FlowIgnore / $FlowFixMe) |
560 | 18 |
| Jest | 200 suites / 4,644 | 200 suites / 4,644 |
97% of remaining Flow suppressions removed. Of the 18 left, 3 aren't real suppressions at all (two sit inside commented-out code; one is the literal text $FlowIgnore[prop-missing] inside a prose comment in __mocks__/asNPTypes.js). So 15 live suppressions remain, each narrowed to a specific error code and carrying a comment explaining why it's still there.
The rebase dropped 6 commits that are now redundant (the multi-level indent fix and its docblock, both shipped in #770; the insertTodos revert and its correction, now obsolete; and a [DIAG] logging add/remove pair that nets to zero).
This is the majority of the PR, and the claim is mechanical rather than a matter of judgement.
118 of 167 files (71%) produce byte-identical JavaScript.
Babel erases Flow types at build time, so a change that only touches annotations cannot alter runtime behaviour. scripts/flow-emit-audit.js proves this per-file: it runs the Babel transform on each changed file at main and again on this branch, and diffs the emitted JS. For 118 files the output is identical byte-for-byte.
A further 4 files differ only by a bare ; — an empty statement, a no-op:
helpers/react/reactDev.jsjgclark.Dashboard/src/react/components/Section/useSectionSortAndFilter.jsxnp.Shared/src/react/Root.jsxnp.Templating/__tests__/NPTemplateRunner.test.js
The repo omits semicolons, so a statement starting with ( continues the previous line. Inline casts at statement start must be written ;(foo: any).bar(). That leading ; survives type erasure as an empty statement.
So 122 of 167 files carry no behavioural risk whatsoever, and this is verifiable by re-running the audit rather than by trusting review.
45 files, 489 changed lines of emitted JavaScript. I reviewed 100% of those lines at the emitted-JS level — not the source diff. That matters: emitted JS has comments and type annotations stripped, so nothing can hide in a plausible-looking comment, and a "type-only" change that isn't shows up immediately.
The 45 files break into three tiers.
Verified equivalent, usually by checking the thing the change depended on rather than assuming:
| File | Change | Why it's safe |
|---|---|---|
helpers/dev.js |
timeEnd - timeStart → Number(timeEnd) - Number(timeStart) |
Number() is exactly what - does to each operand. Important: .getTime() would have been wrong — helpers/promisePolyfill.js:161,183 pass Date.now() (a number) into a param annotated Date, so .getTime() would throw. |
helpers/note.js, helpers/NPnote.js |
6 sorts → Number(a) - Number(b) |
Same reasoning. An agent originally wrote .getTime() here; I reverted that because it adds a crash path the original - never had. |
dwertheimer.EventAutomations/src/config.js |
validateConfigProperties now gets a copy, result discarded |
Verified it only reads config[v] — no writes anywhere in the function. |
dbludeau.TodoistNoteplanSync |
setters write setup.x not this.x |
Verified setup is never spread or copied, so this was always setup. |
helpers/urls.js, jgclark.SearchExtensions/src/searchTriggers.js |
while ((m = re.exec(s))) restructured |
Same exec call order and count. |
dwertheimer.TaskAutomations/src/NPTaskScanAndProcess.js |
hasOwnProperty → truthiness |
Values are always non-empty arrays; [] is truthy anyway. |
dwertheimer.Forms/src/formSubmission.js |
for…in + hasOwnProperty → Object.keys; Function.apply → new Function |
Identical semantics. |
jgclark.SearchExtensions/src/flexiSearch.js |
prefs wrapped in String() |
Verified the values are only substituted into template strings, never used in boolean context ("false" would be truthy). |
helpers/config.js, helpers/NPConfiguration.js, helpers/content.js, dwertheimer.TaskSorting/src/tagTasks.js, np.Templating/lib/support/modules/data/service.js |
local refinements, const destructuring, explicit branches |
Equivalent including null/array edge cases. |
These do change behaviour. In each case the previous behaviour was a crash, an exception swallowed into a misleading message, or a silently wrong value.
Three functions that did not exist — confirmed absent by grep, not inferred:
| Call site | Was | Now |
|---|---|---|
np.Templating/src/Templating.js |
NPTemplating.getTemplate() — threw a TypeError every time, so meeting-note templating was broken |
getTemplateContent() |
np.Templating/lib/TemplatingEngine.js |
this.getTemplateConfig() — no such method |
this.templateConfig |
np.Templating/lib/rendering/index.js |
re-exported renderTemplate — resolved to undefined |
renderTemplateByName |
Crashes:
helpers/NPParagraph.jsparagraphMatches()called.startsWith()on non-string fields (lineIndex,indents,date) → TypeError. Now type-guarded.np.Tidy/src/tidyMain.jsremoveBlankNotes()compared against the string'undefined', so the guard was always true and a note with undefined content threw mid-filter.jgclark.Dashboardmove-day/move-week handlers wrotep.contentthen usedp.note, which really is?TNote— a genuine null dereference the suppression was hiding.np.Templating/lib/support/modules/quote.jsresolvedundefinedout of aPromise<string>that callers assign toconst verse: string.
Silently wrong values:
jgclark.Dashboard/src/reactMain.jsreturned{}on error, so every setting read asundefined. Now returnsnulland the caller keeps the loaded settings.helpers/NPWindows.jsgetStoredWindowRect()returned a malformed preference as a fakeRect.dwertheimer.FormsFormViewpassed a tuple-takingsendToPlugininto context.np.Templating/lib/support/modules/prompts/handlers/index.jscalledpromptDateInterval('', message, default)against a(message, defaultValue)signature — the user saw an empty prompt and the default was dropped.helpers/NPDateStrings.js— anullweek threw, and the outer catch discarded the entire relative-date list. Now that one week is logged and skipped.
These are the only places where reasonable people could disagree. Everything else is either provably inert or a fix to something demonstrably broken.
-
jgclark.SearchExtensions/src/searchHelpers.js—makeAnySyncs()returnsinputinstead ofnullon error. (@jgclark's call.) The old behaviour wasn't a design:nullescaped aPromise<resultOutputType>intosaveSearch(), which reads.resultCountand threw — the whole search visibly failed. Now a failure while adding blockIDs costs only the sync links. The alternative (rethrow, so the user gets an empty result set) is equally defensible. -
Heading levels clamped to 1–8 (
helpers/userInput.js,helpers/paragraph.js). A computed level of0,NaNor12previously went raw toinsertHeading(); it's now rounded and clamped (NaN→ 2). Only affects out-of-range input. -
dwertheimer.TaskAutomations/src/NPOverdueReact.js— field whitelist.para[field] = row[field]became explicittype/contentbranches; anything else logs an error and is skipped. Verified only those two fields are ever sent, but a third would now be dropped rather than blind-assigned. -
helpers/sorting.jsfieldSorter— two non-Dateobjects now compare equal. I found this during review; the agent's claim that only strings/numbers reach that line was wrong.isNaN()coerces, so Dates and booleans still become numbers — only plain objects are affected, and the fields actually sorted arecontent/index/date/priority. Very unlikely to fire, but it is a divergence neither Flow nor the tests would catch. -
dwertheimer.EventAutomations/src/events.js— now skips untitled/undated events. Worth knowing but not testable: its wrappers are exported but appear in neitherindex.jsnorplugin.json, so the command is unreachable.
Four independent mechanisms, in decreasing order of strength:
- Emitted-JS audit (mechanical, re-runnable).
node scripts/flow-emit-audit.js --base origin/main— proves 118 files cannot behave differently. This is the single most important artifact in the PR: it converts "trust the review" into "check the tool." - Full test suite. 200 suites / 4,644 tests green. Note this is a floor, not a ceiling — coverage is thin in exactly the plugin code these changes touch.
- Line-by-line review of the entire behaviour-changing surface. All 489 emitted lines across 45 files, not a sample. Six changes looked like defects and were chased individually; five turned out fine (and could only be known so by checking), one was a real divergence (Tier C item 4).
- Dead-suppression sweep. Placeholder substitution — swap each suppression for a same-line-count placeholder so no line numbers shift, run Flow, see which covered lines actually error. Confirms no remaining suppression is inert (an inert suppression is worse than none: it hides future errors on that line).
Stated plainly, because it calibrates how much to trust the rest:
- I claimed a Babel upgrade would unlock blocked syntax. Wrong — tested 4 versions, all fail. Corrected with a tested matrix.
- An agent's
chroma-jschange traded 3 errors for 5. Caught by re-measuring; reverted. - A
checkType.jsmapped-type rewrite turned 19 suites red — Babel can't parse mapped types at any version. Reverted. - Three incidental behaviour changes slipped into a "type-only" commit. Caught by my own emit audit, not by review.
- I twice attributed observed indent-flattening to a code path that never executed, and wrote the false conclusion into a commit message. Caught by @dwertheimer's logs; the fix that survived is the one he insisted on re-applying.
The pattern: the mechanical checks caught what review missed. That's why the audit matters more than my summary.
Ordered by value. Items 1–2 are the ones I'd actually insist on.
Create Meeting Note using Meeting Note Template.
This path threw a TypeError on every invocation before this PR (NPTemplating.getTemplate doesn't exist). Expect it to work now. If it still fails, the failure is downstream of what was fixed.
Show Dashboard, then switch perspectives several times including back to default.
✅ Settings persist. ❌ Settings blank out, or the window fails to open. Check the log for carry on with the existing settings or Couldn't assemble the initial data.
Why: reactMain used to return {} on error, silently replacing every setting with undefined.
Settings: result style NotePlan, syncOpenResultItems on. Run search for a term matching several open tasks.
✅ Results carry sync IDs (^abc123) exactly as before. The happy path must be untouched — that's the main thing to confirm; the changed path only fires on an error that already broke the search.
Review overdue tasks (by Task). Edit a task's text; complete/cancel a task.
✅ Both work. Check log for unsupported field — its absence confirms only type/content are ever sent.
Any chooser showing relative dates (e.g. Show Dashboard, or a template using them).
✅ this week, last week, next week, 2 weeks ago … 10 weeks' time all present. ❌ Whole categories missing → check log for returned null for week offset.
Open a plugin window, move/resize, close, reopen. ✅ Restored. If the log shows isn't a valid Rect, you had a corrupt stored pref — the intended fix, but worth knowing it fired.
rename note on a normal note ✅; on a title-less note ✅ stops cleanly rather than offering "undefined".
Not testable — dead code, no action needed: events.js (unreachable — not in index.js or plugin.json), TemplatingEngine.getDefaultFormat() (marked "should never be called"), the renderTemplate barrel re-export (nothing imports it).
Listed so they aren't mistaken for regressions during testing:
helpers/calendar.jskeepTodayPortionOnly()claimsArray<TCalendarItem>but returns hand-built plain objects with noid, noisRecurring, no methods. Any caller reading.idor calling.findLinkedFilenames()on a multi-day event getsundefinedor a crash. Four files across three plugins consume it, andhelpers/__tests__/calendar.test.jsassertstoEqual()on the plain-object shape — so the test currently locks in the wrong contract.helpers/NPnote.jschooseNoteV2()can return a fake note: pick a future calendar date with no note yet and you get a stub with onlytitle/type/changedDate.helpers/urls.jsfindProjectNoteUrlInText()— theallowCalendarNotesparameter is dead; the filtered result is computed then discarded.jgclark.SearchExtensions/flexiSearch.js—String(x) ?? 'quick'dead defaults:String()never returns null, so unset preferences become the literal string"undefined".dbludeau.TodoistNoteplanSync—teamAccountis both a data property and a setter; the setter wins, so reads returnundefined. Appears to be dead code.helpers/sorting.jsfieldSorter— declared(a: string, b: string)but always called with objects; also compares against the string'NaN'.
Space-indentation is invisible to plugins. Verified against a note whose bytes on disk were confirmed:
| Line | On disk | API indents |
API rawContent |
|---|---|---|---|
| tab child | 1 tab | 1 ✓ | "\t* A child ONE-TAB" ✓ |
| tab grandchild | 2 tabs | 2 ✓ | tabs preserved ✓ |
| 2-space child | 2 spaces | 0 ✗ | spaces stripped |
| 8-space grandchild | 8 spaces | 0 ✗ | spaces stripped |
rawContent is reconstructed from marker + indents rather than being the literal file bytes (a - [ ] on disk comes back as * ), so once the parser declines to count leading spaces the indentation is gone from both fields before any plugin runs.
The consequence is data loss, not display: any plugin that deletes and re-inserts paragraphs writes back the stripped version and permanently flattens a space-indented file. Repro note is DELETEME/indent-api-test.md; section D shows both behaviours under a single parent. Needs raising with @EduardMe.