Skip to content

Commit d137161

Browse files
Merge branch 'workspace'
2 parents 77c14b1 + 23d496d commit d137161

26 files changed

Lines changed: 4765 additions & 110 deletions

CLAUDE.md renamed to .claude/CLAUDE.md

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ Workflow:
1313
3. Implement to match the spec
1414
4. Verify running software matches the spec
1515

16-
Start at [specs/README.md](specs/README.md). The index of what's where:
16+
Start at [specs/README.md](../specs/README.md). The index of what's where:
1717

18-
- [specs/architecture.md](specs/architecture.md) — stack, repo layout, deploy
19-
- [specs/data-model.md](specs/data-model.md) — entities, fields, relationships
20-
- [specs/deferred.md](specs/deferred.md) — features intentionally out of scope (do NOT silently implement these)
21-
- [specs/api/](specs/api/) — endpoint contracts (one file per resource group)
22-
- [specs/screens/](specs/screens/) — one file per route — what the user sees, what they can do
23-
- [specs/behaviors/](specs/behaviors/) — cross-cutting rules referenced from multiple screens/APIs
18+
- [specs/architecture.md](../specs/architecture.md) — stack, repo layout, deploy
19+
- [specs/data-model.md](../specs/data-model.md) — entities, fields, relationships
20+
- [specs/deferred.md](../specs/deferred.md) — features intentionally out of scope (do NOT silently implement these)
21+
- [specs/api/](../specs/api/) — endpoint contracts (one file per resource group)
22+
- [specs/screens/](../specs/screens/) — one file per route — what the user sees, what they can do
23+
- [specs/behaviors/](../specs/behaviors/) — cross-cutting rules referenced from multiple screens/APIs
2424

2525
## Spec drift auditing
2626

@@ -30,11 +30,20 @@ Run `/audit-spec-drift` to launch a comprehensive audit comparing `specs/` again
3030

3131
`plans/` is the micro-DAG of work that bridges `specs/` to the running code. If specs describe **state** (what should be true forever), plans describe **motion** (how we get there). Start a feature with a plan; the plan declares its scope, the specs it implements, its dependencies, and concrete validation criteria.
3232

33-
Plans index: [plans/README.md](plans/README.md). Workflow:
33+
Plans index: [plans/README.md](../plans/README.md). Workflow:
3434

3535
1. **Add a plan** when starting a new chunk of work. `status: planned` + `depends:` set.
36-
2. **Move to `in-progress`** when you start. Multiple plans can be in-progress in parallel across people, but one plan per contributor at a time is the norm.
37-
3. **Move to `done`** when validation criteria all pass. Link the merged PR in frontmatter (`pr: 42`).
36+
2. **Move to `in-progress`** when you start, as the first commit on the branch (`chore(plans): mark <slug> in-progress`). Skippable for tiny plans — going straight to `done` at the end is fine. Multiple plans can be in-progress in parallel across people, but one plan per contributor at a time is the norm.
37+
3. **Move to `done` as the last commit on the branch, before merge.** That commit does the following, all in one shot, with message `chore(plans): mark <slug> done (PR #<n>)`:
38+
- Frontmatter: `status``done`, add `pr: <PR number>` (knowable once `gh pr create` returns)
39+
- **Validation checklist: flip each `- [ ]` to `- [x]` for criteria you verified.** If a criterion can't be verified at merge time (depends on a downstream plan, needs production deploy, etc.), leave it unchecked and add a one-line note in the Notes section explaining why and where it'll close out. Never silently rewrite a validation criterion to match what you ended up doing — that's a plan amendment in its own earlier commit.
40+
- **Notes** section: non-actionable carry-forwards — decisions, surprises, gotchas, learnings. Things future-you would want to know.
41+
- **Follow-ups** section: actionable items that didn't ship with this plan. Each entry is one of:
42+
- `Issue [#N](link) — short description` — when actionable and not owned by an existing planned-or-in-progress plan, file the issue first (`gh-axi issue create`) and link it
43+
- `Deferred to [`<other-plan>`](<other-plan>.md) — short description` — when an unstarted (`status: planned`) downstream plan should own the work. **The same closeout commit must also edit that downstream plan to absorb the deferral** — typically a new bullet under Approach and a new criterion under Validation. If the downstream plan is already `in-progress` or `done`, use the Issue shape instead; never modify a plan that's actively being implemented or already frozen.
44+
- `Tracked as: <free-form pointer>` — for anything else (waiting on community input, vendor response, etc.)
45+
- `None.` — explicit when there's nothing, so a future reader can see the section was considered, not just absent
46+
- The plan is frozen after merge — historical record, no further edits
3847
4. **Update `depends:`** as the DAG sharpens — a plan can discover it needs a new prereq mid-stream.
3948
5. **Specs come first.** A plan implements specs that already exist. If you realize specs need to change mid-plan, the spec change is its own PR before the plan continues.
4049
6. **Splitting a plan**: rename and add the new one with `depends:` updated.
@@ -58,21 +67,21 @@ pr: 42 # merged PR once done (optional)
5867

5968
`specs:` is for specs we own — the spec-drift-auditor matches them against implementation. `upstream-specs:` is for specs owned by dependencies (e.g., gitsheets) that this plan consumes; they're informational only and the spec-drift-auditor doesn't check them. Use the `<repo>:<path>` form so it's obvious where to look.
6069

61-
A plan's body follows the template in [plans/README.md](plans/README.md): Scope, Implements, Approach, Validation, Risks/unknowns, Notes. The Validation section is the load-bearing part — it converts "in-progress" to "done."
70+
A plan's body follows the template in [plans/README.md](../plans/README.md): Scope, Implements, Approach, Validation, Risks/unknowns, Notes, Follow-ups. The Validation section is the load-bearing part — it converts "in-progress" to "done."
6271

6372
**Plans are not specs.** They're project-management artifacts. Plans rot fast — once a plan is `done`, it's a historical record; don't keep editing it. The `spec-drift-auditor` reads `specs/`, not `plans/`.
6473

6574
## Stack
6675

6776
- **Backend** — Fastify 5.x + TypeScript. Single replica, in-process write mutex.
68-
- **Public storage**[gitsheets](https://github.com/JarvusInnovations/gitsheets) (TOML records in a git repo). Public-by-design — civic transparency. No persistent OLTP. See [specs/behaviors/storage.md](specs/behaviors/storage.md).
69-
- **Private storage** — S3-compatible bucket holding two `.jsonl` files (private profiles + legacy password hashes). Boot-load + in-memory; PUT on mutation. See [specs/behaviors/private-storage.md](specs/behaviors/private-storage.md).
77+
- **Public storage**[gitsheets](https://github.com/JarvusInnovations/gitsheets) (TOML records in a git repo). Public-by-design — civic transparency. No persistent OLTP. See [specs/behaviors/storage.md](../specs/behaviors/storage.md).
78+
- **Private storage** — S3-compatible bucket holding two `.jsonl` files (private profiles + legacy password hashes). Boot-load + in-memory; PUT on mutation. See [specs/behaviors/private-storage.md](../specs/behaviors/private-storage.md).
7079
- **Schemas** — Zod in `packages/shared`, consumed by both web and api, validating records in both stores.
7180
- **Full-text search** — in-memory SQLite FTS5 (or MiniSearch fallback), rebuilt at boot from gitsheets state.
72-
- **Auth** — GitHub OAuth as the sole primary identity provider; stateless JWT sessions. We are also the SAML IdP for codeforphilly.slack.com. See [specs/api/auth.md](specs/api/auth.md), [specs/api/saml.md](specs/api/saml.md).
81+
- **Auth** — GitHub OAuth as the sole primary identity provider; stateless JWT sessions. We are also the SAML IdP for codeforphilly.slack.com. See [specs/api/auth.md](../specs/api/auth.md), [specs/api/saml.md](../specs/api/saml.md).
7382
- **Frontend** — Vite + React 19 + shadcn/ui + Tailwind v4 + React Router v7.
7483

75-
See [specs/architecture.md](specs/architecture.md) for the full stack rationale.
84+
See [specs/architecture.md](../specs/architecture.md) for the full stack rationale.
7685

7786
Per the user's global rules: `npm` workspaces (not bun), `asdf` manages the Node version, commit lockfiles.
7887

@@ -82,7 +91,7 @@ Per the user's global rules: `npm` workspaces (not bun), `asdf` manages the Node
8291
2. **Private bucket** — emails, newsletter prefs, legacy password hashes during migration. Production-only; devs use a local filesystem backend with seeded fakes.
8392
3. **Public snapshot** (`codeforphilly-data-snapshot`) — anonymized, contributor-cloneable copy of the public data. PII-free by construction.
8493

85-
**Real production private data never lands on a dev machine** — see [specs/behaviors/private-storage.md](specs/behaviors/private-storage.md).
94+
**Real production private data never lands on a dev machine** — see [specs/behaviors/private-storage.md](../specs/behaviors/private-storage.md).
8695

8796
## Tooling
8897

@@ -115,9 +124,9 @@ npm run -w apps/web dev
115124
- Field names: `camelCase` in TS and in TOML records. No casing translation.
116125
- IDs: UUIDv7. Slugs (not IDs) in user-facing URLs.
117126
- Timestamps: ISO 8601 UTC strings (e.g., `"2026-05-15T18:42:00Z"`) — in requests, responses, and on disk.
118-
- Use the response envelope from [specs/api/conventions.md](specs/api/conventions.md) for every endpoint.
119-
- Markdown is rendered server-side. Clients never run a markdown library on user content. See [specs/behaviors/markdown-rendering.md](specs/behaviors/markdown-rendering.md).
120-
- Mutations go through the in-process write mutex documented in [specs/behaviors/storage.md](specs/behaviors/storage.md). Don't write to the data repo from anywhere else.
127+
- Use the response envelope from [specs/api/conventions.md](../specs/api/conventions.md) for every endpoint.
128+
- Markdown is rendered server-side. Clients never run a markdown library on user content. See [specs/behaviors/markdown-rendering.md](../specs/behaviors/markdown-rendering.md).
129+
- Mutations go through the in-process write mutex documented in [specs/behaviors/storage.md](../specs/behaviors/storage.md). Don't write to the data repo from anywhere else.
121130

122131
## Source control
123132

@@ -131,8 +140,8 @@ npm run -w apps/web dev
131140

132141
We are migrating from a MySQL-backed PHP/Emergence app to a gitsheets-backed Node app. Every user-facing URL stays the same. See:
133142

134-
- [specs/behaviors/slug-handles.md](specs/behaviors/slug-handles.md) — slug format and uniqueness
135-
- [specs/behaviors/legacy-id-mapping.md](specs/behaviors/legacy-id-mapping.md)`legacyId` column and URL redirects
143+
- [specs/behaviors/slug-handles.md](../specs/behaviors/slug-handles.md) — slug format and uniqueness
144+
- [specs/behaviors/legacy-id-mapping.md](../specs/behaviors/legacy-id-mapping.md)`legacyId` column and URL redirects
136145
- The one-shot importer lives at `apps/api/scripts/import-laddr.ts` (not yet implemented)
137146

138147
## When in doubt

.editorconfig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
11+
[*.md]
12+
trim_trailing_whitespace = false

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
concurrency:
9+
group: ${{ github.workflow }}-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v6
17+
18+
- name: Install asdf-managed tools
19+
uses: asdf-vm/actions/install@v4
20+
21+
- name: Install dependencies
22+
run: npm ci
23+
24+
- name: Type check
25+
run: npm run type-check
26+
27+
- name: Lint
28+
run: npm run lint
29+
30+
- name: Build
31+
run: npm run build

.gitignore

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Build artifacts
5+
dist/
6+
build/
7+
*.tsbuildinfo
8+
9+
# Local environment + secrets
10+
.env
11+
.env.*
12+
!.env.example
13+
*.local
14+
*.local.*
15+
16+
# Dev private storage runtime dir (filesystem backend; see specs/behaviors/private-storage.md).
17+
# The seeded fixture at fixtures/private-storage-seeded/ is intentionally NOT ignored —
18+
# it ships in the code repo so contributors can load a fake-data baseline.
19+
private-storage/
20+
21+
# Editor / OS
22+
.DS_Store
23+
.vscode/*
24+
!.vscode/extensions.json
25+
!.vscode/settings.shared.json
26+
.idea/
27+
28+
# Logs
29+
*.log
30+
npm-debug.log*

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodejs 22.22.3

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# codeforphilly-rewrite
2+
3+
A modernization of [laddr](https://github.com/CodeForPhilly/laddr) — the platform behind [codeforphilly.org](https://codeforphilly.org) — onto a Fastify + Vite/React + [gitsheets](https://github.com/JarvusInnovations/gitsheets) stack.
4+
5+
This site is **spec-driven**: [`specs/`](specs/) declares what should be true and the implementation is brought into conformance with it. Plans in [`plans/`](plans/) are the bridge between specs and code.
6+
7+
## Quick links
8+
9+
- [`specs/README.md`](specs/README.md) — what specs cover, how they're authored, where to start
10+
- [`specs/architecture.md`](specs/architecture.md) — stack, repo layout, deploy
11+
- [`plans/README.md`](plans/README.md) — the work-in-flight DAG that gets us to spec-complete
12+
- [`.claude/CLAUDE.md`](.claude/CLAUDE.md) — authorship conventions, tooling rules, source-control norms
13+
14+
## Getting started
15+
16+
```bash
17+
asdf install
18+
npm ci
19+
npm run dev # api (:3001) + web (:5173) in parallel
20+
```
21+
22+
Root scripts:
23+
24+
| Command | Effect |
25+
| ------- | ------ |
26+
| `npm run dev` | Concurrent dev servers for `apps/api` + `apps/web` with watch + HMR |
27+
| `npm run build` | Builds every workspace |
28+
| `npm run type-check` | `tsc --noEmit` across all workspaces |
29+
| `npm run lint` | ESLint flat-config at root |
30+
31+
## Stack
32+
33+
- **Backend** — Fastify 5.x + TypeScript, single replica, in-process write mutex
34+
- **Public storage** — gitsheets (TOML records in a git repo, civic-transparency public)
35+
- **Private storage** — S3-compatible bucket (emails, newsletter prefs, legacy password hashes during migration)
36+
- **Frontend** — Vite + React 19 + shadcn/ui + Tailwind v4 + React Router v7
37+
- **Auth** — GitHub OAuth + stateless JWT sessions; we are also the SAML IdP for codeforphilly.slack.com
38+
39+
See [`specs/architecture.md`](specs/architecture.md) for the rationale on each choice.
40+
41+
## Contributing
42+
43+
Spec-first: before writing or changing code, read the relevant spec. If the spec doesn't cover what you're about to do, update the spec first. See [`specs/README.md`](specs/README.md) for the workflow and [`.claude/CLAUDE.md`](.claude/CLAUDE.md) for conventions.

apps/api/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@cfp/api",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"main": "dist/index.js",
7+
"scripts": {
8+
"dev": "tsx watch src/index.ts",
9+
"build": "tsc -p tsconfig.json",
10+
"start": "node dist/index.js",
11+
"type-check": "tsc -p tsconfig.json --noEmit"
12+
},
13+
"dependencies": {
14+
"fastify": "^5.8.5"
15+
},
16+
"devDependencies": {
17+
"@types/node": "^25.8.0",
18+
"pino-pretty": "^13.1.3",
19+
"tsx": "^4.22.0",
20+
"typescript": "^6.0.3"
21+
}
22+
}

apps/api/src/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Fastify from 'fastify';
2+
3+
const PORT = Number(process.env.PORT ?? 3001);
4+
const HOST = process.env.HOST ?? '0.0.0.0';
5+
6+
const app = Fastify({
7+
logger:
8+
process.env.NODE_ENV === 'production'
9+
? true
10+
: { transport: { target: 'pino-pretty' } },
11+
});
12+
13+
app.get('/api/health', () => ({ status: 'ok' }));
14+
15+
try {
16+
await app.listen({ port: PORT, host: HOST });
17+
} catch (err) {
18+
app.log.error(err);
19+
process.exit(1);
20+
}

apps/api/tsconfig.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"module": "NodeNext",
5+
"moduleResolution": "NodeNext",
6+
"target": "ES2023",
7+
"lib": ["ES2023"],
8+
"outDir": "dist",
9+
"rootDir": "src",
10+
"types": ["node"]
11+
},
12+
"include": ["src/**/*"]
13+
}

apps/web/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Code for Philly</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>

0 commit comments

Comments
 (0)