A startup enabling researchers to design and run protein folding simulations without requiring coding expertise. A node based editor in Vue and TypeScript wires up inputs, algorithms, outputs and plots, and the app writes the Julia that runs them
03
to
Core maintainerat GNU social
The v3 rewrite of the federated social network, in PHP. An event driven modular architecture, low system requirements, and a deployment a sysadmin can run without much time for it
Brought an acquired ride sharing platform's PHP backend up to date and its iOS and Android apps onto current versions, then built a driver and vehicle management platform with a React Native app
01
to
Table service and catering
Summer jobs waiting tables and working catering
学校Education
BSc Computer ScienceMinor in MathematicsFaculdade de Ciências, Universidade do Porto, Coding since age 13, always learning
ことばLanguages
PortugueseNative
EnglishC2
SpanishC1
JapaneseA1
ばしょLocation
Porto, Portugal
すきなことInterests
Open source, Rubik's cubes, languages, motorcycles, the gym, cooking and bread
Draw a rectangle on the floor and it fills with chairs. Sliders set the spacing,
the aisles and the seat count.
Loading Space Builder…
Space Builder · Add Tool
Drawing No.1 / 5
Scale1:1
Weight38.1KiB
Date
Projection
Loading SelectArea…
Place Area
Space Builder · Add Tool
Drawing No.2 / 5
Scale1:1
Weight38.1KiB
Date
Projection
Loading Options…
Edit Parameters
Space Builder · Add Tool
Drawing No.3 / 5
Scale1:1
Weight38.1KiB
Date
Projection
Loading layouts…
Layout Styles
Space Builder · Add Tool
Drawing No.4 / 5
Scale1:1
Weight38.1KiB
Date
Projection
Loading badge…
Tag offset
φ is the camera's angle relative to the area; r is the distance from center to the area's
silhouette edge at that angle.
Capacity Badge
Space Builder · Add Tool
Drawing No.5 / 5
Scale1:1
Weight38.1KiB
Date
Projection
thereismoreunderneath,turnthepage
thiscornerdragsthepageback
BDetail BScale 1:1 マーカーをつくる Marker editor
Title
Interactive Map Marker Editor
Medium
React, Leaflet, Redux
Year
Notes
A pin editor for venue maps. Shape, border, fill and decoration are separate
parts, and a saved pin rebuilds itself from them.
Interactive Map Marker Editor
Drawing No.1 / 6
Scale1:1
Weight54.7KiB
Date
Projection
Lobby
Cafe
Plaza
Marker Selector
Interactive Map Marker Editor
Drawing No.2 / 6
Scale1:1
Weight54.7KiB
Date
Projection
ReactDragging outside the render loop
The whole interactive map is a React app, so the editor started as a regular component.
That held until I dragged a control point: every pointermove re-rendered a
large subtree (solve constraints, preview SVG, update indicators, update sidebar) and the
frame rate fell apart.
On drag, the handlers now mutate MarkerPart.controlPoints directly, then:
Circles and crosses move by CSS translate through DOM refs.
Each editor step is a class with its own imperativeReactComponent, a
createRoot plus an .update() method, so only the part that
changed is redrawn.
Widgets that still need React (colours, text, icons) read an external
reactiveState through useSyncExternalStore, so React re-renders
only once that store is notified.
Circles: the handle you drag
Crosses: where the constraint put it
Sidebar: the parts a marker is made of
Marker Editor
Interactive Map Marker Editor
Drawing No.3 / 6
Scale1:1
Weight54.7KiB
Date
Projection
Preview chromeCrosshair background
Failed
Overlapping linear-gradients
H + V stripe layers stack into a grid, but each axis rounds its stops alone, so the
columns drift.
Shipped
SVG tile · repeat × 17
One tiny SVG draws a centered + from two short strokes in a
viewBox="-1 -1 2 2". background-repeat tiles that cell so
every cross stays coherent.
H stripes
Rows, no columns
+V stripes
Columns, no rows
→Overlap = grid
Crosses where lines meet
SVG cell
Short +, then tiled
Preview Background
Interactive Map Marker Editor
Drawing No.4 / 6
Scale1:1
Weight54.7KiB
Date
Projection
MarkerPartComposable stack
Shape Border Fill Decoration Composed
+
+
+
→
Serialization
Save writes typed data-* attrs (Point: /
Number: / String:). Load rebuilds the parts.
One shared group id, so Ctrl+Z or Cmd+Z undoes the whole batch.
Undoable Store
Interactive Map Marker Editor
Drawing No.6 / 6
Scale1:1
Weight54.7KiB
Date
Projection
flipthecornerforthenextsheet
thiscornerdragsthepageback
CDetail CScale 1:1 ドラッグ&ドロップ Drag and drop
Title
Space Builder · Drag and Drop
Medium
Vue, Three.js
Year
Notes
Drag a card off the catalogue and the object lands where it is dropped. Double-click
one instead and the next click on the floor places it.
Loading catalog…
Space Builder · Drag & Drop
Drawing No.1 / 2
Scale1:1
Weight38.1KiB
Date
Projection
Picture over the catalog, model over the floor
Four moments of one drag: the card's picture follows the pointer, the model takes
over the moment the pointer crosses onto the floor, and letting go places it.
drag the object to the viewportover the catalog it is still a pictureover the floor it becomes the modellet go and it is placed
Picture to Model
Space Builder · Drag & Drop
Drawing No.2 / 2
Scale1:1
Weight38.1KiB
Date
Projection
onemoresheets,peelthiscorner
thiscornerdragsthepageback
DDetail DScale 1:1 フォントをえらぶ Font picker
Title
Interactive Map Font Picker
Medium
React
Year
Notes
The list of faces comes from document.fonts, each face's Unicode subsets folded
back into one entry. Nothing in it is hardcoded, so a font that loads later shows
up on its own.
Loading the font picker…
Interactive Map Font Picker
Drawing No.1 / 4
Scale1:1
Weight32.2KiB
Date
Projection
Type a URL
Embed URLrust-lang.org
A page the visitor names in the Embed URL subform
Fetch the page
iframex-frame-options: deny/api/font-proxyone origin, so the HTML can be read
rust-lang.org refuses to be framed, so a worker on this origin fetches the HTML
The browser follows every link and @import. It makes each
src absolute and routes it through the proxy, since webfont loads are
CORS-checked too
newFontFace().load() document.fonts.add()
Show the new face
Poppins 400Alfa Slab One 400
The dropdown re-reads document.fonts on loadingdone. The dot
marks what the last source brought
A Google font skips the first two steps. fonts.googleapis.com/css2?family=Lobster
returns the same kind of stylesheet, which joins the flow here
Font Extraction
Interactive Map Font Picker
Drawing No.2 / 4
Scale1:1
Weight32.2KiB
Date
Projection
The field's own border reports the fetch. A quarter turn orbits while the request is
out, then the ring closes from wherever the orbit had got to, green for a font that
exists and red for one that does not
cubic-bezier(0.75,0.25,0.2,0.2) gives the orbit a push and lets it
settle. Kept for every turn it stutters at each restart, so at 750ms the script swaps
the timing function for linear at the point the curve has already
reached. The seam goes, the first turn keeps its kick
One element, two backgrounds. The gradient is painted on the border box and the
field's own colour covers the padding box over it, so the only part left showing is
the border. Three custom properties move it: where the arc starts, how much of the
turn is coloured, and the colour. @property types them as angles and a
colour, which is what makes them animatable at all
Loading Indicator
Interactive Map Font Picker
Drawing No.3 / 4
Scale1:1
Weight32.2KiB
Date
Projection
Font Size sets the column's own text size. Drag it and the row grows, so the slider walks
out from under the pointer. FixedElement pins the row where the drag started. Both columns
get the same drag, with the pointer held in one place
Loading the two columns…
Held Controls
Interactive Map Font Picker
Drawing No.4 / 4
Scale1:1
Weight32.2KiB
Date
Projection
threemoresheets,peelthiscorner
thiscornerdragsthepageback
EDetail EScale 3:1 読みこみ中のロゴ Loading logo
Title
Visrez Animated Loading Logo
Medium
SVG, CSS animation
Year
Notes
The Illustrator export became one path and a cube, animated by hand. The sheets
after it show each step.
Visrez Animated Loading Logo
Drawing No.1 / 6
Scale3:1
Weight1.7KiB
Date
Projection
Original Logo
Visrez Animated Loading Logo
Drawing No.2 / 6
Scale3:1
Weight7.5KiB
Date
Projection
Cube :)
Visrez Animated Loading Logo
Drawing No.3 / 6
Scale3:1
Weight1.7KiB
Date
Projection
Authored Path
Visrez Animated Loading Logo
Drawing No.4 / 6
Scale3:1
Weight1.7KiB
Date
Projection
<svgxmlns="http://www.w3.org/2000/svg"viewBox="-1 -1 2 2"-- Define the coordinate system to go-- from (-1, -1) with a width of (2, 2)><gstyle="scale: 0.707;">-- Scale the overall shape down. Because-- we move up (and down) by 2 * 0.707,-- the overall height becomes 4 * 0.707 = 2.828,-- so we scale down by 0.707 to get a height of 2<pathd="M0.6,-0.42-- Move to 60% of (1, -0.707)L0,0-- Draw a line to (0, 0)l-1,-0.707-- Draw a relative line by moving (-1, -0.707)l1,-0.707-- Draw a relative line by moving ( 1, -0.707)l1,0.707-- Draw a relative line by moving ( 1, 0.707)v1-- Draw a vertical line by moving (0, 1)L0,1-- Draw a line by moving to (0, 1)l-1,-0.707-- Draw a relative line by moving (-1, -0.707)v-0.2"-- Draw a vertical line by moving (0, -0.2)pathLength="1"-- Tell the renderer to treat the length-- of the path as being 1/></g></svg>
Path Data
Visrez Animated Loading Logo
Drawing No.5 / 6
Scale3:1
Weight1.7KiB
Date
Projection
Putting it all together
Visrez Animated Loading Logo
Drawing No.6 / 6
Scale3:1
Weight1.7KiB
Date
Projection
fivemoresheets,peelthiscorner
thiscornerdragsthepageback
FDetail FScale 1:1 テーマをえらぶ Theme picker
Title
Theme Picker
Medium
Astro, CSS, View Transitions
Year
Notes
The control in the corner that picks one of this page's four themes. A pick grows
the icon it was picked with from the corner until it covers the page, and the new
page is cut out of the old along it. The first sheet plays it on a sample page with its
own theme. The sheets behind it draw the wipe, its code, the tokens and who decides.
LightDarkArcticForest
stagedata-demo-theme="light""dark""arctic""dark-forest"unset, so the OS scheme decides
Theme Picker
Drawing No.1 / 5
Scale1:1
Weight6.5KiB
Date
Projection
The visible icon drives the animation by creating a starting point from it's placement and an end point with that same icon sized to cover the whole viewport
123
× 2.4
× 1.2
× 2
× 1.4
1First frame
The checked icon's <path>, mapped from its viewBox onto its box in the corner.
2Last frame
The same path, grown to a square the viewport's diagonal × the theme's factor wide.
3Between
clip-path from the one to the other, 500 ms ease-in, over a picture of the old page. Off under reduced motion.
The Wipe
Theme Picker
Drawing No.2 / 5
Scale1:1
Weight6.5KiB
Date
Projection
The wipe's script in five steps, and the geometry of its last frame
1Start the transition with startViewTransition, the switch as its callback
ThemePicker.astro · _colors.scssFour seeds per theme, three ramps mixed from them, and the names every sheet reads
seeds
canvas
ink
accent
blueprint
color-mix(in oklch)
neutral
100
200
300
400
500
600
650
700
800
850
875
900
accent
100
200
300
400
500
600
650
700
800
850
875
900
blueprint
100
200
300
400
500
600
650
700
800
850
875
900
var()names
--surface-page
--ink
--line
--accent
--blueprint
Seeds
Four oklch colours on :root per theme. A dark theme's ink is light, so its ramps run the other way by themselves.
Ramps
A Sass loop mixes canvas into ink in eleven steps for neutral. accent and blueprint tint to 700, take the seed at 800 and shade to 900.
Names
What a sheet reads, never a step. A blueprint sheet like this one remaps them from its own paper.
Token Layers
Theme Picker
Drawing No.4 / 5
Scale1:1
Weight6.5KiB
Date
Projection
ThemePicker.astro · Layout.astroThe OS decides the theme until a pick, and from then on the stamp decides, before the first paint
OS schemepainting@media(prefers-color-scheme: dark){:root:not([data-theme]) }the OS only knows light and dark
stamppainting:root[data-theme="dark"]written by the pick, kept as cv-theme
radiopainting:root:has(#theme-dark:checked)works without script
localStorage
<head> script
data-theme on <html>
first paint
On a reload, eight lines in the <head> stamp the kept pick before the first paint
Nothing picked
Only the OS rule matches, so the page follows the system and changes with it.
A pick
Writes the stamp, checks the radio, keeps cv-theme. color-scheme: only on <body> takes scrollbars and controls along.
Radios by id
An attribute inside :has() put every input on the page in :root's invalidation set; an id names one.
Default and Override
Theme Picker
Drawing No.5 / 5
Scale1:1
Weight6.5KiB
Date
Projection
fourmoresheets,peelthiscorner
thiscornerdragsthepageback
GDetail GScale 1:1 いろいろなしゅるい Object variants
Title
Space Builder · Object Variants
Medium
Vue, Sass
Year
Notes
One card covers a whole group of library objects. The carousel steps through one
cell's finishes, the dropdowns move between cells, and a pair the catalogue never
made falls back to the nearest one it did.
Space Builder · Object Variants
Drawing No.1 / 3
Scale1:1
Weight40.3KiB
Date
Projection
CatalogObjectField.vue · CatalogObjectThumbnailOverlay.vue · HoverSelect.vueOne card stands for every library object in its group
15
25
35
45
55
Chair42cm x 50cm x 95cm
Banquet Table8 seats
2.4m x 1.2m
Group
Every library object under one catalog entry: 5 chairs, 4 banquet sets
Cells
pax and size cut the group, 3 seat counts by 2 sizes here; a cell's objects are its styles
Preview
An option previews on the card before you pick it
Variant Groups
Space Builder · Object Variants
Drawing No.2 / 3
Scale1:1
Weight40.3KiB
Date
Projection
CatalogObjectFooter.vue · isAvailablePaxWhat happens when the combination does not exist
Banquet Table6 seats
1.8m x 76cm
Every seat count stays on the list. The 8 and 4 seat sets have no
object at the size held, so they go grey. Grey is not disabled.
Try the pairSame seat count, and the size already showing
Else take the seat countThe first object with that many seats, whatever its size
Both readings moveThe size under the list changes to match what was found
Never disabled
A greyed option is still clickable. Grey says the other axis will move, not that the pick is refused
Sorted
Every value sorts in one descending run. Missing ones go grey and keep their place
Nothing selected
With no object picked yet, every value counts as available
Missing Variants
Space Builder · Object Variants
Drawing No.3 / 3
Scale1:1
Weight40.3KiB
Date
Projection
twomoresheets,peelthiscorner
thiscornerdragsthepageback
HDetail HScale 1:1 タグをつけるツール Tagging tool
Title
Library Tagging Tool
Medium
CSS, JavaScript
Year
Notes
One input tags a base object and every variant under it. Variants that already
disagree keep their own value, and an object leaves the page once nothing about it
is missing. Pick another property and the values already stored for it come back.
Library Tagging Tool
Drawing No.1 / 4
Scale1:1
Weight12.6KiB
Date
Projection
One "Shared value" input for the base object and every variant
Banquet Set · shared value
Ivory✓
keyup · focus · click · blur
12 seats · baseIvory--caret:5
11 seatsIvory--caret:5
10 seatsIvory--caret:5
0 seatsIvory--caret:5
Mirror
Every keystroke copies the value into each variant card's own input, scrolled to its end so the tail stays in view
Save
Finishing the field submits with update_styles: one row per object, titleized, and a cleared value destroys the rows
Warn
Variants that already disagree keep the input open but empty; the placeholder lists the overrides and the tick becomes a warning sign
2Each variant form gets --caret and its input's --input-width; the value is copied in and scrolled just far enough to keep that caret in view
3The form is monospace, so 1ch is one character and a ::before holding a literal | lands where the real caret would
4It only paints while the shared input :focus holds, blinks with opacity keyframes, and stands still under prefers-reduced-motion
Simulated Caret
Library Tagging Tool
Drawing No.3 / 4
Scale1:1
Weight12.6KiB
Date
Projection
Fill the last gap, and the object leaves the list
A save is find_or_initialize_by then save!; an emptied field destroys the row.
Completed Objects
Library Tagging Tool
Drawing No.4 / 4
Scale1:1
Weight12.6KiB
Date
Projection
threemoresheets,peelthiscorner
thiscornerdragsthepageback
IDetail IScale 1:1 かみをめくる Paper stack
Title
Paper Stack
Medium
CSS, TypeScript
Year
Notes
Every demo on this page is a stack of drawing sheets, and the dog-ear turns them.
These three sheets take the stack apart: the pile and its front page, the fold's
three layers and the release that turns a page, and the row the stack becomes
without JavaScript.
Paper Stack
Drawing No.1 / 3
Scale1:1
Weight24.1KiB
Date
Projection
How a fold is drawn
the page, cut on the crease
the shade, on the clip wrapper
the flap, the back mirrored over
The Fold
Paper Stack
Drawing No.2 / 3
Scale1:1
Weight24.1KiB
Date
Projection
The stack without JavaScript
One rule carries all of it: @media(scripting: none).
Without JavaScript
Paper Stack
Drawing No.3 / 3
Scale1:1
Weight24.1KiB
Date
Projection
twomoresheets,peelthiscorner
thiscornerdragsthepageback
JDetail JScale 1:1 できごとをくばる GNU social event bus
Title
GNU social · Event Dispatch
Medium
PHP, Symfony
Year
Notes
GNU social is a federated social network server. Its plugins never call each other:
a plugin listens for an event by naming a method after it, and the first one to
answer stop has the job. Switch ImageEncoder off and no other plugin claims the JPEG,
so the template draws a plain link. ImageEncoder shows a GIF too, then asks for its
thumbnail with a second event, FileResizerAvailable. That one has two takers at the
same priority. VideoEncoder asked for image/gif by name, so it goes first and keeps the
GIF moving. Switch it off and ImageEncoder makes a still.
Each listener is [Module,'onViewAttachment'], in the order the modules were found
ImageEncoder's view asks for the GIF's thumbnail, and getOrCreate raises
this. Each listener is [Module,'onFileResizerAvailable']
dispatch
AudioEncoderprio 0
next
ImageEncoderprio 0
stop
VideoEncoderprio 0
skipped
returns stop
Not loaded
{%forblockin handle_event('ViewAttachment', …)%}{{ block | raw }}{%endfor%}{%ifnot handled %}<ahref="{{ attachment.getShowUrl(note)}}">{{ title }}</a>{%endif%}
no block came back, so the template writes its own linka block came back, so the template's link is not written
then getOrCreate tries the resizers in this orderarray_merge($event_map['image/gif'],$event_map['image'])
'image/gif'VideoEncoder::resizeVideoPathtrue
'image'ImageEncoder::resizeImagePathnot called
Every module handler registers at the same priority, so GNU social orders these by how
specific the mimetype is.
One handler returns Event::stop, and propagation ends there
AudioEncoder hears the JPEG first and answers next. ImageEncoder claims it and
appends its view to $res. VideoEncoder never hears the event.
Not this handler's job, or its part is done and others may add theirs. Every encoder answers next to a mimetype that is not its own
case stopchain halts
Claimed the job. Nothing after it hears the event, and $res holds what it appended
case unhandledchain goes on
Declared in the enum, returned by no handler in the tree: a handler with nothing to add says next
EventResult
GNU social · Event Dispatch
Drawing No.2 / 4
Scale1:1
Weight18KiB
Date
Projection
No module registers a listener
Each directory's main class in components/*/ and plugins/*/ is a
module. Its public on<EventName> methods listen for that event.
public static function version(): stringnot a listener
public static function shouldHandle(string $mimetype): boolnot a listener
public function onFileMetaAvailable(array &$event_map, string $mimetype): EventResultbecomesFileMetaAvailable[ImageEncoder,'onFileMetaAvailable']prio 0
public function onFileSanitizerAvailable(array &$event_map, string $mimetype): EventResultbecomesFileSanitizerAvailable[ImageEncoder,'onFileSanitizerAvailable']prio 0
public function onFileResizerAvailable(array &$event_map, string $mimetype): EventResultbecomesFileResizerAvailable[ImageEncoder,'onFileResizerAvailable']prio 0
public function onViewAttachment(array $vars, array &$res): EventResultbecomesViewAttachment[ImageEncoder,'onViewAttachment']prio 0
public function resizeImagePath(string $source, …): boolnot a listener
public function onPluginVersion(array &$versions): EventResultbecomesPluginVersion[ImageEncoder,'onPluginVersion']prio 0
Found by reflection in a compiler pass, once, when the container is built. The table is
written into the compiled container, so a request pays nothing for the wiring.
Module Discovery
GNU social · Event Dispatch
Drawing No.3 / 4
Scale1:1
Weight18KiB
Date
Projection
The wrong return type fails the build, not a request
The same compiler pass that finds a handler reads its declared return type, and anything
but EventResult throws BugFoundException before the container
is written.
$ bin/console cache:clear
// Clearing the cache for the dev environment with debug trueIn ModuleManager.php line 93:
[App\Util\Exception\BugFoundException]$ tail -n 1 var/log/dev.log
app.CRITICAL: Return type of Plugin\ImageEncoder\ImageEncoder::onViewAttachment is not the required EventResult in src/Core/ModuleManager.php:93
A handler that declares no return type gets past the pass. The dispatch catches it the
first time it returns nothing (src/Core/Event.php:93): Events must return
an \EventResult, which {file} for {event} does not.
A command-line tool that runs a small federated network on one machine, so you can
test how fediverse servers talk to each other before a release. Each server gets its
own hostname, database and branch. One YAML file in, a Docker Compose stack out.
Fediverse Playground runs a small federated network on one machine, to test how
fediverse servers talk to each other. Switch servers on and off to redraw the
docker-compose.yml it writes.
# This file was generated by Fediverse Playground (GPL3+)version:"3"name:fediverse-playgroundservices:nginx:image:nginx:alpineports:- 8080:80- 8443:443depends_on:- gnusocial-alice- gnusocial-bob- mastodon-carol- mastodon-carol-streamingdb:image:postgres:alpineredis:image:redis:alpinemedia:image:darthsim/imgproxygnusocial-alice-install:build:instances/gnusocial-alice/docker/phpdepends_on:db:condition:service_healthygnusocial-alice:build:instances/gnusocial-alice/docker/phpdepends_on:db:condition:service_healthyredis:condition:service_startedmedia:condition:service_startedgnusocial-alice-install:condition:service_completed_successfullygnusocial-bob-install:build:instances/gnusocial-bob/docker/phpdepends_on:db:condition:service_healthygnusocial-bob:build:instances/gnusocial-bob/docker/phpdepends_on:db:condition:service_healthyredis:condition:service_startedmedia:condition:service_startedgnusocial-bob-install:condition:service_completed_successfullysearch:image:elasticsearch:7.17.4mastodon-carol-install:build:instances/mastodon-caroldepends_on:db:condition:service_healthyredis:condition:service_startedmastodon-carol:build:instances/mastodon-caroldepends_on:db:condition:service_healthyredis:condition:service_startedsearch:condition:service_startedmastodon-carol-install:condition:service_completed_successfullymastodon-carol-streaming:build:context:instances/mastodon-caroldockerfile:streaming/Dockerfiledepends_on:db:condition:service_healthyredis:condition:service_startedmastodon-carol-install:condition:service_completed_successfullymastodon-carol-sidekiq:build:instances/mastodon-caroldepends_on:db:condition:service_healthyredis:condition:service_startedsearch:condition:service_startedmedia:condition:service_startedmastodon-carol-install:condition:service_completed_successfullyLeft out here: volumes, environment, restart, tty, command and healthcheck.
Fediverse Playground
Drawing No.1 / 4
Scale1:1
Weight16.9KiB
Date
Projection
One bare mirror per software, one worktree per instance
An example: the shipped config puts bob on main. Here he asks for v3, which alice's
worktree already holds, so the branch is renamed after him.
mirrors/social-v3git clone --bare · fetch --all on every sync
instances/gnusocial-alicev3
instances/gnusocial-bobv3held by gnusocial-alice→v3-gnusocial-bob
mirrors/social-v2git clone --bare · fetch --all on every sync
instances/gnusocial-v2nightly
mirrors/mastodongit clone --bare · fetch --all on every sync
instances/mastodon-carolmain
gnusocial-bob: the first rung that matches runs
local branch already existsgitworktreeaddinstances/gnusocial-bobv3-gnusocial-bob
branch off the local headgitworktreeadd-bv3-gnusocial-bobinstances/gnusocial-bobv3
branch off origingitworktreeadd-bv3-gnusocial-bobinstances/gnusocial-boborigin/v3
start a new branchgitworktreeadd-bv3-gnusocial-bobinstances/gnusocial-bob
A Branch for Every Server
Fediverse Playground
Drawing No.2 / 4
Scale1:1
Weight16.9KiB
Date
Projection
A one-shot installer brings each server upgnusocial-alice's containers, in the order the compose file starts them
db: pg_isready every 3s
db to gnusocial-alice-install: service_healthy
gnusocial-alice-install: composer install
gnusocial-alice-install to db: doctrine:schema:create
*.localhost already resolves to 127.0.0.1, so hosts.nix leaves it out. *.fediverse needs an entry.
apply-hosts
Merges the marked block from files/hosts.snippet into /etc/hosts.
NixOS
The flake exports nixosModules.hosts, which reads files/hosts.nix.
Dev shell
Entering it checks every *.fediverse name with getent and prints one hint if one does not resolve.
Names on the Network
Fediverse Playground
Drawing No.4 / 4
Scale1:1
Weight16.9KiB
Date
Projection
threemoresheets,peelthiscorner
thiscornerdragsthepageback
LDetail LScale 1:1 ものをさがす Library search
Title
Library Search & Relevance
Medium
Rails, MariaDB
Year
Notes
One box searches every property of a library object, typed and derived alike.
MariaDB scores each hit against the best hit in the chosen category. A colour
filter only drops rows, so the rows it keeps keep their scores.
MATCH's score, over the best score the same query got
Sent asrectangular "8 seats"
Term
In
idf = log₁₀(19 / in)
idf²
rectangular
5
0.580
0.336
"8 seats"
2
0.978
0.956
Object
Σ tf × idf²
MATCH
÷ 1.292
Banquet Table White · 8 pax
1×0.336 + 1×0.956
1.292
1.00
Round Table White · 8 pax
0×0.336 + 1×0.956
0.956
0.74
Buffet Table White
1×0.336 + 0×0.956
0.336
0.26
Banquet Table White · 6 pax
1×0.336 + 0×0.956
0.336
0.26
Banquet Table White · 4 pax
1×0.336 + 0×0.956
0.336
0.26
Banquet Table White · 6 pax
1×0.336 + 0×0.956
0.336
0.26
Rank
InnoDB scores a term by how often it occurs in the row times its idf squared, and adds the terms up; a term in every row weighs nothing
Normalise
Every score is divided by the query's own maximum, so the top hit reads 1.00 whatever the raw numbers were
Numbers
One gsub quotes every number before MariaDB sees it, so 8 stops matching 81: "8" is the token 8 alone, and "8 seats" keeps a seat count as one phrase
Relevance Scoring
Library Search & Relevance
Drawing No.2 / 4
Scale1:1
Weight26KiB
Date
Projection
A filter narrows the relation, and the maximum comes from what is leftrectangular 8 seats → rectangular "8 seats"
No filter
Banquet Table White · 8 pax1.00
Round Table White · 8 pax0.74
Buffet Table White0.26
Banquet Table White · 6 pax0.26
Banquet Table White · 4 pax0.26
6 rows, max 1.292
+ filter
→↓
category = Catering
Buffet Table White1.00
1 rows, max 0.336
Predicate
Each filter is one more AND on the relation: the category compares a column, a property goes through library_object_synthetic_properties by name and value
Maximum
The maximum comes from the relation with its category, so the best row a category leaves reads 1.00. The controller adds the property filter after it has built the maximum, so a property can leave the top row below 1.00
Options
A property filter only offers values already stored for it. On its own, it never offers one that matches nothing
Filters
Library Search & Relevance
Drawing No.3 / 4
Scale1:1
Weight26KiB
Date
Projection
One match fragment, built once, doing three jobs
Bound asrectangular "8 seats"
Request?query=rectangular+8+seats0 sent · 0 dropped · 0 aborted
-- eager_load(:space_object, :properties, :styles) adds LEFT OUTER JOINs and their columns, left out here
SELECT `library_objects`.*,
((2MATCH(`library_object_properties_concatenated`.values) AGAINST('rectangular "8 seats"' IN BOOLEAN MODE))
/ (maxSELECT MAX(MATCH(`library_object_properties_concatenated`.values) AGAINST('rectangular "8 seats"' IN BOOLEAN MODE)) FROM `library_objects` INNER JOIN `library_object_properties_concatenated` ON `library_object_properties_concatenated`.`library_object_id` = `library_objects`.`id` WHERE `library_objects`.`image_processing` = FALSE AND `library_objects`.`deprecated_at` IS NULL AND (MATCH(`library_object_properties_concatenated`.values) AGAINST('rectangular "8 seats"' IN BOOLEAN MODE)) LIMIT 250)) AS search_relevance
FROM `library_objects`
INNER JOIN `library_object_properties_concatenated` ON `library_object_properties_concatenated`.`library_object_id` = `library_objects`.`id`
WHERE `library_objects`.`image_processing` = FALSE AND `library_objects`.`deprecated_at` IS NULL
AND (1MATCH(`library_object_properties_concatenated`.values) AGAINST('rectangular "8 seats"' IN BOOLEAN MODE))
ORDER BY ((3MATCH(`library_object_properties_concatenated`.values) AGAINST('rectangular "8 seats"' IN BOOLEAN MODE))
/ (maxSELECT MAX(...) ... -- the same subquery, in full, again)) DESC,
`library_objects`.`updated_at` DESC
LIMIT 250
6 rows, the subquery's MAX() = 1.292
1WHERE decides which rows come back at all
2divided by the maximum, it is the search_relevance column; Rails' #select drops library_objects.* once a MATCH is in it, so it goes on arel.projections
3the same ratio is the first ORDER BY term; updated_at, inherited from the visible scope, breaks ties
maxMAX() would fold the rows into one, so the maximum comes from the same relation reselected as a scalar subquery, category and LIMIT250 included. The property filter comes after, so it never moves the maximum
Generated SQL
Library Search & Relevance
Drawing No.4 / 4
Scale1:1
Weight26KiB
Date
Projection
threemoresheets,peelthiscorner
thiscornerdragsthepageback
MDetail MScale 1:1 大きさでさがす Synthetic properties
Title
Synthetic Properties
Medium
Rails, MariaDB
Year
Notes
A library object stores its size once, in centimetres. On each save, database
triggers wrote that size out in centimetres, metres, inches and feet, so a search
for 5ft3in or 1.6m found the same table. The triggers lived five weeks on a branch
before Ruby callbacks replaced them.
One table's size, split, converted and searched
The table's size is stored as 160x80x74, in centimetres. When the
row is saved, triggers split it on x, write each piece in centimetres,
metres, inches and feet, and store every form in the text that search reads.
The walkthrough types in these fields. They don't take your own typing.
1 Stored library_objects
split on x, one row per dimension
160
80
74
2 Converted library_object_synthetic_properties
cm160cm by 80cm by 74cm
m1.6m by 0.8m by 0.74m
in"63" by 31" by 29"
in63in by 31in by 29in
ft'in"5'3" by 2'7" by 2'5"
ft in5ft3in by 2ft7in by 2ft5in
Pax8
Seats8
3 Searched library_object_properties_concatenated
this table's search text
160cm by 80cm by 74cm Size (centimeters) 1.6m by 0.8m by 0.74m Size (meters) 63" by 31" by 29" Size (inches) 63in by 31in by 29in Size (inches) 5'3" by 2'7" by 2'5" Size (feet-inches) 5ft3in by 2ft7in by 2ft5in Size (feet-inches) 8 Pax 8 Seats
Synthetic Properties
Drawing No.1 / 4
Scale1:1
Weight15.9KiB
Date
Projection
Each dimension in six unitsEach row of split_sizes goes through six SELECTs, one
per unit, and GROUP_CONCAT joins a unit's pieces with ' by '.
Centimetres and metres keep two significant figures. Inches round to the whole inch.
Unit
160
80
182
Size (centimeters)ROUND(cm, 1-FLOOR(LOG10(cm)))
160cm
80cm
180cm
Size (meters)ROUND(cm /100, 3-FLOOR(LOG10(cm)))
1.6m
0.8m
1.8m
Size (inches)ROUND(cm /2.54)
63"
31"
72"
Size (inches)ROUND(cm /2.54)
63in
31in
72in
Size (feet-inches)FLOOR(in/12) + carry, ROUND(in MOD 12) ifnot0
5'3"
2'7"
6'
Size (feet-inches)FLOOR(in/12) + carry, ROUND(in MOD 12) ifnot0
5ft3in
2ft7in
6ft
182cm is 71.65in, or 5ft and 11.65in. The
11.65in rounds to 12, so it carries and prints as
6ft, not 5ft12in.
The shipped SQL did not round, so 182.5 printed as 182.5cm and
1.825m. It added a foot from six leftover inches, so 80cm read
3ft7in, and it wrote whole feet as 6ft0in. The rules above fix
all three. When rounding changes a size, the exact centimetres and metres are kept
too, so a search for 182cm still finds it.
Unit Conversions
Synthetic Properties
Drawing No.2 / 4
Scale1:1
Weight15.9KiB
Date
Projection
The same SQL in the backfill and three triggersThe backfill and the insert and update triggers on library_objects call
one Ruby method, each with its own where. The delete trigger only deletes
the rows. The hairtrigger gem writes the triggers into a migration.
app/models/library_object.rb
defself.size_synthetic_property_select_sql(where="1=1")<<~SQL.chomp
WITHRECURSIVE split_sizes AS (…) -- one row per dimensionSELECT id AS library_object_id, 'Size (centimeters)'ASname,
GROUP_CONCAT(CONCAT(ROUND(size, 1-FLOOR(LOG10(size))), 'cm') SEPARATOR ' by ') ASvalueFROM split_sizes WHERE#{where}ANDsize<>''GROUP BY id
UNION ALL-- five more, and the exact cm and m where they round SQLend
An object's rows joined into one string for FULLTEXT searchTriggers on library_object_synthetic_properties rebuild the string whenever one of the object's rows changes.
library_object_synthetic_properties
object
name
value
1024
Size (centimeters)
160cm by 80cm by 74cm
1024
Size (meters)
1.6m by 0.8m by 0.74m
1024
Size (inches)
63" by 31" by 29"
1024
Size (inches)
63in by 31in by 29in
1024
Size (feet-inches)
5'3" by 2'7" by 2'5"
1024
Size (feet-inches)
5ft3in by 2ft7in by 2ft5in
1024
Pax
8
1024
Seats
8
app/models/library_object_synthetic_property.rb · AFTER INSERT and UPDATE, each row
INSERT INTO library_object_properties_concatenated
(`library_object_id`, `values`)
SELECT
lops.library_object_id,
GROUP_CONCAT(CONCAT(lops.`value`, ' ', lops.`name`)
SEPARATOR ' ')
FROM library_object_synthetic_properties lops
WHERE lops.library_object_id = NEW.library_object_id
AND`lops`.`value`IS NOT NULLGROUP BY lops.library_object_id
ON DUPLICATE KEYUPDATE`values`=VALUES(`values`);
160cm by 80cm by 74cm Size (centimeters) 1.6m by 0.8m by 0.74m Size (meters) 63" by 31" by 29" Size (inches) 63in by 31in by 29in Size (inches) 5'3" by 2'7" by 2'5" Size (feet-inches) 5ft3in by 2ft7in by 2ft5in Size (feet-inches) 8 Pax 8 Seats
Searchable Text
Synthetic Properties
Drawing No.4 / 4
Scale1:1
Weight15.9KiB
Date
Projection
threemoresheets,peelthiscorner
thiscornerdragsthepageback
NDetail NScale 1:1 テーブルのかたち Schema driver
Title
schemaDef → Doctrine Metadata
Medium
PHP, Symfony, Doctrine
Year
Notes
GNU social is a federated social network server, written in PHP. Each of its
tables is one PHP array on the entity class, and a Doctrine driver turns that
array into the ORM mapping, the database schema and the types PHPStan checks.
GNU social is a federated social network server. Each table is a PHP array on its entity
class, and this driver turns that array into Doctrine's mapping, printed here as the SQL
it creates.
Actor::schemaDef()src/Entity/Actor.php
publicstaticfunction schemaDef(): array{return ['name'=>'actor','description'=>'local and remote users, groups and bots are actors, for instance','fields'=> ['id'=> ['type'=>'serial', 'not null'=>true, 'description'=>'unique identifier'],'nickname'=> ['type'=>'varchar', 'length'=>64, 'not null'=>true, 'description'=>'nickname or username'],'fullname'=> ['type'=>'text', 'description'=>'display name', 'default'=>null],'roles'=> ['type'=>'int', 'not null'=>true, 'description'=>'Bitmap of permissions this actor has'],'type'=> ['type'=>'int', 'not null'=>true, 'description'=>'The type of actor (person, group, bot, etc)'],'homepage'=> ['type'=>'text', 'description'=>'identifying URL', 'default'=>null],'bio'=> ['type'=>'text', 'description'=>'descriptive biography', 'default'=>null],'location'=> ['type'=>'text', 'description'=>'physical location', 'default'=>null],'lat'=> ['type'=>'numeric', 'precision'=>10, 'scale'=>7, 'description'=>'latitude', 'default'=>null],'lon'=> ['type'=>'numeric', 'precision'=>10, 'scale'=>7, 'description'=>'longitude', 'default'=>null],'location_id'=> ['type'=>'int', 'description'=>'location id if possible', 'default'=>null],'location_service'=> ['type'=>'int', 'description'=>'service used to obtain location id', 'default'=>null],'is_local'=> ['type'=>'bool', 'not null'=>true, 'description'=>'Does this actor have a LocalUser associated'],'created'=> ['type'=>'datetime', 'not null'=>true, 'default'=>'CURRENT_TIMESTAMP', 'description'=>'date this record was created'],'modified'=> ['type'=>'timestamp', 'not null'=>true, 'default'=>'CURRENT_TIMESTAMP', 'description'=>'date this record was modified'],],'primary key'=> ['id'],'indexes'=> ['actor_nickname_idx'=> ['nickname'],],'fulltext indexes'=> ['actor_fulltext_idx'=> ['nickname', 'fullname', 'location', 'bio', 'homepage'],],];}
MySQLSchemaDefDriver, printed as DBAL would
CREATETABLE actor (id INTAUTO_INCREMENTNOTNULLCOMMENT'unique identifier',nickname VARCHAR(64) NOTNULLCOMMENT'nickname or username',fullname LONGTEXTDEFAULTNULLCOMMENT'display name',roles INTNOTNULLCOMMENT'Bitmap of permissions this actor has',type INTNOTNULLCOMMENT'The type of actor (person, group, bot, etc)',homepage LONGTEXTDEFAULTNULLCOMMENT'identifying URL',bio LONGTEXTDEFAULTNULLCOMMENT'descriptive biography',location LONGTEXTDEFAULTNULLCOMMENT'physical location',lat NUMERIC(10, 7) DEFAULTNULLCOMMENT'latitude',lon NUMERIC(10, 7) DEFAULTNULLCOMMENT'longitude',location_id INTDEFAULTNULLCOMMENT'location id if possible',location_service INTDEFAULTNULLCOMMENT'service used to obtain location id',is_local TINYINT(1) NOTNULLCOMMENT'Does this actor have a LocalUser associated',created DATETIMEDEFAULTCURRENT_TIMESTAMPNOTNULLCOMMENT'date this record was created',modified DATETIMEDEFAULTCURRENT_TIMESTAMPNOTNULLCOMMENT'date this record was modified',INDEX actor_nickname_idx (nickname),-- actor_fulltext_idx: not read by SchemaDefDriverPRIMARYKEY(id)) DEFAULTCHARACTERSET utf8 COLLATE`utf8_unicode_ci`ENGINE = InnoDBCOMMENT = 'local and remote users, groups and bots are actors, for instance';
RuleHover, tap or focus a line in the PHP or the SQL to see what the driver did to it
'name''name'=>'actor'→setPrimaryTable(['name' => 'actor'])→CREATE TABLE actor
doctrine.orm.default_metadata_driverOne driver chain, one SchemaDefDriver, 74 entities as of October 2022isTransient($class)=!method_exists($class,'schemaDef')
src/Core/ModuleManager.php · process()foreach($module_pathsas$path){// … $type, $module and $dir from the pathif(!\is_null($container)&&file_exists($entity_dir=$dir.'/Entity')&&is_dir($entity_dir)){$entity_paths[]=$entity_dir;$container->findDefinition('doctrine.orm.default_metadata_driver')->addMethodCall('addDriver',[newReference('app.schemadef_driver'),"{$type}\\{$module}\\Entity"],);}}
plugins/Favourite/Entity/NoteFavourite.php is a whole plugin table: drop it in
and note_favourite exists. Its 'foreign key'=>true is still
read by nobody, since the driver's association branch sits behind if (false && …),
so note_id maps as a plain INT.
1Boot flagPHPSTAN_BOOT_KERNEL=1vendor/bin/phpstan analyseThe analyser runs with the real kernel behind it, not a stubphpstan.sh
2Real kernel$kernel->boot();return$kernel;GNUsocialProvider requires it, then starts every module as a request wouldconfig/phpstan-bootstrap.php
3Table mapDB::initTableMap();'note'=>'App\Entity\Note'Built from getAllMetadata(), so from the same schemaDefs the driver readsDB.php
4Return typenewUnionType([Note,newNullType(),])For find, getReference, getPartialReference and getRepository with a literal first argumentClassFromTableName….php
Without the flag the extension steps aside and PHPStan types the call the ordinary way,
so a plain run still works; with it, whatever is called on $note next is
checked against Note like any other typed value.
Static Analysis
schemaDef → Doctrine Metadata
Drawing No.4 / 4
Scale1:1
Weight6.7KiB
Date
Projection
threemoresheets,peelthiscorner
thiscornerdragsthepageback
ODetail OScale 1:1 いくつものげんごを読む web-ts-mode
Title
web-ts-mode
Medium
Emacs Lisp, tree-sitter
Year
Notes
An Emacs major mode for web templates such as Astro and Vue, where one file mixes
markup, TypeScript and styles. Each language gets its own tree-sitter parser over its
part of the file, and highlighting and indenting work across the whole file.
Notes
web-ts-mode is an Emacs major mode for files that mix languages. This Astro component
from the CV has TypeScript in its frontmatter, markup with expressions in it, and a
SCSS style block.
The astro parser reads the whole file, markup included. The TypeScript and the SCSS
each get a parser of their own that sees only its range. Each dashed box is one range,
drawn inside the range that holds it.
The site's build colours the code with a small lexer that follows the mode's faces.
The ranges are the ones Emacs 31.1 hands each parser for this file.
astro 1-34astro, lines 1 to 34
typescript 2-19typescript, lines 2 to 19
html 22-34html, lines 22 to 34
tsx 22tsx, line 22
tsx 24tsx, line 24
tsx 25tsx, line 25
scss 30-33scss, lines 30 to 33
1---
2/**
3 * The sheet number in the desk's left margin: a stencil numeral with that sheet's own title
4 * under it, riding along while the sheet scrolls past. It is decoration, so assistive tech
5 * never sees it; the sheet carries the same words as its heading.
6 *
7 * It is a child of #main rather than of the sheet, because the margin it stands in belongs to
8 * the desk and not to the paper. Absolutely positioned in the left margin column of the grid
9 * row its sheet sits in, it takes that row as its containing block, which is what gives the
10 * numeral the sheet's own height to stick along. The row is the number: there is one folio
11 * per sheet and they run in page order. Both ends of the row are named, since a grid line an
12 * absolutely positioned child leaves at auto resolves to the grid's own padding edge.
Point at or tab to the code to see which parser owns itastro: the host parser, over the whole buffertypescript: frontmatter (frontmatter_js_block), :local ttsx: attribute_interpolation (attribute_js_expr), :local ttsx: element (html_interpolation), :offset '(1 . -1), :local ttsx: element (html_interpolation), :offset '(1 . -1), :local tscss: style_element (raw_text), picked by lang="scss", :local t
In a>b the right side starts one level past the deepest on its left.
The arguments of :not() and :has() sit one level deeper than the pseudo-class.
The branches of a,b share a depth. A nested rule starts again at 0.
Selector Depth Hue
web-ts-mode
Drawing No.3 / 4
Scale1:1
Weight145.3KiB
Date
Projection
One command says what is installed and what is not
Notes
Each language needs a tree-sitter grammar, and completion and error checks need a
language server on PATH. M-x web-ts-doctor lists both.
A missing grammar shows up as a line here. When a file opens, the mode leaves that
language unhighlighted and carries on.
This is the report from the machine that builds this CV. Every grammar is installed,
and one server, astro-ls.
web-ts-mode doctor=================Emacs: 31.1System type: gnu/linuxGrammars-------- astro: ok css: ok tsx: ok typescript: ok javascript: ok html: ok scss: ok xml: ok jsdoc: ok vue: okLSP--- client: eglot auto-enable: tLanguage servers on PATH------------------------ astro-ls: found (node_modules/.bin/astro-ls --stdio) vue: not found (tried vue-language-server, vls) html: not found (tried vscode-html-language-server, html-language-server, vscode-html-languageserver, html-languageserver) xml: not found (tried lemminx, xml-language-server) typescript: not found (tried typescript-language-server) css: not found (tried vscode-css-language-server, css-languageserver, vscode-css-languageserver)CLI--- bin/web-ts grammars — install pinned tree-sitter grammars bin/web-ts doctor — this report (batch) bin/web-ts test — ERT suite bin/web-ts compile — byte-compile package .el files bin/web-ts bench — soft perf timings
U:%%- *web-ts-doctor* Top (Special Follow)U:%%- *web-ts-doctor* (Special Follow)
Doctor Report
web-ts-mode
Drawing No.4 / 4
Scale1:1
Weight145.3KiB
Date
Projection
threemoresheets,peelthiscorner
thiscornerdragsthepageback
05Open Source
SH-OSS/REV A
オープンソース Open Source
マージ
Merged
23
オープン
Open
25
クローズ
Closed
27
Checked
メンテナー Maintainer
GNU social v3
GNUsocial/gnu-social
Commits
~1200
Period
2021–2022
Led the GNU social v3 rewrite from 2021 to 2022 with ~1200 commits, funded by NLnet through the NGI0 Discovery programme. Ported the federated microblogging stack to Symfony 5, with an event driven and modular architecture, rewrote controllers and the posting form layer, expanded PHPUnit coverage, integrated PHPStan and dependency-hygiene tooling
GSoCの学生 GSoC student
Blender GSoC 2019
blender/blender
Period
2019
Proposed replacing Blender's Python OBJ, STL, and PLY importers/exporters with a shared C/C++ IO framework using memory-mapped files and optional parallelization. A prototype patch made OBJ and STL import and export about 3× faster, and the proposal was accepted into the GSoC programme.
Named the Debian package that carries the BlueZ headers in the PyBluez build requirements, so the README says what to install rather than what is needed.
Kept the microphone open while a push-to-talk dictation transcribes in Voxtype. A press in that window is queued and recorded on the open capture, so the next sentence is no longer lost.
Added hotkey.cancel_on_other_key for a modifier used as the hotkey. Pressing any other key while it is held cancels the recording, so Right Ctrl+C no longer dictates.
Added hotkey.double_tap_latch. A double tap keeps the recording running after the key is released, until the next press, and holding the key works as before. A small TapLatch state machine in the evdev listener does the timing.
Added [hotkey.language_modifiers], which maps modifier keys to Whisper language codes. Holding one during a recording transcribes that recording in that language, and the next goes back to the configured one.
Added a loop check to the Whisper transcriber. When a phrase of three or more words repeats three or more times in a row, it retries with beam search, and keeps one copy of each phrase if the retry loops too.
Fixed picking in the 3D view on fractional-scale screens. toDevicePixelPosition() flipped the Y axis in logical pixels and only then scaled to device pixels, so on a 2.67x panel a click landed 1 to 4 device pixels below the cursor.
Added WISPR_CAPTURE_IGNORE_VK, a list of virtual-key codes the Linux helper leaves out of its global key monitor, so a push-to-talk bind on Right Ctrl stops firing on Left Ctrl as well. Skipped keys do not take a sequence index, so the app never sees a gap, and paste injection is untouched.
Added on-enter and on-leave command hooks to every Waybar module, requested since 2023. Ignores GTK's inferior crossings and latches on a hover flag so each command runs once per hover, and mirrors the on-click guard that keeps built-in actions from also running as shell commands.
Packaged claude-auto-retry as a Nix flake with NixOS and home-manager modules that build from source and manage the shell wrapper, reconcile timer, and StopFailure hook through real options instead of npm i -g. Also fixed the wrapper clobbering each tmux pane's TERM.
Add git rebase --continue --no-edit and rebase.noEdit to skip opening $EDITOR after resolving conflicts, with --edit to override the config for a single continue.
Wrapped every Intl stub class in a class_exists guard, so loading the polyfill next to a real intl extension stops fataling with "Cannot declare class Normalizer, because the name is already in use".
Bumped gtk4 to 4.22.5 for upstream's munmap() fix in the Wayland dmabuf format table, which took down any GTK4 client on Hyprland whenever a monitor was plugged in. Traced from a coredump and first sent as a backported patch; the reviewer asked for the bump once 4.22.5 was tagged. Closed as a duplicate of #559510, the GNOME 51 update on staging, which takes gtk4 to 4.24.0.
Upgraded cte-builder to Doctrine 3 and beberlei/assert 3.3. Closed after the maintainer folded parts of it into 3.1.0, which supports DBAL 2 and 3 side by side
Packaged php-align for MELPA with a recipe covering its PHP alignment rules. Closed once the thread led to php-align.el being merged into php-mode instead
Lets git mv create missing leading directories in the destination instead of failing: git mv -p foo bar/quux/ creates bar/ and bar/quux/ first, then moves foo into place.