From b48cb7f49f76fd5055ee8d8b0e75fdc7af96b99a Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 02:27:38 +0200 Subject: [PATCH 01/14] docs: website docs structure assessment + restructuring proposal Co-Authored-By: Claude Fable 5 --- apps/website/docs-structure-assessment.md | 255 ++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 apps/website/docs-structure-assessment.md diff --git a/apps/website/docs-structure-assessment.md b/apps/website/docs-structure-assessment.md new file mode 100644 index 00000000..d18fcabd --- /dev/null +++ b/apps/website/docs-structure-assessment.md @@ -0,0 +1,255 @@ +# Docs Website — Information Architecture Assessment + +**Scope:** `apps/website/content/docs/**` (69 `.mdx` pages, 12 sections + root), navigation (`meta.json` files), cross-links, entry points, and drift risk vs. the knowledge base. +**Framework:** Fumadocs (`fumadocs-core`/`fumadocs-mdx`/`fumadocs-ui`) on Next.js — nav is folder-based, ordered by per-folder `meta.json`; a folder without an `index.mdx` is sidebar-only (its direct URL 404s). +**Status:** Proposal only. Recommendation: **Option A** (targeted consolidation, see below). + +--- + +## 1. Current-state inventory + +```text +content/docs/ +├── index.mdx 1 page "Welcome" — thin (15 lines), no outbound links +├── getting-started/ 5 pages What is pair?, Quickstart, + solo/team/org variants +├── developer-journey/ 5 pages The 4-level process lifecycle (induction → execution) +├── concepts/ 7 pages SDLC, KB, skills, adoption files, canonical states, +│ agent integration, llms.txt — NO index.mdx +├── customization/ 5 pages Adopt → customize (team) → publish (org) → templates +├── integrations/ 6 pages One page per AI agent (Claude Code, Codex, Cursor, …) +├── pm-tools/ 4 pages One page per PM backend (filesystem, GH Projects, Linear) +├── guides/ 7 pages Grab-bag: cli-workflows, install-from-url, customize-kb, +│ adopter-checklist, packaging, troubleshooting, +│ update-link — NO index.mdx +├── migrations/ 2 pages Version-jump prompts (v0.4→v0.5) +├── tutorials/ 7 pages End-to-end walkthroughs (first-project, team-setup, …) +├── reference/ 10 pages cli/ (commands, examples), specs/ (2), catalogs (2), +│ kb-structure, configuration, quality-model, +│ skill-management — NO index.mdx (nor cli/, specs/) +├── support/ 3 pages index, general-faq ("FAQ"), faq ("Installation FAQ") +└── contributing/ 7 pages Dev setup, architecture, writing skills/guidelines/ + migration pages, release process +``` + +Total: **69 pages**. Root `meta.json` orders 13 sidebar entries in two groups (learn: getting-started → pm-tools; use/reference: guides → contributing) separated by `---`. + +Entry points: the marketing landing page links to `/docs` (root Welcome), `/docs/customization/team`, `/docs/customization/organization` (`app/(landing)/`). `llms.txt` / `llms-full.txt` are generated from the source loader (`lib/get-llm-text.ts`) so they follow the tree automatically. + +Staleness gate (`scripts/docs-staleness-check.js`) covers exactly: skill count + skill rows in `reference/skills-catalog.mdx`, command anchors in `reference/cli/commands.mdx`, and `pair-cli ` references in `tutorials/*.mdx`. Nothing else. + +### Audience-journey read + +- **First-run user:** `getting-started/` is solid (overview + quickstart + 3 variants), but the actual `/docs` entry (Welcome) is a dead end — the good hub is one level down at `getting-started/index.mdx`. +- **Adopting team:** the journey is split across four sections with no principled boundary — `customization/` (adopt/team/org/templates), `guides/` (customize-kb, install-from-url, packaging, adopter-checklist), `pm-tools/`, `integrations/`. Half the KB-lifecycle tasks live in `customization/`, the other half in `guides/`, with pairwise duplication (see F4, F7, F8). +- **Contributor/maintainer:** `contributing/` + `reference/specs/` + `migrations/` serve this well; the weak spot is `reference/` mixing catalogs, config reference, and long-form explanation (`skill-management.mdx`). + +--- + +## 2. Findings + +| # | Severity | Lens | Finding | +|---|----------|------|---------| +| F1 | Major | Findability | **`/docs` root is a dead end.** `content/docs/index.mdx` is 15 lines with zero links; the landing page's docs CTA points here. Every journey starts with a sidebar hunt. The real overview already exists at `getting-started/index.mdx` ("What is pair?"). | +| F2 | Major | Findability | **8 broken internal links to index-less section URLs.** `concepts/`, `guides/`, `reference/` have no `index.mdx`, so their URLs 404 under Fumadocs, yet pages link to them: `/docs/concepts` ← `tutorials/index.mdx`, `tutorials/first-project.mdx`, `tutorials/existing-project.mdx`; `/docs/guides` ← `tutorials/index.mdx`, `tutorials/first-project.mdx`, `tutorials/team-setup.mdx`, `tutorials/enterprise-adoption.mdx`; `/docs/reference` ← `tutorials/index.mdx`. | +| F3 | Major | Consistency / Audience fit | **`guides/` is a grab-bag.** Seven unrelated pages spanning three audiences: adopter recipes (`customize-kb`, `adopter-checklist`), KB-publisher ops (`packaging`, `install-from-url`, `update-link`), CLI usage (`cli-workflows`), and support content (`troubleshooting`). No index page, no shared theme, no principled rule for what lands here vs. `customization/` or `reference/`. | +| F4 | Major | Redundancy | **Two overlapping troubleshooting/FAQ pages.** `support/faq.mdx` ("Installation FAQ", 361 lines) and `guides/troubleshooting.mdx` (236 lines) both cover permission errors, Node version issues, PATH/command-not-found — with different fixes recommended (e.g. npm-permissions vs. nvm). They cross-link each other. Guaranteed drift. | +| F5 | Major | Redundancy / Staleness | **Hardcoded skill counts have already drifted, and the gate doesn't see them.** Actual count: 35. Docs say **30** (`getting-started/index.mdx:18`), **32** (`developer-journey/index.mdx:43,120`, `developer-journey/execution.mdx:123`, `reference/kb-structure.mdx:124`), **34** (`reference/guidelines-catalog.mdx:160`). `scripts/docs-staleness-check.js` only checks `reference/skills-catalog.mdx`. | +| F6 | Major | Findability / Consistency | **Duplicate sidebar titles and undifferentiated parallel tracks.** "Team Setup" appears twice (`getting-started/quickstart-team.mdx` and `tutorials/team-setup.mdx`). The quickstart triad (solo/team/org) and the tutorial triad (first-project/team-setup/enterprise-adoption) are intentionally parallel (quick vs. deep) but their titles don't signal which is which. | +| F7 | Major | Redundancy | **KB packaging documented twice.** `guides/packaging.mdx` (125 lines) and the "Package for Distribution" / "Verify Your Package" sections of `customization/organization.mdx` (221 lines) describe the same `pair-cli package` procedure and layout modes. | +| F8 | Minor | Redundancy | **`guides/customize-kb.mdx`** (63-line recipe) restates the core of `customization/team.mdx` (identify → override → verify) and links to it. Fine as a recipe, wrong as a sibling section. | +| F9 | Minor | Consistency | **Inverted FAQ file naming.** `support/faq.mdx` is titled "Installation FAQ"; `support/general-faq.mdx` is titled "FAQ". Filename and content are swapped relative to reader expectation. | +| F10 | Minor | Consistency | **Section label vs. page title mismatch.** Sidebar says "Developer Journey"; its index page is titled "Process Lifecycle". Pick one. | +| F11 | Minor | Consistency | **Inconsistent section index coverage.** 8 sections have an index page; `concepts/`, `guides/`, `reference/` (and `reference/cli/`, `reference/specs/`) don't. This is the root cause of F2. | +| F12 | Minor | Findability | **`concepts/llms-txt.mdx` has zero inbound cross-links** — reachable only via sidebar. | +| F13 | Minor | Consistency / Growth | **CLI documentation is split across sections.** `guides/cli-workflows.mdx` (305 lines) and `guides/update-link.mdx` (338 lines, single-command deep dive) are CLI docs stranded outside `reference/cli/`; `reference/skill-management.mdx` (272 lines) is half explanation, half reference. | +| F14 | Minor | Growth | **New content has no principled home.** Integrations, PM tools, migrations scale cleanly (one page per item). But any new task-oriented page defaults to `guides/`, reinforcing the grab-bag. 13 top-level nav entries is already at the upper bound of scannable. | + +What is genuinely **fine** and should not be touched: `integrations/`, `pm-tools/`, `migrations/` (clean one-page-per-item patterns), `contributing/` (coherent, complete), `tutorials/` (well differentiated internally), `developer-journey/` content, and the two-group sidebar split. + +--- + +## 3. Proposed target structure (Option A — recommended) + +**Principle:** keep the 11 sections that work; dissolve `guides/` into the sections its pages actually belong to; make every section URL resolvable; fix the two duplication hotspots by merging. + +```text +content/docs/ +├── index.mdx REWRITE: real hub — cards to the 4 journeys + top tasks +├── getting-started/ +│ ├── index.mdx keep ("What is pair?") +│ ├── quickstart.mdx keep +│ ├── quickstart-solo.mdx keep — retitle "Quickstart: Solo" +│ ├── quickstart-team.mdx keep — retitle "Quickstart: Team" (fixes F6) +│ ├── quickstart-org.mdx keep — retitle "Quickstart: Organization" +│ └── checklist.mdx ← guides/adopter-checklist (post-setup verification) +├── tutorials/ unchanged (7 pages) +├── concepts/ +│ ├── index.mdx NEW: one-paragraph map of the 7 concepts (fixes F2/F11) +│ └── … 7 existing pages unchanged +├── developer-journey/ unchanged — align title: "Process Lifecycle" everywhere (F10) +├── customization/ becomes the single home of the KB lifecycle +│ ├── index.mdx keep (adopt → customize → publish narrative) +│ ├── adopt.mdx keep +│ ├── install-from-url.mdx ← guides/install-from-url (advanced install sources) +│ ├── team.mdx keep — absorbs guides/customize-kb as "Quick recipe" section +│ ├── organization.mdx keep — absorbs guides/packaging (dedupe, keep layout-modes +│ │ detail as a subsection) (fixes F7/F8) +│ └── templates.mdx keep +├── integrations/ unchanged (6 pages) +├── pm-tools/ unchanged (4 pages) +├── ─── (separator) +├── reference/ +│ ├── index.mdx NEW: what's in reference, links to catalogs/specs (F2/F11) +│ ├── cli/ +│ │ ├── commands.mdx keep (staleness-gate anchor — do not move) +│ │ ├── examples.mdx keep +│ │ ├── workflows.mdx ← guides/cli-workflows (fixes F13) +│ │ └── update-link.mdx ← guides/update-link +│ ├── specs/ unchanged (2 pages) +│ └── … 6 existing pages unchanged +├── migrations/ unchanged (2 pages) +├── support/ +│ ├── index.mdx keep (update quick links) +│ ├── general-faq.mdx keep URL — remains the general "FAQ" +│ └── troubleshooting.mdx NEW: merge of guides/troubleshooting + support/faq +│ (single install/setup problem-solving page) (fixes F4/F9) +└── contributing/ unchanged (7 pages) +``` + +`guides/` is **deleted** as a section. Top-level nav: 13 → 12 entries. Proposed root `meta.json` order: `index, getting-started, tutorials, concepts, developer-journey, customization, integrations, pm-tools, ---, reference, migrations, support, contributing` (tutorials promoted next to getting-started — they are the learn path, not an appendix). + +### Page-by-page mapping (all 69 pages) + +| Current path (`content/docs/…`) | Disposition | +|---|---| +| `index.mdx` | keep — **rewrite** as hub with links (F1) | +| `getting-started/index.mdx` | keep | +| `getting-started/quickstart.mdx` | keep | +| `getting-started/quickstart-solo.mdx` | keep — retitle "Quickstart: Solo" | +| `getting-started/quickstart-team.mdx` | keep — retitle "Quickstart: Team" | +| `getting-started/quickstart-org.mdx` | keep — retitle "Quickstart: Organization" | +| `developer-journey/index.mdx` | keep — unify title with sidebar label | +| `developer-journey/induction.mdx` | keep | +| `developer-journey/strategic-planning.mdx` | keep | +| `developer-journey/iteration.mdx` | keep | +| `developer-journey/execution.mdx` | keep — fix skill count | +| `concepts/ai-assisted-sdlc.mdx` | keep | +| `concepts/knowledge-base.mdx` | keep | +| `concepts/skills.mdx` | keep | +| `concepts/adoption-files.mdx` | keep | +| `concepts/canonical-states.mdx` | keep | +| `concepts/agent-integration.mdx` | keep | +| `concepts/llms-txt.mdx` | keep — add inbound links (from concepts index + kb-structure) | +| *(new)* `concepts/index.mdx` | **new page** | +| `customization/index.mdx` | keep | +| `customization/adopt.mdx` | keep | +| `customization/team.mdx` | keep — absorbs customize-kb recipe | +| `customization/organization.mdx` | keep — absorbs packaging content | +| `customization/templates.mdx` | keep | +| `guides/adopter-checklist.mdx` | **move** → `getting-started/checklist.mdx` | +| `guides/cli-workflows.mdx` | **move** → `reference/cli/workflows.mdx` | +| `guides/customize-kb.mdx` | **merge-into** `customization/team.mdx` | +| `guides/install-from-url.mdx` | **move** → `customization/install-from-url.mdx` | +| `guides/packaging.mdx` | **merge-into** `customization/organization.mdx` | +| `guides/troubleshooting.mdx` | **merge-into** `support/troubleshooting.mdx` (new) | +| `guides/update-link.mdx` | **move** → `reference/cli/update-link.mdx` | +| `integrations/index.mdx` | keep | +| `integrations/claude-code.mdx` | keep | +| `integrations/codex.mdx` | keep | +| `integrations/cursor.mdx` | keep | +| `integrations/github-copilot.mdx` | keep | +| `integrations/windsurf.mdx` | keep | +| `pm-tools/index.mdx` | keep | +| `pm-tools/filesystem.mdx` | keep | +| `pm-tools/github-projects.mdx` | keep | +| `pm-tools/linear.mdx` | keep | +| `migrations/index.mdx` | keep | +| `migrations/v0.4-to-v0.5.mdx` | keep | +| `tutorials/index.mdx` | keep — fix `/docs/guides`, `/docs/concepts`, `/docs/reference` links | +| `tutorials/first-project.mdx` | keep — fix section links | +| `tutorials/existing-project.mdx` | keep — fix section links | +| `tutorials/team-setup.mdx` | keep — fix `/docs/guides` link | +| `tutorials/enterprise-adoption.mdx` | keep — fix `/docs/guides` link | +| `tutorials/managing-ai-artifacts.mdx` | keep | +| `tutorials/release-testing.mdx` | keep | +| `reference/cli/commands.mdx` | keep (gate anchor — path frozen) | +| `reference/cli/examples.mdx` | keep | +| `reference/specs/cli-contracts.mdx` | keep | +| `reference/specs/kb-source-resolution.mdx` | keep | +| `reference/configuration.mdx` | keep | +| `reference/guidelines-catalog.mdx` | keep — fix skill count | +| `reference/kb-structure.mdx` | keep — fix skill count | +| `reference/quality-model.mdx` | keep | +| `reference/skill-management.mdx` | keep (splitting explanation vs. reference: deferred, low value) | +| `reference/skills-catalog.mdx` | keep (gate anchor — path frozen) | +| *(new)* `reference/index.mdx` | **new page** | +| `support/index.mdx` | keep — update quick links | +| `support/general-faq.mdx` | keep — remains "FAQ" (URL kept; rename to `faq` deferred, F9 accepted as cosmetic) | +| `support/faq.mdx` | **merge-into** `support/troubleshooting.mdx` (new) | +| `contributing/index.mdx` | keep | +| `contributing/development-setup.mdx` | keep | +| `contributing/architecture.mdx` | keep | +| `contributing/writing-skills.mdx` | keep | +| `contributing/writing-guidelines.mdx` | keep | +| `contributing/writing-migration-pages.mdx` | keep | +| `contributing/release-process.mdx` | keep | + +Nothing is deleted outright: 7 moves/merges, 3 new pages (root rewrite counts as rewrite), 60 keeps. Every current URL that changes gets a redirect (below). + +--- + +## 4. Alternatives considered + +### Option B — hygiene only (lighter touch) + +Fix F1, F2, F5, F6, F10, F11, F12 without moving any page: rewrite the root hub, add `concepts/index.mdx`, `guides/index.mdx`, `reference/index.mdx`, fix counts + extend the gate, retitle the quickstart variants. Merge only the FAQ/troubleshooting pair (F4) since that is active drift. + +- **Pro:** no redirects except one, ~1 day total, zero URL churn. +- **Con:** `guides/` grab-bag and the packaging/customize-kb duplication (F3, F7, F8, F13) remain; a `guides/index.mdx` would legitimize the grab-bag and new pages will keep accreting there. + +### Option C — full Diátaxis re-org (heavier) + +Top-level = Tutorials / How-to / Explanation / Reference; every section re-parented. + +- **Pro:** textbook IA. +- **Con:** every URL changes (69-entry redirect map), landing page + e2e tests + external links + `llms.txt` consumers all churn; the current sections *already* map ~1:1 onto Diátaxis (tutorials=tutorials, concepts=explanation, reference=reference) — the only real offender is `guides/`, which Option A fixes surgically. Cost far exceeds benefit. + +**Recommendation: Option A.** It removes every Major finding, changes only 7 URLs, and leaves the healthy 70% of the tree untouched. If you want to defer, Option B batch 1 below is a strict subset of Option A — no wasted work. + +--- + +## 5. Migration notes + +**Redirects** — add a `redirects()` block to `apps/website/next.config.mjs` (none exists today), permanent: + +| From | To | +|---|---| +| `/docs/guides/adopter-checklist` | `/docs/getting-started/checklist` | +| `/docs/guides/cli-workflows` | `/docs/reference/cli/workflows` | +| `/docs/guides/customize-kb` | `/docs/customization/team` | +| `/docs/guides/install-from-url` | `/docs/customization/install-from-url` | +| `/docs/guides/packaging` | `/docs/customization/organization` | +| `/docs/guides/troubleshooting` | `/docs/support/troubleshooting` | +| `/docs/guides/update-link` | `/docs/reference/cli/update-link` | +| `/docs/support/faq` | `/docs/support/troubleshooting` | +| `/docs/guides` | `/docs` (section removed) | + +**Cross-link updates** — inbound links into moved pages (from the link audit): `guides/install-from-url` ← 4 links (`tutorials/managing-ai-artifacts` ×2, `customization/adopt`, `customization/organization`); `guides/customize-kb` ← 5; `guides/troubleshooting` ← 4; `guides/cli-workflows` ← 3; `guides/update-link` ← 2; `guides/adopter-checklist` ← 1; `guides/packaging` ← 0; `support/faq` ← 5. Plus the 8 broken section links (F2). A repo-wide grep for `/docs/guides` and `/docs/support/faq` catches all of it. + +**Docs-staleness gate** (`scripts/docs-staleness-check.js`): + +- Unaffected by Option A moves: its anchors (`reference/skills-catalog.mdx`, `reference/cli/commands.mdx`, `tutorials/`) keep their paths — this is why `commands.mdx` and `skills-catalog.mdx` are frozen in the mapping. +- **Extend it** (batch 1): run the `(\d+)\s+(?:pair\s+)?skills` count check across *all* of `content/docs/**/*.mdx`, not just the catalog — that turns F5 from recurring drift into a CI failure. Optionally add a dead-internal-link check (resolve `](/docs/...)` targets against the source loader), which would have caught F2. + +**Other touchpoints:** `llms.txt`/`llms-full.txt` regenerate from the source loader — no action. `e2e/docs.e2e.test.ts` hardcodes `getting-started/*` URLs (unchanged) but asserts sidebar titles "Solo Setup"/"Team Setup"/"Organization Setup" — update assertions when retitling (batch 1). Landing page links (`/docs`, `/docs/customization/team`, `/docs/customization/organization`) all survive unchanged. + +### Suggested batching (small PRs, each independently shippable) + +| Batch | Content | Effort | +|---|---|---| +| 1. Hygiene (no URL changes) | Rewrite root hub; add `concepts/index.mdx` + `reference/index.mdx`; fix 8 broken section links; fix 6 stale skill counts; extend staleness gate; retitle quickstart variants + align developer-journey title; update e2e title assertions; add llms-txt inbound links | ~0.5 day | +| 2. Support consolidation | Merge `support/faq.mdx` + `guides/troubleshooting.mdx` → `support/troubleshooting.mdx`; redirects; update 9 inbound links; update `support/index.mdx` | ~0.5 day | +| 3. Dissolve `guides/` | 4 moves + 2 content merges (`customize-kb`→team, `packaging`→organization); delete `guides/meta.json`; redirects; update ~15 inbound links; update root `meta.json` (drop guides, promote tutorials) | ~1 day | +| 4. (Optional, deferred) | Split `reference/skill-management.mdx` explanation/reference; rename `support/general-faq.mdx` → `faq.mdx` | ~0.5 day — recommend skipping unless it itches | + +Total for Option A: **~2 days** across 3 PRs. Batch 1 alone = Option B and is worth doing regardless of the go/no-go on batches 2–3. From 605bdfec82a28e4e32c979ca5e1a273dd2fc4ab2 Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 02:52:41 +0200 Subject: [PATCH 02/14] =?UTF-8?q?[#312]=20docs:=20batch=201=20hygiene=20?= =?UTF-8?q?=E2=80=94=20hub,=20section=20indexes,=20links,=20counts,=20gate?= =?UTF-8?q?=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /docs rewritten as real hub (4 journeys + top tasks); new concepts/index + reference/index (F1/F2/F11) - 6 stale skill counts fixed to 35 (verified vs dataset/.skills: 25 capability + 9 process + 1 meta) - docs-staleness-check.js: skill-count check now scans ALL content/docs/**/*.mdx; regex extended with 'composable' variant (getting-started phrasing) — subset counts ('9 process skills') excluded - gate also gains dead-internal-link check (resolves ](/docs/...) against content tree) — the optional check from the story, implemented since clean against the filesystem source - test-first proof: extended gate FAILed with 6 count mismatches + 8 dead section links, PASSes after fixes - 8 broken section links fixed (concepts/reference via new indexes; 4 /docs/guides links retargeted to stable pages ahead of T3 dissolve) - quickstart variants retitled 'Quickstart: Solo/Team/Organization' (F6); developer-journey unified on 'Process Lifecycle' (meta.json + 4 prose labels) (F10) - llms-txt orphan: inbound links from concepts/index + reference/kb-structure (F12) - e2e title/sidebar assertions updated (docs + landing); CP5 QA doc: +2 index URLs, label sync Refs: #312 T1 Co-Authored-By: Claude Fable 5 --- apps/website/content/docs/concepts/index.mdx | 8 +++ .../docs/developer-journey/execution.mdx | 2 +- .../content/docs/developer-journey/index.mdx | 4 +- .../content/docs/developer-journey/meta.json | 2 +- .../content/docs/getting-started/index.mdx | 8 +-- .../docs/getting-started/quickstart-org.mdx | 2 +- .../docs/getting-started/quickstart-solo.mdx | 2 +- .../docs/getting-started/quickstart-team.mdx | 2 +- .../docs/getting-started/quickstart.mdx | 6 +- apps/website/content/docs/index.mdx | 23 +++++-- apps/website/content/docs/pm-tools/index.mdx | 2 +- .../docs/reference/guidelines-catalog.mdx | 6 +- apps/website/content/docs/reference/index.mdx | 28 +++++++++ .../content/docs/reference/kb-structure.mdx | 3 +- .../content/docs/support/general-faq.mdx | 2 +- .../docs/tutorials/enterprise-adoption.mdx | 2 +- .../content/docs/tutorials/first-project.mdx | 2 +- apps/website/content/docs/tutorials/index.mdx | 2 +- .../content/docs/tutorials/team-setup.mdx | 2 +- apps/website/e2e/docs.e2e.test.ts | 16 ++--- apps/website/e2e/landing.e2e.test.ts | 10 +-- .../CP5-website-docs-completeness.md | 12 ++-- scripts/docs-staleness-check.js | 61 ++++++++++++++++--- 23 files changed, 154 insertions(+), 53 deletions(-) create mode 100644 apps/website/content/docs/concepts/index.mdx create mode 100644 apps/website/content/docs/reference/index.mdx diff --git a/apps/website/content/docs/concepts/index.mdx b/apps/website/content/docs/concepts/index.mdx new file mode 100644 index 00000000..0e792f23 --- /dev/null +++ b/apps/website/content/docs/concepts/index.mdx @@ -0,0 +1,8 @@ +--- +title: Concepts +description: How pair works under the hood — the Knowledge Base, skills, adoption files, and the ideas that connect them. +--- + +These pages explain **how pair works** — read them when you want understanding, not step-by-step instructions. + +Start with the [AI-Assisted SDLC](/docs/concepts/ai-assisted-sdlc): the problem pair solves and how structured context changes AI collaboration. The [Knowledge Base](/docs/concepts/knowledge-base) is the core artifact — the documents installed into `.pair/` that give your AI assistant persistent context. [Skills](/docs/concepts/skills) are the actions layered on top: structured instructions your assistant discovers and executes. [Adoption Files](/docs/concepts/adoption-files) record your project-specific decisions so they never drift. [Canonical States](/docs/concepts/canonical-states) is how skills reason about work-item state across any board vocabulary. [Agent Integration](/docs/concepts/agent-integration) explains the bridge pattern that connects any AI coding tool to the single source of truth. Finally, [llms.txt](/docs/concepts/llms-txt) covers the standard pair uses to make documentation and project context natively consumable by AI assistants. diff --git a/apps/website/content/docs/developer-journey/execution.mdx b/apps/website/content/docs/developer-journey/execution.mdx index 0f3349be..c4ff810b 100644 --- a/apps/website/content/docs/developer-journey/execution.mdx +++ b/apps/website/content/docs/developer-journey/execution.mdx @@ -120,6 +120,6 @@ This loop repeats for every story. `/pair-next` always knows what comes next. ## Reference -- [Skills Catalog](/docs/reference/skills-catalog) — all 32 skills with commands and composition +- [Skills Catalog](/docs/reference/skills-catalog) — all 35 skills with commands and composition - [Guidelines & How-To Catalog](/docs/reference/guidelines-catalog) — all 9 guideline categories and 9 how-to guides - [Adopt a Knowledge Base](/docs/customization/adopt) — set up pair if you haven't already diff --git a/apps/website/content/docs/developer-journey/index.mdx b/apps/website/content/docs/developer-journey/index.mdx index 3fb3683b..d1eed060 100644 --- a/apps/website/content/docs/developer-journey/index.mdx +++ b/apps/website/content/docs/developer-journey/index.mdx @@ -40,7 +40,7 @@ Level 4: Execution (per story) You can follow this flow in two ways: -1. **Process Skills (recommended)** — invoke a slash command and the AI handles the structured process for you. See the [Skills Catalog](/docs/reference/skills-catalog) for all 32 skills. +1. **Process Skills (recommended)** — invoke a slash command and the AI handles the structured process for you. See the [Skills Catalog](/docs/reference/skills-catalog) for all 35 skills. 2. **How-to Guides** — follow the manual step-by-step instructions from the Knowledge Base. See the [Guidelines & How-To Catalog](/docs/reference/guidelines-catalog) for all 11 guides. Skills compose capability skills internally, enforce adoption compliance, and update your PM tool automatically. The how-to guides give you the same process with full manual control. @@ -117,5 +117,5 @@ Feeling lost? Check the [FAQ](/docs/support/general-faq) — it answers common q ## Reference -- [Skills Catalog](/docs/reference/skills-catalog) — all 32 skills with commands and composition +- [Skills Catalog](/docs/reference/skills-catalog) — all 35 skills with commands and composition - [Guidelines & How-To Catalog](/docs/reference/guidelines-catalog) — all 9 guideline categories and 9 how-to guides diff --git a/apps/website/content/docs/developer-journey/meta.json b/apps/website/content/docs/developer-journey/meta.json index d8c9bcba..6e51a20e 100644 --- a/apps/website/content/docs/developer-journey/meta.json +++ b/apps/website/content/docs/developer-journey/meta.json @@ -1,4 +1,4 @@ { - "title": "Developer Journey", + "title": "Process Lifecycle", "pages": ["induction", "strategic-planning", "iteration", "execution"] } diff --git a/apps/website/content/docs/getting-started/index.mdx b/apps/website/content/docs/getting-started/index.mdx index de9a74b2..22f539d5 100644 --- a/apps/website/content/docs/getting-started/index.mdx +++ b/apps/website/content/docs/getting-started/index.mdx @@ -15,7 +15,7 @@ A structured set of documents — guidelines, how-tos, architecture decisions, t ## Skills -Structured instructions that AI assistants discover and execute, following the [Agent Skills](https://agentskills.io) open standard. Skills automate the full product lifecycle — from PRD creation to code review — with 30 composable skills covering process and capability concerns. +Structured instructions that AI assistants discover and execute, following the [Agent Skills](https://agentskills.io) open standard. Skills automate the full product lifecycle — from PRD creation to code review — with 35 composable skills covering process and capability concerns. [Learn more about Skills](/docs/concepts/skills) @@ -30,9 +30,9 @@ You don't write these files manually. Run `/pair-next` and the AI walks you thro ## Choose your path - **[Quickstart](/docs/getting-started/quickstart)** — Install pair-cli, set up the Knowledge Base, and start using it. -- **[Solo Setup](/docs/getting-started/quickstart-solo)** — Working alone? Minimal configuration, no team coordination. -- **[Team Setup](/docs/getting-started/quickstart-team)** — Shared standards and the agent bridge pattern for multi-developer teams. -- **[Organization Rollout](/docs/getting-started/quickstart-org)** — Custom KB packaging, distribution, and governance across multiple teams. +- **[Quickstart: Solo](/docs/getting-started/quickstart-solo)** — Working alone? Minimal configuration, no team coordination. +- **[Quickstart: Team](/docs/getting-started/quickstart-team)** — Shared standards and the agent bridge pattern for multi-developer teams. +- **[Quickstart: Organization](/docs/getting-started/quickstart-org)** — Custom KB packaging, distribution, and governance across multiple teams. ## Go deeper with tutorials diff --git a/apps/website/content/docs/getting-started/quickstart-org.mdx b/apps/website/content/docs/getting-started/quickstart-org.mdx index 6b0485ba..071b05b4 100644 --- a/apps/website/content/docs/getting-started/quickstart-org.mdx +++ b/apps/website/content/docs/getting-started/quickstart-org.mdx @@ -1,5 +1,5 @@ --- -title: Organization Setup +title: "Quickstart: Organization" description: Roll out pair across multiple teams with custom KB packaging, distribution, and organization-wide standards. --- diff --git a/apps/website/content/docs/getting-started/quickstart-solo.mdx b/apps/website/content/docs/getting-started/quickstart-solo.mdx index 9bb71c4e..95c584b3 100644 --- a/apps/website/content/docs/getting-started/quickstart-solo.mdx +++ b/apps/website/content/docs/getting-started/quickstart-solo.mdx @@ -1,5 +1,5 @@ --- -title: Solo Setup +title: "Quickstart: Solo" description: Configure pair for solo development — minimal setup, no team coordination needed. --- diff --git a/apps/website/content/docs/getting-started/quickstart-team.mdx b/apps/website/content/docs/getting-started/quickstart-team.mdx index 8b9a4d75..8bb23b03 100644 --- a/apps/website/content/docs/getting-started/quickstart-team.mdx +++ b/apps/website/content/docs/getting-started/quickstart-team.mdx @@ -1,5 +1,5 @@ --- -title: Team Setup +title: "Quickstart: Team" description: Configure pair for a development team with shared standards, adoption files, and multi-agent support. --- diff --git a/apps/website/content/docs/getting-started/quickstart.mdx b/apps/website/content/docs/getting-started/quickstart.mdx index f87d7f3e..62319d43 100644 --- a/apps/website/content/docs/getting-started/quickstart.mdx +++ b/apps/website/content/docs/getting-started/quickstart.mdx @@ -127,9 +127,9 @@ Each step has a **how-to guide** in `.pair/knowledge/how-to/` that the AI follow The steps above work for everyone. For environment-specific configuration: -- **[Solo Setup](/docs/getting-started/quickstart-solo)** — Working alone? Minimal setup, no team coordination needed. -- **[Team Setup](/docs/getting-started/quickstart-team)** — Shared standards, adoption files, and the agent bridge pattern for multi-developer teams. -- **[Organization Rollout](/docs/getting-started/quickstart-org)** — Custom KB packaging, distribution, and governance across multiple teams. +- **[Quickstart: Solo](/docs/getting-started/quickstart-solo)** — Working alone? Minimal setup, no team coordination needed. +- **[Quickstart: Team](/docs/getting-started/quickstart-team)** — Shared standards, adoption files, and the agent bridge pattern for multi-developer teams. +- **[Quickstart: Organization](/docs/getting-started/quickstart-org)** — Custom KB packaging, distribution, and governance across multiple teams. Want a guided, end-to-end walkthrough? Pick a [tutorial](/docs/tutorials) that matches your context — solo developer (~30 min), team lead (~45 min), or enterprise architect (~60 min). diff --git a/apps/website/content/docs/index.mdx b/apps/website/content/docs/index.mdx index 0820d7bf..96d3bd5c 100644 --- a/apps/website/content/docs/index.mdx +++ b/apps/website/content/docs/index.mdx @@ -1,6 +1,6 @@ --- title: Welcome -description: Getting started with pair +description: pair documentation — pick your journey, from first install to publishing your own Knowledge Base. ---
@@ -8,8 +8,23 @@ description: Getting started with pair

Welcome to pair

-**Code is the easy part.** pair helps development teams collaborate with AI assistants throughout the product lifecycle. +**Code is the easy part.** pair helps development teams collaborate with AI assistants throughout the product lifecycle — a Knowledge Base for persistent context, skills that automate the process, and adoption files that keep decisions from drifting. -## What is pair? +## Pick your journey -pair is a suite of resources and workflows designed to guide and enable collaboration between development teams and AI assistants. It provides tools for defining epics, stories, guidelines, and adoptions, integrating with major code assistants and project management tools. +- **[Getting Started](/docs/getting-started)** — What pair is, install pair-cli, and set up your first project in 5–10 minutes. +- **[Tutorials](/docs/tutorials)** — Guided end-to-end walkthroughs: first project, existing project, team setup, enterprise adoption (30–60 min). +- **[Customization](/docs/customization)** — Adapt pair to your context: adopt a Knowledge Base, customize it for your team, publish your own. +- **[Reference](/docs/reference)** — Look up details: CLI commands, skills and guidelines catalogs, KB structure, configuration. + +## Top tasks + +| I want to... | Go to | +| --- | --- | +| Install pair and run my first command | [Quickstart](/docs/getting-started/quickstart) | +| Understand how pair works | [Concepts](/docs/concepts) | +| Follow the development lifecycle | [Process Lifecycle](/docs/developer-journey) | +| Connect my AI coding assistant | [Integrations](/docs/integrations) | +| Connect my project management tool | [PM Tools](/docs/pm-tools) | +| Fix an install or setup problem | [Support](/docs/support) | +| Contribute to pair | [Contributing](/docs/contributing) | diff --git a/apps/website/content/docs/pm-tools/index.mdx b/apps/website/content/docs/pm-tools/index.mdx index 3c16c00d..b481b2af 100644 --- a/apps/website/content/docs/pm-tools/index.mdx +++ b/apps/website/content/docs/pm-tools/index.mdx @@ -59,5 +59,5 @@ All PM tool configuration lives in one place: `.pair/adoption/tech/way-of-workin ## Related - [AI Coding Tools](/docs/integrations) — set up your AI assistant -- [Developer Journey](/docs/developer-journey) — the full development lifecycle +- [Process Lifecycle](/docs/developer-journey) — the full development lifecycle - [Adoption Files concept](/docs/concepts/adoption-files) — how adoption files work diff --git a/apps/website/content/docs/reference/guidelines-catalog.mdx b/apps/website/content/docs/reference/guidelines-catalog.mdx index 871b6175..9986e9d0 100644 --- a/apps/website/content/docs/reference/guidelines-catalog.mdx +++ b/apps/website/content/docs/reference/guidelines-catalog.mdx @@ -9,7 +9,7 @@ The pair Knowledge Base ships with opinionated guidelines organized into 9 categ ## How-To Guides -9 sequential guides that walk you through the complete development process — from product requirements to code review. Each guide can be followed manually or invoked as a skill (recommended). See the [Developer Journey](/docs/developer-journey) for the full step-by-step walkthrough. +9 sequential guides that walk you through the complete development process — from product requirements to code review. Each guide can be followed manually or invoked as a skill (recommended). See the [Process Lifecycle](/docs/developer-journey) for the full step-by-step walkthrough. | # | Guide | Tags | Skill (preferred) | | --- | --- | --- | --- | @@ -157,7 +157,7 @@ Design systems, research, and content strategy. ## Related - [Quality Model](/docs/reference/quality-model) — the single source of default quality rules (classification, tiers, tags) -- [Skills Catalog](/docs/reference/skills-catalog) — all 34 pair skills with commands and composition -- [Developer Journey](/docs/developer-journey) — step-by-step walkthrough of the full process +- [Skills Catalog](/docs/reference/skills-catalog) — all 35 pair skills with commands and composition +- [Process Lifecycle](/docs/developer-journey) — step-by-step walkthrough of the full process - [KB Structure](/docs/reference/kb-structure) — directory layout and file purposes - [Customize for Your Team](/docs/customization/team) — override guidelines via adoption files diff --git a/apps/website/content/docs/reference/index.mdx b/apps/website/content/docs/reference/index.mdx new file mode 100644 index 00000000..1a04f4e2 --- /dev/null +++ b/apps/website/content/docs/reference/index.mdx @@ -0,0 +1,28 @@ +--- +title: Reference +description: Complete specifications — CLI commands, catalogs of skills and guidelines, KB structure, and configuration. +--- + +Look-up material: complete, factual, no narrative. Come here when you know what you need and want the details. + +## CLI + +- [CLI Commands](/docs/reference/cli/commands) — Full syntax and options for every pair-cli command +- [CLI Help Examples](/docs/reference/cli/examples) — Copy-paste ready examples + +## Catalogs + +- [Skills Catalog](/docs/reference/skills-catalog) — Every pair skill with its command and composition +- [Guidelines & How-To Catalog](/docs/reference/guidelines-catalog) — Guideline categories and how-to guides + +## Structure & configuration + +- [KB Structure](/docs/reference/kb-structure) — Directory layout and file purposes of `.pair/` +- [Configuration](/docs/reference/configuration) — `config.json` options +- [Quality Model](/docs/reference/quality-model) — Default quality rules, tiers, and tags +- [Skill Management](/docs/reference/skill-management) — How skills are resolved, transformed, and installed + +## Specs + +- [CLI Contracts](/docs/reference/specs/cli-contracts) — Machine-readable CLI behavior contracts +- [KB Source Resolution](/docs/reference/specs/kb-source-resolution) — How KB sources are resolved diff --git a/apps/website/content/docs/reference/kb-structure.mdx b/apps/website/content/docs/reference/kb-structure.mdx index 3608c2f7..00fbd9ff 100644 --- a/apps/website/content/docs/reference/kb-structure.mdx +++ b/apps/website/content/docs/reference/kb-structure.mdx @@ -121,6 +121,7 @@ Neither subdirectory is scaffolded by `pair install` — a skill creates them on - [Customize the Knowledge Base](/docs/guides/customize-kb) — How to override guidelines - [Configuration Reference](/docs/reference/configuration) — config.json options -- [Skills Catalog](/docs/reference/skills-catalog) — All 32 pair skills +- [Skills Catalog](/docs/reference/skills-catalog) — All 35 pair skills +- [llms.txt](/docs/concepts/llms-txt) — How pair makes docs and project context consumable by AI assistants - [knowledge-hub README](https://github.com/foomakers/pair/blob/main/packages/knowledge-hub/README.md) — KB package operations, scripts, and maintainer guide - [content-ops README](https://github.com/foomakers/pair/blob/main/packages/content-ops/README.md) — File operations API, copy behaviors, and transform configuration diff --git a/apps/website/content/docs/support/general-faq.mdx b/apps/website/content/docs/support/general-faq.mdx index fd7ac9c9..6513c9e6 100644 --- a/apps/website/content/docs/support/general-faq.mdx +++ b/apps/website/content/docs/support/general-faq.mdx @@ -57,7 +57,7 @@ No. Run `/pair-next` and the AI walks you through the full process — from PRD Use pair's default process. It's a structured 9-step lifecycle from product requirements to code review, and it works out of the box. Run `/pair-next` in your AI assistant — it reads your project state and tells you exactly what to do next, step by step. You don't need to plan anything in advance. -[Developer Journey](/docs/developer-journey) +[Process Lifecycle](/docs/developer-journey) ### I already have my own process — do I need to change it? diff --git a/apps/website/content/docs/tutorials/enterprise-adoption.mdx b/apps/website/content/docs/tutorials/enterprise-adoption.mdx index 9d1c2ab9..71cd5c01 100644 --- a/apps/website/content/docs/tutorials/enterprise-adoption.mdx +++ b/apps/website/content/docs/tutorials/enterprise-adoption.mdx @@ -299,4 +299,4 @@ Each team gets the same org foundation with their own customization layer. The A - **[Publish a Knowledge Base](/docs/customization/organization)** — Detailed reference for all packaging and distribution options - **[KB Structure Reference](/docs/reference/kb-structure)** — Full directory layout and file purposes - **[Contributing](/docs/contributing)** — Contribute improvements back to the pair project -- **[Guides](/docs/guides)** — Goal-oriented guides for specific tasks +- **[Support](/docs/support)** — FAQ and troubleshooting when something doesn't work diff --git a/apps/website/content/docs/tutorials/first-project.mdx b/apps/website/content/docs/tutorials/first-project.mdx index 7d215c30..c68aa812 100644 --- a/apps/website/content/docs/tutorials/first-project.mdx +++ b/apps/website/content/docs/tutorials/first-project.mdx @@ -255,4 +255,4 @@ The review skill checks adoption compliance, quality gates, Definition of Done, - **[Concepts](/docs/concepts)** — Understand how the Knowledge Base, Skills, and Adoption Files work together - **[Customization](/docs/customization)** — Adjust guidelines and quality gates for your specific needs - **[Team Setup tutorial](/docs/tutorials/team-setup)** — Ready to share pair with your team? Follow the team configuration tutorial -- **[Guides](/docs/guides)** — Goal-oriented guides for specific tasks +- **[Support](/docs/support)** — FAQ and troubleshooting when something doesn't work diff --git a/apps/website/content/docs/tutorials/index.mdx b/apps/website/content/docs/tutorials/index.mdx index 5dd72860..978560d3 100644 --- a/apps/website/content/docs/tutorials/index.mdx +++ b/apps/website/content/docs/tutorials/index.mdx @@ -33,6 +33,6 @@ Different from [Getting Started](/docs/getting-started) quickstarts (5–10 minu |---------|---------|--------| | [Getting Started](/docs/getting-started) | Quick setup — get running fast | 5–10 min | | **Tutorials** (you are here) | Learn by doing — full end-to-end workflow | 30–60 min | -| [Guides](/docs/guides) | Solve a specific problem — goal-oriented | 10–20 min | +| [Customization](/docs/customization) | Adapt pair to your context — goal-oriented | 10–20 min | | [Concepts](/docs/concepts) | Understand how things work — explanation | 5–15 min | | [Reference](/docs/reference) | Look up details — complete specifications | As needed | diff --git a/apps/website/content/docs/tutorials/team-setup.mdx b/apps/website/content/docs/tutorials/team-setup.mdx index f32ae683..f32505f6 100644 --- a/apps/website/content/docs/tutorials/team-setup.mdx +++ b/apps/website/content/docs/tutorials/team-setup.mdx @@ -212,4 +212,4 @@ If everything works end-to-end, your team setup is complete. - **[Customize for Your Team](/docs/customization/team)** — Deep dive into all customization options - **[Publish a Knowledge Base](/docs/customization/organization)** — Package your team's KB for distribution - **[Enterprise Adoption tutorial](/docs/tutorials/enterprise-adoption)** — Rolling out pair across an organization -- **[Guides](/docs/guides)** — Goal-oriented guides for specific tasks +- **[Support](/docs/support)** — FAQ and troubleshooting when something doesn't work diff --git a/apps/website/e2e/docs.e2e.test.ts b/apps/website/e2e/docs.e2e.test.ts index 2e399555..ec2a4a58 100644 --- a/apps/website/e2e/docs.e2e.test.ts +++ b/apps/website/e2e/docs.e2e.test.ts @@ -59,7 +59,7 @@ test('solo setup journey: quickstart → solo setup with content verification', // Solo setup page renders with expected content const main = page.locator('main') - await expect(page.locator('main h1')).toContainText('Solo Setup') + await expect(page.locator('main h1')).toContainText('Quickstart: Solo') await expect(main).toContainText('Solo Workflow') await expect(main).toContainText('/pair-next') await expect(main).toContainText('pair-cli install --list-targets') @@ -83,7 +83,7 @@ test('team journey: overview → team setup with content verification', async ({ await page.goto('/docs/getting-started') // Navigate to Team Setup via sidebar - await page.locator('a', { hasText: 'Team Setup' }).first().click() + await page.locator('a', { hasText: 'Quickstart: Team' }).first().click() await expect(page).toHaveURL('/docs/getting-started/quickstart-team') // AC-4: shared KB, adoption files, bridge pattern @@ -103,7 +103,7 @@ test('org journey: overview → org setup with content verification', async ({ p await page.goto('/docs/getting-started') // Navigate to Org Setup via sidebar - await page.locator('a', { hasText: 'Organization Setup' }).first().click() + await page.locator('a', { hasText: 'Quickstart: Organization' }).first().click() await expect(page).toHaveURL('/docs/getting-started/quickstart-org') // AC-5: KB packaging, distribution, compliance @@ -160,9 +160,9 @@ test('smoke: all docs pages return 200 with correct titles', async ({ page }) => const pages = [ { url: '/docs/getting-started', title: 'What is pair?' }, { url: '/docs/getting-started/quickstart', title: 'Quickstart' }, - { url: '/docs/getting-started/quickstart-solo', title: 'Solo Setup' }, - { url: '/docs/getting-started/quickstart-team', title: 'Team Setup' }, - { url: '/docs/getting-started/quickstart-org', title: 'Organization Setup' }, + { url: '/docs/getting-started/quickstart-solo', title: 'Quickstart: Solo' }, + { url: '/docs/getting-started/quickstart-team', title: 'Quickstart: Team' }, + { url: '/docs/getting-started/quickstart-org', title: 'Quickstart: Organization' }, { url: '/docs/concepts/ai-assisted-sdlc', title: 'AI-Assisted SDLC' }, { url: '/docs/concepts/knowledge-base', title: 'Knowledge Base' }, { url: '/docs/concepts/skills', title: 'Skills' }, @@ -332,8 +332,8 @@ test('developer journey: navigate through section pages', async ({ page }) => { await expect(main).toContainText('The Four Levels') await expect(main).toContainText('Entry Points') - // Sidebar shows Developer Journey section - await expect(page.locator('body')).toContainText('Developer Journey') + // Sidebar shows Process Lifecycle section (label unified with index title) + await expect(page.locator('body')).toContainText('Process Lifecycle') // Navigate to Induction await page diff --git a/apps/website/e2e/landing.e2e.test.ts b/apps/website/e2e/landing.e2e.test.ts index 05aefa54..66fe236b 100644 --- a/apps/website/e2e/landing.e2e.test.ts +++ b/apps/website/e2e/landing.e2e.test.ts @@ -48,8 +48,10 @@ test('docs sidebar navigates to Concepts', async ({ page }) => { .locator('section[aria-label="Call to action"] a') .filter({ hasText: 'Read the docs' }) .click() - await page.locator('button', { hasText: 'Concepts' }).click() - await page.locator('a', { hasText: 'AI-Assisted SDLC' }).click() + // Concepts now has an index page: the sidebar folder renders as a link, not a button + await page.locator('a', { hasText: 'Concepts' }).first().click() + await expect(page).toHaveURL('/docs/concepts') + await page.locator('a', { hasText: 'AI-Assisted SDLC' }).first().click() await expect(page).toHaveURL('/docs/concepts/ai-assisted-sdlc') await expect(page.locator('h1')).toContainText('AI-Assisted SDLC') }) @@ -70,7 +72,7 @@ test('full flow: landing → quickstart → concept page via Next link', async ( .click() await expect(page).toHaveURL('/docs/getting-started/quickstart') await expect(page.locator('h1')).toContainText('Quickstart') - // Use prev/next to go to Solo Setup - await page.locator('a', { hasText: 'Solo Setup' }).last().click() + // Use prev/next to go to Quickstart: Solo + await page.locator('a', { hasText: 'Quickstart: Solo' }).last().click() await expect(page).toHaveURL('/docs/getting-started/quickstart-solo') }) diff --git a/qa/release-validation/CP5-website-docs-completeness.md b/qa/release-validation/CP5-website-docs-completeness.md index 498c1899..bc91a551 100644 --- a/qa/release-validation/CP5-website-docs-completeness.md +++ b/qa/release-validation/CP5-website-docs-completeness.md @@ -23,7 +23,8 @@ - `$BASE_URL/docs/getting-started/quickstart-team` - `$BASE_URL/docs/getting-started/quickstart-org` -**Concepts** (6 pages): +**Concepts** (7 pages): +- `$BASE_URL/docs/concepts` - `$BASE_URL/docs/concepts/ai-assisted-sdlc` - `$BASE_URL/docs/concepts/knowledge-base` - `$BASE_URL/docs/concepts/skills` @@ -31,7 +32,7 @@ - `$BASE_URL/docs/concepts/agent-integration` - `$BASE_URL/docs/concepts/llms-txt` -**Developer Journey** (5 pages): +**Process Lifecycle** (5 pages): - `$BASE_URL/docs/developer-journey` - `$BASE_URL/docs/developer-journey/induction` - `$BASE_URL/docs/developer-journey/strategic-planning` @@ -67,7 +68,8 @@ - `$BASE_URL/docs/guides/troubleshooting` - `$BASE_URL/docs/guides/update-link` -**Reference** (9 pages): +**Reference** (10 pages): +- `$BASE_URL/docs/reference` - `$BASE_URL/docs/reference/cli/commands` - `$BASE_URL/docs/reference/cli/examples` - `$BASE_URL/docs/reference/specs/cli-contracts` @@ -100,13 +102,13 @@ ### Expected Result -- All 60 URLs return HTTP 200 +- All 62 URLs return HTTP 200 - Log any non-200 as FAIL with status code ### Notes - Use batch `curl -sI` or WebFetch for efficiency -- Total: 60 pages +- Total: 62 pages --- diff --git a/scripts/docs-staleness-check.js b/scripts/docs-staleness-check.js index aac5d3d7..01848d3c 100644 --- a/scripts/docs-staleness-check.js +++ b/scripts/docs-staleness-check.js @@ -9,8 +9,9 @@ const path = require('path') const ROOT = path.resolve(__dirname, '..') const SKILLS_DIR = path.join(ROOT, 'packages/knowledge-hub/dataset/.skills') const COMMANDS_DIR = path.join(ROOT, 'apps/pair-cli/src/commands') -const CATALOG_FILE = path.join(ROOT, 'apps/website/content/docs/reference/skills-catalog.mdx') -const COMMANDS_FILE = path.join(ROOT, 'apps/website/content/docs/reference/cli/commands.mdx') +const DOCS_DIR = path.join(ROOT, 'apps/website/content/docs') +const CATALOG_FILE = path.join(DOCS_DIR, 'reference/skills-catalog.mdx') +const COMMANDS_FILE = path.join(DOCS_DIR, 'reference/cli/commands.mdx') const errors = [] @@ -27,6 +28,18 @@ function getSkillNames(categoryDir) { return [] } +function walkMdx(dir) { + const out = [] + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) out.push(...walkMdx(full)) + else if (entry.name.endsWith('.mdx')) out.push(full) + } + return out +} + +const docsFiles = walkMdx(DOCS_DIR) + // --- Check 1 & 2: Skills --- const categories = fs.readdirSync(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()) @@ -39,12 +52,20 @@ const skillCount = allSkills.length const catalog = fs.readFileSync(CATALOG_FILE, 'utf-8') -// Check 1: every occurrence of "N skills" matches actual count -const countMatches = [...catalog.matchAll(/(\d+)\s+(?:pair\s+)?skills/g)] -for (const m of countMatches) { - const docCount = parseInt(m[1], 10) - if (docCount !== skillCount) { - errors.push(`Skill count mismatch: docs say "${m[0]}", actual count is ${skillCount}`) +// Check 1: every occurrence of "N skills" across ALL docs pages matches actual count. +// Regex is intentionally narrow to avoid prose false positives: it matches +// "N skills", "N pair skills", "N composable skills" (the total-count phrasings +// used in docs). Subset counts ("9 process skills") do NOT match because the +// word between the number and "skills" must be pair/composable or absent. +const COUNT_RE = /(\d+)\s+(?:pair\s+|composable\s+)?skills/g +for (const file of docsFiles) { + const content = fs.readFileSync(file, 'utf-8') + const rel = path.relative(DOCS_DIR, file) + for (const m of content.matchAll(COUNT_RE)) { + const docCount = parseInt(m[1], 10) + if (docCount !== skillCount) { + errors.push(`Skill count mismatch in ${rel}: docs say "${m[0]}", actual count is ${skillCount}`) + } } } @@ -112,6 +133,30 @@ if (fs.existsSync(TUTORIALS_DIR)) { } } +// --- Check 5: Dead internal docs links --- +// Every markdown link target starting with /docs must resolve to a page in the +// content tree (file route, or folder route backed by an index.mdx). Would have +// caught the index-less section links (/docs/concepts, /docs/guides, /docs/reference). + +const validRoutes = new Set() +for (const file of docsFiles) { + const rel = path.relative(DOCS_DIR, file).replace(/\\/g, '/').replace(/\.mdx$/, '') + const route = rel === 'index' ? '/docs' : `/docs/${rel.replace(/\/index$/, '')}` + validRoutes.add(route) +} + +const LINK_RE = /\]\((\/docs[^)\s]*)\)/g +for (const file of docsFiles) { + const content = fs.readFileSync(file, 'utf-8') + const rel = path.relative(DOCS_DIR, file) + for (const m of content.matchAll(LINK_RE)) { + const target = m[1].split('#')[0].split('?')[0].replace(/\/$/, '') || '/docs' + if (!validRoutes.has(target)) { + errors.push(`Dead internal link in ${rel}: ${m[1]} does not resolve to a docs page`) + } + } +} + // --- Output --- console.log('Docs Staleness Check') From d4500badecf784bfa100692ad0dbdd1adcb0875c Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 09:32:56 +0200 Subject: [PATCH 03/14] =?UTF-8?q?[#312]=20docs:=20batch=202=20=E2=80=94=20?= =?UTF-8?q?merge=20FAQ+troubleshooting=20into=20support/troubleshooting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Union of support/faq.mdx (361L) + guides/troubleshooting.mdx (236L) into support/troubleshooting.mdx; conflicting permission fix reconciled (nvm recommended, npm-prefix alternative). Redirects /docs/support/faq + /docs/guides/troubleshooting -> /docs/support/troubleshooting (new redirects() block). Inbound links updated (grep zero in content/). E2e: redirect test added, sidebar/smoke lists updated. CP5 inventory synced. --- .../content/docs/guides/adopter-checklist.mdx | 2 +- .../content/docs/guides/cli-workflows.mdx | 2 +- .../content/docs/guides/install-from-url.mdx | 2 +- apps/website/content/docs/guides/meta.json | 1 - .../content/docs/guides/troubleshooting.mdx | 236 ------------------ .../content/docs/support/general-faq.mdx | 2 +- apps/website/content/docs/support/index.mdx | 6 +- apps/website/content/docs/support/meta.json | 2 +- .../support/{faq.mdx => troubleshooting.mdx} | 232 +++++++++++------ apps/website/e2e/docs.e2e.test.ts | 31 ++- apps/website/next.config.mjs | 15 ++ .../CP5-website-docs-completeness.md | 5 +- 12 files changed, 201 insertions(+), 335 deletions(-) delete mode 100644 apps/website/content/docs/guides/troubleshooting.mdx rename apps/website/content/docs/support/{faq.mdx => troubleshooting.mdx} (65%) diff --git a/apps/website/content/docs/guides/adopter-checklist.mdx b/apps/website/content/docs/guides/adopter-checklist.mdx index 56be0ee7..b32061ea 100644 --- a/apps/website/content/docs/guides/adopter-checklist.mdx +++ b/apps/website/content/docs/guides/adopter-checklist.mdx @@ -162,5 +162,5 @@ If everything worked: If issues occurred: -- Review the [Troubleshooting Guide](/docs/guides/troubleshooting) +- Review the [Troubleshooting](/docs/support/troubleshooting) - File an issue at [GitHub Issues](https://github.com/foomakers/pair/issues) diff --git a/apps/website/content/docs/guides/cli-workflows.mdx b/apps/website/content/docs/guides/cli-workflows.mdx index ff67749e..b9d74c0a 100644 --- a/apps/website/content/docs/guides/cli-workflows.mdx +++ b/apps/website/content/docs/guides/cli-workflows.mdx @@ -302,4 +302,4 @@ If commands fail, check: - Permissions: Try with `sudo` if installing globally - Network: For offline installs, use manual download -See the [Troubleshooting Guide](/docs/guides/troubleshooting) for detailed solutions. +See the [Troubleshooting](/docs/support/troubleshooting) for detailed solutions. diff --git a/apps/website/content/docs/guides/install-from-url.mdx b/apps/website/content/docs/guides/install-from-url.mdx index 31e50d89..edf49e49 100644 --- a/apps/website/content/docs/guides/install-from-url.mdx +++ b/apps/website/content/docs/guides/install-from-url.mdx @@ -172,4 +172,4 @@ pair update --offline --source ./updated-kb - [CLI Commands Reference](/docs/reference/cli/commands) — Full `install` and `update` syntax - [KB Source Resolution Spec](/docs/reference/specs/kb-source-resolution) — Technical spec for resolution algorithm -- [Troubleshooting](/docs/guides/troubleshooting) — Common installation issues +- [Troubleshooting](/docs/support/troubleshooting) — Common installation issues diff --git a/apps/website/content/docs/guides/meta.json b/apps/website/content/docs/guides/meta.json index 418d5ab7..d6d9ba18 100644 --- a/apps/website/content/docs/guides/meta.json +++ b/apps/website/content/docs/guides/meta.json @@ -6,7 +6,6 @@ "customize-kb", "adopter-checklist", "packaging", - "troubleshooting", "update-link" ] } diff --git a/apps/website/content/docs/guides/troubleshooting.mdx b/apps/website/content/docs/guides/troubleshooting.mdx deleted file mode 100644 index f34a8d29..00000000 --- a/apps/website/content/docs/guides/troubleshooting.mdx +++ /dev/null @@ -1,236 +0,0 @@ ---- -title: Troubleshooting -description: Common issues and solutions for pair-cli installation and usage. ---- - -Common issues and solutions for pair-cli installation and usage. - -## Installation Issues - -### Permission Errors - -**Problem:** `npm install -g @foomakers/pair-cli` fails with permission errors. - -**Solution:** Use a Node version manager or install locally. - -```bash -# Option 1: Use nvm (recommended) -curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash -nvm install 18 -nvm use 18 - -# Option 2: Install locally in project -pnpm add -D @foomakers/pair-cli -# Use with: pnpm dlx pair-cli - -# Option 3: Use sudo (not recommended) -sudo npm install -g @foomakers/pair-cli -``` - -### Command Not Found - -**Problem:** `pair-cli` command not found after installation. - -**Solutions:** - -```bash -# Check PATH -echo $PATH -which pair-cli - -# If using nvm, ensure Node version is active -nvm current -nvm use 18 - -# Restart terminal or source profile -source ~/.bashrc # or ~/.zshrc - -# For local install, use npx -npx pair-cli --version -``` - -### Node.js Version Issues - -**Problem:** Errors about unsupported Node version. - -**Solution:** Ensure Node 18+ is active. - -```bash -# Check version -node --version # Should be 18.0.0 or higher - -# Update with nvm -nvm install 18 -nvm use 18 -nvm alias default 18 -``` - -## Runtime Issues - -### Target Directory Permissions - -**Problem:** `pair-cli install` fails with "permission denied". - -**Solution:** Ensure write access to target directory. - -```bash -# Check permissions -ls -la . - -# Fix permissions if needed -chmod 755 . - -# Or run in a directory you own -mkdir ~/my-project -cd ~/my-project -pair-cli install -``` - -### Network/Proxy Issues - -**Problem:** Installation fails due to network restrictions. - -**Solutions:** - -```bash -# Use manual install for offline environments -# Download from: https://github.com/foomakers/pair/releases -# Extract and run directly - -# Configure npm proxy -npm config set proxy http://proxy.company.com:8080 -npm config set https-proxy http://proxy.company.com:8080 - -# Or use manual install -``` - -## Platform-Specific Issues - -### macOS - -**Problem:** "command not found" after global install. - -**Solution:** Check PATH and npm prefix. - -```bash -# Check npm global install location -npm config get prefix - -# Ensure it's in PATH -export PATH="$(npm config get prefix)/bin:$PATH" - -# Add to ~/.zshrc or ~/.bashrc -echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> ~/.zshrc -``` - -### Linux - -**Problem:** Native module compilation fails. - -**Solution:** Install build tools. - -```bash -# Ubuntu/Debian -sudo apt-get update -sudo apt-get install build-essential python3-dev - -# CentOS/RHEL -sudo yum groupinstall "Development Tools" -sudo yum install python3-devel -``` - -### Windows - -**Problem:** Line ending or path issues. - -**Solutions:** - -```bash -# Use Git Bash or WSL -# Ensure Node is in PATH -where node - -# Use manual install with .cmd files -# Download pair-cli-manual-vX.Y.Z.zip -# Run pair-cli.cmd instead of pair-cli -``` - -## Verification Issues - -### Assets Not Installing - -**Problem:** `pair-cli install` runs but no files are created. - -**Solutions:** - -```bash -# Check current directory -pwd - -# Ensure you have write permissions -touch test.txt && rm test.txt - -# Try with debug-level logging (if available) -pair-cli install --log-level debug - -# Check if assets are available -pair-cli install --list-targets -``` - -### Outdated Assets - -**Problem:** Installed assets seem outdated. - -**Solution:** Update to latest version. - -```bash -# Update all assets -pair-cli update - -# Or reinstall -pair-cli install --force # if available -``` - -## Getting Help - -### Diagnostic Information - -When reporting issues, include: - -```bash -# System info -uname -a -node --version -npm --version -pnpm --version - -# pair-cli info -pair-cli --version - -# Installation method -which pair-cli -npm list -g @foomakers/pair-cli -``` - -### Support Channels - -- **Documentation:** Check this guide and the [Installation FAQ](/docs/support/faq) -- **Issues:** File bugs at [GitHub Issues](https://github.com/foomakers/pair/issues) -- **Discussions:** Ask questions in [GitHub Discussions](https://github.com/foomakers/pair/discussions) - -## Quick Fixes - -| Issue | Quick Fix | -| ----------------- | ---------------------------- | -| Permission denied | `chmod 755 .` or use `sudo` | -| Command not found | `npx pair-cli` or check PATH | -| Node version | `nvm use 18` | -| Network blocked | Use manual install | -| Files not created | Check directory permissions | - -## Prevention - -- Always use Node 18+ with pnpm -- Install globally with nvm, locally with pnpm dlx -- Test in a clean directory first -- Keep pair-cli updated: `npm update -g @foomakers/pair-cli` diff --git a/apps/website/content/docs/support/general-faq.mdx b/apps/website/content/docs/support/general-faq.mdx index 6513c9e6..2e44826f 100644 --- a/apps/website/content/docs/support/general-faq.mdx +++ b/apps/website/content/docs/support/general-faq.mdx @@ -161,7 +161,7 @@ Yes. Create a KB from scratch or fork an existing one, structure it following th Check Node.js version (18+), permissions, and PATH. The Installation FAQ covers all common issues step by step. -[Installation FAQ](/docs/support/faq) +[Troubleshooting](/docs/support/troubleshooting) ### The AI ignores my adoption files — what's wrong? diff --git a/apps/website/content/docs/support/index.mdx b/apps/website/content/docs/support/index.mdx index fbd95787..97578544 100644 --- a/apps/website/content/docs/support/index.mdx +++ b/apps/website/content/docs/support/index.mdx @@ -8,7 +8,7 @@ Get help with pair installation, configuration, and usage. ## Quick Links - [FAQ](/docs/support/general-faq) — Common questions about pair, process, languages, and PM tools -- [Installation FAQ](/docs/support/faq) — Solutions for common install issues +- [Troubleshooting](/docs/support/troubleshooting) — Diagnostics and fixes for install and setup issues - [GitHub Issues](https://github.com/foomakers/pair/issues) — Report bugs and feature requests ## Support Scope @@ -30,7 +30,7 @@ We cannot help with: ## Before Reporting Issues -1. **Check the FAQ first**: The [Installation FAQ](/docs/support/faq) covers 80%+ of issues +1. **Check the Troubleshooting page first**: [Troubleshooting](/docs/support/troubleshooting) covers 80%+ of issues 2. **Search existing issues**: [GitHub Issues](https://github.com/foomakers/pair/issues) 3. **Test with clean environment**: Isolate the issue from other tools/configs @@ -38,7 +38,7 @@ We cannot help with: ### Step 1: Self-Service -- Review the [Installation FAQ](/docs/support/faq) +- Review the [Troubleshooting](/docs/support/troubleshooting) page - Search [existing GitHub issues](https://github.com/foomakers/pair/issues) ### Step 2: Community Support diff --git a/apps/website/content/docs/support/meta.json b/apps/website/content/docs/support/meta.json index 927c8d1d..d73c7753 100644 --- a/apps/website/content/docs/support/meta.json +++ b/apps/website/content/docs/support/meta.json @@ -1,4 +1,4 @@ { "title": "Support", - "pages": ["general-faq", "faq"] + "pages": ["general-faq", "troubleshooting"] } diff --git a/apps/website/content/docs/support/faq.mdx b/apps/website/content/docs/support/troubleshooting.mdx similarity index 65% rename from apps/website/content/docs/support/faq.mdx rename to apps/website/content/docs/support/troubleshooting.mdx index d9c6c27d..389b32ec 100644 --- a/apps/website/content/docs/support/faq.mdx +++ b/apps/website/content/docs/support/troubleshooting.mdx @@ -1,9 +1,9 @@ --- -title: Installation FAQ -description: Common installation and setup issues with step-by-step solutions for pair-cli. +title: Troubleshooting +description: One page for install and setup problems — quick diagnostics, common issues, and step-by-step fixes for pair-cli. --- -Common installation and setup issues with step-by-step solutions for pair-cli. +Install or setup problem? This page collects every known issue and its fix — from permission errors to corrupted installs. ## Quick Diagnostics @@ -20,28 +20,50 @@ which pair-cli # Should show installation path npm list -g @foomakers/pair-cli # Global npm installation check ``` +## Quick Fixes + +| Issue | Quick Fix | +| ----------------- | ------------------------------------ | +| Permission denied | Use nvm, or fix the npm prefix | +| Command not found | Check PATH, or use `npx pair-cli` | +| Node version | `nvm use 18` | +| Network blocked | Use manual install | +| Files not created | Check directory permissions | + ## Permission Issues ### Problem: `EACCES` or permission denied errors during installation -**On macOS/Linux:** +**Recommended fix — use a Node version manager (nvm).** It installs Node in your home directory, so global installs never need elevated permissions (and it fixes Node-version problems at the same time): + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash +source ~/.bashrc +nvm install 18 +nvm use 18 +npm install -g @foomakers/pair-cli +``` + +**Alternatives** if you can't use nvm: ```bash -# Option 1: Use npm with correct permissions (recommended) +# Option 2: Point npm's global prefix at a user-writable directory npm config set prefix ~/.npm-global echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.profile source ~/.profile npm install -g @foomakers/pair-cli -# Option 2: Use pnpm (avoids permission issues) +# Option 3: Use pnpm (avoids permission issues) npm install -g pnpm pnpm add -g @foomakers/pair-cli -# Option 3: Fix npm permissions (if you prefer npm) -sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share} -npm install -g @foomakers/pair-cli +# Option 4: Install locally in the project +pnpm add -D @foomakers/pair-cli +# Use with: pnpm dlx pair-cli ``` +Avoid `sudo npm install -g` — it hides the underlying problem and creates root-owned files that break later installs. + **On Windows:** ```cmd @@ -51,14 +73,14 @@ npm install -g @foomakers/pair-cli ### Problem: Can't write to `/usr/local/lib/node_modules` +Same root cause: npm's global prefix points at a system directory. Use nvm (recommended, above) or move the prefix: + ```bash -# Change npm's default directory mkdir ~/.npm-global npm config set prefix '~/.npm-global' echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.profile source ~/.profile -# Now install npm install -g @foomakers/pair-cli ``` @@ -163,6 +185,85 @@ npm install -g pnpm pnpm add -g @foomakers/pair-cli ``` +## Command Not Found & PATH Issues + +### Problem: `pair-cli` command not found after installation + +**Verify installation location:** + +```bash +# Check if installed +npm list -g @foomakers/pair-cli +pnpm list -g @foomakers/pair-cli + +# Check PATH +echo $PATH +which pair-cli +``` + +**Fix PATH issues:** + +```bash +# Add npm global bin to PATH +npm config get prefix # Note the output path +echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> ~/.profile +source ~/.profile + +# If using nvm, ensure the right Node version is active +nvm current +nvm use 18 + +# Restart terminal or source profile +source ~/.bashrc # or ~/.zshrc + +# For local installs, use npx +npx pair-cli --version +``` + +### Problem: Command exists but fails to run + +**Clean reinstall steps:** + +```bash +# 1. Remove completely +npm uninstall -g @foomakers/pair-cli +# or +pnpm remove -g @foomakers/pair-cli + +# 2. Clear caches +npm cache clean --force +# or +pnpm store prune + +# 3. Clear global node_modules (careful!) +rm -rf ~/.npm/_global/node_modules/@pair +# or similar path for your system + +# 4. Reinstall fresh +npm install -g @foomakers/pair-cli +``` + +### Problem: Version conflicts or "command not found" after update + +**Deterministic cleanup:** + +```bash +# 1. Find all pair-cli installations +find /usr/local -name "*pair-cli*" 2>/dev/null +find ~/.npm -name "*pair-cli*" 2>/dev/null +find ~/.pnpm -name "*pair-cli*" 2>/dev/null + +# 2. Remove all found instances +# (Review the list first, then remove manually) + +# 3. Clean install +npm install -g @foomakers/pair-cli + +# 4. Verify +pair-cli --version +which pair-cli +``` + ## Native Build Failures ### Problem: `node-gyp` errors or native module build failures @@ -183,7 +284,7 @@ npm install -g @foomakers/pair-cli ```bash # Install build tools sudo apt update -sudo apt install build-essential python3-distutils +sudo apt install build-essential python3-dev python3-distutils # Clear and reinstall npm uninstall -g @foomakers/pair-cli @@ -236,6 +337,8 @@ echo 'export PATH=$HOME/pair-cli-manual-vX.Y.Z/bin:$PATH' >> ~/.profile source ~/.profile ``` +On Windows, run `pair-cli.cmd` from the extracted archive instead of `pair-cli` (use Git Bash or WSL if you hit line-ending or path issues). + ### Problem: Corporate proxy blocking installs ```bash @@ -251,78 +354,62 @@ npm config set proxy http://username:password@proxy.company.com:8080 npm install -g @foomakers/pair-cli ``` -## Corrupted Installs +## Runtime Issues -### Problem: `pair-cli` command not found after installation +### Problem: `pair-cli install` fails with "permission denied" -**Verify installation location:** +**Solution:** Ensure write access to the target directory. ```bash -# Check if installed -npm list -g @foomakers/pair-cli -pnpm list -g @foomakers/pair-cli +# Check permissions +ls -la . -# Check PATH -echo $PATH -which pair-cli -``` - -**Fix PATH issues:** +# Fix permissions if needed +chmod 755 . -```bash -# Add npm global bin to PATH -npm config get prefix # Note the output path -echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> ~/.profile -source ~/.profile +# Or run in a directory you own +mkdir ~/my-project +cd ~/my-project +pair-cli install ``` -### Problem: Command exists but fails to run +### Problem: `pair-cli install` runs but no files are created -**Clean reinstall steps:** +**Solutions:** ```bash -# 1. Remove completely -npm uninstall -g @foomakers/pair-cli -# or -pnpm remove -g @foomakers/pair-cli +# Check current directory +pwd -# 2. Clear caches -npm cache clean --force -# or -pnpm store prune +# Ensure you have write permissions +touch test.txt && rm test.txt -# 3. Clear global node_modules (careful!) -rm -rf ~/.npm/_global/node_modules/@pair -# or similar path for your system +# Try with debug-level logging (if available) +pair-cli install --log-level debug -# 4. Reinstall fresh -npm install -g @foomakers/pair-cli +# Check if assets are available +pair-cli install --list-targets ``` -### Problem: Version conflicts or "command not found" after update +### Problem: Installed assets seem outdated -**Deterministic cleanup:** +**Solution:** Update to the latest version. ```bash -# 1. Find all pair-cli installations -find /usr/local -name "*pair-cli*" 2>/dev/null -find ~/.npm -name "*pair-cli*" 2>/dev/null -find ~/.pnpm -name "*pair-cli*" 2>/dev/null +# Update all assets +pair-cli update -# 2. Remove all found instances -# (Review the list first, then remove manually) - -# 3. Clean install -npm install -g @foomakers/pair-cli - -# 4. Verify -pair-cli --version -which pair-cli +# Or reinstall +pair-cli install --force # if available ``` -## Diagnostic Commands +## Getting Additional Help + +If these solutions don't resolve your issue: -When reporting issues, include output from these commands: +1. **Check existing issues**: [GitHub Issues](https://github.com/foomakers/pair/issues) +2. **Ask the community**: [GitHub Discussions](https://github.com/foomakers/pair/discussions) +3. **Open a new issue**: Include output from these diagnostic commands: ```bash # System information @@ -342,20 +429,13 @@ npm config get prefix # npm global prefix npm config list # npm configuration ``` -## Getting Additional Help +**Include in your report**: operating system and version, Node.js and npm/pnpm versions, the complete error message, and the steps you've already tried. -If these solutions don't resolve your issue: +**Do not include**: personal paths with usernames, API keys or tokens, corporate proxy credentials. -1. **Check existing issues**: [GitHub Issues](https://github.com/foomakers/pair/issues) -2. **Open a new issue**: Include diagnostic output from commands above -3. **Include in your report**: - - Operating system and version - - Node.js and npm/pnpm versions - - Complete error message - - Steps you've already tried - -**Do not include** in reports: - -- Personal paths with usernames -- API keys or tokens -- Corporate proxy credentials +## Prevention + +- Always use Node 18+ (managed with nvm) and pnpm +- Install globally with nvm-managed Node, or locally with `pnpm dlx` +- Test in a clean directory first +- Keep pair-cli updated: `npm update -g @foomakers/pair-cli` diff --git a/apps/website/e2e/docs.e2e.test.ts b/apps/website/e2e/docs.e2e.test.ts index ec2a4a58..73eed10e 100644 --- a/apps/website/e2e/docs.e2e.test.ts +++ b/apps/website/e2e/docs.e2e.test.ts @@ -194,10 +194,21 @@ test('guides section: navigate and verify content', async ({ page }) => { await expect(page.locator('body')).toContainText('Guides') // Navigate to another guide via sidebar - await page.locator('a', { hasText: 'Troubleshooting' }).first().click() - await expect(page).toHaveURL('/docs/guides/troubleshooting') + await page.locator('a', { hasText: 'Install from URL' }).first().click() + await expect(page).toHaveURL('/docs/guides/install-from-url') + await expect(page.locator('main h1')).toContainText('Install from URL') +}) + +test('redirects: old FAQ/troubleshooting URLs land on support/troubleshooting', async ({ + page, +}) => { + await page.goto('/docs/support/faq') + await expect(page).toHaveURL('/docs/support/troubleshooting') + await expect(page.locator('main h1')).toContainText('Troubleshooting') + + await page.goto('/docs/guides/troubleshooting') + await expect(page).toHaveURL('/docs/support/troubleshooting') await expect(page.locator('main h1')).toContainText('Troubleshooting') - await expect(main).toContainText('Installation Issues') }) test('reference section: navigate CLI, specs, and top-level pages', async ({ page }) => { @@ -263,10 +274,10 @@ test('support section: navigate and verify content', async ({ page }) => { await expect(main).toContainText('Project Management') await expect(main).toContainText('Customization') - // Navigate to Installation FAQ via sidebar - await page.locator('a', { hasText: 'Installation FAQ' }).first().click() - await expect(page).toHaveURL('/docs/support/faq') - await expect(page.locator('main h1')).toContainText('Installation FAQ') + // Navigate to Troubleshooting via sidebar (merged Installation FAQ + guides troubleshooting) + await page.locator('a', { hasText: 'Troubleshooting' }).first().click() + await expect(page).toHaveURL('/docs/support/troubleshooting') + await expect(page.locator('main h1')).toContainText('Troubleshooting') await expect(main).toContainText('Permission Issues') await expect(main).toContainText('Node Version Issues') }) @@ -277,7 +288,6 @@ test('smoke: all guides/reference/support pages return 200', async ({ page }) => { url: '/docs/guides/install-from-url', title: 'Install from URL' }, { url: '/docs/guides/customize-kb', title: 'Customize the Knowledge Base' }, { url: '/docs/guides/adopter-checklist', title: 'Adopter Checklist' }, - { url: '/docs/guides/troubleshooting', title: 'Troubleshooting' }, { url: '/docs/guides/update-link', title: 'Link Update' }, { url: '/docs/reference/cli/commands', title: 'CLI Commands' }, { url: '/docs/reference/cli/examples', title: 'CLI Help Examples' }, @@ -290,7 +300,7 @@ test('smoke: all guides/reference/support pages return 200', async ({ page }) => { url: '/docs/reference/configuration', title: 'Configuration' }, { url: '/docs/support', title: 'Support' }, { url: '/docs/support/general-faq', title: 'FAQ' }, - { url: '/docs/support/faq', title: 'Installation FAQ' }, + { url: '/docs/support/troubleshooting', title: 'Troubleshooting' }, ] for (const { url, title } of pages) { const response = await page.goto(url) @@ -884,7 +894,6 @@ test('no circular prev/next footer links on any docs page', async ({ page }) => '/docs/guides/install-from-url', '/docs/guides/customize-kb', '/docs/guides/adopter-checklist', - '/docs/guides/troubleshooting', '/docs/guides/update-link', '/docs/reference/cli/commands', '/docs/reference/cli/examples', @@ -897,7 +906,7 @@ test('no circular prev/next footer links on any docs page', async ({ page }) => '/docs/reference/configuration', '/docs/support', '/docs/support/general-faq', - '/docs/support/faq', + '/docs/support/troubleshooting', '/docs/tutorials', '/docs/tutorials/first-project', '/docs/tutorials/existing-project', diff --git a/apps/website/next.config.mjs b/apps/website/next.config.mjs index 67e0062b..b4e80f39 100644 --- a/apps/website/next.config.mjs +++ b/apps/website/next.config.mjs @@ -5,6 +5,21 @@ const withMDX = createMDX() /** @type {import('next').NextConfig} */ const nextConfig = { transpilePackages: ['@pair/brand'], + async redirects() { + // Permanent redirects for docs IA restructuring (#312) + return [ + { + source: '/docs/support/faq', + destination: '/docs/support/troubleshooting', + permanent: true, + }, + { + source: '/docs/guides/troubleshooting', + destination: '/docs/support/troubleshooting', + permanent: true, + }, + ] + }, } export default withMDX(nextConfig) diff --git a/qa/release-validation/CP5-website-docs-completeness.md b/qa/release-validation/CP5-website-docs-completeness.md index bc91a551..0062236a 100644 --- a/qa/release-validation/CP5-website-docs-completeness.md +++ b/qa/release-validation/CP5-website-docs-completeness.md @@ -60,12 +60,11 @@ - `$BASE_URL/docs/pm-tools/filesystem` - `$BASE_URL/docs/pm-tools/linear` -**Guides** (6 pages): +**Guides** (5 pages): - `$BASE_URL/docs/guides/cli-workflows` - `$BASE_URL/docs/guides/install-from-url` - `$BASE_URL/docs/guides/customize-kb` - `$BASE_URL/docs/guides/adopter-checklist` -- `$BASE_URL/docs/guides/troubleshooting` - `$BASE_URL/docs/guides/update-link` **Reference** (10 pages): @@ -83,7 +82,7 @@ **Support** (3 pages): - `$BASE_URL/docs/support` - `$BASE_URL/docs/support/general-faq` -- `$BASE_URL/docs/support/faq` +- `$BASE_URL/docs/support/troubleshooting` **Tutorials** (5 pages): - `$BASE_URL/docs/tutorials` From 090eca2a0f32e763f52d6fb5611bb8b02cfaff43 Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 09:43:55 +0200 Subject: [PATCH 04/14] =?UTF-8?q?[#312]=20docs:=20batch=203=20=E2=80=94=20?= =?UTF-8?q?dissolve=20guides/=20(moves,=20merges,=20redirects,=20nav)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 moves: adopter-checklist->getting-started/checklist, cli-workflows-> reference/cli/workflows, install-from-url->customization/install-from-url, update-link->reference/cli/update-link. 2 merges: customize-kb-> customization/team (Quick Recipe section), packaging->customization/ organization (layout-modes detail kept, org flags reconciled with parser, pair->pair-cli normalized). guides/ removed; 7 redirects added (full table now 9 rows incl /docs/guides->/docs). Root meta: drop guides, promote tutorials (13->12). Inbound links updated — grep /docs/guides zero in content/ (incl. one link-only edit in frozen reference/cli/commands.mdx). Hub: duplicate h1 fixed. E2e: moved-pages + dissolved-guides redirect tests, smoke/prev-next lists updated — 31/31 green. CP5 inventory synced. --- .../content/docs/customization/adopt.mdx | 4 +- .../content/docs/customization/index.mdx | 2 +- .../install-from-url.mdx | 0 .../content/docs/customization/meta.json | 2 +- .../docs/customization/organization.mdx | 100 +++++++++++++- .../content/docs/customization/team.mdx | 49 ++++++- .../checklist.mdx} | 2 +- .../content/docs/getting-started/meta.json | 2 +- .../content/docs/guides/customize-kb.mdx | 63 --------- apps/website/content/docs/guides/meta.json | 11 -- .../website/content/docs/guides/packaging.mdx | 125 ------------------ apps/website/content/docs/index.mdx | 4 +- apps/website/content/docs/meta.json | 7 +- .../content/docs/reference/cli/commands.mdx | 2 +- .../content/docs/reference/cli/examples.mdx | 2 +- .../content/docs/reference/cli/meta.json | 2 +- .../{guides => reference/cli}/update-link.mdx | 0 .../cli/workflows.mdx} | 2 +- .../content/docs/reference/kb-structure.mdx | 2 +- .../content/docs/support/general-faq.mdx | 4 +- .../docs/tutorials/managing-ai-artifacts.mdx | 4 +- .../docs/tutorials/release-testing.mdx | 2 +- apps/website/e2e/docs.e2e.test.ts | 56 +++++--- apps/website/next.config.mjs | 35 +++++ .../CP5-website-docs-completeness.md | 21 ++- 25 files changed, 244 insertions(+), 259 deletions(-) rename apps/website/content/docs/{guides => customization}/install-from-url.mdx (100%) rename apps/website/content/docs/{guides/adopter-checklist.mdx => getting-started/checklist.mdx} (98%) delete mode 100644 apps/website/content/docs/guides/customize-kb.mdx delete mode 100644 apps/website/content/docs/guides/meta.json delete mode 100644 apps/website/content/docs/guides/packaging.mdx rename apps/website/content/docs/{guides => reference/cli}/update-link.mdx (100%) rename apps/website/content/docs/{guides/cli-workflows.mdx => reference/cli/workflows.mdx} (98%) diff --git a/apps/website/content/docs/customization/adopt.mdx b/apps/website/content/docs/customization/adopt.mdx index 3dc25e5a..de3be153 100644 --- a/apps/website/content/docs/customization/adopt.mdx +++ b/apps/website/content/docs/customization/adopt.mdx @@ -58,7 +58,7 @@ pair-cli install --source github --url https://github.com/org/my-kb/releases/lat pair-cli install --source local --url /path/to/kb ``` -See [CLI Commands](/docs/reference/cli/commands) for the full `install` reference and [Install from URL](/docs/guides/install-from-url) for advanced source options. +See [CLI Commands](/docs/reference/cli/commands) for the full `install` reference and [Install from URL](/docs/customization/install-from-url) for advanced source options. ## Verify the Installation @@ -101,7 +101,7 @@ As the upstream KB evolves, pull updates without losing your project-specific de pair-cli update ``` -The update replaces `.pair/knowledge/` (upstream content) but preserves `.pair/adoption/` (your decisions). See [Customize the Knowledge Base](/docs/guides/customize-kb) for details on how the layered architecture protects your customizations. +The update replaces `.pair/knowledge/` (upstream content) but preserves `.pair/adoption/` (your decisions). See [Customize the Knowledge Base](/docs/customization/team) for details on how the layered architecture protects your customizations. ## Next Steps diff --git a/apps/website/content/docs/customization/index.mdx b/apps/website/content/docs/customization/index.mdx index cefde3ef..9f386ad5 100644 --- a/apps/website/content/docs/customization/index.mdx +++ b/apps/website/content/docs/customization/index.mdx @@ -51,5 +51,5 @@ Override the built-in templates that control commit messages, PR descriptions, u - [Tutorials](/docs/tutorials) — End-to-end walkthroughs for adopt → customize → publish - [Quickstart](/docs/getting-started/quickstart) — Install pair-cli and set up a project - [KB Structure Reference](/docs/reference/kb-structure) — Directory layout and file purposes -- [Customize the Knowledge Base](/docs/guides/customize-kb) — Quick guide to overriding defaults +- [Customize for Your Team](/docs/customization/team) — Override guidelines, quality gates, and defaults - [Brand Identity Guide](https://github.com/foomakers/pair/blob/main/packages/brand/BRAND.md) — Logo, colors, typography, and design tokens diff --git a/apps/website/content/docs/guides/install-from-url.mdx b/apps/website/content/docs/customization/install-from-url.mdx similarity index 100% rename from apps/website/content/docs/guides/install-from-url.mdx rename to apps/website/content/docs/customization/install-from-url.mdx diff --git a/apps/website/content/docs/customization/meta.json b/apps/website/content/docs/customization/meta.json index d52fd7f2..c8ebd718 100644 --- a/apps/website/content/docs/customization/meta.json +++ b/apps/website/content/docs/customization/meta.json @@ -1,4 +1,4 @@ { "title": "Customization", - "pages": ["adopt", "team", "organization", "templates"] + "pages": ["adopt", "install-from-url", "team", "organization", "templates"] } diff --git a/apps/website/content/docs/customization/organization.mdx b/apps/website/content/docs/customization/organization.mdx index c408bff3..62e625f8 100644 --- a/apps/website/content/docs/customization/organization.mdx +++ b/apps/website/content/docs/customization/organization.mdx @@ -3,6 +3,8 @@ title: Publish a Knowledge Base description: Create, structure, package, and distribute a Knowledge Base for your organization. --- +import { Callout } from 'fumadocs-ui/components/callout' + You've customized pair for your team. Now you want to codify those practices into a distributable Knowledge Base — so every team in your organization starts from the same foundation. This guide covers the full workflow from creation to distribution. ## Plan Your KB @@ -113,18 +115,104 @@ Each `SKILL.md` contains structured instructions the AI discovers and executes. ## Package for Distribution -Package your KB for distribution: +The `pair-cli package` command creates a distributable ZIP containing your KB's knowledge, adoption templates, and skills, with metadata (name, version, description) for registry listing. It supports two **layout modes** that determine which files are included: + +| Layout | Flag | What it packages | When to use | +| -------------------- | ----------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------- | +| **Target** (default) | `--layout target` | Files from installed target directories (e.g., `.pair/knowledge/`, `.claude/skills/`) | Packaging an installed KB for redistribution | +| **Source** | `--layout source` | Files from the single source directory defined in `config.json` | Packaging the original KB dataset before installation | + +### Source layout + +Packages from the **source directory** defined in each registry's `source` field in `config.json`. This is the canonical, pre-installation structure. + +```bash +pair-cli package --layout source +``` + +Use source layout when: + +- You maintain a KB dataset and want to distribute the **original files** +- You want a clean package without installation-time transformations (prefix, flatten) +- You're building a CI/CD pipeline that packages from the dataset directly + +**Example:** A KB with `config.json` registry `skills.source: ".skills"` packages from `.skills/category/name/SKILL.md`. + +### Target layout + +Packages from **installed target directories** — the files as they appear after `pair-cli install`. This is the default. ```bash -pair-cli package +pair-cli package --layout target +``` + +Use target layout when: + +- You want to package the KB **as users see it** after installation +- You need to include prefix/flatten transformations applied during install +- You're redistributing a customized KB from a project + +**Example:** The same skills registry with `flatten: true, prefix: "pair"` packages from `.claude/skills/pair-category-name/SKILL.md`. + + + Target layout excludes symlink targets automatically — only canonical (physical copy) targets are + included. + + +### Layout and config.json + +The layout mode maps to the `source` and `targets` fields in your `config.json` registries: + +```json +{ + "asset_registries": { + "skills": { + "source": ".skills", + "flatten": true, + "prefix": "pair", + "targets": [ + { "path": ".claude/skills", "mode": "canonical" }, + { "path": ".cursor/skills", "mode": "symlink" } + ] + } + } +} +``` + +- `--layout source` reads from `.skills/` +- `--layout target` reads from `.claude/skills/` (canonical target only — `.cursor/skills/` is excluded because it's a symlink) + +### Packaging walkthrough + +```bash +# 1. Validate with the same layout you'll package +pair-cli kb validate --layout source + +# 2. Package (with metadata) +pair-cli package --layout source \ + --name "My KB" \ + --version 1.0.0 \ + --description "Team knowledge base" \ + -o dist/my-kb-source.zip + +# 3. Inspect the ZIP contents +unzip -l dist/my-kb-source.zip + +# 4. Validate the packaged KB +pair-cli kb validate dist/my-kb-source.zip ``` -This creates a distributable archive containing your KB's knowledge, adoption templates, and skills. The package includes metadata (name, version, description) for registry listing. +### Organizational packaging -For organizational metadata: +For enterprise distribution, add organizational metadata: ```bash -pair-cli package --org "your-org-name" +pair-cli package --layout source \ + --org \ + --org-name "Acme Corp" \ + --team "Platform" \ + --compliance "SOC2,ISO27001" \ + --distribution restricted ``` See [CLI Commands](/docs/reference/cli/commands) for the full `package` reference. @@ -188,7 +276,7 @@ pair-cli install --source github --url https://github.com/your-org/your-kb/relea For organizations that restrict external package sources, host the package on an internal registry or file share. Adopters use the `--url` flag to point to the internal location. -See [Install from URL](/docs/guides/install-from-url) for all source options. +See [Install from URL](/docs/customization/install-from-url) for all source options. ## Version and Maintain diff --git a/apps/website/content/docs/customization/team.mdx b/apps/website/content/docs/customization/team.mdx index f7ab52da..795d0929 100644 --- a/apps/website/content/docs/customization/team.mdx +++ b/apps/website/content/docs/customization/team.mdx @@ -27,6 +27,53 @@ The `.pair/` directory has two layers: **Adoption always wins.** When both layers have an opinion on the same topic, the AI reads adoption files first. This means you can override any guideline by recording a decision in the adoption layer. +## Quick Recipe: Override a Guideline + +Need to change a single KB default? Three steps: + +### Step 1: Identify what to change + +Browse the knowledge layer to find the guideline: + +```bash +# List all guideline categories +ls .pair/knowledge/guidelines/ + +# Example: see testing guidelines +ls .pair/knowledge/guidelines/quality-assurance/ +``` + +### Step 2: Record your decision + +Create or update the corresponding adoption file. For example, to change the testing framework, edit `.pair/adoption/tech/tech-stack.md`: + +```markdown +## Testing + +- jest is adopted as the testing framework (jest v29.x). +``` + +Adoption files always override knowledge files — no need to touch `.pair/knowledge/`. + +### Step 3: Verify + +```bash +pair-cli kb validate +pair-cli update-link --dry-run +``` + +### Which file to edit + +| You want to change... | Edit this adoption file | +| ------------------------------------- | --------------------------------------- | +| Testing framework, language, library | `.pair/adoption/tech/tech-stack.md` | +| Architecture pattern | `.pair/adoption/tech/architecture.md` | +| Methodology, quality gates | `.pair/adoption/tech/way-of-working.md` | +| Add a new guideline | Create a new file in `.pair/adoption/` | +| Record a significant decision | Create an ADR in `.pair/adoption/tech/adr/` | + +The rest of this guide covers each of these in depth. + ## Customize Guidelines ### Override the Tech Stack @@ -149,4 +196,4 @@ pair-cli update-link --dry-run - **Want to distribute your customized KB?** [Publish a Knowledge Base](/docs/customization/organization) — package and distribute for other teams. - **Need the directory reference?** [KB Structure](/docs/reference/kb-structure) — every directory and file explained. -- **Quick override guide?** [Customize the Knowledge Base](/docs/guides/customize-kb) — concise override patterns. +- **Config options?** [Configuration Reference](/docs/reference/configuration) — config.json options. diff --git a/apps/website/content/docs/guides/adopter-checklist.mdx b/apps/website/content/docs/getting-started/checklist.mdx similarity index 98% rename from apps/website/content/docs/guides/adopter-checklist.mdx rename to apps/website/content/docs/getting-started/checklist.mdx index b32061ea..e9be4974 100644 --- a/apps/website/content/docs/guides/adopter-checklist.mdx +++ b/apps/website/content/docs/getting-started/checklist.mdx @@ -157,7 +157,7 @@ rm -rf /tmp/pair-test If everything worked: - Start using pair-cli in your actual projects -- Check [CLI Workflows](/docs/guides/cli-workflows) for advanced usage +- Check [CLI Workflows](/docs/reference/cli/workflows) for advanced usage - Join the community at [GitHub Discussions](https://github.com/foomakers/pair/discussions) If issues occurred: diff --git a/apps/website/content/docs/getting-started/meta.json b/apps/website/content/docs/getting-started/meta.json index a0ef0f0e..2d1f91a1 100644 --- a/apps/website/content/docs/getting-started/meta.json +++ b/apps/website/content/docs/getting-started/meta.json @@ -1,4 +1,4 @@ { "title": "Getting Started", - "pages": ["quickstart", "quickstart-solo", "quickstart-team", "quickstart-org"] + "pages": ["quickstart", "quickstart-solo", "quickstart-team", "quickstart-org", "checklist"] } diff --git a/apps/website/content/docs/guides/customize-kb.mdx b/apps/website/content/docs/guides/customize-kb.mdx deleted file mode 100644 index 68d9aaaf..00000000 --- a/apps/website/content/docs/guides/customize-kb.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Customize the Knowledge Base -description: Quick recipe to override a KB guideline using adoption files — identify, override, verify. ---- - -Need to change a single KB default? This is the quick recipe. For the full customization journey — from adopting a KB to publishing your own — see the [Customization](/docs/customization) section. - -## Override a Guideline in 3 Steps - -### Step 1: Identify What to Change - -Browse the knowledge layer to find the guideline: - -```bash -# List all guideline categories -ls .pair/knowledge/guidelines/ - -# Example: see testing guidelines -ls .pair/knowledge/guidelines/quality-assurance/ -``` - -### Step 2: Record Your Decision - -Create or update the corresponding adoption file. For example, to change the testing framework, edit `.pair/adoption/tech/tech-stack.md`: - -```markdown -## Testing - -- jest is adopted as the testing framework (jest v29.x). -``` - -Adoption files always override knowledge files — no need to touch `.pair/knowledge/`. - -### Step 3: Verify - -```bash -pair-cli kb validate -pair-cli update-link --dry-run -``` - -## Quick Reference: Which File to Edit - -| You want to change... | Edit this adoption file | -| --- | --- | -| Testing framework, language, library | `.pair/adoption/tech/tech-stack.md` | -| Architecture pattern | `.pair/adoption/tech/architecture.md` | -| Methodology, quality gates | `.pair/adoption/tech/way-of-working.md` | -| Add a new guideline | Create a new file in `.pair/adoption/` | -| Record a significant decision | Create an ADR in `.pair/adoption/tech/adr/` | - -## What's Safe to Edit - -| Directory | On `pair-cli update` | Safe to edit? | -| --- | --- | --- | -| `.pair/knowledge/` | Overwritten | No — don't edit | -| `.pair/adoption/` | Preserved | Yes — your decisions stay | - -## Going Deeper - -- [Customize for Your Team](/docs/customization/team) — full guide to adapting guidelines, quality gates, and way-of-working -- [Publish a Knowledge Base](/docs/customization/organization) — package and distribute a KB for your organization -- [KB Structure Reference](/docs/reference/kb-structure) — directory layout and file purposes -- [Configuration Reference](/docs/reference/configuration) — config.json options diff --git a/apps/website/content/docs/guides/meta.json b/apps/website/content/docs/guides/meta.json deleted file mode 100644 index d6d9ba18..00000000 --- a/apps/website/content/docs/guides/meta.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "title": "Guides", - "pages": [ - "cli-workflows", - "install-from-url", - "customize-kb", - "adopter-checklist", - "packaging", - "update-link" - ] -} diff --git a/apps/website/content/docs/guides/packaging.mdx b/apps/website/content/docs/guides/packaging.mdx deleted file mode 100644 index 942e143b..00000000 --- a/apps/website/content/docs/guides/packaging.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Packaging a Knowledge Base -description: Package your KB for distribution using source or target layout modes. ---- - -import { Callout } from 'fumadocs-ui/components/callout'; - -## Overview - -The `pair package` command creates a distributable ZIP from your Knowledge Base. It supports two **layout modes** that determine which files are included: - -| Layout | Flag | What it packages | When to use | -|--------|------|-----------------|-------------| -| **Target** (default) | `--layout target` | Files from installed target directories (e.g., `.pair/knowledge/`, `.claude/skills/`) | Packaging an installed KB for redistribution | -| **Source** | `--layout source` | Files from the single source directory defined in `config.json` | Packaging the original KB dataset before installation | - -## Source vs Target Layout - -### Source Layout - -Packages from the **source directory** defined in each registry's `source` field in `config.json`. This is the canonical, pre-installation structure. - -```bash -pair package --layout source -``` - -Use source layout when: -- You maintain a KB dataset and want to distribute the **original files** -- You want a clean package without installation-time transformations (prefix, flatten) -- You're building a CI/CD pipeline that packages from the dataset directly - -**Example:** A KB with `config.json` registry `skills.source: ".skills"` packages from `.skills/category/name/SKILL.md`. - -### Target Layout - -Packages from **installed target directories** — the files as they appear after `pair install`. This is the default. - -```bash -pair package --layout target -``` - -Use target layout when: -- You want to package the KB **as users see it** after installation -- You need to include prefix/flatten transformations applied during install -- You're redistributing a customized KB from a project - -**Example:** The same skills registry with `flatten: true, prefix: "pair"` packages from `.claude/skills/pair-category-name/SKILL.md`. - - - Target layout excludes symlink targets automatically — only canonical (physical copy) targets are included. - - -## Packaging Walkthrough - -### 1. Validate before packaging - -```bash -# Validate with the same layout you'll package -pair kb validate --layout source -pair kb validate --layout target -``` - -### 2. Package - -```bash -# Source layout — original dataset -pair package --layout source -o dist/my-kb-source.zip - -# Target layout — installed structure (default) -pair package --layout target -o dist/my-kb-target.zip - -# With metadata -pair package --layout source \ - --name "My KB" \ - --version 1.0.0 \ - --description "Team knowledge base" -``` - -### 3. Verify the package - -```bash -# Inspect the ZIP contents -unzip -l dist/my-kb-source.zip - -# Validate the packaged KB -pair kb validate dist/my-kb-source.zip -``` - -## Organizational Packaging - -For enterprise distribution, add organizational metadata: - -```bash -pair package --layout source \ - --org \ - --org-name "Acme Corp" \ - --team "Platform" \ - --compliance "SOC2,ISO27001" \ - --distribution restricted -``` - -See [Organization Customization](/docs/customization/organization) for the full enterprise workflow. - -## Layout and config.json - -The layout mode maps to the `source` and `targets` fields in your `config.json` registries: - -```json -{ - "asset_registries": { - "skills": { - "source": ".skills", - "flatten": true, - "prefix": "pair", - "targets": [ - { "path": ".claude/skills", "mode": "canonical" }, - { "path": ".cursor/skills", "mode": "symlink" } - ] - } - } -} -``` - -- `--layout source` reads from `.skills/` -- `--layout target` reads from `.claude/skills/` (canonical target only — `.cursor/skills/` is excluded because it's a symlink) diff --git a/apps/website/content/docs/index.mdx b/apps/website/content/docs/index.mdx index 96d3bd5c..3665b5ae 100644 --- a/apps/website/content/docs/index.mdx +++ b/apps/website/content/docs/index.mdx @@ -5,7 +5,9 @@ description: pair documentation — pick your journey, from first install to pub
-

Welcome to pair

+ + Welcome to pair +
**Code is the easy part.** pair helps development teams collaborate with AI assistants throughout the product lifecycle — a Knowledge Base for persistent context, skills that automate the process, and adoption files that keep decisions from drifting. diff --git a/apps/website/content/docs/meta.json b/apps/website/content/docs/meta.json index 9c93b854..2bb70b50 100644 --- a/apps/website/content/docs/meta.json +++ b/apps/website/content/docs/meta.json @@ -4,16 +4,15 @@ "pages": [ "index", "getting-started", - "developer-journey", + "tutorials", "concepts", + "developer-journey", "customization", "integrations", "pm-tools", "---", - "guides", - "migrations", - "tutorials", "reference", + "migrations", "support", "contributing" ] diff --git a/apps/website/content/docs/reference/cli/commands.mdx b/apps/website/content/docs/reference/cli/commands.mdx index 90535724..a0d4e6c1 100644 --- a/apps/website/content/docs/reference/cli/commands.mdx +++ b/apps/website/content/docs/reference/cli/commands.mdx @@ -166,7 +166,7 @@ pair update-link --url https://github.com/org/repo/releases/download/v1.0/kb.zip pair update-link --url ./relative/path/to/kb ``` -For detailed usage, see the [Link Update Guide](/docs/guides/update-link). +For detailed usage, see the [Link Update Guide](/docs/reference/cli/update-link). --- diff --git a/apps/website/content/docs/reference/cli/examples.mdx b/apps/website/content/docs/reference/cli/examples.mdx index 9ff8d54f..1b94742b 100644 --- a/apps/website/content/docs/reference/cli/examples.mdx +++ b/apps/website/content/docs/reference/cli/examples.mdx @@ -293,5 +293,5 @@ pair install --source https://github.com/org/repo/releases/download/v1.2.0/kb.zi ## Related - [CLI Commands Reference](/docs/reference/cli/commands) — Complete command documentation -- [CLI Workflows Guide](/docs/guides/cli-workflows) — Common workflows +- [CLI Workflows Guide](/docs/reference/cli/workflows) — Common workflows - [KB Source Resolution Spec](/docs/reference/specs/kb-source-resolution) — Technical specification diff --git a/apps/website/content/docs/reference/cli/meta.json b/apps/website/content/docs/reference/cli/meta.json index 39f9b739..e6609b64 100644 --- a/apps/website/content/docs/reference/cli/meta.json +++ b/apps/website/content/docs/reference/cli/meta.json @@ -1,4 +1,4 @@ { "title": "CLI", - "pages": ["commands", "examples"] + "pages": ["commands", "examples", "workflows", "update-link"] } diff --git a/apps/website/content/docs/guides/update-link.mdx b/apps/website/content/docs/reference/cli/update-link.mdx similarity index 100% rename from apps/website/content/docs/guides/update-link.mdx rename to apps/website/content/docs/reference/cli/update-link.mdx diff --git a/apps/website/content/docs/guides/cli-workflows.mdx b/apps/website/content/docs/reference/cli/workflows.mdx similarity index 98% rename from apps/website/content/docs/guides/cli-workflows.mdx rename to apps/website/content/docs/reference/cli/workflows.mdx index b9d74c0a..4a7fe2ca 100644 --- a/apps/website/content/docs/guides/cli-workflows.mdx +++ b/apps/website/content/docs/reference/cli/workflows.mdx @@ -81,7 +81,7 @@ pair-cli update-link - **After project restructure**: `pair-cli update-link` to fix broken links - **Link health check**: `pair-cli update-link --dry-run --log-level debug` to inspect -See the [Link Update Guide](/docs/guides/update-link) for comprehensive documentation. +See the [Link Update Guide](/docs/reference/cli/update-link) for comprehensive documentation. ### 6. Package Distribution and Verification diff --git a/apps/website/content/docs/reference/kb-structure.mdx b/apps/website/content/docs/reference/kb-structure.mdx index 00fbd9ff..06d037b7 100644 --- a/apps/website/content/docs/reference/kb-structure.mdx +++ b/apps/website/content/docs/reference/kb-structure.mdx @@ -119,7 +119,7 @@ Neither subdirectory is scaffolded by `pair install` — a skill creates them on ## Related -- [Customize the Knowledge Base](/docs/guides/customize-kb) — How to override guidelines +- [Customize the Knowledge Base](/docs/customization/team) — How to override guidelines - [Configuration Reference](/docs/reference/configuration) — config.json options - [Skills Catalog](/docs/reference/skills-catalog) — All 35 pair skills - [llms.txt](/docs/concepts/llms-txt) — How pair makes docs and project context consumable by AI assistants diff --git a/apps/website/content/docs/support/general-faq.mdx b/apps/website/content/docs/support/general-faq.mdx index 2e44826f..4d624912 100644 --- a/apps/website/content/docs/support/general-faq.mdx +++ b/apps/website/content/docs/support/general-faq.mdx @@ -135,7 +135,7 @@ Use the [Filesystem](/docs/pm-tools/filesystem) option — stories and tasks are Edit `.pair/adoption/tech/tech-stack.md` and declare your choices. The AI reads adoption files first, so your decisions override the KB defaults. -[Customize for Your Team](/docs/customization/team) · [Quick override recipe](/docs/guides/customize-kb) +[Customize for Your Team](/docs/customization/team) ### How do I customize commit message or PR formats? @@ -167,7 +167,7 @@ Check Node.js version (18+), permissions, and PATH. The Installation FAQ covers Verify your AI assistant can see the `.pair/` directory. Check that AGENTS.md or CLAUDE.md links to `.pair/`. Run `pair-cli kb validate` to verify structure. If using a bridge pattern, ensure the bridge files are in place. -[Agent Integration](/docs/concepts/agent-integration) · [Adopter Checklist](/docs/guides/adopter-checklist) +[Agent Integration](/docs/concepts/agent-integration) · [Adopter Checklist](/docs/getting-started/checklist) ### Where do I report bugs or request features? diff --git a/apps/website/content/docs/tutorials/managing-ai-artifacts.mdx b/apps/website/content/docs/tutorials/managing-ai-artifacts.mdx index 41002385..c25c0ae3 100644 --- a/apps/website/content/docs/tutorials/managing-ai-artifacts.mdx +++ b/apps/website/content/docs/tutorials/managing-ai-artifacts.mdx @@ -262,7 +262,7 @@ pair-cli update --source https://github.com/acme/ai-artifacts.git#v1.1.0 --confi The new locations are populated automatically. Existing assistants' skill directories remain untouched. - You can also host the package as a ZIP on any HTTP server, network share, or artifact registry. See [Install from URL or Path](/docs/guides/install-from-url). + You can also host the package as a ZIP on any HTTP server, network share, or artifact registry. See [Install from URL or Path](/docs/customization/install-from-url). ## What you've learned @@ -277,6 +277,6 @@ The new locations are populated automatically. Existing assistants' skill direct ## Next steps - [Team Setup](/docs/tutorials/team-setup) — Full team onboarding with pair -- [Install from URL or Path](/docs/guides/install-from-url) — Alternative distribution methods +- [Install from URL or Path](/docs/customization/install-from-url) — Alternative distribution methods - [Configuration Reference](/docs/reference/configuration) — Full `config.json` reference - [Skills Concept](/docs/concepts/skills) — How skills work diff --git a/apps/website/content/docs/tutorials/release-testing.mdx b/apps/website/content/docs/tutorials/release-testing.mdx index cd467ce3..c27a8a39 100644 --- a/apps/website/content/docs/tutorials/release-testing.mdx +++ b/apps/website/content/docs/tutorials/release-testing.mdx @@ -201,4 +201,4 @@ The `/pair-process-review` skill can optionally invoke `/pair-capability-execute - Read the [Skills Catalog](/docs/reference/skills-catalog) for the full list of pair skills - Check [Release Process](/docs/contributing/release-process) for how releases are automated -- Explore [CLI Workflows](/docs/guides/cli-workflows) for other CLI commands +- Explore [CLI Workflows](/docs/reference/cli/workflows) for other CLI commands diff --git a/apps/website/e2e/docs.e2e.test.ts b/apps/website/e2e/docs.e2e.test.ts index 73eed10e..b5866064 100644 --- a/apps/website/e2e/docs.e2e.test.ts +++ b/apps/website/e2e/docs.e2e.test.ts @@ -181,22 +181,23 @@ test('smoke: all docs pages return 200 with correct titles', async ({ page }) => // E2E: Docs — Guides, Reference, and Support sections (#124) // ============================================================ -test('guides section: navigate and verify content', async ({ page }) => { - await page.goto('/docs/guides/cli-workflows') +test('moved guides pages: render at their new locations', async ({ page }) => { + await page.goto('/docs/reference/cli/workflows') const main = page.locator('main') - // Guides page renders with expected content + // CLI Workflows now lives under Reference > CLI await expect(page.locator('main h1')).toContainText('CLI Workflows') await expect(main).toContainText('Common Workflows') await expect(main).toContainText('pair-cli install') - // Sidebar shows Guides section - await expect(page.locator('body')).toContainText('Guides') - - // Navigate to another guide via sidebar - await page.locator('a', { hasText: 'Install from URL' }).first().click() - await expect(page).toHaveURL('/docs/guides/install-from-url') + await page.goto('/docs/customization/install-from-url') await expect(page.locator('main h1')).toContainText('Install from URL') + + await page.goto('/docs/getting-started/checklist') + await expect(page.locator('main h1')).toContainText('Adopter Checklist') + + await page.goto('/docs/reference/cli/update-link') + await expect(page.locator('main h1')).toContainText('Link Update') }) test('redirects: old FAQ/troubleshooting URLs land on support/troubleshooting', async ({ @@ -211,6 +212,23 @@ test('redirects: old FAQ/troubleshooting URLs land on support/troubleshooting', await expect(page.locator('main h1')).toContainText('Troubleshooting') }) +test('redirects: dissolved guides URLs land on their new locations', async ({ page }) => { + const redirects = [ + { from: '/docs/guides/adopter-checklist', to: '/docs/getting-started/checklist' }, + { from: '/docs/guides/cli-workflows', to: '/docs/reference/cli/workflows' }, + { from: '/docs/guides/customize-kb', to: '/docs/customization/team' }, + { from: '/docs/guides/install-from-url', to: '/docs/customization/install-from-url' }, + { from: '/docs/guides/packaging', to: '/docs/customization/organization' }, + { from: '/docs/guides/update-link', to: '/docs/reference/cli/update-link' }, + { from: '/docs/guides', to: '/docs' }, + ] + for (const { from, to } of redirects) { + await page.goto(from) + await expect(page, `${from} should redirect to ${to}`).toHaveURL(to) + await expect(page.locator('main h1')).toBeVisible() + } +}) + test('reference section: navigate CLI, specs, and top-level pages', async ({ page }) => { // CLI commands page await page.goto('/docs/reference/cli/commands') @@ -284,11 +302,10 @@ test('support section: navigate and verify content', async ({ page }) => { test('smoke: all guides/reference/support pages return 200', async ({ page }) => { const pages = [ - { url: '/docs/guides/cli-workflows', title: 'CLI Workflows' }, - { url: '/docs/guides/install-from-url', title: 'Install from URL' }, - { url: '/docs/guides/customize-kb', title: 'Customize the Knowledge Base' }, - { url: '/docs/guides/adopter-checklist', title: 'Adopter Checklist' }, - { url: '/docs/guides/update-link', title: 'Link Update' }, + { url: '/docs/reference/cli/workflows', title: 'CLI Workflows' }, + { url: '/docs/customization/install-from-url', title: 'Install from URL' }, + { url: '/docs/getting-started/checklist', title: 'Adopter Checklist' }, + { url: '/docs/reference/cli/update-link', title: 'Link Update' }, { url: '/docs/reference/cli/commands', title: 'CLI Commands' }, { url: '/docs/reference/cli/examples', title: 'CLI Help Examples' }, { url: '/docs/reference/specs/cli-contracts', title: 'CLI Contracts' }, @@ -312,7 +329,7 @@ test('smoke: all guides/reference/support pages return 200', async ({ page }) => test('no broken .md links in guides/reference/support sections', async ({ page }) => { const sections = [ - '/docs/guides/cli-workflows', + '/docs/reference/cli/workflows', '/docs/reference/cli/commands', '/docs/reference/skills-catalog', '/docs/support', @@ -890,11 +907,10 @@ test('no circular prev/next footer links on any docs page', async ({ page }) => '/docs/pm-tools/filesystem', '/docs/pm-tools/github-projects', '/docs/pm-tools/linear', - '/docs/guides/cli-workflows', - '/docs/guides/install-from-url', - '/docs/guides/customize-kb', - '/docs/guides/adopter-checklist', - '/docs/guides/update-link', + '/docs/getting-started/checklist', + '/docs/customization/install-from-url', + '/docs/reference/cli/workflows', + '/docs/reference/cli/update-link', '/docs/reference/cli/commands', '/docs/reference/cli/examples', '/docs/reference/specs/cli-contracts', diff --git a/apps/website/next.config.mjs b/apps/website/next.config.mjs index b4e80f39..50c21117 100644 --- a/apps/website/next.config.mjs +++ b/apps/website/next.config.mjs @@ -18,6 +18,41 @@ const nextConfig = { destination: '/docs/support/troubleshooting', permanent: true, }, + { + source: '/docs/guides/adopter-checklist', + destination: '/docs/getting-started/checklist', + permanent: true, + }, + { + source: '/docs/guides/cli-workflows', + destination: '/docs/reference/cli/workflows', + permanent: true, + }, + { + source: '/docs/guides/customize-kb', + destination: '/docs/customization/team', + permanent: true, + }, + { + source: '/docs/guides/install-from-url', + destination: '/docs/customization/install-from-url', + permanent: true, + }, + { + source: '/docs/guides/packaging', + destination: '/docs/customization/organization', + permanent: true, + }, + { + source: '/docs/guides/update-link', + destination: '/docs/reference/cli/update-link', + permanent: true, + }, + { + source: '/docs/guides', + destination: '/docs', + permanent: true, + }, ] }, } diff --git a/qa/release-validation/CP5-website-docs-completeness.md b/qa/release-validation/CP5-website-docs-completeness.md index 0062236a..e5e34634 100644 --- a/qa/release-validation/CP5-website-docs-completeness.md +++ b/qa/release-validation/CP5-website-docs-completeness.md @@ -16,12 +16,13 @@ 1. For each URL below, issue an HTTP request and check status code -**Getting Started** (5 pages): +**Getting Started** (6 pages): - `$BASE_URL/docs/getting-started` - `$BASE_URL/docs/getting-started/quickstart` - `$BASE_URL/docs/getting-started/quickstart-solo` - `$BASE_URL/docs/getting-started/quickstart-team` - `$BASE_URL/docs/getting-started/quickstart-org` +- `$BASE_URL/docs/getting-started/checklist` **Concepts** (7 pages): - `$BASE_URL/docs/concepts` @@ -39,9 +40,10 @@ - `$BASE_URL/docs/developer-journey/iteration` - `$BASE_URL/docs/developer-journey/execution` -**Customization** (5 pages): +**Customization** (6 pages): - `$BASE_URL/docs/customization` - `$BASE_URL/docs/customization/adopt` +- `$BASE_URL/docs/customization/install-from-url` - `$BASE_URL/docs/customization/team` - `$BASE_URL/docs/customization/templates` - `$BASE_URL/docs/customization/organization` @@ -60,17 +62,12 @@ - `$BASE_URL/docs/pm-tools/filesystem` - `$BASE_URL/docs/pm-tools/linear` -**Guides** (5 pages): -- `$BASE_URL/docs/guides/cli-workflows` -- `$BASE_URL/docs/guides/install-from-url` -- `$BASE_URL/docs/guides/customize-kb` -- `$BASE_URL/docs/guides/adopter-checklist` -- `$BASE_URL/docs/guides/update-link` - -**Reference** (10 pages): +**Reference** (12 pages): - `$BASE_URL/docs/reference` - `$BASE_URL/docs/reference/cli/commands` - `$BASE_URL/docs/reference/cli/examples` +- `$BASE_URL/docs/reference/cli/workflows` +- `$BASE_URL/docs/reference/cli/update-link` - `$BASE_URL/docs/reference/specs/cli-contracts` - `$BASE_URL/docs/reference/specs/kb-source-resolution` - `$BASE_URL/docs/reference/skills-catalog` @@ -101,13 +98,13 @@ ### Expected Result -- All 62 URLs return HTTP 200 +- All 60 URLs return HTTP 200 - Log any non-200 as FAIL with status code ### Notes - Use batch `curl -sI` or WebFetch for efficiency -- Total: 62 pages +- Total: 60 pages --- From a303b60bf53b05b02249340407476fa9713f0311 Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 10:05:23 +0200 Subject: [PATCH 05/14] =?UTF-8?q?[#312]=20fix:=20review=20remediation=20?= =?UTF-8?q?=E2=80=94=20canonical=20CLI=20URL,=20count=20fixes,=20e2e=20smo?= =?UTF-8?q?ke,=20gate=20negative=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../src/commands/update-link/metadata.ts | 2 +- .../content/docs/integrations/claude-code.mdx | 2 +- .../docs/reference/skill-management.mdx | 2 +- apps/website/e2e/docs.e2e.test.ts | 33 +++++++++ apps/website/lib/docs-staleness-check.test.ts | 70 +++++++++++++++++++ scripts/docs-staleness-check.js | 6 +- 6 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 apps/website/lib/docs-staleness-check.test.ts diff --git a/apps/pair-cli/src/commands/update-link/metadata.ts b/apps/pair-cli/src/commands/update-link/metadata.ts index 9eefab5b..2d7290aa 100644 --- a/apps/pair-cli/src/commands/update-link/metadata.ts +++ b/apps/pair-cli/src/commands/update-link/metadata.ts @@ -22,6 +22,6 @@ export const updateLinkMetadata = { 'Creates automatic backup before modifications', 'Skips external URLs and mailto links', 'Processes all markdown files in .pair/ directory', - 'See also: https://pair.foomakers.com/docs/guides/update-link', + 'See also: https://pair.foomakers.com/docs/reference/cli/update-link', ], } as const diff --git a/apps/website/content/docs/integrations/claude-code.mdx b/apps/website/content/docs/integrations/claude-code.mdx index 1b2a2b39..3cb2ada7 100644 --- a/apps/website/content/docs/integrations/claude-code.mdx +++ b/apps/website/content/docs/integrations/claude-code.mdx @@ -25,7 +25,7 @@ your-project/ │ ├── pair-next/ │ ├── pair-process-implement/ │ ├── pair-capability-verify-quality/ -│ └── ... ← 30+ skills +│ └── ... ← 35 skills ├── .pair/ │ ├── knowledge/ ← upstream guidelines (don't edit) │ └── adoption/ ← your project decisions diff --git a/apps/website/content/docs/reference/skill-management.mdx b/apps/website/content/docs/reference/skill-management.mdx index e016efe2..8c349111 100644 --- a/apps/website/content/docs/reference/skill-management.mdx +++ b/apps/website/content/docs/reference/skill-management.mdx @@ -164,7 +164,7 @@ If two different source paths flatten to the same installed name (e.g. `a/b/` an | ------ | ----- | ------- | | `pair-` | 1 | Top-level skills (only `next`) | | `pair-process-` | 9 | Workflow orchestration skills | -| `pair-capability-` | 24 | Atomic operation skills | +| `pair-capability-` | 25 | Atomic operation skills | ### Transform Functions diff --git a/apps/website/e2e/docs.e2e.test.ts b/apps/website/e2e/docs.e2e.test.ts index b5866064..09a4fe6e 100644 --- a/apps/website/e2e/docs.e2e.test.ts +++ b/apps/website/e2e/docs.e2e.test.ts @@ -812,6 +812,39 @@ test('smoke: all tutorials pages return 200 with correct titles', async ({ page } }) +// ============================================================ +// E2E: Docs — Hub + section indexes (#312) +// ============================================================ + +test('smoke: docs hub returns 200 with journey links and top tasks', async ({ page }) => { + const response = await page.goto('/docs') + expect(response?.status(), '/docs should return 200').toBe(200) + await expect(page).toHaveTitle(/Welcome/) + await expect(page.locator('main h1')).toContainText('Welcome') + + const main = page.locator('main') + await expect(main).toContainText('Pick your journey') + await expect(main.locator('a[href="/docs/getting-started"]').first()).toBeVisible() + await expect(main.locator('a[href="/docs/tutorials"]').first()).toBeVisible() + await expect(main.locator('a[href="/docs/reference"]').first()).toBeVisible() + await expect(main).toContainText('Top tasks') +}) + +test('smoke: concepts and reference section indexes return 200 with headings', async ({ + page, +}) => { + const pages = [ + { url: '/docs/concepts', title: 'Concepts' }, + { url: '/docs/reference', title: 'Reference' }, + ] + for (const { url, title } of pages) { + const response = await page.goto(url) + expect(response?.status(), `${url} should return 200`).toBe(200) + await expect(page.locator('main h1')).toContainText(title) + await expect(page).toHaveTitle(new RegExp(title)) + } +}) + // ============================================================ // E2E: Search — Orama client-side search // ============================================================ diff --git a/apps/website/lib/docs-staleness-check.test.ts b/apps/website/lib/docs-staleness-check.test.ts new file mode 100644 index 00000000..33092614 --- /dev/null +++ b/apps/website/lib/docs-staleness-check.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest' +import { spawnSync } from 'node:child_process' +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' + +// Negative test for the docs staleness gate (scripts/docs-staleness-check.js): +// proves the extended "N skills" count check actually FAILS on a stale count, +// not just that it passes on the real tree. + +const SCRIPT = resolve(__dirname, '../../../scripts/docs-staleness-check.js') + +let fixtureRoot: string + +// Fixture tree with exactly 1 skill, so any other count in docs is stale. +function buildFixture(skillCountClaim: string): void { + const docsDir = join(fixtureRoot, 'apps/website/content/docs') + mkdirSync(join(fixtureRoot, 'packages/knowledge-hub/dataset/.skills/capability/verify-quality'), { + recursive: true, + }) + writeFileSync( + join( + fixtureRoot, + 'packages/knowledge-hub/dataset/.skills/capability/verify-quality/SKILL.md', + ), + '# verify-quality\n', + ) + mkdirSync(join(fixtureRoot, 'apps/pair-cli/src/commands'), { recursive: true }) + mkdirSync(join(docsDir, 'reference/cli'), { recursive: true }) + mkdirSync(join(docsDir, 'integrations'), { recursive: true }) + writeFileSync(join(docsDir, 'reference/skills-catalog.mdx'), '| **verify-quality** | row |\n') + writeFileSync(join(docsDir, 'reference/cli/commands.mdx'), 'No commands documented.\n') + writeFileSync( + join(docsDir, 'integrations/claude-code.mdx'), + `The canonical directory contains ${skillCountClaim}.\n`, + ) +} + +function runCheck(): { status: number | null; stdout: string } { + const result = spawnSync(process.execPath, [SCRIPT], { + env: { ...process.env, DOCS_STALENESS_ROOT: fixtureRoot }, + encoding: 'utf-8', + }) + return { status: result.status, stdout: result.stdout } +} + +describe('docs-staleness-check skill count gate', () => { + beforeAll(() => { + fixtureRoot = mkdtempSync(join(tmpdir(), 'docs-staleness-')) + }) + + afterAll(() => { + rmSync(fixtureRoot, { recursive: true, force: true }) + }) + + it('fails (exit 1) when a docs page claims a wrong skill count', () => { + buildFixture('5 skills') + const { status, stdout } = runCheck() + expect(status).toBe(1) + expect(stdout).toContain('Skill count mismatch') + expect(stdout).toContain('integrations/claude-code.mdx') + }) + + it('passes (exit 0) when the claimed skill count matches', () => { + buildFixture('1 skills') + const { status, stdout } = runCheck() + expect(status, stdout).toBe(0) + expect(stdout).toContain('PASS') + }) +}) diff --git a/scripts/docs-staleness-check.js b/scripts/docs-staleness-check.js index 01848d3c..8055bc4e 100644 --- a/scripts/docs-staleness-check.js +++ b/scripts/docs-staleness-check.js @@ -6,7 +6,11 @@ const fs = require('fs') const path = require('path') -const ROOT = path.resolve(__dirname, '..') +// DOCS_STALENESS_ROOT overrides the repo root — used by the negative test +// (apps/website/lib/docs-staleness-check.test.ts) to run against a fixture tree. +const ROOT = process.env.DOCS_STALENESS_ROOT + ? path.resolve(process.env.DOCS_STALENESS_ROOT) + : path.resolve(__dirname, '..') const SKILLS_DIR = path.join(ROOT, 'packages/knowledge-hub/dataset/.skills') const COMMANDS_DIR = path.join(ROOT, 'apps/pair-cli/src/commands') const DOCS_DIR = path.join(ROOT, 'apps/website/content/docs') From fe62307dc10c805ddc22bae2d0ef638277b46da9 Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 19:43:37 +0200 Subject: [PATCH 06/14] chore: relocate IA assessment doc to decision-log apps/website/docs-structure-assessment.md was an analysis/decision artifact sitting inside the published site's app directory (not published content itself, but out of place there). Moved to .pair/adoption/decision-log/ where decision records belong. Updated the two references in the #312 issue body accordingly. Co-Authored-By: Claude Sonnet 5 --- .../2026-07-12-docs-website-ia-restructuring-assessment.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/website/docs-structure-assessment.md => .pair/adoption/decision-log/2026-07-12-docs-website-ia-restructuring-assessment.md (100%) diff --git a/apps/website/docs-structure-assessment.md b/.pair/adoption/decision-log/2026-07-12-docs-website-ia-restructuring-assessment.md similarity index 100% rename from apps/website/docs-structure-assessment.md rename to .pair/adoption/decision-log/2026-07-12-docs-website-ia-restructuring-assessment.md From cc9fcd28a12e0c9328229fb3dbdcacee9b818df3 Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 19:42:09 +0200 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20correct=20migration=20docs=20URL?= =?UTF-8?q?=20=E2=80=94=20was=20pointing=20at=20removed=20guides/=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOCS_MIGRATIONS_BASE built URLs under /docs/guides/migrations, a page that never existed there (migrations live at /docs/migrations, top-level). Found during #312's docs restructuring review — pre-existing, unrelated to that PR's scope, so fixed here. Updated the 4 hardcoded test-expected URLs across migration-url.test.ts, version-check.test.ts, version-check-formatter.test.ts. Co-Authored-By: Claude Sonnet 5 --- apps/pair-cli/src/commands/kb-info/migration-url.test.ts | 6 +++--- apps/pair-cli/src/commands/kb-info/migration-url.ts | 2 +- .../src/commands/kb-info/version-check-formatter.test.ts | 2 +- apps/pair-cli/src/commands/kb-info/version-check.test.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/pair-cli/src/commands/kb-info/migration-url.test.ts b/apps/pair-cli/src/commands/kb-info/migration-url.test.ts index f0fdd8de..97581e7a 100644 --- a/apps/pair-cli/src/commands/kb-info/migration-url.test.ts +++ b/apps/pair-cli/src/commands/kb-info/migration-url.test.ts @@ -4,13 +4,13 @@ import { buildMigrationUrl, migrationsIndexUrl } from './migration-url' describe('buildMigrationUrl', () => { it('builds a docs URL for the version jump', () => { expect(buildMigrationUrl('1.1.0', '1.2.0')).toBe( - 'https://pair.foomakers.com/docs/guides/migrations/v1.1.0-to-v1.2.0', + 'https://pair.foomakers.com/docs/migrations/v1.1.0-to-v1.2.0', ) }) it('strips a leading v from either version', () => { expect(buildMigrationUrl('v1.1.0', 'v1.2.0')).toBe( - 'https://pair.foomakers.com/docs/guides/migrations/v1.1.0-to-v1.2.0', + 'https://pair.foomakers.com/docs/migrations/v1.1.0-to-v1.2.0', ) }) @@ -22,6 +22,6 @@ describe('buildMigrationUrl', () => { describe('migrationsIndexUrl', () => { it('returns the docs migrations index', () => { - expect(migrationsIndexUrl()).toBe('https://pair.foomakers.com/docs/guides/migrations') + expect(migrationsIndexUrl()).toBe('https://pair.foomakers.com/docs/migrations') }) }) diff --git a/apps/pair-cli/src/commands/kb-info/migration-url.ts b/apps/pair-cli/src/commands/kb-info/migration-url.ts index 27ff283f..a13395d2 100644 --- a/apps/pair-cli/src/commands/kb-info/migration-url.ts +++ b/apps/pair-cli/src/commands/kb-info/migration-url.ts @@ -4,7 +4,7 @@ * jump, no migration logic in the CLI) — kept isolated so a docs-structure * change only requires updating this file. */ -const DOCS_MIGRATIONS_BASE = 'https://pair.foomakers.com/docs/guides/migrations' +const DOCS_MIGRATIONS_BASE = 'https://pair.foomakers.com/docs/migrations' function stripLeadingV(version: string): string { return version.replace(/^v/i, '') diff --git a/apps/pair-cli/src/commands/kb-info/version-check-formatter.test.ts b/apps/pair-cli/src/commands/kb-info/version-check-formatter.test.ts index ae8732da..5909c13e 100644 --- a/apps/pair-cli/src/commands/kb-info/version-check-formatter.test.ts +++ b/apps/pair-cli/src/commands/kb-info/version-check-formatter.test.ts @@ -62,7 +62,7 @@ describe('formatVersionCheckHuman', () => { const output = formatVersionCheckHuman(result) expect(output).toContain('Current version unavailable') - expect(output).toContain('https://pair.foomakers.com/docs/guides/migrations') + expect(output).toContain('https://pair.foomakers.com/docs/migrations') }) it('labels non-stable current versions', () => { diff --git a/apps/pair-cli/src/commands/kb-info/version-check.test.ts b/apps/pair-cli/src/commands/kb-info/version-check.test.ts index 53b78257..090a9d67 100644 --- a/apps/pair-cli/src/commands/kb-info/version-check.test.ts +++ b/apps/pair-cli/src/commands/kb-info/version-check.test.ts @@ -29,7 +29,7 @@ describe('compareVersions', () => { expect(result.status).toBe('drift') expect(result.migrationUrl).toBe( - 'https://pair.foomakers.com/docs/guides/migrations/v1.1.0-to-v1.2.0', + 'https://pair.foomakers.com/docs/migrations/v1.1.0-to-v1.2.0', ) }) From d4fba2a9d9d21c06142b89a013c33bc2cda1043a Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 20:04:25 +0200 Subject: [PATCH 08/14] [#312] fix: stale how-to guide counts + gate coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ai-assisted-sdlc said "11 process guides", developer-journey/index "all 11 guides" — actual KB count is 9; normalize phrasing to "how-to guides" - extend docs-staleness-check: "N how-to/process guides" claims now verified against dataset .pair/knowledge/how-to (test-first, negative + positive fixture cases added) Co-Authored-By: Claude Fable 5 --- .../docs/concepts/ai-assisted-sdlc.mdx | 2 +- .../content/docs/developer-journey/index.mdx | 2 +- apps/website/lib/docs-staleness-check.test.ts | 24 ++++++++++++++--- scripts/docs-staleness-check.js | 27 +++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/apps/website/content/docs/concepts/ai-assisted-sdlc.mdx b/apps/website/content/docs/concepts/ai-assisted-sdlc.mdx index bb309072..873496dc 100644 --- a/apps/website/content/docs/concepts/ai-assisted-sdlc.mdx +++ b/apps/website/content/docs/concepts/ai-assisted-sdlc.mdx @@ -13,7 +13,7 @@ pair solves this by providing **structured, persistent context** that AI assista This onboarding takes the form of a [Knowledge Base](/docs/concepts/knowledge-base) — a set of documents installed in your project that cover: -- **How to work** — 11 process guides defining every step from product requirements to code review +- **How to work** — 9 how-to guides defining every step from product requirements to code review - **What standards to follow** — guidelines for architecture, testing, code design, security - **What the team decided** — adoption files recording the chosen tech stack, architecture, and way of working diff --git a/apps/website/content/docs/developer-journey/index.mdx b/apps/website/content/docs/developer-journey/index.mdx index d1eed060..1e251671 100644 --- a/apps/website/content/docs/developer-journey/index.mdx +++ b/apps/website/content/docs/developer-journey/index.mdx @@ -41,7 +41,7 @@ Level 4: Execution (per story) You can follow this flow in two ways: 1. **Process Skills (recommended)** — invoke a slash command and the AI handles the structured process for you. See the [Skills Catalog](/docs/reference/skills-catalog) for all 35 skills. -2. **How-to Guides** — follow the manual step-by-step instructions from the Knowledge Base. See the [Guidelines & How-To Catalog](/docs/reference/guidelines-catalog) for all 11 guides. +2. **How-to Guides** — follow the manual step-by-step instructions from the Knowledge Base. See the [Guidelines & How-To Catalog](/docs/reference/guidelines-catalog) for all 9 how-to guides. Skills compose capability skills internally, enforce adoption compliance, and update your PM tool automatically. The how-to guides give you the same process with full manual control. diff --git a/apps/website/lib/docs-staleness-check.test.ts b/apps/website/lib/docs-staleness-check.test.ts index 33092614..8e77231e 100644 --- a/apps/website/lib/docs-staleness-check.test.ts +++ b/apps/website/lib/docs-staleness-check.test.ts @@ -12,8 +12,8 @@ const SCRIPT = resolve(__dirname, '../../../scripts/docs-staleness-check.js') let fixtureRoot: string -// Fixture tree with exactly 1 skill, so any other count in docs is stale. -function buildFixture(skillCountClaim: string): void { +// Fixture tree with exactly 1 skill and 1 how-to guide, so any other count in docs is stale. +function buildFixture(skillCountClaim: string, extraProse = ''): void { const docsDir = join(fixtureRoot, 'apps/website/content/docs') mkdirSync(join(fixtureRoot, 'packages/knowledge-hub/dataset/.skills/capability/verify-quality'), { recursive: true, @@ -25,6 +25,10 @@ function buildFixture(skillCountClaim: string): void { ), '# verify-quality\n', ) + const howToDir = join(fixtureRoot, 'packages/knowledge-hub/dataset/.pair/knowledge/how-to') + mkdirSync(howToDir, { recursive: true }) + writeFileSync(join(howToDir, '01-how-to-create-PRD.md'), '# how-to\n') + writeFileSync(join(howToDir, 'README.md'), '# index\n') mkdirSync(join(fixtureRoot, 'apps/pair-cli/src/commands'), { recursive: true }) mkdirSync(join(docsDir, 'reference/cli'), { recursive: true }) mkdirSync(join(docsDir, 'integrations'), { recursive: true }) @@ -32,7 +36,7 @@ function buildFixture(skillCountClaim: string): void { writeFileSync(join(docsDir, 'reference/cli/commands.mdx'), 'No commands documented.\n') writeFileSync( join(docsDir, 'integrations/claude-code.mdx'), - `The canonical directory contains ${skillCountClaim}.\n`, + `The canonical directory contains ${skillCountClaim}.\n${extraProse}`, ) } @@ -67,4 +71,18 @@ describe('docs-staleness-check skill count gate', () => { expect(status, stdout).toBe(0) expect(stdout).toContain('PASS') }) + + it('fails (exit 1) when a docs page claims a wrong how-to guide count', () => { + buildFixture('1 skills', 'The KB ships 11 how-to guides.\n') + const { status, stdout } = runCheck() + expect(status).toBe(1) + expect(stdout).toContain('How-to guide count mismatch') + expect(stdout).toContain('integrations/claude-code.mdx') + }) + + it('passes (exit 0) when the claimed how-to guide count matches', () => { + buildFixture('1 skills', 'The KB ships 1 how-to guides.\n') + const { status, stdout } = runCheck() + expect(status, stdout).toBe(0) + }) }) diff --git a/scripts/docs-staleness-check.js b/scripts/docs-staleness-check.js index 8055bc4e..b299ab7f 100644 --- a/scripts/docs-staleness-check.js +++ b/scripts/docs-staleness-check.js @@ -88,6 +88,33 @@ for (const docSkill of catalogSkills) { } } +// --- Check 2b: How-to guide counts --- +// Same protection as Check 1, for "N how-to guides" / "N process guides" +// claims (the manual-flow counterpart of skills). Guide files live in the KB +// dataset as NN-how-to-*.md; README is the index, not a guide. + +const HOW_TO_DIR = path.join(ROOT, 'packages/knowledge-hub/dataset/.pair/knowledge/how-to') + +if (fs.existsSync(HOW_TO_DIR)) { + const howToCount = fs + .readdirSync(HOW_TO_DIR) + .filter((f) => /^\d+-how-to-.*\.md$/.test(f)).length + + const GUIDE_COUNT_RE = /(\d+)\s+(?:how-to|process)\s+guides/g + for (const file of docsFiles) { + const content = fs.readFileSync(file, 'utf-8') + const rel = path.relative(DOCS_DIR, file) + for (const m of content.matchAll(GUIDE_COUNT_RE)) { + const docCount = parseInt(m[1], 10) + if (docCount !== howToCount) { + errors.push( + `How-to guide count mismatch in ${rel}: docs say "${m[0]}", actual count is ${howToCount}`, + ) + } + } + } +} + // --- Check 3: CLI commands --- const commandDirs = fs From 0541e11d7348a124c813ff6e83fdb34c1a475498 Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 20:04:41 +0200 Subject: [PATCH 09/14] =?UTF-8?q?[#312]=20docs:=20IA=20polish=20=E2=80=94?= =?UTF-8?q?=20reading=20order,=20orphan=20link,=20migrations=20task?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - concepts/index: replace wall-of-text with recommended reading order (4 core concepts) + deeper-mechanics list + forward link to lifecycle - customization/index: surface install-from-url (moved here by T3 but invisible from the section index) - docs hub: add Migrations row to top tasks (only section unreachable from the hub) Co-Authored-By: Claude Fable 5 --- apps/website/content/docs/concepts/index.mdx | 19 ++++++++++++++++++- .../content/docs/customization/index.mdx | 3 +++ apps/website/content/docs/index.mdx | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/website/content/docs/concepts/index.mdx b/apps/website/content/docs/concepts/index.mdx index 0e792f23..5e10220b 100644 --- a/apps/website/content/docs/concepts/index.mdx +++ b/apps/website/content/docs/concepts/index.mdx @@ -5,4 +5,21 @@ description: How pair works under the hood — the Knowledge Base, skills, adopt These pages explain **how pair works** — read them when you want understanding, not step-by-step instructions. -Start with the [AI-Assisted SDLC](/docs/concepts/ai-assisted-sdlc): the problem pair solves and how structured context changes AI collaboration. The [Knowledge Base](/docs/concepts/knowledge-base) is the core artifact — the documents installed into `.pair/` that give your AI assistant persistent context. [Skills](/docs/concepts/skills) are the actions layered on top: structured instructions your assistant discovers and executes. [Adoption Files](/docs/concepts/adoption-files) record your project-specific decisions so they never drift. [Canonical States](/docs/concepts/canonical-states) is how skills reason about work-item state across any board vocabulary. [Agent Integration](/docs/concepts/agent-integration) explains the bridge pattern that connects any AI coding tool to the single source of truth. Finally, [llms.txt](/docs/concepts/llms-txt) covers the standard pair uses to make documentation and project context natively consumable by AI assistants. +## Recommended reading order + +The core pages build on each other — if you're new to pair, read them top to bottom: + +1. **[AI-Assisted SDLC](/docs/concepts/ai-assisted-sdlc)** — Start here: the problem pair solves and how structured context changes AI collaboration end-to-end. +2. **[Knowledge Base](/docs/concepts/knowledge-base)** — The core artifact: the documents installed into `.pair/` that give your AI assistant persistent context. +3. **[Skills](/docs/concepts/skills)** — The actions layered on top: structured instructions your assistant discovers and executes. +4. **[Adoption Files](/docs/concepts/adoption-files)** — Your project-specific decisions, recorded so they never drift. + +## Deeper mechanics + +Read these when the topic comes up — they explain specific mechanisms rather than the big picture: + +- **[Canonical States](/docs/concepts/canonical-states)** — How skills reason about work-item state across any board vocabulary. +- **[Agent Integration](/docs/concepts/agent-integration)** — The bridge pattern that connects any AI coding tool to the single source of truth. +- **[llms.txt](/docs/concepts/llms-txt)** — The standard pair uses to make documentation and project context natively consumable by AI assistants. + +Ready to apply the concepts? The [Process Lifecycle](/docs/developer-journey) shows the nine-step workflow they enable, and the [tutorials](/docs/tutorials) walk it through hands-on. diff --git a/apps/website/content/docs/customization/index.mdx b/apps/website/content/docs/customization/index.mdx index 9f386ad5..8c3e8c3d 100644 --- a/apps/website/content/docs/customization/index.mdx +++ b/apps/website/content/docs/customization/index.mdx @@ -13,6 +13,8 @@ Install a published Knowledge Base and start using it as-is. This is the fastest [Adopt a Knowledge Base](/docs/customization/adopt) +Installing from a custom mirror, a local path, or an offline environment? See [Install from URL or Path](/docs/customization/install-from-url). + ## Customize Fork and adjust an adopted KB's guidelines, quality gates, and way-of-working for your team. Adoption files override KB defaults, so your customizations survive upstream updates. @@ -42,6 +44,7 @@ Override the built-in templates that control commit messages, PR descriptions, u | Stage | You want to... | Start here | | --- | --- | --- | | Adopt | Use someone else's KB | [Adopt](/docs/customization/adopt) | +| Adopt | Install from a mirror, local path, or offline | [Install from URL or Path](/docs/customization/install-from-url) | | Customize | Adjust a KB for your team | [Customize](/docs/customization/team) | | Templates | Change commit, PR, or story formats | [Templates](/docs/customization/templates) | | Publish | Create and distribute your own KB | [Publish](/docs/customization/organization) | diff --git a/apps/website/content/docs/index.mdx b/apps/website/content/docs/index.mdx index 3665b5ae..27e15ecd 100644 --- a/apps/website/content/docs/index.mdx +++ b/apps/website/content/docs/index.mdx @@ -28,5 +28,6 @@ description: pair documentation — pick your journey, from first install to pub | Follow the development lifecycle | [Process Lifecycle](/docs/developer-journey) | | Connect my AI coding assistant | [Integrations](/docs/integrations) | | Connect my project management tool | [PM Tools](/docs/pm-tools) | +| Upgrade `.pair/` after a pair release | [Migrations](/docs/migrations) | | Fix an install or setup problem | [Support](/docs/support) | | Contribute to pair | [Contributing](/docs/contributing) | From a9d47644c67e5ffcd957aed48d636426f7bf094e Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 20:06:17 +0200 Subject: [PATCH 10/14] [#312] style: prettier formatting from pre-push hook Co-Authored-By: Claude Fable 5 --- apps/website/e2e/docs.e2e.test.ts | 4 +--- apps/website/lib/docs-staleness-check.test.ts | 5 +---- apps/website/package.json | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/website/e2e/docs.e2e.test.ts b/apps/website/e2e/docs.e2e.test.ts index 09a4fe6e..a1cbd84b 100644 --- a/apps/website/e2e/docs.e2e.test.ts +++ b/apps/website/e2e/docs.e2e.test.ts @@ -830,9 +830,7 @@ test('smoke: docs hub returns 200 with journey links and top tasks', async ({ pa await expect(main).toContainText('Top tasks') }) -test('smoke: concepts and reference section indexes return 200 with headings', async ({ - page, -}) => { +test('smoke: concepts and reference section indexes return 200 with headings', async ({ page }) => { const pages = [ { url: '/docs/concepts', title: 'Concepts' }, { url: '/docs/reference', title: 'Reference' }, diff --git a/apps/website/lib/docs-staleness-check.test.ts b/apps/website/lib/docs-staleness-check.test.ts index 8e77231e..58f4ed70 100644 --- a/apps/website/lib/docs-staleness-check.test.ts +++ b/apps/website/lib/docs-staleness-check.test.ts @@ -19,10 +19,7 @@ function buildFixture(skillCountClaim: string, extraProse = ''): void { recursive: true, }) writeFileSync( - join( - fixtureRoot, - 'packages/knowledge-hub/dataset/.skills/capability/verify-quality/SKILL.md', - ), + join(fixtureRoot, 'packages/knowledge-hub/dataset/.skills/capability/verify-quality/SKILL.md'), '# verify-quality\n', ) const howToDir = join(fixtureRoot, 'packages/knowledge-hub/dataset/.pair/knowledge/how-to') diff --git a/apps/website/package.json b/apps/website/package.json index fbb4e0b3..35f3f0c9 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -12,7 +12,7 @@ "ct": "playwright test -c playwright-ct.config.ts --grep-invert @a11y-report", "e2e": "pnpm clean:server && playwright test -c playwright.e2e.config.ts", "a11y:report": "playwright test -c playwright-ct.config.ts --grep @a11y-report --reporter=list", - "a11y:report:html": "playwright test -c playwright-ct.config.ts --grep @a11y-report --reporter=html && echo '\n\u2192 Report: playwright-report/index.html\n Open: npx playwright show-report'", + "a11y:report:html": "playwright test -c playwright-ct.config.ts --grep @a11y-report --reporter=html && echo '\n→ Report: playwright-report/index.html\n Open: npx playwright show-report'", "test": "vitest run && pnpm ct", "test:coverage": "vitest run --coverage", "postinstall": "fumadocs-mdx", From 0a302bff5eac86632afec3b769f9ccc4db54e95f Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Sun, 12 Jul 2026 20:24:09 +0200 Subject: [PATCH 11/14] =?UTF-8?q?[#312]=20fix:=20re-review=20findings=20?= =?UTF-8?q?=E2=80=94=20gate=20regex=20adjectives=20+=20loud=20missing-dir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GUIDE_COUNT_RE now covers adjective phrasings (sequential, step-by-step) with optional modifier — mirrors Check 1's shape; caught 2 more live stale counts on the real tree (knowledge-base.mdx, quickstart.mdx: "11 step-by-step ... guides" vs actual 9) — fixed - guidelines-catalog description normalized "(9 sequential steps)" → "9 sequential how-to guides" so every guide-count claim is gate-covered - missing how-to dataset dir now pushes an error instead of silently skipping the check (consistent with Check 1) - tests: +2 (adjective-phrasing negative, missing-dir failure) Refs: PR #315 re-review findings 1-2 Co-Authored-By: Claude Fable 5 --- .../content/docs/concepts/knowledge-base.mdx | 2 +- .../docs/getting-started/quickstart.mdx | 2 +- .../docs/reference/guidelines-catalog.mdx | 2 +- apps/website/lib/docs-staleness-check.test.ts | 28 ++++++++++++++++--- scripts/docs-staleness-check.js | 11 ++++++-- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/apps/website/content/docs/concepts/knowledge-base.mdx b/apps/website/content/docs/concepts/knowledge-base.mdx index 000bf88f..64e96f7c 100644 --- a/apps/website/content/docs/concepts/knowledge-base.mdx +++ b/apps/website/content/docs/concepts/knowledge-base.mdx @@ -16,7 +16,7 @@ When you run `pair-cli install`, the CLI creates a `.pair/` folder with two main knowledge/ # Reference material (the "how to") way-of-working.md # Development process definition getting-started.md # Onboarding guide - how-to/ # 11 step-by-step process guides + how-to/ # 9 step-by-step process guides guidelines/ # Technical standards architecture/ # Architecture patterns code-design/ # Code design principles diff --git a/apps/website/content/docs/getting-started/quickstart.mdx b/apps/website/content/docs/getting-started/quickstart.mdx index 62319d43..b8a28f44 100644 --- a/apps/website/content/docs/getting-started/quickstart.mdx +++ b/apps/website/content/docs/getting-started/quickstart.mdx @@ -62,7 +62,7 @@ Your project now has: ```text .pair/ knowledge/ # Reference material: process guides, standards - how-to/ # 11 step-by-step guides (PRD → code review) + how-to/ # 9 step-by-step guides (PRD → code review) guidelines/ # Technical standards (architecture, testing, code design) assets/ # Templates (PRD, checklist, commit, PR) adoption/ # Your decisions (filled in as you make them) diff --git a/apps/website/content/docs/reference/guidelines-catalog.mdx b/apps/website/content/docs/reference/guidelines-catalog.mdx index 9986e9d0..24324346 100644 --- a/apps/website/content/docs/reference/guidelines-catalog.mdx +++ b/apps/website/content/docs/reference/guidelines-catalog.mdx @@ -1,6 +1,6 @@ --- title: Guidelines & How-To Catalog -description: Complete catalog of all pair Knowledge Base guidelines (9 categories) and how-to guides (9 sequential steps). +description: Complete catalog of all pair Knowledge Base guidelines (9 categories) and the 9 sequential how-to guides. --- The pair Knowledge Base ships with opinionated guidelines organized into 9 categories and 9 sequential how-to guides covering the full AI-assisted development lifecycle. Guidelines live in `.pair/knowledge/guidelines/`, how-to guides in `.pair/knowledge/how-to/`. diff --git a/apps/website/lib/docs-staleness-check.test.ts b/apps/website/lib/docs-staleness-check.test.ts index 58f4ed70..75c20a7b 100644 --- a/apps/website/lib/docs-staleness-check.test.ts +++ b/apps/website/lib/docs-staleness-check.test.ts @@ -13,7 +13,7 @@ const SCRIPT = resolve(__dirname, '../../../scripts/docs-staleness-check.js') let fixtureRoot: string // Fixture tree with exactly 1 skill and 1 how-to guide, so any other count in docs is stale. -function buildFixture(skillCountClaim: string, extraProse = ''): void { +function buildFixture(skillCountClaim: string, extraProse = '', withHowToDir = true): void { const docsDir = join(fixtureRoot, 'apps/website/content/docs') mkdirSync(join(fixtureRoot, 'packages/knowledge-hub/dataset/.skills/capability/verify-quality'), { recursive: true, @@ -23,9 +23,12 @@ function buildFixture(skillCountClaim: string, extraProse = ''): void { '# verify-quality\n', ) const howToDir = join(fixtureRoot, 'packages/knowledge-hub/dataset/.pair/knowledge/how-to') - mkdirSync(howToDir, { recursive: true }) - writeFileSync(join(howToDir, '01-how-to-create-PRD.md'), '# how-to\n') - writeFileSync(join(howToDir, 'README.md'), '# index\n') + rmSync(howToDir, { recursive: true, force: true }) + if (withHowToDir) { + mkdirSync(howToDir, { recursive: true }) + writeFileSync(join(howToDir, '01-how-to-create-PRD.md'), '# how-to\n') + writeFileSync(join(howToDir, 'README.md'), '# index\n') + } mkdirSync(join(fixtureRoot, 'apps/pair-cli/src/commands'), { recursive: true }) mkdirSync(join(docsDir, 'reference/cli'), { recursive: true }) mkdirSync(join(docsDir, 'integrations'), { recursive: true }) @@ -82,4 +85,21 @@ describe('docs-staleness-check skill count gate', () => { const { status, stdout } = runCheck() expect(status, stdout).toBe(0) }) + + it('fails (exit 1) on wrong counts in adjective phrasings ("N sequential guides")', () => { + buildFixture('1 skills', 'The KB has 9 sequential guides and 9 step-by-step guides.\n') + const { status, stdout } = runCheck() + expect(status).toBe(1) + expect(stdout).toContain('How-to guide count mismatch') + expect(stdout).toContain('9 sequential guides') + expect(stdout).toContain('9 step-by-step guides') + }) + + it('fails (exit 1) when the how-to dataset dir is missing (no silent skip)', () => { + buildFixture('1 skills', '', false) + const { status, stdout } = runCheck() + expect(status).toBe(1) + expect(stdout).toContain('how-to') + expect(stdout).toContain('not found') + }) }) diff --git a/scripts/docs-staleness-check.js b/scripts/docs-staleness-check.js index b299ab7f..b2506f88 100644 --- a/scripts/docs-staleness-check.js +++ b/scripts/docs-staleness-check.js @@ -95,12 +95,19 @@ for (const docSkill of catalogSkills) { const HOW_TO_DIR = path.join(ROOT, 'packages/knowledge-hub/dataset/.pair/knowledge/how-to') -if (fs.existsSync(HOW_TO_DIR)) { +if (!fs.existsSync(HOW_TO_DIR)) { + // Loud failure (consistent with Check 1): a moved dataset dir must not + // silently disable the guide-count check. + errors.push(`How-to guides dir not found: ${HOW_TO_DIR} — guide-count check cannot run`) +} else { const howToCount = fs .readdirSync(HOW_TO_DIR) .filter((f) => /^\d+-how-to-.*\.md$/.test(f)).length - const GUIDE_COUNT_RE = /(\d+)\s+(?:how-to|process)\s+guides/g + // Optional adjective + optional modifier, mirroring Check 1's shape: + // "9 guides", "9 how-to guides", "9 sequential guides", + // "9 sequential how-to guides", "9 step-by-step guides", "9 process guides". + const GUIDE_COUNT_RE = /(\d+)\s+(?:sequential\s+|step-by-step\s+)?(?:how-to\s+|process\s+)?guides/g for (const file of docsFiles) { const content = fs.readFileSync(file, 'utf-8') const rel = path.relative(DOCS_DIR, file) From 2c25c96cebc303e450cbc60f19d9d2a2f51db9ac Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Mon, 13 Jul 2026 20:02:23 +0200 Subject: [PATCH 12/14] [#312] fix: harden staleness gate regexes + JSX href dead-link scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canonical review found the gate could not catch the very phrasings it was meant to guard: - skill regex now matches "N+ skills" (trailing +) and the agent/idempotent adjectives, not just pair/composable — "30+ skills" and "N agent skills" previously slipped through silently - guide regex tightened: requires a how-to qualifier (adjective OR how-to/process word), so arbitrary "N guides" prose no longer false-positives in CI (was over-broadened in prior remediation) - Check 5 dead-link scan extended to JSX `href="/docs/..."` card attributes (Fumadocs ), not just markdown `](/docs/...)` - README.md documented as intentionally out of docs-gate scope (literal counts owned by PR #325) tests: +6 (N+/agent/composable skill mismatches, bare "N guides" non-false-positive, dead + valid JSX href). All test-first (red→green). Refs: PR #315 canonical review Co-Authored-By: Claude Fable 5 --- apps/website/lib/docs-staleness-check.test.ts | 51 +++++++++++++++++++ scripts/docs-staleness-check.js | 43 ++++++++++------ 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/apps/website/lib/docs-staleness-check.test.ts b/apps/website/lib/docs-staleness-check.test.ts index 75c20a7b..83dd7ad0 100644 --- a/apps/website/lib/docs-staleness-check.test.ts +++ b/apps/website/lib/docs-staleness-check.test.ts @@ -72,6 +72,35 @@ describe('docs-staleness-check skill count gate', () => { expect(stdout).toContain('PASS') }) + it('fails (exit 1) on wrong "N composable skills" (adjective between number and skills)', () => { + buildFixture('7 composable skills') + const { status, stdout } = runCheck() + expect(status).toBe(1) + expect(stdout).toContain('Skill count mismatch') + expect(stdout).toContain('7 composable skills') + }) + + it('fails (exit 1) on wrong "N+ skills" (trailing plus)', () => { + buildFixture('7+ skills') + const { status, stdout } = runCheck() + expect(status).toBe(1) + expect(stdout).toContain('Skill count mismatch') + }) + + it('fails (exit 1) on wrong "N agent skills"', () => { + buildFixture('1 skills', 'The standard exposes 7 agent skills.\n') + const { status, stdout } = runCheck() + expect(status).toBe(1) + expect(stdout).toContain('Skill count mismatch') + expect(stdout).toContain('7 agent skills') + }) + + it('passes (exit 0) on matching "N composable skills" / "N+ skills"', () => { + buildFixture('1 skills', 'Exactly 1 composable skills and 1+ skills.\n') + const { status, stdout } = runCheck() + expect(status, stdout).toBe(0) + }) + it('fails (exit 1) when a docs page claims a wrong how-to guide count', () => { buildFixture('1 skills', 'The KB ships 11 how-to guides.\n') const { status, stdout } = runCheck() @@ -102,4 +131,26 @@ describe('docs-staleness-check skill count gate', () => { expect(stdout).toContain('how-to') expect(stdout).toContain('not found') }) + + it('does NOT false-positive on bare "N guides" prose (non-how-to phrasing)', () => { + // Tightened GUIDE_COUNT_RE must ignore arbitrary "N guides" that lack a + // how-to/process/sequential/step-by-step qualifier — even when N is "wrong". + buildFixture('1 skills', 'The city has 5 guides at the museum and 3 tour guides.\n') + const { status, stdout } = runCheck() + expect(status, stdout).toBe(0) + }) + + it('fails (exit 1) on a dead JSX href="/docs/..." card link', () => { + buildFixture('1 skills', 'Broken\n') + const { status, stdout } = runCheck() + expect(status).toBe(1) + expect(stdout).toContain('Dead internal link') + expect(stdout).toContain('/docs/does-not-exist') + }) + + it('passes (exit 0) on a valid JSX href="/docs/..." card link', () => { + buildFixture('1 skills', 'OK\n') + const { status, stdout } = runCheck() + expect(status, stdout).toBe(0) + }) }) diff --git a/scripts/docs-staleness-check.js b/scripts/docs-staleness-check.js index b2506f88..7361800d 100644 --- a/scripts/docs-staleness-check.js +++ b/scripts/docs-staleness-check.js @@ -57,11 +57,12 @@ const skillCount = allSkills.length const catalog = fs.readFileSync(CATALOG_FILE, 'utf-8') // Check 1: every occurrence of "N skills" across ALL docs pages matches actual count. -// Regex is intentionally narrow to avoid prose false positives: it matches -// "N skills", "N pair skills", "N composable skills" (the total-count phrasings -// used in docs). Subset counts ("9 process skills") do NOT match because the -// word between the number and "skills" must be pair/composable or absent. -const COUNT_RE = /(\d+)\s+(?:pair\s+|composable\s+)?skills/g +// Regex stays narrow to avoid prose false positives but covers the total-count +// phrasings docs actually use: "N skills", "N+ skills" (trailing plus), and an +// optional total-count adjective — "N pair/composable/agent/idempotent skills". +// Subset counts ("9 process skills") still do NOT match: the adjective, when +// present, must be one of the whitelisted total-count words. +const COUNT_RE = /(\d+)\+?\s+(?:pair\s+|composable\s+|agent\s+|idempotent\s+)?skills/g for (const file of docsFiles) { const content = fs.readFileSync(file, 'utf-8') const rel = path.relative(DOCS_DIR, file) @@ -104,10 +105,13 @@ if (!fs.existsSync(HOW_TO_DIR)) { .readdirSync(HOW_TO_DIR) .filter((f) => /^\d+-how-to-.*\.md$/.test(f)).length - // Optional adjective + optional modifier, mirroring Check 1's shape: - // "9 guides", "9 how-to guides", "9 sequential guides", - // "9 sequential how-to guides", "9 step-by-step guides", "9 process guides". - const GUIDE_COUNT_RE = /(\d+)\s+(?:sequential\s+|step-by-step\s+)?(?:how-to\s+|process\s+)?guides/g + // Requires a how-to qualifier so arbitrary "N guides" prose ("5 guides at the + // museum") never false-positives. A match needs EITHER a recognized adjective + // (sequential/step-by-step) OR a how-to/process word — bare "N guides" does not + // match. Covers: "9 how-to guides", "9 process guides", "9 sequential guides", + // "9 step-by-step guides", "9 sequential how-to guides", "9 step-by-step process guides". + const GUIDE_COUNT_RE = + /(\d+)\s+(?:(?:sequential|step-by-step)\s+(?:how-to\s+|process\s+)?|(?:how-to|process)\s+)guides/g for (const file of docsFiles) { const content = fs.readFileSync(file, 'utf-8') const rel = path.relative(DOCS_DIR, file) @@ -172,8 +176,10 @@ if (fs.existsSync(TUTORIALS_DIR)) { } // --- Check 5: Dead internal docs links --- -// Every markdown link target starting with /docs must resolve to a page in the -// content tree (file route, or folder route backed by an index.mdx). Would have +// Every /docs target must resolve to a page in the content tree (file route, or +// folder route backed by an index.mdx). Scans BOTH markdown links `](/docs/...)` +// and JSX card `href="/docs/..."` attributes (Fumadocs /) — the +// latter closes a coverage gap for the card-based section indexes. Would have // caught the index-less section links (/docs/concepts, /docs/guides, /docs/reference). const validRoutes = new Set() @@ -184,17 +190,24 @@ for (const file of docsFiles) { } const LINK_RE = /\]\((\/docs[^)\s]*)\)/g +const HREF_RE = /href="(\/docs[^"]*)"/g for (const file of docsFiles) { const content = fs.readFileSync(file, 'utf-8') const rel = path.relative(DOCS_DIR, file) - for (const m of content.matchAll(LINK_RE)) { - const target = m[1].split('#')[0].split('?')[0].replace(/\/$/, '') || '/docs' - if (!validRoutes.has(target)) { - errors.push(`Dead internal link in ${rel}: ${m[1]} does not resolve to a docs page`) + for (const re of [LINK_RE, HREF_RE]) { + for (const m of content.matchAll(re)) { + const target = m[1].split('#')[0].split('?')[0].replace(/\/$/, '') || '/docs' + if (!validRoutes.has(target)) { + errors.push(`Dead internal link in ${rel}: ${m[1]} does not resolve to a docs page`) + } } } } +// NOTE: repo-root README.md is intentionally OUT of this gate's scope — the gate +// governs the published docs site (apps/website/content/docs) only. README's own +// literal skill/guide counts are tracked and fixed by PR #325. + // --- Output --- console.log('Docs Staleness Check') From bc83c372b745b62dc1439e06cdc6e1876cc9816b Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Mon, 13 Jul 2026 20:57:38 +0200 Subject: [PATCH 13/14] [#312] refactor: docs-staleness logic into tested website module, drop script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner rule: tested functionality lives in production code and is tested there — do NOT unit-test scripts. The gate violated this (logic in scripts/docs-staleness-check.js, test spawned it black-box). - port ALL logic verbatim (incl. #315 hardening: tightened guide regex, widened skill regex with +/adjectives, JSX href dead-link scan, README out-of-scope note) into apps/website/lib/docs-staleness-check.ts — ESM module, functions exported individually, import.meta main-guard CLI wrapper, DOCS_STALENESS_ROOT retained, root resolved up 3 - rewrite the test as WHITE-BOX vitest unit tests importing the exported functions directly; ZERO spawnSync/exec — real-tree parity asserted in-process via runAllChecks(). red->green coverage preserved as per-function negatives (wrong N+/composable/agent skills, bare "N guides" non-false-positive, dead/valid JSX href, missing how-to dir) - wire: tsx (catalog 4.23.1) + website script docs:staleness=tsx; root docs:staleness delegates to @pair/website, unchanged in quality-gate + ci.yml (now delegates) - delete scripts/docs-staleness-check.js (single source of truth); update decision-log path refs Refs: PR #315 architectural rule (no test-on-script) Co-Authored-By: Claude Fable 5 --- ...ocs-website-ia-restructuring-assessment.md | 6 +- apps/website/lib/docs-staleness-check.test.ts | 317 +++++++------- apps/website/lib/docs-staleness-check.ts | 287 +++++++++++++ apps/website/package.json | 4 +- package.json | 2 +- pnpm-lock.yaml | 392 +++++++++++++++--- pnpm-workspace.yaml | 1 + scripts/docs-staleness-check.js | 226 ---------- 8 files changed, 804 insertions(+), 431 deletions(-) create mode 100644 apps/website/lib/docs-staleness-check.ts delete mode 100644 scripts/docs-staleness-check.js diff --git a/.pair/adoption/decision-log/2026-07-12-docs-website-ia-restructuring-assessment.md b/.pair/adoption/decision-log/2026-07-12-docs-website-ia-restructuring-assessment.md index d18fcabd..1f0c6031 100644 --- a/.pair/adoption/decision-log/2026-07-12-docs-website-ia-restructuring-assessment.md +++ b/.pair/adoption/decision-log/2026-07-12-docs-website-ia-restructuring-assessment.md @@ -35,7 +35,7 @@ Total: **69 pages**. Root `meta.json` orders 13 sidebar entries in two groups (l Entry points: the marketing landing page links to `/docs` (root Welcome), `/docs/customization/team`, `/docs/customization/organization` (`app/(landing)/`). `llms.txt` / `llms-full.txt` are generated from the source loader (`lib/get-llm-text.ts`) so they follow the tree automatically. -Staleness gate (`scripts/docs-staleness-check.js`) covers exactly: skill count + skill rows in `reference/skills-catalog.mdx`, command anchors in `reference/cli/commands.mdx`, and `pair-cli ` references in `tutorials/*.mdx`. Nothing else. +Staleness gate (`apps/website/lib/docs-staleness-check.ts`) covers exactly: skill count + skill rows in `reference/skills-catalog.mdx`, command anchors in `reference/cli/commands.mdx`, and `pair-cli ` references in `tutorials/*.mdx`. Nothing else. ### Audience-journey read @@ -53,7 +53,7 @@ Staleness gate (`scripts/docs-staleness-check.js`) covers exactly: skill count + | F2 | Major | Findability | **8 broken internal links to index-less section URLs.** `concepts/`, `guides/`, `reference/` have no `index.mdx`, so their URLs 404 under Fumadocs, yet pages link to them: `/docs/concepts` ← `tutorials/index.mdx`, `tutorials/first-project.mdx`, `tutorials/existing-project.mdx`; `/docs/guides` ← `tutorials/index.mdx`, `tutorials/first-project.mdx`, `tutorials/team-setup.mdx`, `tutorials/enterprise-adoption.mdx`; `/docs/reference` ← `tutorials/index.mdx`. | | F3 | Major | Consistency / Audience fit | **`guides/` is a grab-bag.** Seven unrelated pages spanning three audiences: adopter recipes (`customize-kb`, `adopter-checklist`), KB-publisher ops (`packaging`, `install-from-url`, `update-link`), CLI usage (`cli-workflows`), and support content (`troubleshooting`). No index page, no shared theme, no principled rule for what lands here vs. `customization/` or `reference/`. | | F4 | Major | Redundancy | **Two overlapping troubleshooting/FAQ pages.** `support/faq.mdx` ("Installation FAQ", 361 lines) and `guides/troubleshooting.mdx` (236 lines) both cover permission errors, Node version issues, PATH/command-not-found — with different fixes recommended (e.g. npm-permissions vs. nvm). They cross-link each other. Guaranteed drift. | -| F5 | Major | Redundancy / Staleness | **Hardcoded skill counts have already drifted, and the gate doesn't see them.** Actual count: 35. Docs say **30** (`getting-started/index.mdx:18`), **32** (`developer-journey/index.mdx:43,120`, `developer-journey/execution.mdx:123`, `reference/kb-structure.mdx:124`), **34** (`reference/guidelines-catalog.mdx:160`). `scripts/docs-staleness-check.js` only checks `reference/skills-catalog.mdx`. | +| F5 | Major | Redundancy / Staleness | **Hardcoded skill counts have already drifted, and the gate doesn't see them.** Actual count: 35. Docs say **30** (`getting-started/index.mdx:18`), **32** (`developer-journey/index.mdx:43,120`, `developer-journey/execution.mdx:123`, `reference/kb-structure.mdx:124`), **34** (`reference/guidelines-catalog.mdx:160`). The gate then only checked `reference/skills-catalog.mdx`. | | F6 | Major | Findability / Consistency | **Duplicate sidebar titles and undifferentiated parallel tracks.** "Team Setup" appears twice (`getting-started/quickstart-team.mdx` and `tutorials/team-setup.mdx`). The quickstart triad (solo/team/org) and the tutorial triad (first-project/team-setup/enterprise-adoption) are intentionally parallel (quick vs. deep) but their titles don't signal which is which. | | F7 | Major | Redundancy | **KB packaging documented twice.** `guides/packaging.mdx` (125 lines) and the "Package for Distribution" / "Verify Your Package" sections of `customization/organization.mdx` (221 lines) describe the same `pair-cli package` procedure and layout modes. | | F8 | Minor | Redundancy | **`guides/customize-kb.mdx`** (63-line recipe) restates the core of `customization/team.mdx` (identify → override → verify) and links to it. Fine as a recipe, wrong as a sibling section. | @@ -236,7 +236,7 @@ Top-level = Tutorials / How-to / Explanation / Reference; every section re-paren **Cross-link updates** — inbound links into moved pages (from the link audit): `guides/install-from-url` ← 4 links (`tutorials/managing-ai-artifacts` ×2, `customization/adopt`, `customization/organization`); `guides/customize-kb` ← 5; `guides/troubleshooting` ← 4; `guides/cli-workflows` ← 3; `guides/update-link` ← 2; `guides/adopter-checklist` ← 1; `guides/packaging` ← 0; `support/faq` ← 5. Plus the 8 broken section links (F2). A repo-wide grep for `/docs/guides` and `/docs/support/faq` catches all of it. -**Docs-staleness gate** (`scripts/docs-staleness-check.js`): +**Docs-staleness gate** (`apps/website/lib/docs-staleness-check.ts`): - Unaffected by Option A moves: its anchors (`reference/skills-catalog.mdx`, `reference/cli/commands.mdx`, `tutorials/`) keep their paths — this is why `commands.mdx` and `skills-catalog.mdx` are frozen in the mapping. - **Extend it** (batch 1): run the `(\d+)\s+(?:pair\s+)?skills` count check across *all* of `content/docs/**/*.mdx`, not just the catalog — that turns F5 from recurring drift into a CI failure. Optionally add a dead-internal-link check (resolve `](/docs/...)` targets against the source loader), which would have caught F2. diff --git a/apps/website/lib/docs-staleness-check.test.ts b/apps/website/lib/docs-staleness-check.test.ts index 83dd7ad0..977ecbf9 100644 --- a/apps/website/lib/docs-staleness-check.test.ts +++ b/apps/website/lib/docs-staleness-check.test.ts @@ -1,156 +1,177 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest' -import { spawnSync } from 'node:child_process' -import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join, resolve } from 'node:path' - -// Negative test for the docs staleness gate (scripts/docs-staleness-check.js): -// proves the extended "N skills" count check actually FAILS on a stale count, -// not just that it passes on the real tree. - -const SCRIPT = resolve(__dirname, '../../../scripts/docs-staleness-check.js') - -let fixtureRoot: string - -// Fixture tree with exactly 1 skill and 1 how-to guide, so any other count in docs is stale. -function buildFixture(skillCountClaim: string, extraProse = '', withHowToDir = true): void { - const docsDir = join(fixtureRoot, 'apps/website/content/docs') - mkdirSync(join(fixtureRoot, 'packages/knowledge-hub/dataset/.skills/capability/verify-quality'), { - recursive: true, - }) - writeFileSync( - join(fixtureRoot, 'packages/knowledge-hub/dataset/.skills/capability/verify-quality/SKILL.md'), - '# verify-quality\n', - ) - const howToDir = join(fixtureRoot, 'packages/knowledge-hub/dataset/.pair/knowledge/how-to') - rmSync(howToDir, { recursive: true, force: true }) - if (withHowToDir) { - mkdirSync(howToDir, { recursive: true }) - writeFileSync(join(howToDir, '01-how-to-create-PRD.md'), '# how-to\n') - writeFileSync(join(howToDir, 'README.md'), '# index\n') - } - mkdirSync(join(fixtureRoot, 'apps/pair-cli/src/commands'), { recursive: true }) - mkdirSync(join(docsDir, 'reference/cli'), { recursive: true }) - mkdirSync(join(docsDir, 'integrations'), { recursive: true }) - writeFileSync(join(docsDir, 'reference/skills-catalog.mdx'), '| **verify-quality** | row |\n') - writeFileSync(join(docsDir, 'reference/cli/commands.mdx'), 'No commands documented.\n') - writeFileSync( - join(docsDir, 'integrations/claude-code.mdx'), - `The canonical directory contains ${skillCountClaim}.\n${extraProse}`, - ) -} - -function runCheck(): { status: number | null; stdout: string } { - const result = spawnSync(process.execPath, [SCRIPT], { - env: { ...process.env, DOCS_STALENESS_ROOT: fixtureRoot }, - encoding: 'utf-8', - }) - return { status: result.status, stdout: result.stdout } -} - -describe('docs-staleness-check skill count gate', () => { - beforeAll(() => { - fixtureRoot = mkdtempSync(join(tmpdir(), 'docs-staleness-')) - }) - - afterAll(() => { - rmSync(fixtureRoot, { recursive: true, force: true }) - }) - - it('fails (exit 1) when a docs page claims a wrong skill count', () => { - buildFixture('5 skills') - const { status, stdout } = runCheck() - expect(status).toBe(1) - expect(stdout).toContain('Skill count mismatch') - expect(stdout).toContain('integrations/claude-code.mdx') - }) - - it('passes (exit 0) when the claimed skill count matches', () => { - buildFixture('1 skills') - const { status, stdout } = runCheck() - expect(status, stdout).toBe(0) - expect(stdout).toContain('PASS') - }) - - it('fails (exit 1) on wrong "N composable skills" (adjective between number and skills)', () => { - buildFixture('7 composable skills') - const { status, stdout } = runCheck() - expect(status).toBe(1) - expect(stdout).toContain('Skill count mismatch') - expect(stdout).toContain('7 composable skills') - }) - - it('fails (exit 1) on wrong "N+ skills" (trailing plus)', () => { - buildFixture('7+ skills') - const { status, stdout } = runCheck() - expect(status).toBe(1) - expect(stdout).toContain('Skill count mismatch') - }) - - it('fails (exit 1) on wrong "N agent skills"', () => { - buildFixture('1 skills', 'The standard exposes 7 agent skills.\n') - const { status, stdout } = runCheck() - expect(status).toBe(1) - expect(stdout).toContain('Skill count mismatch') - expect(stdout).toContain('7 agent skills') - }) - - it('passes (exit 0) on matching "N composable skills" / "N+ skills"', () => { - buildFixture('1 skills', 'Exactly 1 composable skills and 1+ skills.\n') - const { status, stdout } = runCheck() - expect(status, stdout).toBe(0) - }) - - it('fails (exit 1) when a docs page claims a wrong how-to guide count', () => { - buildFixture('1 skills', 'The KB ships 11 how-to guides.\n') - const { status, stdout } = runCheck() - expect(status).toBe(1) - expect(stdout).toContain('How-to guide count mismatch') - expect(stdout).toContain('integrations/claude-code.mdx') - }) - - it('passes (exit 0) when the claimed how-to guide count matches', () => { - buildFixture('1 skills', 'The KB ships 1 how-to guides.\n') - const { status, stdout } = runCheck() - expect(status, stdout).toBe(0) - }) - - it('fails (exit 1) on wrong counts in adjective phrasings ("N sequential guides")', () => { - buildFixture('1 skills', 'The KB has 9 sequential guides and 9 step-by-step guides.\n') - const { status, stdout } = runCheck() - expect(status).toBe(1) - expect(stdout).toContain('How-to guide count mismatch') - expect(stdout).toContain('9 sequential guides') - expect(stdout).toContain('9 step-by-step guides') - }) - - it('fails (exit 1) when the how-to dataset dir is missing (no silent skip)', () => { - buildFixture('1 skills', '', false) - const { status, stdout } = runCheck() - expect(status).toBe(1) - expect(stdout).toContain('how-to') - expect(stdout).toContain('not found') +import { describe, it, expect } from 'vitest' +import { resolve } from 'node:path' +import { + findSkillCountMismatches, + findGuideCountMismatches, + findDeadLinks, + checkCatalogSync, + checkCommandAnchors, + checkTutorialCommands, + countHowToGuides, + buildValidRoutes, + runAllChecks, +} from './docs-staleness-check' + +// White-box unit tests for the docs-staleness gate LOGIC. Exported functions are +// tested directly — no spawning of any CLI/script. The thin `tsx` CLI wrapper is +// out of scope here (its logic is these functions); parity with the real docs +// tree is asserted in-process via runAllChecks() below. + +const REPO_ROOT = resolve(__dirname, '../../..') + +describe('findSkillCountMismatches', () => { + it('flags a wrong bare "N skills"', () => { + expect(findSkillCountMismatches('has 5 skills', 'a.mdx', 35)).toHaveLength(1) + }) + + it('passes a matching "N skills"', () => { + expect(findSkillCountMismatches('has 35 skills', 'a.mdx', 35)).toHaveLength(0) + }) + + it('flags a wrong "N composable skills" (adjective between number and skills)', () => { + const errs = findSkillCountMismatches('7 composable skills', 'a.mdx', 1) + expect(errs).toHaveLength(1) + expect(errs[0]).toContain('7 composable skills') + }) + + it('flags a wrong "N+ skills" (trailing plus)', () => { + expect(findSkillCountMismatches('30+ skills', 'a.mdx', 35)).toHaveLength(1) + }) + + it('flags a wrong "N agent skills"', () => { + const errs = findSkillCountMismatches('exposes 7 agent skills', 'a.mdx', 1) + expect(errs).toHaveLength(1) + expect(errs[0]).toContain('7 agent skills') + }) + + it('passes matching "N composable skills" and "N+ skills"', () => { + expect(findSkillCountMismatches('1 composable skills and 1+ skills', 'a.mdx', 1)).toHaveLength( + 0, + ) + }) + + it('ignores subset counts ("N process skills")', () => { + expect(findSkillCountMismatches('9 process skills', 'a.mdx', 35)).toHaveLength(0) + }) +}) + +describe('findGuideCountMismatches', () => { + it('flags a wrong "N how-to guides"', () => { + expect(findGuideCountMismatches('11 how-to guides', 'a.mdx', 9)).toHaveLength(1) + }) + + it('passes a matching "N how-to guides"', () => { + expect(findGuideCountMismatches('9 how-to guides', 'a.mdx', 9)).toHaveLength(0) + }) + + it('flags wrong counts in adjective phrasings ("N sequential/step-by-step guides")', () => { + const errs = findGuideCountMismatches( + '11 sequential guides and 11 step-by-step guides', + 'a.mdx', + 9, + ) + expect(errs).toHaveLength(2) + expect(errs[0]).toContain('11 sequential guides') + expect(errs[1]).toContain('11 step-by-step guides') + }) + + it('flags "N step-by-step process guides" and "N process guides"', () => { + expect(findGuideCountMismatches('11 step-by-step process guides', 'a.mdx', 9)).toHaveLength(1) + expect(findGuideCountMismatches('11 process guides', 'a.mdx', 9)).toHaveLength(1) + }) + + it('does NOT false-positive on bare "N guides" prose (no how-to qualifier)', () => { + expect( + findGuideCountMismatches('5 guides at the museum and 3 tour guides', 'a.mdx', 9), + ).toHaveLength(0) + }) +}) + +describe('countHowToGuides', () => { + it('returns null when the how-to dir is missing (drives the loud gate failure)', () => { + expect(countHowToGuides(resolve(REPO_ROOT, 'does/not/exist'))).toBeNull() + }) + + it('counts NN-how-to-*.md files in the real dataset (ignoring README)', () => { + const n = countHowToGuides( + resolve(REPO_ROOT, 'packages/knowledge-hub/dataset/.pair/knowledge/how-to'), + ) + expect(n).toBe(9) + }) +}) + +describe('findDeadLinks', () => { + const routes = new Set(['/docs', '/docs/reference/skills-catalog', '/docs/tutorials']) + + it('flags a dead markdown link', () => { + expect(findDeadLinks('see [x](/docs/nope)', 'a.mdx', routes)).toHaveLength(1) + }) + + it('flags a dead JSX href="/docs/..." card link', () => { + const errs = findDeadLinks('x', 'a.mdx', routes) + expect(errs).toHaveLength(1) + expect(errs[0]).toContain('/docs/does-not-exist') + }) + + it('passes a valid JSX href and a valid markdown link (incl. anchors)', () => { + const ok = 'x and [t](/docs/tutorials#top)' + expect(findDeadLinks(ok, 'a.mdx', routes)).toHaveLength(0) + }) +}) + +describe('checkCatalogSync', () => { + it('flags a skill dir missing from the catalog', () => { + expect(checkCatalogSync(['implement'], 'no rows here')).toHaveLength(1) }) - it('does NOT false-positive on bare "N guides" prose (non-how-to phrasing)', () => { - // Tightened GUIDE_COUNT_RE must ignore arbitrary "N guides" that lack a - // how-to/process/sequential/step-by-step qualifier — even when N is "wrong". - buildFixture('1 skills', 'The city has 5 guides at the museum and 3 tour guides.\n') - const { status, stdout } = runCheck() - expect(status, stdout).toBe(0) + it('flags a catalog row with no matching dir', () => { + expect(checkCatalogSync([], '| **ghost** | row |')).toHaveLength(1) }) - it('fails (exit 1) on a dead JSX href="/docs/..." card link', () => { - buildFixture('1 skills', 'Broken\n') - const { status, stdout } = runCheck() - expect(status).toBe(1) - expect(stdout).toContain('Dead internal link') - expect(stdout).toContain('/docs/does-not-exist') + it('passes when both directions agree', () => { + expect(checkCatalogSync(['implement'], '| **implement** | row |')).toHaveLength(0) }) +}) + +describe('checkCommandAnchors', () => { + it('flags a command dir with no anchor', () => { + expect(checkCommandAnchors(['install'], 'no anchors')).toHaveLength(1) + }) + + it('passes when the anchor exists', () => { + expect(checkCommandAnchors(['install'], '## install (#install)')).toHaveLength(0) + }) +}) + +describe('checkTutorialCommands', () => { + it('flags an unknown pair-cli command reference', () => { + expect(checkTutorialCommands(['run pair-cli bogus'], ['install'])).toHaveLength(1) + }) + + it('ignores builtins and prose words after pair-cli', () => { + expect( + checkTutorialCommands(['pair-cli --version', 'pair-cli is installed'], ['install']), + ).toHaveLength(0) + }) +}) + +describe('buildValidRoutes', () => { + it('maps index.mdx to /docs and folder index to the folder route', () => { + const docsDir = '/x/docs' + const routes = buildValidRoutes( + ['/x/docs/index.mdx', '/x/docs/reference/index.mdx', '/x/docs/tutorials/first.mdx'], + docsDir, + ) + expect(routes.has('/docs')).toBe(true) + expect(routes.has('/docs/reference')).toBe(true) + expect(routes.has('/docs/tutorials/first')).toBe(true) + }) +}) - it('passes (exit 0) on a valid JSX href="/docs/..." card link', () => { - buildFixture('1 skills', 'OK\n') - const { status, stdout } = runCheck() - expect(status, stdout).toBe(0) +describe('runAllChecks (in-process, real docs tree)', () => { + it('reports zero drift and 35 skills against the actual repo', () => { + const { errors, skillCount } = runAllChecks(REPO_ROOT) + expect(errors, errors.join('\n')).toHaveLength(0) + expect(skillCount).toBe(35) }) }) diff --git a/apps/website/lib/docs-staleness-check.ts b/apps/website/lib/docs-staleness-check.ts new file mode 100644 index 00000000..c2889c85 --- /dev/null +++ b/apps/website/lib/docs-staleness-check.ts @@ -0,0 +1,287 @@ +/** + * Docs Staleness Check — verifies the published docs site matches source-of-truth + * code artifacts (skills corpus, CLI commands, how-to guides) and has no dead + * internal links. + * + * This is the website-docs integrity gate. The LOGIC lives here as individually + * exported, unit-tested functions (see docs-staleness-check.test.ts, white-box). + * The `main()` block is a thin CLI wrapper run via `tsx lib/docs-staleness-check.ts` + * (package script `docs:staleness`); it prints the same output and exit codes as + * before. Exit 0 = in sync, Exit 1 = drift detected. + * + * DOCS_STALENESS_ROOT overrides the repo root (used to point the checks at a + * fixture tree). Absent, the repo root is resolved from this file's location: + * apps/website/lib -> apps/website -> apps -> (up 3). + */ +import { existsSync, readFileSync, readdirSync } from 'node:fs' +import { basename, join, relative, resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const MODULE_DIR = dirname(fileURLToPath(import.meta.url)) + +/** Resolve the repo root, honouring the DOCS_STALENESS_ROOT override. */ +export function resolveRoot(): string { + const override = process.env['DOCS_STALENESS_ROOT'] + return override ? resolve(override) : resolve(MODULE_DIR, '../../..') +} + +// --- Regexes (exported so their intent is documented and directly testable) --- + +// Skill total-count phrasings across docs. Narrow to avoid prose false positives +// but covers "N skills", "N+ skills" (trailing plus), and an optional total-count +// adjective — "N pair/composable/agent/idempotent skills". Subset counts +// ("9 process skills") do NOT match: the adjective, when present, must be one of +// the whitelisted total-count words. +export const SKILL_COUNT_RE = /(\d+)\+?\s+(?:pair\s+|composable\s+|agent\s+|idempotent\s+)?skills/g + +// How-to guide count phrasings. Requires a how-to qualifier so arbitrary +// "N guides" prose ("5 guides at the museum") never false-positives: a match +// needs EITHER a recognized adjective (sequential/step-by-step) OR a +// how-to/process word. Bare "N guides" does not match. Covers "9 how-to guides", +// "9 process guides", "9 sequential guides", "9 step-by-step guides", +// "9 sequential how-to guides", "9 step-by-step process guides". +export const GUIDE_COUNT_RE = + /(\d+)\s+(?:(?:sequential|step-by-step)\s+(?:how-to\s+|process\s+)?|(?:how-to|process)\s+)guides/g + +// Internal /docs targets: markdown links `](/docs/...)` and JSX card +// `href="/docs/..."` attributes (Fumadocs /). +export const LINK_RE = /\]\((\/docs[^)\s]*)\)/g +export const HREF_RE = /href="(\/docs[^"]*)"/g + +// --- Filesystem helpers --- + +/** Skill names under a category dir: its subdirs, or the category itself if it's a meta skill (SKILL.md at the category root). */ +export function getSkillNames(categoryDir: string): string[] { + const entries = readdirSync(categoryDir, { withFileTypes: true }) + const subdirs = entries.filter(d => d.isDirectory()).map(d => d.name) + if (subdirs.length > 0) return subdirs + if (existsSync(join(categoryDir, 'SKILL.md'))) return [basename(categoryDir)] + return [] +} + +/** Every skill name across all category dirs under skillsDir. */ +export function collectSkills(skillsDir: string): string[] { + const categories = readdirSync(skillsDir, { withFileTypes: true }).filter(d => d.isDirectory()) + const all: string[] = [] + for (const cat of categories) all.push(...getSkillNames(join(skillsDir, cat.name))) + return all +} + +/** All .mdx files under dir, recursively. */ +export function walkMdx(dir: string): string[] { + const out: string[] = [] + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name) + if (entry.isDirectory()) out.push(...walkMdx(full)) + else if (entry.name.endsWith('.mdx')) out.push(full) + } + return out +} + +/** Count of how-to guide files (NN-how-to-*.md) in a KB how-to dir. `null` if the dir is missing. */ +export function countHowToGuides(howToDir: string): number | null { + if (!existsSync(howToDir)) return null + return readdirSync(howToDir).filter(f => /^\d+-how-to-.*\.md$/.test(f)).length +} + +// --- Pure per-content checks (return error strings; no I/O) --- + +/** Check 1: every "N skills" phrasing in content matches the actual skill count. */ +export function findSkillCountMismatches(content: string, rel: string, actual: number): string[] { + const errors: string[] = [] + for (const m of content.matchAll(SKILL_COUNT_RE)) { + const n = m[1] + if (n !== undefined && parseInt(n, 10) !== actual) { + errors.push(`Skill count mismatch in ${rel}: docs say "${m[0]}", actual count is ${actual}`) + } + } + return errors +} + +/** Check 2b: every "N how-to guides" phrasing in content matches the actual guide count. */ +export function findGuideCountMismatches(content: string, rel: string, actual: number): string[] { + const errors: string[] = [] + for (const m of content.matchAll(GUIDE_COUNT_RE)) { + const n = m[1] + if (n !== undefined && parseInt(n, 10) !== actual) { + errors.push( + `How-to guide count mismatch in ${rel}: docs say "${m[0]}", actual count is ${actual}`, + ) + } + } + return errors +} + +/** Check 5: every /docs link/href in content resolves to a known route. */ +export function findDeadLinks(content: string, rel: string, validRoutes: Set): string[] { + const errors: string[] = [] + for (const re of [LINK_RE, HREF_RE]) { + for (const m of content.matchAll(re)) { + const raw = m[1] + if (raw === undefined) continue + const head = (raw.split('#')[0] ?? '').split('?')[0] ?? '' + const target = head.replace(/\/$/, '') || '/docs' + if (!validRoutes.has(target)) { + errors.push(`Dead internal link in ${rel}: ${raw} does not resolve to a docs page`) + } + } + } + return errors +} + +/** Check 2: catalog lists every skill dir, and no catalog row lacks a dir (both directions). */ +export function checkCatalogSync(allSkills: string[], catalog: string): string[] { + const errors: string[] = [] + for (const skill of allSkills) { + if (!catalog.includes(`**${skill}**`)) { + errors.push(`Skill "${skill}" exists in .skills/ but missing from skills-catalog.mdx`) + } + } + const catalogSkills = [...catalog.matchAll(/\| \*\*([a-z0-9-]+)\*\* \|/g)] + .map(m => m[1]) + .filter((s): s is string => s !== undefined) + for (const docSkill of catalogSkills) { + if (!allSkills.includes(docSkill)) { + errors.push(`Skill "${docSkill}" in skills-catalog.mdx but no matching dir in .skills/`) + } + } + return errors +} + +/** Check 3: every command dir has an anchor in commands.mdx. */ +export function checkCommandAnchors(commandDirs: string[], commandsDoc: string): string[] { + const errors: string[] = [] + for (const cmd of commandDirs) { + if (!commandsDoc.includes(`(#${cmd})`)) { + errors.push(`CLI command "${cmd}" has a dir in commands/ but missing from commands.mdx`) + } + } + return errors +} + +const CLI_BUILTINS = new Set(['--version', '--help']) +const PROSE_WORDS = new Set(['as', 'is', 'on', 'to', 'installed', 'and', 'or', 'in', 'for', 'the']) + +/** Check 4: every `pair-cli ` referenced in tutorial content maps to a command dir. */ +export function checkTutorialCommands(tutorialContents: string[], commandDirs: string[]): string[] { + const errors: string[] = [] + const referenced = new Set() + for (const content of tutorialContents) { + for (const m of content.matchAll(/pair-cli\s+([a-z][a-z0-9-]*)/g)) { + if (m[1] !== undefined) referenced.add(m[1]) + } + } + for (const cmd of referenced) { + if (CLI_BUILTINS.has(`--${cmd}`)) continue + if (PROSE_WORDS.has(cmd)) continue + if (!commandDirs.includes(cmd)) { + errors.push(`Tutorial references "pair-cli ${cmd}" but no matching command dir in commands/`) + } + } + return errors +} + +/** Build the set of valid /docs routes from the docs .mdx file list. */ +export function buildValidRoutes(docsFiles: string[], docsDir: string): Set { + const routes = new Set() + for (const file of docsFiles) { + const rel = relative(docsDir, file) + .replace(/\\/g, '/') + .replace(/\.mdx$/, '') + routes.add(rel === 'index' ? '/docs' : `/docs/${rel.replace(/\/index$/, '')}`) + } + return routes +} + +export interface RunResult { + errors: string[] + skillCount: number + commandCount: number +} + +/** Checks 3 & 4: command anchors in commands.mdx, and tutorial `pair-cli ` references. */ +export function checkCliCommands( + commandsDir: string, + commandsFile: string, + tutorialsDir: string, +): { errors: string[]; commandCount: number } { + const errors: string[] = [] + const commandDirs = readdirSync(commandsDir, { withFileTypes: true }) + .filter(d => d.isDirectory()) + .map(d => d.name) + errors.push(...checkCommandAnchors(commandDirs, readFileSync(commandsFile, 'utf-8'))) + if (existsSync(tutorialsDir)) { + const tutorialContents = readdirSync(tutorialsDir) + .filter(f => f.endsWith('.mdx')) + .map(f => readFileSync(join(tutorialsDir, f), 'utf-8')) + errors.push(...checkTutorialCommands(tutorialContents, commandDirs)) + } + return { errors, commandCount: commandDirs.length } +} + +/** Run every check against a repo root and collect all drift errors. */ +export function runAllChecks(root: string): RunResult { + const SKILLS_DIR = join(root, 'packages/knowledge-hub/dataset/.skills') + const COMMANDS_DIR = join(root, 'apps/pair-cli/src/commands') + const DOCS_DIR = join(root, 'apps/website/content/docs') + const CATALOG_FILE = join(DOCS_DIR, 'reference/skills-catalog.mdx') + const COMMANDS_FILE = join(DOCS_DIR, 'reference/cli/commands.mdx') + const HOW_TO_DIR = join(root, 'packages/knowledge-hub/dataset/.pair/knowledge/how-to') + const TUTORIALS_DIR = join(DOCS_DIR, 'tutorials') + + const errors: string[] = [] + const docsFiles = walkMdx(DOCS_DIR) + const allSkills = collectSkills(SKILLS_DIR) + const skillCount = allSkills.length + const validRoutes = buildValidRoutes(docsFiles, DOCS_DIR) + const howToCount = countHowToGuides(HOW_TO_DIR) + + // Check 2b (loud failure if the how-to dataset dir moved) + if (howToCount === null) { + errors.push(`How-to guides dir not found: ${HOW_TO_DIR} — guide-count check cannot run`) + } + + // Per-file checks — read each doc once and run all content-level checks: + // 1 (skill counts), 2b (guide counts), 5 (dead links, markdown + JSX href). + for (const file of docsFiles) { + const content = readFileSync(file, 'utf-8') + const rel = relative(DOCS_DIR, file) + errors.push(...findSkillCountMismatches(content, rel, skillCount)) + if (howToCount !== null) errors.push(...findGuideCountMismatches(content, rel, howToCount)) + errors.push(...findDeadLinks(content, rel, validRoutes)) + } + + // Check 2: catalog sync (both directions) + errors.push(...checkCatalogSync(allSkills, readFileSync(CATALOG_FILE, 'utf-8'))) + + // Checks 3 & 4: CLI command anchors + tutorial references + const cli = checkCliCommands(COMMANDS_DIR, COMMANDS_FILE, TUTORIALS_DIR) + errors.push(...cli.errors) + + // NOTE: repo-root README.md is intentionally OUT of this gate's scope — the gate + // governs the published docs site (apps/website/content/docs) only. README's own + // literal skill/guide counts are tracked and fixed by PR #325. + + return { errors, skillCount, commandCount: cli.commandCount } +} + +/** Thin CLI wrapper: print the report and set the exit code. */ +export function main(): void { + const { errors, skillCount, commandCount } = runAllChecks(resolveRoot()) + console.log('Docs Staleness Check') + console.log('====================') + if (errors.length === 0) { + console.log(`PASS — ${skillCount} skills, ${commandCount} commands in sync`) + process.exit(0) + } + console.log(`FAIL — ${errors.length} issue${errors.length > 1 ? 's' : ''}\n`) + for (const e of errors) console.log(` • ${e}`) + console.log() + process.exit(1) +} + +// Main-guard: run only when invoked directly (tsx lib/docs-staleness-check.ts), +// not when imported by the unit tests. ESM equivalent of `require.main === module`. +if (import.meta.url === `file://${process.argv[1]}`) { + main() +} diff --git a/apps/website/package.json b/apps/website/package.json index 35f3f0c9..41c93c1f 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -22,7 +22,8 @@ "prettier:fix": "prettier-fix", "prettier:check": "prettier-check", "mdlint:check": "markdownlint-check", - "mdlint:fix": "markdownlint-fix" + "mdlint:fix": "markdownlint-fix", + "docs:staleness": "tsx lib/docs-staleness-check.ts" }, "dependencies": { "@pair/brand": "workspace:*", @@ -54,6 +55,7 @@ "jsdom": "catalog:", "postcss": "catalog:", "tailwindcss": "catalog:", + "tsx": "catalog:", "typescript": "catalog:", "vitest": "catalog:", "vitest-axe": "catalog:" diff --git a/package.json b/package.json index b215bc1d..0709ab22 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "a11y:report": "turbo a11y:report", "a11y:report:html": "turbo a11y:report:html", "hygiene:check": "node scripts/code-hygiene-check.js", - "docs:staleness": "node scripts/docs-staleness-check.js", + "docs:staleness": "pnpm --filter @pair/website docs:staleness", "dup:check": "jscpd apps packages", "quality-gate": "turbo ts:check test lint && turbo prettier:fix mdlint:fix && ./tools/markdownlint-config/bin/markdownlint-fix.sh '*.md' && pnpm hygiene:check && pnpm docs:staleness && pnpm dup:check", "e2e": "pnpm --filter @pair/website e2e", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c9d6808..e721878a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,9 @@ catalogs: ts-node: specifier: 10.9.2 version: 10.9.2 + tsx: + specifier: 4.23.1 + version: 4.23.1 turbo: specifier: 2.5.6 version: 2.5.6 @@ -239,7 +242,7 @@ importers: version: 24.3.0 '@vitest/coverage-v8': specifier: 'catalog:' - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2)) ts-node: specifier: 'catalog:' version: 10.9.2(@types/node@24.3.0)(typescript@5.4.5) @@ -248,10 +251,10 @@ importers: version: 5.4.5 vite-tsconfig-paths: specifier: 'catalog:' - version: 5.1.4(typescript@5.4.5)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)) + version: 5.1.4(typescript@5.4.5)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2) apps/website: dependencies: @@ -266,7 +269,7 @@ importers: version: 14.7.7(@types/react@19.0.6)(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) fumadocs-mdx: specifier: 'catalog:' - version: 11.10.1(fumadocs-core@14.7.7(@types/react@19.0.6)(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)) + version: 11.10.1(fumadocs-core@14.7.7(@types/react@19.0.6)(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)) fumadocs-ui: specifier: 'catalog:' version: 14.7.7(@types/react-dom@19.0.6(@types/react@19.0.6))(@types/react@19.0.6)(fumadocs-core@14.7.7(@types/react@19.0.6)(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.4.5))) @@ -303,7 +306,7 @@ importers: version: link:../../tools/ts-config '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.58.2(@types/node@24.3.0)(jiti@1.21.7)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2))(yaml@2.8.2) + version: 1.58.2(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2))(yaml@2.8.2) '@playwright/test': specifier: 'catalog:' version: 1.58.2 @@ -321,10 +324,10 @@ importers: version: 19.0.6(@types/react@19.0.6) '@vitejs/plugin-react': specifier: 'catalog:' - version: 4.3.4(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)) + version: 4.3.4(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2)) autoprefixer: specifier: 'catalog:' version: 10.4.20(postcss@8.4.49) @@ -337,15 +340,18 @@ importers: tailwindcss: specifier: 'catalog:' version: 3.4.17(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.4.5)) + tsx: + specifier: 'catalog:' + version: 4.23.1 typescript: specifier: 'catalog:' version: 5.4.5 vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2) vitest-axe: specifier: 'catalog:' - version: 1.0.0-pre.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2)) + version: 1.0.0-pre.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2)) packages/brand: devDependencies: @@ -366,7 +372,7 @@ importers: version: link:../../tools/ts-config '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.58.2(@types/node@24.3.0)(jiti@1.21.7)(vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2))(yaml@2.8.2) + version: 1.58.2(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2))(yaml@2.8.2) '@testing-library/jest-dom': specifier: 'catalog:' version: 6.6.3 @@ -381,10 +387,10 @@ importers: version: 19.0.6(@types/react@19.0.6) '@vitejs/plugin-react': specifier: 'catalog:' - version: 4.3.4(vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)) + version: 4.3.4(vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2)) autoprefixer: specifier: 'catalog:' version: 10.4.20(postcss@8.4.49) @@ -411,13 +417,13 @@ importers: version: 5.4.5 vite: specifier: 'catalog:' - version: 6.0.7(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) + version: 6.0.7(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2) vitest-axe: specifier: 'catalog:' - version: 1.0.0-pre.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2)) + version: 1.0.0-pre.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2)) packages/content-ops: dependencies: @@ -463,7 +469,7 @@ importers: version: 24.3.0 '@vitest/coverage-v8': specifier: 'catalog:' - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2)) ts-node: specifier: 'catalog:' version: 10.9.2(@types/node@24.3.0)(typescript@5.4.5) @@ -472,10 +478,10 @@ importers: version: 5.4.5 vite-tsconfig-paths: specifier: 'catalog:' - version: 5.1.4(typescript@5.4.5)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)) + version: 5.1.4(typescript@5.4.5)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2) packages/knowledge-hub: devDependencies: @@ -499,7 +505,7 @@ importers: version: 24.3.0 '@vitest/coverage-v8': specifier: 'catalog:' - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2)) ts-node: specifier: 'catalog:' version: 10.9.2(@types/node@24.3.0)(typescript@5.4.5) @@ -508,10 +514,10 @@ importers: version: 5.4.5 vite-tsconfig-paths: specifier: 'catalog:' - version: 5.1.4(typescript@5.4.5)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)) + version: 5.1.4(typescript@5.4.5)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)) vitest: specifier: 'catalog:' - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2) tools/eslint-config: devDependencies: @@ -768,6 +774,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.24.2': resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} engines: {node: '>=18'} @@ -780,6 +792,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.24.2': resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} engines: {node: '>=18'} @@ -792,6 +810,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.24.2': resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} engines: {node: '>=18'} @@ -804,6 +828,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.24.2': resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} engines: {node: '>=18'} @@ -816,6 +846,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.24.2': resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} engines: {node: '>=18'} @@ -828,6 +864,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.24.2': resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} engines: {node: '>=18'} @@ -840,6 +882,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.24.2': resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} engines: {node: '>=18'} @@ -852,6 +900,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.24.2': resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} engines: {node: '>=18'} @@ -864,6 +918,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.24.2': resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} engines: {node: '>=18'} @@ -876,6 +936,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.24.2': resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} engines: {node: '>=18'} @@ -888,6 +954,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.24.2': resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} engines: {node: '>=18'} @@ -900,6 +972,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.24.2': resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} engines: {node: '>=18'} @@ -912,6 +990,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.24.2': resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} engines: {node: '>=18'} @@ -924,6 +1008,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.24.2': resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} engines: {node: '>=18'} @@ -936,6 +1026,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.24.2': resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} engines: {node: '>=18'} @@ -948,6 +1044,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.24.2': resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} engines: {node: '>=18'} @@ -960,6 +1062,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.24.2': resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} engines: {node: '>=18'} @@ -972,6 +1080,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.24.2': resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} engines: {node: '>=18'} @@ -984,6 +1098,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.24.2': resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} engines: {node: '>=18'} @@ -996,6 +1116,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.24.2': resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} engines: {node: '>=18'} @@ -1008,12 +1134,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.24.2': resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} engines: {node: '>=18'} @@ -1026,6 +1164,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.24.2': resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} engines: {node: '>=18'} @@ -1038,6 +1182,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.24.2': resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} engines: {node: '>=18'} @@ -1050,6 +1200,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.24.2': resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} engines: {node: '>=18'} @@ -1062,6 +1218,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2908,6 +3070,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4759,6 +4926,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + turbo-darwin-64@2.5.6: resolution: {integrity: sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==} cpu: [x64] @@ -5490,153 +5662,231 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + '@esbuild/android-arm64@0.24.2': optional: true '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.28.1': + optional: true + '@esbuild/android-arm@0.24.2': optional: true '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.28.1': + optional: true + '@esbuild/android-x64@0.24.2': optional: true '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.28.1': + optional: true + '@esbuild/darwin-arm64@0.24.2': optional: true '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.28.1': + optional: true + '@esbuild/darwin-x64@0.24.2': optional: true '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.28.1': + optional: true + '@esbuild/freebsd-arm64@0.24.2': optional: true '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.28.1': + optional: true + '@esbuild/freebsd-x64@0.24.2': optional: true '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.28.1': + optional: true + '@esbuild/linux-arm64@0.24.2': optional: true '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.28.1': + optional: true + '@esbuild/linux-arm@0.24.2': optional: true '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.28.1': + optional: true + '@esbuild/linux-ia32@0.24.2': optional: true '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.28.1': + optional: true + '@esbuild/linux-loong64@0.24.2': optional: true '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.28.1': + optional: true + '@esbuild/linux-mips64el@0.24.2': optional: true '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.28.1': + optional: true + '@esbuild/linux-ppc64@0.24.2': optional: true '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.28.1': + optional: true + '@esbuild/linux-riscv64@0.24.2': optional: true '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.28.1': + optional: true + '@esbuild/linux-s390x@0.24.2': optional: true '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.28.1': + optional: true + '@esbuild/linux-x64@0.24.2': optional: true '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.28.1': + optional: true + '@esbuild/netbsd-arm64@0.24.2': optional: true '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.28.1': + optional: true + '@esbuild/netbsd-x64@0.24.2': optional: true '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.28.1': + optional: true + '@esbuild/openbsd-arm64@0.24.2': optional: true '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.28.1': + optional: true + '@esbuild/openbsd-x64@0.24.2': optional: true '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.28.1': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.28.1': + optional: true + '@esbuild/sunos-x64@0.24.2': optional: true '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.28.1': + optional: true + '@esbuild/win32-arm64@0.24.2': optional: true '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.28.1': + optional: true + '@esbuild/win32-ia32@0.24.2': optional: true '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.28.1': + optional: true + '@esbuild/win32-x64@0.24.2': optional: true '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.28.1': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.34.0(jiti@1.21.7))': dependencies: eslint: 9.34.0(jiti@1.21.7) @@ -6141,11 +6391,11 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/experimental-ct-core@1.58.2(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)': + '@playwright/experimental-ct-core@1.58.2(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)': dependencies: playwright: 1.58.2 playwright-core: 1.58.2 - vite: 6.4.1(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) + vite: 6.4.1(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -6159,10 +6409,10 @@ snapshots: - tsx - yaml - '@playwright/experimental-ct-react@1.58.2(@types/node@24.3.0)(jiti@1.21.7)(vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2))(yaml@2.8.2)': + '@playwright/experimental-ct-react@1.58.2(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2))(yaml@2.8.2)': dependencies: - '@playwright/experimental-ct-core': 1.58.2(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) - '@vitejs/plugin-react': 4.3.4(vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)) + '@playwright/experimental-ct-core': 1.58.2(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) + '@vitejs/plugin-react': 4.3.4(vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)) transitivePeerDependencies: - '@types/node' - jiti @@ -6178,10 +6428,10 @@ snapshots: - vite - yaml - '@playwright/experimental-ct-react@1.58.2(@types/node@24.3.0)(jiti@1.21.7)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2))(yaml@2.8.2)': + '@playwright/experimental-ct-react@1.58.2(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2))(yaml@2.8.2)': dependencies: - '@playwright/experimental-ct-core': 1.58.2(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) - '@vitejs/plugin-react': 4.3.4(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)) + '@playwright/experimental-ct-core': 1.58.2(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) + '@vitejs/plugin-react': 4.3.4(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)) transitivePeerDependencies: - '@types/node' - jiti @@ -6935,29 +7185,29 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.3.4(vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2))': + '@vitejs/plugin-react@4.3.4(vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.0.7(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) + vite: 6.0.7(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.3.4(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2))': + '@vitejs/plugin-react@4.3.4(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) + vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -6972,7 +7222,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -6984,13 +7234,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) + vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -7633,6 +7883,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -7909,7 +8188,7 @@ snapshots: - '@types/react' - supports-color - fumadocs-mdx@11.10.1(fumadocs-core@14.7.7(@types/react@19.0.6)(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)): + fumadocs-mdx@11.10.1(fumadocs-core@14.7.7(@types/react@19.0.6)(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(next@15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 @@ -7930,7 +8209,7 @@ snapshots: optionalDependencies: next: 15.5.12(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 - vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) + vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -10065,6 +10344,12 @@ snapshots: tslib@2.8.1: {} + tsx@4.23.1: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + turbo-darwin-64@2.5.6: optional: true @@ -10220,13 +10505,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2): + vite-node@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) + vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -10241,18 +10526,18 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.4.5)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)): + vite-tsconfig-paths@5.1.4(typescript@5.4.5)(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.4.5) optionalDependencies: - vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) + vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript - vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2): + vite@6.0.7(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2): dependencies: esbuild: 0.24.2 postcss: 8.5.6 @@ -10261,9 +10546,10 @@ snapshots: '@types/node': 24.3.0 fsevents: 2.3.3 jiti: 1.21.7 + tsx: 4.23.1 yaml: 2.8.2 - vite@6.4.1(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2): + vite@6.4.1(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -10275,9 +10561,10 @@ snapshots: '@types/node': 24.3.0 fsevents: 2.3.3 jiti: 1.21.7 + tsx: 4.23.1 yaml: 2.8.2 - vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2): + vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -10289,21 +10576,22 @@ snapshots: '@types/node': 24.3.0 fsevents: 2.3.3 jiti: 1.21.7 + tsx: 4.23.1 yaml: 2.8.2 - vitest-axe@1.0.0-pre.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2)): + vitest-axe@1.0.0-pre.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2)): dependencies: '@vitest/pretty-format': 3.2.4 axe-core: 4.11.1 chalk: 5.6.2 lodash-es: 4.17.23 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2) - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@1.21.7)(jsdom@25.0.1)(tsx@4.23.1)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -10321,8 +10609,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.3.0)(jiti@1.21.7)(yaml@2.8.2) + vite: 7.2.6(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.3.0)(jiti@1.21.7)(tsx@4.23.1)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 58d4ccda..6ae7daeb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -52,6 +52,7 @@ catalog: remark-parse: 11.0.0 tailwindcss: 3.4.17 ts-node: 10.9.2 + tsx: 4.23.1 turbo: 2.5.6 typescript: 5.4.5 unified: 11.0.5 diff --git a/scripts/docs-staleness-check.js b/scripts/docs-staleness-check.js deleted file mode 100644 index 7361800d..00000000 --- a/scripts/docs-staleness-check.js +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env node -/** - * Docs Staleness Check — verifies docs match source-of-truth code artifacts. - * Exit 0 = in sync, Exit 1 = drift detected. - */ -const fs = require('fs') -const path = require('path') - -// DOCS_STALENESS_ROOT overrides the repo root — used by the negative test -// (apps/website/lib/docs-staleness-check.test.ts) to run against a fixture tree. -const ROOT = process.env.DOCS_STALENESS_ROOT - ? path.resolve(process.env.DOCS_STALENESS_ROOT) - : path.resolve(__dirname, '..') -const SKILLS_DIR = path.join(ROOT, 'packages/knowledge-hub/dataset/.skills') -const COMMANDS_DIR = path.join(ROOT, 'apps/pair-cli/src/commands') -const DOCS_DIR = path.join(ROOT, 'apps/website/content/docs') -const CATALOG_FILE = path.join(DOCS_DIR, 'reference/skills-catalog.mdx') -const COMMANDS_FILE = path.join(DOCS_DIR, 'reference/cli/commands.mdx') - -const errors = [] - -// --- Helpers --- - -function getSkillNames(categoryDir) { - const entries = fs.readdirSync(categoryDir, { withFileTypes: true }) - const subdirs = entries.filter((d) => d.isDirectory()).map((d) => d.name) - if (subdirs.length > 0) return subdirs - // Meta skill: category dir itself contains SKILL.md - if (fs.existsSync(path.join(categoryDir, 'SKILL.md'))) { - return [path.basename(categoryDir)] - } - return [] -} - -function walkMdx(dir) { - const out = [] - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name) - if (entry.isDirectory()) out.push(...walkMdx(full)) - else if (entry.name.endsWith('.mdx')) out.push(full) - } - return out -} - -const docsFiles = walkMdx(DOCS_DIR) - -// --- Check 1 & 2: Skills --- - -const categories = fs.readdirSync(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()) - -const allSkills = [] -for (const cat of categories) { - allSkills.push(...getSkillNames(path.join(SKILLS_DIR, cat.name))) -} -const skillCount = allSkills.length - -const catalog = fs.readFileSync(CATALOG_FILE, 'utf-8') - -// Check 1: every occurrence of "N skills" across ALL docs pages matches actual count. -// Regex stays narrow to avoid prose false positives but covers the total-count -// phrasings docs actually use: "N skills", "N+ skills" (trailing plus), and an -// optional total-count adjective — "N pair/composable/agent/idempotent skills". -// Subset counts ("9 process skills") still do NOT match: the adjective, when -// present, must be one of the whitelisted total-count words. -const COUNT_RE = /(\d+)\+?\s+(?:pair\s+|composable\s+|agent\s+|idempotent\s+)?skills/g -for (const file of docsFiles) { - const content = fs.readFileSync(file, 'utf-8') - const rel = path.relative(DOCS_DIR, file) - for (const m of content.matchAll(COUNT_RE)) { - const docCount = parseInt(m[1], 10) - if (docCount !== skillCount) { - errors.push(`Skill count mismatch in ${rel}: docs say "${m[0]}", actual count is ${skillCount}`) - } - } -} - -// Check 2: every skill dir has a table row in the catalog -for (const skill of allSkills) { - if (!catalog.includes(`**${skill}**`)) { - errors.push(`Skill "${skill}" exists in .skills/ but missing from skills-catalog.mdx`) - } -} - -// Reverse check: catalog entries that no longer exist as dirs -const catalogSkills = [...catalog.matchAll(/\| \*\*([a-z0-9-]+)\*\* \|/g)].map((m) => m[1]) -for (const docSkill of catalogSkills) { - if (!allSkills.includes(docSkill)) { - errors.push(`Skill "${docSkill}" in skills-catalog.mdx but no matching dir in .skills/`) - } -} - -// --- Check 2b: How-to guide counts --- -// Same protection as Check 1, for "N how-to guides" / "N process guides" -// claims (the manual-flow counterpart of skills). Guide files live in the KB -// dataset as NN-how-to-*.md; README is the index, not a guide. - -const HOW_TO_DIR = path.join(ROOT, 'packages/knowledge-hub/dataset/.pair/knowledge/how-to') - -if (!fs.existsSync(HOW_TO_DIR)) { - // Loud failure (consistent with Check 1): a moved dataset dir must not - // silently disable the guide-count check. - errors.push(`How-to guides dir not found: ${HOW_TO_DIR} — guide-count check cannot run`) -} else { - const howToCount = fs - .readdirSync(HOW_TO_DIR) - .filter((f) => /^\d+-how-to-.*\.md$/.test(f)).length - - // Requires a how-to qualifier so arbitrary "N guides" prose ("5 guides at the - // museum") never false-positives. A match needs EITHER a recognized adjective - // (sequential/step-by-step) OR a how-to/process word — bare "N guides" does not - // match. Covers: "9 how-to guides", "9 process guides", "9 sequential guides", - // "9 step-by-step guides", "9 sequential how-to guides", "9 step-by-step process guides". - const GUIDE_COUNT_RE = - /(\d+)\s+(?:(?:sequential|step-by-step)\s+(?:how-to\s+|process\s+)?|(?:how-to|process)\s+)guides/g - for (const file of docsFiles) { - const content = fs.readFileSync(file, 'utf-8') - const rel = path.relative(DOCS_DIR, file) - for (const m of content.matchAll(GUIDE_COUNT_RE)) { - const docCount = parseInt(m[1], 10) - if (docCount !== howToCount) { - errors.push( - `How-to guide count mismatch in ${rel}: docs say "${m[0]}", actual count is ${howToCount}`, - ) - } - } - } -} - -// --- Check 3: CLI commands --- - -const commandDirs = fs - .readdirSync(COMMANDS_DIR, { withFileTypes: true }) - .filter((d) => d.isDirectory()) - .map((d) => d.name) - -const commandsDoc = fs.readFileSync(COMMANDS_FILE, 'utf-8') - -for (const cmd of commandDirs) { - if (!commandsDoc.includes(`(#${cmd})`)) { - errors.push(`CLI command "${cmd}" has a dir in commands/ but missing from commands.mdx`) - } -} - -// --- Check 4: Tutorial CLI command references --- - -const TUTORIALS_DIR = path.join(ROOT, 'apps/website/content/docs/tutorials') -const CLI_BUILTINS = new Set(['--version', '--help']) - -if (fs.existsSync(TUTORIALS_DIR)) { - const tutorialFiles = fs - .readdirSync(TUTORIALS_DIR) - .filter((f) => f.endsWith('.mdx')) - - const referencedCommands = new Set() - for (const file of tutorialFiles) { - const content = fs.readFileSync(path.join(TUTORIALS_DIR, file), 'utf-8') - // Match pair-cli in code blocks (``` or backtick-inline) - const matches = [...content.matchAll(/pair-cli\s+([a-z][a-z0-9-]*)/g)] - for (const m of matches) { - referencedCommands.add(m[1]) - } - } - - // Filter to likely commands: must exist as a command dir or be a known builtin - // Ignore English prose words that follow "pair-cli" in non-code context - const proseWords = new Set(['as', 'is', 'on', 'to', 'installed', 'and', 'or', 'in', 'for', 'the']) - for (const cmd of referencedCommands) { - if (CLI_BUILTINS.has(`--${cmd}`)) continue - if (proseWords.has(cmd)) continue - if (!commandDirs.includes(cmd)) { - errors.push( - `Tutorial references "pair-cli ${cmd}" but no matching command dir in commands/`, - ) - } - } -} - -// --- Check 5: Dead internal docs links --- -// Every /docs target must resolve to a page in the content tree (file route, or -// folder route backed by an index.mdx). Scans BOTH markdown links `](/docs/...)` -// and JSX card `href="/docs/..."` attributes (Fumadocs /) — the -// latter closes a coverage gap for the card-based section indexes. Would have -// caught the index-less section links (/docs/concepts, /docs/guides, /docs/reference). - -const validRoutes = new Set() -for (const file of docsFiles) { - const rel = path.relative(DOCS_DIR, file).replace(/\\/g, '/').replace(/\.mdx$/, '') - const route = rel === 'index' ? '/docs' : `/docs/${rel.replace(/\/index$/, '')}` - validRoutes.add(route) -} - -const LINK_RE = /\]\((\/docs[^)\s]*)\)/g -const HREF_RE = /href="(\/docs[^"]*)"/g -for (const file of docsFiles) { - const content = fs.readFileSync(file, 'utf-8') - const rel = path.relative(DOCS_DIR, file) - for (const re of [LINK_RE, HREF_RE]) { - for (const m of content.matchAll(re)) { - const target = m[1].split('#')[0].split('?')[0].replace(/\/$/, '') || '/docs' - if (!validRoutes.has(target)) { - errors.push(`Dead internal link in ${rel}: ${m[1]} does not resolve to a docs page`) - } - } - } -} - -// NOTE: repo-root README.md is intentionally OUT of this gate's scope — the gate -// governs the published docs site (apps/website/content/docs) only. README's own -// literal skill/guide counts are tracked and fixed by PR #325. - -// --- Output --- - -console.log('Docs Staleness Check') -console.log('====================') - -if (errors.length === 0) { - console.log(`PASS — ${skillCount} skills, ${commandDirs.length} commands in sync`) - process.exit(0) -} else { - console.log(`FAIL — ${errors.length} issue${errors.length > 1 ? 's' : ''}\n`) - for (const e of errors) { - console.log(` \u2022 ${e}`) - } - console.log() - process.exit(1) -} From 46292c8e0165b416068166d3fcecd17669a48faa Mon Sep 17 00:00:00 2001 From: Gianluca Carucci Date: Mon, 13 Jul 2026 21:00:01 +0200 Subject: [PATCH 14/14] =?UTF-8?q?[#312]=20docs:=20ADL=20=E2=80=94=20gate/t?= =?UTF-8?q?ooling=20code=20in=20tested=20modules,=20no=20unit-test=20on=20?= =?UTF-8?q?scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the architectural rule the docs-staleness refactor implements as a non-architectural ADL (adoption/decision-log/2026-07-13-gate-tooling- code-in-tested-modules.md): gate logic lives in tested owning-package modules; scripts/CLIs are thin entrypoints; root gates delegate; scripts are never unit-tested (smoke tests cover CLI end-to-end). - complements (does not amend) the test-file-colocation ADL — placement vs module-vs-script boundary are orthogonal; cross-linked - surfaced from way-of-working Quality Gates ("Gate & tooling code" bullet) - no dataset mirror (adoption decision-log is not auto-mirrored; siblings husky/colocation are adoption-only) Refs: PR #315 architectural rule (no test-on-script) Co-Authored-By: Claude Fable 5 --- ...-13-gate-tooling-code-in-tested-modules.md | 59 +++++++++++++++++++ .pair/adoption/tech/way-of-working.md | 1 + 2 files changed, 60 insertions(+) create mode 100644 .pair/adoption/decision-log/2026-07-13-gate-tooling-code-in-tested-modules.md diff --git a/.pair/adoption/decision-log/2026-07-13-gate-tooling-code-in-tested-modules.md b/.pair/adoption/decision-log/2026-07-13-gate-tooling-code-in-tested-modules.md new file mode 100644 index 00000000..8a580081 --- /dev/null +++ b/.pair/adoption/decision-log/2026-07-13-gate-tooling-code-in-tested-modules.md @@ -0,0 +1,59 @@ +# Decision: Gate/tooling code lives in tested modules; scripts are thin entrypoints, never unit-tested + +## Date + +2026-07-13 + +## Status + +Active + +## Category + +Convention Adoption + +## Context + +Several repository quality gates started life as standalone scripts under `scripts/` with their logic inline, then grew a sibling vitest test that reached into the script to exercise that logic. Two anti-patterns emerged: + +- `scripts/docs-staleness-check.js` held all the docs-integrity logic (skill/guide count checks, catalog sync, dead-link scan) and its test (`apps/website/lib/docs-staleness-check.test.ts`) black-box **spawned the script** via `spawnSync` against fixture trees — testing an opaque process, not importable logic, and duplicating a real build artifact's behavior from outside. +- The same shape had already been corrected once for the skills-conformance gate (#313/#324): logic moved into a module, script deleted. + +This is orthogonal to the existing co-location ADL, [2026-07-08-test-file-colocation-multi-module.md](./2026-07-08-test-file-colocation-multi-module.md), which governs **where** a test file sits (root-of-call-chain co-location; one file per entry point for e2e). It does not say **what** should be a testable module versus a script, nor how scripts themselves may be verified. This ADL fills that gap; the two are complementary and non-overlapping. + +The repository owner stated the rule directly: "tested functionality lives in production code and is tested there; do NOT unit-test scripts" and, on how to verify a script/CLI when desired, "use a smoke test, not a unit test." + +## Decision + +**Functionality worth testing lives in a production module** — an importable module in the package that owns the concern, unit-tested white-box (tests import the module's exported functions and assert on them directly). + +**Scripts/CLIs are thin entrypoints** that call those modules. A repo-root gate delegates to the owning package rather than holding logic itself: + +```json +"docs:staleness": "pnpm --filter @pair/website docs:staleness" +``` + +and the package script runs the module through a TS runner (`ts-node`/`tsx`) behind a main-guard, e.g. `"docs:staleness": "tsx lib/docs-staleness-check.ts"`. + +**Scripts are never unit-tested.** No importing a script's functions into a test, and no black-box `spawnSync`/`exec` of a script inside a vitest unit test. Unit tests target the module's exported logic. When script/CLI-level (end-to-end) verification is wanted, it uses the **smoke-test suite** (`scripts/smoke-tests/`, `pnpm smoke-tests`), not vitest. + +Rationale: a gate is testable logic, not an opaque script; keeping the logic in an importable module removes duplication and orphan tests that reach into root `scripts/`; unit tests then cover module logic while smoke tests cover CLI wiring end-to-end. The module's public functions are the single tested surface; the CLI wrapper is a trivial, unit-test-exempt shell. + +## Alternatives Considered + +- **Keep logic in the script, test it via `spawnSync`/`exec`**: Rejected. Tests an opaque process instead of logic, is slow, forces fixture-tree scaffolding for cases a pure function test expresses in one line, and leaves the logic un-importable and un-refactorable. This is the exact pattern being removed. +- **Keep logic in the script, `require()`/`import` its functions into a unit test**: Rejected. Makes `scripts/` a de-facto source tree with orphan tests reaching across the repo into it, contradicting package ownership and the co-location ADL; a script is meant to be a thin entrypoint, not a module. +- **Move logic to a module but skip a runnable CLI (call only from tests/CI code)**: Rejected. The gate must stay runnable locally and in CI as a single command; a thin main-guarded CLI wrapper delegating to the module keeps that ergonomics without holding logic. + +## Consequences + +- Quality/integrity gates are authored as owning-package modules with white-box unit tests; the repo-root gate script becomes a one-line delegation (`pnpm --filter `), and CI's step is unchanged because it already calls the root script. +- Reviewers can reject a new gate that puts logic in `scripts/` or that tests a script by spawning it; the fix is "extract to a module + white-box test, delegate the script." +- Script/CLI end-to-end coverage, when needed, is added to `scripts/smoke-tests/` (surfaced as gate `pnpm smoke-tests`), keeping vitest free of script spawning. +- Applied on landing: docs-staleness → `apps/website/lib/docs-staleness-check.ts` + white-box test, `scripts/docs-staleness-check.js` deleted, root gate delegates (PR #315). Prior exemplar: skills-conformance → module in `packages/knowledge-hub/src/tools/` + sibling test, script deleted (#313/#324). Pre-existing exemplars that already followed this shape: knowledge-hub `check-broken-links.ts` and `transfer-dataset.ts` (package modules run via package scripts). + +## Adoption Impact + +- [way-of-working.md](../tech/way-of-working.md): Quality Gates section gains a short "Gate & tooling code" bullet pointing to this ADL (surfacing). +- No knowledge-base/dataset mirror: sibling ADLs in `adoption/decision-log/` are adoption-only records; the dataset (`packages/knowledge-hub/dataset/`) is a curated sample, not an auto-mirror of adoption, so this decision is not copied there. +- Complements (does not amend) [2026-07-08-test-file-colocation-multi-module.md](./2026-07-08-test-file-colocation-multi-module.md): that ADL owns test-file placement; this ADL owns module-vs-script boundaries and the no-unit-test-on-scripts rule. diff --git a/.pair/adoption/tech/way-of-working.md b/.pair/adoption/tech/way-of-working.md index 98c2ffa9..7a1392d1 100644 --- a/.pair/adoption/tech/way-of-working.md +++ b/.pair/adoption/tech/way-of-working.md @@ -22,6 +22,7 @@ - `pnpm quality-gate` is the adopted project-level quality gate command. - Quality gate includes: type checking (`ts:check`), testing (`test`), linting (`lint`), formatting (`prettier:fix`), markdown lint (`mdlint:fix`). +- **Gate & tooling code:** a gate's logic lives in a tested module in its owning package (white-box unit tests); scripts/CLIs are thin entrypoints and a root gate delegates (`pnpm --filter `). Scripts are never unit-tested — CLI-level checks go to smoke tests. See ADL [2026-07-13-gate-tooling-code-in-tested-modules.md](../decision-log/2026-07-13-gate-tooling-code-in-tested-modules.md). ### Custom Gate Registry