diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..420c056 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +# Exit immediately on error, treat unset variables as errors, fail on pipe errors +set -euo pipefail + +ROOT=$(git rev-parse --show-toplevel) +RUMDL="$ROOT/node_modules/.bin/rumdl" + +# Intentionally only use the npm-pinned rumdl (package.json devDependency), +# never a globally installed binary (e.g. via cargo/brew) on PATH. Different +# rumdl versions format markdown differently, so falling back to whatever +# version a dev happens to have installed causes the pre-push hook to +# reformat files inconsistently across machines and block pushes with +# unrelated formatting diffs. See: +# https://github.com/holdex/marketing-website/issues/1138 +if [ ! -x "$RUMDL" ]; then + echo "rumdl not found — run npm install in the repo root" + exit 1 +fi + +cd "$ROOT" + +# git passes one line per ref being pushed via stdin: +# +while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do + # All-zeros local SHA means the ref is being deleted — nothing to check + if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then + continue + fi + + if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then + # All-zeros remote SHA means the branch doesn't exist on the remote yet. + # Diffing against git's empty-tree object would treat every file in the repo + # as "added" and lint the whole tree, blocking the push on unrelated + # pre-existing formatting debt. Instead, scope to only what this branch + # changed relative to its base: the merge-base against the default branch. + # Fall back to the empty-tree object if that can't be resolved (e.g. no + # origin/HEAD and no origin/main), so a first push is never left unchecked. + default_ref=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || echo "origin/main") + base=$(git merge-base "$default_ref" "$local_sha" 2>/dev/null || echo "4b825dc642cb6eb9a060e54bf8d69288fbee4904") + else + # Branch already exists — only check files changed in the commits being pushed + base="$remote_sha" + fi + + # List markdown files touched by the commits about to be pushed. + # --diff-filter=ACMR keeps Added/Copied/Modified/Renamed (the new path) and + # excludes Deleted, so renamed-away or removed files aren't linted (they no + # longer exist on disk and would fail rumdl with "File not found"). + files=$(git diff --name-only --diff-filter=ACMR "$base" "$local_sha" -- '*.md' 2>/dev/null || true) + + if [ -n "$files" ]; then + echo "$files" | xargs "$RUMDL" check --fix + + # If rumdl modified any of the linted files the working tree will be dirty — + # block the push. Scope the check to the linted files so unrelated uncommitted + # changes elsewhere in the tree are not misattributed to rumdl. + if ! git diff --quiet -- $files; then + echo "" + echo "rumdl check --fix modified the following files — commit the fixes and push again:" + git diff --name-only -- $files + exit 1 + fi + fi + + # When any doc changed, audit the rules system and the docs tree. The checks + # are holistic (cross-file: id-to-filename, dependency resolution, index + # completeness, and reachability from the root README), so a change to one + # file can orphan or break an invariant in another. Query git directly (not the + # ACMR-filtered $files) so a deletion — e.g. removing a rule — still triggers + # the audit and cannot silently break a dependency or index link. + docs_changed=$(git diff --name-only "$base" "$local_sha" -- 'docs/' 'README.md' 2>/dev/null || true) + if [ -n "$docs_changed" ]; then + node "$ROOT/scripts/check-rules.mjs" + fi +done + +exit 0 diff --git a/README.md b/README.md index 4bb9774..3ce5267 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,36 @@ starts here. ## For Developers -_Developers_ are everyone creating value—business developers, designers, +_Developers_ are everyone creating value: business developers, designers, engineers, marketers, and beyond. We build businesses, products, partnerships, customer relationships, processes, and delivery methods, crafting the future we envision. -Align with: - -- [Contributing Guidelines](./docs/CONTRIBUTING.md) -- [Developer Rules](./docs/rules/README.md) -- [Advocacy Guidelines](./docs/ADVOCACY.md) -- [Code of Conduct](./docs/CODE_OF_CONDUCT.md) -- [Leave Policy](./docs/LEAVE_POLICY.md) -- [Compensation Guide](./docs/COMPENSATION.md) -- [Referral Program](./docs/REFERRAL.md) -- [Expenses](./docs/EXPENSES.md) +Everything is indexed from the [documentation index](./docs/README.md): start +there to reach the contributing guide, the developer rules, and the rest. Subscribe to repository notifications to stay updated with frequent fixes and improvements. +## Setup + +### Local + +After cloning, run `npm install` once to install the pinned `rumdl` version and +enable the markdown lint and rules-audit checks on push (`postinstall` sets +`core.hooksPath` to `.githooks` automatically). + +### Stage / Preview + +This repository is documentation, not a deployed service. Open a pull request to +preview changes: reviewers read the rendered markdown on the PR branch before it +merges. + +### Production + +The `main` branch is the published source of truth, and merged changes are live +for the whole organization immediately. There is no separate hosting to deploy. + ## Claude Code Skills Clone this repository locally and symlink the commands directory to make all diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index b1a0690..566c7e6 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -5,320 +5,13 @@ approachable and respectful. > [!TIP] > You can use [Wizard GitHub App][2] and [Wizard Browser Extension][1] to -> simplify some of the workflows described in these Guidelines. +> simplify some of the workflows the rules describe. -These conventions are being restructured into a -[rules system](./rules/README.md): small numbered files that each enforce one -checkable behavior. The Authoring rules (`DEV-0xx`) define how a rule is -written; the remaining categories migrate the sections below into rules. This -document threads them with the workflow narrative. - -## Table of Contents - -- [Getting started](#getting-started) - - [Specs](#specs) -- [Communication Guidelines](#communication-guidelines) -- [PR Requirements](#pr-requirements) - - [Commit Signature Verification](#commit-signature-verification) - - [Scoping](#scoping) - - [Naming](#naming) - - [PR Lifecycle](#pr-lifecycle) - - [Review Process](#review-process) - -## Getting started - -There are three core contribution pillars: - -1. **Goal**: a business aim -1. **Problem**: a barrier to achieving the Goal -1. **Solution**: the deliverable that resolves the Problem - -Each pillar is enforced by rules in the [Contribution model](./rules/README.md) -category: - -- [DEV-110](./rules/DEV-110.md): take ownership of a Goal -- [DEV-120](./rules/DEV-120.md): keep the Goal description to the allowed - sections -- [DEV-130](./rules/DEV-130.md): understand and agree the Spec first -- [DEV-140](./rules/DEV-140.md): give an ETA once the goal is clear -- [DEV-150](./rules/DEV-150.md): map every barrier blocking the goal -- [DEV-160](./rules/DEV-160.md): write a clear Problem statement -- [DEV-170](./rules/DEV-170.md): deliver work as a pull request -- [DEV-180](./rules/DEV-180.md): keep the Spec as unimplemented behavior and - graduate it - -Goals can be managed with Wizard, which sets the ETA, attaches Google Documents, -and more. See the [Wizard commands](https://wizard.holdex.io/docs/commands), or -use the **Create Google Document** button in the GitHub sidebar -([Wizard Browser Extension][1]). - -### Specs - -A Spec describes the intended behavior for a Goal, not what currently exists but -what the Goal aims to deliver. It is a markdown file in `docs/specs/`, linked -from the Goal under `# Spec`. [DEV-180](./rules/DEV-180.md) governs how its -sections graduate into `docs/` as behavior ships: - -```text -docs/specs/.md ← only unimplemented sections -docs/.md ← only what is currently shipped -``` - -#### Spec format - -```md ---- -goal: ---- - -# Feature Name - -## Overview - -What this Goal enables for users. - -## [Section] - -Describe what users can do, not how the system works internally. -``` - -Sections are author-defined. Keep them user-focused and scoped to observable -behavior. Include a `## Design` section using the markup described in -[Design PRs](#design-prs) when the Goal has a design component. - -#### Discussing a Spec - -If the Spec PR is not yet merged, propose changes via review comments on that -PR. If the Spec is already merged, open a new PR against the spec file. Do not -use Goal issue comments for scope discussions; they belong in the Spec. - -## Communication Guidelines - -### Discussion channels - -Direct discussions to the appropriate channel: - -- **Spec file** — clarifications about Goal scope or business context; propose - changes via PR or review comments on an open Spec PR -- **Problem issues** — tracking obstacles that prevent achieving the Goal -- **Goal issues** — linking Specs, tracking Problems, and monitoring progress - only - -> [!IMPORTANT] -> Do not post Problem status updates, PR notifications, or progress updates in -> Goal issues. The Goal → Problem → PR chain makes these redundant and adds -> noise. - -If you identify a potential new problem but are unsure whether it is planned: - -1. Check if there is an existing Problem issue related to your concern. -1. If not, open a PR against the Spec file, or leave a review comment if the - Spec PR is not yet merged. -1. If necessary, create a new Problem issue and discuss it there. - -If someone's action is required to unblock progress, assign them to the Goal -issue so the dependency is visible. - -### Referencing issues and PRs - -When referencing issues or pull requests, use a list item format — GitHub -automatically expands it to show the title. - -**Correct** — use a list item: - -```md -See these related items: - -- -- -- #4 -- #12 -``` - -**Incorrect** — avoid inline pasting: - -```md -Check this out: Related: See - for details -``` - -## PR Requirements - -> [!WARNING] -> PRs that do not meet the following requirements will be rejected. - -Before marking your PR as ready for review, confirm: - -- [ ] Commits are signed -- [ ] PR scope fits within 3–4 hours of work -- [ ] All CI checks pass -- [ ] PR is linked to a Problem issue -- [ ] At least one reviewer is assigned -- [ ] Time is reported -- [ ] PR title follows `type(scope): action` naming convention -- [ ] Preview link is included (if applicable) -- [ ] README is updated to reflect any functional changes -- [ ] Spec sections moved to `docs/` for any behavior this PR delivers (if - applicable) - -### Commit Signature Verification - -All commits must be signed. See -[GitHub's documentation on commit signature verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification). - -> [!NOTE] -> We recommend signing commits using an -> [SSH key](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#ssh-commit-signature-verification). -> Ensure your Git version supports SSH signature verification (Git 2.34 or -> later). - -### Scoping - -> [!TIP] -> Here's a [good resource](https://youtu.be/bmSAYlu0NcY?si=2lLQeY1PGCY9tcvX) on -> software design philosophy. - -When planning the scope of work, make sure you -[keep PRs small](https://artsy.github.io/blog/2021/03/09/strategies-for-small-focused-pull-requests/). -You must be able to complete your PR within 3–4 hours. -If the solution requires more time, then decompose it into smaller independent -PRs. In case your smaller PRs can't be used on production, use feature flags. - -PRs with no activity for 24 hours are closed unless a comment explains the -delay. - -When introducing functional changes, cross-check the README and update it in the -same PR. If your change affects anything documented there — setup steps, -environment requirements, file references — the README must stay in sync. - -When adding new documentation files, ensure they are reachable via interlinking -from the root entry point. Do not create orphaned files. - -Do not duplicate content across files. Each piece of information — procedures, -templates, configuration steps — must live in exactly one place. Reference it -from other docs rather than copying it. - -### Naming - -> [!NOTE] -> We use PR titles to communicate changes to all stakeholders, including -> non-technical users. - -PR names must be: - -1. **User-focused**: Describe what users gain, not technical implementation -1. **Follow [Conventional Commits](https://www.conventionalcommits.org)** -1. **Clear & simple** (present tense, action-oriented) -1. **Under 65 characters** - -| **Good Examples** ✅ | **Bad Examples** ❌ | **Why?** | -| ---------------------- | ------------------------------ | ------------------ | -| `feat(ui): play music` | `Create player` | Missing scope/type | -| `fix(sdk): mute sound` | `Fix: add file to mute sound` | Technical details | -| `test(api): open door` | `Feat: modified door function` | Vague, past tense | - -A feature isn't a button, toggle, or handler — it's what the user gains from it. -Ask _"What will users be able to do?"_ not _"What am I building?"_ Use action -verbs: _View, Play, Customize, Save_. - -> [!WARNING] -> This rule applies to **all PR types**, including `docs`. Do not use verbs that -> describe what you did ("document", "update", "add") — use verbs that describe -> what users can now do. -> -> | **Good** ✅ | **Bad** ❌ | -> | --------------------------------------------- | ------------------------------------------ | -> | `docs(typefully): log in with shared account` | `docs(typefully): document shared account` | -> | `docs(api): authenticate with OAuth` | `docs(api): add OAuth section to README` | - -#### Design PRs - -Design PRs use `docs(ui)` as the type and scope. e.g.: -`docs(ui): design table component` - -Add a `## Design` section to the relevant Spec file. Structure it with the -following markup: - -```text -## Design -- [/page](https://figma.com/your-design-file-url) - - ./page/{params} - - (group) - - [[state]](https://figma.com/your-design-file-url) -``` - -**Key:** - -- **`/...`** — a page -- **`{...}`** — a dynamic URL parameter -- **`(...)`** — a grouping of related features or components -- **`[...]`** — a specific state (e.g. popup or modal) -- Indentation represents nesting hierarchy - -Example: - -```text -## Design -- [/lending](https://figma.com/your-design-file-url) - - ./vaults/{poolAddr} - - (Auction) - - [[Withdraw Popup]](https://figma.com/your-design-file-url) - - [[Bid Popup]](https://figma.com/your-design-file-url) -``` - -### PR Lifecycle - -Follow these steps in order from start to submission: - -1. **Open a [draft - PR](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests)** - right away when you start working on a Problem. -1. **[Link the - PR](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)** - to the corresponding Problem issue using a closing keyword. -1. **Assign yourself** so it is clear who is working on it. -1. **Report your time** spent across all stages: planning (40%), implementation, - and QA (20–30%). Open the PR early so time tracking starts from the - beginning, including investigation. -1. **Assign at least one reviewer** (team or individual). -1. **Include a preview link** — if your changes are visually verifiable (UI, - design, or any deployable artifact), add a link to the deployed preview or - prototype in the PR description. -1. **Mark as ready for review** only once all steps above are complete. -1. **Resolve all CI checks** — CI runs after marking ready; do not request - approval until all checks pass. - -> [!WARNING] -> Do not merge without an approved review and passing CI checks. - -### Review Process - -#### Giving a Review - -If a PR is not ready to merge, you **must** use **Request Changes** (reject). Do -not leave a plain comment when rejection is warranted — comments do not block -merging, are not recorded as rejections, and prevent the author from -re-requesting a review. - -Use **Request Changes** (reject) for objective problems: - -- PR doesn't solve the stated problem. -- A bug is introduced. -- Code style is inconsistent. -- Required guidelines are violated. - -Use **Comment** for optional improvements or suggestions that should not block -the PR. - -#### Scout Approach - -When not actively working on a PR, look for PRs that need reviewers and offer -timely feedback to keep work moving. - -#### Code Quality - -Deliver bug-free software. Push back on subjective feedback — reviewers are a -final safety check, not a QA team. +How to contribute is defined by the [rules system](./rules/README.md): small +numbered `DEV-` files that each enforce one checkable behavior, grouped into +authoring, contribution model, communication, PR requirements, and review. That +index is the canonical guide, so start there. This file stays a thin pointer so +it does not repeat, and drift from, the rules. --- diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..bc6ff6d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,38 @@ +# Documentation + +The index for the `docs/` tree, grouped by who reads it. The repo-root +[README](../README.md) is the entry point and links here; this file links every +document below it, so nothing is orphaned (see +[DEV-337](./rules/DEV-337.md)). + +## Contributors + +How work is proposed, built, reviewed, and shipped. + +- [Contributing Guidelines](./CONTRIBUTING.md) +- [Developer Rules](./rules/README.md) + +## Team + +Working here: policies and programs. + +- [Advocacy](./ADVOCACY.md) +- [Application Success](./APPLICATION_SUCCESS.md) +- [Code of Conduct](./CODE_OF_CONDUCT.md) +- [Compensation](./COMPENSATION.md) +- [Expenses](./EXPENSES.md) +- [Leave Policy](./LEAVE_POLICY.md) +- [Referral Program](./REFERRAL.md) +- [Trial](./TRIAL.md) + +## Product (end users) + +Shipped, user-facing product documentation lives under `docs/product/`, added as +features ship. It is the only subtree meant to render as an end-user docs site. +See [DEV-180](./rules/DEV-180.md) and [DEV-390](./rules/DEV-390.md). + +## Specs (developers) + +Intended, not-yet-shipped behavior lives under `docs/specs/`, a planning +artifact for developers rather than end-user documentation. See +[DEV-180](./rules/DEV-180.md). diff --git a/docs/rules/DEV-050.md b/docs/rules/DEV-050.md index 3a034ae..b013be9 100644 --- a/docs/rules/DEV-050.md +++ b/docs/rules/DEV-050.md @@ -19,7 +19,7 @@ single broken thing and stop. Motivation, context, alternatives, and any restating of the title belong in the Solution, or nowhere. The character cap is the forcing function: if you are over it, you are explaining, not stating. -## Acceptance Criteria +### Acceptance Criteria - [ ] The Problem paragraph is 250 characters or fewer - [ ] It states only what is broken, with no background or justification diff --git a/docs/rules/DEV-125.md b/docs/rules/DEV-125.md new file mode 100644 index 0000000..7da6686 --- /dev/null +++ b/docs/rules/DEV-125.md @@ -0,0 +1,62 @@ +--- +id: DEV-125 +title: "Write a Spec in the Standard Format" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-180", "DEV-350"] +--- + +## Problem + +A Spec written without a shared structure is hard to read and review: readers +cannot tell where the goal, the overview, or the behavior sections are, and +specs drift into describing internals instead of user behavior. + +## Solution + +Give every Spec the same skeleton so any reader knows where to look, and keep it +about what users can do, not how the system works. + +1. Store the Spec as a markdown file under `docs/specs/`, linked from its Goal + under `# Spec`; its sections graduate to `docs/product/` per + [DEV-180](./DEV-180.md). +1. Start with `goal:` frontmatter linking the Goal, then an H1 feature name and + a `## Overview` of what the Goal enables for users. +1. Add author-defined `##` sections that describe observable user behavior, not + internal mechanics. +1. Include a `## Design` section using the markup in [DEV-350](./DEV-350.md) + when the Goal has a design component. + +```md +--- +goal: +--- + +# Feature Name (max 45 chars) + +## Objective +(describes business objective, max 250 chars) + +## Key results +(three items; max 160 chars each) + +## User Types +(list of end-users' types and their short definition; each item max 150 chars) + +## Key Concepts + +(subsections describing key concepts end-user needs to know about; +the relevant methods (actions) specific users can use) + +## [Section] + +Describe what users can do, not how the system works internally. +``` + +### Acceptance Criteria + +- [ ] The Spec is a `docs/specs/` file with `goal:` frontmatter, an H1 name, and + a `## Overview` +- [ ] Sections describe user-observable behavior, not internal mechanics +- [ ] A Goal with a design component includes a `## Design` section per DEV-350 diff --git a/docs/rules/DEV-180.md b/docs/rules/DEV-180.md index 1d6b98d..e6b0fc0 100644 --- a/docs/rules/DEV-180.md +++ b/docs/rules/DEV-180.md @@ -9,7 +9,7 @@ depends_on: ["DEV-110", "DEV-170"] ## Problem -When a Spec describes behavior that already shipped, or end-user `docs/` +When a Spec describes behavior that already shipped, or `docs/product/` describes behavior that was never built, a reader cannot tell what the product actually does today. The backlog and the documentation rot into fiction. @@ -19,10 +19,10 @@ Split intended behavior from shipped behavior, and move sections across as work lands. 1. `docs/specs/.md` holds only unimplemented behavior for a Goal. - `docs/.md` holds only what currently ships. + `docs/product/.md` holds only what currently ships. 1. Each PR that delivers behavior moves the corresponding sections out of the - spec and into the appropriate file under `docs/`. A spec may graduate into - more than one `docs/` file. + spec and into the appropriate file under `docs/product/`. A spec may graduate + into more than one `docs/product/` file. 1. When scope is added after the Spec merges, open a new PR against the spec file and create Problem issues for the added scope. 1. When every section has graduated, keep the spec file as frontmatter only. @@ -31,6 +31,7 @@ lands. ### Acceptance Criteria - [ ] `docs/specs/` holds only unimplemented behavior -- [ ] `docs/` holds only shipped behavior -- [ ] Each shipping PR graduates the delivered sections from spec to `docs/` +- [ ] `docs/product/` holds only shipped behavior +- [ ] Each shipping PR graduates the delivered sections from spec to + `docs/product/` - [ ] A fully graduated spec file is kept as frontmatter only, not deleted diff --git a/docs/rules/DEV-210.md b/docs/rules/DEV-210.md new file mode 100644 index 0000000..418eb3a --- /dev/null +++ b/docs/rules/DEV-210.md @@ -0,0 +1,38 @@ +--- +id: DEV-210 +title: "Route Discussion to the Right Channel" +status: "active" +enforcement: "manual" +severity: "warning" +depends_on: ["DEV-110"] +--- + +## Problem + +When scope debates and status updates land in Goal issues, they bury the +Goal-to-Problem-to-PR chain in noise, and scope decisions drift away from the +Spec that is supposed to hold them. + +## Solution + +Direct each discussion to its channel. + +- **Spec file**: clarifications about Goal scope or business context. Propose + changes via a PR against the spec, or review comments on an open Spec PR. +- **Problem issues**: tracking the obstacles that prevent achieving the Goal. +- **Goal issues**: linking Specs, tracking Problems, and monitoring progress + only. Do not post Problem status updates, PR notifications, or progress + updates here. + +If you spot a potential new problem but are unsure it is planned: check for an +existing Problem issue; if none, open a PR against the Spec (or a review comment +if the Spec PR is open); if needed, create a new Problem issue and discuss it +there. When someone must act to unblock progress, assign them to the Goal issue +so the dependency is visible. + +### Acceptance Criteria + +- [ ] Scope and business clarifications go to the Spec, not Goal comments +- [ ] Goal issues carry only Spec links, Problem tracking, and progress +- [ ] No status updates or PR notifications are posted in Goal issues +- [ ] Blocking dependencies are surfaced by assigning the person to the Goal diff --git a/docs/rules/DEV-220.md b/docs/rules/DEV-220.md new file mode 100644 index 0000000..a608dd6 --- /dev/null +++ b/docs/rules/DEV-220.md @@ -0,0 +1,32 @@ +--- +id: DEV-220 +title: "Reference Issues and PRs as List Items" +status: "active" +enforcement: "manual" +severity: "warning" +--- + +## Problem + +An issue or PR URL pasted inline in a sentence does not expand to its title and +is easily lost in prose. Readers cannot see at a glance what is referenced. + +## Solution + +Reference issues and pull requests as list items, so GitHub expands each to show +its title. + +```md +See these related items: + +- +- #4 +- #12 +``` + +Do not paste references inline in a sentence. + +### Acceptance Criteria + +- [ ] Issue and PR references are list items, not inline in prose +- [ ] Each reference sits on its own line diff --git a/docs/rules/DEV-310.md b/docs/rules/DEV-310.md new file mode 100644 index 0000000..09bc6db --- /dev/null +++ b/docs/rules/DEV-310.md @@ -0,0 +1,25 @@ +--- +id: DEV-310 +title: "Sign Every Commit" +status: "active" +enforcement: "automated" +severity: "error" +--- + +## Problem + +An unsigned commit cannot be verified as authored by the contributor. A PR +carrying unsigned commits fails the requirements and cannot be trusted for +attribution. + +## Solution + +Sign every commit. Signing with an +[SSH key](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#ssh-commit-signature-verification) +is recommended; ensure your Git version supports SSH signature verification (Git +2.34 or later). + +### Acceptance Criteria + +- [ ] Every commit in the PR is signed +- [ ] Each commit shows as verified on GitHub diff --git a/docs/rules/DEV-320.md b/docs/rules/DEV-320.md new file mode 100644 index 0000000..0627b50 --- /dev/null +++ b/docs/rules/DEV-320.md @@ -0,0 +1,33 @@ +--- +id: DEV-320 +title: "Scope a PR to a Few Hours" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-140"] +--- + +## Problem + +A large PR is slow to review, hard to revert, and prone to stalling. The bigger +the change, the longer it sits and the more risk it carries into production. + +## Solution + +Keep each PR small. The time budget is a proxy for diff size, not a target for +effort: with AI assistance the same hours produce far more code, so a loose +budget quietly lets PRs grow into the slow, risky reviews this rule exists to +prevent. + +1. Scope the work so you can complete the PR within 1 to 2 hours. +1. If the solution needs more, decompose it into smaller independent PRs. +1. When a smaller PR cannot ship to production on its own, put it behind a + feature flag. +1. Keep the PR moving. A PR with no activity for 24 hours is closed unless a + comment explains the delay. + +### Acceptance Criteria + +- [ ] The PR is scoped to complete within 1 to 2 hours +- [ ] Larger work is split into independent PRs, feature-flagged where needed +- [ ] Any delay beyond 24 hours is explained in a comment diff --git a/docs/rules/DEV-330.md b/docs/rules/DEV-330.md new file mode 100644 index 0000000..8a3a090 --- /dev/null +++ b/docs/rules/DEV-330.md @@ -0,0 +1,38 @@ +--- +id: DEV-330 +title: "Keep Docs in Sync" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-335", "DEV-337"] +--- + +## Problem + +When a functional change skips the README or a new doc is left unlinked, the +documentation no longer matches the code or cannot be found, and readers act on +information that is wrong or invisible. + +## Solution + +Documentation is only useful while it tracks the code. It falls out of sync when +a change updates behavior but not its docs, or when a new doc lands with nothing +pointing at it. + +1. Ship doc updates in the PR that makes them true. A functional change that + touches setup steps, environment requirements, or file references leaves the + README wrong the moment it merges, and a follow-up PR to fix it often never + comes. +1. Link a new doc from the index it belongs under, the root `README.md` or a + subtree index, per [DEV-337](./DEV-337.md), so it is reachable and not + orphaned. An unreachable file is invisible, so readers fall back on a stale + copy or ask a question the doc already answers. + +Each fact must also live in a single canonical place, per +[DEV-335](./DEV-335.md), so the copy you keep in sync is the only copy. + +### Acceptance Criteria + +- [ ] Functional changes update the README in the same PR +- [ ] New docs are linked from their index and reachable from the root, not + orphaned diff --git a/docs/rules/DEV-335.md b/docs/rules/DEV-335.md new file mode 100644 index 0000000..0b78074 --- /dev/null +++ b/docs/rules/DEV-335.md @@ -0,0 +1,43 @@ +--- +id: DEV-335 +title: "Keep Each Fact in One Canonical Place" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-140"] +--- + +## Problem + +When the same fact is copied into several files, or restated from a system that +owns it, the copies drift apart as one changes and the others do not. Readers +cannot tell which copy is current, and each is a chance to be wrong. + +## Solution + +Every fact has exactly one canonical home; everywhere else references that home +instead of copying it. A copy is a second source of truth that will drift, so +the goal is to make sure there is only ever one. + +1. Keep each fact (a procedure, template, configuration step, or value) in + exactly one file, and link to it from anywhere else that needs it rather than + restating it. +1. When a fact belongs to another system (bot commands, third-party APIs, + another service's config), treat that system's own documentation as its + canonical home and link it, instead of restating details that change without + notice. For example, [DEV-140](./DEV-140.md) links the Wizard command docs + rather than hardcoding a command string. + +A good and bad shape for the same fact: + +```md +Good: To set an ETA, use the [Wizard commands](https://wizard.holdex.io/docs/commands). +Bad: To set an ETA, comment `@holdex issue set-eta ` on the Goal. +``` + +### Acceptance Criteria + +- [ ] Each fact lives in exactly one file; other places link to it, not copy it +- [ ] No fact is duplicated across files +- [ ] Facts owned by another system link to that system's canonical + documentation rather than restating them diff --git a/docs/rules/DEV-337.md b/docs/rules/DEV-337.md new file mode 100644 index 0000000..b4b6cc5 --- /dev/null +++ b/docs/rules/DEV-337.md @@ -0,0 +1,45 @@ +--- +id: DEV-337 +title: "Index Every Docs Tree From a README" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-180"] +--- + +## Problem + +Without a single entry point that links to every document, readers cannot tell +what exists, and a new file is easily added where nothing points to it. The +documentation becomes pages no one can navigate to. + +## Solution + +Give the documentation a navigable spine: one root entry point, and a README +index inside any subtree large enough to need one, so every document is +reachable by following links from the root. + +1. Keep the repo-root `README.md` as the entry point. It links the documentation + index `docs/README.md` (and any other top-level subtree index), so a reader + starts there and reaches everything through the indexes below it. +1. Keep `docs/README.md` as the index of the documentation tree, grouped by + audience: the team handbook, end-user product docs under `docs/product/`, + developer specs under `docs/specs/`, and contributor rules under + `docs/rules/` (see [DEV-180](./DEV-180.md) for the product and spec split). +1. Give every documentation subdirectory its own `README.md` that indexes the + documents in it, and link that index from its parent index, never the files + inside it. Each subtree, such as `docs/product/`, is then self-contained and + can render on its own. +1. Index each document from its nearest README only. A parent links a child + README, not that child's files, so the tree has no shortcuts that skip a + level. A document that no directory README lists is orphaned. + +### Acceptance Criteria + +- [ ] The repo-root README links `docs/README.md`, its one documentation child +- [ ] `docs/README.md` exists and indexes the docs tree grouped by audience +- [ ] Every documentation subdirectory has its own README index, linked from its + parent index +- [ ] Each document is listed in its own directory's README, not linked past it + from an ancestor +- [ ] Every document is reachable from the root through the index spine diff --git a/docs/rules/DEV-338.md b/docs/rules/DEV-338.md new file mode 100644 index 0000000..9ff1199 --- /dev/null +++ b/docs/rules/DEV-338.md @@ -0,0 +1,42 @@ +--- +id: DEV-338 +title: "Give the Repository a Complete Root README" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-330", "DEV-337"] +--- + +## Problem + +A root README that omits what the repository is, where its docs are, or how to +run and deploy it forces every newcomer to reverse-engineer the project from +source. The one page everyone opens first fails them. + +## Solution + +The root README is the first and most-read page in the repository, so it must +tell a newcomer what the project is and how to run it. Where to find the rest of +the docs is the entry-point role [DEV-337](./DEV-337.md) already governs (it +links `docs/README.md`); this rule covers the landing page's own content. + +1. Open with an H1 title and a short description of what the repository is and + does, so a newcomer understands it in one read. +1. Include a Setup section covering every environment the project runs in, each + as its own subsection: Local (run it on your machine), Stage or Preview + (deploy for internal demo and review), and Production (deploy for real use). + Where an environment does not apply, say so and why, rather than omitting it. +1. Keep the commands and steps truthful and in sync with the project, per + [DEV-330](./DEV-330.md). A README that lies about how to run the project is + worse than one that says nothing. +1. Where the repository provides an automated README check, run it before + pushing and enforce it in the pre-push hook or CI. In this repository that is + `npm run check:rules`, run by the pre-push hook; a repo without such a script + verifies these requirements by review instead. + +### Acceptance Criteria + +- [ ] The README opens with an H1 title and a description of what the repository + is +- [ ] Its Setup section has Local, Stage or Preview, and Production subsections +- [ ] Environments that do not apply are stated as such, not omitted diff --git a/docs/rules/DEV-340.md b/docs/rules/DEV-340.md new file mode 100644 index 0000000..9422d84 --- /dev/null +++ b/docs/rules/DEV-340.md @@ -0,0 +1,39 @@ +--- +id: DEV-340 +title: "Name a PR for What Users Gain" +status: "active" +enforcement: "manual" +severity: "error" +--- + +## Problem + +A technical or vague PR title such as `Create player` or +`Fix: add file to mute sound` tells stakeholders nothing about the change. PR +titles communicate to everyone, including non-technical readers. + +## Solution + +Name the PR for what a user can now do. + +1. Follow [Conventional Commits](https://www.conventionalcommits.org): + `type(scope): action`. +1. Make it user-focused: describe what users gain, not the implementation. Ask + "what will users be able to do?", not "what am I building?" Use action verbs + such as View, Play, Customize, Save. +1. Keep it present tense and under 65 characters. +1. Apply this to all PR types, including `docs`. Do not use verbs that describe + what you did ("document", "update", "add"); use verbs for what users can now + do. + +| Good | Bad | +| --- | --- | +| `feat(ui): play music` | `Create player` | +| `docs(api): authenticate with OAuth` | `docs(api): add OAuth section` | + +### Acceptance Criteria + +- [ ] The title follows `type(scope): action` +- [ ] It describes what a user can now do, not the implementation +- [ ] It is present tense and under 65 characters +- [ ] It holds even for `docs` PRs diff --git a/docs/rules/DEV-350.md b/docs/rules/DEV-350.md new file mode 100644 index 0000000..f5f26b4 --- /dev/null +++ b/docs/rules/DEV-350.md @@ -0,0 +1,40 @@ +--- +id: DEV-350 +title: "Mark a Design PR docs(ui) With a Design Section" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-340"] +--- + +## Problem + +Design work without a consistent type and scope, or without a structured design +reference, is hard to find, track, and review against the intended screens. + +## Solution + +Give design work a consistent home. + +1. Use `docs(ui)` as the type and scope. Name it for what users gain, per + [DEV-340](./DEV-340.md), for example `docs(ui): sort the transactions table`. +1. Add a `## Design` section to the relevant Spec file, structured with this + markup: + +```text +## Design +- [/page](https://figma.com/your-design-file-url) + - ./page/{params} + - (group) + - [[state]](https://figma.com/your-design-file-url) +``` + +Key: `/...` a page, `{...}` a dynamic URL parameter, `(...)` a grouping of +related features or components, `[...]` a specific state (for example a popup), +and indentation represents nesting. + +### Acceptance Criteria + +- [ ] The design PR uses the `docs(ui)` type and scope +- [ ] A `## Design` section is added to the relevant Spec +- [ ] The section uses the page, parameter, group, and state markup diff --git a/docs/rules/DEV-360.md b/docs/rules/DEV-360.md new file mode 100644 index 0000000..6fe2ab8 --- /dev/null +++ b/docs/rules/DEV-360.md @@ -0,0 +1,32 @@ +--- +id: DEV-360 +title: "Open Work as a Draft PR Linked to Its Problem" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-170"] +--- + +## Problem + +Work started without a draft PR, or a PR that never links its Problem, is +invisible: no one can see it is underway, and it does not close its Problem when +it merges. + +## Solution + +Make the work visible and traceable from the first commit by opening the PR +before it is finished, not after. An early, linked draft is what lets others +track progress and lets the merge close its Problem. + +1. Open a draft PR as soon as you start work on a Problem, so progress is + visible and time tracking can start early. +1. Link the PR to its Problem issue with a closing keyword (for example + `Closes #123`), so merging the PR closes the Problem. +1. Assign yourself, so it is clear who owns it. + +### Acceptance Criteria + +- [ ] A draft PR is opened at the start of the work +- [ ] It links its Problem issue with a closing keyword +- [ ] The author is assigned to it diff --git a/docs/rules/DEV-365.md b/docs/rules/DEV-365.md new file mode 100644 index 0000000..0003946 --- /dev/null +++ b/docs/rules/DEV-365.md @@ -0,0 +1,34 @@ +--- +id: DEV-365 +title: "Mark a PR Ready Only When It Is Complete" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-360"] +--- + +## Problem + +A PR marked ready before it has a reviewer, a preview, or passing CI wastes +reviewer time on unfinished work and can merge unchecked. + +## Solution + +Treat "ready for review" as a promise that the PR is complete and only the +review remains. Prepare everything first, then flip it, so a reviewer never +opens half-finished work. + +1. Assign at least one reviewer. +1. Include a preview link when the change is visually verifiable. +1. Mark ready for review only once the PR is a linked, self-assigned draft (per + [DEV-360](./DEV-360.md)) with a reviewer and a preview where applicable. +1. Resolve all CI checks. CI runs after the PR is marked ready, so do not + request approval until it passes. +1. Do not merge without an approved review and passing CI. + +### Acceptance Criteria + +- [ ] The PR has at least one reviewer and a preview link where applicable +- [ ] It is marked ready only after it is fully prepared +- [ ] CI passes before approval is requested +- [ ] It is not merged without an approved review and passing CI diff --git a/docs/rules/DEV-370.md b/docs/rules/DEV-370.md new file mode 100644 index 0000000..f671a58 --- /dev/null +++ b/docs/rules/DEV-370.md @@ -0,0 +1,28 @@ +--- +id: DEV-370 +title: "Report Time Across All Stages" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-360", "DEV-340"] +--- + +## Problem + +Unreported or partial time hides the real effort behind a change, which distorts +planning and compensation and makes future estimates unreliable. + +## Solution + +Report time across every stage of the work. + +1. Cover all stages: planning (around 40%), implementation, and QA (around 20 to + 30%). +1. Open the PR early so tracking starts from the beginning, including + investigation. + +### Acceptance Criteria + +- [ ] Time is reported on the PR +- [ ] It spans planning, implementation, and QA +- [ ] The PR was opened early enough to capture investigation time diff --git a/docs/rules/DEV-380.md b/docs/rules/DEV-380.md new file mode 100644 index 0000000..880c69d --- /dev/null +++ b/docs/rules/DEV-380.md @@ -0,0 +1,32 @@ +--- +id: DEV-380 +title: "Enforce Markdown Lint on Push With a Pinned rumdl Hook" +status: "active" +enforcement: "automated" +severity: "error" +--- + +## Problem + +If lint runs from whatever `rumdl` a contributor has installed, versions format +the same file differently and pushes get blocked by unrelated reformatting. If +it is not enforced at all, malformed markdown lands unchecked. + +## Solution + +Make lint automatic and version-stable, from the npm-pinned `rumdl` only. + +1. Pin `rumdl` as a devDependency, so everyone runs the same version and avoids + the version drift that blocks pushes with unrelated reformatting. +1. In `postinstall`, set `git config core.hooksPath .githooks`, so the hook + installs on `npm install`. +1. Add a `.githooks/pre-push` that runs the pinned binary + (`node_modules/.bin/rumdl`), never a global one, over the markdown changed in + the push, and blocks the push if it reformats anything. + +### Acceptance Criteria + +- [ ] `rumdl` is pinned in `devDependencies` +- [ ] `postinstall` sets `core.hooksPath` to `.githooks` +- [ ] `.githooks/pre-push` runs `node_modules/.bin/rumdl`, not a global binary +- [ ] A push containing a lint violation is blocked until it is fixed diff --git a/docs/rules/DEV-390.md b/docs/rules/DEV-390.md new file mode 100644 index 0000000..44e9c95 --- /dev/null +++ b/docs/rules/DEV-390.md @@ -0,0 +1,42 @@ +--- +id: DEV-390 +title: "Update User-Facing Docs in the Same PR" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-180", "DEV-330"] +--- + +## Problem + +When code changes user-facing behavior but its documentation does not, users +follow instructions that no longer match the product. The docs and the software +disagree, and no one can tell which one is wrong. + +## Solution + +User-facing documentation is part of the behavior you ship, not a follow-up +chore. A separate docs PR often never lands, and every hour the docs lag the +product misleads users, so the change and its documentation must move together. + +1. In the same PR that changes user-facing behavior, update the shipped-behavior + docs that describe it (`docs/product/.md`, per + [DEV-180](./DEV-180.md)). Do not defer it to a later PR. +1. Make the documentation describe what the change actually ships, so documented + behavior and real behavior match exactly. +1. Treat any gap between shipped behavior and its user-facing docs as a bug: if + you find docs that already contradict the product, fix them or raise a Bug + for them, rather than leaving the contradiction to stand. + +This is stricter than repo-doc coherence ([DEV-330](./DEV-330.md)): there the +concern is that facts stay single-sourced and reachable; here a mismatch between +the product and its user-facing docs is a defect, not just untidy docs. + +### Acceptance Criteria + +- [ ] A PR that changes user-facing behavior updates the matching + `docs/product/.md` in the same PR +- [ ] The documented behavior matches what the PR actually ships +- [ ] No user-facing doc update is deferred to a separate PR +- [ ] Docs that contradict shipped behavior are treated as a bug, not left to + stand diff --git a/docs/rules/DEV-410.md b/docs/rules/DEV-410.md new file mode 100644 index 0000000..4dce3ef --- /dev/null +++ b/docs/rules/DEV-410.md @@ -0,0 +1,30 @@ +--- +id: DEV-410 +title: "Reject With Request Changes for Objective Problems" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-365"] +--- + +## Problem + +Leaving a plain comment on a PR that should be rejected does not block the +merge, is not recorded as a rejection, and stops the author from re-requesting +review. Real problems slip through as if they were optional. + +## Solution + +Match the review action to the finding. + +1. When a PR is not ready to merge, use **Request Changes** for objective + problems: it does not solve the stated problem, it introduces a bug, its code + style is inconsistent, or it violates a required guideline. +1. Use **Comment** for optional improvements or suggestions that should not + block the PR. + +### Acceptance Criteria + +- [ ] Objective problems get Request Changes, not a plain comment +- [ ] Optional suggestions use Comment +- [ ] A rejection names the objective problem it is based on diff --git a/docs/rules/DEV-415.md b/docs/rules/DEV-415.md new file mode 100644 index 0000000..e6153f1 --- /dev/null +++ b/docs/rules/DEV-415.md @@ -0,0 +1,31 @@ +--- +id: DEV-415 +title: "Re-request Review Explicitly After Addressing Changes" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-410"] +--- + +## Problem + +After a reviewer requests changes and the author fixes them, without an explicit +re-request the reviewer is never notified and the PR keeps its stale "changes +requested" state, so it stalls unseen. + +## Solution + +A resolved review is only resolved once the reviewer knows to look again. +Closing the loop explicitly is what notifies them and clears the blocking state, +so a comment or a fresh push is not enough on its own. + +1. After addressing every requested change, use GitHub's re-request review + control on each reviewer who requested changes. +1. Re-requesting resets that reviewer from "changes requested" to "review + requested" and notifies them, so the PR is no longer silently blocked. + +### Acceptance Criteria + +- [ ] Review is explicitly re-requested from each reviewer who requested changes +- [ ] The review state is reset from "changes requested" to "review requested" +- [ ] Readiness is not signaled by a comment or a push alone diff --git a/docs/rules/DEV-420.md b/docs/rules/DEV-420.md new file mode 100644 index 0000000..0f1fafc --- /dev/null +++ b/docs/rules/DEV-420.md @@ -0,0 +1,27 @@ +--- +id: DEV-420 +title: "Scout Open PRs When Idle" +status: "active" +enforcement: "manual" +severity: "warning" +--- + +## Problem + +PRs wait and stall when no one picks them up for review. Work that is otherwise +ready sits idle because reviewers are focused only on their own changes. + +## Solution + +Reviewing is shared work, not something that happens only to your own PRs. When +your own work is not blocking you, spend that idle time clearing the queue so +ready changes do not wait on attention. + +Look for PRs that have no reviewer or are still waiting on one, and give +feedback while it still moves the work. A fast review on a ready PR is worth +more than a thorough one a day later. + +### Acceptance Criteria + +- [ ] Idle time is spent reviewing PRs that are waiting for a reviewer +- [ ] Feedback is given promptly enough to keep the work moving diff --git a/docs/rules/DEV-430.md b/docs/rules/DEV-430.md new file mode 100644 index 0000000..dfe6a2e --- /dev/null +++ b/docs/rules/DEV-430.md @@ -0,0 +1,32 @@ +--- +id: DEV-430 +title: "Deliver Bug-Free Work; Review Is a Safety Check" +status: "active" +enforcement: "manual" +severity: "error" +depends_on: ["DEV-410"] +--- + +## Problem + +Treating reviewers as the QA team pushes defects downstream, slows everyone, and +lets subjective debates block otherwise sound work. + +## Solution + +Own the quality of what you ship instead of outsourcing it to review. A reviewer +who has to find your bugs becomes a bottleneck, and a defect caught in review +costs more to fix than one you catch before opening the PR. + +Test and self-review the change until you believe it is bug-free, then request +review. Treat the reviewer as a final safety check on work you already trust, +not the QA pass that first exercises it. When feedback is subjective rather than +an objective problem (see [DEV-410](./DEV-410.md)), discuss and push back +instead of absorbing every preference as a required change, so sound work does +not stall on taste. + +### Acceptance Criteria + +- [ ] Work is delivered bug-free, not dependent on review to find defects +- [ ] Review is treated as a final safety check, not QA +- [ ] Subjective feedback is pushed back on rather than blocking the PR diff --git a/docs/rules/README.md b/docs/rules/README.md index d0cb1f3..0baf332 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -31,6 +31,7 @@ Solution, Spec. - [DEV-110](./DEV-110.md): take ownership of a Goal - [DEV-120](./DEV-120.md): keep the Goal description to the allowed sections +- [DEV-125](./DEV-125.md): write a Spec in the standard format - [DEV-130](./DEV-130.md): understand and agree the Spec first - [DEV-140](./DEV-140.md): give an ETA once the goal is clear - [DEV-150](./DEV-150.md): map every barrier blocking the goal @@ -39,8 +40,40 @@ Solution, Spec. - [DEV-180](./DEV-180.md): keep the Spec as unimplemented behavior and graduate it -The remaining categories are migrating from the -[Contributing Guidelines](../CONTRIBUTING.md) into rules. +### 2. Communication + +Where discussion goes and how work is referenced. + +- [DEV-210](./DEV-210.md): route discussion to the right channel +- [DEV-220](./DEV-220.md): reference issues and PRs as list items + +### 3. PR requirements + +What a pull request must satisfy before it merges. + +- [DEV-310](./DEV-310.md): sign every commit +- [DEV-320](./DEV-320.md): scope a PR to a few hours +- [DEV-330](./DEV-330.md): keep docs in sync +- [DEV-335](./DEV-335.md): keep each fact in one canonical place +- [DEV-337](./DEV-337.md): index every docs tree from a README +- [DEV-338](./DEV-338.md): give the repository a complete root README +- [DEV-340](./DEV-340.md): name a PR for what users gain +- [DEV-350](./DEV-350.md): mark a design PR docs(ui) with a Design section +- [DEV-360](./DEV-360.md): open work as a draft PR linked to its Problem +- [DEV-365](./DEV-365.md): mark a PR ready only when it is complete +- [DEV-370](./DEV-370.md): report time across all stages +- [DEV-380](./DEV-380.md): enforce markdown lint on push with a pinned rumdl + hook +- [DEV-390](./DEV-390.md): update user-facing docs in the same PR + +### 4. Review + +How to review, and the quality bar work is held to. + +- [DEV-410](./DEV-410.md): reject with Request Changes for objective problems +- [DEV-415](./DEV-415.md): re-request review explicitly after addressing changes +- [DEV-420](./DEV-420.md): scout open PRs when idle +- [DEV-430](./DEV-430.md): deliver bug-free work; review is a safety check ## Rule file format diff --git a/package-lock.json b/package-lock.json index 3a64f47..07b704f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,157 @@ { "name": "developers", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, - "packages": {} + "packages": { + "": { + "name": "developers", + "version": "0.0.1", + "hasInstallScript": true, + "devDependencies": { + "rumdl": "0.2.27" + } + }, + "node_modules/@rumdl/cli-darwin-arm64": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@rumdl/cli-darwin-arm64/-/cli-darwin-arm64-0.2.27.tgz", + "integrity": "sha512-orXZKZTL2nEVCOzvEguQlmc64VELnJhxajQhoaU3GzxjJfcYQxrhByyP9nRlBY/xUVji+r5DUBSjGU8neX/pjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@rumdl/cli-darwin-x64": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@rumdl/cli-darwin-x64/-/cli-darwin-x64-0.2.27.tgz", + "integrity": "sha512-8E3QwzD9LjA008NRIo+3ciiUmqN+fckrXNMfhw1p5n97dB67+xZs2VXYul40sKFT6NUhUJ4IC8aF22fLgnD05w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@rumdl/cli-linux-arm64": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@rumdl/cli-linux-arm64/-/cli-linux-arm64-0.2.27.tgz", + "integrity": "sha512-GOiNbRyliUFyHkbFte/zEQM8i7cgglDTjFthKf51UutGJmndNBH6z8CCrPEwVg8hQKc3Z+vqPI1A6WTnsmfViQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@rumdl/cli-linux-arm64-musl": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@rumdl/cli-linux-arm64-musl/-/cli-linux-arm64-musl-0.2.27.tgz", + "integrity": "sha512-jdjTqzYgpyEMuQ4WVjDqEodpBMXRN9apNkkF8QmfdwIOVzqKHbFWgunBOGq7B5aVKrIJ6gT6OhoJ4ma5gAlLoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@rumdl/cli-linux-x64": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@rumdl/cli-linux-x64/-/cli-linux-x64-0.2.27.tgz", + "integrity": "sha512-90ZMOv5KoB05I9D0lXAgKrB7top2Vhuu5aHvyj8cP8CZQHE86sw6Wj+1VMpnaLRit+S3cZXfhv8wXJUEe67upw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@rumdl/cli-linux-x64-musl": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@rumdl/cli-linux-x64-musl/-/cli-linux-x64-musl-0.2.27.tgz", + "integrity": "sha512-Nr1w3mY1CslMCNN/F7uR7Fyal5rc0D7YLb/8Bil/fk3gPAG9qK23cb2F6lfgjAZP+hH0rA9Wyp9LiQHRVGc/ug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@rumdl/cli-win32-x64": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@rumdl/cli-win32-x64/-/cli-win32-x64-0.2.27.tgz", + "integrity": "sha512-rUgXKqukPyhLLR7/MClKZ4EWHo/4MGX/9zD2tGew/sKFUtaA7xnd/VOeVKgMA7+lJevuGs9g++5xRw1WI2oqDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/rumdl": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/rumdl/-/rumdl-0.2.27.tgz", + "integrity": "sha512-Y4av1JJloDhpd4F+FC7NgNN6g5FA/uMMKFtn6QmF0yFuk+IlB21A/OLXVE6RgRqxo3dZSV3yWl/wgVPbuEQcog==", + "dev": true, + "license": "MIT", + "bin": { + "rumdl": "bin/rumdl" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@rumdl/cli-darwin-arm64": "0.2.27", + "@rumdl/cli-darwin-x64": "0.2.27", + "@rumdl/cli-linux-arm64": "0.2.27", + "@rumdl/cli-linux-arm64-musl": "0.2.27", + "@rumdl/cli-linux-x64": "0.2.27", + "@rumdl/cli-linux-x64-musl": "0.2.27", + "@rumdl/cli-win32-x64": "0.2.27" + } + } + } } diff --git a/package.json b/package.json new file mode 100644 index 0000000..f867e0b --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "developers", + "version": "0.0.1", + "private": true, + "scripts": { + "postinstall": "git config core.hooksPath .githooks", + "check:rules": "node scripts/check-rules.mjs" + }, + "devDependencies": { + "rumdl": "0.2.27" + } +} diff --git a/scripts/check-rules.mjs b/scripts/check-rules.mjs new file mode 100644 index 0000000..772b412 --- /dev/null +++ b/scripts/check-rules.mjs @@ -0,0 +1,188 @@ +#!/usr/bin/env node +// Audits the developer rules system (docs/rules/DEV-*.md) against the authoring +// rules those files themselves define, plus the docs-tree reachability that +// DEV-337 requires. Mechanical checks only; wording quality is still a human +// review. Exits non-zero (listing every failure) if any rule breaks structure, +// frontmatter, linking, or index invariants, or a doc is orphaned, so the +// pre-push hook can block the push. Run directly with `npm run check:rules`. + +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { basename, dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const DOCS_DIR = join(REPO_ROOT, "docs"); +const RULES_DIR = join(DOCS_DIR, "rules"); +const PROBLEM_MAX = 250; // DEV-050 + +const errors = []; +const fail = (file, msg) => errors.push(`${file}: ${msg}`); + +const files = readdirSync(RULES_DIR) + .filter((f) => /^DEV-\d+\.md$/.test(f)) + .sort(); + +const ids = new Map(); // id -> filename +const deps = new Map(); // id -> [ids] + +for (const file of files) { + // Report every rule failure against its repo-relative path, so the message + // names the offending rule and doubles as a clickable link to it. + const ref = `docs/rules/${file}`; + const text = readFileSync(join(RULES_DIR, file), "utf8"); + const fmMatch = text.match(/^---\n([\s\S]*?)\n---/); + if (!fmMatch) { + fail(ref, "missing frontmatter block"); + continue; + } + const fm = fmMatch[1]; + + const idMatch = fm.match(/^id:\s*(DEV-\d+)\s*$/m); + if (!idMatch) { + fail(ref, "frontmatter has no id"); + continue; + } + const id = idMatch[1]; + const base = file.replace(/\.md$/, ""); + if (id !== base) fail(ref, `id ${id} does not match filename`); + ids.set(id, ref); + + const depMatch = fm.match(/^depends_on:\s*\[(.*?)\]\s*$/m); + deps.set( + id, + depMatch + ? depMatch[1] + .split(",") + .map((s) => s.trim().replace(/^["']|["']$/g, "")) + .filter(Boolean) + : [], + ); + + // DEV-020: body opens Problem, Solution, then nested Acceptance Criteria. + if (!/^## Problem$/m.test(text)) fail(ref, "missing `## Problem`"); + if (!/^## Solution$/m.test(text)) fail(ref, "missing `## Solution`"); + if (!/^### Acceptance Criteria$/m.test(text)) + fail(ref, "missing `### Acceptance Criteria` (must nest under `###`)"); + + // DEV-050: Problem paragraph length. + const problem = text.match(/## Problem\s*\n+([\s\S]*?)\n\n/); + if (problem) { + const len = problem[1].replace(/\s+/g, " ").trim().length; + if (len > PROBLEM_MAX) fail(ref, `Problem is ${len} chars (max ${PROBLEM_MAX})`); + } + + // Writing convention: no em/en dashes. + if (/[—–]/.test(text)) fail(ref, "contains an em/en dash"); +} + +// DEV-040: depends_on entries resolve. +for (const [id, list] of deps) { + for (const dep of list) { + if (!ids.has(dep)) fail(ids.get(id), `depends_on ${dep} does not resolve`); + } +} + +// DEV-040 sanity: no dependency cycles. +const inCycle = (node, seen) => + seen.has(node) || (deps.get(node) || []).some((d) => inCycle(d, new Set([...seen, node]))); +for (const id of deps.keys()) { + if (inCycle(id, new Set())) fail(ids.get(id), `is part of a dependency cycle`); +} + +// DEV-040: every prose rule link resolves. +for (const file of files) { + const text = readFileSync(join(RULES_DIR, file), "utf8"); + for (const m of text.matchAll(/\]\(\.\/(DEV-\d+)\.md\)/g)) { + if (!ids.has(m[1])) fail(`docs/rules/${file}`, `links ${m[1]} which does not exist`); + } +} + +// Index: README lists every rule, and links no rule that does not exist. +const readme = readFileSync(join(RULES_DIR, "README.md"), "utf8"); +for (const id of ids.keys()) { + if (!readme.includes(`[${id}]`)) fail("docs/rules/README.md", `does not list ${id}`); +} +for (const m of new Set([...readme.matchAll(/\[(DEV-\d+)\]/g)].map((x) => x[1]))) { + if (!ids.has(m)) fail("docs/rules/README.md", `links ${m} which does not exist`); +} + +// DEV-337: docs/README.md indexes the tree, and every doc is reachable from the +// root README through the index SPINE only. From a README, a spine link is +// either a sibling .md in the same directory or an immediate subdirectory's +// README.md. Deeper links are cross-references, not index structure: they are +// ignored here, so a grandchild can never be reached by a shortcut from an +// ancestor, only through its own directory's README. This enforces that each +// doc is indexed by its nearest README and parents link child indexes, not +// files. +const rel = (p) => p.slice(REPO_ROOT.length + 1); + +const walkMarkdown = (dir) => { + const out = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, entry.name); + if (entry.isDirectory()) out.push(...walkMarkdown(p)); + else if (entry.name.endsWith(".md")) out.push(p); + } + return out; +}; + +if (!existsSync(join(DOCS_DIR, "README.md"))) { + fail("docs/README.md", "missing docs index"); +} + +const rootReadme = join(REPO_ROOT, "README.md"); +const reachable = new Set(); +const queue = [rootReadme]; +while (queue.length) { + const file = queue.shift(); + if (reachable.has(file)) continue; + reachable.add(file); + if (!existsSync(file)) continue; + const dir = dirname(file); + for (const m of readFileSync(file, "utf8").matchAll(/\]\(([^)\s]+)\)/g)) { + const target = m[1].split("#")[0]; + if (!target.endsWith(".md") || /^[a-z]+:/i.test(target)) continue; + const abs = resolve(dir, target); + const sameDir = dirname(abs) === dir; + const childIndex = basename(abs) === "README.md" && dirname(dirname(abs)) === dir; + if ((sameDir || childIndex) && !reachable.has(abs)) queue.push(abs); + } +} + +for (const md of walkMarkdown(DOCS_DIR)) { + if (!reachable.has(md)) + fail(rel(md), "is not indexed by its directory's README (unreachable through the index spine)"); +} + +// DEV-338: the root README opens with a title + description and documents +// Local, Stage/Preview, and Production. Its link to the docs index is DEV-337's +// concern (already enforced by the spine reachability check above), not +// re-checked here. +if (!existsSync(rootReadme)) { + fail("README.md", "repository has no root README"); +} else { + const readme = readFileSync(rootReadme, "utf8"); + const h1 = readme.match(/^#\s+\S.*$/m); + if (!h1) fail("README.md", "has no H1 title"); + else { + const afterH1 = readme.slice(readme.indexOf(h1[0]) + h1[0].length); + const description = afterH1.split(/^##\s/m)[0].replace(/^\s*(#.*)?$/gm, "").trim(); + if (!description) fail("README.md", "has no description before the first section"); + } + if (!/^##\s+(setup|installation|getting started)\b/im.test(readme)) + fail("README.md", "has no Setup / Installation section"); + for (const [label, re] of [ + ["Local", /^###\s+.*\blocal\b/im], + ["Stage/Preview", /^###\s+.*\b(stage|preview)\b/im], + ["Production", /^###\s+.*\bproduction\b/im], + ]) { + if (!re.test(readme)) fail("README.md", `Setup is missing a ${label} subsection`); + } +} + +if (errors.length) { + console.error(`Rules audit failed (${errors.length} issue${errors.length > 1 ? "s" : ""}):`); + for (const e of errors) console.error(` - ${e}`); + process.exit(1); +} +console.log(`Rules audit passed: ${files.length} rules, no issues.`);