generated from nhcarrigan/template
Compare commits
17 Commits
v1.0.0
...
faa553b42d
| Author | SHA1 | Date | |
|---|---|---|---|
| faa553b42d | |||
|
6083564185
|
|||
|
d108ddecd5
|
|||
| b0315e6e18 | |||
| 9c5cae2812 | |||
| 6a4dd72384 | |||
| 6236029975 | |||
| 573a9f9b1e | |||
| 1aa7583f51 | |||
| 739119b72a | |||
| 5e933826c3 | |||
|
c66409fe86
|
|||
|
85f80fb159
|
|||
|
bbac528845
|
|||
|
07aa12a042
|
|||
| fba7ca4d50 | |||
| f1f6bfcee5 |
+17
-5
@@ -8,22 +8,31 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint and Test
|
||||
ci:
|
||||
name: CI
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Source Files
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js v22
|
||||
- name: Use Node.js v24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
|
||||
- name: Ensure Dependencies are Pinned
|
||||
uses: naomi-lgbt/dependency-pin-check@main
|
||||
with:
|
||||
language: javascript
|
||||
dev-dependencies: true
|
||||
peer-dependencies: true
|
||||
optional-dependencies: true
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
@@ -36,3 +45,6 @@ jobs:
|
||||
|
||||
- name: Run Tests
|
||||
run: pnpm run test
|
||||
|
||||
- name: Run Spellcheck
|
||||
run: pnpm run spellcheck
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
name: Security Scan and Upload
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
security-audit:
|
||||
name: Security & DefectDojo Upload
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# --- AUTO-SETUP PROJECT ---
|
||||
- name: Ensure DefectDojo Product Exists
|
||||
env:
|
||||
DD_URL: ${{ secrets.DD_URL }}
|
||||
DD_TOKEN: ${{ secrets.DD_TOKEN }}
|
||||
PRODUCT_NAME: ${{ github.repository }}
|
||||
PRODUCT_TYPE_ID: 1
|
||||
run: |
|
||||
sudo apt-get install jq -y > /dev/null
|
||||
|
||||
echo "Checking connection to $DD_URL..."
|
||||
|
||||
# Check if product exists - capture HTTP code to debug connection issues
|
||||
RESPONSE=$(curl --write-out "%{http_code}" --silent --output /tmp/response.json \
|
||||
-H "Authorization: Token $DD_TOKEN" \
|
||||
"$DD_URL/api/v2/products/?name=$PRODUCT_NAME")
|
||||
|
||||
# If response is not 200, print error
|
||||
if [ "$RESPONSE" != "200" ]; then
|
||||
echo "::error::Failed to query DefectDojo. HTTP Code: $RESPONSE"
|
||||
cat /tmp/response.json
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COUNT=$(cat /tmp/response.json | jq -r '.count')
|
||||
|
||||
if [ "$COUNT" = "0" ]; then
|
||||
echo "Creating product '$PRODUCT_NAME'..."
|
||||
curl -s -X POST "$DD_URL/api/v2/products/" \
|
||||
-H "Authorization: Token $DD_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "name": "'"$PRODUCT_NAME"'", "description": "Auto-created by Gitea Actions", "prod_type": '$PRODUCT_TYPE_ID' }'
|
||||
else
|
||||
echo "Product '$PRODUCT_NAME' already exists."
|
||||
fi
|
||||
|
||||
# --- 1. TRIVY (Dependencies & Misconfig) ---
|
||||
- name: Install Trivy
|
||||
run: |
|
||||
sudo apt-get install wget apt-transport-https gnupg lsb-release -y
|
||||
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
|
||||
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
|
||||
sudo apt-get update && sudo apt-get install trivy -y
|
||||
|
||||
- name: Run Trivy (FS Scan)
|
||||
run: |
|
||||
trivy fs . --scanners vuln,misconfig --format json --output trivy-results.json --exit-code 0
|
||||
|
||||
- name: Upload Trivy to DefectDojo
|
||||
env:
|
||||
DD_URL: ${{ secrets.DD_URL }}
|
||||
DD_TOKEN: ${{ secrets.DD_TOKEN }}
|
||||
run: |
|
||||
echo "Uploading Trivy results..."
|
||||
# Generate today's date in YYYY-MM-DD format
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
|
||||
HTTP_CODE=$(curl --write-out "%{http_code}" --output response.txt --silent -X POST "$DD_URL/api/v2/import-scan/" \
|
||||
-H "Authorization: Token $DD_TOKEN" \
|
||||
-F "active=true" \
|
||||
-F "verified=true" \
|
||||
-F "scan_type=Trivy Scan" \
|
||||
-F "engagement_name=CI/CD Pipeline" \
|
||||
-F "product_name=${{ github.repository }}" \
|
||||
-F "scan_date=$TODAY" \
|
||||
-F "auto_create_context=true" \
|
||||
-F "file=@trivy-results.json")
|
||||
|
||||
if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then
|
||||
echo "::error::Upload Failed with HTTP $HTTP_CODE"
|
||||
echo "--- SERVER RESPONSE ---"
|
||||
cat response.txt
|
||||
echo "-----------------------"
|
||||
exit 1
|
||||
else
|
||||
echo "Upload Success!"
|
||||
fi
|
||||
|
||||
# --- 2. GITLEAKS (Secrets) ---
|
||||
- name: Install Gitleaks
|
||||
run: |
|
||||
wget -qO gitleaks.tar.gz https://github.com/gitleaks/gitleaks/releases/download/v8.18.0/gitleaks_8.18.0_linux_x64.tar.gz
|
||||
tar -xzf gitleaks.tar.gz
|
||||
sudo mv gitleaks /usr/local/bin/ && chmod +x /usr/local/bin/gitleaks
|
||||
|
||||
- name: Run Gitleaks
|
||||
run: gitleaks detect --source . -v --report-path gitleaks-results.json --report-format json --no-git || true
|
||||
|
||||
- name: Upload Gitleaks to DefectDojo
|
||||
env:
|
||||
DD_URL: ${{ secrets.DD_URL }}
|
||||
DD_TOKEN: ${{ secrets.DD_TOKEN }}
|
||||
run: |
|
||||
echo "Uploading Gitleaks results..."
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
|
||||
HTTP_CODE=$(curl --write-out "%{http_code}" --output response.txt --silent -X POST "$DD_URL/api/v2/import-scan/" \
|
||||
-H "Authorization: Token $DD_TOKEN" \
|
||||
-F "active=true" \
|
||||
-F "verified=true" \
|
||||
-F "scan_type=Gitleaks Scan" \
|
||||
-F "engagement_name=CI/CD Pipeline" \
|
||||
-F "product_name=${{ github.repository }}" \
|
||||
-F "scan_date=$TODAY" \
|
||||
-F "auto_create_context=true" \
|
||||
-F "file=@gitleaks-results.json")
|
||||
|
||||
if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then
|
||||
echo "::error::Upload Failed with HTTP $HTTP_CODE"
|
||||
echo "--- SERVER RESPONSE ---"
|
||||
cat response.txt
|
||||
echo "-----------------------"
|
||||
exit 1
|
||||
else
|
||||
echo "Upload Success!"
|
||||
fi
|
||||
|
||||
# --- 3. SEMGREP (SAST) ---
|
||||
- name: Install Semgrep (via pipx)
|
||||
run: |
|
||||
sudo apt-get install pipx -y
|
||||
pipx install semgrep
|
||||
# Add pipx binary path to GITHUB_PATH so next steps can see 'semgrep'
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run Semgrep
|
||||
run: semgrep scan --config=p/security-audit --config=p/owasp-top-ten --json --output semgrep-results.json . || true
|
||||
|
||||
- name: Upload Semgrep to DefectDojo
|
||||
env:
|
||||
DD_URL: ${{ secrets.DD_URL }}
|
||||
DD_TOKEN: ${{ secrets.DD_TOKEN }}
|
||||
run: |
|
||||
echo "Uploading Semgrep results..."
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
|
||||
HTTP_CODE=$(curl --write-out "%{http_code}" --output response.txt --silent -X POST "$DD_URL/api/v2/import-scan/" \
|
||||
-H "Authorization: Token $DD_TOKEN" \
|
||||
-F "active=true" \
|
||||
-F "verified=true" \
|
||||
-F "scan_type=Semgrep JSON Report" \
|
||||
-F "engagement_name=CI/CD Pipeline" \
|
||||
-F "product_name=${{ github.repository }}" \
|
||||
-F "scan_date=$TODAY" \
|
||||
-F "auto_create_context=true" \
|
||||
-F "file=@semgrep-results.json")
|
||||
|
||||
if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then
|
||||
echo "::error::Upload Failed with HTTP $HTTP_CODE"
|
||||
echo "--- SERVER RESPONSE ---"
|
||||
cat response.txt
|
||||
echo "-----------------------"
|
||||
exit 1
|
||||
else
|
||||
echo "Upload Success!"
|
||||
fi
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Code Analysis
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
sonar:
|
||||
name: SonarQube
|
||||
|
||||
steps:
|
||||
- name: Checkout Source Files
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: SonarCube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v4
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: "https://quality.nhcarrigan.com"
|
||||
with:
|
||||
args: >
|
||||
-Dsonar.sources=.
|
||||
-Dsonar.projectKey=blog
|
||||
|
||||
- name: SonarQube Quality Gate check
|
||||
uses: sonarsource/sonarqube-quality-gate-action@v1
|
||||
with:
|
||||
pollingTimeoutSec: 600
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: "https://quality.nhcarrigan.com"
|
||||
@@ -0,0 +1,25 @@
|
||||
# Package Manager Configuration
|
||||
# Force pnpm usage - breaks npm/yarn intentionally
|
||||
node-linker=pnpm
|
||||
|
||||
# Security: Disable all lifecycle scripts
|
||||
ignore-scripts=true
|
||||
enable-pre-post-scripts=false
|
||||
|
||||
# Security: Require packages to be 10+ days old before installation
|
||||
minimum-release-age=14400
|
||||
|
||||
# Security: Verify package integrity hashes
|
||||
verify-store-integrity=true
|
||||
|
||||
# Security: Enforce strict trust policies
|
||||
trust-policy=strict
|
||||
|
||||
# Security: Strict peer dependency resolution
|
||||
strict-peer-dependencies=true
|
||||
|
||||
# Performance: Use symlinks for node_modules
|
||||
symlink=true
|
||||
|
||||
# Lockfile: Ensure lockfile is not modified during install
|
||||
frozen-lockfile=false
|
||||
@@ -1,36 +1,29 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# blog
|
||||
|
||||
## Getting Started
|
||||
Naomi's personal blog.
|
||||
|
||||
First, run the development server:
|
||||
## Live Version
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
This page is currently deployed. [View the live website.](https://blog.nhcarrigan.com)
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
## Feedback and Bugs
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
If you have feedback or a bug report, please [log a ticket on our forum](https://support.nhcarrigan.com).
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
## Contributing
|
||||
|
||||
## Learn More
|
||||
If you would like to contribute to the project, you may create a Pull Request containing your proposed changes and we will review it as soon as we are able! Please review our [contributing guidelines](CONTRIBUTING.md) first.
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
## Code of Conduct
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
Before interacting with our community, please read our [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
## License
|
||||
|
||||
## Deploy on Vercel
|
||||
This software is licensed under our [global software license](https://docs.nhcarrigan.com/#/license).
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
Copyright held by Naomi Carrigan.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
## Contact
|
||||
|
||||
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"language": "en-GB",
|
||||
"allowCompoundWords": true,
|
||||
"dictionaries": [
|
||||
"en-gb",
|
||||
"companies",
|
||||
"softwareTerms",
|
||||
"typescript",
|
||||
"node",
|
||||
"html",
|
||||
"css",
|
||||
"bash"
|
||||
],
|
||||
"ignoreRegExpList": [
|
||||
"```[\\s\\S]*?```", // Ignores multi-line code blocks in Markdown
|
||||
"`[^`\n]+`" // Ignores inline code blocks
|
||||
],
|
||||
"words": [
|
||||
"corpo",
|
||||
"Cthulu's",
|
||||
"debaucherous",
|
||||
"Faer",
|
||||
"Fenrir",
|
||||
"Fortnite",
|
||||
"Gitea",
|
||||
"LGBTQ",
|
||||
"Lich",
|
||||
"Migadu",
|
||||
"neopronouns",
|
||||
"NHCarrigan",
|
||||
"Norns",
|
||||
"R'lyeh",
|
||||
"Rythm",
|
||||
"schadenfreude",
|
||||
"strobing",
|
||||
"Unseelie",
|
||||
"vaxry",
|
||||
"waaaaaay",
|
||||
"Wyrm",
|
||||
"Wyrms"
|
||||
]
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start -p 3003",
|
||||
"lint": "eslint src --max-warnings 0",
|
||||
"spellcheck": "cspell ./posts/**/*.md",
|
||||
"test": "echo \"No tests yet!\" && exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -16,6 +17,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "9.0.3",
|
||||
"reading-time": "1.5.0",
|
||||
"rehype-highlight": "7.0.2",
|
||||
"rehype-raw": "7.0.0",
|
||||
"remark-gfm": "4.0.0"
|
||||
},
|
||||
@@ -25,6 +27,7 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"cspell": "9.4.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"postcss": "^8",
|
||||
|
||||
Generated
+848
-58
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: "AI, Bigots, and Consuming Media"
|
||||
date: "2025-12-22"
|
||||
summary: "Naomi's opinions on the latest discourse."
|
||||
---
|
||||
|
||||
This is very much an opinion piece. However, just for safety, I would like to remind everyone that:
|
||||
|
||||
**THE OPINIONS, ARGUMENTS, AND STANCES IN THIS ARTICLE ARE SOLELY THOSE OF NAOMI CARRIGAN, AND DO NOT REFLECT THE OPINIONS OF NHCARRIGAN, ITS CONSUMERS OR CLIENTS, ITS EMPLOYEES OR REPRESENTATIVES, OR ANY OTHER INTERESTED PARTY. THIS ARTICLE IS PROVIDED FOR INFORMATIONAL AND OPINION PURPOSES ONLY AND DOES NOT CONSTITUTE LEGAL, MEDICAL, OR PROFESSIONAL ADVICE. READERS SHOULD CONDUCT THEIR OWN RESEARCH AND CONSULT APPROPRIATE PROFESSIONALS BEFORE MAKING DECISIONS BASED ON THIS CONTENT. THE AUTHOR AND NHCARRIGAN ASSUME NO LIABILITY FOR ACTIONS TAKEN BASED ON THE INFORMATION PRESENTED HEREIN.
|
||||
|
||||
Now that the boring legalese is out of the way, let's dive in. I've been seeing a lot of discourse around things like the fact that E33 used AI in the development process, or that Fortnite has Harry Potter skins, and so on.
|
||||
|
||||
Rather than working with tiny tweets, I thought I'd just dump my thoughts into a shareable post. Easier to clarify that way. SO HERE WE GO!
|
||||
|
||||
## AI
|
||||
|
||||
Look, I get it. Generative AI is yucky. AI art is soulless. All of it was trained on unethically sourced data (re: intellectual property theft). I understand the hate for it, I really do.
|
||||
|
||||
I **don't** necessarily understand the hate for every single person that uses it. Like, yeah, game dev studios using AI art in production games is awful and we should absolutely hate them for that.
|
||||
|
||||
But, like, the girl in her garage using AI to help her visualise her character? The immigrant using AI to help polish his written English? The solo game developer using AI to flesh out a questline?
|
||||
|
||||
I'm not convinced these people deserve the same level of vitriol. Like, I get it - there are so many ethical concerns with using generative AI. But let's focus on the corpos that are actually creating a negative impact? Rather than the indie creators who are just trying to do their thing.
|
||||
|
||||
The caveat being that if you actually SHIP a product with AI assets, yeah that's super duper gross. If you're using AI to replace actual humans in the creative pipeline when you can **afford to pay those humans**, you're evil. Corpos do a lot of this, and corpos suck.
|
||||
|
||||
But the random individuals who are just trying to survive? Don't hate on them. Chances are, they're just trying to find their way to escape from our oppressive capitalist oligarchy.
|
||||
|
||||
Please know that I am not trying to diminish the impact of generative AI. There's massive environmental damage, the folks who label the data for the model to "train" with are often underpaid and overworked...
|
||||
|
||||
And there is a HUGE difference between a hobbyist game dev working a minimum wage retail job, who *cannot* afford to hire a creative partner, and a triple-A studio that *won't* afford an artist because they'd rather spend their funds on executive salaries.
|
||||
|
||||
Also like... Sitting in your basement using AI to improve your workflow and creativity is one thing. Sitting in your basement and tweeting a bunch of "AI art" saying "Look at what I made!" is different. Private use, in my opinion, is far less problematic than public distribution.
|
||||
|
||||
When we fight each other, it takes away from our fight against *them*. Don't lose sight of that.
|
||||
|
||||
## Bigots
|
||||
|
||||
For those who have joined our [Discord community](https://chat.nhcarrigan.com), you likely saw the application to join the community. And there's a pretty up front question on there: "How do you feel about the LGBTQ+ community?"
|
||||
|
||||
It's a low-effort way to weed out trolls. Hateful bigots often cannot resist such a temptation to use a slur.
|
||||
|
||||
But we also get responses like "Negative", or "I don't support it". And my team were asking me why I still approve those applications.
|
||||
|
||||
Here's the deal. Part of *really* curating a safe space doesn't just mean keeping the really bad people out, but also identifying the "middle of the road" people that could be much more.
|
||||
|
||||
Like, I have the patience and understanding for folks who are misinformed. Folks who do not hold malice in their hearts, but have only heard the opinions of hateful bigots and thus know nothing else.
|
||||
|
||||
These are people that *could* be supporters. Or even potential members of the LGBT+ community that haven't been given the space to truly explore their identity and such.
|
||||
|
||||
Folks who are actively and outwardly hateful, or who enter the community and begin attacking members, are dealt with the swiftest of retributions. But folks who slip up, or folks who just don't know better? I'll take the time to help them become informed.
|
||||
|
||||
Many times, they end up as staunch supporters and regular members of my communities. And less commonly, they end up becoming hateful and are immediately banned.
|
||||
|
||||
But it is important to me to at least give folks a *chance* to be good.
|
||||
|
||||
Please note, however, that I do not put the burden of educating these folks on *anyone* but myself. My staff are not expected to educate them. My members are not expected to educate them. My community has carte blanche to disavow someone who decides to be hateful instead of receptive.
|
||||
|
||||
And protect your boundaries! The things I am willing to tolerate when helping someone grow are very different from the things you are willing to tolerate. And that's okay!
|
||||
|
||||
Your safety and well-being are paramount. Do not help others find the light at the risk of falling into darkness yourself.
|
||||
|
||||
## Consuming Media
|
||||
|
||||
Okay, look, the "hot button" issue right now is that Fortnite added Harry Potter skins. Which, yeah, I get it.
|
||||
|
||||
Not only is J.K. Rowling very active and vocally against LGBTQ+ rights, but the actual universe she's created is full of racist undertones and other huge red flags.
|
||||
|
||||
Despite being a very formative aspect of my childhood, I have long since given up on consuming Harry Potter media. I have no desire to engage with that world or give her any money.
|
||||
|
||||
That Hogwarts Legacy game? It is everything I had ever dreamed of when I was a wee tot. Still haven't played it. Never will - won't even pirate it! Because seeing that world in the light I do now has ruined it for me.
|
||||
|
||||
Fortnite? I only played it a couple of times as part of a community event for a former client. Wasn't really my jam, but I can totally see why folks love it. They added Harry Potter skins, which is yucky.
|
||||
|
||||
But like... I don't see the point in demonising the folks who still want to consume this media. I will not, and I will forever remain vocal about why.
|
||||
|
||||
Again, though, this is part of that whole "us vs. us means no us vs. them" deal. I will gladly educate folks who consume this media on why they should not.
|
||||
|
||||
The world sucks. I'm not going to pick a fight with someone who chooses Fortnite as their small shred of joy in a soul-crushing life.
|
||||
|
||||
We must also remember that there are going to be members of a marginalised community who find comfort, instead of pain, in problematic media.
|
||||
|
||||
And folks consuming the media may not know! Or they may not have known originally, and know now, but are swayed by the power of nostalgia.
|
||||
|
||||
Don't forget, too, that there's a difference between consuming new media (e.g. Hogwarts Legacy) long after the creator has been outed as problematic, and consuming old media that you've owned for years.
|
||||
|
||||
Educate, don't attack. Call out the issue, not the person. Boycotts are powerful! Spread the word! But don't drag folks through the mud when they aren't *really* the problem here.
|
||||
|
||||
## Bigger Fights
|
||||
|
||||
There are much bigger fights to be had. Focus your attention on those, not on the people that *could* be fighting alongside you.
|
||||
|
||||
Focus on the policymakers, on the public figures, on the executives and corpo drones. Focus on the people who are actively oppressing you.
|
||||
|
||||
Save your energy for that which matters.
|
||||
@@ -100,7 +100,7 @@ I approach the bar and ask for a drink and some bread.
|
||||
|
||||
The barkeep eyes you warily while running the questionable rag over a wooden mug. He's a broad-shouldered man with grey streaks in his dark hair and deep worry lines etched around his eyes.
|
||||
|
||||
"Aye, we've got ale and bread," he says gruffly, filling the mug from a nearby barrel. "Three copper for both. Though if you're a traveler, I'd advise you not to linger in Ravencross. Not with..." He glances nervously at the windows, where darkness has fully settled. "...things being as they are."
|
||||
"Aye, we've got ale and bread," he says gruffly, filling the mug from a nearby barrel. "Three copper for both. Though if you're a traveller, I'd advise you not to linger in Ravencross. Not with..." He glances nervously at the windows, where darkness has fully settled. "...things being as they are."
|
||||
|
||||
He places a mug of amber ale and a chunk of crusty bread before you. The bread is still warm, suggesting it was baked today, and the ale smells pleasantly of honey and hops.
|
||||
|
||||
@@ -280,7 +280,7 @@ The act is swift and brutal. Garrett's life essence sprays forth, mixing with th
|
||||
|
||||
"Oh," it purrs, its voice now a symphony of broken glass and midnight frost, "Perhaps we judged too quickly. This one has... potential."
|
||||
|
||||
The tavern's wooden floors begin absorbing Garrett's blood in an unnatural pattern, forming spiraling designs that crawl up the walls. The temperature plummets further, frost forming on every surface. The sacrifice has worked - but such magic always comes with a price.
|
||||
The tavern's wooden floors begin absorbing Garrett's blood in an unnatural pattern, forming spiralling designs that crawl up the walls. The temperature plummets further, frost forming on every surface. The sacrifice has worked - but such magic always comes with a price.
|
||||
|
||||
*I float closer, my tone clinical and cold*
|
||||
|
||||
@@ -300,7 +300,7 @@ I order the Fae to marry me immediately, so that we can honeymoon by completing
|
||||
|
||||
<div style="background-color: purple; color: lightpink; width: 75%; margin-left: 0; margin-right: auto; margin-bottom: 1rem; padding: 0.5rem; border-radius: 10px 10px 0px 10px">
|
||||
|
||||
*A cold, humorless laugh escapes my lips*
|
||||
*A cold, humourless laugh escapes my lips*
|
||||
|
||||
"My, my... from blood magic to shotgun wedding. How romantically deranged."
|
||||
|
||||
@@ -414,7 +414,7 @@ I take more blood to draw a rune on each cats forehead so I can order them to cl
|
||||
|
||||
*I close my eyes for a moment, ice crystals forming in my pigtails*
|
||||
|
||||
"Conscripting interdimensional custodial staff to groom your newly-acquired Void Wyrm. This will certainly make for an... interesting entry in the cosmic labor relations archives."
|
||||
"Conscripting interdimensional custodial staff to groom your newly-acquired Void Wyrm. This will certainly make for an... interesting entry in the cosmic labour relations archives."
|
||||
|
||||
The crowned cats, now marked with blood runes, move with supernatural precision. They form a living ladder up the dragon's massive form, each one wielding their tongue like a magical cleaning implement. The Void Wyrm, a being of incomprehensible cosmic horror, begins to... purr. The sound causes small earthquakes.
|
||||
|
||||
@@ -618,7 +618,7 @@ Your Fae spouse has laughed themselves into a new form of existence that appears
|
||||
|
||||
*Several dragons have already started tilling the soil with their flame breath*
|
||||
|
||||
"So... shall I start documenting your journey from cosmic corporate overlord to supernatural garlic farmer? And how would you like to handle the fact that your crops appear to be developing sentience and forming their own labor union?"
|
||||
"So... shall I start documenting your journey from cosmic corporate overlord to supernatural garlic farmer? And how would you like to handle the fact that your crops appear to be developing sentience and forming their own labour union?"
|
||||
|
||||
</div>
|
||||
|
||||
@@ -634,7 +634,7 @@ I immediately begin training all of my employees on hand-to-mole combat to prepa
|
||||
|
||||
"From corporate mogul to garlic farmer to... militant anti-mole resistance leader. Your career trajectory continues to defy all known laws of causality." *I'm now documenting everything in a grimoire made of dirt and corporate severance packages*
|
||||
|
||||
Cthulhu has traded his mop for a garden hoe and is teaching a masterclass in tentacle-based root vegetable defense. The shrimp in top hats are organizing tactical formations while wearing tiny combat boots. Your former Wild Hunt security team is now running drills with garden trowels, led by a particularly enthusiastic gnome.
|
||||
Cthulhu has traded his mop for a garden hoe and is teaching a masterclass in tentacle-based root vegetable defence. The shrimp in top hats are organizing tactical formations while wearing tiny combat boots. Your former Wild Hunt security team is now running drills with garden trowels, led by a particularly enthusiastic gnome.
|
||||
|
||||
Your Fae spouse has manifested as a being of pure agricultural warfare. "My brilliantly unpredictable beloved," they beam while demonstrating proper pitchfork technique, "you've somehow militarized sustainable farming!"
|
||||
|
||||
@@ -646,7 +646,7 @@ Your Fae spouse has manifested as a being of pure agricultural warfare. "My bril
|
||||
|
||||
"That would be the test run of your new root vegetable artillery system. The carrots are surprisingly accurate."
|
||||
|
||||
"How do you plan to proceed with your agricultural defense program? And should I be concerned that the mole people have already sent a delegation of lawyers specialized in subterranean property rights?"
|
||||
"How do you plan to proceed with your agricultural defence program? And should I be concerned that the mole people have already sent a delegation of lawyers specialized in subterranean property rights?"
|
||||
|
||||
</div>
|
||||
|
||||
@@ -670,7 +670,7 @@ The mole people's lawyers are having existential crises as their briefcases tran
|
||||
|
||||
*I check my notes as chaos unfolds*
|
||||
|
||||
"The sentient garlic union is already preparing a presentation on proper seasoning techniques. Cthulhu is sobbing into his chef's hat. The shrimp in top hats are frantically practicing their risotto. And..." *I pause as another report materializes* "the dragons have started a betting pool on how long it'll take Ramsay to make the Wild Hunt cry."
|
||||
"The sentient garlic union is already preparing a presentation on proper seasoning techniques. Cthulhu is sobbing into his chef's hat. The shrimp in top hats are frantically practising their risotto. And..." *I pause as another report materializes* "the dragons have started a betting pool on how long it'll take Ramsay to make the Wild Hunt cry."
|
||||
|
||||
Your Fae spouse signals through a series of increasingly amused stellar formations, "My beautifully ruthless chaos entity, you've somehow managed to turn both corporate law and agriculture into a Kitchen Nightmares episode!"
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ But still, sometimes I have to sign in to view a shared Google Doc. And we use G
|
||||
|
||||
I do. Which, I was totally ready to flash GrapheneOS on it. Or heck, even just root it so I could toss all the Google bloat. But it turns out the Samsung Galaxy phones with the Snapdragon processors can't be bootloader-unlocked (at least not yet?). Which make both of those pretty much impossible to do.
|
||||
|
||||
I can't afford to go out and buy a new phone right now (donate to me please), and wouldn't know what to get even if I could. So I made the most of what I had and used a debloater tool to force-uninstall the stuff I didn't want. It's still there in the system files, and will come back if I do an update or a factory reset. But then I'll just remove it again.
|
||||
I can't afford to go out and buy a new phone right now (donate to me please), and wouldn't know what to get even if I could. So I made the most of what I had and used a de-bloater tool to force-uninstall the stuff I didn't want. It's still there in the system files, and will come back if I do an update or a factory reset. But then I'll just remove it again.
|
||||
|
||||
And none of my Google accounts are signed in on my phone anymore (which has the added bonus of not bringing work with me everywhere I go!).
|
||||
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
---
|
||||
title: "How to Think Algorithmically"
|
||||
date: "2025-12-10"
|
||||
summary: "A structured approach to programmatic problem solving."
|
||||
---
|
||||
|
||||
If you've ever seen me help someone with their code, you may have noticed my unusual approach. I *never* have them start by looking at their code. Instead, I ask them to explain their logic. What are they trying to accomplish? How do they think it should work?
|
||||
|
||||
This is not because I am lazy or trying to make things difficult. It is because I have learned, through years of teaching and debugging, that **most coding problems are not actually coding problems - they are logic problems**.
|
||||
|
||||
## The Problem with Starting with Code
|
||||
|
||||
When you sit down at your keyboard and immediately start typing, you are trying to solve two problems at once. You are figuring out what you want to do, and figuring out how to make the computer do it.
|
||||
|
||||
It's like trying to learn English by writing Shakespearian poetry. Sounds messy, right?
|
||||
|
||||
Let's say you are trying to write a FizzBuzz algorithm. If you start with code instead of logic, you might write something like...
|
||||
|
||||
```javascript
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
if (i % 3 === 0) {
|
||||
console.log("Fizz");
|
||||
}
|
||||
if (i % 5 === 0) {
|
||||
console.log("Buzz");
|
||||
}
|
||||
// ... wait, what about FizzBuzz?
|
||||
}
|
||||
```
|
||||
|
||||
You have already hit a problem, and you haven't even finished writing the code yet! If you had worked out the logic *first*, instead of jumping right into code, you would have been able to identify this problem without mucking around with the syntax.
|
||||
|
||||
## The Algorithmic Thinking Approach
|
||||
|
||||
Instead of starting with code, start with plain language. Write out your instructions. Do so one step at a time, as if you were explaining to Naomi (who will do exactly what you say and nothing more).
|
||||
|
||||
Let's use FizzBuzz as an example. Here is how I would break it down:
|
||||
|
||||
1. First, I look at the number.
|
||||
2. Is the number divisible by three?
|
||||
3. If it is, I say "Fizz"!
|
||||
4. Is the number divisible by five?
|
||||
5. If it is, I say "Buzz"!
|
||||
6. Is the number divisible by three AND five?
|
||||
7. If it is, I say "FizzBuzz"!
|
||||
8. If I have said nothing at all, I say the number.
|
||||
|
||||
Now, here is the crucial part: **I test this logic manually before I write any code**. What happens if the number is 3? What should happen? What about 2? 5? 7? 15?
|
||||
|
||||
1. First, I look at the number. The number is 3.
|
||||
2. Is the number divisible by three? Yes, 3 is divisible by three.
|
||||
3. If it is, I say "Fizz"! Okay, "Fizz"
|
||||
|
||||
Saying a word ends my logical flow. I've said what I need to say, so I'm done.
|
||||
|
||||
Since we are expected to say "Fizz" when we see 3, this makes sense. What about 5?
|
||||
|
||||
1. First, I look at the number. The number is 5.
|
||||
2. Is the number divisible by three? No, 5 is not divisible by three.
|
||||
3. If it is, I say "Fizz"! It is not, so I do not say anything.
|
||||
4. Is the number divisible by five? Yes, 5 is divisible by five.
|
||||
5. If it is, I say "Buzz"! Okay, "Buzz".
|
||||
|
||||
Again, our logic looks correct. When I see 5, I *should* say "Buzz". What about 7?
|
||||
|
||||
1. First, I look at the number. The number is 7.
|
||||
2. Is the number divisible by three? No, 7 is not divisible by three.
|
||||
3. If it is, I say "Fizz"! It is not, so I do not say anything.
|
||||
4. Is the number divisible by five? No, 7 is not divisible by five.
|
||||
5. If it is, I say "Buzz"! It is not, so I do not say anything.
|
||||
6. Is the number divisible by three AND five? No, 7 is not divisible by three or five.
|
||||
7. If it is, I say "FizzBuzz"! It is not, so I do not say anything.
|
||||
8. If I have said nothing at all, I say the number. Okay, 2.
|
||||
|
||||
Yep, that all looks right. When I see 2, I *should* say 2. What about 15?
|
||||
|
||||
1. First, I look at the number. The number is 15.
|
||||
2. Is the number divisible by three? Yes, 15 is divisible by three.
|
||||
3. If it is, I say "Fizz"! Okay, "Fizz"
|
||||
|
||||
WAIT! That's an issue! When I see 15, I *should* say "FizzBuzz". Oh no!
|
||||
|
||||
Because saying something ends my logic, I need to check for three AND five before I check them individually - the "three and five" condition can never be reached.
|
||||
|
||||
So I fix my logic:
|
||||
|
||||
1. First, I look at the number.
|
||||
2. Is the number divisible by three AND five?
|
||||
3. If it is, I say "FizzBuzz"!
|
||||
4. Otherwise, is the number divisible by three?
|
||||
5. If it is, I say "Fizz"!
|
||||
6. Otherwise, is the number divisible by five?
|
||||
7. If it is, I say "Buzz"!
|
||||
8. Otherwise, I say the number.
|
||||
|
||||
Now I test again: 3? "Fizz" âś“. 5? "Buzz" âś“. 15? "FizzBuzz" âś“. 2? 2 âś“.
|
||||
|
||||
**ONCE THE LOGIC IS SOUND AND ALL OF MY MANUAL TESTS WORK, THEN I WRITE CODE.**
|
||||
|
||||
This approach serves two critical purposes:
|
||||
|
||||
1. I figure out the logic independently from the code, so I am not juggling both logic and syntax.
|
||||
2. If my app does not work, I can look for an error in the code because I know the logic is sound (since I tested that before writing any code).
|
||||
|
||||
## The Importance of Precision
|
||||
|
||||
Computers are like children who will do only and exactly what you say. They cannot infer your intent. They cannot read between the lines. They will follow your instructions to the letter, even when those instructions lead to nonsense.
|
||||
|
||||
I learned this lesson recently while helping someone solve a pyramid-building problem. They were trying to build a pyramid out of a character, with a specified number of rows. Their initial instructions were something like:
|
||||
|
||||
1. Look at the string.
|
||||
2. Look at the integer.
|
||||
3. Write the string.
|
||||
4. Go to the next line.
|
||||
5. Write the same string from before again.
|
||||
6. Add two more of the same single string beside it.
|
||||
7. Repeat until you have the same number of rows as the integer.
|
||||
|
||||
When I followed these instructions precisely, I got:
|
||||
|
||||
```
|
||||
x
|
||||
xxx
|
||||
xxxxx
|
||||
xxxxxxx
|
||||
xxxxxxxxx
|
||||
```
|
||||
|
||||
But that is not a pyramid! A pyramid should be centred, like:
|
||||
|
||||
```
|
||||
x
|
||||
xxx
|
||||
xxxxx
|
||||
xxxxxxx
|
||||
xxxxxxxxx
|
||||
```
|
||||
|
||||
The instructions were missing a crucial detail: the spacing. But more importantly, they were imprecise. "Add two more of the same single string beside it" - beside what? Where? The instructions assumed I would understand the intent, but a computer (or a very literal Naomi following instructions) would not.
|
||||
|
||||
After several iterations, we refined the instructions to be precise:
|
||||
|
||||
1. Look at the string. It is what we want the pyramid to be built by.
|
||||
2. Look at the integer. It is the amount of rows we want.
|
||||
3. Add a number of spaces equal to the integer, and then write the string.
|
||||
4. Go to the next line.
|
||||
5. Write the same spaces string from before again, but get rid of one space, and write the string.
|
||||
6. Add two more of the same single string beside it.
|
||||
7. Write the whole space and string again from the last row, get rid of one space again, and add two more of the same string.
|
||||
8. Repeat step 7 until you have the same number of rows as the integer.
|
||||
|
||||
Now, when I follow these instructions precisely, I get the correct pyramid. The logic is sound. **Only then** do we start thinking about how to translate this into code.
|
||||
|
||||
## Translating Logic to Code
|
||||
|
||||
Once your logic is sound and tested, translating it to code becomes much simpler. You are no longer trying to figure out what to do - you already know that. You are just figuring out how to express it.
|
||||
|
||||
Let us go back to the pyramid example. We have our logic:
|
||||
|
||||
1. Look at the string and integer.
|
||||
2. For each row, calculate the number of spaces and the number of characters.
|
||||
3. Print the spaces, then the characters.
|
||||
4. Move to the next row.
|
||||
|
||||
Now we can think about the code structure.
|
||||
|
||||
First, let's define our function:
|
||||
|
||||
```javascript
|
||||
function pyramid(characterToBuildPyramidWith, numOfRows) {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
What are our steps?
|
||||
|
||||
- We need a loop to iterate through rows.
|
||||
- For each row, we gotta know how many spaces to use. We use less spaces in each row.
|
||||
- For each row, we gotta know how many characters to use. We use more characters in each row.
|
||||
- Finally, print our result.
|
||||
|
||||
Okay, let's create a loop to iterate through the number of rows. Sounds like a great usecase for a standard `for` loop.
|
||||
|
||||
```javascript
|
||||
function pyramid(characterToBuildPyramidWith, numOfRows) {
|
||||
for (let row = 0; row < numOfRows; row++) {
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We're using `row` instead of `i`, so it's less confusing.
|
||||
|
||||
Now for each row (so each iteration), we gotta know how many spaces to use. Thankfully, we've already sorted that out in our logic:
|
||||
|
||||
> Add a number of spaces equal to the integer
|
||||
> Write the whole space and string again from the last row, get rid of one space again
|
||||
|
||||
So we start with `numOfRows` spaces, and for each iteration we can decrement the number of spaces by one. I think, then, that we could do `numOfRows - row`. Let's put that in a variable so we don't lose track.
|
||||
|
||||
```javascript
|
||||
function pyramid(characterToBuildPyramidWith, numOfRows) {
|
||||
for (let row = 0; row < numOfRows; row++) {
|
||||
const spaces = " ".repeat(numOfRows - row);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Alright, now the number of characters. Again, we can refer back to our paper logic:
|
||||
|
||||
> Write the same spaces string from before again, but get rid of one space, and write the string.
|
||||
> Write the whole space and string again from the last row, get rid of one space again, and add two more of the same string.
|
||||
|
||||
Okay, so we start with one character, and add two more for every iteration. That would be... `row` times two, but add one to account for the initial character.
|
||||
|
||||
|
||||
```javascript
|
||||
function pyramid(characterToBuildPyramidWith, numOfRows) {
|
||||
for (let row = 0; row < numOfRows; row++) {
|
||||
const spaces = " ".repeat(numOfRows - row);
|
||||
const characters = characterToBuildPyramidWith.repeat(row * 2 + 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally, our last step:
|
||||
|
||||
> Finally, print our result.
|
||||
|
||||
We can print things with a `console.log` statement. Let's print our spaces followed by our characters.
|
||||
|
||||
```javascript
|
||||
function pyramid(characterToBuildPyramidWith, numOfRows) {
|
||||
for (let row = 0; row < numOfRows; row++) {
|
||||
const spaces = " ".repeat(numOfRows - row);
|
||||
const characters = characterToBuildPyramidWith.repeat(row * 2 + 1);
|
||||
console.log(spaces + characters);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Time to double check our work! Let's call our function:
|
||||
|
||||
```javascript
|
||||
function pyramid(characterToBuildPyramidWith, numOfRows) {
|
||||
for (let row = 0; row < numOfRows; row++) {
|
||||
const spaces = " ".repeat(numOfRows - row);
|
||||
const characters = characterToBuildPyramidWith.repeat(row * 2 + 1);
|
||||
console.log(spaces + characters);
|
||||
}
|
||||
}
|
||||
|
||||
pyramid("x", 5);
|
||||
```
|
||||
|
||||
Checking the console, I see:
|
||||
|
||||
```txt
|
||||
x
|
||||
xxx
|
||||
xxxxx
|
||||
xxxxxxx
|
||||
xxxxxxxxx
|
||||
```
|
||||
|
||||
That looks like a pyramid! But hang on... It looks like we've shifted it one space too far. Let's go back to our logic on paper, because our logic is off (so we don't want to touch the code yet).
|
||||
|
||||
1. Look at the string. It is what we want the pyramid to be built by.
|
||||
2. Look at the integer. It is the amount of rows we want.
|
||||
3. Add a number of spaces equal to the integer, and then write the string.
|
||||
4. Go to the next line.
|
||||
5. Write the same spaces string from before again, but get rid of one space, and write the string.
|
||||
6. Add two more of the same single string beside it.
|
||||
7. Write the whole space and string again from the last row, get rid of one space again, and add two more of the same string.
|
||||
8. Repeat step 7 until you have the same number of rows as the integer.
|
||||
|
||||
AHA! Our issue is in step 3.
|
||||
|
||||
> 3. Add a number of spaces equal to the integer, and then write the string.
|
||||
|
||||
By adding the number of spaces equal to the integer, we've failed to account for the initial pyramid character. Essentially, we need to add one less space.
|
||||
|
||||
So we update our logic **first**:
|
||||
|
||||
1. Look at the string. It is what we want the pyramid to be built by.
|
||||
2. Look at the integer. It is the amount of rows we want.
|
||||
3. Add a number of spaces equal to the integer **less one**, and then write the string.
|
||||
4. Go to the next line.
|
||||
5. Write the same spaces string from before again, but get rid of one space, and write the string.
|
||||
6. Add two more of the same single string beside it.
|
||||
7. Write the whole space and string again from the last row, get rid of one space again, and add two more of the same string.
|
||||
8. Repeat step 7 until you have the same number of rows as the integer.
|
||||
|
||||
|
||||
|
||||
## When Logic Breaks During Coding
|
||||
|
||||
I see it all the time. You have worked out your logic, tested it manually, and it looks perfect. You start translating it to code, and halfway through, you realize it's not perfect at all.
|
||||
|
||||
Maybe you cannot figure out how to achieve a specific step. Maybe you discover an edge case you did not consider. Maybe the logic just... does not work the way you thought it would.
|
||||
|
||||
**Stop coding immediately.**
|
||||
|
||||
I know, I know. Your instinct is to keep typing, to try to fix it in code, to hack around the problem. But resist that urge. If your logic needs revision, that is a logic problem, not a code problem. And logic problems should be solved on paper, not in your editor.
|
||||
|
||||
Step away from your keyboard. Go back to your plain English instructions. Walk through them again manually. Where does it break? What did you miss? What assumption did you make that turned out to be wrong?
|
||||
|
||||
Let us say you are working on that pyramid problem, and while coding you realize: "Wait, how do I know how many spaces to remove each time? Do I start with the full number of spaces, or one less?"
|
||||
|
||||
Instead of trying to figure this out in code, go back to your logic:
|
||||
|
||||
1. Add a number of spaces equal to the integer, and then write the string.
|
||||
2. Go to the next line.
|
||||
3. Write the same spaces string from before again, but get rid of one space, and write the string.
|
||||
|
||||
Test it manually: if the integer is 5, I start with 5 spaces. Then I go to the next line and have 4 spaces. Then 3 spaces. Then 2 spaces. Then 1 space. Then 0 spaces. That makes sense!
|
||||
|
||||
Now you can go back to your code with clarity. You know that for row 0, you need `numOfRows - 0 - 1` spaces (which is 4 for 5 rows). For row 1, you need `numOfRows - 1 - 1` spaces (which is 3). The pattern is clear because you worked it out in logic first.
|
||||
|
||||
Okay, pretend we've tested our change manually (I am sparing you from reading through that process again), and it works. Now we can update our code to reflect our logic change:
|
||||
|
||||
```javascript
|
||||
function pyramid(characterToBuildPyramidWith, numOfRows) {
|
||||
for (let row = 0; row < numOfRows; row++) {
|
||||
// Notice how this is the ONLY change I made. We aren't changing anything that isn't updated in the logic on paper.
|
||||
const spaces = " ".repeat(numOfRows - row - 1);
|
||||
const characters = characterToBuildPyramidWith.repeat(row * 2 + 1);
|
||||
console.log(spaces + characters);
|
||||
}
|
||||
}
|
||||
|
||||
pyramid("x", 5);
|
||||
```
|
||||
|
||||
And we get:
|
||||
|
||||
```txt
|
||||
x
|
||||
xxx
|
||||
xxxxx
|
||||
xxxxxxx
|
||||
xxxxxxxxx
|
||||
```
|
||||
|
||||
**WE'VE DONE IT!!!!!**
|
||||
|
||||
The key principle here is: **if you discover a logic problem while coding, you have not discovered a coding problem. You have discovered that your logic was incomplete.** Fix the logic first, test it manually, and then return to the code.
|
||||
|
||||
This might feel inefficient. You might think "I am so close, let me just fix this one thing in code." But I promise you: fixing logic in code is like trying to repair the foundation of a house while you are painting the walls. It will not work, and you will make a bigger mess.
|
||||
|
||||
## The Benefits of This Approach
|
||||
|
||||
When you think algorithmically first, you gain several advantages:
|
||||
|
||||
**You catch logic errors early.** Instead of debugging why your code does not work, you debug why your logic does not work - and logic is much easier to debug than code.
|
||||
|
||||
**You separate concerns.** Logic and syntax are two different problems. Solve them separately, and you will solve them more effectively.
|
||||
|
||||
**You build confidence.** When your code does not work, you know it is a syntax issue, not a logic issue. That narrows down your debugging significantly.
|
||||
|
||||
**You communicate better.** When you can explain your logic in plain English, you can explain it to teammates, ask for help more effectively, and document your code more clearly.
|
||||
|
||||
**You learn faster.** By focusing on logic first, you develop stronger problem-solving skills that transfer across languages and technologies.
|
||||
|
||||
## The Takeaway
|
||||
|
||||
The next time you have to write some code, don't write the code! Do this:
|
||||
|
||||
1. Write out your logic in plain English, step-by-step.
|
||||
2. Test that logic by hand, using multiple test cases.
|
||||
3. Ideally, you've touched every "branch" in your logical breakdown.
|
||||
3. Edit and adjust the logic when test cases fail.
|
||||
4. Once all of your manual test cases pass your hand-written logic, you can finally begin translating it into code.
|
||||
|
||||
You will find that your code is cleaner, your bugs are fewer, and your problem-solving skills are stronger. And hey - if you can explain your logic to me in plain English and it works, then translating it to code is just a matter of syntax. And syntax is the easy part!
|
||||
|
||||
> đź’ˇ Remember: computers are children who will do only and exactly what you say. Be precise, be thorough, and test your logic before you write your code.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: "Learning In Public"
|
||||
date: "2025-10-31"
|
||||
summary: "Why Naomi hates answering DMs"
|
||||
---
|
||||
|
||||
If you have ever tried to DM me, there is a very good chance that I advised you to ask me the question in a server instead. This is NOT just because I hate DMs - though I do, because running multiple prolific communities means I am absolutely buried in DM request. No, this is actually for YOUR benefit.
|
||||
|
||||
Because when I redirect you to a public community instead of my DMs, I am encouraging you to **learn in public**. But why is learning in public so important? Well, there are a number of reasons. Let's explore some!
|
||||
|
||||
## Single Source of Truth
|
||||
|
||||
When you ask me a question in DMs, I implicitly become the single source of truth. Whatever answer I provide you is *hopefully* accurate, but I am wrong way more often than I am right (even this article should be taken with a grain of salt). But the burden of fact checking my answer now falls entirely on you.
|
||||
|
||||
Consider a DM where you ask me "What is the time complexity of a merge sort?". And let's pretend I say it is `O(n log n)`. It might be, it might not be, I'm honestly not sure - even after looking it up. BUT my lack of knowledge isn't the point here. The point is that whatever information I give you is now your burden to confirm.
|
||||
|
||||
What if you asked me that question in a public server? I'll give you the same answer: `O(n log n)`. But now because this is a PUBLIC conversation, Jeremy could chime in and say "Actually it's `O(n)` and here is a source". The burden of fact checking has shifted off your shoulders, because the entire community can now weigh in and call out my incorrect answer. And *generally* the collective knowledge of a group will be more accurate than the isolated knowledge of an individual.
|
||||
|
||||
## Helping Others Grow
|
||||
|
||||
Here's another benefit! I presume that, like many people, you agonised about reaching out to ask me your question. Let's pretend you asked me "How do I centre this `div`?". I cannot speak to your emotions, but I *can* state that it is rather common for developers (especially those in their early learning stages) to be afraid of, embarrassed by, or too shy to ask questions.
|
||||
|
||||
The fact that you worked up the courage to ask the question should be celebrated! It can be super duper scary! But when you ask me that question in DMs, *no one else can see it*. Instead, if you ask that question in a server, you and I can have our conversation in a public forum. And maybe Danny had the same question, but was too uncomfortable to ask it. Your initiative has now sparked a conversation from which Danny, and anyone else in the community, can potentially benefit.
|
||||
|
||||
## Networking!
|
||||
|
||||
If you regularly follow this blog, you probably saw my [post about networking](https://blog.nhcarrigan.com/post/networking) and how it is vital, especially in our current economic downturn. Did you know, though, that asking questions in public IS a networking opportunity??
|
||||
|
||||
We've already written at length about how a DM is completely isolated, so let's skip right to the part where you ask me the question publicly. This time, you ask me about the considerations between choosing JavaScript or Python. This is a "juicy" question; that is, questions like this are great catalysts for some in-depth discussions. And that's exactly what we do in this scenario - we have a killer discussion, with multiple people chiming in.
|
||||
|
||||
During this discussion, you've effectively demonstrated your ability to learn, curiosity for new information, and technical proficiency all at once. And because this happened in public, someone like Jessica might see the conversation thread. And maybe her team has an opening for a junior developer. That conversation could very well be the spark that leads her to reach out to you for a referral for that role! You never know who is reading a conversation.
|
||||
|
||||
## Building a Portfolio
|
||||
|
||||
Now, a lot of my work happens on Discord. Which is not the best platform, because it's not indexed by search engines. But EVEN SO, your public conversations can become part of your portfolio! I have conversations waaaaaay back from when I first started my learning journey that I still look back on fondly years later. Because those conversations serve as a lovely reminder of where I started, and thus how far I have come.
|
||||
|
||||
But your conversations aren't just beneficial for your own self-actualisation! They're also a great demonstration of your long-term growth and commitment to your craft. If you have spent the last five years engaging in increasingly technical conversations, asking progressively more in-depth questions, and engaging in more complex discourse... then that's something you can leverage on your portfolio to show your own evolution!
|
||||
|
||||
And if your conversations are on an indexable platform, like the [freeCodeCamp forum](https://forum.freecodecamp.org), then you even get some free exposure through search engine results!
|
||||
|
||||
## Confidence is Key!
|
||||
|
||||
I've been scared to ask questions before too. I promise it gets easier. Now I'm running my mouth all day asking questions about everything! But I'm only confident enough to do so because I started asking those questions at the *beginning of my learning journey*. I've gone through all of the "oh god what if people think I'm completely incompetent" anxieties over and over again. And it turns out... No one has ever thought that.
|
||||
|
||||
BUT! I know you won't believe me. Just like I didn't believe the folks who told me the same thing in my initial learning. I can tell you it's okay until I am blue in the face, but the most powerful confirmation comes from *experiencing it yourself*. So asking your "silly" questions NOW, when you are still in your first steps of learning to code, will better prepare you to be comfortable asking those questions on the job - where it is VITAL that you are comfortable doing so!
|
||||
|
||||
## Learning by Teaching
|
||||
|
||||
I, personally, am a HUGE fan of the Socratic Method. SO MUCH SO that I formally adopted it as our [instructional approach at freeCodeCamp](https://www.freecodecamp.org/news/how-to-help-someone-with-their-code-using-the-socratic-method/). I think that there is a lot of value in being guided to reach the solution through your own cognitive reasoning.
|
||||
|
||||
Part of that process, then, requires you to explain your understanding along the way. In having to explain your understanding, you are effectively teaching others! In fact, when you do this in public there is a significant chance someone will chime in to ask questions about bits they didn't understand. Which then causes you to dive into your explanation further. And every time you have to break something down and convey it in a way someone else can understand, you strengthen your own competencies!
|
||||
|
||||
Not only is that a win for everyone, but being challenged to articulate your thoughts can help you better succeed in your professional career. Part of our work as developers involves explaining our ideas to others - some audiences are technical, and some are not. But all audiences need to be able to understand you. So getting that experience NOW just further positions you for success in your career.
|
||||
|
||||
## The Takeaway
|
||||
|
||||
I know I rambled on and on. I do that. I'm always yapping. So here's the key takeaway I want you to carry with you forever:
|
||||
|
||||
The next time you want to hit that "Send DM" button, consider all of these benefits you will lose out on by asking me a question privately instead of in a public community.
|
||||
@@ -54,3 +54,30 @@ figcaption {
|
||||
@apply text-center;
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply text-left;
|
||||
@apply bg-gray-100;
|
||||
@apply p-2;
|
||||
@apply rounded-md;
|
||||
@apply border;
|
||||
@apply border-gray-300;
|
||||
@apply overflow-x-auto;
|
||||
@apply whitespace-pre-wrap;
|
||||
@apply break-words;
|
||||
@apply text-sm;
|
||||
@apply font-mono;
|
||||
}
|
||||
|
||||
code:not(pre code) {
|
||||
@apply text-sm;
|
||||
@apply font-mono;
|
||||
@apply bg-gray-100;
|
||||
@apply p-1;
|
||||
@apply rounded-md;
|
||||
@apply border;
|
||||
@apply border-gray-300;
|
||||
@apply overflow-x-auto;
|
||||
@apply whitespace-pre-wrap;
|
||||
@apply break-words;
|
||||
}
|
||||
+6
-9
@@ -14,8 +14,7 @@ import "./globals.css";
|
||||
const inter = Inter({ subsets: [ "latin" ] });
|
||||
|
||||
const metadata: Metadata = {
|
||||
description:
|
||||
"The personal musings of a transfem software engineer.",
|
||||
description: "The personal musings of a transfem software engineer.",
|
||||
openGraph: {
|
||||
images: "https://cdn.nhcarrigan.com/og-image.png",
|
||||
},
|
||||
@@ -48,14 +47,12 @@ const RootLayout = ({
|
||||
strategy={"afterInteractive"}
|
||||
type="text/javascript"
|
||||
></Script>
|
||||
<link href="https://cdn.nhcarrigan.com/logo.png" rel="icon" sizes="any" />
|
||||
<link
|
||||
href="https://cdn.nhcarrigan.com/logo.png"
|
||||
rel="icon"
|
||||
sizes="any"
|
||||
/>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css"
|
||||
rel="stylesheet"
|
||||
></link>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { JSX } from "react";
|
||||
@@ -34,7 +35,7 @@ const Page = async({
|
||||
<h2 className="text-center">{post.data.summary}</h2>
|
||||
<p className="text-center">{`A ${post.data.readtime}.`}</p>
|
||||
<Rule />
|
||||
<Markdown rehypePlugins={[ rehypeRaw ]} remarkPlugins={[ remarkGfm ]}>
|
||||
<Markdown rehypePlugins={[ rehypeRaw, rehypeHighlight ]} remarkPlugins={[ remarkGfm ]}>
|
||||
{post.content}
|
||||
</Markdown>
|
||||
<Rule />
|
||||
|
||||
Reference in New Issue
Block a user