Compare commits

...

441 Commits

Author SHA1 Message Date
lucaronin
51010ca578 fix: show manual update feedback 2026-04-25 17:15:19 +02:00
lucaronin
15bc3a4701 fix: disable command palette text correction 2026-04-25 16:49:48 +02:00
lucaronin
3ebfa642fa fix: allow runtime stylesheet injection 2026-04-25 16:06:17 +02:00
lucaronin
8207b87c33 fix: include linux signatures in stable release 2026-04-25 14:39:19 +02:00
lucaronin
602a099ac3 fix: polish contribute dialog copy 2026-04-25 13:54:39 +02:00
lucaronin
2315122c4a fix: clear stale editor selection on note switch 2026-04-25 13:27:33 +02:00
lucaronin
ab4fcdbaae ci: include linux alpha updater signatures 2026-04-25 12:45:31 +02:00
lucaronin
a56cb8c287 fix: allow raw editor styles under CSP 2026-04-25 12:03:54 +02:00
lucaronin
b93e6d491a ci: test linux alpha release artifacts 2026-04-25 11:59:21 +02:00
lucaronin
3152ff2c6e fix: allow optional pending diff loaders 2026-04-25 11:46:59 +02:00
lucaronin
dd018cf98b fix: queue diff opening until tab activation 2026-04-25 11:46:59 +02:00
lucaronin
d0d9d90096 test: stabilize H1 wikilink smoke 2026-04-25 11:20:14 +02:00
lucaronin
944efada94 fix: load note windows without vault scan 2026-04-25 10:53:09 +02:00
lucaronin
ef4ad256e3 feat: add Refactoring support card 2026-04-25 10:31:26 +02:00
lucaronin
c427cd2492 fix: guard native file drops 2026-04-25 10:07:43 +02:00
lucaronin
151e993ab7 chore: ratchet codescene thresholds 2026-04-25 02:29:08 +02:00
lucaronin
60554d0b96 fix: handle IME composition in AI composer 2026-04-25 02:29:08 +02:00
lucaronin
4929f11b6d fix: harden windows release and git subprocesses 2026-04-25 00:58:49 +02:00
lucaronin
3f4e5b585f fix: refresh image asset scope after attachment writes 2026-04-25 00:19:57 +02:00
lucaronin
cd49c81bd3 feat: add status bar theme toggle 2026-04-24 23:58:26 +02:00
lucaronin
b9f3dc9e1a fix: refine contribute panel actions 2026-04-24 23:44:02 +02:00
lucaronin
122aba7878 fix: load current note in new window 2026-04-24 23:29:44 +02:00
lucaronin
05896bde19 fix: restore editor external link toolbar actions 2026-04-24 23:25:22 +02:00
lucaronin
87a933f39c fix: replace silent catch blocks with warnings 2026-04-24 22:44:36 +02:00
lucaronin
54c0efa3e4 feat: add dark mode foundation 2026-04-24 22:28:07 +02:00
lucaronin
bdde0c8943 fix: bound note prefetch cache by size 2026-04-24 21:53:47 +02:00
lucaronin
a474303eb9 fix: preserve back history after cmd-click wikilinks 2026-04-24 21:15:27 +02:00
lucaronin
4701485ee5 fix: keep empty title clicks in h1 2026-04-24 20:13:34 +02:00
lucaronin
135a8a0adf fix: handle nested release artifacts 2026-04-24 19:48:56 +02:00
lucaronin
0f5d7d5340 fix: restore folder boundary validation 2026-04-24 19:23:55 +02:00
lucaronin
d6b3c0aef3 feat: add windows desktop release support 2026-04-24 19:23:55 +02:00
lucaronin
f896c01829 fix: publish linux stable release artifacts 2026-04-24 18:53:43 +02:00
lucaronin
c3cff0c461 feat: replace feedback with contribute modal 2026-04-24 17:45:43 +02:00
lucaronin
8c7f9238cb fix: gate macos shortcut constants for linux clippy 2026-04-24 16:45:05 +02:00
lucaronin
38acebba7c feat: add linux desktop support 2026-04-24 16:25:36 +02:00
lucaronin
622977aeb8 fix: restore clone modal invoke typing 2026-04-24 15:18:50 +02:00
lucaronin
9ce1c6f854 refactor: restore clone modal code health 2026-04-24 15:13:24 +02:00
lucaronin
46865638cd fix: harden clone git repo flow 2026-04-24 14:56:17 +02:00
lucaronin
564ee0a387 test: cover native command wrappers 2026-04-24 14:44:01 +02:00
lucaronin
11655c0283 fix: harden inbox auto-advance merge 2026-04-24 14:35:54 +02:00
github-actions[bot]
c24fd30266 Merge branch 'main' into main 2026-04-24 12:04:53 +00:00
lucaronin
daa1e3e985 fix: keep ai composer responsive after image paste 2026-04-24 13:52:58 +02:00
github-actions[bot]
44c7f43e57 Merge branch 'main' into main 2026-04-24 11:43:53 +00:00
lucaronin
f2c83be4b3 fix: format MCP config tests 2026-04-24 13:32:30 +02:00
lucaronin
6104402bfb fix: write Claude Code MCP config to .claude.json 2026-04-24 13:22:33 +02:00
github-actions[bot]
b3744e3bf5 Merge branch 'main' into main 2026-04-24 10:43:45 +00:00
lucaronin
67c8598b87 fix: keep clone flows off the UI thread 2026-04-24 12:34:00 +02:00
github-actions[bot]
52620c80dc Merge branch 'main' into main 2026-04-24 10:27:33 +00:00
lucaronin
7e386ff70c docs: move contributing guide to repo root 2026-04-24 12:27:17 +02:00
github-actions[bot]
aba5550b8e Merge branch 'main' into main 2026-04-24 10:07:41 +00:00
Luca Rossi
309b816e6d Add custom funding URL to FUNDING.yml 2026-04-24 12:07:27 +02:00
github-actions[bot]
f552d8abff Merge branch 'main' into main 2026-04-24 10:02:39 +00:00
lucaronin
9f6779e9f1 docs: add contributing guide 2026-04-24 11:46:35 +02:00
github-actions[bot]
bddf5106bb Merge branch 'main' into main 2026-04-24 06:31:23 +00:00
lucaronin
ba5c165016 test: harden macos titlebar coverage 2026-04-24 08:20:30 +02:00
github-actions[bot]
39415c350f Merge branch 'main' into main 2026-04-24 06:14:23 +00:00
Brian Lee
499e481df8 Merge branches 'main', 'main', 'main' and 'main' of https://github.com/refactoringhq/tolaria 2026-04-24 15:07:00 +09:00
Brian Lee
843cedd14e fix: align auto-advance test bridge types 2026-04-24 15:06:13 +09:00
Brian Lee
05156ca793 feat: add inbox auto-advance setting 2026-04-24 15:04:08 +09:00
lucaronin
2308b269c8 docs: add ADRs for cache concurrency and git signing fallback (guard — from commits 42bb6c4, 6dbcc33) 2026-04-24 08:03:58 +02:00
lucaronin
b7f482bf27 fix: respect macos header double click action 2026-04-24 07:47:53 +02:00
lucaronin
33d7404d6c fix: keep blocknote tables editable after focus changes 2026-04-24 07:02:40 +02:00
lucaronin
0ecac2ab79 fix: allow unicode type filenames 2026-04-24 04:01:43 +02:00
lucaronin
7c9f2b0dda fix: guard editor selection churn around inline actions 2026-04-24 03:25:50 +02:00
lucaronin
24e317cfa6 fix: handle unreadable non-utf8 files gracefully 2026-04-24 03:02:03 +02:00
lucaronin
66c183fa61 fix: keep IME composition stable in editors 2026-04-24 02:50:12 +02:00
lucaronin
dacf9bb19b fix: guard note loading without active vault 2026-04-24 02:16:49 +02:00
lucaronin
3c92c200ab fix: sync onboarding vault handoff 2026-04-24 01:45:11 +02:00
lucaronin
61c3a3e21b docs: ratchet codescene thresholds 2026-04-24 01:39:28 +02:00
lucaronin
2e7c94d159 fix: register created onboarding vaults 2026-04-24 01:26:22 +02:00
lucaronin
98ade7411a fix: avoid duplicate onboarding vault writes 2026-04-24 01:23:16 +02:00
lucaronin
18776341f8 fix: persist onboarding vault selections 2026-04-24 01:08:13 +02:00
lucaronin
57bbcdbaef fix: format cache hardening helpers 2026-04-24 00:00:10 +02:00
lucaronin
42bb6c4b18 fix: harden vault cache updates 2026-04-24 00:00:10 +02:00
lucaronin
070cd3ac6d test: stabilize telemetry onboarding smoke 2026-04-23 23:11:46 +02:00
lucaronin
accc6270d3 fix: restore vault images and default folders 2026-04-23 23:11:46 +02:00
lucaronin
941f12aa1b test: clear vitest cache before coverage 2026-04-23 22:45:22 +02:00
lucaronin
4710a2d0a9 fix: reuse local smoke server by default 2026-04-23 22:27:38 +02:00
lucaronin
65a421215a fix: satisfy arrow ligature type checks 2026-04-23 22:14:20 +02:00
lucaronin
4ed0289989 Revert "test: simplify main entrypoint setup"
This reverts commit 4ad32faa7b.
2026-04-23 22:14:11 +02:00
lucaronin
4ad32faa7b test: simplify main entrypoint setup 2026-04-23 22:10:02 +02:00
lucaronin
c9623d5ca8 feat: add arrow ligatures in both editors 2026-04-23 22:06:01 +02:00
lucaronin
77286457bf fix: anchor settings default agent dropdown 2026-04-23 20:09:49 +02:00
lucaronin
5b8aa8da0d fix: guard pull refresh during unsaved editor edits 2026-04-23 19:34:15 +02:00
lucaronin
21b6d8984a fix: support symlinked vault notes 2026-04-23 18:36:45 +02:00
lucaronin
11fc123492 fix: guard folder picker after update install 2026-04-23 18:16:43 +02:00
lucaronin
d98fa94e14 fix: retry commits when signing helper is missing 2026-04-23 17:27:53 +02:00
lucaronin
add3c4075f fix: thread vault image path through tab swaps 2026-04-23 16:35:27 +02:00
lucaronin
fb0d550eb2 fix: make starter vault images portable 2026-04-23 16:31:36 +02:00
lucaronin
6adf7fd53b fix: capture react root errors in sentry 2026-04-23 15:34:39 +02:00
lucaronin
3b4d9599d2 fix: finish empty Claude Code runs 2026-04-23 14:15:15 +02:00
lucaronin
bb1b18686f fix: stabilize smoke dev server watch 2026-04-23 13:45:24 +02:00
lucaronin
6dbcc334bf fix: create repos despite commit signing 2026-04-23 13:26:01 +02:00
lucaronin
4944365ed7 fix: detect local Claude Code installs 2026-04-23 12:24:26 +02:00
lucaronin
c4c6bb6c57 fix: restore folder row toggle 2026-04-23 09:27:17 +02:00
lucaronin
35dd2dd47b docs: add/update ADR for note retargeting (guard — from commit 2c9361d7) 2026-04-23 08:07:56 +02:00
lucaronin
89141bae14 chore: ratchet codescene thresholds 2026-04-23 00:53:45 +02:00
lucaronin
2b9294f884 style: format note move backend 2026-04-23 00:41:04 +02:00
lucaronin
a756af54ec fix: keep note drop targets active during drag 2026-04-23 00:27:09 +02:00
lucaronin
b01b156dfb fix: align folder rows with sidebar items 2026-04-23 00:21:20 +02:00
lucaronin
2c9361d704 feat: retarget notes to types and folders 2026-04-22 23:56:00 +02:00
lucaronin
cea71a4fd9 Merge branch 'main' of https://github.com/refactoringhq/tolaria 2026-04-22 22:54:40 +02:00
lucaronin
8b73ef5836 feat: add sidebar folder rename and delete actions 2026-04-22 22:49:16 +02:00
lucaronin
a6a727a4c0 fix: align inspector create-type typing 2026-04-22 22:42:04 +02:00
lucaronin
bf13eed3ab fix: handle filename collisions in create flows 2026-04-22 22:37:01 +02:00
lucaronin
a27a70e552 fix: allow PostHog and Google Fonts in app CSP 2026-04-22 22:24:45 +02:00
lucaronin
ca92bf4f77 fix: restore keyboard shortcut typing 2026-04-22 21:22:35 +02:00
lucaronin
9fda0a67ec fix: make note renames crash-safe 2026-04-22 21:18:38 +02:00
lucaronin
0ffb7c65a9 Ignore history shortcuts while editing 2026-04-22 21:06:27 +02:00
lucaronin
633d9f1496 Add security policy and private reporting guidance 2026-04-22 20:46:24 +02:00
lucaronin
ba3d413b94 fix: recover from missing note paths 2026-04-22 20:18:26 +02:00
lucaronin
f3286922ad fix: satisfy clippy for MCP status 2026-04-22 19:28:58 +02:00
lucaronin
b0b476c9e5 fix: harden external AI tool setup 2026-04-22 19:18:52 +02:00
lucaronin
dac696b752 fix: guard toolbar popover anchors 2026-04-22 17:57:30 +02:00
lucaronin
cda87ee6c0 feat: add sidebar folder rename and delete actions 2026-04-22 17:16:56 +02:00
lucaronin
33324a5c79 fix: harden mcp bridge and vault boundaries 2026-04-22 17:14:42 +02:00
lucaronin
f28626bbfe fix: finish claude agent streams 2026-04-22 15:59:12 +02:00
lucaronin
1bc2e3d1e0 docs: add ADRs for startup and linkify lifecycles (guard — from commits 59268acd, bf442646) 2026-04-22 15:09:45 +02:00
lucaronin
8e6bdab3e6 docs: ratchet codescene thresholds 2026-04-22 00:31:54 +02:00
lucaronin
d52362ad9b fix: repair tiptap patch integrity 2026-04-22 00:21:46 +02:00
lucaronin
bf442646f5 fix: silence duplicate linkify init warnings 2026-04-22 00:05:32 +02:00
lucaronin
1b3a444368 fix: show welcome on fresh install without missing vault 2026-04-21 23:34:07 +02:00
lucaronin
c4da7f1f2f fix: narrow raw mode sync override 2026-04-21 19:04:02 +02:00
lucaronin
c5fe291a9a fix: sync inspector edits into raw editor 2026-04-21 19:04:02 +02:00
lucaronin
59268acd04 fix: guard empty vault path during startup 2026-04-21 19:04:01 +02:00
lucaronin
3e4e2ebdc6 refactor: simplify breadcrumb and note-list search ui 2026-04-21 19:03:11 +02:00
Luca Rossi
05375258a2 Rename 'Documentation' section to 'Tech Docs' 2026-04-21 18:06:30 +02:00
Luca Rossi
0c0354bfb8 Updated README badges 2026-04-21 18:05:54 +02:00
lucaronin
6023e95d5b fix: serialize vitest coverage files 2026-04-21 17:38:44 +02:00
lucaronin
88d5694353 fix: retry flaky vitest coverage teardown 2026-04-21 17:35:00 +02:00
lucaronin
db6b38843b fix: keep vitest coverage shards off workspace 2026-04-21 17:21:35 +02:00
lucaronin
ad9901bbf5 fix: stabilize coverage suite reruns 2026-04-21 17:08:43 +02:00
lucaronin
b785c53ef9 test: increase strategic coverage 2026-04-21 16:54:52 +02:00
lucaronin
8ae1ade220 test: cover live sidebar type picker sync 2026-04-21 15:16:39 +02:00
lucaronin
35b035643e fix: adapt save reload callback 2026-04-21 15:09:27 +02:00
lucaronin
d3d663af0b fix: reload vault entries on tauri startup 2026-04-21 15:02:42 +02:00
lucaronin
8f624350d8 test: cover journal vault scan 2026-04-21 12:37:46 +02:00
lucaronin
0e54173834 test: cover explicit journal visibility 2026-04-21 12:27:06 +02:00
lucaronin
a6f1524a78 ci: reject placeholder telemetry secrets 2026-04-21 11:06:16 +02:00
lucaronin
4cd5baeb55 fix: sync sidebar types with live vault state 2026-04-21 10:45:39 +02:00
lucaronin
fb1265e088 test: stabilize note list search timing 2026-04-21 10:14:18 +02:00
lucaronin
ca89662231 fix: sync raw editor frontmatter state 2026-04-21 10:06:54 +02:00
lucaronin
97f285e3cb ci: upload coverage to codecov 2026-04-21 10:06:54 +02:00
Luca Rossi
4bd4ef43dc Added release badge in README 2026-04-21 09:43:40 +02:00
Luca Rossi
be29880a3e Added walkthroughs to the README 2026-04-21 09:22:51 +02:00
lucaronin
7be5519f61 fix: sort alpha releases chronologically 2026-04-21 09:10:16 +02:00
lucaronin
8cefbe949a fix: refine derived inverse relationship labels 2026-04-21 09:08:25 +02:00
lucaronin
dd0a55b8e8 docs: add ADR for external vault refresh after pulls and agent edits (guard — from commit 0020ade6) 2026-04-21 08:03:02 +02:00
lucaronin
610276b1bc docs: ratchet codescene thresholds 2026-04-21 00:56:59 +02:00
lucaronin
e6c968e37e fix: satisfy note list keyboard build 2026-04-21 00:50:44 +02:00
lucaronin
367a66f32a fix: debounce note list search 2026-04-21 00:47:45 +02:00
lucaronin
6a4046915c fix: harden startup telemetry urls 2026-04-20 23:38:41 +02:00
lucaronin
ec8b637c84 fix: hide uncloned getting started vault 2026-04-20 23:04:34 +02:00
lucaronin
3ee42fb856 test: align create-note smoke placeholder 2026-04-20 22:13:19 +02:00
lucaronin
eacf05a9f8 fix: reload notes after agent edits 2026-04-20 22:04:38 +02:00
lucaronin
a199fad803 fix: widen editor focus block typing 2026-04-20 18:38:51 +02:00
lucaronin
e956c806e5 fix: keep editor sync from clobbering live inserts 2026-04-20 18:35:50 +02:00
lucaronin
7a6e3863ad fix: reopen active note after pull updates 2026-04-20 16:04:11 +02:00
lucaronin
e0e2dc4063 test: add pull refresh regression 2026-04-20 15:53:56 +02:00
lucaronin
0020ade6d7 fix: tighten pulled note refresh flow 2026-04-20 14:43:52 +02:00
lucaronin
503e482c7d fix: normalize inverse relationship labels 2026-04-20 12:49:57 +02:00
lucaronin
0d0e6af4c9 fix: keep vault menu remove action in row 2026-04-20 12:19:10 +02:00
lucaronin
dbaf361878 test: fix starter-vault app shell coverage 2026-04-20 11:32:44 +02:00
lucaronin
d3741c82f5 test: fix starter-vault shell regression 2026-04-20 11:20:27 +02:00
lucaronin
b90d2f8612 fix: reorder welcome onboarding actions 2026-04-20 11:04:29 +02:00
lucaronin
9536efee0e fix: persist AI panel chats across reopen 2026-04-20 10:28:48 +02:00
lucaronin
c86a6ab9c5 docs: add/update ADR for starter vault local-first remote flow (guard — from commit 374b4f66) 2026-04-20 10:28:00 +02:00
Luca Rossi
1d7c027700 Updated Tolaria Icon 2026-04-20 10:25:46 +02:00
lucaronin
74e50c64bb fix: hide stale getting started vault entries 2026-04-20 01:02:08 +02:00
lucaronin
8b97ef711f feat: refresh welcome screen branding 2026-04-20 00:45:14 +02:00
lucaronin
08855cee92 fix: defer git history during note switching 2026-04-20 00:09:52 +02:00
lucaronin
03042bb49c fix: normalize shipped telemetry secrets 2026-04-19 23:27:57 +02:00
lucaronin
cee4bef179 fix: harden shipped telemetry config 2026-04-19 23:16:34 +02:00
lucaronin
f223d4f654 refactor: curate demo-vault-v2 fixture 2026-04-19 22:26:35 +02:00
lucaronin
2f693c4476 fix: dedupe new type palette action 2026-04-19 21:38:26 +02:00
lucaronin
44f0c7cb74 feat: open favorites in neighborhood mode 2026-04-19 21:27:32 +02:00
lucaronin
ea14f1a389 fix: use rendered release note html 2026-04-19 20:53:39 +02:00
lucaronin
db21ae4708 feat: improve release history page 2026-04-19 20:37:53 +02:00
lucaronin
8019c4fb2d fix: defer missing vault startup state 2026-04-19 20:08:10 +02:00
lucaronin
0d552e1370 fix: satisfy note-open perf build checks 2026-04-19 19:34:05 +02:00
lucaronin
977f5593fa fix: stop note navigation from scaling with vault size 2026-04-19 19:30:58 +02:00
lucaronin
261a5befc7 chore: ratchet codescene thresholds 2026-04-19 18:56:47 +02:00
lucaronin
8d3cce7b49 fix: satisfy neighborhood history build checks 2026-04-19 18:50:08 +02:00
lucaronin
e0594820f0 fix: unwind neighborhood history on escape 2026-04-19 18:46:10 +02:00
lucaronin
b98d41212e fix: stop seeding title during property init 2026-04-19 18:18:35 +02:00
lucaronin
098dc421cb test: add isolated vitest coverage runner 2026-04-19 18:09:58 +02:00
lucaronin
326ccb08e1 test: stabilize vitest coverage artifacts 2026-04-19 18:03:58 +02:00
lucaronin
c2bbdb0085 test: stabilize git default branch in helpers 2026-04-19 17:52:58 +02:00
lucaronin
039f9e447f fix: cache inspector link lookups 2026-04-19 17:35:08 +02:00
lucaronin
8581a1b1e6 fix: organize seeded claude shim files 2026-04-19 17:08:45 +02:00
Luca Rossi
2c5cb593d3 Removed old Project spec reverence 2026-04-19 17:05:30 +02:00
Luca Rossi
348b627029 Updated README 2026-04-19 16:44:50 +02:00
lucaronin
f5cc5ffb4f fix: satisfy clippy and stabilize app startup test 2026-04-19 16:28:02 +02:00
lucaronin
374b4f6686 feat: keep getting started vaults local by default 2026-04-19 16:18:35 +02:00
lucaronin
36c20f34f4 fix: hide unavailable getting started vault 2026-04-19 15:21:59 +02:00
Luca Rossi
8551d819bd Deleted old unused font 2026-04-19 15:16:43 +02:00
lucaronin
d5497d7252 Match getting started subtitle copy 2026-04-19 14:30:07 +02:00
lucaronin
046afe0e67 Simplify Getting Started vault subtitle 2026-04-19 14:23:05 +02:00
lucaronin
cb1cc4b394 fix: include inverse neighborhood groups 2026-04-19 14:13:10 +02:00
lucaronin
00cb938766 fix: add cmd o quick-open alias 2026-04-19 13:47:02 +02:00
lucaronin
80c13272f3 fix: keep neighborhood reverse matches path-aware 2026-04-19 12:53:07 +02:00
lucaronin
9534449307 fix: hide absent neighborhood groups 2026-04-19 12:26:31 +02:00
lucaronin
2d5361b2e9 fix: keep keyboard note switching snappy 2026-04-19 11:49:48 +02:00
lucaronin
b22da65d44 fix: keep note switches instant 2026-04-19 11:29:04 +02:00
lucaronin
862af864d3 fix: preserve image selection while hovering toolbar 2026-04-19 10:48:45 +02:00
lucaronin
9ecefc90d5 feat: add stable landing page download link 2026-04-19 04:44:17 +02:00
lucaronin
9693fe24c9 fix: restore type filter icon colors 2026-04-19 04:15:12 +02:00
lucaronin
92ea32c0b8 feat: add neighborhood browsing mode 2026-04-19 03:37:33 +02:00
lucaronin
2e2bd2f3b3 fix: preserve native welcome vault creation 2026-04-19 02:37:29 +02:00
lucaronin
3556d934ee test: format vault command assertions 2026-04-19 01:55:39 +02:00
lucaronin
feef938174 fix: align fresh vault seed files 2026-04-19 01:33:53 +02:00
lucaronin
67a9930ea1 feat: add empty vault creation flow 2026-04-19 01:06:40 +02:00
lucaronin
aed4d05f1d docs: ratchet codescene thresholds 2026-04-19 00:25:35 +02:00
lucaronin
cac7f12c6d fix: stop duplicate native quick prompt paste 2026-04-19 00:18:42 +02:00
lucaronin
72b5b52140 feat: add view note-list controls 2026-04-18 20:31:25 +02:00
lucaronin
7deeb49751 feat: add number property type 2026-04-18 19:40:00 +02:00
lucaronin
8c0a2e7ec8 fix: share onboarding drag shell 2026-04-18 18:50:30 +02:00
lucaronin
c4e66dc74f fix: make onboarding window draggable 2026-04-18 18:09:37 +02:00
lucaronin
c0bb722db5 fix: reuse note content cache for rapid note switches 2026-04-18 17:30:49 +02:00
lucaronin
bc11c869aa fix: stabilize ai mode paste editing 2026-04-18 16:52:36 +02:00
lucaronin
502ecf0572 test: stabilize wikilink smoke coverage 2026-04-18 16:18:43 +02:00
lucaronin
8bbbba98b7 fix: correct vault path type guard 2026-04-18 16:02:19 +02:00
lucaronin
86adf860b5 fix: restore bottom bar vault switching 2026-04-18 15:59:34 +02:00
lucaronin
cec1627a81 fix: preserve image toolbar hover bridge 2026-04-18 15:12:39 +02:00
lucaronin
b00183419e fix: show Note metadata in autocomplete 2026-04-18 13:37:15 +02:00
lucaronin
ed9ceff1d2 fix: align Notes counts with explicit type 2026-04-18 13:07:17 +02:00
lucaronin
e18491bcd3 fix: preserve untitled rename session continuity 2026-04-18 12:17:24 +02:00
lucaronin
fd0cfc7fb6 fix: exclude attachments from all notes totals 2026-04-18 11:53:06 +02:00
lucaronin
6541ce5755 test: stabilize wikilink smoke timing 2026-04-18 11:19:41 +02:00
lucaronin
4456801526 fix: keep h1 typing responsive 2026-04-18 11:06:55 +02:00
lucaronin
db7aaf1385 fix: restore note type during vault repair 2026-04-18 10:38:50 +02:00
lucaronin
54b2616fa4 fix: restore manual commit dialog when autogit is off 2026-04-18 09:58:49 +02:00
lucaronin
6a90e9f770 fix: correct editor layout diff callback type 2026-04-18 09:28:18 +02:00
lucaronin
b79c78a5ba fix: save raw note changes before switching notes 2026-04-18 09:28:18 +02:00
Luca Rossi
48a8882970 Reorder CodeScene badges in README 2026-04-18 09:16:06 +02:00
Luca Rossi
98276702a2 Added codescene badges 2026-04-18 09:14:59 +02:00
lucaronin
857d55a7cd fix: stop cmd+s from renaming note titles 2026-04-18 08:25:43 +02:00
lucaronin
b78dfad384 fix: resume onboarding after telemetry on fresh start 2026-04-18 03:10:41 +02:00
lucaronin
9ac51bab9a test: pin app test active vault 2026-04-18 02:35:41 +02:00
lucaronin
1fee994863 test: cover mock vault config path 2026-04-18 02:21:48 +02:00
lucaronin
764bbf0e4c test: scope inbox sidebar assertion 2026-04-18 02:10:54 +02:00
lucaronin
ff05ba0430 chore: ratchet codescene thresholds 2026-04-18 01:56:12 +02:00
lucaronin
347df47c62 fix: stabilize smoke vault harness 2026-04-18 01:50:29 +02:00
lucaronin
7536169508 fix: resume onboarding after telemetry consent 2026-04-18 00:21:52 +02:00
lucaronin
34de384738 fix: preserve yml views in raw editor 2026-04-17 23:32:36 +02:00
lucaronin
954ee42f0b fix: restore seeded vault guidance compatibility 2026-04-17 22:55:34 +02:00
lucaronin
f67aafe442 fix: guard optional filename marks 2026-04-17 22:23:38 +02:00
lucaronin
b2122ac24c fix: stop auto-linking plain filenames 2026-04-17 22:20:48 +02:00
lucaronin
6f8810146e Sync getting-started AGENTS guidance 2026-04-17 19:15:05 +02:00
lucaronin
929ba9241e Refresh getting-started view guidance 2026-04-17 19:08:59 +02:00
lucaronin
c5e84c8218 fix: keep image toolbar controls open 2026-04-17 18:29:37 +02:00
lucaronin
fb55ed2f6d fix: remove unsupported block colors menu 2026-04-17 17:48:12 +02:00
lucaronin
91a0247187 fix: stop editor toggles from changing modified order 2026-04-17 17:12:25 +02:00
lucaronin
ce78abfd57 Restore icon property in inspector 2026-04-17 15:33:17 +02:00
lucaronin
6202892512 Shorten AI agent footer badge 2026-04-17 14:04:12 +02:00
lucaronin
2eb21356ef Polish settings panel and tooltip alignment 2026-04-17 10:53:56 +02:00
lucaronin
4cbf359c7b docs: add/update ADRs for git checkpoints and H1 rename setting (guard — from commit 323ef19) 2026-04-17 08:04:36 +02:00
lucaronin
4e7069174e test: refresh smoke selectors for toolbar actions 2026-04-17 02:49:51 +02:00
lucaronin
22cefe4912 feat: add action tooltips to note controls 2026-04-17 02:34:41 +02:00
lucaronin
ce26c57e99 feat: expand note list search content 2026-04-17 01:46:04 +02:00
lucaronin
8238134435 chore: ratchet codescene thresholds 2026-04-17 00:54:16 +02:00
lucaronin
10fe1239bd fix: restore inspector build types 2026-04-17 00:47:28 +02:00
lucaronin
a84f3c93ca feat: warn on missing note types 2026-04-17 00:44:14 +02:00
lucaronin
072591ad59 test: restore serialized smoke menu bridge 2026-04-16 23:47:20 +02:00
lucaronin
323ef1913b feat: add AutoGit idle and inactive checkpoints 2026-04-16 23:38:30 +02:00
lucaronin
2b85f4e45f fix: preserve valid bold markdown in rich editor 2026-04-16 22:31:19 +02:00
lucaronin
3bcfedda5b refactor: split vault mod tests by responsibility 2026-04-16 21:59:54 +02:00
lucaronin
3e45fd898a fix: limit markdown list highlighting to markers 2026-04-16 21:13:57 +02:00
lucaronin
6334d10540 💅 polish inspector, note list, and history UI 2026-04-16 17:03:50 +02:00
lucaronin
5eeb37e32d fix: make pulse rows edge to edge 2026-04-16 16:18:45 +02:00
lucaronin
193b7c2a44 fix: align sidebar count column 2026-04-16 15:45:28 +02:00
lucaronin
a36760efe6 fix: satisfy rust clippy gate 2026-04-16 15:09:24 +02:00
lucaronin
edd5a1ce8f feat: align release version naming 2026-04-16 14:43:08 +02:00
lucaronin
991ee8689e refactor: simplify note item composition 2026-04-16 13:58:17 +02:00
lucaronin
e111700203 fix: restore relationship panel layout 2026-04-16 13:04:35 +02:00
lucaronin
2772064b2a fix: prefer local mtimes in modified sort 2026-04-16 12:44:12 +02:00
lucaronin
c0d16e4b4d feat: add h1 auto-rename setting 2026-04-16 12:18:11 +02:00
lucaronin
711d49b3bb fix: align property panel rows 2026-04-16 11:53:29 +02:00
lucaronin
53b3f5a14b fix: type editor formatting block selector 2026-04-16 11:28:50 +02:00
lucaronin
d1dbada9e4 fix: restore markdown-safe editor formatting controls 2026-04-16 11:24:37 +02:00
lucaronin
4e66f09eef fix: make note-list property pickers searchable 2026-04-16 10:25:56 +02:00
lucaronin
f5b1d763c1 docs: ratchet codescene thresholds 2026-04-16 09:51:59 +02:00
lucaronin
905754e0c5 fix: reduce editor-only window width floor 2026-04-16 09:38:34 +02:00
lucaronin
f665f089dc fix: narrow note list entity selection 2026-04-16 09:11:22 +02:00
lucaronin
57a87e43b3 fix: make keyboard note navigation predictable 2026-04-16 09:04:13 +02:00
lucaronin
1b3a9e3ecf fix: keep raw frontmatter out of markdown headings 2026-04-16 07:49:31 +02:00
lucaronin
f8bd3e1b4a feat: show property kind icons 2026-04-16 07:27:11 +02:00
lucaronin
1b10aa9660 test: cover mcp config edge cases 2026-04-16 07:03:08 +02:00
lucaronin
514c1f9ef7 fix: satisfy rust quality gates 2026-04-16 06:42:58 +02:00
lucaronin
e9c1ae2b8a fix: canonicalize hidden system metadata 2026-04-16 06:21:34 +02:00
lucaronin
e8e2004aa8 fix: make the pulse header draggable 2026-04-16 05:41:41 +02:00
lucaronin
a448945ed5 fix: format window constraint command imports 2026-04-16 05:07:33 +02:00
lucaronin
d469f220e7 fix: make main window min width follow visible panes 2026-04-16 05:03:01 +02:00
lucaronin
d60c47ee27 feat: standardize relationship property labels 2026-04-16 03:45:34 +02:00
lucaronin
087e9f13c8 fix: rename getting started starter repo 2026-04-16 03:08:29 +02:00
lucaronin
ed93b4b234 fix: stop defaulting new note status 2026-04-16 02:42:43 +02:00
lucaronin
b8dfc79e80 fix: align note list model event types 2026-04-16 02:21:55 +02:00
lucaronin
bbf98d6112 feat: customize all-notes note list columns 2026-04-16 02:18:43 +02:00
lucaronin
f3ee593dee test: include multi-selection smoke in curated suite 2026-04-16 01:46:24 +02:00
lucaronin
2bd5d64497 fix: include multi-selection keyboard action type 2026-04-16 01:39:35 +02:00
lucaronin
9c0bda55f6 fix: apply note shortcuts to full multi-selection 2026-04-16 01:36:44 +02:00
lucaronin
2c0c464eac chore: ratchet codescene thresholds 2026-04-16 00:45:17 +02:00
lucaronin
82192f23be feat: simplify bulk action bar 2026-04-16 00:38:43 +02:00
lucaronin
310e159894 fix: remove status bar note count 2026-04-16 00:01:26 +02:00
lucaronin
77a7d9d519 fix: preserve editor mode position 2026-04-15 23:33:30 +02:00
lucaronin
37f099ab1e fix: keep editor formatting markdown-safe 2026-04-15 22:42:40 +02:00
lucaronin
8f72c4839d fix: preserve zoomed block handle hover 2026-04-15 19:42:25 +02:00
lucaronin
a1331453ad fix: keep untitled rename non-blocking 2026-04-15 18:59:51 +02:00
lucaronin
8518e89f03 fix: prevent duplicate untitled saves during rename 2026-04-15 18:33:44 +02:00
lucaronin
a5cde9e2d6 fix: open pulse rows in commit diff mode 2026-04-15 17:57:38 +02:00
lucaronin
2ad3d1cae1 fix: inject telemetry envs into release builds 2026-04-15 16:59:31 +02:00
lucaronin
7eaf57d040 fix: make getting started clone destination explicit 2026-04-15 14:55:56 +02:00
lucaronin
e142b4c8f9 docs: add/update ADR for root AI guidance files (guard — from commit ec3f90e) 2026-04-15 10:23:24 +02:00
lucaronin
8f04ed59a4 fix: remap buffered saves after untitled rename 2026-04-14 23:48:30 +02:00
lucaronin
8a5763959b fix: keep untitled rename non-blocking 2026-04-14 21:15:01 +02:00
lucaronin
4ab44fbe1a fix: preserve untitled rename cursor 2026-04-14 21:10:51 +02:00
lucaronin
2720c14e22 fix: refresh stale vault agent guidance 2026-04-14 20:45:52 +02:00
lucaronin
8d3bfc2ca7 chore: ratchet codescene thresholds 2026-04-14 20:12:56 +02:00
lucaronin
9ccd0a7221 fix: remove all notes status pills 2026-04-14 20:07:54 +02:00
lucaronin
1388e7ddbe fix: align sidebar view count chips 2026-04-14 19:45:32 +02:00
lucaronin
cd76c0d65d feat: add searchable type combobox 2026-04-14 18:48:56 +02:00
lucaronin
d9fd615d4c fix: open note windows from direct note paths 2026-04-14 18:17:45 +02:00
lucaronin
1f4ae56c1c fix: preserve aliased link labels 2026-04-14 17:34:22 +02:00
lucaronin
084d3d9c71 fix: preserve exact linked note titles 2026-04-14 17:32:47 +02:00
lucaronin
9041246a32 test: stabilize CI vault defaults 2026-04-14 17:04:23 +02:00
lucaronin
6a8fe11027 fix: flush title rename saves before path changes 2026-04-14 16:49:18 +02:00
lucaronin
0078cc257a fix: harden rename saves and async deletes 2026-04-14 15:40:46 +02:00
lucaronin
9f46f22667 fix: skip alpha dmg packaging on CI 2026-04-14 15:39:33 +02:00
lucaronin
2b734da4cb fix: clear cached tauri bundle artifacts 2026-04-14 15:30:10 +02:00
lucaronin
73c619892b fix: rotate tauri target cache keys 2026-04-14 15:18:05 +02:00
lucaronin
8f1b3c75b2 feat: show property type icons in rows 2026-04-14 15:14:46 +02:00
lucaronin
39da825443 fix: standardize property chip sizing 2026-04-14 15:01:59 +02:00
lucaronin
7b86e3a524 fix: align properties panel value columns 2026-04-14 14:40:31 +02:00
lucaronin
ab92ea178f fix: align vault guidance status typing 2026-04-14 14:20:30 +02:00
lucaronin
ec3f90e865 feat: add vault ai guidance restore flow 2026-04-14 14:16:55 +02:00
lucaronin
2356afdc42 style: format vault command wrappers 2026-04-14 12:56:16 +02:00
lucaronin
9154a8153f fix: refresh default vault agents guidance 2026-04-14 12:52:32 +02:00
lucaronin
c0fbb1c40e refactor: remove obsolete scripts tooling 2026-04-14 12:28:37 +02:00
lucaronin
76fe4611c2 test: stabilize untitled auto-rename smoke 2026-04-14 12:16:50 +02:00
lucaronin
62154535c0 feat: add ai agent status bar switcher 2026-04-14 11:55:48 +02:00
lucaronin
cff9583a46 fix: support caret boundary on current target 2026-04-14 11:37:44 +02:00
lucaronin
70252b6324 fix: anchor ai mode caret after wikilinks 2026-04-14 11:35:38 +02:00
lucaronin
60da17e13b fix: restore blocknote slash menu styling 2026-04-14 11:14:49 +02:00
lucaronin
0c9bfb14c3 fix: format async delete command 2026-04-14 10:57:32 +02:00
lucaronin
48d2246f0b fix: make note deletion non-blocking 2026-04-14 10:53:08 +02:00
lucaronin
d879457f89 docs: add ADR for ratcheted CodeScene thresholds (guard — from commit 311dcc64) 2026-04-14 10:11:05 +02:00
lucaronin
311dcc6413 chore: ratchet codescene thresholds after ADR guard checks 2026-04-14 10:03:12 +02:00
lucaronin
f61e7d2ab3 docs: update ADR for selectable CLI AI agents (guard — from commit d540d76f) 2026-04-14 09:57:20 +02:00
lucaronin
1ac6bd887b fix: format alpha build label parser 2026-04-13 23:52:13 +02:00
lucaronin
f31f827a4f fix: restore alpha build label 2026-04-13 23:48:19 +02:00
lucaronin
a1ffe7a074 fix: align note list header icons 2026-04-13 23:33:46 +02:00
lucaronin
1b45394d0a fix: show filenames in changes view 2026-04-13 23:18:53 +02:00
lucaronin
95f0153c1b fix: restore aligned property panel values 2026-04-13 23:01:37 +02:00
lucaronin
a034a79db2 fix: align blocknote editor context 2026-04-13 22:45:25 +02:00
lucaronin
1f148ebe32 fix: restore vault type command export 2026-04-13 22:23:31 +02:00
lucaronin
b7cb3c2130 fix: remove legacy journal command surfaces 2026-04-13 22:21:42 +02:00
lucaronin
f50d9127d1 fix: restore editor code block highlighting 2026-04-13 22:00:50 +02:00
lucaronin
2a353d5749 fix: keep caret after command palette ai trigger 2026-04-13 21:07:10 +02:00
lucaronin
c2f9b7c20d fix: restore ai smoke compatibility 2026-04-13 19:47:38 +02:00
lucaronin
d540d76f73 feat: support selectable cli ai agents 2026-04-13 19:37:59 +02:00
lucaronin
cdfc0c9135 docs: clarify demo vault automation hygiene 2026-04-13 17:59:23 +02:00
lucaronin
5b29e061fe fix: refresh renamed note metadata 2026-04-13 17:44:41 +02:00
lucaronin
5102923c9c fix: repair release workflow parsing 2026-04-13 17:24:03 +02:00
lucaronin
87be231f9f test: format git status assertions 2026-04-13 15:30:17 +02:00
lucaronin
557a45189d fix: show diff stats in changes list 2026-04-13 15:22:21 +02:00
lucaronin
2107764550 fix: cap properties panel label column width 2026-04-13 15:01:07 +02:00
lucaronin
0a52ae6c1d fix: prevent stale raw content after note switch 2026-04-13 14:23:20 +02:00
lucaronin
487e8d0352 fix: preserve hard breaks when toggling editors 2026-04-13 14:04:25 +02:00
lucaronin
d4aacf1bea fix: focus command palette ai mode immediately 2026-04-13 13:42:46 +02:00
lucaronin
71df363bb6 fix: satisfy command palette ai push checks 2026-04-13 13:29:06 +02:00
lucaronin
91ab5be914 fix: stabilize command palette ai mode editing 2026-04-13 13:26:38 +02:00
lucaronin
9623203756 fix: restore blocknote code block theme 2026-04-13 12:59:51 +02:00
lucaronin
5897782a7e fix: stabilize new note title focus smoke 2026-04-13 12:46:22 +02:00
lucaronin
b4abc05f0e fix: harden legacy release channel fallback 2026-04-13 12:35:02 +02:00
lucaronin
8f408d9ef0 fix: guard blocknote table handles on raw return 2026-04-13 12:19:36 +02:00
lucaronin
1d778826e8 fix: restore empty editor drag region 2026-04-13 11:52:55 +02:00
lucaronin
5120d75903 fix: update getting_started.rs AGENTS template to use type: instead of is_a: 2026-04-13 08:37:40 +02:00
lucaronin
5d7426797f docs: add ADR-0060 and ADR-0061 for network-aware UI gating and AI prompt bridge (guard — from commits 739e1b3, 7933fe1) 2026-04-13 08:09:28 +02:00
lucaronin
ed54cef69e chore: update codescene thresholds after ADR additions 2026-04-13 08:05:30 +02:00
lucaronin
df6f956cde docs: add ADR-0058 and ADR-0059 for Claude Code onboarding gate and local-only commits (guard — from commits fd9de04, a13e36a) 2026-04-13 08:01:24 +02:00
lucaronin
61aae139c7 fix: remove stale daily note menu test id 2026-04-12 23:17:17 +02:00
lucaronin
14dc07084d fix: remove daily note command surfaces 2026-04-12 23:14:03 +02:00
lucaronin
73c1ecf8a9 fix: use muted editor code surfaces 2026-04-12 22:51:32 +02:00
lucaronin
de91167a28 fix: align note list header actions 2026-04-12 22:29:36 +02:00
lucaronin
a4397d48de fix: split alpha and stable release channels 2026-04-12 22:14:53 +02:00
lucaronin
fbbd53d204 test: restore smoke flows after Claude onboarding 2026-04-12 20:10:01 +02:00
lucaronin
fd9de04275 feat: add first-launch Claude Code onboarding 2026-04-12 19:56:10 +02:00
lucaronin
a13e36a504 feat: support local-only git commits without remotes 2026-04-12 19:38:25 +02:00
lucaronin
73a63085c7 fix: tighten vault switcher typing 2026-04-12 19:19:52 +02:00
lucaronin
739e1b3c20 feat: handle offline onboarding gracefully 2026-04-12 19:17:49 +02:00
lucaronin
e2745d96eb refactor: remove github auth integration 2026-04-12 17:08:07 +02:00
lucaronin
a2c84d3ce8 test: align AI smoke selector with inline input 2026-04-12 13:20:26 +02:00
lucaronin
afbea1367c fix: narrow inline wikilink selection lookup 2026-04-12 13:15:51 +02:00
lucaronin
7933fe1bfb feat: add inline AI prompts to command palette 2026-04-12 13:14:18 +02:00
lucaronin
d16aa29d18 docs: test notes must be deleted from disk after QA; update QA script paths to tolaria-qa 2026-04-12 12:31:35 +02:00
lucaronin
6188395141 fix: align desktop test bridge types 2026-04-12 12:29:13 +02:00
lucaronin
1bf5a7d6e4 fix: preserve resized tables across raw mode toggles 2026-04-12 11:45:59 +02:00
lucaronin
27e371c7fe docs: restore README content lost during Laputa→Tolaria rename 2026-04-12 10:40:50 +02:00
lucaronin
867e97b199 fix: left-align property values in inspector 2026-04-12 02:03:54 +02:00
lucaronin
2441518cee fix: restore breadcrumb center drag region 2026-04-12 01:52:31 +02:00
lucaronin
010bc32ee1 style: format Rust rename updates 2026-04-12 01:41:26 +02:00
lucaronin
361898b187 refactor: rename app branding from Laputa to Tolaria 2026-04-12 01:35:34 +02:00
lucaronin
5b5f949c74 fix: initialize new notes with empty h1 focus 2026-04-12 00:58:11 +02:00
lucaronin
57a5693922 fix: stabilize new note editor focus 2026-04-12 00:06:49 +02:00
lucaronin
2ca8f1b2a6 fix: remove legacy title section fallback 2026-04-11 23:51:58 +02:00
lucaronin
eb65bb8f05 fix: dedupe new note command palette entries 2026-04-11 22:05:13 +02:00
lucaronin
8fb229ede3 fix: preserve square brackets in parsed note titles 2026-04-11 21:27:30 +02:00
lucaronin
ce84b34890 fix: restore cmd shift i properties shortcut 2026-04-11 20:59:16 +02:00
lucaronin
e98a186389 fix: align note list date row spacing 2026-04-11 20:30:48 +02:00
lucaronin
258b54b074 refactor: make shortcut QA modes explicit 2026-04-11 19:03:15 +02:00
lucaronin
f694b9b5e4 fix: unblock native ai panel shortcut in tauri 2026-04-11 18:34:39 +02:00
lucaronin
32ee2b781b test: stabilize keyboard shortcut smoke 2026-04-11 17:36:23 +02:00
lucaronin
c78793cfe3 fix: verify editor-focused keyboard shortcuts 2026-04-11 17:23:20 +02:00
lucaronin
cd9e5fc403 fix: guard table resize against stale editor views 2026-04-11 16:37:44 +02:00
lucaronin
798a6b2125 refactor: make shortcut execution renderer-first 2026-04-11 15:42:53 +02:00
lucaronin
dbf54657f0 fix: harden untitled h1 auto-rename flow 2026-04-11 15:22:34 +02:00
lucaronin
0c365eb7dd test: cover raw editor keyboard toggle 2026-04-11 14:23:25 +02:00
lucaronin
20b789271d fix: verify view row hover state 2026-04-11 14:06:03 +02:00
lucaronin
36d3c8731b fix: hide view counts while row actions are visible 2026-04-11 13:52:35 +02:00
lucaronin
e412fa8fd7 fix: restore breadcrumb action icon size 2026-04-11 13:33:42 +02:00
lucaronin
76c37cf783 fix: narrow shortcut dispatcher route handling 2026-04-11 13:02:31 +02:00
lucaronin
4b60b9539d refactor: centralize shortcut routing metadata 2026-04-11 12:59:11 +02:00
lucaronin
69e520b5aa fix: prevent cmd+n note creation crash 2026-04-11 12:30:05 +02:00
lucaronin
272f2c0b3c design: replace app icon with Tolaria drop icon 2026-04-11 12:12:12 +02:00
lucaronin
d7bdab5b2e fix: relax test bridge command typing 2026-04-11 11:07:52 +02:00
lucaronin
5a5ea8d6f0 fix: align test bridge window typing 2026-04-11 11:04:47 +02:00
lucaronin
86306dc9de test: stabilize keyboard menu smoke bridge 2026-04-11 10:59:38 +02:00
lucaronin
974ca6148b fix: return false for unhandled app commands 2026-04-11 10:44:45 +02:00
lucaronin
b1bc056afb refactor: unify shortcut command routing 2026-04-11 10:39:08 +02:00
lucaronin
c24f60c594 fix: restore breadcrumb action icon sizing 2026-04-10 21:22:27 +02:00
lucaronin
71a3be577d fix: always show breadcrumb filename 2026-04-10 21:08:45 +02:00
lucaronin
717fa9d1a6 fix: prefer note icons in favorite rows 2026-04-10 20:58:18 +02:00
lucaronin
78e76e0d54 fix: defer native menu shortcuts in tauri 2026-04-10 20:43:36 +02:00
lucaronin
b01c6b4a4b fix: format rename rust helpers 2026-04-10 20:25:19 +02:00
lucaronin
db4359981f fix: update rename filename wikilinks 2026-04-10 20:20:08 +02:00
lucaronin
98d19e4c41 fix: align created label to right edge in note list date row 2026-04-10 20:01:40 +02:00
lucaronin
5ebffc447b feat: standardize canonical wikilink targets 2026-04-10 19:57:21 +02:00
lucaronin
a44dd41f95 feat: style note list status chips 2026-04-10 18:31:51 +02:00
lucaronin
eb67b98d96 test: scope quick open smoke locators 2026-04-10 18:14:52 +02:00
lucaronin
968c4d05a9 feat: add breadcrumb filename rename controls 2026-04-10 17:59:21 +02:00
lucaronin
c8db92c92d fix: debounce untitled H1 filename sync 2026-04-10 17:18:48 +02:00
2359 changed files with 84489 additions and 47937 deletions

View File

@@ -1,2 +1,2 @@
HOTSPOT_THRESHOLD=9.68
AVERAGE_THRESHOLD=9.33
HOTSPOT_THRESHOLD=9.97
AVERAGE_THRESHOLD=9.85

View File

@@ -1,5 +1,10 @@
# Copy to .env.local and fill in real values
# These are never committed — .env.local is gitignored
# Copy to .env.local and fill in real values.
# These are never committed — .env.local is gitignored.
# Release workflows must mirror these values into GitHub Actions secrets:
# - VITE_SENTRY_DSN
# - SENTRY_DSN (same value as VITE_SENTRY_DSN for Rust-side crash reporting)
# - VITE_POSTHOG_KEY
# - VITE_POSTHOG_HOST
# Sentry DSN (https://sentry.io → Project → Settings → Client Keys)
VITE_SENTRY_DSN=

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
custom: https://refactoring.fm/

2
.github/HOOKS.md vendored
View File

@@ -49,7 +49,7 @@ If the remote CodeScene scores are better than the current thresholds, the hook
## Legacy Files
The legacy `pre-commit` and `post-commit` files under `.github/hooks/` are archival only. Do not copy them into `.git/hooks`; use Husky and `.husky/` instead. `install-hooks.sh` remains as a reinstall helper that runs Husky.
The legacy `pre-commit` file under `.github/hooks/` is archival only. Do not copy it into `.git/hooks`; use Husky and `.husky/` instead. The old design `post-commit` auto-implementation hook was removed because it depended on obsolete one-off scripts. `install-hooks.sh` remains as a reinstall helper that runs Husky.
## Troubleshooting

26
.github/SETUP.md vendored
View File

@@ -14,6 +14,26 @@ Nel repository GitHub (Settings → Secrets and variables → Actions → New re
**CODESCENE_PROJECT_ID**
Trova l'ID del progetto nella dashboard CodeScene (URL: `https://codescene.io/projects/<PROJECT_ID>/...`)
**VITE_SENTRY_DSN**
```
<frontend Sentry DSN used by shipped Tolaria builds>
```
**SENTRY_DSN**
```
<same DSN as VITE_SENTRY_DSN, passed to the Rust/Tauri build for native crash reporting>
```
**VITE_POSTHOG_KEY**
```
<PostHog project API key used by shipped Tolaria builds>
```
**VITE_POSTHOG_HOST**
```
https://eu.i.posthog.com
```
### 2. Enable GitHub Actions
- Vai su Settings → Actions → General
@@ -65,6 +85,12 @@ cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check
- **Fail se code health diminuisce**
- Confronta HEAD vs base branch
### 📡 Telemetry In Release Builds
- `release.yml` e `release-stable.yml` devono ricevere `VITE_SENTRY_DSN`, `SENTRY_DSN`, `VITE_POSTHOG_KEY`, `VITE_POSTHOG_HOST`
- `VITE_SENTRY_DSN` inizializza il frontend Sentry bundle
- `SENTRY_DSN` inizializza Sentry nel binary Rust/Tauri
- `VITE_POSTHOG_KEY` / `VITE_POSTHOG_HOST` permettono ai build distribuiti di inizializzare PostHog quando l'utente abilita analytics
### 📝 Documentation
- **Warning se modifichi `src/` o `src-tauri/` ma non aggiorni `docs/`**
- Non blocca il merge, solo un reminder

View File

@@ -1,72 +0,0 @@
#!/bin/bash
# Post-commit hook: Auto-implement design changes via Claude Code
# Copy to .git/hooks/post-commit and make executable
set -e
# Check if ui-design.pen was modified in this commit
if ! git diff --name-only HEAD~1 HEAD | grep -q "ui-design.pen"; then
exit 0
fi
echo ""
echo "🎨 Design file changed - analyzing..."
# Run analyzer
ANALYZER_OUTPUT=$(node scripts/design-diff-analyzer.js --json 2>&1)
if [ $? -eq 0 ]; then
echo "✅ No implementation tasks needed (content-only changes)"
exit 0
fi
# Extract tasks from JSON
TASKS_JSON=$(echo "$ANALYZER_OUTPUT" | sed -n '/---JSON---/,$ p' | tail -n +2)
if [ -z "$TASKS_JSON" ]; then
echo "✅ No significant design changes"
exit 0
fi
echo "$ANALYZER_OUTPUT" | sed '/---JSON---/,$ d'
# Generate Claude Code prompt
PROMPT=$(cat <<EOF
The design file ui-design.pen was just updated. Analyze the changes and implement them.
Design changes detected:
$ANALYZER_OUTPUT
Tasks:
1. Review the git diff of ui-design.pen (HEAD vs HEAD~1)
2. Identify what changed (colors, typography, spacing, layout)
3. Update the corresponding code:
- Colors/Typography/Spacing → update src/theme.json
- Layout changes → update React components
- New elements → create new components
4. Test visually in Chrome (pnpm dev, open localhost:5173)
5. Run tests: pnpm test
6. Commit changes with descriptive message referencing the design update
When completely finished, run:
openclaw system event --text "✅ Design changes implemented and tested" --mode now
EOF
)
echo ""
echo "🚀 Spawning Claude Code to implement changes..."
echo ""
# Spawn Claude Code via OpenClaw
openclaw sessions spawn \
--task "$PROMPT" \
--label "design-auto-implement" \
--cleanup delete \
--timeout-seconds 600 \
--agent-id main
echo ""
echo "✅ Claude Code spawned - you'll be notified when implementation is complete"
echo ""
exit 0

View File

@@ -10,6 +10,7 @@ Il workflow `ci.yml` esegue i seguenti check automatici:
### 2. Test Coverage
- Frontend: vitest con coverage reporting
- Upload automatico su Codecov dai report LCOV frontend + Rust
- Threshold configurabile in `vitest.config.ts`
### 3. Code Health (CodeScene)
@@ -40,6 +41,23 @@ CODESCENE_PROJECT_ID=<your-project-id>
Il PAT di CodeScene è lo stesso che usi localmente (~/.codescene/token).
Il project ID lo trovi nella dashboard CodeScene.
### Codecov Setup
- Installa/attiva il repo in Codecov una volta sola tramite GitHub App / import del repository.
- Nessun `CODECOV_TOKEN` richiesto in GitHub Actions: `ci.yml` usa OIDC (`id-token: write` + `use_oidc: true`).
- Il workflow carica `coverage/lcov.info` (Vitest) e `coverage/rust.lcov` (cargo-llvm-cov).
### Telemetry Secrets For Release Builds
Aggiungi anche questi secrets per i workflow `release.yml` e `release-stable.yml`:
```
VITE_SENTRY_DSN=<frontend sentry dsn>
SENTRY_DSN=<same dsn for rust/native crash reporting>
VITE_POSTHOG_KEY=<posthog project api key>
VITE_POSTHOG_HOST=https://eu.i.posthog.com
```
Senza questi valori, i build distribuiti possono mantenere i toggle telemetry nelle Settings ma non inizializzare davvero PostHog/Sentry.
### Coverage Thresholds
Configura in `vitest.config.ts`:
@@ -85,8 +103,11 @@ codescene delta-analysis --base-revision origin/main
## Workflow Triggers
- **Push**: su `main` e branch `experiment/*`
- **Push**: su `main`
- **Pull Request**: verso `main`
- **Manuale**: `workflow_dispatch`
Nota: l'upload a Codecov gira su push a `main` e sulle PR dello stesso repository. Le PR da fork saltano l'upload per evitare problemi di permessi OIDC.
## Status Checks

View File

@@ -3,6 +3,17 @@ name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
id-token: write
env:
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
jobs:
test:
@@ -37,9 +48,9 @@ jobs:
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
@@ -74,10 +85,23 @@ jobs:
cargo llvm-cov \
--manifest-path src-tauri/Cargo.toml \
--ignore-filename-regex 'lib\.rs|main\.rs|menu\.rs' \
--lcov \
--output-path coverage/rust.lcov \
--fail-under-lines 85
# cargo-llvm-cov exits non-zero if line coverage drops below 85%
# lib.rs/main.rs/menu.rs are Tauri boilerplate -- not meaningfully unit-testable.
- name: Upload coverage to Codecov
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: codecov/codecov-action@v5
with:
use_oidc: true
fail_ci_if_error: true
disable_search: true
files: ./coverage/lcov.info,./coverage/rust.lcov
verbose: true
# OIDC avoids long-lived CODECOV_TOKEN secrets.
# ── 3. Code Health (CodeScene — Hotspot + Average Code Health gates) ──
# Enforces minimum floors on BOTH hotspot and average code health.
# Thresholds come from .codescene-thresholds so CI and local hooks match.
@@ -143,3 +167,63 @@ jobs:
- name: Format check (Rust)
run: cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check
linux-build:
name: Linux build verification
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Tauri Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libsoup-3.0-dev \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
build-essential \
file
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Frontend build
run: pnpm build
- name: Cargo check
run: cargo check --manifest-path=src-tauri/Cargo.toml
- name: Clippy
run: cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings

View File

@@ -1,286 +0,0 @@
name: Release (Canary)
on:
push:
branches:
- canary
concurrency:
group: release-canary-${{ github.ref }}
cancel-in-progress: true
jobs:
# ─────────────────────────────────────────────────────────────
# Phase 1: Compute the canary version string
# ─────────────────────────────────────────────────────────────
version:
name: Compute version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
tag: ${{ steps.ver.outputs.tag }}
steps:
- id: ver
run: |
VERSION="0.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}-canary"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
echo "### Canary version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────
# Phase 2: Build each architecture in parallel
# ─────────────────────────────────────────────────────────────
build:
name: Build (${{ matrix.arch }})
needs: version
runs-on: macos-15
strategy:
fail-fast: true
matrix:
include:
- arch: aarch64
target: aarch64-apple-darwin
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Bun (required for bundle-qmd.sh)
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Import Apple Developer certificate into keychain
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Build Tauri app (with signing + notarization)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
pnpm tauri build --target ${{ matrix.target }}
- name: Upload .dmg
uses: actions/upload-artifact@v4
with:
name: dmg-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
retention-days: 1
- name: Upload updater artifacts (.tar.gz + .sig)
uses: actions/upload-artifact@v4
with:
name: updater-${{ matrix.arch }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
retention-days: 1
# ─────────────────────────────────────────────────────────────
# Phase 3: Publish GitHub Release (prerelease)
# ─────────────────────────────────────────────────────────────
release:
name: GitHub Release (canary)
needs: [version, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Generate release notes
run: |
PREV_TAG=$(git tag --sort=-version:refname | grep canary | head -n 1 || echo "")
if [ -z "$PREV_TAG" ]; then
NOTES=$(git log --oneline --no-merges -20)
else
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD")
fi
{
echo "## What's Changed (Canary)"
echo ""
echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
echo ""
echo "---"
echo "**Canary build — pre-release, may be unstable**"
echo ""
echo "**Requires Apple Silicon (M1/M2/M3)**"
echo ""
echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*"
} > release_notes.md
- name: Build latest-canary.json
run: |
VERSION="${{ needs.version.outputs.version }}"
TAG="${{ needs.version.outputs.tag }}"
REPO="refactoringhq/laputa-app"
ARM_SIG=$(cat updater-aarch64/*.app.tar.gz.sig)
ARM_TARBALL=$(ls updater-aarch64/*.app.tar.gz | xargs basename)
cat > latest-canary.json << EOF
{
"version": "${VERSION}",
"notes": "Canary build. See https://refactoringhq.github.io/laputa-app/ for release notes.",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"darwin-aarch64": {
"signature": "${ARM_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}"
}
}
}
EOF
echo "latest-canary.json:"; cat latest-canary.json
- name: Publish GitHub Release (prerelease)
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.version.outputs.tag }}
name: Laputa ${{ needs.version.outputs.version }} (Canary)
body_path: release_notes.md
draft: false
prerelease: true
files: |
dmg-aarch64/*.dmg
updater-aarch64/*.app.tar.gz
updater-aarch64/*.app.tar.gz.sig
latest-canary.json
# ─────────────────────────────────────────────────────────────
# Phase 4: Update GitHub Pages (preserve stable latest.json)
# ─────────────────────────────────────────────────────────────
pages:
name: Update release history page
needs: [version, release]
runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: github-pages
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- name: Build release history page
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p _site
gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json
# Download stable latest.json from existing GH Pages (preserve it)
curl -fsSL "https://refactoringhq.github.io/laputa-app/latest.json" -o _site/latest.json || echo '{}' > _site/latest.json
# Copy canary latest.json from this release
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "latest-canary.json" --output _site/latest-canary.json || true
cat > _site/index.html << 'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laputa — Release History</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #F7F6F3; color: #37352F; line-height: 1.6; padding: 2rem; max-width: 720px; margin: 0 auto; }
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.5rem; }
.subtitle { color: #787774; margin-bottom: 2rem; }
.release { background: #fff; border: 1px solid #E9E9E7; border-radius: 8px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; }
.release h2 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.25rem; }
.release .meta { font-size: 0.8125rem; color: #787774; margin-bottom: 0.75rem; }
.release .body { font-size: 0.875rem; white-space: pre-wrap; }
.release .downloads { margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap; }
.release .downloads a { display: inline-block; padding: 0.375rem 0.75rem; background: #155DFF; color: #fff; border-radius: 6px; text-decoration: none; font-size: 0.8125rem; font-weight: 500; }
.release .downloads a:hover { background: #1248CC; }
.canary { border-left: 3px solid #f59e0b; }
.empty { color: #787774; text-align: center; padding: 3rem; }
</style>
</head>
<body>
<h1>Laputa Release History</h1>
<p class="subtitle">Auto-updated on every release</p>
<div id="releases"></div>
<script>
fetch('releases.json').then(r=>r.json()).then(releases=>{
const el=document.getElementById('releases');
if(!releases.length){el.innerHTML='<p class="empty">No releases yet.</p>';return;}
releases.forEach(r=>{
const date=new Date(r.published_at).toLocaleDateString('en-US',{year:'numeric',month:'long',day:'numeric'});
const dmgs=(r.assets||[]).filter(a=>a.name.endsWith('.dmg'));
const links=dmgs.map(a=>'<a href="'+a.browser_download_url+'">'+a.name+'</a>').join('');
const body=(r.body||'').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const div=document.createElement('div');
div.className='release'+(r.prerelease?' canary':'');
div.innerHTML='<h2>'+(r.name||r.tag_name)+'</h2><div class="meta">'+date+' · '+r.tag_name+(r.prerelease?' · <strong>Canary</strong>':'')+'</div><div class="body">'+body+'</div>'+(links?'<div class="downloads">'+links+'</div>':'');
el.appendChild(div);
});
});
</script>
</body>
</html>
HTMLEOF
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_site
commit_message: "Update release history for canary ${{ needs.version.outputs.tag }}"

667
.github/workflows/release-stable.yml vendored Normal file
View File

@@ -0,0 +1,667 @@
name: Release (Stable)
on:
push:
tags:
- 'stable-v*'
env:
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
concurrency:
group: release-stable-${{ github.ref }}
cancel-in-progress: true
jobs:
# ─────────────────────────────────────────────────────────────
# Phase 1: Compute the stable version string once
# ─────────────────────────────────────────────────────────────
version:
name: Compute stable version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
display_version: ${{ steps.ver.outputs.display_version }}
tag: ${{ steps.ver.outputs.tag }}
steps:
- id: ver
shell: bash
run: |
python3 <<'PY' > version.env
import os
import re
from datetime import date
tag = os.environ["GITHUB_REF_NAME"]
version = tag.removeprefix("stable-v")
match = re.fullmatch(r"(\d{4})\.(\d{1,2})\.(\d{1,2})", version)
if not match:
raise SystemExit(f"Stable tags must use stable-vYYYY.M.D, got {tag}")
date(*map(int, match.groups()))
print(f"version={version}")
print(f"display_version={version}")
print(f"tag={tag}")
PY
cat version.env >> "$GITHUB_OUTPUT"
DISPLAY_VERSION=$(grep '^display_version=' version.env | cut -d= -f2-)
echo "### Stable version: \`$DISPLAY_VERSION\`" >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────
# Phase 2: Build release bundles in parallel
# ─────────────────────────────────────────────────────────────
build:
name: Build (${{ matrix.arch }})
needs: version
runs-on: macos-15
strategy:
fail-fast: true
matrix:
include:
- arch: aarch64
target: aarch64-apple-darwin
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Bun (required for bundle-qmd.sh)
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/${{ matrix.target }}/release/bundle
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i '' "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Import Apple Developer certificate into keychain
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Validate telemetry env
env:
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
python3 <<'PY'
import os
import re
import sys
from urllib.parse import urlparse
DISALLOWED_PLACEHOLDERS = {
"",
"-",
"_",
"false",
"true",
"null",
"undefined",
"none",
"disabled",
}
def normalize(name: str) -> str:
value = os.getenv(name, "").strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1].strip()
return value
def normalize_http_like(value: str) -> str:
if "://" in value:
return value
return f"https://{value}"
def normalize_hostname(hostname: str) -> str:
normalized = hostname.strip().rstrip('.').lower()
if normalized.startswith('[') and normalized.endswith(']'):
normalized = normalized[1:-1]
return normalized
def is_ip_address(hostname: str) -> bool:
if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname):
return all(0 <= int(part) <= 255 for part in hostname.split('.'))
return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None
def is_allowed_hostname(hostname: str) -> bool:
normalized = normalize_hostname(hostname)
if not normalized or normalized in DISALLOWED_PLACEHOLDERS:
return False
if normalized == 'localhost':
return True
return '.' in normalized or is_ip_address(normalized)
def is_http_url(value: str) -> bool:
parsed = urlparse(normalize_http_like(value))
return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "")
values = {
name: normalize(name)
for name in (
"VITE_SENTRY_DSN",
"SENTRY_DSN",
"VITE_POSTHOG_KEY",
"VITE_POSTHOG_HOST",
)
}
errors = []
for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"):
value = values[name]
if value.lower() in DISALLOWED_PLACEHOLDERS:
errors.append(f"{name} must be set to a real value, not a placeholder")
elif not is_http_url(value):
errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host")
if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS:
errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder")
if errors:
print("Telemetry env validation failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
raise SystemExit(1)
print("Telemetry env validation passed.")
PY
- name: Build Tauri app (with signing + notarization)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target ${{ matrix.target }}
- name: Upload .dmg
uses: actions/upload-artifact@v4
with:
name: dmg-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
retention-days: 1
- name: Upload updater artifacts (.tar.gz + .sig)
uses: actions/upload-artifact@v4
with:
name: updater-${{ matrix.arch }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
retention-days: 1
build-linux:
name: Build (linux-x86_64)
needs: version
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Tauri Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libsoup-3.0-dev \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
curl \
wget \
patchelf \
build-essential \
file
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Build Tauri app (Linux bundles)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage
- name: Validate Linux bundles
run: |
shopt -s nullglob
installers=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
)
signatures=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
)
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage or deb bundle."
exit 1
fi
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Linux build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Linux bundles
uses: actions/upload-artifact@v4
with:
name: linux-x86_64-bundles
path: |
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
if-no-files-found: error
retention-days: 1
build-windows:
name: Build (windows-x86_64)
needs: version
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~\.cargo\registry
~\.cargo\git
src-tauri\target
key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached Windows bundle artifacts
shell: pwsh
run: |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
- name: Set version
shell: pwsh
run: |
$version = "${{ needs.version.outputs.version }}"
$tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json
$tauri.version = $version
$tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json"
(Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml"
- name: Validate Windows release env
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
run: |
for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do
if [ -z "${!name}" ]; then
echo "::error::$name is required to build signed Windows updater artifacts."
exit 1
fi
done
- name: Build Tauri app (Windows bundles)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis
- name: Validate Windows bundles
shell: bash
run: |
shopt -s nullglob
installers=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
)
signatures=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig
)
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Windows build produced no installable NSIS or MSI bundle."
exit 1
fi
for installer in "${installers[@]}"; do
if [[ "$(basename "$installer")" != *"${{ needs.version.outputs.version }}"* ]]; then
echo "::error::Windows build produced an installer for a different version: $(basename "$installer")"
exit 1
fi
done
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Windows build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Windows bundles
uses: actions/upload-artifact@v4
with:
name: windows-x86_64-bundles
path: |
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig
if-no-files-found: error
retention-days: 1
# ─────────────────────────────────────────────────────────────
# Phase 3: Publish GitHub Release
# ─────────────────────────────────────────────────────────────
release:
name: GitHub Release (stable)
needs: [version, build, build-linux, build-windows]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Generate release notes
run: |
PREV_TAG=$(git tag --list 'stable-v*' --sort=-version:refname | grep -vx "${{ needs.version.outputs.tag }}" | head -n 1 || echo "")
if [ -z "$PREV_TAG" ]; then
NOTES=$(git log --oneline --no-merges -20)
else
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..${{ needs.version.outputs.tag }}")
fi
{
echo "## What's Changed"
echo ""
echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
echo ""
echo "---"
echo "**Stable release — manually promoted from \`main\`**"
echo ""
echo "**Includes macOS (Apple Silicon), Windows x64, and Linux x64 bundles**"
echo ""
echo "*Built from \`$(git rev-parse --short ${{ needs.version.outputs.tag }})\` on $(date -u +%Y-%m-%d)*"
} > release_notes.md
- name: Build stable-latest.json
run: |
VERSION="${{ needs.version.outputs.version }}"
TAG="${{ needs.version.outputs.tag }}"
REPO="${GITHUB_REPOSITORY}"
REPO_NAME="${REPO#*/}"
PAGES_URL="https://refactoringhq.github.io/${REPO_NAME}/"
find_required() {
local patterns=("$@")
for pattern in "${patterns[@]}"; do
set -- $pattern
if [ -e "$1" ]; then
printf '%s\n' "$1"
return 0
fi
done
echo "::error::Missing required artifact matching one of: ${patterns[*]}" >&2
return 1
}
ARM_SIG_FILE=$(find_required "updater-aarch64/*.app.tar.gz.sig")
ARM_UPDATER_FILE="${ARM_SIG_FILE%.sig}"
ARM_SIG=$(cat "$ARM_SIG_FILE")
ARM_TARBALL=$(basename "$ARM_UPDATER_FILE")
ARM_DMG=$(basename "$(find_required "dmg-aarch64/*.dmg")")
LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig")
LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}"
LINUX_SIG=$(cat "$LINUX_SIG_FILE")
LINUX_UPDATER=$(basename "$LINUX_UPDATER_FILE")
LINUX_DOWNLOAD=$(basename "$(find_required "linux-x86_64-bundles/*/*.AppImage" "linux-x86_64-bundles/*/*.deb" "linux-x86_64-bundles/*/*.AppImage.tar.gz" "linux-x86_64-bundles/*.AppImage" "linux-x86_64-bundles/*.deb" "linux-x86_64-bundles/*.AppImage.tar.gz")")
WINDOWS_SIG_FILE=$(find_required "windows-x86_64-bundles/*/*-setup.exe.sig" "windows-x86_64-bundles/*/*.msi.sig" "windows-x86_64-bundles/*/*.nsis.zip.sig" "windows-x86_64-bundles/*/*.msi.zip.sig" "windows-x86_64-bundles/*-setup.exe.sig" "windows-x86_64-bundles/*.msi.sig" "windows-x86_64-bundles/*.nsis.zip.sig" "windows-x86_64-bundles/*.msi.zip.sig")
WINDOWS_UPDATER_FILE="${WINDOWS_SIG_FILE%.sig}"
WINDOWS_SIG=$(cat "$WINDOWS_SIG_FILE")
WINDOWS_UPDATER=$(basename "$WINDOWS_UPDATER_FILE")
WINDOWS_DOWNLOAD=$(basename "$(find_required "windows-x86_64-bundles/*/*-setup.exe" "windows-x86_64-bundles/*/*.msi" "windows-x86_64-bundles/*/*.nsis.zip" "windows-x86_64-bundles/*/*.msi.zip" "windows-x86_64-bundles/*-setup.exe" "windows-x86_64-bundles/*.msi" "windows-x86_64-bundles/*.nsis.zip" "windows-x86_64-bundles/*.msi.zip")")
cat > stable-latest.json << EOF
{
"version": "${VERSION}",
"notes": "Stable release. See ${PAGES_URL} for full release notes.",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"darwin-aarch64": {
"signature": "${ARM_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}",
"dmg_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_DMG}"
},
"linux-x86_64": {
"signature": "${LINUX_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_DOWNLOAD}"
},
"windows-x86_64": {
"signature": "${WINDOWS_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_DOWNLOAD}"
}
}
}
EOF
echo "stable-latest.json:"; cat stable-latest.json
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.version.outputs.tag }}
name: Tolaria ${{ needs.version.outputs.display_version }}
body_path: release_notes.md
draft: false
prerelease: false
files: |
dmg-aarch64/*.dmg
updater-aarch64/*.app.tar.gz
updater-aarch64/*.app.tar.gz.sig
linux-x86_64-bundles/*.deb
linux-x86_64-bundles/*.deb.sig
linux-x86_64-bundles/*.AppImage
linux-x86_64-bundles/*.AppImage.sig
linux-x86_64-bundles/*.AppImage.tar.gz
linux-x86_64-bundles/*.AppImage.tar.gz.sig
linux-x86_64-bundles/*/*.deb
linux-x86_64-bundles/*/*.deb.sig
linux-x86_64-bundles/*/*.AppImage
linux-x86_64-bundles/*/*.AppImage.sig
linux-x86_64-bundles/*/*.AppImage.tar.gz
linux-x86_64-bundles/*/*.AppImage.tar.gz.sig
windows-x86_64-bundles/*.exe
windows-x86_64-bundles/*.exe.sig
windows-x86_64-bundles/*.msi
windows-x86_64-bundles/*.msi.sig
windows-x86_64-bundles/*.zip
windows-x86_64-bundles/*.zip.sig
windows-x86_64-bundles/*/*.exe
windows-x86_64-bundles/*/*.exe.sig
windows-x86_64-bundles/*/*.msi
windows-x86_64-bundles/*/*.msi.sig
windows-x86_64-bundles/*/*.zip
windows-x86_64-bundles/*/*.zip.sig
stable-latest.json
# ─────────────────────────────────────────────────────────────
# Phase 4: Update GitHub Pages
# ─────────────────────────────────────────────────────────────
pages:
name: Update release history page
needs: [version, release]
runs-on: ubuntu-latest
permissions:
contents: write
concurrency:
group: github-pages
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build release history page
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p _site/alpha _site/stable
gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > _site/releases.json
PAGES_URL="https://refactoringhq.github.io/${GITHUB_REPOSITORY#*/}"
curl -fsSL "${PAGES_URL}/alpha/latest.json" -o _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "stable-latest.json" --output _site/stable/latest.json || echo '{}' > _site/stable/latest.json
bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html
bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/index.html
mkdir -p _site/download
cp _site/stable/download/index.html _site/download/index.html
cp _site/alpha/latest.json _site/latest.json
cp _site/alpha/latest.json _site/latest-canary.json
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_site
commit_message: "Update release history for ${{ needs.version.outputs.tag }}"

View File

@@ -1,31 +1,111 @@
name: Release
name: Release (Alpha)
on:
push:
branches:
- main
env:
# Bump this when Tauri/Rust target artifacts capture stale absolute paths.
RUST_TARGET_CACHE_VERSION: v2026-04-14-tolaria
concurrency:
group: release-${{ github.ref }}
group: release-alpha-${{ github.ref }}
cancel-in-progress: true
jobs:
# ─────────────────────────────────────────────────────────────
# Phase 1: Compute the version string once
# Phase 1: Compute the alpha version string once
# Alpha builds use calendar semver and stay newer than the latest stable tag.
# ─────────────────────────────────────────────────────────────
version:
name: Compute version
name: Compute alpha version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
display_version: ${{ steps.ver.outputs.display_version }}
tag: ${{ steps.ver.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: ver
shell: bash
run: |
VERSION="0.$(date -u +%Y%m%d).${GITHUB_RUN_NUMBER}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
echo "### Version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY"
python3 <<'PY' > version.env
import re
import subprocess
from datetime import datetime, timedelta, timezone
def lines(command: list[str]) -> list[str]:
output = subprocess.check_output(command, text=True).strip()
return [line for line in output.splitlines() if line]
alpha_pattern = re.compile(r"^alpha-v(\d{4}\.\d{1,2}\.\d{1,2})-alpha\.(\d+)$")
def parse_alpha_tag(tag: str) -> tuple[str, int] | None:
match = alpha_pattern.fullmatch(tag)
if not match:
return None
calendar_version, sequence = match.groups()
return calendar_version, int(sequence)
def alpha_version(calendar_version: str, sequence: int) -> str:
return f"{calendar_version}-alpha.{sequence}"
def alpha_tag(calendar_version: str, sequence: int) -> str:
return f"alpha-v{calendar_version}-alpha.{sequence:04d}"
existing_tags = [
tag for tag in lines(["git", "tag", "--points-at", "HEAD"])
if tag.startswith("alpha-v")
]
if existing_tags:
tag = existing_tags[0]
parsed = parse_alpha_tag(tag)
version = alpha_version(*parsed) if parsed is not None else tag.removeprefix("alpha-v")
else:
today = datetime.now(timezone.utc).date()
stable_date = None
stable_pattern = re.compile(r"^stable-v(\d{4})\.(\d{1,2})\.(\d{1,2})$")
for stable_tag in lines(["git", "tag", "--list", "stable-v*", "--sort=-version:refname"]):
match = stable_pattern.fullmatch(stable_tag)
if not match:
continue
year, month, day = map(int, match.groups())
try:
stable_date = datetime(year, month, day, tzinfo=timezone.utc).date()
except ValueError:
continue
break
alpha_date = today if stable_date is None or today > stable_date else stable_date + timedelta(days=1)
calendar_version = f"{alpha_date.year}.{alpha_date.month}.{alpha_date.day}"
sequence = len(lines(["git", "tag", "--list", f"alpha-v{calendar_version}-alpha.*"])) + 1
version = alpha_version(calendar_version, sequence)
tag = alpha_tag(calendar_version, sequence)
display_match = re.fullmatch(r"(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)", version)
display_version = (
f"Alpha {int(display_match.group(1))}.{int(display_match.group(2))}.{int(display_match.group(3))}.{int(display_match.group(4))}"
if display_match
else version
)
print(f"version={version}")
print(f"display_version={display_version}")
print(f"tag={tag}")
PY
cat version.env >> "$GITHUB_OUTPUT"
VERSION=$(grep '^version=' version.env | cut -d= -f2-)
DISPLAY_VERSION=$(grep '^display_version=' version.env | cut -d= -f2-)
echo "### Alpha version: \`$DISPLAY_VERSION\` (\`$VERSION\`)" >> "$GITHUB_STEP_SUMMARY"
# ─────────────────────────────────────────────────────────────
# Phase 2: Build each architecture in parallel
@@ -72,13 +152,17 @@ jobs:
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
key: ${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-
${{ runner.os }}-release-cargo-${{ matrix.target }}-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/${{ matrix.target }}/release/bundle
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
@@ -90,7 +174,6 @@ jobs:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
# Decode and import the certificate so codesign can use it in beforeBuildCommand
CERT_PATH="$RUNNER_TEMP/apple_cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/laputa-signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
@@ -103,6 +186,95 @@ jobs:
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
- name: Validate telemetry env
env:
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
python3 <<'PY'
import os
import re
import sys
from urllib.parse import urlparse
DISALLOWED_PLACEHOLDERS = {
"",
"-",
"_",
"false",
"true",
"null",
"undefined",
"none",
"disabled",
}
def normalize(name: str) -> str:
value = os.getenv(name, "").strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
value = value[1:-1].strip()
return value
def normalize_http_like(value: str) -> str:
if "://" in value:
return value
return f"https://{value}"
def normalize_hostname(hostname: str) -> str:
normalized = hostname.strip().rstrip('.').lower()
if normalized.startswith('[') and normalized.endswith(']'):
normalized = normalized[1:-1]
return normalized
def is_ip_address(hostname: str) -> bool:
if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname):
return all(0 <= int(part) <= 255 for part in hostname.split('.'))
return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None
def is_allowed_hostname(hostname: str) -> bool:
normalized = normalize_hostname(hostname)
if not normalized or normalized in DISALLOWED_PLACEHOLDERS:
return False
if normalized == 'localhost':
return True
return '.' in normalized or is_ip_address(normalized)
def is_http_url(value: str) -> bool:
parsed = urlparse(normalize_http_like(value))
return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "")
values = {
name: normalize(name)
for name in (
"VITE_SENTRY_DSN",
"SENTRY_DSN",
"VITE_POSTHOG_KEY",
"VITE_POSTHOG_HOST",
)
}
errors = []
for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"):
value = values[name]
if value.lower() in DISALLOWED_PLACEHOLDERS:
errors.append(f"{name} must be set to a real value, not a placeholder")
elif not is_http_url(value):
errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host")
if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS:
errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder")
if errors:
print("Telemetry env validation failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
raise SystemExit(1)
print("Telemetry env validation passed.")
PY
- name: Build Tauri app (with signing + notarization)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
@@ -113,15 +285,14 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target ${{ matrix.target }}
- name: Upload .dmg
uses: actions/upload-artifact@v4
with:
name: dmg-${{ matrix.arch }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
retention-days: 1
# Alpha releases only need the notarized app bundle and updater tarball.
# Skipping DMG packaging avoids fragile bundle_dmg.sh failures on macOS runners.
pnpm tauri build --target ${{ matrix.target }} --bundles app
- name: Upload updater artifacts (.tar.gz + .sig)
uses: actions/upload-artifact@v4
@@ -132,13 +303,243 @@ jobs:
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.app.tar.gz.sig
retention-days: 1
build-linux:
name: Build (linux-x86_64)
needs: version
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Tauri Linux system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libsoup-3.0-dev \
libxdo-dev \
libssl-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
curl \
wget \
patchelf \
build-essential \
file
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-unknown-linux-gnu-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached bundle artifacts
run: |
rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle
- name: Set version
run: |
VERSION="${{ needs.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > tmp.json && mv tmp.json src-tauri/tauri.conf.json
sed -i "s/^version = \".*\"/version = \"$VERSION\"/" src-tauri/Cargo.toml
- name: Build Tauri app (Linux bundles)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage
- name: Validate Linux bundles
run: |
shopt -s nullglob
installers=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
)
signatures=(
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
)
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Linux build produced no AppImage or deb bundle."
exit 1
fi
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Linux build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Linux bundles
uses: actions/upload-artifact@v4
with:
name: linux-x86_64-bundles
path: |
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig
if-no-files-found: error
retention-days: 1
build-windows:
name: Build (windows-x86_64)
needs: version
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~\.cargo\registry
~\.cargo\git
src-tauri\target
key: ${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-${{ hashFiles('src-tauri/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-release-cargo-x86_64-pc-windows-msvc-${{ env.RUST_TARGET_CACHE_VERSION }}-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Clear cached Windows bundle artifacts
shell: pwsh
run: |
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
- name: Set version
shell: pwsh
run: |
$version = "${{ needs.version.outputs.version }}"
$tauri = Get-Content "src-tauri/tauri.conf.json" | ConvertFrom-Json
$tauri.version = $version
$tauri | ConvertTo-Json -Depth 100 | Set-Content "src-tauri/tauri.conf.json"
(Get-Content "src-tauri/Cargo.toml") -replace '^version = ".*"$', "version = `"$version`"" | Set-Content "src-tauri/Cargo.toml"
- name: Validate Windows release env
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
run: |
for name in TAURI_SIGNING_PRIVATE_KEY TAURI_KEY_PASSWORD; do
if [ -z "${!name}" ]; then
echo "::error::$name is required to build signed Windows updater artifacts."
exit 1
fi
done
- name: Build Tauri app (Windows bundles)
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
VITE_POSTHOG_KEY: ${{ secrets.VITE_POSTHOG_KEY }}
VITE_POSTHOG_HOST: ${{ secrets.VITE_POSTHOG_HOST }}
run: |
pnpm tauri build --target x86_64-pc-windows-msvc --bundles nsis
- name: Validate Windows bundles
shell: bash
run: |
shopt -s nullglob
installers=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
)
signatures=(
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.nsis.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.zip.sig
)
if [ ${#installers[@]} -eq 0 ]; then
echo "::error::Windows build produced no installable NSIS or MSI bundle."
exit 1
fi
for installer in "${installers[@]}"; do
if [[ "$(basename "$installer")" != *"${{ needs.version.outputs.version }}"* ]]; then
echo "::error::Windows build produced an installer for a different version: $(basename "$installer")"
exit 1
fi
done
if [ ${#signatures[@]} -eq 0 ]; then
echo "::error::Windows build produced no updater signature (.sig) artifact."
exit 1
fi
- name: Upload Windows bundles
uses: actions/upload-artifact@v4
with:
name: windows-x86_64-bundles
path: |
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.zip.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi.sig
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip
src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.zip.sig
if-no-files-found: error
retention-days: 1
# ─────────────────────────────────────────────────────────────
# Phase 3: Publish GitHub Release
# No lipo/re-signing — use the per-arch artifacts directly
# ─────────────────────────────────────────────────────────────
release:
name: GitHub Release
needs: [version, build]
name: GitHub Release (alpha)
needs: [version, build, build-linux, build-windows]
runs-on: ubuntu-latest
permissions:
contents: write
@@ -152,60 +553,143 @@ jobs:
- name: Generate release notes
run: |
PREV_TAG=$(git tag --sort=-version:refname | head -n 1 || echo "")
PREV_TAG=$(python3 <<'PY'
import re
import subprocess
current_tag = '${{ needs.version.outputs.tag }}'
pattern = re.compile(r'^alpha-v(\d{4})\.(\d{1,2})\.(\d{1,2})-alpha\.(\d+)$')
output = subprocess.check_output(['git', 'tag', '--list', 'alpha-v*'], text=True).strip()
tags = [line for line in output.splitlines() if line and line != current_tag]
parsed_tags = []
for tag in tags:
match = pattern.fullmatch(tag)
if not match:
continue
year, month, day, sequence = map(int, match.groups())
parsed_tags.append(((year, month, day, sequence), tag))
print(max(parsed_tags)[1] if parsed_tags else '')
PY
)
if [ -z "$PREV_TAG" ]; then
NOTES=$(git log --oneline --no-merges -20)
else
NOTES=$(git log --oneline --no-merges "${PREV_TAG}..HEAD")
fi
{
echo "## What's Changed"
echo "## What's Changed (Alpha)"
echo ""
echo "$NOTES" | while IFS= read -r line; do echo "- $line"; done
echo ""
echo "---"
echo "**Requires Apple Silicon (M1/M2/M3)**"
echo "**Alpha build — updated on every push to \`main\`**"
echo ""
echo "**Includes macOS (Apple Silicon), Linux x64, and Windows x64 bundles**"
echo ""
echo "*Built from \`$(git rev-parse --short HEAD)\` on $(date -u +%Y-%m-%d)*"
} > release_notes.md
- name: Build latest.json
- name: Build alpha-latest.json
run: |
VERSION="${{ needs.version.outputs.version }}"
TAG="${{ needs.version.outputs.tag }}"
REPO="refactoringhq/laputa-app"
REPO="${GITHUB_REPOSITORY}"
REPO_NAME="${REPO#*/}"
PAGES_URL="https://refactoringhq.github.io/${REPO_NAME}/"
ARM_SIG=$(cat updater-aarch64/*.app.tar.gz.sig)
ARM_TARBALL=$(ls updater-aarch64/*.app.tar.gz | xargs basename)
find_required() {
for pattern in "$@"; do
set -- $pattern
if [ -e "$1" ]; then
printf '%s\n' "$1"
return 0
fi
done
return 1
}
cat > latest.json << EOF
ARM_SIG_FILE=$(find_required "updater-aarch64/*.app.tar.gz.sig")
ARM_UPDATER_FILE="${ARM_SIG_FILE%.sig}"
ARM_SIG=$(cat "$ARM_SIG_FILE")
ARM_UPDATER=$(basename "$ARM_UPDATER_FILE")
LINUX_SIG_FILE=$(find_required "linux-x86_64-bundles/*/*.AppImage.sig" "linux-x86_64-bundles/*/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*/*.deb.sig" "linux-x86_64-bundles/*.AppImage.sig" "linux-x86_64-bundles/*.AppImage.tar.gz.sig" "linux-x86_64-bundles/*.deb.sig")
LINUX_UPDATER_FILE="${LINUX_SIG_FILE%.sig}"
LINUX_SIG=$(cat "$LINUX_SIG_FILE")
LINUX_UPDATER=$(basename "$LINUX_UPDATER_FILE")
LINUX_DOWNLOAD=$(basename "$(find_required "linux-x86_64-bundles/*/*.AppImage" "linux-x86_64-bundles/*/*.deb" "linux-x86_64-bundles/*/*.AppImage.tar.gz" "linux-x86_64-bundles/*.AppImage" "linux-x86_64-bundles/*.deb" "linux-x86_64-bundles/*.AppImage.tar.gz")")
WINDOWS_SIG_FILE=$(find_required "windows-x86_64-bundles/*/*-setup.exe.sig" "windows-x86_64-bundles/*/*.msi.sig" "windows-x86_64-bundles/*/*.nsis.zip.sig" "windows-x86_64-bundles/*/*.msi.zip.sig" "windows-x86_64-bundles/*-setup.exe.sig" "windows-x86_64-bundles/*.msi.sig" "windows-x86_64-bundles/*.nsis.zip.sig" "windows-x86_64-bundles/*.msi.zip.sig")
WINDOWS_UPDATER_FILE="${WINDOWS_SIG_FILE%.sig}"
WINDOWS_SIG=$(cat "$WINDOWS_SIG_FILE")
WINDOWS_UPDATER=$(basename "$WINDOWS_UPDATER_FILE")
WINDOWS_DOWNLOAD=$(basename "$(find_required "windows-x86_64-bundles/*/*-setup.exe" "windows-x86_64-bundles/*/*.msi" "windows-x86_64-bundles/*/*.nsis.zip" "windows-x86_64-bundles/*/*.msi.zip" "windows-x86_64-bundles/*-setup.exe" "windows-x86_64-bundles/*.msi" "windows-x86_64-bundles/*.nsis.zip" "windows-x86_64-bundles/*.msi.zip")")
cat > alpha-latest.json << EOF
{
"version": "${VERSION}",
"notes": "See https://refactoringhq.github.io/laputa-app/ for full release notes.",
"notes": "Alpha build. See ${PAGES_URL} for full release notes.",
"pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"platforms": {
"darwin-aarch64": {
"signature": "${ARM_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_TARBALL}"
"url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${ARM_UPDATER}"
},
"linux-x86_64": {
"signature": "${LINUX_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${LINUX_DOWNLOAD}"
},
"windows-x86_64": {
"signature": "${WINDOWS_SIG}",
"url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_UPDATER}",
"download_url": "https://github.com/${REPO}/releases/download/${TAG}/${WINDOWS_DOWNLOAD}"
}
}
}
EOF
echo "latest.json:"; cat latest.json
echo "alpha-latest.json:"; cat alpha-latest.json
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.version.outputs.tag }}
name: Laputa ${{ needs.version.outputs.version }}
name: Tolaria ${{ needs.version.outputs.display_version }}
body_path: release_notes.md
draft: false
prerelease: false
prerelease: true
files: |
dmg-aarch64/*.dmg
updater-aarch64/*.app.tar.gz
updater-aarch64/*.app.tar.gz.sig
latest.json
linux-x86_64-bundles/*.deb
linux-x86_64-bundles/*.deb.sig
linux-x86_64-bundles/*.AppImage
linux-x86_64-bundles/*.AppImage.sig
linux-x86_64-bundles/*.AppImage.tar.gz
linux-x86_64-bundles/*.AppImage.tar.gz.sig
linux-x86_64-bundles/*/*.deb
linux-x86_64-bundles/*/*.deb.sig
linux-x86_64-bundles/*/*.AppImage
linux-x86_64-bundles/*/*.AppImage.sig
linux-x86_64-bundles/*/*.AppImage.tar.gz
linux-x86_64-bundles/*/*.AppImage.tar.gz.sig
windows-x86_64-bundles/*.exe
windows-x86_64-bundles/*.exe.sig
windows-x86_64-bundles/*.msi
windows-x86_64-bundles/*.msi.sig
windows-x86_64-bundles/*.zip
windows-x86_64-bundles/*.zip.sig
windows-x86_64-bundles/*/*.exe
windows-x86_64-bundles/*/*.exe.sig
windows-x86_64-bundles/*/*.msi
windows-x86_64-bundles/*/*.msi.sig
windows-x86_64-bundles/*/*.zip
windows-x86_64-bundles/*/*.zip.sig
alpha-latest.json
# ─────────────────────────────────────────────────────────────
# Phase 4: Update GitHub Pages with release history
@@ -222,61 +706,28 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build release history page
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p _site
gh api repos/${{ github.repository }}/releases --paginate > _site/releases.json
# Copy latest.json to GitHub Pages for auto-updater endpoint
gh release download --repo ${{ github.repository }} --pattern "latest.json" --output _site/latest.json || true
# Preserve canary latest.json from existing GH Pages
curl -fsSL "https://refactoringhq.github.io/laputa-app/latest-canary.json" -o _site/latest-canary.json || true
cat > _site/index.html << 'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laputa — Release History</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #F7F6F3; color: #37352F; line-height: 1.6; padding: 2rem; max-width: 720px; margin: 0 auto; }
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.5rem; }
.subtitle { color: #787774; margin-bottom: 2rem; }
.release { background: #fff; border: 1px solid #E9E9E7; border-radius: 8px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; }
.release h2 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.25rem; }
.release .meta { font-size: 0.8125rem; color: #787774; margin-bottom: 0.75rem; }
.release .body { font-size: 0.875rem; white-space: pre-wrap; }
.release .downloads { margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap; }
.release .downloads a { display: inline-block; padding: 0.375rem 0.75rem; background: #155DFF; color: #fff; border-radius: 6px; text-decoration: none; font-size: 0.8125rem; font-weight: 500; }
.release .downloads a:hover { background: #1248CC; }
.empty { color: #787774; text-align: center; padding: 3rem; }
</style>
</head>
<body>
<h1>Laputa Release History</h1>
<p class="subtitle">Auto-updated on every release</p>
<div id="releases"></div>
<script>
fetch('releases.json').then(r=>r.json()).then(releases=>{
const el=document.getElementById('releases');
if(!releases.length){el.innerHTML='<p class="empty">No releases yet.</p>';return;}
releases.forEach(r=>{
const date=new Date(r.published_at).toLocaleDateString('en-US',{year:'numeric',month:'long',day:'numeric'});
const dmgs=(r.assets||[]).filter(a=>a.name.endsWith('.dmg'));
const links=dmgs.map(a=>'<a href="'+a.browser_download_url+'">'+a.name+'</a>').join('');
const body=(r.body||'').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const div=document.createElement('div');
div.className='release';
div.innerHTML='<h2>'+(r.name||r.tag_name)+'</h2><div class="meta">'+date+' · '+r.tag_name+'</div><div class="body">'+body+'</div>'+(links?'<div class="downloads">'+links+'</div>':'');
el.appendChild(div);
});
});
</script>
</body>
</html>
HTMLEOF
mkdir -p _site/alpha _site/stable
gh api -H "Accept: application/vnd.github.html+json" repos/${{ github.repository }}/releases --paginate > _site/releases.json
PAGES_URL="https://refactoringhq.github.io/${GITHUB_REPOSITORY#*/}"
gh release download --repo ${{ github.repository }} "${{ needs.version.outputs.tag }}" --pattern "alpha-latest.json" --output _site/alpha/latest.json || echo '{}' > _site/alpha/latest.json
curl -fsSL "${PAGES_URL}/stable/latest.json" -o _site/stable/latest.json || echo '{}' > _site/stable/latest.json
bun scripts/build-release-download-page.ts --latest-json _site/stable/latest.json --releases-json _site/releases.json --output-file _site/stable/download/index.html
bun scripts/build-release-history-page.ts --releases-json _site/releases.json --output-file _site/index.html
mkdir -p _site/download
cp _site/stable/download/index.html _site/download/index.html
cp _site/alpha/latest.json _site/latest.json
cp _site/alpha/latest.json _site/latest-canary.json
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4

1
.gitignore vendored
View File

@@ -32,6 +32,7 @@ dist-ssr
# Demo vault and helper scripts
demo-vault/
generated-fixtures/
select_demo_notes*.py
final_selection.py

View File

@@ -1,6 +1,6 @@
# AGENTS.md — Laputa App
# AGENTS.md — Tolaria App
> Quick links: [Project Spec](docs/PROJECT-SPEC.md) · [Architecture](docs/ARCHITECTURE.md) · [Abstractions](docs/ABSTRACTIONS.md) · [Wireframes](ui-design.pen)
> Quick links: [Architecture](docs/ARCHITECTURE.md) · [Abstractions](docs/ABSTRACTIONS.md) · [Wireframes](ui-design.pen)
---
@@ -41,8 +41,8 @@ BASE_URL="http://localhost:5201" npx playwright test tests/smoke/<slug>.spec.ts
```bash
pnpm tauri dev &
sleep 10
bash ~/.openclaw/skills/laputa-qa/scripts/focus-app.sh laputa
bash ~/.openclaw/skills/laputa-qa/scripts/screenshot.sh /tmp/qa-native.png
bash ~/.openclaw/skills/tolaria-qa/scripts/focus-app.sh laputa
bash ~/.openclaw/skills/tolaria-qa/scripts/screenshot.sh /tmp/qa-native.png
```
Use `osascript` for keyboard interactions. Write result as Todoist comment (✅ or ❌). **⚠️ WKWebView:** `osascript keystroke` blocked inside editor — rely on Playwright for text input features.
@@ -105,9 +105,22 @@ After any Tauri command, new component/hook, data model change, or new integrati
## 3. Product Rules
### Demo vault hygiene (`demo-vault/`, `demo-vault-v2/`)
Default to `demo-vault-v2/` for testing.
- Treat `demo-vault/` and `demo-vault-v2/` as disposable QA fixtures unless the task explicitly changes demo content.
- If you create untracked notes, attachments, or other temporary files there for testing, delete them before the task is complete.
- If you modify tracked demo-vault files only to test or QA behavior, revert those edits before the final commit.
- Before declaring a task done, make sure `git status --short -- demo-vault demo-vault-v2` is empty unless demo fixture changes are part of the task.
- If a fresh run starts and the only local dirt is inside `demo-vault/` or `demo-vault-v2/`, clean those paths first and continue. That case is recoverable QA residue, not a blocker.
### User vault (`~/Laputa/`)
Default to `demo-vault-v2/`. If you must use `~/Laputa/` for testing: **never commit changes** — always run `cd ~/Laputa && git checkout -- . && git clean -fd` when done.
Default to `demo-vault-v2/`. If you must use `~/Laputa/` for testing:
- **Never commit or push** any test notes to the remote vault
- **Delete all test notes from disk** when done — do not leave untitled or temporary notes on the filesystem. Run `cd ~/Laputa && git checkout -- . && git clean -fd` to restore the vault to its last committed state.
- **Rationale:** test notes pollute the local vault over time, making it a collection of nonsensical untitled files. The vault must stay clean on disk, not just on the remote.
### UI design
@@ -130,7 +143,7 @@ Open `ui-design.pen` first (light mode). Create `design/<slug>.pen` for the task
| Toggle/switch | `Switch` or `ToggleGroup` from shadcn/ui |
| Dialog/modal | `Dialog` from shadcn/ui |
**When in doubt:** search `src/components/` for an existing component before building new. **Visual language:** all new UI must feel native to Laputa — if it looks like a browser default, it's wrong.
**When in doubt:** search `src/components/` for an existing component before building new. **Visual language:** all new UI must feel native to Tolaria — if it looks like a browser default, it's wrong.
---
@@ -145,9 +158,9 @@ Open `ui-design.pen` first (light mode). Create `design/<slug>.pen` for the task
### QA scripts
```bash
bash ~/.openclaw/skills/laputa-qa/scripts/focus-app.sh laputa
bash ~/.openclaw/skills/laputa-qa/scripts/screenshot.sh /tmp/out.png
bash ~/.openclaw/skills/laputa-qa/scripts/shortcut.sh "command" "s"
bash ~/.openclaw/skills/tolaria-qa/scripts/focus-app.sh laputa
bash ~/.openclaw/skills/tolaria-qa/scripts/screenshot.sh /tmp/out.png
bash ~/.openclaw/skills/tolaria-qa/scripts/shortcut.sh "command" "s"
```
### Diagrams

49
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,49 @@
# Contributing to Tolaria
Thanks for being here! Tolaria is still early, and every bug report, idea, and contribution genuinely helps shape the app.
## 🗳️ Where to share what
To keep things clean:
- 🐛 Bugs → GitHub Issues
- 💡 Feature requests / ideas → Canny • <https://tolaria.canny.io/>
If you have a feature idea, please check Canny first and upvote it if it already exists.
## 📥 Pull requests are welcome
PRs are very welcome.
A few things to keep in mind before opening one:
- Bug fixes are always great
- Small improvements are great too
- For bigger features, please check Canny first before building
- Try to avoid things that are already marked **in progress**
- Requests marked **planned** are usually great contribution targets
- Keep PRs small, focused, and easy to review
- Include a short explanation of the problem and your solution
- Follow the dev process described in Tolarias `AGENTS.md` (tests, code health, etc.)
- Avoid bundling unrelated refactors into the same PR
If you want to contribute a feature, the best place to start is here: <https://tolaria.canny.io/>
## 📋 What makes a good bug report
If you open a bug report on GitHub, it really helps to include:
- your Tolaria version
- your OS version
- steps to reproduce
- what you expected to happen
- what actually happened
- screenshots or screen recordings if useful
The clearer the report, the easier it is for us to reproduce and fix it.
## 🙏 Thank you
Tolaria is getting better because people care enough to try it, report whats broken, suggest whats missing, and contribute improvements.
That means a lot. Thanks for helping build it.

Binary file not shown.

661
LICENSE Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

143
README.md
View File

@@ -1,93 +1,100 @@
# Laputa App
![Latest stable](https://img.shields.io/github/v/release/refactoringhq/tolaria?display_name=tag) [![CI](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/refactoringhq/tolaria/actions/workflows/ci.yml) [![Build](https://github.com/refactoringhq/tolaria/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/refactoringhq/tolaria/actions/workflows/release.yml) [![Codecov](https://codecov.io/gh/refactoringhq/tolaria/graph/badge.svg?branch=main)](https://codecov.io/gh/refactoringhq/tolaria) [![CodeScene Hotspot Code Health](https://codescene.io/projects/76865/status-badges/hotspot-code-health)](https://codescene.io/projects/76865)
Personal knowledge and life management desktop app built with Tauri v2 + React + TypeScript + BlockNote.
# 💧 Tolaria
## Documentation
Tolaria is a desktop app for Mac and Linux for managing **markdown knowledge bases**. People use it for a variety of use cases:
- 📐 [ARCHITECTURE.md](docs/ARCHITECTURE.md) — System design, tech stack, data flow
- 🧩 [ABSTRACTIONS.md](docs/ABSTRACTIONS.md) — Core abstractions and models
- 🚀 [GETTING-STARTED.md](docs/GETTING-STARTED.md) — How to navigate the codebase
- 🎨 [THEMING.md](docs/THEMING.md) — Theme system and customization
* Operate second brains and personal knowledge
* Organize company docs as context for AI
* Store OpenClaw/assistants memory and procedures
## Quick Start
Personally, I use it to **run my life** (hey 👋 [Luca here](http://x.com/lucaronin)). I have a massive workspace of 10,000+ notes, which are the result of my [Refactoring](https://refactoring.fm/) work + a ton of personal journaling and *second braining*.
<img width="1000" height="656" alt="1776506856823-CleanShot_2026-04-18_at_12 06 57_2x" src="https://github.com/user-attachments/assets/8aeafb0a-b236-43c2-a083-ec111f903c38" />
## Walkthroughs
You can find some Loom walkthroughs below — they are short and to the point:
- [How I Organize My Own Tolaria Workspace](https://www.loom.com/share/bb3aaffa238b4be0bd62e4464bca2528)
- [My Inbox Workflow](https://www.loom.com/share/dffda263317b4fa8b47b59cdf9330571)
- [How I Save Web Resources to Tolaria](https://www.loom.com/share/8a3c1776f801402ebbf4d7b0f31e9882)
## Principles
- 📑 **Files-first** — Your notes are plain markdown files. They're portable, work with any editor, and require no export step. Your data belongs to you, not to any app.
- 🔌 **Git-first** — Every vault is a git repository. You get full version history, the ability to use any git remote, and zero dependency on Tolaria servers.
- 🛜 **Offline-first, zero lock-in** — No accounts, no subscriptions, no cloud dependencies. Your vault works completely offline and always will. If you stop using Tolaria, you lose nothing.
- 🔬 **Open source** — Tolaria is free and open source. I built this for [myself](https://x.com/lucaronin) and for sharing it with others.
- 📋 **Standards-based** — Notes are markdown files with YAML frontmatter. No proprietary formats, no locked-in data. Everything works with standard tools if you decide to move away from Tolaria.
- 🔍 **Types as lenses, not schemas** — Types in Tolaria are navigation aids, not enforcement mechanisms. There's no required fields, no validation, just helpful categories for finding notes.
- 🪄**AI-first but not AI-only** — A vault of files works very well with AI agents, but you are free to use whatever you want. We support Claude Code and Codex CLI (for now), but you can edit the vault with any AI you want. We provide an AGENTS file for your agents to figure out.
- ⌨️ **Keyboard-first** — Tolaria is designed for power-users who want to use keyboard as much as possible. A lot of how we designed the Editor and the Command Palette is based on this.
- 💪 **Built from real use** — Tolaria was created for manage my personal vault of 10,000+ notes, and I use it every day. Every feature exists because it solved a real problem.
## Getting started
Download the [latest release here](https://github.com/refactoringhq/tolaria/releases/latest/download/Tolaria.app.tar.gz).
When you open Tolaria for the first time you get the chance of cloning the [getting started vault](https://github.com/refactoringhq/tolaria-getting-started) — which gives you a walkthrough of the whole app.
## Open source and local setup
Tolaria is open source and built with Tauri, React, and TypeScript. If you want to run or contribute to the app locally, here is [how to get started](https://github.com/refactoringhq/tolaria/blob/main/docs/GETTING-STARTED.md). You can also find the gist below 👇
### Prerequisites
- Node.js 20+
- pnpm 8+
- Rust (latest stable)
- macOS (for development)
- Rust stable
- macOS or Linux for development
### Setup
#### Linux system dependencies
Tauri 2 on Linux requires WebKit2GTK 4.1 and GTK 3:
- Arch / Manjaro:
```bash
sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl \
appmenu-gtk-module libappindicator-gtk3 librsvg
```
- Debian / Ubuntu (22.04+):
```bash
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev \
libsoup-3.0-dev patchelf
```
- Fedora 38+:
```bash
sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file \
libappindicator-gtk3-devel librsvg2-devel
```
The bundled MCP server still spawns the system `node` binary at runtime on Linux, so install Node from your distro package manager if you want the external AI tooling flow.
### Quick start
```bash
# Install dependencies
pnpm install
# Run dev server
pnpm dev
```
# Open in browser (mock mode)
open http://localhost:5173
Open `http://localhost:5173` for the browser-based mock mode, or run the native desktop app with:
# Or run in Tauri
```bash
pnpm tauri dev
```
### Testing
## Tech Docs
```bash
# Frontend tests
pnpm test
- 📐 [ARCHITECTURE.md](docs/ARCHITECTURE.md) — System design, tech stack, data flow
- 🧩 [ABSTRACTIONS.md](docs/ABSTRACTIONS.md) — Core abstractions and models
- 🚀 [GETTING-STARTED.md](docs/GETTING-STARTED.md) — How to navigate the codebase
- 📚 [ADRs](docs/adr) — Architecture Decision Records
# Backend tests
cargo test
## Security
# Coverage
pnpm test:coverage
# E2E tests
pnpm test:e2e
```
### Code Quality
```bash
# Lint
pnpm lint
# Rust checks
cargo clippy
cargo fmt --check
# CodeScene (via Claude Code)
claude 'Check code health with CodeScene MCP'
```
## Development Workflow
See [AGENTS.md](AGENTS.md) for coding guidelines and workflow. [CLAUDE.md](CLAUDE.md) remains as a compatibility shim for Claude Code.
**Key principles:**
- Small, atomic commits
- Test as you go
- Visual verification mandatory
- Documentation updated with code changes
## CI/CD
GitHub Actions runs on every push to `main`:
- ✅ Tests (frontend + Rust)
- 📊 Coverage (70% threshold)
- 🎨 Lint & format
- ⚠️ Documentation check
See [.github/SETUP.md](.github/SETUP.md) for CI/CD configuration.
## Git Hooks
Husky installs the git hooks from `.husky/` during `pnpm install`. The repo enforces `main`-only commits and pushes; see [.github/HOOKS.md](.github/HOOKS.md) for details.
If you believe you have found a security issue, please report it privately as described in [SECURITY.md](./SECURITY.md).
## License
Private repository — not licensed for public use.
Tolaria is licensed under AGPL-3.0-or-later. The Tolaria name and logo remain covered by the projects trademark policy.

55
SECURITY.md Normal file
View File

@@ -0,0 +1,55 @@
# Security Policy
Thanks for helping keep Tolaria safe.
If you believe you have found a security vulnerability, **please do not open a public GitHub issue**. Report it privately instead.
## Supported versions
We currently support security fixes for:
| Version | Supported |
| --- | --- |
| Latest stable release | ✅ |
| `main` branch | Best effort |
| Older releases / prereleases | ❌ |
## Reporting a vulnerability
Please email **luca@refactoring.club** with the subject line **`[Tolaria Security]`**.
Include as much of the following as you can:
- a short description of the issue
- reproduction steps or a proof of concept
- affected version / commit, if known
- impact assessment
- any suggested mitigation
If the issue involves sensitive user data, credentials, or a working exploit, keep the report private and do not post details publicly.
## What to expect
We will try to:
- acknowledge receipt within a few business days
- reproduce and assess the report
- work on a fix or mitigation if the issue is valid
- coordinate public disclosure after users have had a reasonable chance to update
## Disclosure guidelines
Please give us a reasonable amount of time to investigate and ship a fix before publishing details.
We appreciate responsible disclosure and good-faith research.
## Out of scope
The following are generally out of scope unless they demonstrate a real security impact:
- missing best-practice headers or hardening with no practical exploit
- self-XSS or editor behavior that requires unrealistic user actions
- reports that only affect unsupported old builds
- purely theoretical issues with no plausible attack path
If you are unsure whether something qualifies, please still report it privately.

View File

@@ -0,0 +1,55 @@
{
"name": "Tolaria QA fixture",
"purpose": "Curated local vault for native QA and developer flows. This is not the public Getting Started starter vault.",
"large_fixture": {
"generator": "python3 scripts/generate_demo_vault.py",
"default_output": "generated-fixtures/demo-vault-large"
},
"scenarios": [
{
"id": "exact-match-search",
"reason": "Quick Open should rank the exact title 'Writing' above prefix matches.",
"files": [
"topic-writing.md",
"writing-for-clarity-vs-writing-for-credit.md",
"writing-weekly-rhythm.md"
]
},
{
"id": "relationship-rendering",
"reason": "Relationship keys should render in the inspector instead of as plain properties.",
"files": [
"responsibility-sponsorships.md",
"measure-sponsorship-mrr.md",
"measure-close-rate.md",
"procedure-quarterly-sponsor-outreach.md",
"procedure-sponsor-onboarding.md",
"24q4-laputa-start.md",
"24q4.md",
"person-luca-rossi.md"
]
},
{
"id": "project-navigation",
"reason": "Projects, quarters, and a saved view give keyboard QA a compact but representative browsing path.",
"files": [
"24q4.md",
"25q1.md",
"25q2.md",
"24q4-laputa-start.md",
"25q1-laputa-v1.md",
"25q2-laputa-v2.md",
"views/active-projects.yml"
]
},
{
"id": "attachment-rendering",
"reason": "A note with a real binary attachment keeps image/block QA anchored to the fixture.",
"files": [
"laputa-qa-reference.md",
"attachments/laputa-reference.png"
]
}
]
}

View File

@@ -1,16 +1,5 @@
# Laputa app files (machine-specific, never commit)
.laputa/settings.json
# macOS
.git/
.laputa/
.laputa-index.json
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Editors
.vscode/
.idea/
*.swp
*.swo

View File

@@ -1,3 +0,0 @@
{
"theme": null
}

View File

@@ -1,29 +0,0 @@
---
aliases: ["January 2024"]
Is A: Month
Created at: "2024-01-28"
Belongs to: "[[24q1]]"
Rating: "😄"
---
# January 2024
## Focus
- Set up [[24q1-launch-sponsorship-packages]] — research pricing, structure tiers, draft sponsor-facing materials
- Begin planning [[24q1-podcast-season-1]] — outline first 6 episode topics, research recording setup
- Map out cycling calendar for the year ([[24q1-plan-cycling-season]])
## Highlights
- Finalized the three-tier sponsorship model (Gold, Silver, Bronze) with [[person-matteo-cellini]] — spent two full days on pricing research and competitive analysis
- Ordered podcast equipment: Shure SM7B, Focusrite Scarlett, pop filter — the home studio is starting to take shape
- Newsletter hit 35.4k subscribers, steady organic growth from December's end-of-year content
- Read "The Hard Thing About Hard Things" and "Measure What Matters" — both immediately applicable to how I think about Refactoring's goals
- Registered for Nove Colli (May) and submitted lottery application for Maratona dles Dolomites (July)
- Started base training: 380 km this month, mostly zone 2 rides on the trainer
## Reflections
January always feels like a slow month, but this one was quietly productive. The sponsorship package work was the kind of unglamorous, strategic task that doesn't produce visible output but sets up everything that follows. [[person-matteo-cellini]] and I went back and forth on pricing for days — we kept second-guessing whether Gold at EUR 2.8k/issue was too high. In the end, we decided to launch at that price and see what happens. Worst case, we adjust.
The podcast planning was fun but nerve-wracking. I've been talking about launching a podcast for over a year, and now it's actually happening. The equipment is here, the topics are outlined. No more excuses. February is recording month.

View File

@@ -1,29 +0,0 @@
---
aliases: ["February 2024"]
Is A: Month
Created at: "2024-02-28"
Belongs to: "[[24q1]]"
Rating: "🤩"
---
# February 2024
## Focus
- Record and edit first batch of podcast episodes ([[24q1-podcast-season-1]])
- Launch [[24q1-launch-sponsorship-packages]] to existing sponsors and new prospects
- Start [[24q1-redesign-newsletter-template]] with improved mobile layout
## Highlights
- Recorded episodes 1-4 of [[24q1-podcast-season-1]] in a single weekend marathon session — exhausting but exhilarating
- Published Episode 1 ("Why Engineering Managers Fail") — 600 downloads in the first 48 hours, way above my modest expectations
- [[24q1-launch-sponsorship-packages]] went live — sent the new pricing deck to 12 existing sponsors and 20 cold prospects
- [[person-matteo-cellini]] closed the first Gold sponsor deal within a week of launch — a DevOps platform company, EUR 2.8k/issue for 4 issues
- Newsletter grew to 36.1k subscribers — the podcast announcement email had a 52% open rate
- First draft of [[24q1-redesign-newsletter-template]] ready — cleaner header, better CTA placement, responsive images
## Reflections
This was the month everything started to feel real. Hearing my own voice on the podcast for the first time was deeply uncomfortable — I hated it, edited obsessively, almost scrapped the whole thing. But then the downloads came in, and the feedback was overwhelmingly positive. People said things like "it feels like you're explaining this to me over coffee." That's exactly the vibe I was going for. Maybe my voice isn't so bad after all.
The sponsorship launch going well this quickly was a confidence boost. [[person-matteo-cellini]] deserves the credit — he positioned the packages perfectly and his follow-up game is relentless. Having a second Gold sponsor in the pipeline already makes the Q2 revenue forecast look strong. February was a month where preparation met opportunity. Best month in a while.

View File

@@ -1,29 +0,0 @@
---
aliases: ["March 2024"]
Is A: Month
Created at: "2024-03-28"
Belongs to: "[[24q1]]"
Rating: "😄"
---
# March 2024
## Focus
- Finalize [[24q1-redesign-newsletter-template]] and ship to production
- Continue podcast momentum with episodes 3-4 release
- Set up [[24q1-set-investing-framework]] — automate monthly contributions
## Highlights
- Shipped [[24q1-redesign-newsletter-template]] — [[measure-open-rate]] improved by 1.5pp in the first two weeks, click-through rate also up
- Released episodes 3 and 4 of [[24q1-podcast-season-1]] — Episode 3 on "Technical Debt as a Strategic Tool" got shared widely on LinkedIn
- Set up [[24q1-set-investing-framework]]: automated monthly DCA into VWCE (global ETF), topped up emergency fund to 6 months of expenses
- Newsletter crossed 37k subscribers — strong organic growth from podcast cross-promotion
- Got accepted for Maratona dles Dolomites via lottery — both gran fondos now confirmed for [[2024-complete-two-gran-fondos]]
- Cycling: 420 km this month, first outdoor rides of the season — legs feel good after months on the trainer
## Reflections
March wrapped up Q1 nicely. The newsletter template redesign was one of those things that seems small but makes a real difference — the new layout just looks more professional, and the numbers back it up. Sometimes the boring improvements are the most impactful.
Getting into Maratona via lottery was a huge relief. I'd been anxious about it for weeks. Now both races are locked in and the training plan has clear milestones. The investing framework is the kind of "set it and forget it" system I love — one less thing to think about each month. [[24q1]] was a strong quarter overall. Time to execute in Q2.

View File

@@ -1,30 +0,0 @@
---
aliases: ["April 2024"]
Is A: Month
Created at: "2024-04-28"
Belongs to: "[[24q2]]"
Rating: "😄"
---
# April 2024
## Focus
- Kick off [[24q2-hire-editor]] search — post the role, screen candidates, run test edits
- Start [[24q2-10-pillar-articles]] strategy — identify topics, outline first 3 articles
- Begin building [[24q2-sponsor-crm]] in Notion
## Highlights
- Posted the freelance editor role on LinkedIn and two newsletter job boards — received 45 applications in two weeks
- Shortlisted 5 editor candidates and ran paid test edits on a sample newsletter issue — two stood out clearly
- Outlined the first 3 [[24q2-10-pillar-articles]]: "Staff Engineer vs. Manager," "How to Prioritize Technical Debt," and "The 1:1 Framework That Actually Works"
- Started building [[24q2-sponsor-crm]] in Notion — pipeline view, deal stages, revenue tracking
- [[person-paco-furiani]] took over invoicing — freed up ~3 hours/month of admin work
- Newsletter at 38.5k subscribers — steady growth, no spikes
- Cycling: ramping up volume — 480 km this month, including a 120 km long ride in Tuscany
## Reflections
A transitional month. Not flashy, but the groundwork being laid is important. The editor hire is going to be transformative if I get it right — spending Sunday evenings editing is my least favorite part of the workflow. The test edits revealed that most applicants can fix grammar but few can preserve voice. The two finalists both "got" the Refactoring tone, which is rare.
The pillar articles strategy feels right. Instead of grinding out a new topic every week, investing in fewer, deeper pieces that can rank on search and compound over time. It's a bet on long-term value over short-term output. [[person-paco-furiani]] quietly picking up the invoicing was a small moment that mattered more than it looked — the team is starting to function as a team, not just a collection of helpers.

View File

@@ -1,30 +0,0 @@
---
aliases: ["May 2024"]
Is A: Month
Created at: "2024-05-28"
Belongs to: "[[24q2]]"
Rating: "😐"
---
# May 2024
## Focus
- Finalize [[24q2-hire-editor]] — make the offer, onboard
- Publish first batch of [[24q2-10-pillar-articles]]
- Race Nove Colli ([[24q2-spring-gran-fondo]])
## Highlights
- Hired the editor — she starts full-time in June, doing first-pass editing on every newsletter issue
- Published "Staff Engineer vs. Manager" — it went semi-viral on LinkedIn, drove 800+ new subscribers in a single week
- Completed Nove Colli ([[24q2-spring-gran-fondo]]) in 7h42m — finished but bonked hard on the fourth climb due to heat
- [[24q2-build-podcast-landing-page]] kicked off — basic wireframes done, episodes 1-6 now need a proper home
- Newsletter hit 40.5k subscribers — the pillar article spike was significant
## Reflections
Mixed feelings this month. The editor hire is great news — having someone who can handle the first pass means I can focus on the ideas rather than the polish. But Nove Colli was humbling. The heat in Romagna was 34 degrees, and I didn't manage my nutrition well enough. Bonking on the fourth climb — legs completely empty, seeing spots — was scary. I sat on the side of the road for 10 minutes eating everything in my pockets before I could continue.
I finished, and that matters. But the time was disappointing. Lesson learned: nutrition strategy needs to be as planned as the training itself. The Maratona in July will be at altitude, which adds another variable. Need to take fueling much more seriously.
The "Staff Engineer vs. Manager" article reminded me why I do this. The number of engineers who DMed me saying "I needed to read this three years ago" — that's the impact I care about. More than subscriber counts.

View File

@@ -1,32 +0,0 @@
---
aliases: ["June 2024"]
Is A: Month
Created at: "2024-06-28"
Belongs to: "[[24q2]]"
Rating: "🤩"
---
# June 2024
## Focus
- Ship [[24q2-build-podcast-landing-page]] with full episode archive
- Complete [[24q2-sponsor-crm]] and migrate all sponsor data
- Ramp up training for Maratona dles Dolomites in July
## Highlights
- Launched [[24q2-build-podcast-landing-page]] — clean design, Apple/Spotify links, full show notes for all 8 episodes — podcast downloads crossed 5k/month
- [[24q2-sponsor-crm]] completed and live — all pipeline data migrated from scattered spreadsheets, [[person-matteo-cellini]] now tracks everything in one place
- Published 3 more [[24q2-10-pillar-articles]] including "How to Prioritize Technical Debt" (32k views)
- Editor fully onboarded and in rhythm — first-pass drafts arriving every Monday morning, quality is excellent
- Newsletter hit 42k subscribers — right on pace for [[2024-reach-50k-subscribers]]
- [[measure-sponsorship-mrr]] reached EUR 8.2k — strong Q2 performance
- Cycling: biggest training month yet — 620 km, including two back-to-back long rides simulating Maratona's profile
## Reflections
June was one of those months where everything clicks. The podcast landing page looks professional and is already converting visitors to subscribers. The sponsor CRM might be the most impactful internal tool we've built — no more "where's that sponsor contract?" emails. [[person-matteo-cellini]] went from managing sponsors in his head to having a proper pipeline view.
The editor hire is already paying off. She catches things I'd miss after staring at my own words for hours. The Monday morning routine of reviewing a clean draft instead of starting from a rough mess is a genuine quality-of-life improvement. My writing quality is actually better because I can focus on substance rather than mechanics.
Training is going well. The two back-to-back long rides (140 km + 120 km over a weekend) left me wrecked but confident. If I can do that in Tuscany's hills, the Dolomites are within reach. Bring on July.

View File

@@ -1,29 +0,0 @@
---
aliases: ["July 2024"]
Is A: Month
Created at: "2024-07-28"
Belongs to: "[[24q3]]"
Rating: "😄"
---
# July 2024
## Focus
- Race Maratona dles Dolomites — the big one for [[2024-complete-two-gran-fondos]]
- Begin [[24q3-premium-tier]] planning — pricing, content strategy, platform selection
- Launch [[24q3-summer-reading-sprint]] — target 8 books in July-August
## Highlights
- Completed Maratona dles Dolomites in 9h15m — Passo Giau at dawn was transcendent, Fedaia was brutal, but finished strong — [[2024-complete-two-gran-fondos]] achieved
- Started planning [[24q3-premium-tier]]: researched Substack paid, Ghost memberships, and Stripe checkout — leaning toward custom Stripe integration for flexibility
- Read "An Elegant Puzzle" and "Team Topologies" — both excellent, immediately useful for newsletter content
- Newsletter at 43.5k subscribers — summer slowdown typical but growth still positive
- Began outlining [[24q3-codemotion-talk]] abstract — submitted "Scaling Engineering Culture" as the proposed topic
- [[measure-podcast-downloads]] hit 6.5k/month — organic growth without new episodes (between seasons)
## Reflections
Maratona was the cycling highlight of the year, maybe of my life so far. 138 km through the Dolomites, 4,200m of climbing, starting in the dark at 6am in Corvara. The Sella and Gardena passes were hard, Campolongo was deceptively steep, but Giau — Giau was magical. The sunrise hitting the peaks as I climbed the final switchbacks, the silence broken only by breathing and chain noise. I didn't care about my time. I was just there.
The post-race week was recovery — both physical and mental. After months of structured training and race anxiety, there's a strange emptiness when the goal is achieved. I channeled that into reading and planning the premium tier. Pricing is the hardest part: too cheap and it devalues the content, too expensive and the audience is too small to sustain it. Need to think about this more in August.

View File

@@ -1,29 +0,0 @@
---
aliases: ["August 2024"]
Is A: Month
Created at: "2024-08-28"
Belongs to: "[[24q3]]"
Rating: "🤩"
---
# August 2024
## Focus
- Build and launch [[24q3-premium-tier]] — set up payments, create first premium content
- Continue [[24q3-summer-reading-sprint]] — deep reading weeks with lighter work schedule
- Record first episodes of [[24q3-podcast-season-2]]
## Highlights
- Launched [[24q3-premium-tier]] at EUR 8/month — 120 subscribers in the first week, bumped to EUR 12/month after seeing strong demand
- Premium content: published first deep-dive case study ("How Stripe Scales Engineering Teams") and announced monthly AMA calls
- Finished [[24q3-summer-reading-sprint]]: 8 books total across July-August, including "The Ride of a Lifetime" and "Thinking in Systems"
- Recorded episodes 13-16 of [[24q3-podcast-season-2]] — new guest interview format with [[person-sara-ricci]] coordinating guests
- Newsletter at 44.8k — August is always slow, but premium launch generated a subscriber bump
- [[person-matteo-cellini]] began outreach for [[24q3-new-sponsor-verticals]] — targeting cloud and DevOps companies
## Reflections
August is usually my favorite work month — lighter pace, longer mornings, room to think. This year it was productive in a different way. The premium tier launch felt risky: asking people to pay for something they'd been getting for free. But the response was encouraging. 120 subscribers in the first week validated the hypothesis. Bumping the price from EUR 8 to EUR 12 was a gut call based on [[person-matteo-cellini]]'s advice — "if people are subscribing this fast, you're underpriced." He was right.
The reading sprint was exactly what I needed. Eight books in two months, with time to actually think about each one. "Thinking in Systems" by Donella Meadows is probably the most underrated book in the business canon — it changed how I think about feedback loops in newsletter growth. Summer energy is real — I feel recharged and ready for an intense September.

View File

@@ -1,29 +0,0 @@
---
aliases: ["September 2024"]
Is A: Month
Created at: "2024-09-28"
Belongs to: "[[24q3]]"
Rating: "😄"
---
# September 2024
## Focus
- Deliver [[24q3-codemotion-talk]] at Codemotion Milan
- Push premium tier to 300+ subscribers
- Close Q3 strong on sponsor pipeline ([[24q3-new-sponsor-verticals]])
## Highlights
- Delivered [[24q3-codemotion-talk]] on "Scaling Engineering Culture" to ~400 attendees — standing ovation, 3 inbound sponsor inquiries within a week
- [[24q3-premium-tier]] hit 320 paid subscribers — AMA calls proving to be the most valued feature
- [[24q3-new-sponsor-verticals]]: [[person-matteo-cellini]] closed deals with two DevOps platform companies, diversifying the sponsor base
- Released episodes 17-20 of [[24q3-podcast-season-2]] — guest episode with a VP Engineering at a Series D startup was the most downloaded yet
- Newsletter hit 46k subscribers — Codemotion exposure plus podcast cross-promotion driving growth
- [[measure-sponsorship-mrr]] reached EUR 10.1k — first time crossing EUR 10k
## Reflections
Codemotion was terrifying and wonderful. I spent the week before rehearsing obsessively, convinced I'd forget my lines or that the audience wouldn't care. None of that happened. The talk landed well, the Q&A was engaged, and the hallway conversations afterward were some of the best I've had this year. Three sponsors reaching out unsolicited — that's the power of being on stage in front of your target audience.
But September also felt like the start of a crunch period. Premium content, podcast production, conference prep, sponsor pipeline — the workload is stacking up. I'm starting to feel the edges of my capacity. [[person-paco-furiani]] flagged that I missed two invoicing deadlines because I was focused on the talk. He's right — I need to stay disciplined about the operational basics even when the creative work is exciting. Heading into Q4, I need to be honest about what I can sustain.

View File

@@ -1,28 +0,0 @@
---
aliases: ["October 2024"]
Is A: Month
Created at: "2024-10-28"
Belongs to: "[[24q4]]"
Rating: "😐"
---
# October 2024
## Focus
- Begin [[24q4-laputa-start]] — initial prototype of the PKM tool
- Design [[24q4-annual-review-process]] template
- Start planning [[24q4-black-friday-campaign]]
## Highlights
- Started [[24q4-laputa-start]]: Tauri v2 + React scaffold, basic markdown file reading, first pass at YAML frontmatter parsing — spending evenings and weekends on it
- Drafted [[24q4-annual-review-process]] template with quarterly, goal, and project review sections
- [[24q4-black-friday-campaign]] strategy defined: 30% off annual premium, referral bonuses, limited-time offers
- Newsletter at 47.5k subscribers — growth steady but nothing spectacular
- [[person-matteo-cellini]] started building the [[24q4-sponsor-dashboard]] concept — mockups in Figma
## Reflections
October was a grind. The post-Codemotion high wore off fast, replaced by the reality of a packed Q4. Starting Laputa was exciting but also a bit reckless — adding a software project on top of everything else. But I couldn't help it. The frustration with existing PKM tools had been building for months, and once I started coding, I couldn't stop. The first time I saw my vault rendered in the four-panel layout, I got that spark you only get when you're building something you genuinely need.
The newsletter growth plateauing at ~47k is worrying. We need a big push to hit [[2024-reach-50k-subscribers]] by year end. The Black Friday campaign is the obvious lever, but we need it to be genuinely compelling, not just another discount email. [[person-paco-furiani]] suggested adding a community element to the campaign — worth exploring. Energy-wise, I'm running at about 70%. Not burned out, but not firing on all cylinders either. Need to protect sleep and exercise.

View File

@@ -1,31 +0,0 @@
---
aliases: ["November 2024"]
Is A: Month
Created at: "2024-11-28"
Belongs to: "[[24q4]]"
Rating: "😄"
---
# November 2024
## Focus
- Execute [[24q4-black-friday-campaign]] — the big push to 50k subscribers
- Ship [[24q4-sponsor-dashboard]] for sponsor self-service metrics
- Continue Laputa development ([[24q4-laputa-start]])
## Highlights
- [[24q4-black-friday-campaign]] was a massive success: 2,800 new subscribers in 10 days, 180 free-to-premium conversions, best campaign ever
- Newsletter crossed 50k subscribers on November 22nd — [[2024-reach-50k-subscribers]] achieved, two months ahead of my mental timeline
- Shipped [[24q4-sponsor-dashboard]] — sponsors can now see impressions, clicks, and conversion data in real-time — [[person-matteo-cellini]] reports dramatically fewer support emails
- Laputa ([[24q4-laputa-start]]) now has working search, type-based navigation, and basic frontmatter editing — using it for my own vault daily
- [[measure-sponsorship-mrr]] hit EUR 11.2k — November renewals came in strong
- Recorded end-of-year reflections episode for the podcast — raw and personal, felt right
## Reflections
Crossing 50k subscribers was the defining moment of the month. I remember refreshing the Substack dashboard at 11pm on a Friday, watching the number tick from 49,998 to 50,001. It sounds silly, but I actually teared up a little. Two years ago, 50k felt like a fantasy. The Black Friday campaign was the catalyst, but it only worked because of 11 months of consistent work before it.
The sponsor dashboard was [[person-matteo-cellini]]'s baby, and he executed it beautifully. Giving sponsors self-serve access to their data is the kind of product thinking that separates a media business from a newsletter. [[person-paco-furiani]] handled the backend automation — pulling data from Substack's API into a clean dashboard. Small team, big impact.
Laputa is becoming a real tool. I'm using it every day now, and the workflow of navigating my vault through types and relationships is already better than what I had in Obsidian. It's rough around the edges, but the bones are good. Heading into December with real momentum.

View File

@@ -1,32 +0,0 @@
---
aliases: ["December 2024"]
Is A: Month
Created at: "2024-12-28"
Belongs to: "[[24q4]]"
Rating: "😄"
---
# December 2024
## Focus
- Run [[24q4-annual-review-process]] — review all goals, projects, and metrics for [[2024]]
- Complete [[24q4-cycling-year-review]] — analyze training data, plan 2025 cycling goals
- Set [[2025]] goals and plan [[25q1]]
## Highlights
- Completed [[24q4-annual-review-process]]: reviewed all 4 quarters, 20 projects, and 5 annual goals — documented everything in structured templates
- [[24q4-cycling-year-review]]: 6,200 km total, FTP up 12% YoY, 2 gran fondos completed — decided to target Stelvio for [[2025-ride-stelvio]]
- Set [[2025]] goals: [[2025-reach-85k-subscribers]], [[2025-reach-22k-mrr]], [[2025-ship-laputa]], [[2025-ride-stelvio]], [[2025-read-20-books]]
- Newsletter closed the year at 50.2k subscribers — slight post-Black-Friday churn, but net position is excellent
- [[measure-sponsorship-mrr]] ended at EUR 11.4k — [[2024-double-revenue]] confirmed (was EUR 5.5k in January)
- Read 2 books in December to finish at 26 total — [[2024-read-24-books]] exceeded
- [[person-paco-furiani]] and I sketched out the 2025 operational calendar — quarterly planning cadence locked in
## Reflections
December is always reflective, and this one more than most. Sitting down to review [[2024]] in detail, I realized that every major goal was either hit or exceeded. That almost never happens. The newsletter from 35k to 50k. Revenue doubled. Podcast launched and growing. Two gran fondos. 26 books. It was a genuinely great year.
But the review also surfaced where I was lucky vs. where I was good. The Black Friday campaign was well-executed, but the timing was also favorable — low competition in the engineering leadership newsletter space. The pillar articles strategy worked partly because of SEO tailwinds. I don't want to mistake a rising tide for good swimming.
Looking ahead to [[2025]], the goals are deliberately ambitious: 85k subscribers, EUR 22k MRR, shipping Laputa as a product, riding Stelvio. These require a different gear than 2024. More team, more systems, less heroics. The cycling review was satisfying — seeing 6,200 km logged, the FTP curve rising, the race results improving — it's the same compounding principle as the newsletter, just applied to watts instead of words. Here's to [[2025]].

View File

@@ -1,32 +0,0 @@
---
aliases: ["Complete Two Gran Fondos"]
Is A: Goal
Belongs to: "[[2024]]"
Status: Done
---
# Complete Two Gran Fondos
Cycling is a core part of how I manage energy and sustain focus for the business. Completing two gran fondos in 2024 was about proving that endurance fitness and a demanding content business can coexist, and about setting a physical benchmark for the year tied to [[responsibility-health-fitness]].
## Success criteria
- Finish at least two official gran fondo events (120+ km each)
- Complete both without DNF, regardless of placement
- Maintain consistent training volume of 400+ km/month during the build phase
- No injuries that impact work capacity
## Key milestones
- Plan the cycling season and select target events during [[24q1-plan-cycling-season]]
- Build base fitness through structured winter training (Jan-Mar)
- Complete first gran fondo by end of Q2
- Complete second gran fondo by end of Q3
- Maintain [[measure-cycling-km-per-month]] above 400 km during peak training months
## Notes
- Completed both events successfully: Nove Colli in May and Maratona dles Dolomites in July.
- Training volume averaged 480 km/month from February through July, well above target.
- The structured training plan from [[24q1-plan-cycling-season]] was essential. Without it, the business would have crowded out ride time.
- Key insight: blocking training rides on the calendar like meetings is non-negotiable. Treating fitness as optional leads to skipped sessions.
- This goal set the foundation for the more ambitious [[2025-ride-stelvio]] target.

View File

@@ -1,34 +0,0 @@
---
aliases: ["Double Sponsorship Revenue"]
Is A: Goal
Belongs to: "[[2024]]"
Status: Done
---
# Double Sponsorship Revenue
Revenue growth is the clearest signal that the newsletter business is becoming a sustainable long-term venture. Doubling MRR from ~7k to 14k+ in 2024 would prove that the sponsorship model scales with audience growth and that the business can support a small team without requiring a pivot to paid subscriptions or courses.
## Success criteria
- Reach 14,000 EUR MRR by December 2024
- Maintain or increase average deal size (target: 1,200+ EUR per placement)
- Onboard at least 6 new sponsors over the year
- Achieve 80%+ renewal rate on existing sponsor contracts
- Keep [[measure-close-rate]] above 30% on inbound leads
## Key milestones
- Launch restructured sponsorship packages with tiered pricing in [[24q1-launch-sponsorship-packages]]
- Build a lightweight CRM to manage pipeline and renewals via [[24q2-sponsor-crm]]
- Hit 10k MRR by end of Q2 (midpoint target)
- Reach 12k MRR by end of Q3 with [[24q3-premium-tier]] upsells
- Close Q4 at 14k+ MRR, with pipeline visibility into Q1 2025
## Notes
- Final MRR in December 2024: approximately 15,200 EUR. Goal exceeded by ~8%.
- The tiered sponsorship packages from [[24q1-launch-sponsorship-packages]] were the single biggest driver. The premium tier accounted for 40% of new revenue.
- [[24q2-sponsor-crm]] was simple (a Notion database) but effective. Having a structured pipeline view reduced missed follow-ups significantly.
- Renewal rate landed at 78%, just below the 80% target. Two sponsors churned due to budget cuts, not dissatisfaction.
- The subscriber growth from [[2024-reach-50k-subscribers]] directly enabled higher pricing. Audience size and engagement metrics are the foundation of sponsorship value.
- This sets the trajectory for [[2025-reach-22k-mrr]], which will require both audience growth and continued pricing optimization.

View File

@@ -1,34 +0,0 @@
---
aliases: ["Launch Refactoring Podcast"]
Is A: Goal
Belongs to: "[[2024]]"
Status: Done
---
# Launch Refactoring Podcast
The podcast is the biggest new distribution channel for Refactoring in 2024. Audio content reaches a different audience segment than the newsletter and deepens engagement with existing subscribers. A successful podcast also opens new sponsorship inventory, directly supporting [[2024-double-revenue]] and [[responsibility-sponsorships]].
## Success criteria
- Launch Season 1 by end of Q1 2024
- Publish at least 20 episodes across two seasons in the year
- Reach 1,000+ downloads per episode by Season 2
- Secure at least 2 podcast-specific sponsors
- Maintain a sustainable production cadence without degrading newsletter quality
## Key milestones
- Produce and launch Season 1 during [[24q1-podcast-season-1]] (8-10 episodes)
- Establish production workflow with [[person-paco-furiani]] handling editing and distribution
- Launch Season 2 during [[24q3-podcast-season-2]] with improved format based on Season 1 feedback
- Track growth via [[measure-podcast-downloads]] and [[measure-podcast-episodes-per-month]]
- Review analytics quarterly using [[procedure-podcast-analytics]]
## Notes
- Season 1 launched in March 2024 with 10 episodes. Season 2 launched in August with 12 episodes. Total: 22 episodes, exceeding the 20-episode target.
- Downloads per episode grew from ~400 in Season 1 to ~1,300 by late Season 2. Crossed the 1,000 threshold in episode 16.
- Secured 2 podcast sponsors by Q3, both cross-sold from existing newsletter sponsors. The podcast inventory added approximately 1,800 EUR/month to [[measure-sponsorship-mrr]].
- Production workflow stabilized by episode 5. [[person-paco-furiani]] now handles end-to-end production with minimal input needed.
- Interview format performed better than solo episodes for downloads. Solo episodes had higher completion rates.
- The podcast is now a permanent channel. Planning [[25q2-podcast-season-3]] to continue momentum into 2025.

View File

@@ -1,34 +0,0 @@
---
aliases: ["Reach 50k Subscribers"]
Is A: Goal
Belongs to: "[[2024]]"
Status: Done
---
# Reach 50k Subscribers
Subscriber count is the foundational growth metric for the newsletter business. Reaching 50,000 subscribers by end of 2024 would put Refactoring firmly in the top tier of engineering newsletters, enabling premium sponsorship pricing and establishing the audience base needed for future product launches. This goal is the north star for all [[responsibility-grow-newsletter]] efforts.
## Success criteria
- End 2024 with 50,000+ total newsletter subscribers
- Maintain monthly net growth of 2,000+ subscribers
- Keep unsubscribe rate below 1.5% per issue
- Achieve organic growth rate of at least 60% (not purely paid acquisition)
- Maintain [[measure-open-rate]] above 45% as the list scales
## Key milestones
- Cross 35,000 subscribers by end of Q1, validating early-year growth tactics
- Launch [[24q2-10-pillar-articles]] to drive SEO-based organic acquisition
- Cross 42,000 by mid-year, on pace for the 50k target
- Experiment with [[24q4-linkedin-crossposting]] as an additional growth channel in Q4
- Hit 50,000 by December with a sustained final push through [[responsibility-grow-newsletter]]
## Notes
- Final count: 53,000 subscribers by December 2024. Exceeded target by 6%.
- Organic growth accounted for approximately 65% of new subscribers. Top sources: Twitter/X, SEO from pillar articles, and word-of-mouth referrals.
- The [[24q2-10-pillar-articles]] project was the single biggest organic growth lever. Those 10 articles now account for ~30% of monthly organic signups.
- [[measure-open-rate]] held at 47% even at scale, which is strong for a list this size. This is a key selling point for sponsors.
- LinkedIn cross-posting ([[24q4-linkedin-crossposting]]) added ~150 subscribers directly but had an outsized brand awareness effect.
- The path to [[2025-reach-85k-subscribers]] is clear but will require new channels. Organic growth from existing channels alone will not close the gap.

View File

@@ -1,33 +0,0 @@
---
aliases: ["Read 24 Books in 2024"]
Is A: Goal
Belongs to: "[[2024]]"
Status: Behind
---
# Read 24 Books in 2024
Consistent reading is the primary input for original thinking and content quality. A target of 24 books (2 per month) ensures a steady flow of ideas feeding into newsletter essays, podcast topics, and [[responsibility-learning]]. Falling behind on reading directly correlates with weaker content output.
## Success criteria
- Read 24 books by December 31, 2024 (2 per month average)
- Maintain at least 1 book per month even in busy periods
- Track progress via [[measure-books-per-month]]
- At least 50% should be non-fiction relevant to the newsletter (engineering, business, leadership)
- Create at least 5 [[measure-evergreen-notes-created]] from each book
## Key milestones
- Establish the reading habit in Q1 with 6 books by March
- Maintain pace through Q2 despite the busy season for [[24q2-10-pillar-articles]]
- Reach 18 books by end of Q3
- Close the year at 24 by maintaining 2/month in Q4
## Notes
- Final count: 18 books. Missed the 24-book target by 6 books (75% completion).
- Q1 started strong at 7 books, ahead of pace. Q2 dropped to 3 books due to the pillar articles push and podcast launch.
- Q3 recovered to 5 books. Q4 was only 3 books as year-end business priorities took over.
- The shortfall was entirely a time management issue, not a motivation issue. During weeks with heavy content production, reading was the first thing to get cut.
- Adjusted the 2025 target to 20 books ([[2025-read-20-books]]) to be more realistic given current business demands.
- Key insight: audiobooks during cycling training significantly boosted volume. 6 of the 18 books were consumed this way. Will lean into this more in 2025.

View File

@@ -1,49 +0,0 @@
---
aliases: ["2024"]
Is A: Year
Created at: "2024-01-01"
Has: ["[[24q1]]", "[[24q2]]", "[[24q3]]", "[[24q4]]"]
---
# 2024
## Theme
2024 was the growth year — the year Refactoring went from "successful newsletter" to "real business." At the start of January, I had ~35,000 subscribers, ad-hoc sponsor deals, no podcast, and a vague sense that this could become something bigger. By December, the newsletter had crossed 50k subscribers, revenue had more than doubled, the podcast was a genuine channel, and I had a small team helping me run things. The word that keeps coming back when I think about this year is *professionalization*.
But 2024 was also the year I stopped being just a content creator and started being a founder. Launching sponsorship packages, hiring an editor, building a sponsor CRM, creating a premium tier — these aren't creative acts, they're business-building acts. And toward the end of the year, starting [[24q4-laputa-start]] added a product dimension that I hadn't anticipated. I ended the year wearing more hats than ever, but feeling more focused than ever. Paradox noted.
## Highlights
- Newsletter grew from ~35k to 50k+ subscribers — [[2024-reach-50k-subscribers]] achieved in November
- Revenue more than doubled: [[measure-sponsorship-mrr]] went from EUR 5.5k in January to EUR 11.4k in December — [[2024-double-revenue]] hit
- Launched the podcast in [[24q1]] — grew from 0 to 8k+ downloads/month across two seasons ([[24q1-podcast-season-1]], [[24q3-podcast-season-2]])
- Shipped [[24q3-premium-tier]] with 320+ paid subscribers by year end
- Completed both gran fondos — Nove Colli ([[24q2-spring-gran-fondo]]) and Maratona dles Dolomites — [[2024-complete-two-gran-fondos]] done
- Published [[24q2-10-pillar-articles]] including "Staff Engineer vs. Manager" (50k+ views)
- Delivered [[24q3-codemotion-talk]] to ~400 attendees — first major conference appearance
- Built the team: [[person-matteo-cellini]] on partnerships, [[person-paco-furiani]] on operations, freelance editor for content
- [[24q4-black-friday-campaign]] drove 2,800 new subscribers in 10 days
- Started building [[24q4-laputa-start]] — the PKM tool I'd been dreaming about for years
## By the numbers
- **Subscribers**: 35k to 50.2k (+43%)
- **Sponsorship MRR**: EUR 5.5k to EUR 11.4k (+107%)
- **Podcast downloads**: 0 to 8.2k/month (24 episodes across 2 seasons)
- **Premium subscribers**: 0 to 320 (launched in [[24q3]])
- **Books read**: 26 — [[2024-read-24-books]] exceeded by 2
- **Cycling km**: 6,200 km — [[measure-cycling-km-per-month]] averaged ~517 km
- **Gran fondos**: 2/2 — Nove Colli (7h42m), Maratona dles Dolomites (9h15m)
- **Open rate**: [[measure-open-rate]] averaged 44.2%, up from 41.8% in 2023
- **Conference talks**: 1 (Codemotion Milan)
- **Total revenue**: ~EUR 155k (sponsorships + premium + speaking)
## Reflections
Looking back at 2024, the thing I'm most proud of isn't any single achievement — it's the system. At the start of the year, everything ran through me. By December, [[person-matteo-cellini]] was closing sponsor deals I never even saw, [[person-paco-furiani]] was handling invoicing and logistics autonomously, and the editor was shipping polished drafts on a weekly cadence. I built leverage, not just output.
The podcast was the surprise of the year. I'd been so anxious about launching it — imposter syndrome about my speaking voice, worry about production quality, fear that nobody would listen. But the format I landed on (20-minute focused deep dives) resonated immediately. Season 2 adding guest interviews was the right evolution. By December, the podcast was driving subscriber growth, sponsor interest, and content ideas in a way I hadn't anticipated. It's now an essential pillar, not a side experiment.
The premium tier launch in Q3 validated something I'd been unsure about: that people would pay for curated, structured content even when the free version was already high quality. The 320 subscribers at EUR 12/month represent a small but meaningful revenue stream — and more importantly, a direct relationship with my most engaged readers. The AMA calls are gold for understanding what engineering leaders actually struggle with.
If I'm being honest about what didn't go well: I pushed too hard in Q3. The combination of the premium launch, Codemotion, and Maratona training left me running on fumes by October. The Black Friday campaign in Q4 was successful but felt mechanical — I was going through motions rather than creating with energy. That's a warning sign I need to heed going into [[2025]]. The business can scale, but my energy can't scale linearly with it. I need to hire more, delegate more, and protect creative time more aggressively. The Laputa project ([[24q4-laputa-start]]) is a wildcard heading into next year — it could be a distraction or it could be the most important thing I build. We'll see.

View File

@@ -1,29 +0,0 @@
---
aliases: ["January 2025"]
Is A: Month
Created at: "2025-01-28"
Belongs to: "[[25q1]]"
Rating: "😄"
---
# January 2025
## Focus
- Begin [[25q1-laputa-v1]] development sprint — four-panel layout, markdown rendering, search
- Plan [[25q1-rate-increase]] strategy with [[person-matteo-cellini]]
- Start [[25q1-strength-program]] — 3x/week gym sessions
## Highlights
- Kicked off [[25q1-laputa-v1]] development with a focused 2-week sprint — four-panel layout working, markdown rendering with frontmatter extraction functional
- Researched competitor sponsor rates and prepared [[25q1-rate-increase]] proposal with [[person-matteo-cellini]] — planning to raise Gold from EUR 2.8k to EUR 3.5k/issue
- Started [[25q1-strength-program]]: working with a coach, 3x/week focusing on core stability and leg strength for cycling
- Newsletter at 51.8k subscribers — steady organic growth to start the year
- Read "Build" by Tony Fadell — excellent on product thinking, relevant to Laputa
- Cycling: 320 km, mostly indoor on the trainer — January weather in northern Italy is brutal
## Reflections
January is always a reset month, and this one felt especially deliberate. Coming off the high of [[2024]], I wanted to start [[2025]] with intention rather than momentum alone. The Laputa development sprint was intense — coding every evening after the newsletter work was done, often until midnight. But seeing the four-panel layout come alive with my own vault data was deeply motivating. It's scratching an itch that's been bothering me for years.
The strength program is humbling. My first session, I could barely hold a plank for 60 seconds. Years of cycling have given me the cardiovascular fitness of someone 10 years younger and the upper body strength of someone 10 years older. The coach just smiled and said "we'll fix that." Three weeks in, everything hurts, but I can already feel the difference in posture and stability on the bike.

View File

@@ -1,29 +0,0 @@
---
aliases: ["February 2025"]
Is A: Month
Created at: "2025-02-28"
Belongs to: "[[25q1]]"
Rating: "🤩"
---
# February 2025
## Focus
- Execute [[25q1-rate-increase]] — communicate new pricing to sponsors
- Launch [[25q1-referral-program]] — design tiers, set up tracking
- Continue [[25q1-laputa-v1]] — add search and navigation features
## Highlights
- Executed [[25q1-rate-increase]]: communicated new Gold rate (EUR 3.5k/issue) to all sponsors — 9 out of 10 renewed without pushback, one negotiated a 6-month lock at the old rate
- Launched [[25q1-referral-program]] with three tiers: 3 referrals (exclusive article), 10 referrals (premium trial), 25 referrals (1:1 call) — 1,100 referral subscribers in the first 3 weeks
- [[25q1-laputa-v1]] now has full-text search, type-based sidebar navigation, and keyboard shortcuts — my daily driver for vault browsing
- [[measure-sponsorship-mrr]] jumped to EUR 13.5k after rate increase — significant lift with minimal churn
- Newsletter at 54.2k subscribers — referral program providing a noticeable acceleration
- [[person-matteo-cellini]] onboarded a new enterprise sponsor (cloud infrastructure company) at the higher rate — first deal at the new pricing
## Reflections
This was the month I learned that raising prices is one of the highest-leverage moves in a business. We spent weeks agonizing over the rate increase, running scenarios, worrying about sponsor churn. And then... almost nothing happened. Sponsors said "sure, sounds fair" and renewed. One even said "honestly, I'm surprised you didn't raise earlier." That's the curse of the bootstrapper mindset — you're so grateful for every dollar that you forget to charge what you're worth.
The referral program is generating genuine excitement. Seeing readers actively sharing Refactoring with their colleagues because they want the rewards — it's word-of-mouth at scale. The 1:1 call tier at 25 referrals is clever because it creates a personal connection between me and my most engaged readers. Three people earned it in the first month, and those calls were some of the best conversations I've had about engineering leadership. February was excellent. Everything is tracking.

View File

@@ -1,30 +0,0 @@
---
aliases: ["March 2025"]
Is A: Month
Created at: "2025-03-28"
Belongs to: "[[25q1]]"
Rating: "😄"
---
# March 2025
## Focus
- Complete [[25q1-newsletter-seo-sprint]] — optimize top 15 articles for search
- Ship [[25q1-laputa-v1]] as internal release — feature-complete for personal use
- First outdoor rides of the season
## Highlights
- Completed [[25q1-newsletter-seo-sprint]]: optimized 15 pillar articles with updated titles, meta descriptions, internal linking, and structured data — organic traffic up 40% by month end
- [[25q1-laputa-v1]] shipped as "internal v1" — search, navigation, frontmatter editing, and property panel all working — vault has 9,000+ files and performs well
- Referral program ([[25q1-referral-program]]) running strong — 3,200 total referral subscribers in Q1
- Newsletter hit 56k subscribers — [[25q1]] delivered well above expectations
- [[25q1-strength-program]] paying off — can hold a 2-minute plank now, leg press up 40%, climbing power noticeably improved
- First outdoor ride of the season: 85 km along the Po River, 12 degrees, absolutely beautiful
- [[measure-sponsorship-mrr]] at EUR 14.2k — strong close to [[25q1]]
## Reflections
March was the month where Q1 came together. The SEO sprint was unglamorous work — rewriting titles, adding alt text to images, building internal link maps — but the results were immediate. One article ("How to Run Effective 1:1s") went from page 3 to the #2 result for its target keyword. That single article now brings in 200+ organic subscribers per month. The compounding effect of SEO on a content business is underrated.
Shipping Laputa v1 felt like a real milestone, even if I'm the only user. It's not pretty, it has bugs, and the graph view is more confusing than helpful. But it works. I browse my vault in it every day — types on the left, notes in the middle, properties on the right. The workflow is faster than Obsidian for my use case, and that's all the validation I need to keep building. Next up: bidirectional linking and a better graph. Closing [[25q1]] feeling strong and focused. [[25q2]] is going to be intense.

View File

@@ -1,31 +0,0 @@
---
aliases: ["April 2025"]
Is A: Month
Created at: "2025-04-28"
Belongs to: "[[25q2]]"
Rating: "😄"
---
# April 2025
## Focus
- Begin [[25q2-podcast-season-3]] production — new "Tactical Tuesday" format alongside regular episodes
- Start [[25q2-laputa-v2]] development — bidirectional linking, graph view improvements
- Plan [[25q2-team-retreat]] logistics for late May
## Highlights
- Recorded first 4 episodes of [[25q2-podcast-season-3]] including the debut "Tactical Tuesday" — 15-minute actionable episodes dropping every Tuesday
- Started [[25q2-laputa-v2]] with bidirectional linking engine — the backlinks panel is already transforming how I navigate between notes
- [[25q2-team-retreat]] booked: farmhouse outside Siena, 3 days in late May, agenda drafted with [[person-paco-furiani]]
- Newsletter at 58.5k subscribers — growth accelerating, referral program still compounding
- Published a deep-dive on "The Architecture of Engineering Onboarding" — 28k views, widely shared on HackerNews
- Cycling: 480 km, first real outdoor training block — spring is here and the motivation is high
## Reflections
April felt like the start of the real work for [[25q2]]. The "Tactical Tuesday" format for the podcast was an experiment born from reader feedback — people kept saying "I love the deep dives but sometimes I just need a quick actionable takeaway." So we're doing both: deep dives on Thursdays, tactical tips on Tuesdays. Early numbers are promising — the short episodes have a higher completion rate.
The Laputa work on bidirectional linking was technically challenging but incredibly satisfying. Clicking on a note and seeing all the other notes that link to it, without having to manually maintain those connections — it's the kind of feature that makes you wonder how you ever worked without it. My vault is starting to feel alive, like a knowledge graph I can actually traverse.
Planning the retreat with [[person-paco-furiani]] was a reminder of how much the team dynamic matters. We're four people working remotely, mostly asynchronously. That works for execution, but not for alignment. Three days together in Tuscany should fix that.

View File

@@ -1,30 +0,0 @@
---
aliases: ["May 2025"]
Is A: Month
Created at: "2025-05-28"
Belongs to: "[[25q2]]"
Rating: "😐"
---
# May 2025
## Focus
- Execute [[25q2-team-retreat]] in Tuscany
- Push toward [[25q2-reach-70k]] — growth sprint with coordinated content + referral push
- Continue [[25q2-laputa-v2]] — graph view and frontmatter editing
## Highlights
- [[25q2-team-retreat]] in Siena was transformational — 3 days of strategy, bonding, and cooking together with [[person-matteo-cellini]], [[person-paco-furiani]], and [[person-sara-ricci]]
- The community idea emerged from a dinner conversation at the retreat — [[person-sara-ricci]] pitched it, everyone immediately saw the potential
- Newsletter at 64k subscribers — growth strong but the 70k target for June feels aggressive
- [[25q2-laputa-v2]] graph view working but confusing at scale — need to rethink the visual density for 9,000+ nodes
- Two newsletter issues underperformed — open rates dipped below 40% for the first time this year
## Reflections
The retreat was everything I hoped for and more. There's a moment on the second evening — the four of us on the terrace, bottles of Brunello open, whiteboarding H2 plans on a flip chart — where it hit me that this is a real team, not just a collection of freelancers. [[person-sara-ricci]]'s community pitch was brilliant in its simplicity: "Your readers already help each other in reply-all email chains. Give them a proper space to do it." That became [[25q3-community-launch]].
But May was also the first month this year where I felt like I was slipping. Two issues with below-40% open rates is a warning sign. The topics weren't wrong, but the subject lines were lazy — I wrote them in a rush. In a newsletter business, subject lines are the product as much as the content. Sloppy subject lines = sloppy product. Need to get disciplined about this again.
The Laputa graph view at 9,000 nodes is essentially unusable — just a hairball of connections. I need to rethink the approach: maybe cluster by type, maybe progressive disclosure, maybe skip the full graph entirely and focus on local neighborhoods. Technical challenge, but also a product design challenge. Not everything that works at 100 nodes works at 10,000.

View File

@@ -1,32 +0,0 @@
---
aliases: ["June 2025"]
Is A: Month
Created at: "2025-06-28"
Belongs to: "[[25q2]]"
Rating: "🤩"
---
# June 2025
## Focus
- Hit [[25q2-reach-70k]] subscribers
- Ship [[25q2-laputa-v2]] with refined graph view and improved frontmatter editing
- Start planning [[25q2-dolomites-trip]] logistics for late June
## Highlights
- Hit [[25q2-reach-70k]] subscribers on June 12th — two weeks ahead of schedule, driven by two viral LinkedIn posts and the referral program
- Shipped [[25q2-laputa-v2]]: bidirectional linking, local graph view (neighborhood-based, not full graph), improved frontmatter editing — feels like a real product now
- Completed [[25q2-dolomites-trip]]: 4 days, 5 passes, 380 km with three friends — Passo Fedaia, Pordoi, Sella, Gardena, Campolongo
- [[25q2-podcast-season-3]] "Tactical Tuesday" format growing fast — 14k total downloads/month
- [[measure-sponsorship-mrr]] hit EUR 17.5k — two new enterprise sponsors signed
- Premium tier reached 680 paying subscribers — AMA calls now have 40+ attendees
- Two pillar articles syndicated by InfoQ and The New Stack — drove ~4k new subscribers
## Reflections
June was the peak of [[25q2]] — everything came together. Hitting 70k subscribers two weeks early felt effortless, which is a sign that the flywheel is genuinely spinning now. The LinkedIn posts that went viral weren't planned; they were honest reflections on engineering leadership challenges that resonated. Authenticity scales, apparently.
The Dolomites trip was the cycling highlight of the year so far. Four days of pure mountain riding with friends — the kind of trip where you forget about metrics and just pedal. Passo Fedaia at sunset, the Marmolada glacier turning orange overhead, was a moment I'll remember forever. My legs felt strong — the [[25q1-strength-program]] is paying dividends in climbing power and stability.
Shipping Laputa v2 with the local graph view was the right call. Instead of showing all 9,000 nodes (useless hairball), it shows the current note plus 2 degrees of connection. Clean, useful, navigable. Sometimes the best product decisions are about what you don't show. Heading into Q3 with serious momentum and slightly too much on my plate.

View File

@@ -1,31 +0,0 @@
---
aliases: ["July 2025"]
Is A: Month
Created at: "2025-07-28"
Belongs to: "[[25q3]]"
Rating: "😄"
---
# July 2025
## Focus
- Begin [[25q3-ebook]] production — compile, edit, and design "The Engineering Leader's Playbook"
- Prepare for [[25q3-community-launch]] on Circle — set up platform, invite founding members
- Enter [[25q3-peak-training]] block for Stelvio in August
## Highlights
- [[25q3-ebook]] first draft completed — 12 chapters compiled from the best newsletter content, heavily rewritten for book format, sent to editor
- [[25q3-community-launch]] platform set up on Circle — designed channels (ask-anything, career-advice, tech-leadership, off-topic), invited first 200 premium subscribers as founding members
- [[25q3-peak-training]] underway: structured intervals 4x/week, long ride every Saturday, altitude simulation sessions on the trainer
- Newsletter at 72k subscribers — summer slowdown minimal thanks to the community buzz
- Recorded first episodes of [[25q3-podcast-season-4]] — the 3-part "Building Engineering Culture from Scratch" series
- [[person-sara-ricci]] took over community moderation and onboarding, freeing me to focus on content and training
## Reflections
July was the month where the ambition of Q3 became real. Three major launches (ebook, community, podcast season) plus peak training for Stelvio. On paper it looks insane. In practice... it felt intense but manageable, mostly because [[person-sara-ricci]] stepped up massively on the community side. She went from coordinating podcast guests to designing the entire community architecture. The founding members are engaged, the conversations are high-quality, and I barely had to touch it.
The ebook rewriting was harder than expected. Newsletter articles are written for a 5-minute read with specific context. A book chapter needs to stand alone, build on previous chapters, and go deeper. I spent more time rewriting than compiling. But the result feels substantial — not a lazy content recycling, but a genuine book that adds value beyond the newsletter.
Training is going well. The altitude simulation sessions are brutal — 90 minutes at reduced oxygen while doing threshold intervals — but I can feel my body adapting. FTP is climbing toward 280W. Stelvio from Bormio is 24 km at 7.1% average gradient. At my weight and power, that's roughly 1h50m. Tight, but possible. The mountain is calling.

View File

@@ -1,31 +0,0 @@
---
aliases: ["August 2025"]
Is A: Month
Created at: "2025-08-28"
Belongs to: "[[25q3]]"
Rating: "🤩"
---
# August 2025
## Focus
- Publish [[25q3-ebook]] — final edits, cover design, launch campaign
- Ride Stelvio — the culmination of [[25q3-peak-training]] and [[2025-ride-stelvio]]
- Open [[25q3-community-launch]] to all premium subscribers
## Highlights
- Published [[25q3-ebook]] "The Engineering Leader's Playbook" — 1,400 copies sold in the first month at EUR 29, far exceeding the 500-copy target
- Rode Stelvio from Bormio in 1h52m — [[2025-ride-stelvio]] achieved — the hardest and most beautiful thing I've done on a bike
- [[25q3-community-launch]] opened to all premium members — 850 founding members, 65% weekly active rate from day one
- [[25q3-podcast-season-4]] debut: "Building Engineering Culture from Scratch" Part 1 got 22k downloads in the first week — biggest episode ever
- FTP peaked at 285W during [[25q3-peak-training]] — best numbers of my life
- Newsletter at 75k subscribers
## Reflections
Stelvio. I don't know how to write about it without sounding dramatic, so I'll just be dramatic. It was the hardest thing I've ever done on a bike. The Bormio side starts deceptively gentle — wide roads, gradual grade, you think "this isn't so bad." Then the switchbacks begin. 48 of them. Each one steeper than the last, each one revealing another section of road winding above you. At km 18, with 6 km still to go, I almost stopped. My legs were screaming, my heart rate was at 175, and a voice in my head was saying "you've proved enough." But I kept going because the summit was up there, and I'd been thinking about it since December. 1h52m. Not fast. But done. I cried at the top. No shame.
The ebook launch going well was almost anticlimactic after Stelvio. But 1,400 copies in the first month is genuinely impressive for a self-published niche book. The "real book" effect is interesting — people treat you differently when you have a book, even if the content is largely derived from free newsletter articles. Perception is reality in media.
I need to be honest: by the end of August, I was running on empty. Three big things shipped (ebook, community, Stelvio), podcast recording ongoing, newsletter still weekly. Sleep has been under 6 hours most nights. [[person-paco-furiani]] told me I looked tired on our weekly call. He's right. September needs to include real rest, or I'll pay for it later.

View File

@@ -1,31 +0,0 @@
---
aliases: ["September 2025"]
Is A: Month
Created at: "2025-09-28"
Belongs to: "[[25q3]]"
Rating: "😄"
---
# September 2025
## Focus
- Deliver keynote at [[25q3-leaddev-london]]
- Recover from August intensity — schedule white space, prioritize sleep
- Close Q3 metrics and plan Q4
## Highlights
- Delivered keynote at [[25q3-leaddev-london]] on "The Newsletter-to-Business Pipeline" to 1,200 attendees — biggest stage ever, standing ovation, 5 enterprise sponsor inquiries
- Took 10 days fully off in early September — first real break since January, no email, no Slack, just cycling and reading
- Newsletter hit 78k subscribers by month end — the LeadDev exposure and ebook cross-promotion driving a late-Q3 surge
- [[measure-sponsorship-mrr]] reached EUR 20.1k — [[2025-reach-22k-mrr]] within reach for Q4
- [[25q3-podcast-season-4]] wrapped up with 8 episodes released — consistently above 15k downloads per episode
- Community stabilizing at 850 members with strong engagement — [[person-sara-ricci]] running it almost independently
## Reflections
The 10 days off in early September saved me. I came back from August brittle — snapping at [[person-paco-furiani]] over minor things, dreading the newsletter instead of enjoying it, skipping workouts. The break was non-negotiable: I told the team I'd be unreachable, set up auto-responders, and disappeared into the Dolomites with my bike and a stack of books. By day 4, the fog lifted. By day 7, I was excited about work again. By day 10, I had a notebook full of ideas for Q4.
LeadDev London was the professional peak of the year. The keynote was different from Codemotion last year — bigger stage, broader audience, higher stakes. But I was more prepared and, honestly, more confident. The talk traced the arc from "person with a newsletter" to "media business with a team, a podcast, a book, and a community." The vulnerability of sharing revenue numbers on stage felt risky but landed well. Authenticity again.
Q3 was the most productive quarter of my career. Ebook published, community launched, Stelvio climbed, LeadDev delivered, podcast season completed. But it was also the most draining. The lesson is crystal clear: I can sustain this intensity for 13 weeks, but not 26. Q4 needs to be calmer. Fewer launches. More depth. More sleep.

View File

@@ -1,31 +0,0 @@
---
aliases: ["October 2025"]
Is A: Month
Created at: "2025-10-28"
Belongs to: "[[25q4]]"
Rating: "😐"
---
# October 2025
## Focus
- Begin [[25q4-laputa-v3]] development — AI chat panel, type creation, graph improvements
- Start [[25q4-2026-sponsors]] pipeline with [[person-matteo-cellini]]
- Settle into a sustainable Q4 rhythm after the intensity of Q3
## Highlights
- [[25q4-laputa-v3]] development started: AI chat panel prototype working — can ask questions about vault content and get contextual answers with linked references
- [[person-matteo-cellini]] began [[25q4-2026-sponsors]] outreach — 4 sponsors already in early conversations for Q1-Q2 2026
- Newsletter at 80k subscribers — growth continuing but I'm less obsessed with the number than I used to be
- Community reaching a self-sustaining rhythm — members helping each other without prompting, which is the whole point
- Cycling winding down for the season — 280 km this month, mostly easy rides, letting the body recover from Stelvio
- Read "Working in Public" by Nadia Eghbal and "The Mom Test" by Rob Fitzpatrick — both relevant to the community and Laputa product thinking
## Reflections
October was deliberately quieter than the previous three months, and I'm grateful for it. After Q3's intensity, I needed a month of sustained work at 70% capacity rather than another sprint at 110%. The Laputa v3 work has been the right kind of creative — exploratory, no deadlines, just building something cool. The AI chat panel started as a "what if" experiment and quickly became the most interesting feature I've built. Asking your vault "what did I write about technical debt last year?" and getting a contextual, linked answer feels like magic.
But I'm also feeling a low-grade anxiety about hitting [[2025-reach-85k-subscribers]] by year end. We're at 80k with two months to go. The math works — 2.5k/month is within our normal range — but it doesn't leave room for a bad month. The Black Friday campaign will be critical again.
The sponsor pipeline work is less exciting but equally important. [[person-matteo-cellini]] is already mapping out 2026, which means I need to make decisions about pricing, formats, and what we offer. The business is maturing in ways that require more strategic thinking and less reactive selling. That's a good problem to have, but it requires a different kind of energy.

View File

@@ -1,32 +0,0 @@
---
aliases: ["November 2025"]
Is A: Month
Created at: "2025-11-28"
Belongs to: "[[25q4]]"
Rating: "😄"
---
# November 2025
## Focus
- Push for [[25q4-reach-85k]] with Black Friday campaign
- Ship [[25q4-laputa-v3]] features — type creation UI, improved AI chat
- Begin [[25q4-financial-review]] — compile 2025 revenue and expense data
## Highlights
- Black Friday campaign launched: bundled ebook + premium annual subscription at 30% off — early results strong, 1,800 new subscribers in the first week
- Newsletter at 83.5k subscribers — on pace for [[25q4-reach-85k]] by mid-December
- [[25q4-laputa-v3]] type creation shipped — users (well, me) can now define custom types with properties directly in the app, no more editing YAML manually
- [[25q4-financial-review]] started: 2025 tracking to ~EUR 260k total revenue (sponsorships EUR 210k, premium EUR 35k, ebook EUR 15k)
- [[person-matteo-cellini]] closed 3 more [[25q4-2026-sponsors]] deals — Q1 2026 pipeline at EUR 24k+ MRR
- [[measure-podcast-downloads]] averaging 18k/month — consistent and growing
- AI chat panel refined: better context retrieval, source linking, faster responses — starting to share with a few trusted friends for feedback
## Reflections
The Black Friday campaign this year was more strategic than last year's. Instead of a pure discount play, we bundled the ebook with premium — giving people a reason to upgrade that goes beyond price. Early numbers suggest it's working better than last year's approach. [[person-paco-furiani]] set up the automation so cleanly that I barely had to touch it — sign-ups flowing in, welcome emails going out, access provisioning automatic. The team is genuinely running the machine now.
The financial review numbers are staggering to me. EUR 260k from what was, not that long ago, a side project newsletter. The breakdown is healthy too — sponsorships dominate but premium and the ebook are diversifying the revenue. If the 2026 sponsor pipeline closes as projected, we'll be looking at EUR 300k+ next year. That's "hire a full-time person" territory.
Laputa v3 is getting close to something I could show other people without apologizing for it. The type creation UI was the missing piece — non-technical users (hypothetical ones, since I'm still the only user) can now define their own taxonomy without touching YAML. [[2025-ship-laputa]] feels achievable. I've started sharing it with 3 friends who maintain large vaults. Their feedback has been encouraging: "this is what I wanted Obsidian to be." Music to my ears.

View File

@@ -1,34 +0,0 @@
---
aliases: ["December 2025"]
Is A: Month
Created at: "2025-12-28"
Belongs to: "[[25q4]]"
Rating: "😄"
---
# December 2025
## Focus
- Close the year: hit [[25q4-reach-85k]], finalize [[25q4-financial-review]], run [[25q4-year-review-2025]]
- Complete [[25q4-laputa-v3]] — prepare for closed beta in January
- Reflect, rest, and plan [[2025]] wrap-up
## Highlights
- Newsletter crossed 85k subscribers on December 11th — [[2025-reach-85k-subscribers]] achieved, exactly one year after hitting 50k
- [[25q4-financial-review]] finalized: 2025 total revenue EUR 262k, up 69% from 2024 — [[2025-reach-22k-mrr]] hit in November with MRR at EUR 22.3k
- [[25q4-year-review-2025]] completed: every 2025 goal either hit or exceeded — [[2025-reach-85k-subscribers]], [[2025-reach-22k-mrr]], [[2025-ride-stelvio]], [[2025-ship-laputa]] (beta), [[2025-read-20-books]] (21 books)
- [[25q4-laputa-v3]] beta-ready: 12 closed beta testers lined up for January, onboarding docs written, feedback channels set up
- [[person-matteo-cellini]] locked in 8 sponsors for [[25q4-2026-sponsors]] Q1-Q2 pipeline — projected MRR EUR 24.5k
- End-of-year podcast episode recorded: honest, vulnerable, grateful — the kind of episode that's hard to record and easy to listen to
- Read 3 books in December: finished at 21 for the year
## Reflections
Crossing 85k subscribers felt different from crossing 50k last year. Last year was euphoria — "I can't believe this is happening." This year was quieter — "of course it happened, look at the system we built." That shift from surprise to expectation is both a sign of maturity and a subtle loss. I miss the wonder a little. But I'll take sustainable confidence over periodic amazement.
The financial review was the most sobering exercise of the month. EUR 262k in revenue. A real team of four people. A product in beta. A published book. A keynote at LeadDev. Two years ago, I was a solo newsletter writer hoping to make rent from sponsorships. The speed of change is disorienting when you zoom out. In the day-to-day, it feels gradual. In the annual review, it feels dramatic.
Laputa going to beta testers in January is the thing I'm most excited about heading into 2026. Building software for yourself is satisfying, but it's also a trap — you optimize for your own quirks and blind spots. Having 12 other people use it will surface assumptions I didn't know I was making. Some of those assumptions will be wrong. That's the point.
[[2025]] was the year I stopped being a content creator with a side hustle and became a founder running a media company with a software product. Both descriptions are true, but the second one is the more accurate framing now. Looking ahead: more team, more product, more depth, less heroics. And maybe Ventoux next summer. We'll see.

View File

@@ -1,33 +0,0 @@
---
aliases: ["Reach €22k MRR"]
Is A: Goal
Belongs to: "[[2025]]"
Status: Open
---
# Reach €22k MRR
Growing sponsorship revenue to 22,000 EUR/month is the key financial milestone for 2025. Hitting this target would make Refactoring a comfortably profitable business capable of supporting a team of 3-4, funding product development like [[25q1-laputa-v1]], and providing the financial runway to experiment with new revenue lines such as community or premium offerings.
## Success criteria
- Reach 22,000 EUR MRR by December 2025
- Maintain average deal size above 1,400 EUR per placement
- Grow sponsor roster to 15+ active sponsors (up from ~10)
- Achieve 85%+ renewal rate on existing contracts
- Keep [[measure-close-rate]] above 35% on inbound leads
## Key milestones
- Start the year at 15k MRR baseline (carry-over from 2024 contracts)
- Launch updated sponsorship packages with audience segmentation data in [[25q1]]
- Cross 18k MRR by mid-year through new sponsor acquisition
- Introduce premium placement options tied to [[25q2-reach-70k]] subscriber milestone
- Close Q4 at 22k MRR, with strong pipeline for 2026
## Notes
- Currently tracking at approximately 19k MRR as of Q3. The gap to 22k is closable but requires 2-3 new sponsors or upsells in Q4.
- Subscriber growth ([[2025-reach-85k-subscribers]]) is the enabler. Every 10k subscriber increase unlocks ~1,500 EUR in pricing power.
- The [[25q1-referral-program]] is driving higher-quality subscribers, which improves engagement metrics that sponsors care about (open rate, click rate).
- Risk: if [[measure-subscribers]] growth stalls, pricing increases become harder to justify. Revenue and audience growth are tightly coupled.
- The community experiment ([[25q3-discord-community-soft]]) could become a supplemental revenue line, but is not included in the 22k MRR target to keep it focused on sponsorships.

View File

@@ -1,33 +0,0 @@
---
aliases: ["Reach 85k Subscribers"]
Is A: Goal
Belongs to: "[[2025]]"
Status: Open
---
# Reach 85k Subscribers
Reaching 85,000 subscribers by end of 2025 is the primary growth target for the year. This represents roughly 60% growth over the 53k base from end of 2024 and would position Refactoring as one of the largest independent engineering newsletters globally. Audience scale is the engine that drives [[measure-sponsorship-mrr]], enables premium pricing for [[2025-reach-22k-mrr]], and provides the distribution base for any future product launches.
## Success criteria
- End 2025 with 85,000+ total newsletter subscribers
- Maintain net monthly growth of 2,500+ subscribers
- Keep unsubscribe rate below 1.3% per issue
- Maintain [[measure-open-rate]] above 43% at scale
- Diversify acquisition channels: no single channel should account for more than 40% of new subscribers
## Key milestones
- Launch [[25q1-newsletter-seo-sprint]] to build a long-term organic acquisition engine
- Launch [[25q1-referral-program]] to drive word-of-mouth growth
- Cross 65,000 by end of Q1 (strong start to the year)
- Reach 70,000 by mid-year through [[25q2-reach-70k]] push
- Hit 85,000 by December with sustained multi-channel growth through [[25q4-reach-85k]]
## Notes
- Currently at approximately 75,000 subscribers as of Q3. On track but the pace needs to hold through Q4.
- The [[25q1-referral-program]] has been the strongest new channel in 2025, accounting for ~20% of new subscribers. Referral subscribers also have higher engagement.
- SEO from the [[25q1-newsletter-seo-sprint]] is a slow burn. Articles are ranking but take 3-6 months to reach full traffic potential. Expect the biggest impact in Q4.
- Twitter/X remains the largest single channel at ~35%, close to the 40% cap. Need to continue diversifying.
- Key risk: list quality at scale. As the audience grows, maintaining [[measure-open-rate]] above 43% requires active list hygiene and consistently high content quality from [[responsibility-content-production]].

View File

@@ -1,33 +0,0 @@
---
aliases: ["Read 20 Books in 2025"]
Is A: Goal
Belongs to: "[[2025]]"
Status: Open
---
# Read 20 Books in 2025
Reading remains the highest-leverage input for content quality and original thinking. The 2025 target of 20 books is calibrated to be ambitious but realistic based on the [[2024-read-24-books]] experience, where the 24-book target was missed at 18. The adjusted goal accounts for the increasing demands of a growing business while preserving [[responsibility-learning]] as a non-negotiable priority.
## Success criteria
- Read 20 books by December 31, 2025
- Maintain at least 1 book per month, even in the busiest months
- Track monthly progress via [[measure-books-per-month]]
- At least 60% should be non-fiction relevant to content, engineering, or business
- Create at least 3 newsletter essay ideas per quarter sourced from reading
## Key milestones
- Read 5 books by end of Q1 to build early momentum
- Integrate audiobooks into cycling training to boost volume (learned from 2024)
- Reach 10 books by mid-year
- Maintain pace through the busy Q3 podcast and community launch season
- Close the year at 20 by protecting reading time in Q4
## Notes
- Currently at 14 books as of Q3. On pace to hit 20, but Q4 needs to deliver 6 books which matches the best quarterly performance in 2024.
- Audiobooks during cycling continue to be the most reliable reading channel. 8 of the 14 books so far were consumed via audio during rides.
- The shift to 60% non-fiction is working well. Recent reads on platform economics and community building directly informed the [[25q3-community-launch]] strategy.
- The biggest risk to this goal is the Q4 crunch: [[25q4-reach-85k]] and year-end sponsor renewals tend to consume all available bandwidth.
- Linking reading notes to [[measure-evergreen-notes-created]] helps ensure books translate into lasting knowledge rather than forgotten highlights.

View File

@@ -1,34 +0,0 @@
---
aliases: ["Ride the Stelvio Pass"]
Is A: Goal
Belongs to: "[[2025]]"
Status: Done
---
# Ride the Stelvio Pass
The Stelvio Pass is one of the most iconic climbs in professional cycling: 24.3 km at 7.4% average gradient, reaching 2,758 meters of elevation. Completing a full ascent has been a bucket-list goal for years and represents the intersection of [[responsibility-health-fitness]] and personal ambition. After successfully completing two gran fondos in 2024 ([[2024-complete-two-gran-fondos]]), the Stelvio was the natural next challenge.
## Success criteria
- Complete a full ascent of the Stelvio Pass from Prato allo Stelvio (the classic east side)
- Finish under 2 hours 15 minutes
- No mechanical issues or health incidents during the climb
- Maintain [[measure-cycling-km-per-month]] above 500 km during the 3-month build phase
- Sustain [[measure-resting-hr]] below 52 bpm during peak training
## Key milestones
- Begin structured climbing-specific training block in March 2025
- Complete at least 3 alpine climbs of 1,500+ meters elevation gain as preparation rides by May
- Ride the Stelvio in June or July, weather permitting
- Document the ride for a potential newsletter essay on [[topic-cycling-training]]
- Recover and return to baseline training volume within 2 weeks
## Notes
- Completed the Stelvio in late June 2025 in 2 hours 8 minutes. Under the 2:15 target by 7 minutes.
- The climbing-specific training block made a significant difference. Focused on sustained threshold efforts and long climbs in the Dolomites during April-June.
- [[measure-resting-hr]] averaged 50 bpm during peak training, indicating good aerobic adaptation.
- Weather was perfect on the day. Started early (6:30 AM) to avoid afternoon traffic and wind.
- Wrote a newsletter essay about the experience that became one of the most-engaged issues of the year. Personal stories consistently outperform tactical content.
- This was the most personally meaningful goal completed in 2025. The physical challenge is a reminder that capacity is often self-limited.

View File

@@ -1,34 +0,0 @@
---
aliases: ["Ship Laputa App"]
Is A: Goal
Belongs to: "[[2025]]"
Status: Done
---
# Ship Laputa App
Laputa is a personal knowledge and life management desktop app designed to replace the patchwork of tools currently used to manage the vault of ~9,200 markdown files that power content production, goal tracking, and personal operations. Shipping a usable v1 and adopting it as the daily driver is both a product goal and an infrastructure investment: a better tool for managing knowledge directly improves output quality for [[responsibility-content-production]] and operational clarity across all responsibilities.
## Success criteria
- Ship Laputa v1 as a functional Tauri desktop app with four-panel UI
- Support reading and editing markdown files with YAML frontmatter
- Implement vault navigation, search, and filtering by type
- Use Laputa as the primary daily knowledge management tool (replace current workflow)
- Achieve stable performance with the full ~9,200 file vault
## Key milestones
- Build the core Tauri + React architecture and file I/O layer during [[25q1-laputa-v1]]
- Implement the four-panel UI (sidebar, list, editor, properties) by end of Q1
- Add frontmatter parsing, type filtering, and basic search by mid-Q2
- Iterate on editor experience (CodeMirror 6 integration) during [[25q2-laputa-v2]]
- Reach daily-driver status by end of Q2 and sustain through Q3-Q4
## Notes
- Laputa v1 shipped in March 2025 and has been the daily driver since April. The app handles the full vault without performance issues.
- The Tauri v2 + React + TypeScript stack was the right choice. Desktop performance is excellent and the Rust backend handles file I/O efficiently even at scale.
- The mock layer (`src/mock-tauri.ts`) proved invaluable for rapid UI iteration without needing the full backend running.
- CodeMirror 6 integration was the most complex part of the frontend. Live preview with reveal-on-focus required significant customization.
- [[25q2-laputa-v2]] added refinements: better search, improved frontmatter editing, and the AI chat panel.
- Key lesson: building your own tools is high-leverage when you are the primary user. Every improvement to Laputa directly improves daily workflow efficiency across [[responsibility-content-production]], [[responsibility-personal-finance]], and [[responsibility-learning]].

View File

@@ -1,51 +0,0 @@
---
aliases: ["2025"]
Is A: Year
Created at: "2025-01-01"
Has: ["[[25q1]]", "[[25q2]]", "[[25q3]]", "[[25q4]]"]
---
# 2025
## Theme
2025 is the scale year — the year the question shifted from "can this work?" to "how big can this get?" Coming off the momentum of [[2024]], every metric pointed upward: subscribers, revenue, podcast downloads, team capacity. The goal wasn't just to grow more, but to grow *better* — building systems and a team that could sustain 85k subscribers, EUR 22k MRR, and a product (Laputa) without everything depending on me.
But scaling surfaced a tension I hadn't anticipated: the more successful Refactoring becomes, the more it pulls me away from the craft that made it successful in the first place — writing. Managing sponsors, coordinating a team, shipping a product, speaking at conferences — these are all valuable, but they're not writing. The creative struggle of 2025 has been protecting the space to think deeply and write well, while everything else demands attention. Some months I won that battle. Others, I didn't.
## Highlights
- Newsletter grew from 50k to 82k+ subscribers (on track for [[2025-reach-85k-subscribers]] by year end)
- [[measure-sponsorship-mrr]] grew from EUR 11.4k to EUR 20.1k by end of Q3, tracking toward [[2025-reach-22k-mrr]]
- Shipped three versions of Laputa: [[25q1-laputa-v1]], [[25q2-laputa-v2]], [[25q4-laputa-v3]] — from internal prototype to closed beta candidate ([[2025-ship-laputa]])
- Podcast grew to 18k downloads/month across Seasons 3 and 4 ([[25q2-podcast-season-3]], [[25q3-podcast-season-4]])
- Published [[25q3-ebook]] "The Engineering Leader's Playbook" — 1,400+ copies sold in the first month
- Launched [[25q3-community-launch]] with 850 founding members
- Rode Stelvio from Bormio in 1h52m — [[2025-ride-stelvio]] achieved
- Keynote at [[25q3-leaddev-london]] to 1,200 attendees — biggest speaking engagement to date
- [[25q2-team-retreat]] in Tuscany aligned the team for H2 and catalyzed the community idea
- Executed [[25q1-rate-increase]] — sponsor rates up 25%, retention at 90%
## By the numbers
- **Subscribers**: 50.2k to ~82k (projected 85k by Dec) — +64% YoY
- **Sponsorship MRR**: EUR 11.4k to EUR 20.1k (Q3 close) — targeting EUR 22k by year end
- **Podcast downloads**: 8.2k/month to 18k/month — +120% YoY
- **Premium subscribers**: 320 to 680+ — +112% YoY
- **Ebook sales**: 1,400+ copies (launched Q3)
- **Community members**: 850 (launched Q3)
- **Books read**: on track for [[2025-read-20-books]] (16 through Q3)
- **Cycling km**: ~5,800 km through Q3, projected 7,500+ for year — [[measure-cycling-km-per-month]]
- **Stelvio**: 1h52m from Bormio — personal milestone
- **Conference talks**: 2 (LeadDev London keynote, plus a workshop)
- **Total revenue**: projected ~EUR 260k (+68% vs 2024)
- **Resting HR**: [[measure-resting-hr]] averaged 48 bpm, down from 52 bpm in 2024
## Reflections
The most important thing I did in 2025 was hire well and delegate aggressively. [[person-matteo-cellini]] now runs the entire sponsor relationship lifecycle — from prospecting to renewal — and does it better than I did. [[person-paco-furiani]] owns operations, invoicing, and now coordinates the community logistics. [[person-sara-ricci]] handles podcast production and guest coordination. For the first time, I can take a week off and nothing breaks. That's the real milestone, even if it doesn't show up in the metrics.
The ebook and community launches in Q3 represented a philosophical shift. For three years, Refactoring was a one-to-many broadcast channel: I write, you read. The community made it many-to-many. Engineering leaders helping each other, sharing war stories, giving feedback on each other's challenges. I'm a facilitator now, not just a broadcaster. It's a better model, but it requires a different kind of energy — more listening, less performing.
Laputa has been the most surprising journey of the year. What started as a weekend hack in Q4 2024 has become a serious product with three major versions shipped. Using it daily to manage my 9,000+ file vault has been the best kind of dogfooding — every friction point I hit becomes a feature. The AI chat panel in v3 was a breakthrough moment: asking your own vault questions and getting contextual, linked answers feels like the future of personal knowledge management. Whether this becomes a real product with real users or stays a personal tool, building it has made me a better engineer and a better thinker.
If I'm being honest, Q3 nearly broke me. Three major launches, a keynote, and peak cycling training in the same 13 weeks was too much. I hit a wall in late August — low energy, poor sleep, short temper. [[person-paco-furiani]] noticed before I did. I took 10 days fully off in early September and came back feeling human again. The lesson is one I keep having to relearn: ambition without recovery is just a burnout timeline. Going into Q4 and planning for 2026, I'm trying to build more white space into the calendar. Fewer launches per quarter. More weeks with nothing scheduled. The business can handle it — the question is whether I can resist the urge to fill every gap with a new project. History suggests I can't, but I'm working on it.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Launch Sponsorship Packages"]
Is A: Project
Belongs to: "[[24q1]]"
Advances: "[[responsibility-sponsorships]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Launch Sponsorship Packages
## Overview
This project formalized Refactoring's monetization through sponsorships by creating structured packages that sponsors could evaluate and purchase. Before this, sponsorship deals were ad-hoc and inconsistent — pricing varied per deal, deliverables were loosely defined, and there was no media kit to send prospects.
The goal was to create three clear tiers (Logo, Spotlight, and Deep Dive) with fixed pricing, well-defined deliverables, and a professional media kit PDF. This would make outreach scalable and give [[person-luca-rossi]] a repeatable process instead of negotiating every deal from scratch. The project directly supported [[2024-double-revenue]] by establishing the revenue engine for the year.
## Goals
- Define three sponsorship tiers with clear deliverables and pricing (Logo: $500, Spotlight: $1,200, Deep Dive: $2,500)
- Create a professional media kit PDF with audience demographics, open rates, and testimonials
- Set up a Stripe billing workflow for recurring sponsors
- Draft email templates for outreach, follow-up, and renewal
- Close the first 3 paying sponsors before end of [[24q1]]
## Key decisions
- **Three tiers, not two or four.** Considered a simpler two-tier model, but having three gives sponsors a clear upgrade path. The Logo tier serves as a low-friction entry point.
- **Monthly pricing, not per-issue.** Charging monthly (4 issues) simplifies invoicing and gives sponsors more predictable budgets. This also aligns with how [[measure-sponsorship-mrr]] is tracked.
- **No exclusivity clauses.** Decided against offering category exclusivity because the newsletter audience is too broad to guarantee meaningful exclusivity, and it would limit revenue potential.
## Notes
- The media kit took longer than expected — gathering accurate audience data required exporting from Substack and ConvertKit, which have inconsistent analytics. Settled on a conservative subscriber count to maintain credibility.
- [[person-matteo-cellini]] provided useful feedback on the pricing deck layout. His experience with B2B sales helped shape the value proposition framing.
- First three sponsors came from warm outreach to developer tool companies who had previously engaged with newsletter content. Cold outreach conversion was near zero at this stage.
- This project established the foundation for [[procedure-sponsor-onboarding]] and [[procedure-quarterly-sponsor-outreach]], which were formalized later in [[24q2]].

View File

@@ -1,36 +0,0 @@
---
aliases: ["Plan 2024 Cycling Season"]
Is A: Project
Belongs to: "[[24q1]]"
Advances: "[[responsibility-health-fitness]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Plan 2024 Cycling Season
## Overview
This project laid out the full 2024 cycling calendar — target events, training blocks, rest weeks, and equipment needs. The main objective was to complete two gran fondos in 2024, supporting [[2024-complete-two-gran-fondos]], while maintaining a sustainable training load that would not interfere with work commitments.
Planning happened in January and February so that structured training could begin in March. This included selecting target events, mapping out a periodized training plan (base, build, peak, recovery), and identifying any gear upgrades needed before the season. [[person-paco-furiani]] helped review the training plan and suggested adjustments based on his own racing experience.
## Goals
- Select two target gran fondos for 2024 (spring and autumn)
- Design a 24-week periodized training plan with base, build, and peak phases
- Establish weekly volume targets: 8-12 hours per week during build phase
- Budget and plan equipment upgrades (new wheelset, tire strategy for events)
- Set up tracking in Strava and TrainingPeaks for [[measure-cycling-km-per-month]]
## Key decisions
- **Granfondo di Varese (May) and Granfondo dell'Appennino (September)** selected as the two target events. Varese was chosen for its accessibility and moderate difficulty; Appennino for its challenging profile that would test peak fitness.
- **Polarized training model** rather than threshold-heavy. After reading several studies on amateur endurance performance, decided that 80/20 (easy/hard) distribution would be more sustainable alongside a demanding work schedule.
- **No coach for now.** Considered hiring a cycling coach but decided to self-coach for 2024 using structured plans from TrainingPeaks. If results are good, revisit for 2025.
## Notes
- The biggest risk to the plan was always going to be consistency — weeks with heavy newsletter deadlines or sponsor calls tend to eat into training time. Built in buffer weeks to account for this.
- Realized during planning that the winter base phase was already partially missed. Adjusted by extending the base phase by two weeks and compressing the first build block slightly.
- Equipment decision: went with a new set of carbon wheels (Campagnolo Bora WTO 45) rather than upgrading the frame. Better bang for the buck in terms of performance gain per euro.
- This plan feeds directly into [[24q2-spring-gran-fondo]] for the first event execution and into [[topic-cycling-training]] for ongoing training notes.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Podcast Season 1 Launch"]
Is A: Project
Belongs to: "[[24q1]]"
Advances: "[[responsibility-podcast]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Podcast Season 1 Launch
## Overview
This project launched the Refactoring podcast — a long-form interview show focused on engineering culture, technical leadership, and the human side of building software. The idea had been brewing for months, but the decision to actually ship was driven by [[2024-launch-podcast]] as a key goal for the year.
The strategy was to batch-record 6 episodes before publishing anything, so that the launch would have a backlog and a consistent weekly cadence from day one. This meant spending most of January and February on guest outreach, recording, and editing, with the public launch in mid-March. [[person-sara-ricci]] joined as editor toward the end of the project to handle post-production, which proved essential for maintaining quality while keeping up with the newsletter schedule.
## Goals
- Record 6 episodes before public launch (buffer for consistent weekly releases)
- Secure guests with strong engineering leadership backgrounds (CTOs, VPEs, Staff+ engineers)
- Set up podcast hosting, RSS feed, and distribution to Apple Podcasts, Spotify, and YouTube
- Design cover art and episode template graphics
- Establish [[procedure-podcast-recording]] and [[procedure-podcast-editing]] workflows
## Key decisions
- **Interview format, not solo.** Considered doing solo episodes or co-hosted commentary, but interviews leverage guest audiences for cross-promotion and are more engaging for a new show with no existing listener base.
- **Season model, not continuous.** Adopted a seasonal structure (8-10 episodes per season) with breaks between seasons. This prevents burnout and allows time to plan each season's theme. Season 1 theme: "Engineering Culture."
- **Audio-first, video as bonus.** Recorded video but optimized for audio quality. Video clips are used for social promotion, but the primary distribution is audio podcasts. This reduced production overhead significantly.
## Notes
- Guest booking was the hardest part. Sent about 40 outreach emails to get 8 confirmed guests (6 recorded, 2 cancelled). Cold outreach to high-profile CTOs had about a 5% response rate. Warm intros from [[person-matteo-cellini]] and [[person-david-kim]] were far more effective.
- The first two episodes had noticeable audio quality issues — learned the hard way that remote recordings via Zoom are not sufficient. Switched to Riverside.fm for episodes 3-6, which was a significant improvement.
- Launch week saw about 1,200 downloads across the first 3 episodes, which was above the initial target of 500. The newsletter cross-promotion was the primary driver — [[measure-podcast-downloads]] tracked closely with newsletter send days.
- This project created the foundation for [[24q3-podcast-season-2]] and all subsequent seasons. The workflows established here, particularly [[procedure-podcast-recording]], have remained largely unchanged.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Redesign Newsletter Template"]
Is A: Project
Belongs to: "[[24q1]]"
Advances: "[[responsibility-content-production]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Redesign Newsletter Template
## Overview
The Refactoring newsletter template had been largely unchanged since launch — a plain text-heavy layout that worked at small scale but felt increasingly dated as the subscriber base grew. This project redesigned the email template for better readability, visual hierarchy, and sponsor placement. The redesign needed to improve the reading experience while also making sponsorship placements more visually distinct and valuable, supporting [[2024-double-revenue]].
The new template introduced a cleaner header, better typography spacing, a dedicated sponsor block with clear visual boundaries, and a structured footer with social links and podcast promotion. It was tested across major email clients (Gmail, Apple Mail, Outlook) and went through three iterations based on feedback from a small group of beta readers.
## Goals
- Redesign the email template with improved visual hierarchy and readability
- Create a dedicated, visually distinct sponsor placement section
- Ensure compatibility across Gmail, Apple Mail, Outlook, and mobile clients
- Improve click-through rate on inline links by at least 15%
- Establish the new template as the standard for [[procedure-weekly-newsletter]]
## Key decisions
- **Single-column layout.** Evaluated two-column layouts with sidebar content, but testing showed that single-column performs significantly better on mobile (where 65%+ of opens happen). Simplicity won.
- **Sponsor block above the fold.** Moved the sponsor placement from mid-article to just below the header intro. This increased sponsor visibility without feeling intrusive, since the block is clearly labeled and visually separated. Sponsors noticed and appreciated the change.
- **No images in the main body.** Kept the template text-focused with minimal imagery. Images in email are unreliable (blocked by many clients), increase load time, and distract from the content. The only image is the sponsor logo.
## Notes
- The biggest surprise was how much email client rendering varies. A layout that looked perfect in Gmail was broken in Outlook. Ended up using a very conservative CSS approach with inline styles and table-based layout for maximum compatibility.
- [[person-sara-ricci]] helped with copy feedback on the new footer and CTA sections. Her editorial eye caught several awkward phrasings in the template boilerplate.
- A/B tested the new template against the old one over two issues. The new template showed a 22% increase in click-through rate, primarily driven by better link placement and the structured "further reading" section at the bottom.
- [[measure-open-rate]] remained stable through the transition, confirming that the redesign did not trigger spam filters or cause deliverability issues. This was a real concern given how sensitive email infrastructure is to template changes.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Set Up Investing Framework"]
Is A: Project
Belongs to: "[[24q1]]"
Advances: "[[responsibility-personal-finance]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Set Up Investing Framework
## Overview
As Refactoring's revenue grew, it became clear that personal finance needed a more structured approach. This project defined a personal investment policy statement (IPS) and set up automated monthly contributions to a diversified portfolio of index funds. The goal was to remove emotion and ad-hoc decision-making from investing and replace it with a simple, repeatable system.
Before this project, savings were sitting in a low-yield savings account with no clear allocation strategy. The framework needed to be simple enough to maintain alongside a busy content business, while being sophisticated enough to actually build long-term wealth. Research phase took about three weeks, followed by account setup and automation.
## Goals
- Draft a personal Investment Policy Statement defining asset allocation, risk tolerance, and rebalancing rules
- Open a brokerage account with a low-cost provider (selected Degiro for European access and low fees)
- Set up automated monthly contributions (20% of net monthly revenue)
- Define a target allocation: 70% global equities (VWCE), 20% bonds (VAGF), 10% cash reserve
- Create a quarterly review checklist to assess allocation drift and rebalance if needed
## Key decisions
- **Passive index funds, not individual stocks.** After extensive reading (Bogle, Bernstein, Housel), decided that passive investing is the only approach that makes sense for someone whose primary income comes from a content business. Time spent stock-picking is time not spent on the newsletter.
- **Single accumulating ETF for equities (VWCE).** Rather than splitting across US, European, and emerging market ETFs, chose a single all-world accumulating ETF. Simplicity reduces friction and the urge to tinker.
- **20% of revenue, not a fixed amount.** Tying contributions to revenue percentage means the system scales with business growth and naturally reduces in leaner months without requiring manual adjustment.
## Notes
- The hardest part was actually committing to a framework and stopping the research phase. There is an infinite amount of personal finance content online, and it is easy to fall into analysis paralysis. Set a hard deadline of February 15 to finalize the IPS and stop reading.
- [[person-marco-bianchi]] shared his own investment framework, which was helpful for validating the approach. His suggestion to keep a separate 6-month emergency fund in a high-yield savings account was incorporated.
- The [[24q2-stock-screener]] experiment later in the year was a brief detour into individual stock analysis, but it reinforced the decision to stick with passive investing. The time spent did not justify the marginal (and uncertain) returns.
- Quarterly reviews are tracked as part of [[responsibility-personal-finance]]. The first two reviews in 2024 showed minimal drift, confirming that the set-and-forget approach works as intended.

View File

@@ -1,31 +0,0 @@
---
aliases: ["Q1 2024"]
Is A: Quarter
Created at: "2024-01-01"
Belongs to: "[[2024]]"
Has: ["[[24q1-launch-sponsorship-packages]]", "[[24q1-redesign-newsletter-template]]", "[[24q1-plan-cycling-season]]", "[[24q1-set-investing-framework]]", "[[24q1-podcast-season-1]]"]
Status: Done
---
# Q1 2024
## Focus
The first quarter of 2024 was about laying foundations. After a strong 2023, the goal was to professionalize the business side of Refactoring — move from ad-hoc sponsor deals to structured packages, refresh the newsletter design, and finally launch the podcast that had been in planning for months. On the personal side, I wanted to get the cycling season mapped out early and put a real investing framework in place.
## Highlights
- Launched [[24q1-launch-sponsorship-packages]] with three tiers (Gold, Silver, Bronze) — immediately booked two Gold sponsors for Q2
- Shipped [[24q1-redesign-newsletter-template]] with a cleaner layout, better mobile rendering, and improved CTA placement — [[measure-open-rate]] ticked up ~1.5pp
- Recorded and released the first four episodes of [[24q1-podcast-season-1]], crossing 2k downloads in the first month
- Mapped out the full [[24q1-plan-cycling-season]] including target events: Nove Colli in May and Maratona dles Dolomites in July (goal: [[2024-complete-two-gran-fondos]])
- Built out [[24q1-set-investing-framework]] — monthly DCA into a global ETF portfolio, emergency fund topped up
- Newsletter crossed 37k subscribers by end of March, up from ~35k at year start
- [[person-matteo-cellini]] closed the first two sponsor deals under the new packages within weeks of launch
## Reflections
Q1 felt like the quarter where things started clicking into place on the business side. The sponsorship packages were long overdue — I'd been doing custom deals for every sponsor, which was eating time and leaving money on the table. Having a clear menu made everything faster: [[person-matteo-cellini]] could sell without looping me in on every detail, and sponsors appreciated the transparency.
The podcast launch was nerve-wracking. I'd been procrastinating on it for almost a year, finding reasons to delay. But once the first episode went live and the feedback started coming in, I wondered why I'd waited so long. The format — 20-minute deep dives on one engineering leadership topic — resonated immediately. Production quality was rough in the first couple of episodes, but it improved fast.
On the personal front, planning the cycling season early was a game changer. Having Nove Colli and Maratona on the calendar gave structure to my training weeks. The investing framework was less exciting but equally important — automating contributions meant I could stop thinking about it and focus on building the business. All in all, a solid start to [[2024]].

View File

@@ -1,36 +0,0 @@
---
aliases: ["Write 10 Pillar Articles"]
Is A: Project
Belongs to: "[[24q2]]"
Advances: "[[responsibility-content-production]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Write 10 Pillar Articles
## Overview
This project focused on creating 10 long-form "pillar" articles — comprehensive, SEO-optimized pieces targeting high-traffic search terms in the engineering leadership and developer productivity space. These articles serve a dual purpose: they drive organic search traffic to the Refactoring website (feeding [[measure-subscribers]] growth) and establish topical authority that supports [[2024-reach-50k-subscribers]].
Each article was 2,500-4,000 words, thoroughly researched, and structured for both readability and search engine performance. Topics were selected based on keyword research using Ahrefs, focusing on terms with decent search volume (1K-10K monthly) and moderate competition. The articles were published on the Refactoring blog and cross-promoted through the newsletter over [[24q2]].
## Goals
- Research and select 10 high-value keyword targets using Ahrefs
- Write 10 articles of 2,500-4,000 words each, with clear structure and internal linking
- Optimize each article for on-page SEO (meta descriptions, headers, image alt text)
- Publish on a bi-weekly cadence throughout Q2
- Achieve first-page Google ranking for at least 3 target keywords within 6 months
## Key decisions
- **Evergreen topics only.** All 10 articles target topics that will remain relevant for 2+ years (e.g., "how to run effective 1:1s," "staff engineer vs. engineering manager"). Avoided trending or news-driven topics that would decay quickly.
- **Newsletter-first drafts, then expanded for SEO.** Several articles started as popular newsletter issues that were expanded with additional depth, examples, and structure. This reduced research time and ensured the topics had proven audience interest.
- **No freelance writers.** Considered outsourcing some articles but decided that the Refactoring voice is too distinctive to delegate at this stage. [[person-sara-ricci]] edited all 10 pieces, which maintained quality while freeing time for drafting.
## Notes
- Writing 10 pillar articles in one quarter while maintaining the weekly newsletter was extremely demanding. Weeks 5-8 were particularly brutal — the output quality dipped on newsletter issues during that period. In hindsight, 7-8 articles would have been more sustainable.
- The article on "Engineering Manager vs. Tech Lead" became the highest-performing piece, ranking on page 1 within 8 weeks and driving ~3,000 monthly organic visits. This single article accounted for about 30% of the total organic traffic from all 10 pieces.
- Internal linking between pillar articles and existing newsletter content created a noticeable boost in site-wide SEO metrics. Average session duration increased by 40% during Q2.
- This project directly informed [[topic-content-strategy]] and established the content pillar framework that was later extended in [[25q1-newsletter-seo-sprint]].

View File

@@ -1,36 +0,0 @@
---
aliases: ["Build Podcast Landing Page"]
Is A: Project
Belongs to: "[[24q2]]"
Advances: "[[responsibility-podcast]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Build Podcast Landing Page
## Overview
After launching the podcast in [[24q1-podcast-season-1]], it became clear that there was no proper home for the show on the web. Episodes were scattered across Apple Podcasts, Spotify, and YouTube, but there was no single page where listeners could browse the archive, read show notes, and subscribe. This project created a dedicated landing page on the Refactoring website to serve as the podcast's canonical home.
The page needed to work both as a discovery tool for new listeners and as a reference for existing ones. It also needed to support sponsor visibility, since podcast sponsorships were part of the package tiers defined in [[24q1-launch-sponsorship-packages]]. The page was built using the existing site's tech stack (Next.js) and went live in early May.
## Goals
- Design and build a podcast landing page with episode archive, show notes, and embedded audio player
- Include subscribe CTAs for Apple Podcasts, Spotify, YouTube, and RSS
- Add a "Featured Guest" section to highlight notable episodes and drive social proof
- Integrate sponsor logos and links for current podcast sponsors
- Ensure the page is mobile-responsive and loads quickly (target: <2s LCP)
## Key decisions
- **Static generation, not dynamic.** Each episode page is statically generated at build time from a markdown file with frontmatter metadata. This keeps hosting costs zero (Vercel free tier) and ensures fast page loads. New episodes trigger a rebuild via a GitHub webhook.
- **Embedded player, not custom.** Used the native Spotify/Apple embed widgets rather than building a custom audio player. The maintenance cost of a custom player was not justified given that most listeners use their preferred podcast app anyway.
- **Show notes as full articles.** Rather than just listing bullet points, each episode's show notes page is a full article with key takeaways, timestamps, and links. This gives the pages SEO value and provides genuine utility for listeners who prefer reading to listening.
## Notes
- The page design went through two iterations. The first version was too sparse — just a list of episodes with play buttons. [[person-giulia-conti]] suggested adding guest photos and pull quotes, which made the page significantly more engaging.
- Getting the embedded players to render correctly across browsers was more annoying than expected. Spotify's embed widget has inconsistent height behavior on Safari. Ended up using a fixed-height iframe with a fallback link.
- The landing page became the default link shared in the newsletter for podcast promotion, replacing direct Spotify/Apple links. This centralized analytics tracking through [[measure-podcast-downloads]] and gave better visibility into listener behavior.
- Page traffic is modest (~500 unique visitors/month) but steady, and it ranks for several long-tail podcast-related keywords. The SEO investment in show notes is paying off gradually.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Hire Editor (Sara)"]
Is A: Project
Belongs to: "[[24q2]]"
Advances: "[[responsibility-team-management]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Hire Editor (Sara)
## Overview
By mid-2024, writing the weekly newsletter, managing sponsorships, recording podcast episodes, and handling business operations was becoming unsustainable as a solo operation. The most impactful hire at this stage was an editor — someone who could elevate the writing quality, reduce revision cycles, and free up time for higher-leverage activities like strategy and guest relationships.
After a lightweight hiring process, [[person-sara-ricci]] joined as a part-time editor in May 2024. Sara had previous experience editing technical content for developer publications, which was critical — engineering leadership content requires an editor who understands the domain, not just grammar. This hire directly supported scaling [[responsibility-content-production]] and was a prerequisite for hitting [[2024-reach-50k-subscribers]].
## Goals
- Define the editor role: scope, hours, compensation, and working relationship
- Source and evaluate 5-10 candidates through network referrals and job boards
- Run a paid trial edit with top 3 candidates using a real newsletter draft
- Onboard the selected editor with style guide, editorial calendar, and tooling access
- Achieve a steady-state workflow where drafts go through one editorial pass before publish
## Key decisions
- **Part-time contractor, not full-time employee.** The workload did not justify a full-time hire yet, and contractor status keeps the arrangement flexible as the business scales. Sara works approximately 15 hours/week.
- **Paid trial before commitment.** Rather than hiring based on interviews alone, asked the top 3 candidates to edit the same newsletter draft. This revealed huge differences in editorial judgment that interviews could not surface. Sara's edits were the most substantive — she restructured sections and challenged weak arguments, not just fixed typos.
- **Editor has veto power on publish readiness.** Gave Sara explicit authority to push back on drafts that are not ready. This was uncomfortable at first but has significantly improved quality. If Sara says it needs another pass, it gets another pass.
## Notes
- The hiring process took about 4 weeks from posting to signed contract. Received ~30 applications, shortlisted 8, did calls with 5, trial edits with 3. The trial edit step was by far the most valuable signal.
- Biggest adjustment was learning to write "editor-friendly" first drafts — meaning rough structure and arguments are solid, but prose is intentionally unpolished. Trying to write perfectly on the first pass and then having an editor change it creates friction. Better to write fast and let Sara shape the prose.
- Sara also took on podcast show notes editing, which was not in the original scope but made sense as her familiarity with the content grew. This expanded into the workflow documented in [[procedure-podcast-editing]].
- [[person-matteo-cellini]] recommended Sara — they had worked together at a previous company. Network referrals continue to be the best hiring channel for roles that require domain expertise.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Set Up Sponsor CRM"]
Is A: Project
Belongs to: "[[24q2]]"
Advances: "[[responsibility-sponsorships]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Set Up Sponsor CRM
## Overview
With sponsorship deals increasing after [[24q1-launch-sponsorship-packages]], tracking outreach, deal status, invoicing, and renewals in a spreadsheet was no longer viable. This project built a proper CRM system in Airtable to manage the full sponsor lifecycle — from initial outreach through booking, fulfillment, reporting, and renewal.
The CRM needed to support [[procedure-quarterly-sponsor-outreach]] and [[procedure-sponsor-onboarding]] while being lightweight enough for a one-person sales operation (with occasional help from [[person-matteo-cellini]] on outreach). The system went live in April 2024 and has been the backbone of sponsor management since, directly supporting [[2024-double-revenue]].
## Goals
- Design an Airtable base with tables for Companies, Contacts, Deals, Invoices, and Placements
- Build views for pipeline management (kanban), upcoming renewals, and revenue tracking
- Set up automations for deal stage notifications and renewal reminders (30 days before expiry)
- Import existing sponsor data from the old spreadsheet (~15 companies, ~25 deals)
- Document the CRM workflow and train [[person-matteo-cellini]] on outreach tracking
## Key decisions
- **Airtable, not a dedicated CRM tool.** Evaluated HubSpot, Pipedrive, and Notion. HubSpot was overkill (and expensive) for ~50 deals/year. Pipedrive was close but lacked the flexibility for tracking placement fulfillment. Airtable's combination of structured data, views, and automations hit the sweet spot for this scale.
- **Deal-centric model, not contact-centric.** The primary entity is the Deal (a specific sponsorship booking), not the Contact or Company. This reflects the actual workflow — most actions are about advancing a specific deal, not managing a relationship in the abstract.
- **Automated renewal reminders at 30 and 7 days.** Renewal is the highest-leverage moment in the sponsor lifecycle. Automating reminders ensures no renewal slips through the cracks, which was happening with the spreadsheet system.
## Notes
- Migrating from the spreadsheet exposed several data quality issues — duplicate companies, inconsistent naming, missing invoice dates. Cleaning this up took a full day but was worth it for a clean foundation.
- The kanban view for pipeline management became the default "daily check" for sponsor operations. Being able to see all deals by stage (Prospect, Outreach, Negotiation, Booked, Fulfilled, Renewed) at a glance changed how proactive the outreach process feels.
- Airtable's automation limits on the free plan were a constraint. Upgraded to the Team plan ($20/month) to get enough automation runs. The cost is trivial relative to the revenue it manages.
- [[measure-sponsorship-mrr]] is now calculated directly from the CRM data, which eliminated the manual reconciliation that was happening before. Revenue reporting went from a quarterly headache to an always-current dashboard.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Spring Gran Fondo 2024"]
Is A: Project
Belongs to: "[[24q2]]"
Advances: "[[responsibility-health-fitness]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Spring Gran Fondo 2024
## Overview
This was the first of two target events for 2024, as planned in [[24q1-plan-cycling-season]]. The Granfondo di Varese — 130km with 2,200m of elevation gain — served as both a fitness benchmark and a motivational milestone for the spring training block. The event was scheduled for late May, giving a solid 12-week build phase from March through mid-May.
Preparation included structured interval training (3 sessions/week), long weekend rides (4-5 hours), and two recon rides on sections of the actual course. The goal was not to race competitively but to complete the full route in under 5 hours and 30 minutes, maintaining good form throughout. This project directly advanced [[2024-complete-two-gran-fondos]].
## Goals
- Complete the Granfondo di Varese full route (130km, 2,200m elevation) in under 5:30
- Execute the 12-week build training plan with at least 80% adherence
- Complete two course recon rides to familiarize with key climbs and descents
- Maintain race weight (72kg) through the build phase
- Log all training data for post-event analysis via [[measure-cycling-km-per-month]]
## Key decisions
- **Full route, not the medio.** The event offered a 90km "medio" option. Chose the full 130km because the training plan was designed for it, and the shorter route would not provide a meaningful fitness test. If the legs failed on race day, it would be better to know that at full distance.
- **Conservative pacing strategy.** Rather than going hard on the first climb (which is where most amateurs blow up), planned to ride the first 60km at 75% of threshold power and increase effort in the second half. This negative-split approach is less exciting but far more reliable.
- **No travel the day before.** Drove to Varese two days before the event to avoid travel fatigue. Stayed at a simple hotel near the start line. The extra cost was worth the better sleep and relaxed morning.
## Notes
- Finished in 5:17, which was well within the target. The conservative pacing strategy worked perfectly — felt strong on the final climb while many riders around were walking. Negative split confirmed: second half was 3 minutes faster than the first.
- Training adherence was about 85%, which was above target. The two weeks lost to a mild cold in April were compensated by extending the build phase slightly and cutting one recovery week short.
- [[person-paco-furiani]] rode the event as well and finished about 20 minutes ahead. His feedback on nutrition strategy (eating every 30 minutes from the start, not waiting until hungry) was a game-changer. Applied the same approach going forward.
- Biggest lesson: the mental game matters more than fitness above a certain threshold. The climbs at km 90-110 were physically manageable but mentally draining. Having a clear pacing target on the bike computer helped maintain focus.
- Post-event recovery took about 10 days before training resumed. This feeds into preparation for the autumn event, which was later scoped as a separate project.

View File

@@ -1,35 +0,0 @@
---
aliases: ["Vibe-coding a Stock Screener"]
Is A: Experiment
Belongs to: "[[24q2]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Vibe-coding a Stock Screener
Built a stock screener in Python over a weekend to test whether a simple EMA bounce strategy could surface actionable trade setups on US equities. The broader motivation was to explore whether personal tooling for investment analysis could complement the [[procedure-monthly-portfolio-review]] and eventually improve [[measure-net-worth]] trajectory.
## Hypothesis
A lightweight, self-built screener using exponential moving average crossovers would surface 2-3 viable swing trade candidates per week, outperforming manual chart scanning in both speed and consistency.
## Setup
- Wrote a Python script pulling daily OHLCV data from Yahoo Finance for the S&P 500 universe.
- Implemented a 21/50 EMA bounce filter: flag stocks where price touches the 21 EMA from above, with the 50 EMA still trending up.
- Ran the screener nightly via cron for 4 weeks, logging all flagged tickers and tracking outcomes over 10-day windows.
- Compared results against manual chart review done for the same period.
## Results
- The screener flagged an average of 5 candidates per day (higher than expected).
- Roughly 40% of flagged setups produced a positive 10-day return above 2%.
- Manual chart review caught about 60% of the same setups, but took 3x longer.
- False positives were mostly in low-volume mid-caps where the bounce pattern was noise.
## Takeaways
- Vibe-coding a quick tool like this is a high-leverage weekend project. Total effort was about 8 hours.
- The EMA bounce strategy has signal, but needs volume and volatility filters to reduce false positives.
- Automating the screener freed up time previously spent on manual scanning for the [[procedure-monthly-portfolio-review]].
- Worth maintaining as a personal tool. Not worth productizing, but could become a newsletter content piece on [[topic-personal-finance]].

View File

@@ -1,36 +0,0 @@
---
aliases: ["Video Format Experiment"]
Is A: Experiment
Belongs to: "[[24q2]]"
Status: Abandoned
Owner: "[[person-luca-rossi]]"
---
# Video Format Experiment
Tested whether short-form video (3-5 minute explainers) could work as a distribution channel alongside the newsletter. The idea was to repurpose existing [[responsibility-content-production]] into video format to reach audiences who prefer visual content, potentially feeding [[measure-subscribers]] growth from a new channel.
## Hypothesis
Publishing 2 short technical explainer videos per week on YouTube and Twitter would generate at least 500 new newsletter subscribers over 6 weeks, with a production cost under 3 hours per video once a workflow was established.
## Setup
- Produced 3 videos covering topics already published as newsletter essays: system design patterns, engineering management tips, and a career growth framework.
- Used screen recording with voiceover (no face camera) to minimize production overhead.
- Published on YouTube Shorts and Twitter/X with a newsletter CTA in the description and pinned comment.
- Tracked view counts, click-through to newsletter signup, and time spent per video.
## Results
- Total views across 3 videos: ~4,200 (mostly Twitter, YouTube was negligible).
- Newsletter signups attributed to video: 23 total.
- Average production time: 5-6 hours per video (scripting, recording, editing, captioning).
- The time-to-subscriber ratio was roughly 15x worse than writing a newsletter essay.
## Takeaways
- Video production cost was significantly underestimated. Editing and captioning alone took 2+ hours per video.
- The audience overlap between short-form video viewers and newsletter subscribers appears small.
- Abandoned after 3 videos. The ROI does not justify the effort given current team size and [[responsibility-content-production]] bandwidth.
- If revisited, would need a dedicated video editor or a fundamentally simpler format (e.g., talking head with no editing).
- Better to double down on written content and [[topic-newsletter-growth]] strategies that already work.

View File

@@ -1,32 +0,0 @@
---
aliases: ["Q2 2024"]
Is A: Quarter
Created at: "2024-04-01"
Belongs to: "[[2024]]"
Has: ["[[24q2-hire-editor]]", "[[24q2-build-podcast-landing-page]]", "[[24q2-10-pillar-articles]]", "[[24q2-spring-gran-fondo]]", "[[24q2-sponsor-crm]]"]
Status: Done
---
# Q2 2024
## Focus
Q2 was the execution quarter. With the foundations from Q1 in place, the focus shifted to scaling content production and building systems that could run without me doing everything. Hiring an editor was the biggest unlock. On the cycling side, Nove Colli was the target — months of base training coming to a head.
## Highlights
- [[24q2-hire-editor]] completed — brought on a freelance editor who now handles first-pass editing on every newsletter issue, saving me 4-5 hours per week
- Published [[24q2-10-pillar-articles]]: long-form pieces on topics like "Staff Engineer vs. Manager" and "Technical Debt Prioritization" — two of them hit 50k+ views
- Built and launched [[24q2-build-podcast-landing-page]] with episode archive, show notes, and Apple/Spotify links — podcast downloads crossed 5k/month
- Completed Nove Colli ([[24q2-spring-gran-fondo]]) in 7h42m — not my best time, but finished strong on the climbs
- Shipped [[24q2-sponsor-crm]] in Notion — tracking pipeline, renewal dates, and revenue per sponsor in one place
- Newsletter hit 42k subscribers by end of June, right on track for the [[2024-reach-50k-subscribers]] goal
- [[person-paco-furiani]] started handling more operational tasks — invoicing, sponsor logistics, calendar management
- [[measure-sponsorship-mrr]] reached EUR 8.2k, up from EUR 5.5k at start of year
## Reflections
This was the quarter where I started to feel the leverage of having a small team. With [[person-matteo-cellini]] handling sponsor relationships and [[person-paco-furiani]] taking on operations, I could actually focus on writing and recording. The editor hire was transformative — I went from spending Sunday evenings frantically editing to having a polished draft waiting for me on Monday morning.
The pillar articles strategy paid off more than expected. Instead of chasing weekly topics, I invested in fewer, deeper pieces that could rank on search and get shared. "Staff Engineer vs. Manager" alone brought in 800+ new subscribers organically. This shifted my thinking about content strategy: depth over frequency.
Nove Colli was harder than I expected. The heat in Romagna in late May was brutal, and I bonked on the fourth climb. But crossing the finish line in Cesenatico — exhausted, sunburned, grinning — reminded me why I do this. Already looking forward to Maratona in July. The cycling and the business run on the same fuel: consistent effort over months, then showing up on race day. [[2024-complete-two-gran-fondos]] is halfway done.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Speak at Codemotion Milan"]
Is A: Project
Belongs to: "[[24q3]]"
Advances: "[[responsibility-content-production]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Speak at Codemotion Milan
## Overview
Codemotion Milan 2024 invited [[person-luca-rossi]] to deliver a 30-minute talk on growing a newsletter business as a technical founder. The talk, titled "From Side Project to 50K Subscribers: Building a Content Business in Public," covered the Refactoring growth story, key inflection points, and practical lessons for engineers considering content as a career path or side business.
This was the first major conference speaking engagement and represented an important step in building personal brand credibility beyond the newsletter audience. Conference talks drive a different kind of trust than written content — they put a face and voice to the brand, which strengthens both subscriber acquisition and sponsor confidence. The talk was well-received and led to several valuable connections, including two future podcast guests.
## Goals
- Write and rehearse a 30-minute talk with slides
- Target key themes: newsletter economics, audience building, and the engineering-to-content pipeline
- Deliver the talk at Codemotion Milan in October 2024
- Record the talk for repurposing as a YouTube video and newsletter content
- Generate at least 200 new newsletter subscribers from the event
## Key decisions
- **Story-driven structure, not a how-to.** Rather than a generic "10 tips for growing a newsletter" format, structured the talk as a chronological narrative of the Refactoring journey. Stories are more memorable and more authentic than advice lists.
- **Include real numbers.** Shared actual subscriber counts, revenue milestones, and open rates. Transparency is a core Refactoring value and it differentiates the talk from vague "I grew my audience" presentations. This decision was slightly uncomfortable but generated the most positive feedback.
- **No live demo.** Considered showing the newsletter creation process live but decided the risk of technical issues on stage was not worth it. Used screenshots and short video clips instead.
## Notes
- Preparation took about 3 weeks of part-time work — mostly slide design and rehearsal. Rehearsed the full talk 5 times, including twice in front of [[person-sara-ricci]] and [[person-matteo-cellini]] who gave tough but useful feedback on pacing and clarity.
- The audience was about 300 people, skewing toward mid-career developers and engineering managers. Q&A was lively — the most common question was about time management (how to write a weekly newsletter while also doing other work). This became a topic for a future newsletter issue.
- Post-talk networking led to meeting [[person-emma-wilson]] and [[person-david-kim]], both of whom later appeared as podcast guests in [[24q3-podcast-season-2]] and [[25q2-podcast-season-3]] respectively.
- The talk recording was published on YouTube and performed modestly (~2,500 views in 3 months), but the subscriber spike from the event itself was about 180 — slightly below the 200 target but still a strong result for a single event. The real value was in credibility and relationship building, not direct conversion.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Morning Journaling Habit"]
Is A: Experiment
Belongs to: "[[24q3]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Morning Journaling Habit
Ran an 8-week experiment with daily morning journaling to evaluate its impact on focus, decision-making clarity, and overall stress management. This ties into the broader theme of personal operating systems: if running a content business and training for cycling events simultaneously, mental bandwidth becomes the bottleneck, and journaling is often cited as a way to decompress and prioritize.
## Hypothesis
Writing 10-15 minutes each morning (freeform, no template) would reduce decision fatigue and improve weekly [[measure-task-completion-rate]] by at least 10%, while also providing a subjective sense of reduced stress.
## Setup
- Journaled every morning for 8 weeks using a plain markdown file in the vault.
- No rigid template: some days were gratitude lists, some were brain dumps, some were planning sessions.
- Tracked adherence (did I journal today? yes/no), subjective stress rating (1-5), and weekly task completion rate.
- Weeks 1-4 were during a high-stress period (podcast launch prep for [[24q3-podcast-season-2]] and newsletter deadlines). Weeks 5-8 were calmer.
## Results
- Adherence: 85% in weeks 1-4 (high stress), dropped to 55% in weeks 5-8 (low stress).
- Subjective stress rating improved by ~0.8 points on average during weeks 1-4.
- Task completion rate showed no statistically meaningful change (too many confounding variables).
- The most useful entries were "decision journals" where a specific choice was weighed explicitly.
## Takeaways
- Morning journaling is most valuable during high-stress, high-decision-load periods. It acts as a pressure valve.
- During calm stretches, the habit felt forced and adherence dropped naturally. This is fine; it does not need to be daily forever.
- Decision journals (structured: "What am I deciding? What are the options? What do I fear?") were the highest-signal format.
- Will keep journaling as a situational tool rather than a rigid daily habit. Likely to resurface during intense project sprints like [[25q1-laputa-v1]].
- No direct impact on [[measure-task-completion-rate]], but the qualitative clarity benefit is real.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Expand Sponsor Verticals"]
Is A: Project
Belongs to: "[[24q3]]"
Advances: "[[responsibility-sponsorships]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Expand Sponsor Verticals
## Overview
The first two quarters of sponsorship revenue came primarily from developer tools (CI/CD, monitoring, testing platforms). While this vertical was reliable, concentration risk was high — losing one or two devtools sponsors could significantly impact [[measure-sponsorship-mrr]]. This project expanded the sponsor base into three new verticals: cloud infrastructure, AI/ML platforms, and developer education.
Expanding verticals required adapting the outreach messaging and media kit to resonate with different buyer personas. Cloud infra companies care about enterprise decision-makers in the audience; AI/ML companies want to reach early adopters; education companies want ambitious individual developers. [[person-matteo-cellini]] led much of the outreach effort, using the CRM built in [[24q2-sponsor-crm]] to manage the pipeline. This project was critical for [[2024-double-revenue]].
## Goals
- Identify 30+ target companies across cloud infrastructure, AI/ML, and developer education verticals
- Adapt the media kit with vertical-specific audience data and case studies
- Run targeted outreach campaigns for each vertical (10+ companies each)
- Close at least 5 new sponsors from the new verticals by end of [[24q3]]
- Achieve vertical diversification: no single vertical should represent more than 40% of sponsorship revenue
## Key decisions
- **Three verticals, not more.** Considered also targeting recruitment platforms and SaaS tools, but decided to focus on three verticals where the audience fit was strongest. Spreading too thin would dilute outreach quality.
- **Vertical-specific case studies.** Rather than using a generic media kit, created tailored one-pagers for each vertical showing relevant audience segments. For example, the cloud infra pitch highlighted the percentage of readers who are engineering managers or directors (the buying decision-makers for infrastructure).
- **Higher pricing for AI/ML vertical.** AI/ML companies had larger marketing budgets and higher urgency to reach developers. Tested a 20% price premium on the Deep Dive tier for this vertical, and it held without pushback.
## Notes
- Cloud infrastructure was the easiest vertical to crack — many companies were already familiar with newsletter sponsorships and had established budgets for developer marketing. Closed 3 deals in this vertical within 6 weeks.
- AI/ML was more volatile — companies had budget but were less predictable. Two deals fell through after verbal commitment because of budget freezes. The companies that did close were excellent sponsors with high renewal rates.
- Developer education was harder than expected. Many education companies preferred affiliate/commission models over flat-rate sponsorships. Only closed 1 deal in this vertical during Q3, though it renewed for 3 months.
- By end of Q3, vertical distribution was: devtools 35%, cloud infra 30%, AI/ML 25%, education 10%. This was a significant improvement from the previous 85% devtools concentration.
- The outreach templates developed during this project became part of [[procedure-quarterly-sponsor-outreach]] and continue to be used with minor updates.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Podcast Season 2"]
Is A: Project
Belongs to: "[[24q3]]"
Advances: "[[responsibility-podcast]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Podcast Season 2
## Overview
Building on the momentum from [[24q1-podcast-season-1]], Season 2 expanded the podcast to 8 episodes with a focused theme on engineering leadership and organizational design. The season explored how high-performing engineering teams are structured, how technical leaders make decisions, and the human dynamics that make or break engineering organizations.
The production workflow was significantly smoother this time — [[person-sara-ricci]] handled all post-production editing using the [[procedure-podcast-editing]] workflow established during Season 1, and [[person-paco-furiani]] helped with guest research and outreach. The landing page built in [[24q2-build-podcast-landing-page]] served as the canonical home for all episodes. Season 2 launched in July and ran through September.
## Goals
- Record and publish 8 episodes on engineering leadership and org design
- Secure higher-profile guests (target: at least 2 guests with 10K+ Twitter following)
- Grow average episode downloads to 2,000 (up from Season 1's ~1,500 average)
- Introduce a podcast sponsorship slot (mid-roll read) aligned with newsletter sponsor packages
- Improve audio quality: all episodes recorded on Riverside.fm with backup local recordings
## Key decisions
- **Themed season with a narrative arc.** Rather than random topics, structured Season 2 around a progression: from individual contributor leadership (episodes 1-2) to team dynamics (3-4) to org design (5-6) to executive leadership (7-8). This gave the season a cohesive feel and encouraged binge listening.
- **Mid-roll sponsor read, not pre-roll.** Tested sponsor placements and found that mid-roll reads (inserted at a natural break around the 15-minute mark) had significantly better recall and click-through than pre-roll. This aligned with the sponsorship packages from [[24q1-launch-sponsorship-packages]].
- **No video this season.** Dropped the video component entirely to reduce production overhead. The video clips from Season 1 generated minimal YouTube traction, and the effort-to-impact ratio was poor. Refocused entirely on audio quality.
## Notes
- Guest booking was easier in Season 2 — having a published Season 1 with real download numbers made outreach emails far more credible. Response rate improved from ~15% to ~30%.
- [[person-emma-wilson]] (VP Engineering at a Series C startup) was the standout guest — her episode on "Building Engineering Culture from Scratch" became the most downloaded episode of the season (~3,200 downloads) and was widely shared on LinkedIn.
- Average downloads per episode reached 2,100, exceeding the 2,000 target. The newsletter cross-promotion and improved show notes SEO from [[24q2-build-podcast-landing-page]] were the primary growth drivers.
- The podcast sponsorship slot generated about $1,500 in additional monthly revenue during the season, which validated the channel as a revenue stream alongside newsletter sponsorships.
- [[measure-podcast-downloads]] showed a clear pattern: downloads spike on newsletter send days and taper gradually. This confirms that the newsletter audience is the primary distribution channel for the podcast.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Launch Premium Newsletter Tier"]
Is A: Project
Belongs to: "[[24q3]]"
Advances: "[[responsibility-grow-newsletter]]"
Status: Abandoned
Owner: "[[person-luca-rossi]]"
---
# Launch Premium Newsletter Tier
## Overview
This project explored adding a paid subscription tier to the Refactoring newsletter. The hypothesis was that a segment of the audience would pay $10/month for premium content — deeper case studies, exclusive interviews, and a private community. If successful, this would create a second revenue stream alongside sponsorships and reduce dependency on a single monetization model.
The premium tier launched in mid-July with a 2-week free trial. After 6 weeks of operation, conversion rates were significantly below projections (0.3% vs. the 2% target), and the incremental content burden was unsustainable alongside the free newsletter and podcast. The project was abandoned in early September. While the outcome was a clear negative, the experiment provided valuable data about the audience's willingness to pay and the true cost of premium content production.
## Goals
- Design the premium tier: pricing, content cadence, and exclusive features
- Set up Substack paid subscriptions with a 2-week free trial
- Create 4 premium-only deep dives to serve as the initial content library
- Acquire 200 paying subscribers within the first 6 weeks
- Evaluate viability: target a sustainable $2,000/month from paid subscriptions
## Key decisions
- **$10/month, not $5 or $15.** Benchmarked against similar engineering newsletters. $5 felt too cheap to justify the effort; $15 was above what comparable newsletters charged. $10 was the market rate, but it turned out the audience was not price-sensitive — they simply did not want to pay for content that was perceived as similar to the free tier.
- **Deep dives as the core premium offering.** Rather than gating regular newsletter issues, created separate premium-only deep dives (3,000-5,000 words). This preserved the free newsletter's value while offering something genuinely different. The problem was that the deep dives took 8-10 hours each to write, making the unit economics terrible at low subscriber counts.
- **Abandoned rather than pivoted.** Considered pivoting to a community-only premium tier (no premium content, just Discord access), but decided the timing was wrong. The audience was not large enough to sustain a vibrant paid community. Revisiting community as a concept in [[25q3-community-launch]] with a different approach.
## Notes
- The 0.3% conversion rate was the clearest signal. Industry benchmarks for engineering newsletters suggest 1-3% is typical. Being at 0.3% meant the value proposition was fundamentally off, not just undermarketed.
- The 60 subscribers who did convert were highly engaged and gave positive feedback. Several said they subscribed to "support the newsletter" rather than for the premium content itself. This suggests a patronage model (like Patreon) might work better than a content-gated model.
- The biggest cost was not financial but attentional. Writing premium deep dives while maintaining the free newsletter, podcast, and sponsor obligations created unsustainable pressure during July-August. [[person-sara-ricci]] flagged this early and was right.
- Key lesson: for a sponsorship-funded newsletter, adding paid subscriptions creates a conflict. Free reach drives sponsor value; gating content behind a paywall reduces reach. The two models work against each other unless the audience is very large. Better to focus on growing [[measure-subscribers]] and increasing sponsor rates.
- This experiment informed the decision to try [[25q1-paid-newsletter-trial]] with a completely different approach (annual pricing, bundled with podcast content).

View File

@@ -1,37 +0,0 @@
---
aliases: ["Summer Reading Sprint"]
Is A: Project
Belongs to: "[[24q3]]"
Advances: "[[responsibility-learning]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Summer Reading Sprint
## Overview
July and August are traditionally lighter months for the newsletter (sponsors pause, audience engagement dips during European summer holidays), which creates space for deeper reading. This project set an intentional reading goal of 6 books in two months, focused on history, business, and philosophy of science — topics that inform the newsletter's intellectual depth without being directly "content research."
The sprint contributed to [[2024-read-24-books]] and was structured around dedicated morning reading blocks (7:00-8:30 AM, before work) and longer sessions during weekends. The mix of topics was deliberate: history for perspective, business for practical frameworks, and philosophy of science for clearer thinking about complex systems.
## Goals
- Read 6 books in July and August (3 per month)
- Topic mix: 2 history, 2 business, 2 philosophy of science
- Write brief reading notes for each book in the vault
- Identify at least 3 ideas from the reading that can be developed into newsletter issues
- Maintain the reading habit: minimum 45 minutes per day, 6 days per week
## Key decisions
- **Curated list, not discovery-driven.** Selected all 6 books before the sprint began rather than picking the next book after finishing one. This avoided the decision fatigue that usually slows down reading between books. The list was finalized in late June.
- **Physical books only.** Committed to reading physical copies rather than Kindle during the sprint. The tactile experience and absence of screen distractions improved focus and retention. Kindle is fine for casual reading, but for a concentrated sprint, physical books won.
- **Notes in the vault, not Goodreads.** All reading notes written as markdown files in the personal vault rather than on Goodreads or a public platform. The notes are for personal reference and newsletter idea generation, not social signaling.
## Notes
- Completed 5 of 6 books (83% completion). The sixth book (a dense history of the Scientific Revolution) was about 60% finished by end of August and completed in early September. Close enough to call it a success.
- The standout read was "The Innovator's Dilemma" (re-read) — it directly influenced thinking about the newsletter's competitive dynamics and informed two Q4 newsletter issues on disruption in developer tools.
- Morning reading blocks worked exceptionally well. The consistency of a fixed time slot made the habit nearly automatic by the third week. This approach has been maintained beyond the sprint.
- [[person-marco-bianchi]] recommended "The Structure of Scientific Revolutions" (Kuhn), which turned out to be the most intellectually challenging and rewarding book of the sprint. The concept of paradigm shifts maps surprisingly well onto how engineering organizations evolve.
- Three newsletter issues in Q4 drew directly from the reading: one on Christensen's disruption theory applied to DevOps tools, one on historical patterns in technology adoption, and one on mental models from philosophy of science. The reading-to-content pipeline works, but with a 2-3 month delay.

View File

@@ -1,32 +0,0 @@
---
aliases: ["Q3 2024"]
Is A: Quarter
Created at: "2024-07-01"
Belongs to: "[[2024]]"
Has: ["[[24q3-premium-tier]]", "[[24q3-codemotion-talk]]", "[[24q3-summer-reading-sprint]]", "[[24q3-podcast-season-2]]", "[[24q3-new-sponsor-verticals]]"]
Status: Done
---
# Q3 2024
## Focus
Q3 was about diversifying revenue and expanding reach. The premium tier launch was the biggest bet — adding a paid subscription layer on top of the free newsletter. I also committed to a speaking slot at Codemotion and kicked off Season 2 of the podcast. On the personal side, summer meant a reading sprint and Maratona dles Dolomites.
## Highlights
- Launched [[24q3-premium-tier]] with deep-dive case studies, templates, and a monthly AMA — 320 paid subscribers in the first 6 weeks
- Delivered [[24q3-codemotion-talk]] on "Scaling Engineering Culture" to ~400 attendees — led to 3 inbound sponsor inquiries and a burst of new subscribers
- Completed [[24q3-summer-reading-sprint]]: read 8 books in July-August including "An Elegant Puzzle", "Team Topologies", and "The Ride of a Lifetime"
- Recorded and shipped [[24q3-podcast-season-2]] (episodes 13-24) — introduced guest interviews, with [[person-sara-ricci]] helping coordinate guests
- Opened [[24q3-new-sponsor-verticals]] targeting DevOps tooling and cloud platforms, expanding beyond the original developer tools niche
- Newsletter hit 46k subscribers — [[2024-reach-50k-subscribers]] within reach
- Completed Maratona dles Dolomites in 9h15m — [[2024-complete-two-gran-fondos]] achieved
- [[measure-podcast-downloads]] reached 8k/month, nearly 4x since launch
## Reflections
The premium tier launch taught me a lot about pricing psychology. I initially set it at EUR 8/month, then bumped to EUR 12/month after the first week when conversion was higher than expected. The people who pay aren't price-sensitive — they want signal, not savings. The AMA calls turned out to be the most valued feature, which I didn't expect. Hearing directly from engineering leaders about their challenges gives me better content ideas than any amount of keyword research.
Codemotion was a turning point for visibility. Speaking on stage to 400 people, then having a line of folks wanting to chat afterward — it hit me that Refactoring had become a real brand, not just "Luca's newsletter." Three sponsors reached out within a week of the talk. [[person-matteo-cellini]] converted two of them.
Finishing Maratona was the personal highlight of the year. The Dolomites are otherworldly — Passo Giau at dawn, with the peaks turning pink, was one of those moments where you forget about pace and just exist. Both gran fondos done, [[2024-complete-two-gran-fondos]] checked off. The [[24q3-summer-reading-sprint]] kept me intellectually fueled during the quieter August weeks. Heading into Q4 feeling strong on all fronts.

View File

@@ -1,36 +0,0 @@
---
aliases: ["Team Annual Review"]
Is A: Project
Belongs to: "[[24q4]]"
Advances: "[[responsibility-team-management]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Team Annual Review
## Overview
As the Refactoring team grew from a solo operation to a small but functional team, it became clear that informal check-ins were not sufficient for aligning on expectations, giving structured feedback, and planning development. This project designed and executed the first formal annual review process for the three team members: [[person-matteo-cellini]] (sponsorship outreach), [[person-paco-furiani]] (operations and podcast support), and [[person-sara-ricci]] (editing).
The goal was not to create a heavyweight corporate HR process but to establish a lightweight, honest, and useful review cycle that fits a small indie team. Reviews happened in December 2024, covering the full calendar year, and set the stage for [[25q1]] planning. The process was designed to be reusable and will run annually going forward.
## Goals
- Design a review template covering: role scope, key accomplishments, areas for growth, and goals for next year
- Write a self-assessment prompt for each team member to complete before the review meeting
- Conduct 1-hour review conversations with each team member
- Document agreed-upon goals and any compensation adjustments for 2025
- Establish this as a repeatable annual process with a calendar reminder and documented procedure
## Key decisions
- **Conversation-first, not form-first.** The review is structured around a 1-hour conversation, not a lengthy written form. The self-assessment is a short (half-page) prompt to help team members reflect before the meeting. The conversation is where the real feedback happens.
- **Bidirectional feedback.** Each team member was explicitly asked to give feedback on working with [[person-luca-rossi]] — what works, what does not, what they need more of. This was uncomfortable but produced the most actionable insights of the entire process.
- **Compensation adjustments tied to review, not negotiation.** Rather than waiting for team members to ask for raises, proactively included compensation review as part of the process. All three team members received adjustments based on expanded scope and demonstrated value.
## Notes
- [[person-sara-ricci]]'s review was the most impactful. She articulated clearly that the editing workflow needed more lead time — receiving drafts 24 hours before publish was creating unnecessary stress and reducing edit quality. This led to adjusting the [[procedure-weekly-newsletter]] timeline to give her 48 hours minimum.
- [[person-matteo-cellini]] expressed interest in taking on more strategic work beyond outreach execution. This was a growth signal — his Q4 performance on [[24q3-new-sponsor-verticals]] showed he could handle strategic thinking, not just tactical execution. Agreed to expand his role in 2025.
- [[person-paco-furiani]] was the most straightforward review — he is happy with his current role scope and compensated fairly. His main feedback was wanting more visibility into the business roadmap, which is a fair ask. Started sharing quarterly plans with the full team afterward.
- The process took about 6 hours total (prep + 3 conversations + documentation). This is very manageable for a small team. The key learning: do this every year, not just when it feels necessary. Scheduled the 2025 review for December in advance.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Black Friday Newsletter Campaign"]
Is A: Project
Belongs to: "[[24q4]]"
Advances: "[[responsibility-grow-newsletter]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Black Friday Newsletter Campaign
## Overview
Black Friday represents a unique opportunity for a technical newsletter: readers are actively looking for tool and resource recommendations, and companies are offering significant discounts. This project designed and executed a curated Black Friday campaign — a special edition newsletter with hand-picked tool recommendations for engineering leaders and developers, paired with a subscriber acquisition push through social media and cross-promotions.
The campaign ran from November 25-29, 2024, and generated +1,200 new subscribers in 3 days — the single largest acquisition spike of the year. The key insight was that curation is the value: readers do not want to sift through hundreds of generic deals. They want a trusted voice telling them which 10-15 tools are actually worth buying. This directly supported [[2024-reach-50k-subscribers]] and pushed the subscriber count past the 48K mark.
## Goals
- Curate a list of 15-20 Black Friday deals on developer tools, SaaS products, and learning resources
- Negotiate exclusive or enhanced discount codes with at least 5 companies
- Publish a special-edition newsletter issue on Black Friday week
- Run a social media campaign (Twitter, LinkedIn) driving traffic to the newsletter signup with the deal list as the lead magnet
- Acquire 1,000+ new subscribers during the campaign window
## Key decisions
- **Curated recommendations, not affiliate links.** While affiliate revenue was tempting, prioritized trust over short-term monetization. Every recommendation was a tool that [[person-luca-rossi]] personally uses or has thoroughly evaluated. Included honest caveats alongside recommendations. This authenticity drove shares and word-of-mouth signups.
- **Gated the full list behind email signup.** Shared 5 deals publicly on social media with a CTA to "get the full list of 20 deals" via newsletter signup. This was the primary acquisition mechanism and converted at approximately 35% of landing page visitors.
- **Separate from regular sponsor placements.** Black Friday sponsors paid a premium rate for a dedicated section, clearly labeled as "sponsored picks." This maintained editorial integrity while monetizing the high-attention window. Three sponsors participated at $3,000 each.
## Notes
- The +1,200 subscribers in 3 days was remarkable, but retention was the real test. At the 30-day mark, 78% of Black Friday subscribers were still active (industry benchmark is ~60% for acquisition spikes). This suggests the deal-seekers were genuinely interested in the newsletter's regular content, not just the deals.
- Negotiating exclusive discount codes with tool companies was easier than expected. Most developer tool companies are eager for newsletter placement during Black Friday and will create custom codes quickly. Started these conversations in early November — next year, will start in October.
- [[person-matteo-cellini]] handled sponsor coordination for the campaign, managing the three premium placements. His work on the CRM ([[24q2-sponsor-crm]]) made tracking these deals seamless.
- The social media push was amplified significantly by retweets/reshares from the tools featured in the list. This organic amplification was not planned but accounted for an estimated 30-40% of the total reach. Will be more intentional about asking for shares in future campaigns.
- [[measure-subscribers]] jumped from ~46,800 to ~48,000 during the campaign. Combined with organic growth, this put the year-end total within striking distance of the 50K target set in [[2024-reach-50k-subscribers]].

View File

@@ -1,38 +0,0 @@
---
aliases: ["Cycling Year in Review 2024"]
Is A: Project
Belongs to: "[[24q4]]"
Advances: "[[responsibility-health-fitness]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Cycling Year in Review 2024
## Overview
This project reviewed the full 2024 cycling season — training volumes, event results, fitness progression, and gear decisions — and used the analysis to plan the 2025 season. The review covered everything from the spring training plan established in [[24q1-plan-cycling-season]] through the two gran fondos and into the autumn detraining period.
The review was conducted in November-December, using data from Strava, TrainingPeaks, and personal notes. It combined quantitative analysis (volume, intensity distribution, power data) with qualitative reflection (what felt good, what caused fatigue, where motivation dipped). The output was both a retrospective document and a set of concrete recommendations for [[25q1-strength-program]] and the 2025 race calendar.
## Goals
- Compile and analyze full-year training data: total distance, climbing, hours, and TSS
- Review performance at both gran fondos (Varese in May, Appennino in September)
- Assess the effectiveness of the polarized training model adopted in Q1
- Identify 3-5 specific areas for improvement in 2025
- Draft a preliminary 2025 season plan including target events and training philosophy adjustments
## Key decisions
- **Add structured strength training for 2025.** The year review revealed that upper body and core fatigue was a limiter on long climbs. Pure cycling volume is not enough — need complementary strength work. This directly led to [[25q1-strength-program]].
- **Target the Stelvio in 2025.** The two 2024 gran fondos were rewarding but relatively conservative choices. For 2025, the stretch goal is the Stelvio — one of cycling's most iconic climbs. This requires a step up in training specificity and altitude preparation. Supports [[2025-ride-stelvio]].
- **Hire a coach for 2025.** Self-coaching worked reasonably well in 2024 (both events completed within targets), but the Stelvio goal requires more sophisticated periodization and real-time plan adjustments. Will look for a coach in Q1.
## Notes
- Full-year numbers: 8,200 km ridden, 98,000m elevation gain, approximately 320 hours on the bike. This was above target (7,500 km) and sustainable — no overtraining symptoms or significant injuries.
- The polarized model (80% easy / 20% hard) was broadly successful, though the actual distribution ended up closer to 75/25 because "easy" rides often crept into moderate territory. Discipline on easy days is something to improve.
- [[person-paco-furiani]] completed the same two events and his season review is a useful comparison point. His higher-volume approach (10,500 km) yielded better absolute results but also led to a knee issue in October. Reinforces the value of sustainability over pure volume.
- The Campagnolo Bora WTO 45 wheels purchased pre-season were the best equipment decision of the year. Noticeable improvement on climbs and crosswind stability. No regrets on prioritizing wheels over frame upgrade.
- [[measure-cycling-km-per-month]] data shows clear seasonal pattern: low in Jan-Feb (800-900km/month), building through Mar-May (1,000-1,200km), peaking in Jun-Aug (1,200-1,400km), and tapering in Sep-Nov. December was deliberately rest-focused (~400km).
- The retrospective feeds directly into [[topic-cycling-training]] notes and sets the direction for 2025 season planning.

View File

@@ -1,37 +1,17 @@
---
aliases: ["Start Laputa App Project"]
Is A: Project
Belongs to: "[[24q4]]"
Advances: "[[responsibility-learning]]"
Status: Done
Owner: "[[person-luca-rossi]]"
type: Project
aliases:
- "[[Start Laputa App Project]]"
belongs_to: "[[24q4]]"
owner: "[[person-luca-rossi]]"
status: Done
---
# Start Laputa App Project
## Overview
The original spike that proved Tolaria could read a markdown vault, render note metadata, and support keyboard-first navigation.
After years of managing a personal knowledge vault in Obsidian and finding it increasingly insufficient for the structured, frontmatter-heavy workflow that had evolved, this project kicked off the development of Laputa — a custom desktop application built specifically for the vault's unique requirements. The app is built with Tauri v2 (Rust backend) and React (TypeScript frontend), designed to read and write the same markdown files with YAML frontmatter that the vault already uses.
- Set the initial four-panel layout.
- Proved the note list, editor, and inspector could coexist in one flow.
- Led directly into [[25q1-laputa-v1]].
The motivation was not to replace Obsidian for general note-taking, but to build a purpose-built tool for the structured data layer — types, relationships, properties, and views that Obsidian handles poorly. Laputa treats the vault as a lightweight database of markdown files, offering a four-panel UI with type-aware navigation, property editing, and relational browsing. This project covers the initial spike: architecture decisions, proof of concept, and first working prototype. It advances [[2025-ship-laputa]] as the longer-term goal.
## Goals
- Define the technical architecture: Tauri v2, React 18, CodeMirror 6, Vite
- Build a proof of concept that reads markdown files from a directory and displays them in a panel UI
- Implement frontmatter parsing in Rust (using serde and gray_matter)
- Create the basic four-panel layout: type sidebar, note list, editor, and property panel
- Validate that the app can handle the full vault (~9,200 markdown files) without performance issues
## Key decisions
- **Tauri v2, not Electron.** Tauri produces much smaller binaries, uses less memory, and leverages native webview rather than shipping a full Chromium instance. For a single-user desktop app, Tauri's trade-offs (no cross-platform webview consistency, newer ecosystem) are acceptable. The Rust backend is also more natural for file I/O heavy workloads.
- **Files as the source of truth, not a database.** Laputa reads and writes markdown files directly. No SQLite, no JSON store, no separate database. This means the vault remains portable, version-controllable with git, and readable in any text editor. The cost is that some queries are slower than they would be with a database, but for ~10K files, filesystem operations are fast enough.
- **CodeMirror 6, not ProseMirror or TipTap.** CodeMirror 6 provides the best foundation for a reveal-on-focus markdown editor — showing rendered markdown by default but revealing raw syntax when the cursor enters a block. This is the editing model that feels most natural for power users.
## Notes
- The initial proof of concept took about 3 weeks of focused evening/weekend work. Getting Tauri v2 set up and communicating between the Rust backend and React frontend required working through several documentation gaps — Tauri v2 was still relatively new and the ecosystem was evolving.
- Performance testing with the full vault (~9,200 files) showed that initial directory scanning takes about 2 seconds, and frontmatter parsing adds another 1.5 seconds. This is acceptable for app startup, though indexing will need optimization for real-time search.
- [[person-david-kim]] (who works on developer tools) provided useful early feedback on the UI concept during a podcast conversation. His perspective on tool-building for personal use versus building products helped frame the project's scope.
- The mock data layer (`src/mock-tauri.ts`) was built early and proved invaluable for browser-based testing. Being able to develop and test the React frontend without the Rust backend running dramatically accelerated UI iteration.
- This project transitions directly into [[25q1-laputa-v1]] for the first real milestone of the app.

View File

@@ -1,37 +0,0 @@
---
aliases: ["LinkedIn Cross-posting Experiment"]
Is A: Experiment
Belongs to: "[[24q4]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# LinkedIn Cross-posting Experiment
Tested whether repurposing newsletter essays as LinkedIn posts could drive meaningful follower growth and referral traffic back to the newsletter. LinkedIn's algorithm favors native content, so the hypothesis was that adapted versions of existing essays would perform well without requiring net-new writing effort, supporting both [[responsibility-grow-newsletter]] and [[topic-newsletter-growth]].
## Hypothesis
Posting 3 adapted newsletter essays per week on LinkedIn for 6 weeks would generate at least 500 new LinkedIn followers and drive 200+ clicks to the newsletter signup page, at under 30 minutes of incremental work per post.
## Setup
- Selected the top-performing newsletter essays from [[24q3]] and [[24q4]] based on [[measure-open-rate]].
- Adapted each essay into LinkedIn-native format: shorter paragraphs, hook-first structure, no external links in the body (link in first comment instead).
- Posted 3x per week (Monday, Wednesday, Friday) for 6 weeks.
- Tracked LinkedIn follower growth, post impressions, engagement rate, and UTM-tagged clicks to the newsletter signup page.
## Results
- LinkedIn followers: +812 over 6 weeks (from ~2,400 to ~3,200).
- Average post impressions: ~6,500 (range: 1,200 to 28,000).
- Newsletter signups via LinkedIn referral: 147 (below the 200 target but still meaningful).
- Time per post: approximately 20 minutes (mostly reformatting, not rewriting).
- Top-performing posts were contrarian takes and personal stories; tactical how-to posts underperformed.
## Takeaways
- LinkedIn cross-posting is a high-ROI channel for newsletter growth. The effort-to-output ratio is excellent since the content already exists.
- Follower growth exceeded the target. The platform rewards consistency and native formatting.
- Referral traffic was modest. LinkedIn users tend to engage on-platform rather than clicking out. The real value is brand awareness and top-of-funnel reach.
- Will continue cross-posting as a permanent part of the [[procedure-weekly-newsletter]] workflow.
- Consider having [[person-matteo-cellini]] handle the reformatting to free up time for [[responsibility-content-production]].

View File

@@ -1,37 +0,0 @@
---
aliases: ["Build Sponsor Dashboard"]
Is A: Project
Belongs to: "[[24q4]]"
Advances: "[[responsibility-sponsorships]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Build Sponsor Dashboard
## Overview
As the sponsor base grew through 2024, reporting became a bottleneck. After each newsletter issue, sponsors would ask for performance data — click counts, open rates, and placement impressions. This data was being compiled manually from ConvertKit analytics and sent via email, which was time-consuming and error-prone. This project built an Airtable-based dashboard that gives sponsors self-serve access to real-time performance data for their placements.
The dashboard connects to the CRM built in [[24q2-sponsor-crm]] and pulls performance data from ConvertKit's API via a Zapier integration. Each sponsor gets a unique link to a filtered Airtable view showing only their placements, metrics, and invoice history. This project was a significant step toward professionalizing the sponsor experience and supporting [[2024-double-revenue]] through improved retention and upsell conversations.
## Goals
- Build an Airtable dashboard with per-sponsor performance data (clicks, CTR, impressions)
- Set up a Zapier integration to pull click and open data from ConvertKit after each newsletter send
- Create unique, filtered dashboard links for each sponsor (no login required, link-based access)
- Include historical performance trends (week-over-week, month-over-month)
- Reduce sponsor reporting time from ~2 hours/week to near-zero
## Key decisions
- **Airtable interface, not a custom web app.** Considered building a custom dashboard with Next.js and a database, but the engineering effort was not justified for ~20 active sponsors. Airtable's Interface Designer provides 80% of the functionality at 5% of the development cost. Can always migrate to a custom solution if the sponsor base grows significantly.
- **Self-serve access via unique links.** Rather than requiring sponsors to log into a portal, each sponsor gets a unique Airtable shared view URL. This is simpler for both parties — no password management, no onboarding friction. The trade-off is that anyone with the link can see the data, but the risk is low for non-sensitive marketing metrics.
- **Automated data ingestion, manual quality checks.** The Zapier integration pulls data automatically after each send, but [[person-matteo-cellini]] reviews the numbers weekly before they appear on sponsor dashboards. This catches any data anomalies (e.g., bot clicks, tracking issues) before sponsors see them.
## Notes
- The dashboard launch received very positive feedback from sponsors. Several mentioned it was the most transparent reporting they had seen from any newsletter sponsorship. This transparency became a competitive advantage in renewal conversations.
- The Zapier integration was the most fragile part of the system. ConvertKit's API rate limits and occasional data delays caused the integration to fail about once a month. Building retry logic and error notifications helped, but this remains a maintenance burden.
- Sponsor self-serve reporting reduced [[person-matteo-cellini]]'s weekly reporting workload from approximately 2 hours to about 15 minutes (just the quality review). This freed significant time for outreach and relationship management.
- The dashboard data also proved valuable for internal analysis. Being able to see click-through rates across all sponsors and placements revealed which types of copy and positioning perform best, informing both editorial decisions and sponsor pitch strategy.
- One unexpected benefit: sponsors who could see their own performance data were more likely to experiment with different ad copy and CTAs. This improved their results and, by extension, their satisfaction and renewal rates. The dashboard supported [[measure-sponsorship-mrr]] growth indirectly through improved retention.

View File

@@ -1,32 +1,16 @@
---
aliases: ["Q4 2024"]
Is A: Quarter
Created at: "2024-10-01"
Belongs to: "[[2024]]"
Has: ["[[24q4-annual-review-process]]", "[[24q4-sponsor-dashboard]]", "[[24q4-laputa-start]]", "[[24q4-black-friday-campaign]]", "[[24q4-cycling-year-review]]"]
Status: Done
type: Quarter
aliases:
- "[[Q4 2024]]"
status: Done
has:
- "[[24q4-laputa-start]]"
---
# Q4 2024
## Focus
The quarter where the Laputa prototype became real enough to replace sketches and notes.
Q4 was about closing the year strong and planting seeds for 2025. The Black Friday campaign was the commercial centerpiece. But the most exciting thing was starting work on [[24q4-laputa-start]] — the personal knowledge management tool I'd been sketching in notebooks for years. The quarter also meant wrapping up the annual review process and reflecting on a transformative cycling season.
- Started [[24q4-laputa-start]] as the first working app spike.
- Captured the initial panel layout and editor decisions.
## Highlights
- [[24q4-black-friday-campaign]] drove 2,800 new subscribers in 10 days and converted 180 free readers to premium — best single campaign ever
- Built [[24q4-sponsor-dashboard]] giving sponsors real-time access to their campaign metrics — reduced back-and-forth emails by ~70%
- Started [[24q4-laputa-start]]: initial Tauri + React prototype, basic markdown reading, YAML frontmatter parsing — the MVP of the MVP
- Designed and documented [[24q4-annual-review-process]] — a structured template for reviewing goals, projects, and metrics across the year
- Completed [[24q4-cycling-year-review]]: 6,200 km ridden, two gran fondos, FTP up 12% year-over-year
- Newsletter crossed 50k subscribers in November — [[2024-reach-50k-subscribers]] achieved
- [[measure-sponsorship-mrr]] closed the year at EUR 11.4k, more than doubling from January — [[2024-double-revenue]] hit
- [[2024-read-24-books]] finished with 26 books total — exceeded target
## Reflections
Hitting 50k subscribers felt like a milestone that validated the whole year's strategy. When I started 2024 at 35k, doubling the growth rate felt ambitious. But the combination of pillar content, podcast reach, the Codemotion talk, and the Black Friday push all compounded. Each channel fed the others in ways I couldn't have predicted at the start of the year.
Starting Laputa was the sleeper highlight of Q4. I'd been frustrated with Obsidian's limitations for months — specifically around structured data, relationships between notes, and the inability to build custom views. So I started building my own tool. The early prototype was rough, but the moment I saw my own vault rendered in a four-panel layout I'd designed, something clicked. This could be more than a side project.
The sponsor dashboard was [[person-matteo-cellini]]'s idea, and it was brilliant. Sponsors love transparency, and giving them self-serve access to metrics reduced our support burden dramatically. [[person-paco-furiani]] set up the automated reporting pipeline. Looking back at [[2024]] as a whole, every major goal was hit or exceeded. That doesn't happen often, and I don't take it for granted. Now the question is: how do we scale this in [[2025]] without burning out?

View File

@@ -1,37 +1,17 @@
---
aliases: ["Laputa App V1"]
Is A: Project
Belongs to: "[[25q1]]"
Advances: "[[responsibility-learning]]"
Status: Done
Owner: "[[person-luca-rossi]]"
type: Project
aliases:
- "[[Laputa App V1]]"
belongs_to: "[[25q1]]"
owner: "[[person-luca-rossi]]"
status: Done
---
# Laputa App V1
## Overview
The first usable release for daily browsing, quick open, and note-property editing.
Following the proof of concept built in [[24q4-laputa-start]], this project delivered the first working version of Laputa with a complete four-panel layout, a property inspector, and a quick-open command palette. V1 represents the transition from "can this work?" to "I can actually use this daily" — the app became the primary interface for browsing and editing the vault, replacing Obsidian for structured data workflows.
- Shipped the working command palette.
- Made the inspector practical for real frontmatter editing.
- Captured enough confidence to continue with [[25q2-laputa-v2]].
The four-panel layout includes a type sidebar (showing all note types like Project, Person, Quarter), a filterable note list, the main editor panel with CodeMirror 6, and a property inspector for viewing and editing YAML frontmatter fields. Quick Open (Cmd+K) enables instant navigation across all ~9,200 vault files. This milestone is a key step toward [[2025-ship-laputa]] and was completed during [[25q1]].
## Goals
- Implement the four-panel layout with resizable panels and responsive behavior
- Build the property inspector panel for viewing and editing frontmatter properties
- Implement Quick Open (Cmd+K) with fuzzy search across all vault files
- Add type-aware sidebar navigation with collapsible sections and file counts
- Achieve sub-200ms navigation between notes (read file, parse frontmatter, render editor)
## Key decisions
- **CodeMirror 6 with reveal-on-focus editing.** The editor shows rendered markdown by default but reveals raw syntax when the cursor enters a block. This hybrid approach was chosen over a pure WYSIWYG editor (TipTap/ProseMirror) because power users need direct access to markdown syntax, especially for frontmatter and wiki-links. The reveal-on-focus model provides the best of both worlds.
- **Frontmatter parsing in Rust, rendering in React.** The Rust backend parses YAML frontmatter and provides structured data to the frontend via Tauri commands. This keeps the parsing fast and consistent, while React handles all presentation logic. The alternative — parsing in JavaScript — would be slower for bulk operations and harder to keep consistent with the Rust-side file operations.
- **Mock layer for browser development.** Built `src/mock-tauri.ts` to return realistic test data when running in a browser without the Tauri backend. This allows UI development and testing in Chrome without starting the full Rust backend, which dramatically accelerated the development loop.
## Notes
- The biggest engineering challenge was making the editor performant with large files. Some vault files are 5,000+ lines, and CodeMirror 6's default configuration struggled with these. Enabling viewport-based rendering and lazy decoration computation solved the performance issues.
- Quick Open search across 9,200 files needed careful optimization. The initial naive approach (filter on every keystroke) was too slow. Implemented a pre-built search index that loads at startup and supports fuzzy matching with sub-50ms response times.
- [[person-david-kim]] tested an early build and identified several UX issues with the property inspector — particularly around editing array-type frontmatter fields (like tags and aliases). His feedback led to a redesigned array editor with inline add/remove controls.
- The mock data layer proved its value immediately. It allowed running Playwright E2E tests and visual verification in Chrome without any Rust toolchain setup. This made CI testing feasible and significantly reduced the feedback loop for UI changes.
- V1 was usable but rough around the edges. The editor lacked many creature comforts (undo/redo state persistence, find-and-replace, syntax highlighting for YAML). These were deferred to [[25q2-laputa-v2]] to keep V1 scope manageable.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Newsletter SEO Sprint"]
Is A: Project
Belongs to: "[[25q1]]"
Advances: "[[responsibility-grow-newsletter]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Newsletter SEO Sprint
## Overview
Building on the pillar article strategy from [[24q2-10-pillar-articles]], this project was a concentrated 30-day sprint to boost organic search traffic and subscriber acquisition. The sprint had two components: writing 5 new high-traffic articles targeting keywords identified in an updated Ahrefs analysis, and overhauling the internal linking structure across all existing content to improve site-wide SEO authority.
The sprint ran through February 2025 and was timed to take advantage of the post-holiday traffic recovery in the engineering content space. By this point, the blog had about 40 published articles, but many lacked internal links and had suboptimal meta descriptions. The combination of new content and structural improvements was designed to create a compounding traffic effect that would support [[2025-reach-85k-subscribers]] through organic growth.
## Goals
- Write and publish 5 new SEO-optimized articles (2,500-4,000 words each)
- Audit and update internal links across all existing blog posts (target: every article links to at least 3 other articles)
- Update meta descriptions and title tags for the 20 highest-traffic existing articles
- Improve site-wide Core Web Vitals scores (target: all pages in "Good" range)
- Achieve a 30% increase in organic search traffic within 90 days of sprint completion
## Key decisions
- **Concentrated sprint, not ongoing drip.** Rather than publishing one SEO article per month, concentrated the effort into a 30-day window. This allowed full focus on SEO without the context-switching cost of alternating between newsletter writing and SEO writing. The trade-off was that the regular newsletter required extra preparation in advance to cover the sprint period.
- **Internal linking overhaul as a force multiplier.** The internal linking audit was actually higher-impact than the new articles. Many existing articles had zero internal links, meaning Google could not discover or weight them properly. Adding contextual links between related articles created a proper content topology that improved rankings site-wide.
- **Content refresh over new content for some keywords.** For 3 keywords where existing articles were already ranking on page 2-3, chose to refresh and expand those articles rather than write new ones. Refreshing existing URLs preserves accumulated link equity and is faster than starting from scratch.
## Notes
- The 30% organic traffic increase was achieved in about 75 days (ahead of the 90-day target). The internal linking overhaul was the primary driver — several existing articles jumped from page 2 to page 1 within weeks of gaining internal links.
- [[person-sara-ricci]] edited all 5 new articles and the refreshed content. The editing workload during the sprint was significant, and she flagged that future sprints should give her more lead time. Noted for next time.
- The article on "How to Run Effective Engineering All-Hands" became the strongest performer, reaching #3 on Google for its target keyword and driving ~1,500 monthly organic visits. This single article generated an estimated 200 new subscribers per month through its email capture CTA.
- Core Web Vitals optimization required some technical work — lazy loading images, reducing JavaScript bundle size, and optimizing font loading. These changes improved site speed across the board and had a modest positive effect on rankings.
- This sprint validated the content pillar strategy and demonstrated that concentrated SEO investment compounds over time. The articles written in [[24q2-10-pillar-articles]] continued to grow in traffic alongside the new content, suggesting that the internal linking network creates a rising-tide effect. Feeds into [[topic-newsletter-growth]] and [[topic-content-strategy]].

View File

@@ -1,36 +0,0 @@
---
aliases: ["Paid Newsletter Trial"]
Is A: Experiment
Belongs to: "[[25q1]]"
Status: Abandoned
Owner: "[[person-luca-rossi]]"
---
# Paid Newsletter Trial
Tested a Substack-style paid tier to evaluate whether the newsletter audience would convert to a direct subscription model. The motivation was to explore revenue diversification beyond [[measure-sponsorship-mrr]], which currently accounts for the majority of income. A paid tier could provide more predictable, audience-aligned revenue while reducing dependence on the sponsorship sales cycle.
## Hypothesis
Offering a paid tier at $10/month (or $100/year) with one exclusive deep-dive essay per month and early access to all posts would convert at least 2% of the free subscriber base within 8 weeks, generating $2,000+ MRR.
## Setup
- Built a paid tier on the existing newsletter platform with two perks: one exclusive monthly deep-dive and 48-hour early access to all free posts.
- Announced the paid tier via a dedicated newsletter issue, a pinned tweet, and a LinkedIn post.
- Ran a 2-week launch promotion at 50% off the annual plan.
- Tracked conversion rate, churn after first month, and qualitative feedback from subscribers who did and did not convert.
## Results
- Total paid subscribers after 8 weeks: 89 (approximately 0.2% of the free base, well below the 2% target).
- MRR generated: ~$620 (mix of monthly and discounted annual).
- Churn after first month: 18% of monthly subscribers cancelled.
- Qualitative feedback: most free subscribers said the free content was already "more than enough." Those who paid cited "supporting the creator" as the primary reason, not exclusive content.
## Takeaways
- The audience does not perceive enough differentiation between free and paid content to justify a subscription. The newsletter's value proposition is built on free, high-quality essays.
- Conversion was 10x below target. The paid tier would need a fundamentally different value prop (e.g., community, tools, templates) to work.
- Abandoned the paid tier to avoid splitting focus. The sponsorship model ([[responsibility-sponsorships]], [[24q1-launch-sponsorship-packages]]) delivers significantly higher revenue per hour of effort.
- If revisited, would explore a premium community model (see [[25q3-discord-community-soft]]) rather than paywalled content.
- Key lesson: do not assume that "more content" is a compelling paid offering. Access, community, and tools tend to convert better for this audience segment.

View File

@@ -1,37 +0,0 @@
---
aliases: ["Increase Sponsorship Rates Q2"]
Is A: Project
Belongs to: "[[25q1]]"
Advances: "[[responsibility-sponsorships]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Increase Sponsorship Rates Q2
## Overview
With the subscriber base approaching 60K and click-through performance data showing consistent improvement through 2024, the sponsorship rates set during [[24q1-launch-sponsorship-packages]] were underpriced relative to the audience value delivered. This project prepared and executed a 25% rate increase across all sponsorship tiers, effective for Q2 2025 bookings.
The rate increase required careful preparation: assembling performance data, updating the media kit, communicating the changes to existing sponsors, and adjusting outreach messaging for new prospects. The goal was to increase [[measure-sponsorship-mrr]] without losing existing sponsors. [[person-matteo-cellini]] led the communication with current sponsors while [[person-luca-rossi]] handled the strategic positioning and media kit updates.
## Goals
- Analyze per-sponsor performance data to build the justification case (CTR, impressions, conversions)
- Update the media kit with current audience size, demographics, and performance benchmarks
- Communicate the rate increase to all existing sponsors with 60 days advance notice
- Update pricing across all three tiers: Logo ($625), Spotlight ($1,500), Deep Dive ($3,125)
- Retain at least 80% of existing sponsors through the transition
## Key decisions
- **25% increase, not 15% or 30%.** Benchmarked against similar-sized engineering newsletters and found the current rates were 20-35% below market. A 25% increase brings pricing to market rate while leaving room for another adjustment in 12-18 months as the audience continues to grow.
- **Grandfathered pricing for 90 days.** Offered existing sponsors who renewed before April 1 the option to lock in the old rate for one additional quarter. This rewarded loyalty and created urgency for renewals, which improved cash flow predictability.
- **Performance data as the lead message.** Rather than leading with "rates are going up," led with "here is how your sponsorship performed." Sharing concrete performance data first made the rate increase feel like a natural consequence of value delivered, not an arbitrary price hike.
## Notes
- Sponsor retention through the increase was 88% — above the 80% target. The two sponsors who did not renew cited budget constraints (not value concerns), which is a healthy signal. They indicated interest in returning at a future date.
- [[person-matteo-cellini]] handled the sponsor communication calls and reported that the response was overwhelmingly positive. Several sponsors said the previous rates felt "too cheap" and appreciated the transparency about pricing methodology.
- The rate increase translated to approximately $3,200/month in additional MRR, which was a significant contribution to [[2025-reach-22k-mrr]]. This was the highest-leverage revenue action of Q1 — no new sponsors needed, just better pricing for existing value.
- The updated media kit now includes a "Performance Highlights" section with anonymized case studies showing sponsor CTR and conversion data. This has become one of the most effective tools in the outreach process.
- Key lesson: rate increases should be proactive and regular (annually), not reactive and rare. Waiting too long to adjust pricing means leaving significant revenue on the table. The discomfort of raising prices is always greater in anticipation than in reality.

View File

@@ -1,38 +0,0 @@
---
aliases: ["Newsletter Referral Program"]
Is A: Project
Belongs to: "[[25q1]]"
Advances: "[[responsibility-grow-newsletter]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Newsletter Referral Program
## Overview
Word-of-mouth had always been the newsletter's strongest organic growth channel, but it was entirely passive — readers shared when they felt like it, with no structured incentive. This project launched a formal referral program that rewards subscribers who bring in new readers, turning the most engaged audience members into active growth ambassadors. The program contributes to [[2025-reach-85k-subscribers]] and [[measure-subscribers]] growth.
The program was built using SparkLoop (a referral platform designed for newsletters) and integrated with the existing ConvertKit setup. Rewards are tiered: 3 referrals earns a digital resource pack, 10 referrals earns a private Q&A session, and 25 referrals earns a 1-year premium membership. The program launched in February 2025 and immediately became the second-largest subscriber acquisition channel after organic search.
## Goals
- Design a tiered reward structure that motivates sharing without creating perverse incentives (spam, fake signups)
- Set up SparkLoop integration with ConvertKit for automated tracking and reward fulfillment
- Create shareable assets: personalized referral links, social cards, and email forward templates
- Acquire 2,000+ new subscribers through the referral program in Q1
- Achieve a referral conversion rate of at least 20% (referred visitors who subscribe)
## Key decisions
- **SparkLoop, not a custom solution.** Evaluated building a custom referral system with unique links and a tracking database, but SparkLoop handles fraud detection, analytics, and reward automation out of the box. The $99/month cost is trivially justified by the subscriber value it generates.
- **Digital rewards, not physical.** Physical rewards (stickers, merchandise) create fulfillment complexity and shipping costs. Digital rewards (resource packs, access, sessions) scale infinitely and are more aligned with what the audience values. The private Q&A session at 10 referrals proved to be the most motivating tier.
- **Anti-fraud measures from day one.** Required referred subscribers to confirm via double opt-in and excluded signups from disposable email domains. This reduced fraudulent referrals to less than 2% of total, which is well below SparkLoop's industry average of 5-8%.
## Notes
- The referral program generated approximately 2,400 new subscribers in the first 8 weeks — above the 2,000 target. About 15% of the existing subscriber base activated their referral link, which is strong engagement for a referral program.
- The conversion rate on referred visitors was 28%, significantly above the 20% target. This makes sense: referred visitors arrive with a personal recommendation from someone they trust, which dramatically reduces the "should I subscribe?" friction.
- The top referrer brought in 47 new subscribers in the first month. Sent a personal thank-you email and offered an hour-long call. This kind of super-referrer engagement is worth cultivating.
- [[person-elena-rossi]] suggested adding a leaderboard showing top referrers (anonymized). Implemented this as a monthly "Referral Champions" section in the newsletter footer. It created a subtle competitive dynamic that boosted engagement.
- One unexpected finding: referred subscribers have higher 30-day retention (85%) compared to subscribers from other channels (~72% average). This suggests that referral-acquired subscribers are genuinely interested in the content, not just casually signing up.
- The program runs continuously and feeds into [[topic-newsletter-growth]]. The per-subscriber acquisition cost through referrals is approximately $0.40 (cost of SparkLoop + digital rewards amortized), compared to $2-3 for paid acquisition channels.

View File

@@ -1,38 +0,0 @@
---
aliases: ["New Strength Program"]
Is A: Project
Belongs to: "[[25q1]]"
Advances: "[[responsibility-health-fitness]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# New Strength Program
## Overview
The [[24q4-cycling-year-review]] identified a clear gap: upper body and core strength were limiters on long climbs, and pure cycling volume was not addressing them. This project introduced a structured 12-week strength training program to complement the cycling training plan, with the primary goal of improving climbing performance and reducing injury risk for the 2025 season — specifically for [[2025-ride-stelvio]].
The program followed a progressive overload model with three sessions per week (Monday, Wednesday, Friday mornings), focusing on core stability, hip strength, and upper body endurance. The sessions were designed to be cycling-compatible — meaning they build functional strength without adding excessive muscle mass that would penalize climbing performance. The program ran from January through March 2025.
## Goals
- Complete a 12-week strength program with at least 85% session adherence
- Focus areas: core stability (planks, dead bugs, pallof press), hip strength (single-leg squats, hip thrusts), upper body endurance (rows, push-ups, shoulder press)
- Sessions capped at 45 minutes to fit before the work day
- Improve core endurance test (plank hold) from 2:00 to 3:00+ minutes
- No negative impact on cycling performance metrics (FTP, weekly TSS)
## Key decisions
- **Gym-based, not home-based.** Considered a bodyweight-only home program for convenience, but decided that gym access (barbells, cable machines) allows for more precise progressive overload. Signed up at a gym 10 minutes from home with early morning hours (opens 6:30 AM).
- **Three sessions per week, not two or four.** Two sessions would not provide sufficient stimulus for meaningful adaptation. Four would risk interfering with cycling recovery, especially during build-phase weeks. Three sessions, alternating between upper/lower focus, hit the sweet spot.
- **No heavy squats or deadlifts.** Traditional powerlifting movements build too much leg mass for a cyclist targeting climbing performance. Instead, focused on single-leg movements and moderate-weight, higher-rep schemes that build strength endurance rather than maximal strength.
## Notes
- Session adherence was 89% (32 of 36 planned sessions). The four missed sessions were all during a particularly heavy newsletter week in February. Built in makeup sessions the following week when possible.
- The plank hold test improved from 2:00 to 3:25 — well above the 3:00 target. More importantly, subjective feedback from early-season rides confirmed that upper body fatigue during long climbs was significantly reduced.
- [[person-paco-furiani]] joined for several sessions and followed a modified version of the same program. Training together added accountability and made the early morning sessions more tolerable.
- The biggest adaptation was not physical but scheduling. Finding 45 minutes three mornings per week while also cycling 8-10 hours per week and running a content business required ruthless calendar management. The key was treating strength sessions as non-negotiable appointments.
- No measurable negative impact on cycling FTP (remained at ~275W throughout the program). This confirmed that the moderate-weight, endurance-focused approach was compatible with cycling performance.
- The program transitions into a maintenance phase (2 sessions/week) during the peak cycling season. Detailed in [[topic-cycling-training]] notes.

View File

@@ -1,32 +1,13 @@
---
aliases: ["Q1 2025"]
Is A: Quarter
Created at: "2025-01-01"
Belongs to: "[[2025]]"
Has: ["[[25q1-laputa-v1]]", "[[25q1-newsletter-seo-sprint]]", "[[25q1-strength-program]]", "[[25q1-rate-increase]]", "[[25q1-referral-program]]"]
Status: Done
type: Quarter
aliases:
- "[[Q1 2025]]"
status: Done
has:
- "[[25q1-laputa-v1]]"
---
# Q1 2025
## Focus
The first period where Laputa was usable for daily navigation, quick open, and inspector flows.
Q1 2025 was about building momentum for the scale year. The two big bets were shipping Laputa v1 as a usable internal tool and pushing a sponsor rate increase now that we had the audience and metrics to justify it. On the content side, an SEO sprint to capture organic search traffic. On the fitness side, starting a strength training program to complement cycling.
## Highlights
- Shipped [[25q1-laputa-v1]] — four-panel layout, markdown rendering, frontmatter-driven navigation, and a working search. Using it daily for my own vault
- Executed [[25q1-rate-increase]]: raised Gold sponsorship from EUR 2.8k to EUR 3.5k per issue — retained 90% of sponsors, [[measure-sponsorship-mrr]] jumped to EUR 14.2k
- Launched [[25q1-referral-program]] with tiered rewards — drove 3,200 referral subscribers in Q1
- [[25q1-newsletter-seo-sprint]]: optimized 15 pillar articles for search, added internal linking structure — organic traffic up 40% by March
- Started [[25q1-strength-program]]: 3x/week gym sessions focusing on core and legs — noticeable improvement in climbing power
- Newsletter reached 56k subscribers by end of March
- [[person-matteo-cellini]] onboarded two new enterprise sponsors at the higher rate with no pushback
- Podcast backlog strategy: released "best of Season 1 & 2" compilation episodes during recording break — maintained download numbers
## Reflections
The rate increase was the decision I was most nervous about. Raising prices always feels like you're testing whether the value you think you provide matches what the market believes. But [[person-matteo-cellini]] was right — we were underpriced. When sponsors didn't blink at the new rate, it was a clear signal we could have moved sooner. Lesson learned: if you're anxious about raising prices, you're probably already late.
Laputa v1 was a labor of love. Spending evenings and weekends building a tool that only I use might seem indulgent, but it's the most energized I've felt about a coding project in years. There's something deeply satisfying about building software for a problem you understand intimately. The vault has 9,000+ files now, and seeing them organized in my own four-panel UI — types, relationships, properties — feels like the PKM tool I've been waiting for.
The strength program was a humbling start. Years of cycling had given me good cardio but embarrassingly weak upper body strength. The first month was all DOMS and ego management. By March, though, I could feel the difference on the bike — more stable in the saddle, better power transfer on steep gradients. The [[2025-ride-stelvio]] goal feels more realistic with this foundation. Big quarter overall. [[2025]] is off to a strong start.

View File

@@ -1,38 +0,0 @@
---
aliases: ["Cycling Trip: Dolomites"]
Is A: Project
Belongs to: "[[25q2]]"
Advances: "[[responsibility-health-fitness]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Cycling Trip: Dolomites
## Overview
This was the centerpiece event of the 2025 cycling season — a 5-day cycling trip through the Dolomites with [[person-paco-furiani]], tackling two of cycling's most legendary climbs: the Stelvio and the Mortirolo. The trip totaled approximately 650km with over 12,000m of elevation gain, and it represented the culmination of the training work from [[25q1-strength-program]] and the 2025 season plan outlined in [[24q4-cycling-year-review]].
The trip was planned as both a personal challenge and a goal completion event for [[2025-ride-stelvio]]. Route planning, accommodation booking, and logistics were handled in April-May, with the ride itself taking place in the second half of June when the high-altitude passes are reliably open and the weather is most favorable. Each day featured one major climb plus connecting roads through the valleys.
## Goals
- Complete the Stelvio from Bormio (the hardest side: 21.5km, 1,533m elevation gain, avg 7.1% gradient)
- Complete the Mortirolo from Mazzo (12.4km, 1,300m elevation gain, avg 10.5% gradient)
- Ride 5 consecutive days with an average of 130km and 2,400m elevation per day
- Maintain a sustainable pace throughout — no bonking, no mechanical failures, no injuries
- Document the trip with photos and notes for the vault and a potential newsletter piece
## Key decisions
- **Stelvio from Bormio, not Prato.** The Bormio side is considered the hardest but also the most dramatic — 48 hairpin turns through alpine scenery. The Prato side is more famous (classic from the Giro d'Italia) but less challenging. Chose difficulty over fame.
- **Self-supported, not organized tour.** Rather than joining a commercial cycling tour, planned the trip independently. This allowed full control over pacing, rest stops, and daily distance. Carried minimal gear (jersey pockets + a small saddle bag) and relied on valley-town hotels rather than mountain huts.
- **Rest day strategy: active recovery on day 3.** Rather than a full rest day mid-trip, did a short flat recovery ride (50km, no significant climbing) on day 3. This kept the legs moving and prevented the stiffness that comes from a complete rest day mid-tour.
## Notes
- The Stelvio was everything expected and more. Completed the climb in 2:08, which was within the target window (sub 2:15). The final 5km above the tree line, with the glacier visible ahead, were genuinely transcendent. The strength training from [[25q1-strength-program]] made a noticeable difference — upper body and core held up far better than they would have a year ago.
- The Mortirolo was harder than the Stelvio in terms of pure suffering. The sustained 10%+ gradients for 12km with minimal respite are relentless. Finished but it required every ounce of mental discipline. [[person-paco-furiani]] later said it was the hardest thing he has done on a bike.
- Nutrition strategy (eating every 25 minutes, starting from the first pedal stroke) worked perfectly. No bonking episodes across all 5 days. This was a direct application of the lesson from [[24q2-spring-gran-fondo]].
- Day 4 featured unexpected rain on the descent from Passo Gavia, which made the wet hairpins genuinely nerve-wracking. Descended very conservatively (brake check every turn) and lost about 40 minutes compared to the planned schedule. Safety over speed — always.
- The trip generated a newsletter issue ("What Climbing the Stelvio Taught Me About Endurance") that became one of the most engaged pieces of the year. The personal story format resonated strongly with readers, even those who do not cycle.
- [[measure-cycling-km-per-month]] for June peaked at 1,450km — the highest monthly total ever. The Dolomites trip alone contributed 650km of that.

View File

@@ -1,38 +1,19 @@
---
aliases: ["Laputa App V2"]
Is A: Project
Belongs to: "[[25q2]]"
Advances: "[[responsibility-learning]]"
Status: Done
Owner: "[[person-luca-rossi]]"
type: Project
aliases:
- "[[Laputa App V2]]"
belongs_to: "[[25q2]]"
owner: "[[person-luca-rossi]]"
status: Active
related_to:
- "[[laputa-qa-reference]]"
---
# Laputa App V2
## Overview
The active polish project used for current QA, especially richer editing, keyboard navigation, and attachment rendering.
V2 of Laputa was a major iteration that addressed the rough edges from [[25q1-laputa-v1]] and introduced three headline features: a BlockNote-based rich text editor replacing the raw CodeMirror setup, wiki-link autocomplete for seamless navigation between notes, and a completely redesigned theme system with CSS custom properties. These changes transformed the app from a functional prototype into something that feels genuinely pleasant to use daily.
- Tightened the editor interaction model.
- Reduced friction in wikilink navigation.
- Uses [[laputa-qa-reference]] as a lightweight visual reference note.
The BlockNote integration was the most significant architectural change — it provides a block-based editing experience similar to Notion while preserving the underlying markdown format. Wiki-link autocomplete (triggered by typing `[[`) searches across all vault files and inserts links with proper display names. The theme system uses design tokens defined as CSS custom properties, making it straightforward to adjust the visual style without touching component code. This milestone continues the path toward [[2025-ship-laputa]].
## Goals
- Replace the basic CodeMirror editor with BlockNote for a block-based editing experience
- Implement wiki-link autocomplete with fuzzy search across all vault files
- Redesign the theme system using CSS custom properties and a centralized token file
- Improve the property inspector with inline editing for all frontmatter field types
- Fix the top 10 UX issues reported during V1 daily use
## Key decisions
- **BlockNote over plain CodeMirror.** While CodeMirror 6 is excellent for code editing, the reveal-on-focus model proved too jarring for prose-heavy content. BlockNote provides a smoother editing experience where formatting is always visible and blocks can be manipulated as units. The trade-off is less control over the raw markdown, but for the primary use case (structured notes with frontmatter), this is acceptable.
- **CSS custom properties, not a CSS-in-JS theme.** Evaluated styled-components and Tailwind CSS theme approaches but chose plain CSS custom properties. They are natively supported, have zero runtime cost, can be inspected in browser DevTools, and align with the project philosophy of preferring simple, standard approaches over framework-dependent abstractions.
- **Wiki-link search index prebuilt at startup.** The autocomplete needs to search across all ~9,200 files in sub-50ms. Rather than querying the filesystem on each keystroke, the Rust backend builds a search index at startup (including file names, aliases, and titles) and the frontend queries this index. Incremental updates happen when files change.
## Notes
- The BlockNote integration took longer than expected — about 3 weeks of focused work. The main challenge was ensuring that markdown round-tripping (markdown to BlockNote blocks to markdown) preserved formatting perfectly. Several edge cases with nested lists and code blocks required custom serializers.
- Wiki-link autocomplete was the most impactful feature from a daily use perspective. Being able to type `[[` and instantly find any note in the vault by name, alias, or title makes the linking experience far superior to Obsidian's implementation (which does not search aliases by default).
- [[person-david-kim]] continued to provide UX feedback, particularly around keyboard navigation patterns. His suggestion to add Vim-style `j/k` navigation in the note list was implemented and feels natural for power users.
- The theme redesign consolidated about 200 scattered color values into 45 semantic design tokens. This made it trivial to create a dark mode variant (completed in a single afternoon) and establishes a foundation for future theme customization.
- Performance remained solid despite the architectural changes. Note switching is still sub-200ms, and the BlockNote editor handles files up to 5,000 lines without noticeable lag. Memory usage increased by about 15% compared to V1 due to BlockNote's richer DOM, which is acceptable.
- The V2 release was the point where Laputa became genuinely preferable to Obsidian for daily vault management. Feeds into [[25q4-laputa-v3]] for the next major milestone.

View File

@@ -1,38 +0,0 @@
---
aliases: ["Podcast Season 3"]
Is A: Project
Belongs to: "[[25q2]]"
Advances: "[[responsibility-podcast]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Podcast Season 3
## Overview
Season 3 expanded the Refactoring podcast to 10 episodes with a thematic focus on building in public, founder journeys, and product-led growth. After two seasons focused on engineering leadership and org design, this season shifted toward the entrepreneurial side of the audience — developers who are building products, running side projects, or considering the leap to full-time founder. The theme resonated strongly, producing the highest average downloads per episode of any season so far.
Production was now a well-oiled operation: [[person-sara-ricci]] handled post-production, [[person-paco-furiani]] managed guest scheduling and logistics, and [[person-luca-rossi]] focused purely on guest selection, interview preparation, and hosting. The workflows from [[procedure-podcast-recording]] and [[procedure-podcast-editing]] continued to function with only minor updates.
## Goals
- Record and publish 10 episodes on building in public, founder journeys, and product-led growth
- Secure at least 3 guests who are active indie hackers or solopreneurs with public revenue data
- Grow average episode downloads to 3,000 (up from Season 2's ~2,100)
- Introduce YouTube video for 3 select episodes as an experiment
- Generate at least 5 newsletter issues from podcast conversations (repurposed content)
## Key decisions
- **10 episodes, up from 8.** The seasonal model allows for flexibility in episode count. 10 episodes fit cleanly into the Q2 window (April-June) with a weekly release schedule. The extra two episodes justified by the stronger guest pipeline and revenue from podcast sponsorships.
- **Selective video for 3 episodes.** Rather than doing video for all episodes (too much production overhead) or none (missed YouTube opportunity), selected 3 episodes with the most visually engaging guests for video. This tests the YouTube channel without committing to full video production.
- **Founder-focused theme, not pure engineering.** The shift toward entrepreneurial topics was a deliberate audience expansion move. Refactoring's readership increasingly includes developers who are also founders or aspire to be. This theme serves them while maintaining technical credibility through the "building" angle.
## Notes
- Average downloads per episode reached 3,400, significantly exceeding the 3,000 target. The standout episode was a conversation with an indie hacker who publicly shared their journey from $0 to $25K MRR, which was shared widely on Twitter/X and LinkedIn.
- The 3 video episodes performed well on YouTube (~4,000-6,000 views each), which is a promising signal for a channel with no prior content. The production cost was manageable because Riverside.fm captures both audio and video in the same recording session.
- [[person-david-kim]] appeared as a guest for an episode on developer tool startups. His insights on distribution in the developer tools market were among the most practical advice shared on the show.
- Content repurposing worked exceptionally well this season. 6 newsletter issues drew directly from podcast conversations (exceeding the 5-issue target), and the cross-promotion between newsletter and podcast continued to be the primary growth engine for [[measure-podcast-downloads]].
- Podcast sponsorship revenue reached $2,500/month during the season, representing meaningful incremental revenue on top of newsletter sponsorships. The combined sponsorship revenue contributes significantly to [[2025-reach-22k-mrr]].
- Season 3 was the first season where the podcast felt like it was pulling its own weight as a growth channel, not just a newsletter supplement. This has implications for resource allocation in [[25q3-podcast-season-4]].

View File

@@ -1,38 +0,0 @@
---
aliases: ["Reach 70k Subscribers"]
Is A: Project
Belongs to: "[[25q2]]"
Advances: "[[responsibility-grow-newsletter]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Reach 70k Subscribers
## Overview
This project was a focused growth sprint to push [[measure-subscribers]] from approximately 60K to 70K during [[25q2]]. The strategy combined two approaches: amplifying the referral program launched in [[25q1-referral-program]] with targeted campaigns, and establishing cross-promotion partnerships with 3 complementary newsletters in adjacent spaces (startup operations, product management, and design leadership).
The 70K milestone was significant both psychologically (a round number that signals scale) and commercially (larger audience commands higher sponsorship rates and opens doors to enterprise-tier sponsors). The sprint ran from April through June and involved weekly tracking against a linear growth trajectory to catch underperformance early.
## Goals
- Grow from ~60K to 70K subscribers by end of June 2025
- Drive 4,000+ subscribers through the referral program (with targeted campaigns and incentive boosts)
- Establish 3 newsletter cross-promotion partnerships with reciprocal recommendation swaps
- Maintain subscriber quality: 30-day retention rate above 75% for new subscribers
- Achieve the milestone while keeping cost-per-acquisition below $1.00
## Key decisions
- **Cross-promotion over paid acquisition.** Considered investing in paid newsletter ads (Sparkloop's partner network, Twitter ads), but the economics favored cross-promotion. A reciprocal recommendation swap with a complementary newsletter costs nothing and generates high-quality subscribers who are already newsletter readers. Paid acquisition was kept as a backup if organic channels underperformed.
- **Three partners, carefully selected.** Rather than doing many low-quality cross-promotions, invested time in selecting three newsletters where the audience overlap is genuine but not complete. The partners were a startup operations newsletter (~40K subscribers), a product management newsletter (~55K subscribers), and a design leadership newsletter (~30K subscribers). Each introduces Refactoring to a slightly different but adjacent audience.
- **Weekly tracking cadence.** Set up a weekly check against a linear growth trajectory (~2,500 new subscribers/month needed). This allowed early detection of underperformance and mid-course corrections. In week 3, growth was tracking below target, which led to an additional referral incentive push that course-corrected by week 5.
## Notes
- Final subscriber count at end of June: 71,200 — slightly above the 70K target. The growth distribution was approximately: cross-promotions (3,800), referral program (3,500), organic search (2,200), social media (800), other (900).
- Cross-promotion partnerships were the highest-quality growth channel. Subscribers from cross-promotions had a 30-day retention rate of 82%, the highest of any acquisition channel. This makes sense — they were already engaged newsletter readers, just not yet aware of Refactoring.
- The referral program saw a significant boost from a "Spring Referral Challenge" campaign run in May, where the rewards were temporarily enhanced (extra rewards at the 5-referral tier). This generated a spike of ~1,200 new subscribers in a single week.
- [[person-elena-rossi]] helped design the cross-promotion creative (email copy and graphics) for the partner newsletters. Her marketing background was valuable for crafting the recommendation copy that appeared in partner newsletters.
- Cost-per-acquisition across all channels was $0.62, well below the $1.00 target. The primary costs were SparkLoop subscription, referral rewards, and the time spent on partnership management.
- The 70K milestone was used in the updated media kit and referenced in sponsorship conversations, directly supporting the rate increases and new sponsor acquisition feeding into [[2025-reach-22k-mrr]].

View File

@@ -1,37 +0,0 @@
---
aliases: ["Team Retreat Milan"]
Is A: Project
Belongs to: "[[25q2]]"
Advances: "[[responsibility-team-management]]"
Status: Done
Owner: "[[person-luca-rossi]]"
---
# Team Retreat Milan
## Overview
After more than a year of working together remotely, the Refactoring team gathered for its first in-person retreat in Milan. The retreat brought together [[person-luca-rossi]], [[person-matteo-cellini]], [[person-paco-furiani]], and [[person-sara-ricci]] for two days of strategy sessions, workshops, and team bonding. The purpose was to align on the business direction for the second half of 2025, address operational friction points, and build the personal relationships that make remote collaboration smoother.
The retreat was planned for early May, timed to fall between Season 3 podcast production and the summer growth push. The agenda was deliberately balanced between structured work sessions (morning) and unstructured social time (afternoon/evening). Milan was chosen for its central location relative to all team members.
## Goals
- Conduct a half-day business strategy session covering H2 2025 priorities and goals
- Run a workflow retrospective: identify and resolve the top 3 operational friction points
- Host a creative workshop on content format experimentation for H2
- Provide space for unstructured team bonding (dinner, activities)
- Document decisions and action items for follow-up within 1 week
## Key decisions
- **Two days, not three.** Considered a 3-day retreat but decided that two full days (plus a team dinner the evening before) provided enough time for meaningful work without pulling everyone away from their schedules for too long. Especially for a small team where everyone wears multiple hats, minimizing away-from-work time matters.
- **Strategy session first, social second.** Ran the strategy and retrospective sessions on the first day (when energy and focus are highest) and the creative workshop + social activities on the second day. This ensured the most important decisions got the best thinking.
- **External facilitator for the retrospective.** Rather than running the retrospective internally, brought in [[person-giulia-conti]] (who has experience facilitating team sessions) to moderate. Having an external voice made it easier for team members to surface honest feedback without the founder-employee dynamic filtering the conversation.
## Notes
- The retrospective produced the most actionable outcomes. The top 3 friction points identified were: (1) newsletter draft handoff timing (Sara needs more lead time), (2) sponsor pipeline visibility (Matteo wants to see content calendar earlier for pitch alignment), and (3) podcast scheduling conflicts (Paco needs more advance notice for guest confirmations). All three had clear action items assigned.
- The strategy session confirmed H2 priorities: community launch ([[25q3-community-launch]]), continued Laputa development ([[25q4-laputa-v3]]), and the push toward [[2025-reach-85k-subscribers]]. There was a genuine debate about whether to invest in a premium podcast (higher production value, video-first) versus the community. Decided on community as the higher-leverage bet.
- The creative workshop generated several content format ideas for H2, including a "Refactoring Case Studies" series (deep dives into real company engineering decisions) and a "Founder Metrics" segment sharing real revenue/growth numbers. Both were added to the content roadmap.
- Team dinner was at a classic Milanese trattoria near the Navigli. The informal conversation over dinner revealed a lot about individual motivations and career aspirations that would never surface in a video call. [[person-matteo-cellini]] mentioned wanting to eventually run his own content business, which was valuable context for thinking about his long-term role.
- Total cost: approximately 2,800 EUR (hotel, meals, workspace rental, facilitator). Very reasonable for the alignment value produced. Planning to make this a biannual event (spring and autumn).

View File

@@ -1,32 +1,13 @@
---
aliases: ["Q2 2025"]
Is A: Quarter
Created at: "2025-04-01"
Belongs to: "[[2025]]"
Has: ["[[25q2-reach-70k]]", "[[25q2-podcast-season-3]]", "[[25q2-team-retreat]]", "[[25q2-laputa-v2]]", "[[25q2-dolomites-trip]]"]
Status: Done
type: Quarter
aliases:
- "[[Q2 2025]]"
status: Active
has:
- "[[25q2-laputa-v2]]"
---
# Q2 2025
## Focus
The polish cycle focused on richer editing, faster linking, and better keyboard QA.
Q2 was the heart of the execution phase for 2025. The 70k subscriber milestone was the key growth target. Season 3 of the podcast brought a new format — shorter episodes, more actionable. The team retreat in Tuscany was about alignment and bonding. And the Dolomites cycling trip was both training for Stelvio and pure joy.
## Highlights
- Hit [[25q2-reach-70k]] subscribers in early June — two weeks ahead of schedule, driven by viral LinkedIn posts and the referral program compounding
- Launched [[25q2-podcast-season-3]] with a new 15-minute "Tactical Tuesday" format alongside the regular deep dives — downloads surged to 14k/month
- Held [[25q2-team-retreat]] in a farmhouse outside Siena with [[person-matteo-cellini]], [[person-paco-furiani]], and [[person-sara-ricci]] — aligned on H2 roadmap, built real trust
- Shipped [[25q2-laputa-v2]] with bidirectional linking, graph view, and improved frontmatter editing — starting to feel like a real product
- Completed [[25q2-dolomites-trip]]: 4 days, 5 passes, 380 km — Passo Fedaia, Pordoi, Sella, Gardena, Campolongo
- [[measure-sponsorship-mrr]] hit EUR 17.5k — tracking well toward [[2025-reach-22k-mrr]]
- Premium tier reached 680 paying subscribers, up from 420 at start of quarter
- Two pillar articles syndicated by major tech publications, driving ~4k new subscribers
## Reflections
The team retreat was the highlight of the quarter, and maybe the most important thing we did all year. Working remotely with contractors is efficient, but you miss the informal conversations that build real relationships. Three days in Tuscany — cooking together, long walks, whiteboard sessions on the terrace — transformed how we collaborate. [[person-sara-ricci]] pitched the community idea over dinner on the second night, and it was so obvious in hindsight that we all wondered why we hadn't done it sooner. That became [[25q3-community-launch]].
Reaching 70k subscribers felt almost routine, which is itself remarkable. A year ago, every thousand felt hard-won. Now the flywheel is spinning: content drives subscribers, subscribers attract sponsors, sponsors fund better content. The referral program is the turbocharger — word-of-mouth from engineering leaders who trust their peers' recommendations.
The Dolomites trip was pure magic. Four days of riding through the most beautiful mountain landscape in Europe with three friends. Passo Fedaia at sunset, the Marmolada glacier glowing overhead — these are the moments that make the 5am training rides worth it. My legs are ready for [[2025-ride-stelvio]] in August. Bring it on.

View File

@@ -1,38 +0,0 @@
---
aliases: ["Launch Refactoring Community"]
Is A: Project
Belongs to: "[[25q3]]"
Advances: "[[responsibility-grow-newsletter]]"
Status: Open
Owner: "[[person-luca-rossi]]"
---
# Launch Refactoring Community
## Overview
This project is building a private Discord community for Refactoring's most engaged readers — a space where engineering leaders, indie hackers, and technical founders can connect, share challenges, and learn from each other beyond the one-way format of the newsletter and podcast. The community concept was validated during the [[25q2-team-retreat]] strategy session and represents the most significant new product surface since the podcast launch.
The approach is a soft launch with a small, curated initial cohort rather than opening the doors to all subscribers at once. The [[25q3-discord-community-soft]] experiment is testing engagement patterns, channel structure, and moderation needs with approximately 150 founding members before a broader rollout. The community is free for now, with potential for a paid tier once the value proposition is validated. This is a fundamentally different approach from the failed [[24q3-premium-tier]] — the focus is on community value, not content gating.
## Goals
- Set up the Discord server with initial channel structure (introductions, weekly discussions, resource sharing, career advice, project showcases)
- Recruit 150 founding members from the most engaged newsletter subscribers (based on open rate, click-through, and referral data)
- Establish community guidelines, moderation policies, and a code of conduct
- Host 2 live events during the soft launch period (AMA sessions with podcast guests)
- Achieve a weekly active member rate of at least 40% during the soft launch
## Key decisions
- **Discord, not Circle or Slack.** Evaluated all three platforms. Discord offers the best combination of real-time and async communication, has robust moderation tools, and most of the target audience already has accounts. Circle has a cleaner UX but lacks real-time chat. Slack is too work-associated — people do not want another Slack workspace.
- **Soft launch with curated cohort, not open launch.** The lesson from [[24q3-premium-tier]] was that launching to everyone at once creates a spike-and-fade pattern. A soft launch with hand-selected engaged members builds genuine activity and social proof before scaling. The founding members become community champions.
- **Free entry, no paywall yet.** The community needs to prove its value before asking members to pay. Charging too early kills participation momentum. Will evaluate a paid tier once weekly active member rate stabilizes above 40% and there is clear demand for premium features (private channels, exclusive events).
## Notes
- The soft launch began in late July with 150 invitations sent to subscribers identified through engagement scoring. Acceptance rate was 72% (108 active members), which is strong for a community invitation.
- Early engagement is promising but uneven. The "weekly discussion" channel (a prompted discussion thread posted every Monday) generates the most activity. Organic conversation in open channels is still sparse — this is the typical early-community challenge.
- The two AMA sessions (with guests from [[25q2-podcast-season-3]]) were well-attended (~60 members each) and generated follow-up discussions that lasted several days. Live events appear to be the strongest engagement driver.
- [[person-paco-furiani]] is handling day-to-day community moderation. His operational experience and even temperament make him well-suited for this role. The time commitment is currently about 3 hours/week.
- The biggest risk is that the community becomes another content channel (one-to-many) rather than a genuine peer space (many-to-many). Actively encouraging member-initiated discussions and resisting the urge to "fill the silence" with content is important.
- This project feeds into the broader question of whether the community becomes a revenue stream or remains a free engagement tool. Will evaluate after the soft launch period ends in [[25q4]].

View File

@@ -1,38 +0,0 @@
---
aliases: ["Discord Community Soft Launch"]
Is A: Experiment
Belongs to: "[[25q3]]"
Status: Open
Owner: "[[person-luca-rossi]]"
---
# Discord Community Soft Launch
Soft-launched a Discord community for the Refactoring newsletter with an initial cohort of 50 beta subscribers. The goal is to validate whether a private community can drive meaningful engagement, reduce churn, and eventually become a monetizable product tied to [[25q3-community-launch]]. This experiment directly informs whether community is the right next revenue line beyond sponsorships.
## Hypothesis
A curated Discord community with 50 hand-picked beta members will achieve at least 40% weekly active participation (defined as posting or reacting at least once per week) over the first 8 weeks, and at least 70% of members will express willingness to pay $20/month for continued access.
## Setup
- Invited 50 beta members from the most engaged newsletter subscribers (high open rate, frequent replies, active on social).
- Created a minimal channel structure: #introductions, #general, #content-ideas, #career-advice, #weekly-thread.
- [[person-luca-rossi]] posts a weekly discussion prompt every Monday tied to that week's newsletter topic.
- [[person-paco-furiani]] moderates and tracks engagement metrics.
- Running a survey at week 4 and week 8 to gauge willingness to pay and feature requests.
## Results
- Currently in progress. Early signals after 3 weeks:
- Weekly active participation: ~52% (above the 40% target so far).
- Most active channels: #career-advice and #general. #content-ideas is quieter than expected.
- Three members have organically started peer accountability threads (unplanned, positive signal).
- No dropoffs yet, though engagement tends to decay after the novelty period.
## Takeaways
- Early results are encouraging but it is too soon to draw conclusions. The real test is whether engagement sustains past week 6.
- The #career-advice channel suggests the community's core value may be peer networking rather than content discussion. Worth leaning into.
- If the willingness-to-pay survey at week 8 confirms demand, this becomes the foundation for [[25q3-community-launch]] and a potential path to the [[2025-reach-22k-mrr]] goal.
- Lessons from the [[25q1-paid-newsletter-trial]] apply here: the value proposition must be access and connection, not more content.
- Key risk: community management overhead. Need to assess whether [[person-paco-furiani]] can handle moderation long-term or if additional support is needed from [[responsibility-team-management]].

Some files were not shown because too many files have changed in this diff Show More