Snorca Profile Sync
CLI-værktøj der synkroniserer OrcaSlicer-filamentprofiler til Snapmaker U1-printere fra begge upstream GitHub-repos. Håndterer rekursiv profilarv, deep merge og GitHub API-crawling med rate-limit-håndtering.
Selvinitieret, selvhostet projekt. Bygget og drevet af mig som arkitekt, produktejer og reviewer, med AI som superviseret "lærling" i udviklingsprocessen.
DEL 1: Resume
Snorca Profile Sync (SPS) er et CLI-værktøj der holder Snapmaker U1-filamentprofiler ærlige mod upstream OrcaSlicer. Snapmaker vendor'er sin egen fork af OrcaSlicers presets (Snapmaker/Orca_Presets), men den fork driver bagud i forhold til det rigtige upstream-projekt — nye og opdaterede filamentprofiler lander i OrcaSlicer og når aldrig tilbage til Snapmakers distribution. SPS crawler begge GitHub-repositories, hash'er hver profil for at finde hvad der mangler eller er anderledes, resolver hver profils fulde inheritance-kæde (OrcaSlicer-profiler kan arve fra en forælder der arver fra en anden forælder), flader resultatet ud med en regelbaseret deep merge, og skriver færdige profiler direkte ind i den lokale SnapmakerOrcaSlicer-konfigurationsmappe.
Teknologistak
- Kerne: Python 3.11+,
uv, Pydantic v2 (BaseSettings-konfiguration, typede API-response-modeller) - GitHub-integration: REST API (tree- og contents-endpoints), caching af tree/contents-responses, inspektion af
X-RateLimit-Remaining-headeren med en eksplicit advarsel ved uautentificeret rate limit - Sammenligning: SHA-256-indholdshash over sorted-key JSON for en stabil, whitespace-uafhængig diff
- Test:
pytest, 18 unit-tests der specifikt dækker merge-logikken
Største resultat
At gøre "disse to forks er drevet fra hinanden" fra en gætning til et tal. At sammenligne hver Snapmaker-vendor'ede profil mod upstream OrcaSlicer via indholdshash — ikke filnavn, ikke en visuel diff — afslørede et eksakt, efterprøveligt gab: 151 profiler der mangler helt fra Snapmakers fork, og 27 der findes men er ændret upstream. Det tal er kun troværdigt fordi det der ligger under det — rekursiv inheritance-resolution med detektion af cirkulære afhængigheder, og en 8-regel deep merge der eksplicit håndterer typekonflikter, None som en bevidst clearing, og falsy-men-meningsfulde værdier som 0 og False — også er den ene del af kodebasen med reel unit-testdækning. Grundighed gik præcis derhen hvor korrekthed faktisk afhang af den.
DEL 2 — Dybdegående baggrund
Problemet
En printer-specifik slicer-fork der vendor'er presets fra et hurtigere-bevægende upstream-projekt er en vedligeholdelsesfælde: ingen auditerer gabet før en profil en bruger forventer simpelthen ikke er der. For Snapmaker U1-ejere betød det manglende eller forældede filamentprofiler uden nogen indsigt i hvor stort problemet var, og ingen måde at trække en specifik upstream-profil ind — komplet med dens fulde inheritance-kæde — uden at gøre det manuelt.
Arkitektur og løsning
Overordnet form. Én CLI (main.py) der orkestrerer fokuserede, for det meste rene moduler:
| Modul | Ansvar |
|---|---|
github_client.py | Session + tree/contents API-kald mod begge repos, med response-caching og rate-limit-håndtering |
comparator.py | compare_filaments() — SHA-256-indholdshash-diff der producerer lister over manglende / anderledes / kun-snapmaker |
resolver.py | flatten() — rekursiv inheritance-resolution med en forælder-cache og detektion af cirkulære afhængigheder |
merger.py | deep_merge(parent, child) — ren funktion, ingen I/O, det ene modul med dedikerede tests |
writer.py | Skriver fladede profiler lokalt, med dry-run, overwrite og prefix-baseret kollisionshåndtering |
config.py | Pydantic BaseSettings, SPS_-prefikserede env-overrides for hver justerbar indstilling (markører, prefikser, stier) |
Dataflow. github_client crawler begge repos' trees (cachet) → comparator indholdshash'er hver profil og klassificerer den som manglende / anderledes / uændret → for en valgt profil vandrer resolver.flatten() gennem inherits-kæden, henter og cacher hver forælder → merger.deep_merge() folder kæden sammen til ét fladt dict, hvor child altid vinder → writer.save() skriver det under et konfigurerbart prefiks så synkroniserede filer altid kan skelnes fra brugerens egne.
Designbeslutninger værd at fremhæve
- Indholdshash, ikke filnavn, definerer "anderledes."
_content_hash()normaliserer medjson.dumps(data, sort_keys=True)før hashing, så nøgle-rækkefølge og tilfældig formatering aldrig producerer en falsk positiv diff. - Deep merge med 8 eksplicitte, dokumenterede regler — ikke "merge dicts og håb." Typekonflikter logger en advarsel frem for at merge i stilhed;
Nonebehandles som en bevidst clearing, ikke en manglende værdi; lister erstattes wholesale frem for at forsøge en element-merge der ikke ville have et fornuftigt standardvalg;False,0og{}bevares ved brug afis None-tjek, aldrig truthiness, så en legitimt-nul indstilling aldrig kan forveksles med "ikke sat." - Cirkulær inheritance detekteres, løber ikke i uendelig loop.
flatten()bærer etresolving-sæt gennem sin rekursion og kaster enMergeErrorder navngiver den præcise kæde i det øjeblik en forælder dukker op igen, frem for at rekursere til stakken løber over. - Markør-baseret udvælgelse frem for hardkodede profillister. Hvilke upstream OrcaSlicer-profiler der tæller som "Snapmaker U1-relevante" (
@U1) versus universelle (@System) er et konfigurerbart substring-match, så værktøjet tilpasser sig hvis upstreams egen navngivningskonvention skifter uden en kodeændring. - Værktøjet er ærligt om sin egen rate limit. Uden
GITHUB_TOKENsat logger SPS en fremtrædende advarsel om at den er begrænset til 60 requests/time frem for stille at crawle langsomt eller fejle uigennemskueligt midt i en sync. - Tree- og contents-responses cachet per kørsel. Gentagne opslag mod samme sti under inheritance-resolution rammer en in-memory cache i stedet for at genforespørge GitHub API'et, hvilket betyder direkte noget for den uautentificerede rate limit ovenfor.
AI som lærling
AI inde i produktet. Ingen. Profilsammenligning, inheritance-resolution og merging er alle deterministiske operationer over struktureret data — der er ingen tvetydighed her for en model at løse.
AI i udviklingsprocessen (human-in-the-loop). PLAN.md lægger den fulde modulstruktur, ansvarsfordeling og konfigurationsskema før noget modul eksisterede, inklusive de eksakte merge-regler der senere blev implementeret i merger.py — planen var reelt den specifikation deep-merge-unit-testene blev skrevet imod. En AI-kodeassistent implementerede mod den plan over 17 commits på én dag; jeg reviewede specifikt merge-semantikken, da en forkert præcedens-regel der stille ville korrumpere hver synkroniseret profil frem for at fejle højlydt.
Lederens refleksion
- Mål gabet før du beskriver det. "Forken er bagud" er en mening; "151 manglende, 27 anderledes, via indholdshash" er et faktum en bruger kan handle på — forskellen er at lave sammenligningen ordentligt i stedet for at stole på et indtryk.
- Læg dit testbudget hvor logikken faktisk forgrener sig. Af alt i denne kodebase er
deep_merge's otte regler den del med reel beslutningskompleksitet, og det er den eneste del med dedikerede tests — den fordeling var bevidst, ikke tilfældig dækning. - Detekter den fejltilstand du ved er mulig, håb ikke bare den ikke sker. Cirkulær inheritance i en kæde af konfigurationsfiler er en realistisk fejl, ikke en hypotetisk en; at fange den med en navngivet, specifik fejl slår en stack overflow tre lag fjernet fra årsagen.
- Skriv planen testene senere tjekkes imod. At
PLAN.md's merge-regler blev de bogstavelige testcases itest_deep_merge.pyer det der gjorde "matcher implementeringen specifikationen" til et spørgsmål med et mekanisk svar. - Vær ærlig om eksterne grænser i stedet for at maskere dem. At logge den uautentificerede rate-limit-advarsel på forhånd er en lille ting, men det er forskellen mellem et værktøj en bruger stoler på og ét der mystisk bliver langsomt af uforklarede årsager.
Ærlige begrænsninger. Bygget på én dag (17 commits, 22. marts 2026). Kun deep_merge har unit-testdækning — GitHub-klienten, resolveren, comparatoren og CLI-indgangspunkterne har det ikke, og der er ingen CI der kører dem. Uautentificeret brug er begrænset til 60 GitHub API-requests/time, hvilket sætter en grænse for hvor stor en sync kan køre uden en token. Værktøjet er specifikt scopet til Snapmaker U1 og de to repos det allerede kender — at tilføje en tredje profilkilde ville betyde at røre klienten og konfigurationen direkte frem for bare konfiguration.