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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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 <pkg> <gate>`), 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.
1 change: 1 addition & 0 deletions .pair/adoption/tech/way-of-working.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <pkg> <gate>`). 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

Expand Down
6 changes: 3 additions & 3 deletions apps/pair-cli/src/commands/kb-info/migration-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
})

Expand All @@ -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')
})
})
2 changes: 1 addition & 1 deletion apps/pair-cli/src/commands/kb-info/migration-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/pair-cli/src/commands/kb-info/version-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)
})

Expand Down
2 changes: 1 addition & 1 deletion apps/pair-cli/src/commands/update-link/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion apps/website/content/docs/concepts/ai-assisted-sdlc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 25 additions & 0 deletions apps/website/content/docs/concepts/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
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.

## 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.
2 changes: 1 addition & 1 deletion apps/website/content/docs/concepts/knowledge-base.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/website/content/docs/customization/adopt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion apps/website/content/docs/customization/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) |
Expand All @@ -51,5 +54,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
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion apps/website/content/docs/customization/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Customization",
"pages": ["adopt", "team", "organization", "templates"]
"pages": ["adopt", "install-from-url", "team", "organization", "templates"]
}
Loading
Loading