21a1c4a701989eb40d9643b70589062ec647c013 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..f24f7a5830cea089bfbb66473bdb9cf152bcfb74 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,124 @@ +# Contributing to tdd.md + +This file is the **on-ramp** for new contributors — human or agent. It links to canonical sources rather than restating them. If something here drifts from what the spec or workflow memory says, the canonical artifact wins; please file an issue. + +Two URLs, one file: + +- **Browse**: [`/GIT/tdd.md/blob/main/CONTRIBUTING.md`](/GIT/tdd.md/blob/main/CONTRIBUTING.md) — via the site's own source viewer +- **Canonical**: [`/contributing`](/contributing) — the rendered permalink + +## Before you start + +Read in this order: + +1. [`/sama/v2`](/sama/v2) — the architectural spec (rules, profile, verifier, §5 metrics, §6 evolution policy) +2. [`/blog/sama-v2-on-ramp-gap`](/blog/sama-v2-on-ramp-gap) — why this file exists +3. [`/blog/sama-v2-goal-chain-gap`](/blog/sama-v2-goal-chain-gap) — why `/goal`s are now in git + +Then check the live state: [`/sama/v2/verify`](/sama/v2/verify) must report **7/7 ✓** before and after every merge. This is the load-bearing anti-fudge gate. + +## How contribution works on this codebase + +This is a self-hosted project. The canonical surfaces are all on `tdd.md`: + +- **Read** the source: [`/GIT/tdd.md/tree/main`](/GIT/tdd.md/tree/main) +- **Issues + discussion**: [git.tdd.md/syntaxai/tdd.md/issues](https://git.tdd.md/syntaxai/tdd.md/issues) (the self-hosted Forgejo) +- **Clone (read-only)**: `git clone https://tdd.md/syntaxai/tdd.md.git` + +Editing the live site is **admin-only** by design — see [`src/b51_render_edit.ts`](/GIT/tdd.md/blob/main/src/b51_render_edit.ts). The admin uses the GitHub PR flow internally; external contributors engage through the issue tracker on Forgejo, not via GitHub PRs. + +## The `/goal` workflow + +The contract for every PR on this site is a `/goal` slash command. The canonical workflow definition lives at [`/goals/migrate-historical-goals`](/goals/migrate-historical-goals); a 5-line summary: + +1. User fires `/goal` with `Goal: ... Done when: ... Constraints ... Load-bearing files ...` structure +2. Agent's **first** action: write the verbatim text to `goals/.md` with `status: pending`, branch + commit +3. Implementation work happens in subsequent commits +4. PR body MUST include the verbatim `/goal` under a `## /goal` heading (defense-in-depth) +5. Final commit before deploy flips `status: shipped` + fills `merge_sha` + +Every shipped `/goal` is browseable at [`/goals`](/goals). + +## SAMA layer convention + +The file prefix encodes the layer. The canonical definition is in [`/sama/v2 §1.1`](/sama/v2#11-layers); the table below shows concrete examples from this repo: + +| Prefix | Layer | Means | Example | +|---|---|---|---| +| `a*_` | 0 — Pure | data + types, no I/O | [`a31_blog.ts`](/GIT/tdd.md/blob/main/src/a31_blog.ts) | +| `b*_` | 1 — Core | pure logic, no I/O | [`b32_sitemap.ts`](/GIT/tdd.md/blob/main/src/b32_sitemap.ts) | +| `c*_` | 2 — Adapter | parses boundaries; DB, network, fs | [`c14_git.ts`](/GIT/tdd.md/blob/main/src/c14_git.ts) | +| `d*_` | 3 — Entry | route handlers, app bootstrap | [`d21_app.ts`](/GIT/tdd.md/blob/main/src/d21_app.ts) | +| `b51_` | 1 — Core (render) | pure HTML rendering | [`b51_render_layout.ts`](/GIT/tdd.md/blob/main/src/b51_render_layout.ts) | + +The Law (§1.2): imports flow downward only. The verifier rejects upward or sideways edges mechanically. + +## How to add a blog post + +1. Write `content/blog/.md` with the body +2. Add an entry to `ALL_POSTS` in [`src/a31_blog.ts`](/GIT/tdd.md/blob/main/src/a31_blog.ts) with `{ slug, title, description, date }` +3. The sitemap, blog index, and `/blog/` route pick it up automatically — registry is the single source of truth +4. Branch → PR → merge → deploy + +Worked example: [`/blog/sama-v2-sitemap-implementation-plan`](/blog/sama-v2-sitemap-implementation-plan). + +## How to add a `/goal` + +Type `/goal` in conversation with the agent. The agent handles the rest per the workflow above. + +If you're not the admin: file an issue at [git.tdd.md/syntaxai/tdd.md/issues](https://git.tdd.md/syntaxai/tdd.md/issues) using the `Goal: ... Done when: ... Constraints ... Load-bearing files ...` shape. The admin (or agent on their behalf) fires it. + +## How to add an image + +1. Write `public/images/.svg` with a `https://tdd.md` watermark text somewhere on the canvas (typically bottom-right in a muted color) +2. Render PNG: `rsvg-convert -w 1200 -h 600 public/images/.svg -o public/images/.png` +3. Reference from markdown: `![alt](/images/.png?v=1)` + +The `/images/*` wildcard is served by the fallback handler in [`d21_handlers_fallback.ts`](/GIT/tdd.md/blob/main/src/d21_handlers_fallback.ts) — no per-image route registration needed. + +## How to add a Layer-1 helper + +1. Write `src/b32_.ts` — pure, no I/O, single export per concern +2. Write the **sibling** `src/b32_.test.ts` — required by [`§4.3 Modeled-tests`](/sama/v2#43-modeled-tests) +3. Caller (typically Layer 2 or Layer 3) imports from `./b32_.ts` + +Worked example: [`b32_sitemap.ts`](/GIT/tdd.md/blob/main/src/b32_sitemap.ts) + [`b32_sitemap.test.ts`](/GIT/tdd.md/blob/main/src/b32_sitemap.test.ts). + +## How to add a top-level directory + +⚠ **Important**: every new top-level directory needs a corresponding `COPY ./` line in [`Containerfile`](/GIT/tdd.md/blob/main/Containerfile). Without it, the directory doesn't ship with the runtime image and request-time disk reads return 404. This was discovered in PR #46 when `/goals/` returned 404 in production. + +Checklist for a new top-level dir: +1. Create the directory and any initial files +2. Add `COPY ./` to `Containerfile` (after the existing COPY lines) +3. Live-verify a file from the new directory loads after deploy + +## Anti-fudge defaults + +Every PR satisfies these without exception: + +- **Site language**: English only. No Dutch in user-facing strings (blog bodies, banners, error text, page copy) +- **Tests stay green**: `bun test` must pass; new behaviour gets a sibling test +- **`/sama/v2/verify` stays 7/7 ✓**: structural choices must preserve the verifier verdict +- **No `--no-verify`, no force-push to main**: hooks exist for a reason +- **GitHub flow via `flatpak-spawn`**: the sandbox doesn't have `gh`; use `flatpak-spawn --host gh ...` + +## Branch / deploy flow (admin path) + +The full sequence the agent (or admin) runs end-to-end: + +1. `git checkout -b ` — branch from main +2. Implementation work + sibling tests +3. `git push -u origin ` + `gh pr create` (body includes `## /goal` verbatim per workflow) +4. `gh pr merge --merge --delete-branch` after CI green +5. `git checkout main && git pull origin main` — sync local +6. `git push p620 main` — push to self-hosted bare repo +7. `flatpak-spawn --host /var/home/scri/Documents/tdd.md/scripts/p620/deploy-tdd-md.sh` — rebuild + redeploy container +8. Live-verify: curl the changed URLs, confirm `/sama/v2/verify` still 7/7 ✓ + +## When in doubt + +- The spec ([`/sama/v2`](/sama/v2)) is the single source of truth for architectural rules +- The verifier ([`/sama/v2/verify`](/sama/v2/verify)) is the single source of truth for whether a commit is conformant +- The `/goals` registry ([`/goals`](/goals)) is the single source of truth for what every PR was held against +- This file links to all three. If it contradicts any of them, the canonical artifact wins; please file an issue at [git.tdd.md/syntaxai/tdd.md/issues](https://git.tdd.md/syntaxai/tdd.md/issues). diff --git a/Containerfile b/Containerfile index a724bd238dc5d40d377b87aa7d2a158a05a5432b..c530c2919538e926f22696829724c8f1aea28555 100644 --- a/Containerfile +++ b/Containerfile @@ -13,7 +13,7 @@ FROM docker.io/oven/bun:1-alpine AS runtime RUN apk add --no-cache git WORKDIR /app COPY --from=deps /app/node_modules ./node_modules -COPY package.json bun.lock tsconfig.json sama.profile.toml ./ +COPY package.json bun.lock tsconfig.json sama.profile.toml CONTRIBUTING.md ./ COPY src ./src COPY content ./content COPY goals ./goals diff --git a/src/b32_contributing_drift.test.ts b/src/b32_contributing_drift.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..bdf6c3b6a2039da94a5d4c9bc8f4e0af3c0081a7 --- /dev/null +++ b/src/b32_contributing_drift.test.ts @@ -0,0 +1,45 @@ +// Drift-detection sibling test for ./CONTRIBUTING.md. +// CONTRIBUTING.md is a SYNTHESIS that links to canonical sources — +// it must not become a restater. These tests check that the load- +// bearing links are still present + the file hasn't bloated past +// a ceiling that would suggest someone copied rule definitions +// inline instead of linking out. +// +// Lives next to b32_*.ts as a Layer-1-adjacent meta-test (it reads +// disk at test time, which is allowed for sibling tests per /sama/v2 +// §4.3 — see b32_sama_verify.test.ts for the same shape). + +import { describe, expect, test } from "bun:test"; + +const contributingMd = await Bun.file("./CONTRIBUTING.md").text(); + +describe("CONTRIBUTING.md drift detection", () => { + test("links to the SAMA v2 spec (canonical rules)", () => { + expect(contributingMd).toContain("/sama/v2"); + }); + + test("links to the /goal workflow's canonical /goal file", () => { + expect(contributingMd).toContain("/goals/migrate-historical-goals"); + }); + + test("links to the on-ramp gap drama post", () => { + expect(contributingMd).toContain("/blog/sama-v2-on-ramp-gap"); + }); + + test("links to the chain-gap drama post (prior context)", () => { + expect(contributingMd).toContain("/blog/sama-v2-goal-chain-gap"); + }); + + test("links to the external-engagement issue tracker", () => { + expect(contributingMd).toContain("git.tdd.md/syntaxai/tdd.md/issues"); + }); + + test("links to /sama/v2/verify as the anti-fudge gate", () => { + expect(contributingMd).toContain("/sama/v2/verify"); + }); + + test("does not exceed bloat ceiling (250 lines)", () => { + const lines = contributingMd.split("\n").length; + expect(lines).toBeLessThan(250); + }); +}); diff --git a/src/b32_sitemap.test.ts b/src/b32_sitemap.test.ts index 8cdfb8ded7ef17d758ee34c22b4d1664f227b9dc..59962f4b8b0143233d450075ede19c53d18160c8 100644 --- a/src/b32_sitemap.test.ts +++ b/src/b32_sitemap.test.ts @@ -89,10 +89,11 @@ describe("renderSitemap", () => { }); describe("STATIC_PATHS", () => { - test("covers the load-bearing routes (incl. /goals from goal #1)", () => { + test("covers the load-bearing routes (incl. /goals + /contributing)", () => { expect(STATIC_PATHS).toEqual([ "/", "/blog", + "/contributing", "/games", "/goals", "/leaderboard", diff --git a/src/b32_sitemap.ts b/src/b32_sitemap.ts index 502b666ceab1682ee7e404dd1f7a254870348c8f..3ecedf73f533fc62e0b34ab4f50f01fcd59a5447 100644 --- a/src/b32_sitemap.ts +++ b/src/b32_sitemap.ts @@ -16,6 +16,7 @@ export interface SitemapUrl { export const STATIC_PATHS: ReadonlyArray = [ "/", "/blog", + "/contributing", "/games", "/goals", "/leaderboard", diff --git a/src/b51_render_layout.ts b/src/b51_render_layout.ts index add70e5572302cd74d404f57a9ebcfff678dcf6b..cdd70160761fe921355aa9b1471d11869bd393f1 100644 --- a/src/b51_render_layout.ts +++ b/src/b51_render_layout.ts @@ -11,7 +11,7 @@ import type { Phase } from "./a31_commits.ts"; const STYLE_CSS = "./public/style.css"; const css = await Bun.file(STYLE_CSS).text(); -export type Section = "home" | "games" | "guides" | "blog" | "agents" | "leaderboard" | "sama" | "goals"; +export type Section = "home" | "games" | "guides" | "blog" | "agents" | "leaderboard" | "sama" | "goals" | "contributing"; export interface PageOptions { title: string; @@ -44,7 +44,7 @@ const navLink = (href: string, label: string, active: boolean): string => { return `${label}`; }; -const nav = (active?: Section): string => ``; +const nav = (active?: Section): string => ``; export const renderPage = async (opts: PageOptions): Promise => { const body = opts.bodyHtml ?? await marked.parse(opts.bodyMarkdown ?? "", { gfm: true, breaks: false }); diff --git a/src/d21_app.ts b/src/d21_app.ts index 4809c461e071a7422cf99495b6e02703190c8f3f..85c3defae187af85376bdfac7ff3fe4b3121da3a 100644 --- a/src/d21_app.ts +++ b/src/d21_app.ts @@ -51,6 +51,7 @@ import { goalsLandingHandler, goalSlugHandler, } from "./d21_handlers_goals.ts"; +import { contributingHandler } from "./d21_handlers_contributing.ts"; import { editPageHandler } from "./d21_handlers_edit.ts"; import { adminListHandler, @@ -420,6 +421,8 @@ ${rows} "/goals/:slug": goalSlugHandler, + "/contributing": contributingHandler, + "/games/:kata": async (req) => { const res = await renderKata(req.params.kata); if (res) return res; diff --git a/src/d21_handlers_contributing.ts b/src/d21_handlers_contributing.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa23d5c865b08025fb36afbefeb87862702c6396 --- /dev/null +++ b/src/d21_handlers_contributing.ts @@ -0,0 +1,30 @@ +// c21 — handler: /contributing reads ./CONTRIBUTING.md from the repo +// root and renders it through the docs layout. Mirrors the shape of +// other content-driven pages (samaLandingHandler, blogLandingHandler) +// but reads from the project-meta root file rather than from a +// per-section content/ subdirectory. The file is also browseable at +// /GIT/tdd.md/blob/main/CONTRIBUTING.md via the SAMA-native source +// viewer — same content, two URLs. + +import { renderDocsPage } from "./b51_render_docs_layout.ts"; +import { htmlResponse, renderNotFound } from "./b51_render_layout.ts"; + +export const contributingHandler = async (): Promise => { + const file = Bun.file("./CONTRIBUTING.md"); + if (!(await file.exists())) { + const html = await renderNotFound("/contributing"); + return htmlResponse(html, 404); + } + const body = await file.text(); + const html = await renderDocsPage({ + title: "Contributing — tdd.md", + description: + "How to add a feature, blog post, /goal, image, or top-level directory to tdd.md. Single-source-of-truth on-ramp that links to the canonical artifacts; doesn't restate them.", + bodyMarkdown: body, + ogPath: "https://tdd.md/contributing", + active: "contributing", + pathForDocs: "/contributing", + editPathOverride: null, + }); + return htmlResponse(html); +};