[{"data":1,"prerenderedAt":1208},["ShallowReactive",2],{"projects:list":3},[4,130,267,469,598,741,856,962,1059],{"id":5,"title":6,"body":7,"demo":93,"description":94,"docs":95,"draft":96,"extension":97,"highlights":98,"image":108,"language":109,"meta":110,"navigation":111,"order":112,"path":113,"repo":114,"seo":115,"site":93,"stack":116,"stem":123,"tagline":124,"tags":125,"__hash__":129},"projects\u002Fprojects\u002Fforge.md","forge",{"type":8,"value":9,"toc":86},"minimark",[10,15,19,27,31,39,42,68,72],[11,12,14],"h2",{"id":13},"the-problem-it-solves","The problem it solves",[16,17,18],"p",{},"If your PHP toolchain lives inside a container, every mainstream hook runner\nmakes you write a wrapper script to shell into it. Husky and lint-staged assume\nNode on the host. lefthook is a single binary, but it still runs tools where it\nhappens to be, not where they are installed.",[16,20,21,22,26],{},"forge routes each tool to the container for you. Set ",[23,24,25],"code",{},"backend = \"ddev\""," (or a\ncontainer name), and it detects the running environment and executes the tool\nthere. If the container is not up, it falls back to the host rather than failing\nthe commit.",[11,28,30],{"id":29},"why-it-is-on-this-page","Why it is on this page",[16,32,33,34,38],{},"This is the project where the test suite matters most, because a hook runner\nthat misbehaves blocks every commit on the team. ",[35,36,37],"strong",{},"297 test functions across 22\ntest files"," cover config parsing, staged-file filtering, container detection,\nand the commit-message policy engine.",[16,40,41],{},"The design constraints were deliberate:",[43,44,45,56,62],"ul",{},[46,47,48,51,52,55],"li",{},[35,49,50],{},"A single static binary."," A tool that enforces quality gates cannot itself\nrequire a package manager to install. ",[23,53,54],{},"go install",", Homebrew tap, or a curl\ninstaller.",[46,57,58,61],{},[35,59,60],{},"TOML config, not shell."," The config file is data, so it can be validated\nand diffed, rather than a script that can do anything.",[46,63,64,67],{},[35,65,66],{},"Graceful degradation."," Container missing, tool missing, no staged files -\neach has a defined behavior instead of an unhandled error.",[11,69,71],{"id":70},"what-is-inside","What is inside",[43,73,74,77,80,83],{},[46,75,76],{},"Commit-message policy (conventional commits) built into the runner, so\ncommitlint and its Node dependency are not needed",[46,78,79],{},"Monorepo workspace mode, staged-file filtering, parallel execution",[46,81,82],{},"A migration path from Husky that reads the existing config",[46,84,85],{},"Documentation site, changelog automation via release-please, and a demo\nrecorded with VHS",{"title":87,"searchDepth":88,"depth":88,"links":89},"",2,[90,91,92],{"id":13,"depth":88,"text":14},{"id":29,"depth":88,"text":30},{"id":70,"depth":88,"text":71},null,"A git hook runner that executes your linters and formatters inside DDEV or Docker, where the tools actually live. One Go binary, no Node.js, no wrapper scripts.","https:\u002F\u002Fterrorsquad.github.io\u002Fforge\u002F",false,"md",[99,102,105],{"value":100,"label":101},"297","Go test functions across 22 test files",{"value":103,"label":104},"1","static binary, zero runtime dependencies",{"value":106,"label":107},"5","CI workflows, including an install smoke test","\u002Fprojects\u002Fforge.webp","Go",{},true,1,"\u002Fprojects\u002Fforge","https:\u002F\u002Fgithub.com\u002FTerrorSquad\u002Fforge",{"title":6,"description":94},[109,117,118,119,120,121,122],"Cobra","TOML","DDEV","Docker","GitHub Actions","Homebrew","projects\u002Fforge","The git hook runner for containerized dev",[126,127,109,128],"Developer Tooling","CLI","Testing","C3w1BgwjmaLfFBPKqedb_gXMWBHkI7slEWfyWq5w7Zw",{"id":131,"title":132,"body":133,"demo":93,"description":234,"docs":93,"draft":96,"extension":97,"highlights":235,"image":245,"language":246,"meta":247,"navigation":111,"order":248,"path":249,"repo":93,"seo":250,"site":251,"stack":252,"stem":259,"tagline":260,"tags":261,"__hash__":266},"projects\u002Fprojects\u002Fzakonik.md","Zakonik",{"type":8,"value":134,"toc":228},[135,137,153,168,171,175,178,189,192,196,211,214,218,221],[11,136,14],{"id":13},[16,138,139,140,144,145,148,149,152],{},"Type ",[141,142,143],"em",{},"otkaz na bolovanju"," into any Serbian legal search and you get nothing.\nNot because the law is silent, but because the statute never uses the word\n",[141,146,147],{},"bolovanje"," — it says ",[141,150,151],{},"privremena sprečenost za rad usled bolesti",". Not one\nword in common, so keyword search returns an empty page while the answer sits\nin article 183 of the Labour Act.",[16,154,155,156,159,160,163,164,167],{},"The gap is vocabulary, not knowledge. People know what happened to them; they\ncannot guess the term it was filed under. Measured against the corpus, the word\n",[141,157,158],{},"predomislio"," (\"changed my mind\") appears in ",[35,161,162],{},"none"," of the 5,056 indexed\narticles — the law calls it ",[141,165,166],{},"odustanak od ugovora",".",[16,169,170],{},"So the question is translated into statutory vocabulary first, and only then\nsearched. That single step is what replaces embeddings.",[11,172,174],{"id":173},"no-vector-database","No vector database",[16,176,177],{},"The first build did it the orthodox way: Postgres, pgvector, an embedding per\narticle. It worked, and for a project with no budget it meant a database that\nidles into suspension, a monthly bill, and one more service that can fail.",[16,179,180,181,184,185,188],{},"The whole corpus is now a single ",[35,182,183],{},"8 MB SQLite file committed to the repo"," and\nread in-process through ",[23,186,187],{},"node:sqlite",", with FTS5 doing retrieval in about\n0.1 ms. Deploying needs exactly one environment variable.",[16,190,191],{},"Honest caveat, because it matters: in my own measurements vectors still ranked\nthe correct article first slightly more often. SQLite is cheaper and sufficient\nhere — not equivalent.",[11,193,195],{"id":194},"citations-are-verified-not-trusted","Citations are verified, not trusted",[16,197,198,199,202,203,206,207,210],{},"The model is given the retrieved articles and told to use nothing else, with a\nsanctioned way to refuse. That instruction is ",[35,200,201],{},"not trusted",". After generation,\ncode extracts every ",[23,204,205],{},"[Zakon o radu, član 183]"," citation and checks it against\nthe articles actually retrieved; anything unmatched is flagged in the UI as\n",[141,208,209],{},"nepotvrđen citat",". The legal disclaimer is appended by the application, so the\nmodel cannot reword or drop it.",[16,212,213],{},"Refusal is treated as a feature. Ask something outside the corpus and it says\nso rather than assembling a confident-sounding paragraph — which, for legal\nquestions, is the difference between useless and harmful.",[11,215,217],{"id":216},"built-in-a-day","Built in a day",[16,219,220],{},"First commit to live site inside a day, written with Claude Code.\nWhat that time actually bought was not the UI but the parts that are easy to\nskip: the citation verifier, graceful degradation to the retrieved articles\nwhen the upstream model quota runs out, and a privacy page that states plainly\nthat questions are sent to Google.",[16,222,223,224,227],{},"Repealed articles are a good example of how narrow the margin is. The corpus\nkeeps them, because a statute printed with silent gaps is harder to read — but\nretrieval excludes anything opening with ",[23,225,226],{},"(Brisano)",", so a dead provision can\nnever end up in an answer. The filter matches on that prefix rather than the\nword anywhere in the text: 23 articles are genuinely repealed, while 14 live\nones merely mention it, including one about the property of a struck-off\ncompany. Catching that distinction is the difference between fixing a bug and\nquietly deleting fourteen valid articles.",{"title":87,"searchDepth":88,"depth":88,"links":229},[230,231,232,233],{"id":13,"depth":88,"text":14},{"id":173,"depth":88,"text":174},{"id":194,"depth":88,"text":195},{"id":216,"depth":88,"text":217},"Serbian legislation searched in plain language, answered with the exact article it rests on. Thirteen statutes and 5,056 articles indexed, every citation verified against the retrieved text before it reaches the page.",[236,239,242],{"value":237,"label":238},"5,056","articles across 13 statutes, in one 8 MB SQLite file",{"value":240,"label":241},"~0.1ms","lexical retrieval, in-process, no database server",{"value":243,"label":244},"100%","of citations checked against the retrieved text, in code","\u002Fprojects\u002Fzakonik.webp","TypeScript",{},1.1,"\u002Fprojects\u002Fzakonik",{"title":132,"description":234},"https:\u002F\u002Fwww.zakonik.rs",[253,246,254,255,256,257,258],"Nuxt 4","SQLite FTS5","Google Gemini","Nuxt UI v4","Vercel","Upstash Redis","projects\u002Fzakonik","Ask in plain Serbian, get the article",[262,263,264,265],"RAG","Search","Legal tech","Nuxt","rOtpVkNDOC4TRhiuhF9sh6FNqcdA_tT3IK5WoXUTIfU",{"id":268,"title":269,"body":270,"demo":93,"description":439,"docs":440,"draft":96,"extension":97,"highlights":441,"image":451,"language":452,"meta":453,"navigation":111,"order":454,"path":455,"repo":456,"seo":457,"site":93,"stack":458,"stem":463,"tagline":464,"tags":465,"__hash__":468},"projects\u002Fprojects\u002Fporter.md","Porter",{"type":8,"value":271,"toc":431},[272,274,277,280,282,289,300,314,318,321,369,373,380,391,394,398,424,428],[11,273,14],{"id":13},[16,275,276],{},"Some machines are not on a network, and are not going to be. The usual answers\nare a USB stick you are not allowed to plug in, or a support ticket. Porter\nuses the one channel that is always open: the screen is already showing you\ndata, and you already have a camera.",[16,278,279],{},"A terminal sender renders the file as a slideshow of QR frames. A Flutter app\npoints a camera at that screen, decodes frames as they go past, and\nreassembles the original bytes — verified against a SHA-256 the sender\ntransmits in a final frame.",[11,281,30],{"id":29},[16,283,284,285,288],{},"Because the interesting problem is not \"draw a QR code\". It is that ",[35,286,287],{},"a camera\nfilming a screen drops frames",", unpredictably and in bursts. A naive\nsequential transfer means the receiver must eventually catch frame 48,213\nspecifically, and will sit there through several full loops waiting for it.",[16,290,291,292,295,296,299],{},"Porter's answer is a ",[35,293,294],{},"Luby Transform fountain code",". Every frame is the XOR\nof a pseudo-random subset of source blocks, so no individual frame is special\n— collect enough of ",[141,297,298],{},"any"," of them and the file decodes. The subset is derived\nfrom the frame's sequence number by a PRNG both sides run independently, so\nthe mapping is never transmitted.",[16,301,302,303,306,307,167],{},"That constraint is the spine of the project: an xorshift32 PRNG and a\nrobust-soliton degree table must produce ",[35,304,305],{},"bit-identical"," results in Rust and\nDart, or transfers fail silently. It is enforced with shared fixtures and\ndocumented as a normative\n",[308,309,313],"a",{"href":310,"rel":311},"https:\u002F\u002Fporter.goranninkovic.com\u002Fdocs\u002Freference\u002Fwire-format",[312],"nofollow","wire-format spec",[11,315,317],{"id":316},"what-real-usage-taught-it","What real usage taught it",[16,319,320],{},"Most of the hard bugs only appeared at scale, on a genuine 115 MB transfer\nwith 70,965 source blocks — none were visible at test sizes:",[43,322,323,332,346,360],{},[46,324,325,328,329,167],{},[35,326,327],{},"A quadratic endgame."," Every peeled block rescanned the entire pending\npool, so decoding collapsed from ~190,000 symbols\u002Fs to 64\u002Fs right at the\nfinish, freezing the UI for 78 seconds at a stretch. A reverse index from\nblock to dependent symbols made it linear: ",[35,330,331],{},"99.4 s → 1.2 s",[46,333,334,337,338,341,342,345],{},[35,335,336],{},"Boxed integer lists."," Dart's ",[23,339,340],{},"List\u003Cint>"," costs roughly 8 bytes per byte.\nSwitching the symbol buffers to ",[23,343,344],{},"Uint8List"," cut decoder memory from 158 MB\nto 56 MB, measured.",[46,347,348,351,352,355,356,359],{},[35,349,350],{},"A 32-bit overflow in the degree table."," ",[23,353,354],{},"i * (i - 1)"," wraps past\n",[23,357,358],{},"i ≈ 65536",", silently corrupting the probability distribution in release\nbuilds for any large file.",[46,361,362,365,366,167],{},[35,363,364],{},"Content-hashed transfer IDs are not session IDs."," The ID derives from the\nfile's SHA-256, so re-sending the same file at a different terminal size\nreuses it with a different block size — and the two streams are mutually\nundecodable. Transfer identity had to become ",[23,367,368],{},"(id, K, blockSize)",[11,370,372],{"id":371},"down-to-one-runtime","Down to one runtime",[16,374,375,376,379],{},"The sender began as TypeScript, which meant the ",[141,377,378],{},"sending"," machine needed Node\ninstalled — precisely the machine least likely to have it, on a tool whose\nwhole premise is that it is cut off from everything.",[16,381,382,383,386,387,390],{},"It moved to Rust for a single static binary, and ",[23,384,385],{},"ratatui"," bought the sender\ncontrols the hand-rolled ANSI renderer could not: scrubbing, jump-to-chunk, and\na gap-fill mode that loops only the frames a receiver reports missing. The last\nsubcommand to hold out was ",[23,388,389],{},"join",", pure local file concatenation with a\nchecksum — no wire-format surface, so nothing to keep in sync. Porting it let\nthe Node package and its entire lint\u002Fformat toolchain be deleted outright.",[16,392,393],{},"The repo is now two languages instead of three, with no JavaScript anywhere in\nit. The wire format shrank from three implementations to two, which is one\nfewer place for a bit to drift.",[11,395,397],{"id":396},"design-constraints","Design constraints",[43,399,400,406,412,418],{},[46,401,402,405],{},[35,403,404],{},"The receiver never blocks on the UI thread."," Decoding, peeling and disk\nI\u002FO live in a worker isolate; only small progress snapshots cross back.",[46,407,408,411],{},[35,409,410],{},"Progress must be honest."," Fountain decoding recovers almost nothing until\na late avalanche, so a bar tracking recovered blocks reads as frozen. It\ntracks collected symbols against the ~2x K actually needed — measured, not\nassumed.",[46,413,414,417],{},[35,415,416],{},"Interrupted transfers resume."," Blocks are written to disk as they are\nrecovered and the seen-symbol set is persisted to an append-only sidecar, so\na killed app does not restart from zero.",[46,419,420,423],{},[35,421,422],{},"Memory stays bounded."," Above a size threshold the un-peeled symbol pool\nspills to disk, which keeps a gigabyte-scale transfer from becoming an\nout-of-memory crash.",[11,425,427],{"id":426},"honest-limits","Honest limits",[16,429,430],{},"QR is roughly a 0.3 MB\u002Fs channel. A 1 GB file is about 9.5 hours of continuous\nscanning, and no amount of engineering changes that — so the sender now\nestimates the duration up front and asks for confirmation rather than letting\nyou discover it 90 minutes in. Porter is built for the tens-of-megabytes case,\nwhere it is genuinely the shortest path between two machines that cannot talk.",{"title":87,"searchDepth":88,"depth":88,"links":432},[433,434,435,436,437,438],{"id":13,"depth":88,"text":14},{"id":29,"depth":88,"text":30},{"id":316,"depth":88,"text":317},{"id":371,"depth":88,"text":372},{"id":396,"depth":88,"text":397},{"id":426,"depth":88,"text":427},"Air-gapped file transfer over dynamic QR codes. A terminal sender animates a file as QR frames; a Flutter app films the screen and rebuilds the bytes. No network, no cloud — the QR codes are the wire.","https:\u002F\u002Fporter.goranninkovic.com",[442,445,448],{"value":443,"label":444},"160","tests across Rust and Dart, including cross-language parity",{"value":446,"label":447},"2","independent implementations of one bit-exact wire format",{"value":449,"label":450},"82x","faster fountain decode after fixing a quadratic endgame","\u002Fprojects\u002Fporter.webp","Rust",{},1.2,"\u002Fprojects\u002Fporter","https:\u002F\u002Fgithub.com\u002FTerrorSquad\u002Fporter",{"title":269,"description":439},[452,385,459,460,461,462,121],"axum","Dart","Flutter","LT fountain codes","projects\u002Fporter","Move files across an air gap with a camera",[466,452,461,467],"Systems","Error-Correcting Codes","rI0mCwpnkMrbBIVEyQqatpXPMAb5KSPm-Og_Z5li0yg",{"id":470,"title":471,"body":472,"demo":93,"description":567,"docs":93,"draft":96,"extension":97,"highlights":568,"image":578,"language":246,"meta":579,"navigation":111,"order":580,"path":581,"repo":93,"seo":582,"site":93,"stack":583,"stem":590,"tagline":591,"tags":592,"__hash__":597},"projects\u002Fprojects\u002Fjob-finder.md","job·finder",{"type":8,"value":473,"toc":561},[474,485,489,492,495,499,502,505,509,512,518,521,525],[475,476,477],"blockquote",{},[16,478,479,480,484],{},"Built on ",[308,481,483],{"href":482},"\u002Fprojects\u002Fgstack","GStack",", which is the point: the starter existed\nso that this could be about the product instead of about auth, tenancy and\nbilling. First commit to a working multi-tenant application in 13 days.",[11,486,488],{"id":487},"the-problem","The problem",[16,490,491],{},"Job boards optimise for volume. You scroll for forty minutes to find one posting\nworth reading, and the ones that match you best are buried under hundreds that\ndo not. The work is not applying. The work is finding the three postings a week\nthat justify the effort.",[16,493,494],{},"job·finder inverts it. It reads the boards on a schedule, scores every posting\nagainst your actual CV and preferences, and stays silent unless something clears\nyour threshold.",[11,496,498],{"id":497},"no-scraping-on-purpose","No scraping, on purpose",[16,500,501],{},"Every posting comes from a public JSON API that companies publish deliberately:\nGreenhouse, Lever, Ashby, plus a handful of open feeds. Nine adapters, each a\npure function - raw payload in, normalised postings out, no database access, a\nfixture test attached.",[16,503,504],{},"No headless browser, no anti-bot evasion, no proxy rotation. That is a decision\nrecorded as an ADR, not a shortcut. A scraper wins the demo and loses the year:\nit breaks on a renamed CSS class and turns the product into an adversarial\nsystem you maintain forever. The cost of the choice is real - Workday and\nLinkedIn are simply unreachable - and it was worth paying.",[11,506,508],{"id":507},"where-the-llm-is-and-where-it-is-not","Where the LLM is, and where it is not",[16,510,511],{},"Scoring runs on Gemini 2.5 Flash Lite, server-side only, with the rubric and\nprofile in a cached system instruction and the posting as the payload. Structured\noutput is validated against a schema on the way back, and a response that does\nnot parse leaves the row unscored for the next run rather than storing something\nplausible and wrong.",[16,513,514,517],{},[35,515,516],{},"The model does not do arithmetic."," It scores four dimensions independently;\napplication code computes the final number. Language models are good at\njudgement and unreliable at consistent multiplication, so it is only asked for\nthe part it is good at.",[16,519,520],{},"The whole pipeline is off behind a flag until the rubric is calibrated. An\nuncalibrated scorer that runs automatically is just an expensive random number\ngenerator.",[11,522,524],{"id":523},"operationally","Operationally",[43,526,527,533,543,549,555],{},[46,528,529,532],{},[35,530,531],{},"Multi-tenant from the first migration"," - one account is one tenant, RLS\nscoped, with its own profile documents, tracked boards, settings and quota",[46,534,535,538,539,542],{},[35,536,537],{},"Scheduled in the database"," - ingestion, scoring, digest and retention run on\n",[23,540,541],{},"pg_cron"," rather than external workflow runners, so the schedule lives with the\ndata it operates on",[46,544,545,548],{},[35,546,547],{},"Idempotent ingestion"," - a re-run can never overwrite a status, score or\ngenerated letter, enforced at the upsert",[46,550,551,554],{},[35,552,553],{},"Alerts that respect quiet hours"," - Telegram for anything above threshold, a\ndaily email digest for the rest, at most once each",[46,556,557,560],{},[35,558,559],{},"It never applies on your behalf."," A human reviews and sends, always. That is\na product boundary, not a missing feature.",{"title":87,"searchDepth":88,"depth":88,"links":562},[563,564,565,566],{"id":487,"depth":88,"text":488},{"id":497,"depth":88,"text":498},{"id":507,"depth":88,"text":508},{"id":523,"depth":88,"text":524},"A multi-tenant job-discovery product that reads company ATS boards, scores every posting against your CV with an LLM, and prepares tailored application material. Built on GStack in 13 days.",[569,572,575],{"value":570,"label":571},"13","days from empty repo to a working multi-tenant product",{"value":573,"label":574},"9","ATS adapters, each a pure function with a fixture test",{"value":576,"label":577},"766","tests - 737 unit, 29 end-to-end","\u002Fprojects\u002Fjob-finder.webp",{},1.5,"\u002Fprojects\u002Fjob-finder",{"title":471,"description":567},[253,246,584,585,586,541,587,588,589],"Supabase","PostgreSQL","Gemini","Playwright","Telegram","Resend","projects\u002Fjob-finder","LLM-scored job discovery, built on my own starter",[593,594,595,596],"Product","AI","Multi-tenancy","Architecture","hCsDPWUsK5P7Ikh-GUOcFb5qxh6Dfb6OKc0-oJJfs60",{"id":599,"title":483,"body":600,"demo":93,"description":718,"docs":719,"draft":96,"extension":97,"highlights":720,"image":730,"language":246,"meta":731,"navigation":111,"order":88,"path":482,"repo":732,"seo":733,"site":93,"stack":734,"stem":736,"tagline":737,"tags":738,"__hash__":740},"projects\u002Fprojects\u002Fgstack.md",{"type":8,"value":601,"toc":713},[602,606,625,631,635,673,676,680],[11,603,605],{"id":604},"the-architectural-idea","The architectural idea",[16,607,608,609,612,613,616,617,620,621,624],{},"Most SaaS starters put tenancy in the application layer: a ",[23,610,611],{},"where tenant_id = ?","\nthat someone eventually forgets. GStack pushes it into Postgres. Every table is\ntenant-scoped by row-level security through a ",[23,614,615],{},"current_tenant_id()"," helper, so a\nmissing filter in application code is not a data leak. Roles (",[23,618,619],{},"member"," \u002F\n",[23,622,623],{},"admin",") are enforced the same way, with page metadata handling UX only.",[16,626,627,628],{},"That is the distinction I care about: ",[35,629,630],{},"the database is the gate, the UI is the\nconvenience.",[11,632,634],{"id":633},"feature-layers","Feature layers",[16,636,637,638,641,642,641,644,647,648,641,651,641,654,641,657,641,660,641,663,641,666,669,670,672],{},"The codebase is split into ten Nuxt layers - ",[23,639,640],{},"account",", ",[23,643,623],{},[23,645,646],{},"analytics",",\n",[23,649,650],{},"billing",[23,652,653],{},"email",[23,655,656],{},"feedback",[23,658,659],{},"marketing",[23,661,662],{},"notes",[23,664,665],{},"tour",[23,667,668],{},"ui",". Each owns\nits pages, composables, and types, and most can be switched off with an\nenvironment variable. ",[23,671,662],{}," is a deliberately boring CRUD example that shows\nthe pattern to copy.",[16,674,675],{},"Layers are not decoration here. They mean a feature can be deleted without\ngrepping the whole app, and a new domain can be added without touching existing\nones.",[11,677,679],{"id":678},"operational-maturity","Operational maturity",[43,681,682,692,698,704],{},[46,683,684,687,688,691],{},[35,685,686],{},"Billing"," via Polar checkout, customer portal, and webhook, with plan gating\nthrough a ",[23,689,690],{},"useSubscription()"," composable",[46,693,694,697],{},[35,695,696],{},"Notifications"," - a database trigger fans an event out to tenant admins, a\nwebhook mirrors it to transactional email",[46,699,700,703],{},[35,701,702],{},"Observability"," - Sentry on client and server, PostHog analytics, and\nfeature flags that degrade safely when the service is unreachable",[46,705,706,709,710],{},[35,707,708],{},"CI"," - lint, typecheck, tests, plus dedicated accessibility and screenshot\nworkflows; screenshots in the README regenerate with ",[23,711,712],{},"pnpm screenshots",{"title":87,"searchDepth":88,"depth":88,"links":714},[715,716,717],{"id":604,"depth":88,"text":605},{"id":633,"depth":88,"text":634},{"id":678,"depth":88,"text":679},"A production-ready starting point for multi-tenant SaaS: Nuxt 4, Supabase, row-level-security tenancy, billing, notifications, and observability already wired together and tested.","https:\u002F\u002Fgstack.goranninkovic.com",[721,724,727],{"value":722,"label":723},"10","isolated Nuxt layers, each a self-contained feature",{"value":725,"label":726},"RLS","tenancy enforced in the database, not app code",{"value":728,"label":729},"19","test files, plus automated a11y and screenshot CI","\u002Fprojects\u002Fgstack.webp",{},"https:\u002F\u002Fgithub.com\u002FTerrorSquad\u002Fgstack",{"title":483,"description":718},[253,246,584,585,256,587,735,589],"Polar","projects\u002Fgstack","Multi-tenant SaaS starter, batteries included",[596,739,595,265],"SaaS","2LZruijOdeK6uT4WrObO56N83b_yLWKzFnZwjNQCvOw",{"id":742,"title":743,"body":744,"demo":93,"description":832,"docs":93,"draft":96,"extension":97,"highlights":833,"image":843,"language":246,"meta":844,"navigation":111,"order":845,"path":846,"repo":93,"seo":847,"site":753,"stack":848,"stem":851,"tagline":852,"tags":853,"__hash__":855},"projects\u002Fprojects\u002Fmoj-go.md","moj·GO",{"type":8,"value":745,"toc":827},[746,757,761,764,770,774,780,783,787],[475,747,748],{},[16,749,750,751,756],{},"This one is a commercial product running at ",[308,752,755],{"href":753,"rel":754},"https:\u002F\u002Fmojgo.rs",[312],"mojgo.rs",", so\nthe repository is private. The numbers below are from the codebase itself, and\nI am happy to walk through the architecture or the test suite on a call.",[11,758,760],{"id":759},"why-this-one-is-the-real-test","Why this one is the real test",[16,762,763],{},"A leave tracker sounds simple until you write down the rules. Entitlement\naccrues over a year. Unused days carry over, but only some of them, and only\nuntil a cutoff. Public holidays are national but companies add their own. A\nrequest spans a weekend differently than a Tuesday. Approvals route to a manager\nwho may themselves be on leave.",[16,765,766,767],{},"Getting that wrong means someone loses days they earned. ",[35,768,769],{},"This is the kind of\ndomain where the tests are not ceremony.",[11,771,773],{"id":772},"how-it-is-tested","How it is tested",[16,775,776,779],{},[35,777,778],{},"43 end-to-end Playwright specs and 28 unit test files."," The end-to-end weight\nis deliberate: the risky behavior in this app is multi-step and role-dependent -\nan employee submits, an admin approves, a balance changes - and that only holds\ntogether in a real browser against a real database.",[16,781,782],{},"The e2e suite includes visual snapshot tests across desktop and mobile, so a\nlayout regression fails CI rather than reaching a user.",[11,784,786],{"id":785},"engineering-practice-on-display","Engineering practice on display",[43,788,789,799,805,815,821],{},[46,790,791,794,795,798],{},[35,792,793],{},"81 SQL migrations."," Every schema change is a reviewable, replayable file.\nTypes are regenerated from the live schema (",[23,796,797],{},"pnpm db:types","), so the database\nis the source of truth for TypeScript, not a hand-maintained parallel model.",[46,800,801,804],{},[35,802,803],{},"150 markdown documents"," - architecture decision records, runbooks, email\ntemplate references. The decisions are written down where the next person\nlooks.",[46,806,807,810,811,814],{},[35,808,809],{},"Seeded demo data"," - realistic tenants, employees, and requests, all tagged\n",[23,812,813],{},"is_demo",", so the app can be demonstrated and tested without touching real\nrecords.",[46,816,817,820],{},[35,818,819],{},"Bilingual"," (Serbian \u002F English) from the start, because retrofitting i18n is\nhow you learn not to retrofit i18n.",[46,822,823,826],{},[35,824,825],{},"Sentry"," on client and server, plus accessibility and screenshot CI\nworkflows.",{"title":87,"searchDepth":88,"depth":88,"links":828},[829,830,831],{"id":759,"depth":88,"text":760},{"id":772,"depth":88,"text":773},{"id":785,"depth":88,"text":786},"A multi-tenant HR leave-management app for Serbian companies. Real product, real domain rules: accrual, carryover, public holidays, and an approval chain that has to be right.",[834,837,840],{"value":835,"label":836},"71","test files - 43 end-to-end specs, 28 unit",{"value":838,"label":839},"81","SQL migrations, schema versioned from day one",{"value":841,"label":842},"150","markdown docs: ADRs, runbooks, references","\u002Fprojects\u002Fmoj-go.webp",{},3,"\u002Fprojects\u002Fmoj-go",{"title":743,"description":832},[253,246,584,585,849,587,825,850],"Pinia","i18n","projects\u002Fmoj-go","Multi-tenant HR leave management, in production",[593,595,128,854],"Domain Modelling","dSJd7m7u65RcRKhq2DbQPkw-7OEB5fD_3QddkuMlIjs",{"id":857,"title":858,"body":859,"demo":93,"description":931,"docs":932,"draft":96,"extension":97,"highlights":933,"image":941,"language":942,"meta":943,"navigation":111,"order":944,"path":945,"repo":946,"seo":947,"site":93,"stack":948,"stem":955,"tagline":956,"tags":957,"__hash__":961},"projects\u002Fprojects\u002Fphp-booster.md","Booster",{"type":8,"value":860,"toc":926},[861,865,868,871,875,913,917,923],[11,862,864],{"id":863},"the-argument","The argument",[16,866,867],{},"Every team relitigates the same setup: which static analyzer, at what level,\nwhich formatter, which git hooks, and who fixes the 400 pre-existing violations.\nThe discussion costs more than the decision. Booster is that decision made once,\nwith the configuration that makes it survive contact with an existing codebase.",[16,869,870],{},"One command adds the whole stack to a project, with an interactive wizard for\nchoosing which tools you actually want.",[11,872,874],{"id":873},"what-it-installs","What it installs",[43,876,877,883,889,895,901,907],{},[46,878,879,882],{},[35,880,881],{},"Static analysis"," - PHPStan and Psalm, configured at a level a real\ncodebase can adopt rather than the maximum that forces a mass baseline",[46,884,885,888],{},[35,886,887],{},"Formatting"," - EasyCodingStandard, applied automatically",[46,890,891,894],{},[35,892,893],{},"Refactoring"," - Rector, for mechanical language-version upgrades",[46,896,897,900],{},[35,898,899],{},"Git hooks"," - quality gates before the commit, not after the review",[46,902,903,906],{},[35,904,905],{},"IDE config"," - VS Code and PhpStorm settings, so the editor agrees with CI",[46,908,909,912],{},[35,910,911],{},"DDEV integration"," - optional, but the tools run inside the container when\nit is there",[11,914,916],{"id":915},"the-part-i-think-is-right","The part I think is right",[16,918,919,922],{},[35,920,921],{},"Formatting and modernization are fixed in CI, not in code review."," A GitHub\nAction applies the automated fixes and pushes them back. Style is never a review\ncomment, because a human arguing about brace placement is a human not reviewing\nthe logic.",[16,924,925],{},"The bundle versions itself, updates its own dependencies through a scheduled\nworkflow, and ships a documentation site. Integration tests verify the installer\nactually works against a fresh project, which is the test that matters for\nsomething whose entire job is installation.",{"title":87,"searchDepth":88,"depth":88,"links":927},[928,929,930],{"id":863,"depth":88,"text":864},{"id":873,"depth":88,"text":874},{"id":915,"depth":88,"text":916},"The PHP quality toolchain I bring to a team, packaged as one installable bundle: PHPStan, Psalm, Rector, ECS, git hooks, CI, and IDE config, pre-wired and ready on day one.","https:\u002F\u002Fterrorsquad.github.io\u002Fphp-booster\u002F",[934,936,939],{"value":103,"label":935},"command to add the full toolchain to a project",{"value":937,"label":938},"Auto","formatting and modernization fixed in CI, not review",{"value":106,"label":940},"CI workflows, including integration tests","\u002Fprojects\u002Fphp-booster.webp","PHP \u002F Shell",{},4,"\u002Fprojects\u002Fphp-booster","https:\u002F\u002Fgithub.com\u002FTerrorSquad\u002Fphp-booster",{"title":858,"description":931},[949,950,951,952,953,119,121,954],"PHP","PHPStan","Psalm","Rector","ECS","Python","projects\u002Fphp-booster","A PHP quality toolchain, packaged",[958,959,949,960],"Developer Experience","Standards","Automation","Zw-hgoMSsYZXEskH9x0dPibkd6ZTDJRLqPpNdGYLheo",{"id":963,"title":964,"body":965,"demo":93,"description":1028,"docs":1029,"draft":96,"extension":97,"highlights":1030,"image":1040,"language":1041,"meta":1042,"navigation":111,"order":1043,"path":1044,"repo":1045,"seo":1046,"site":93,"stack":1047,"stem":1053,"tagline":1054,"tags":1055,"__hash__":1058},"projects\u002Fprojects\u002Fgriffin.md","Griffin",{"type":8,"value":966,"toc":1023},[967,971,978,981,985,991,1002,1006],[11,968,970],{"id":969},"the-premise","The premise",[16,972,973,974,977],{},"A development machine accumulates state. Six months of ",[23,975,976],{},"apt install",", a dotfile\nedited at 1am, a PATH entry nobody remembers adding. Then the laptop dies, or a\nnew person joins, and the setup is archaeology.",[16,979,980],{},"Griffin makes the workstation reproducible: the machine is described in version\ncontrol, and rebuilding it is a command rather than a weekend.",[11,982,984],{"id":983},"why-it-earns-its-place-here","Why it earns its place here",[16,986,987,990],{},[35,988,989],{},"It is tested on three platforms in continuous integration."," Ubuntu, WSL, and\nmacOS each have their own workflow that runs the playbook on a clean runner. An\nAnsible role that works on the author's machine is a script; one that provisions\na fresh Ubuntu runner, a WSL environment, and both Intel and Apple Silicon macOS\nis infrastructure.",[16,992,993,994,997,998,1001],{},"That is also the hard part. The same intent - install this tool, set this\npreference - is ",[23,995,996],{},"apt"," here, ",[23,999,1000],{},"homebrew"," there, and a registry-adjacent quirk in\nWSL. The playbooks abstract the intent and branch on the platform, rather than\nforking into three copies that drift.",[11,1003,1005],{"id":1004},"structure","Structure",[43,1007,1008,1014,1017,1020],{},[46,1009,1010,1013],{},[35,1011,1012],{},"65 YAML task files",", organized by concern, each idempotent, so re-running\nconverges rather than duplicating",[46,1015,1016],{},"Separate playbooks for Linux and macOS sharing common roles",[46,1018,1019],{},"Configuration import for KDE and editor settings",[46,1021,1022],{},"Linting via reviewdog in CI, documentation built and published automatically,\nreleases managed by release-please",{"title":87,"searchDepth":88,"depth":88,"links":1024},[1025,1026,1027],{"id":969,"depth":88,"text":970},{"id":983,"depth":88,"text":984},{"id":1004,"depth":88,"text":1005},"Ansible playbooks that take a bare Linux or macOS machine to a fully configured development workstation, verified on every push across Ubuntu, WSL, and macOS runners.","https:\u002F\u002Fterrorsquad.github.io\u002Fgriffin\u002F",[1031,1034,1037],{"value":1032,"label":1033},"3","platforms tested in CI: Ubuntu, WSL, macOS",{"value":1035,"label":1036},"65","YAML task files, idempotent by construction",{"value":1038,"label":1039},"0","manual steps to rebuild a machine","\u002Fprojects\u002Fgriffin.webp","Ansible \u002F YAML",{},5,"\u002Fprojects\u002Fgriffin","https:\u002F\u002Fgithub.com\u002FTerrorSquad\u002Fgriffin",{"title":964,"description":1028},[1048,1049,1050,1051,121,122,1052],"Ansible","YAML","Bash","Zsh","WSL","projects\u002Fgriffin","A dev workstation from bare metal, reproducibly",[1056,960,1057,708],"Infrastructure","Cross-platform","KrFgIZz-l4yg0iC3HU1RZLZ9PIu9FYbjspaslw6gBmo",{"id":1060,"title":1061,"body":1062,"demo":1181,"description":1182,"docs":93,"draft":96,"extension":97,"highlights":1183,"image":1193,"language":246,"meta":1194,"navigation":111,"order":1195,"path":1196,"repo":1197,"seo":1198,"site":93,"stack":1199,"stem":1202,"tagline":1203,"tags":1204,"__hash__":1207},"projects\u002Fprojects\u002Fsudoku.md","Sudoku Pro",{"type":8,"value":1063,"toc":1176},[1064,1068,1071,1078,1085,1089,1092,1166,1169,1173],[11,1065,1067],{"id":1066},"the-interesting-problem","The interesting problem",[16,1069,1070],{},"Writing a Sudoku solver is a known exercise: backtracking search finishes in\nmilliseconds. But a solver that brute-forces the answer cannot teach anyone\nanything, because \"the answer is 7 because I tried every branch\" is not a\nreason.",[16,1072,1073,1074,1077],{},"So the engine here solves the way a person does. It models candidates per cell\nand applies human techniques in ascending order of difficulty - naked single,\nhidden single, then pointing pairs, box\u002Fline reduction, and further - returning\nthe ",[141,1075,1076],{},"simplest"," technique that makes progress on the current board, along with\nthe cells that justify it.",[16,1079,1080,1081,1084],{},"That constraint, ",[35,1082,1083],{},"explainability over speed",", is what makes the design\nnon-trivial. The hint system and the difficulty grader fall out of it: a puzzle's\nrating is the hardest technique required to solve it without guessing.",[11,1086,1088],{"id":1087},"where-the-tests-are","Where the tests are",[16,1090,1091],{},"The logic is separated from the UI precisely so it can be tested as logic. Six\nunit-tested modules cover the core:",[1093,1094,1095,1108],"table",{},[1096,1097,1098],"thead",{},[1099,1100,1101,1105],"tr",{},[1102,1103,1104],"th",{},"Module",[1102,1106,1107],{},"What it proves",[1109,1110,1111,1126,1136,1146,1156],"tbody",{},[1099,1112,1113,1123],{},[1114,1115,1116,1119,1120],"td",{},[23,1117,1118],{},"sudokuCore"," \u002F ",[23,1121,1122],{},"sudokuEngine",[1114,1124,1125],{},"Constraint propagation and validity",[1099,1127,1128,1133],{},[1114,1129,1130],{},[23,1131,1132],{},"sudokuGrader",[1114,1134,1135],{},"Difficulty rating matches required technique",[1099,1137,1138,1143],{},[1114,1139,1140],{},[23,1141,1142],{},"nextHint",[1114,1144,1145],{},"The right technique is chosen, with correct justification",[1099,1147,1148,1153],{},[1114,1149,1150],{},[23,1151,1152],{},"score",[1114,1154,1155],{},"Scoring and streak rules",[1099,1157,1158,1163],{},[1114,1159,1160],{},[23,1161,1162],{},"dailyPuzzle",[1114,1164,1165],{},"Deterministic daily generation",[16,1167,1168],{},"Four Playwright specs cover the parts unit tests cannot: game flow, pausing,\npreferences, and resuming an interrupted session.",[11,1170,1172],{"id":1171},"the-rest","The rest",[16,1174,1175],{},"Installable PWA that works offline, keyboard-driven controls, pencil-mark notes\nwith auto-fill, undo, and a bilingual interface. Deployed to GitHub Pages on\nevery push.",{"title":87,"searchDepth":88,"depth":88,"links":1177},[1178,1179,1180],{"id":1066,"depth":88,"text":1067},{"id":1087,"depth":88,"text":1088},{"id":1171,"depth":88,"text":1172},"https:\u002F\u002Fterrorsquad.github.io\u002Fsudoku\u002F","A Sudoku game that teaches you to solve Sudoku. A constraint-propagation engine detects which human technique applies to the current board and walks through the reasoning, step by step.",[1184,1187,1190],{"value":1185,"label":1186},"6","unit-tested modules: engine, grader, hints, scoring",{"value":1188,"label":1189},"8+","solving techniques, from naked single upward",{"value":1191,"label":1192},"PWA","installable, works offline","\u002Fprojects\u002Fsudoku.webp",{},6,"\u002Fprojects\u002Fsudoku","https:\u002F\u002Fgithub.com\u002FTerrorSquad\u002Fsudoku",{"title":1061,"description":1182},[253,1200,246,1201,587,1191],"Vue 3","Vitest","projects\u002Fsudoku","A solver that explains itself",[1205,246,128,1206],"Algorithms","Vue","y6oALmqADy3WmQ6gRGDNv76FOBbn-H-OB8OArn-RVrQ",1788019316917]