Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .claude/hooks/format-lint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# Formats and lints one edited file with whichever toolchain owns its extension
# (docs/tooling.md). Lint failures exit 2 so the findings go back to the agent.
set -uo pipefail

repo_root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}"
[[ -n "$repo_root" ]] || exit 0

payload="$(cat)"
file="$(printf '%s' "$payload" | jq -r '.tool_response.filePath // .tool_input.file_path // empty')"
[[ -n "$file" ]] || exit 0
[[ -f "$file" ]] || exit 0

# Absolute, and inside this repo.
case "$file" in
/*) ;;
*) file="$repo_root/$file" ;;
esac
case "$file" in
"$repo_root"/*) ;;
*) exit 0 ;;
esac

rel="${file#"$repo_root"/}"
case "$rel" in
legacy/* | dist/* | .astro/* | node_modules/* | plan/* | tools/lint/anti-slop/* | pnpm-lock.yaml)
exit 0
;;
esac

cd "$repo_root" || exit 0

# Use the installed binaries directly so the hook works without pnpm on PATH.
# Status 127 means the tool itself is missing, which callers must not report as
# lint findings: on a fresh clone, before `pnpm install`, every edit would
# otherwise be rejected with an empty message.
readonly TOOL_MISSING=127

run() {
local tool="$1"; shift
local bin="$repo_root/node_modules/.bin/$tool"
if [[ -x "$bin" ]]; then
"$bin" "$@" 2>&1
return
fi
command -v pnpm >/dev/null || return "$TOOL_MISSING"
[[ -d "$repo_root/node_modules" ]] || return "$TOOL_MISSING"
pnpm exec "$tool" "$@" 2>&1
}

# Runs a linter and exits 2 with its findings. A missing toolchain exits 0 so the
# edit is allowed through un-linted rather than blocked with nothing to act on.
lint() {
local findings status
findings="$(run "$@")"
status=$?
case "$status" in
0) return 0 ;;
"$TOOL_MISSING") exit 0 ;;
*)
printf '%s\n' "$findings" >&2
exit 2
;;
esac
}

case "${file##*.}" in
ts | tsx | js | jsx | mjs | cjs | json | jsonc | json5 | css)
# Without --ignore-path, oxfmt also reads .prettierignore, which excludes
# every extension oxfmt owns.
run oxfmt --ignore-path .gitignore "$file" >/dev/null
lint oxlint --type-aware "$file"
;;
# oxfmt's directory scan formats these too, so `pnpm check` fails on an unformatted one —
# but oxlint has no rules for them, so they are formatted and not linted.
yaml | yml | toml)
run oxfmt --ignore-path .gitignore "$file" >/dev/null
;;
astro)
run prettier --write "$file" >/dev/null
lint eslint --max-warnings 0 "$file"
;;
md)
run prettier --write "$file" >/dev/null
;;
esac

exit 0
16 changes: 16 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-lint.sh",
"timeout": 60
}
]
}
]
}
}
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# For what neither oxfmt nor Prettier covers (yaml, toml, shell, gitignore-likes).
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: CI

on:
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: Check + Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Install mise tools
uses: jdx/mise-action@v4
with:
version: 2026.8.14
install: true
cache: true

# mise-action's cache covers mise-managed tool binaries (node, pnpm), not
# pnpm's content-addressable store, so without this every run re-downloads
# the whole tree.
- name: Locate pnpm store
run: echo "PNPM_STORE=$(pnpm store path --silent)" >>"$GITHUB_ENV"

- name: Cache pnpm store
uses: actions/cache@v6
with:
path: ${{ env.PNPM_STORE }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-store-${{ runner.os }}-

- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline

- name: Check (typecheck + lint + format + knip)
run: pnpm check

- name: Build
run: pnpm build

# --root-dir is required for the root-relative hrefs Astro emits (/_astro/...);
# without it lychee cannot resolve them in local files and errors on every page.
- name: Link check
uses: lycheeverse/lychee-action@v2
with:
args: >-
--offline --include-fragments --no-progress
--root-dir ${{ github.workspace }}/dist
'dist/**/*.html'
fail: true
27 changes: 6 additions & 21 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage
node_modules
.pnpm-store

# next.js
/.next/
/out/

# production
/build
# astro
.astro
dist

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
24 changes: 24 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"sortImports": true,
"sortPackageJson": true,
"sortTailwindcss": true,
"trailingComma": "all",
"overrides": [
{
"files": ["**/*.json", "**/*.jsonc", "**/*.json5"],
"options": {
"trailingComma": "none"
}
}
],
"ignorePatterns": [
"legacy/**",
"dist/**",
".astro/**",
".claude/**",
"tools/lint/anti-slop/**",
"**/*.md",
"**/*.astro"
]
}
68 changes: 68 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["./tools/lint/nkzw/oxlintrc.json"],
"plugins": ["typescript", "unicorn", "oxc", "import"],
"options": {
"typeAware": true,
"typeCheck": true
},
"categories": {
"correctness": "error",
"suspicious": "error",
"perf": "error",
"style": "off"
},
"jsPlugins": [
{
"name": "anti-slop",
"specifier": "./tools/lint/anti-slop/index.ts"
}
],
"rules": {
"anti-slop/no-chained-type-assertions": "error",
"anti-slop/no-conditional-empty-object-spread": "error",
"anti-slop/no-known-value-widening": "error",
"anti-slop/no-module-mocking": "error",
"anti-slop/no-object-parameters": "error",
"anti-slop/no-reflect-apply": "error",
"anti-slop/no-reflect-get": "error",
"anti-slop/no-runtime-typeof": "error",
"anti-slop/no-shape-in-symbol-names": "error",
"anti-slop/no-unknown-parameters": "error",
"anti-slop/no-unknown-returns": "error",
"anti-slop/no-unknown-type-aliases": "error",
"anti-slop/no-unsafe-dictionary-type": "error",
"anti-slop/no-widen-then-assert": "error",
"anti-slop/require-safety-comment-for-type-assertion": "error",
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["legacy/**", "**/legacy/**"],
"message": "legacy/ is reference only \u2014 never import from it (plan/00-overview.md)"
}
]
}
],
"perfectionist/sort-objects": "off"
},
"overrides": [
{
"files": ["functions/**"],
"rules": {
"no-console": "off"
}
}
],
"ignorePatterns": [
"**/*.astro",
"legacy/**",
"dist/**",
".astro/**",
"tools/lint/anti-slop/**",
".claude/**",
"plan/**",
"node_modules/**"
]
}
13 changes: 13 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Prettier is invoked against an explicit `**/*.{astro,md}` glob (see package.json),
# because a blanket `*` ignore prunes directories and cannot be undone per-file.
# oxfmt owns every other extension — see docs/tooling.md.
legacy/**
dist/**
.astro/**
node_modules/**
tools/lint/anti-slop/**

# Hand-formatted markdown: tables, checkboxes, and line breaks here are deliberate.
plan/**
docs/adr/**
DESIGN.md
13 changes: 13 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"printWidth": 100,
"trailingComma": "all",
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}
10 changes: 9 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
{
"recommendations": ["bradlc.vscode-tailwindcss", "biomejs.biome"]
"recommendations": [
"astro-build.astro-vscode",
"oxc.oxc-vscode",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"hverlin.mise-vscode",
"dbaeumer.vscode-eslint",
"typescriptteam.native-preview"
]
}
Loading
Loading