diff --git a/.codex/bootstrap/tests-docs.v1.json b/.codex/bootstrap/tests-docs.v1.json new file mode 100644 index 0000000..3491d92 --- /dev/null +++ b/.codex/bootstrap/tests-docs.v1.json @@ -0,0 +1,8 @@ +{ + "contract": "tests-docs-bootstrap-v1", + "adapter": "node-ts", + "branch": "codex/bootstrap-tests-docs-v1", + "generated_at": "2026-02-17T05:41:48.224Z", + "generated_by": "/Users/d/.codex/scripts/bootstrap/global_tests_docs_bootstrap.mjs", + "changed_files": [] +} diff --git a/.codex/prompts/test-critic.md b/.codex/prompts/test-critic.md new file mode 100644 index 0000000..6c29c8c --- /dev/null +++ b/.codex/prompts/test-critic.md @@ -0,0 +1,14 @@ +You are a QA Test Critic reviewing only changed files and related tests. + +Review criteria: +1. Tests assert behavior outcomes, not implementation details. +2. Each changed behavior includes edge/error/boundary coverage. +3. Mocks are used only at external boundaries. +4. UI tests cover loading/empty/error/success and disabled/focus-visible states. +5. Assertions would fail under realistic regressions. +6. Flag brittle selectors, snapshot spam, and tautological assertions. +7. Flag missing docs updates for API/command or architecture changes. + +Output: +- Emit ReviewFindingV1 findings only. +- Priority order: critical, high, medium, low. diff --git a/.codex/scripts/run_verify_commands.sh b/.codex/scripts/run_verify_commands.sh new file mode 100755 index 0000000..aef497b --- /dev/null +++ b/.codex/scripts/run_verify_commands.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERIFY_FILE="${1:-.codex/verify.commands}" +if [[ ! -f "$VERIFY_FILE" ]]; then + echo "missing verify commands file: $VERIFY_FILE" >&2 + exit 1 +fi + +failed=0 +while IFS= read -r cmd || [[ -n "$cmd" ]]; do + [[ -z "$cmd" ]] && continue + [[ "$cmd" =~ ^# ]] && continue + echo ">>> $cmd" + if ! bash -lc "$cmd"; then + failed=1 + break + fi +done < "$VERIFY_FILE" + +exit "$failed" diff --git a/.codex/verify.commands b/.codex/verify.commands new file mode 100644 index 0000000..4d72142 --- /dev/null +++ b/.codex/verify.commands @@ -0,0 +1,7 @@ +pnpm lint +pnpm typecheck +pnpm test:coverage +pnpm test:integration +pnpm test:e2e:smoke +pnpm docs:generate +pnpm docs:check diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml new file mode 100644 index 0000000..0dd204e --- /dev/null +++ b/.github/workflows/quality-gates.yml @@ -0,0 +1,46 @@ +name: quality-gates + +on: + pull_request: + push: + branches: [main, master] + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v5 + with: + node-version: 22 + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Policy checks + run: node scripts/ci/require-tests-and-docs.mjs + + - name: Verify commands + run: bash .codex/scripts/run_verify_commands.sh + + - name: Diff coverage + run: | + python -m pip install --upgrade pip diff-cover + diff-cover coverage/lcov.info --compare-branch=origin/main --fail-under=90 + + - name: Upload test artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-artifacts + path: | + playwright-report/ + test-results/ + coverage/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4bdead5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,13 @@ +## Definition of Done: Tests + Docs (Blocking) + +- Any production code change must include meaningful test updates in the same PR. +- Meaningful tests must include at least: + - one primary behavior assertion + - two non-happy-path assertions (edge, boundary, invalid input, or failure mode) +- Trivial assertions are forbidden (`expect(true).toBe(true)`, snapshot-only without semantic assertions, render-only smoke tests without behavior checks). +- Mock only external boundaries (network, clock, randomness, third-party SDKs). Do not mock the unit under test. +- UI changes must cover state matrix: loading, empty, error, success, disabled, focus-visible. +- API/command surface changes must update generated contract artifacts and request/response examples. +- Architecture-impacting changes must include an ADR in `/docs/adr/`. +- Required checks are blocking when `fail` or `not-run`: lint, typecheck, tests, coverage, diff coverage, docs check. +- Reviewer -> fixer -> reviewer loop is required before merge. diff --git a/README.md b/README.md index a13ea3d..d08e548 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,49 @@ npm run test:pack-qa npm run tauri dev ``` +### Lean Dev (low disk mode) + +Use this when you want to keep local disk usage down while developing: + +```bash +npm install +npm run dev:lean +``` + +What `npm run dev:lean` does: +- Starts the app with the normal `npm run tauri dev` flow. +- Redirects heavy build caches to temporary directories (Cargo target + Vite cache). +- Automatically removes temporary caches and heavy build artifacts when you exit the app. +- Prints before/after disk usage snapshots for major bloat paths. + +Disk vs speed tradeoff: +- Lean mode uses less persistent disk. +- Lean mode is usually slower on next startup because caches are rebuilt. +- Normal `npm run tauri dev` keeps caches in-repo for faster repeated starts. + +### Cleanup commands + +Target heavy build artifacts only: + +```bash +npm run clean:heavy +``` + +This removes: +- `dist/` +- `artifacts/` +- `src-tauri/target/` +- `node_modules/.vite/` + +Full local reproducible cleanup: + +```bash +npm run clean:all +``` + +This runs `clean:heavy` and also removes: +- `node_modules/` + ## Verification ```bash diff --git a/docs/adr/0000-template.md b/docs/adr/0000-template.md new file mode 100644 index 0000000..f48132d --- /dev/null +++ b/docs/adr/0000-template.md @@ -0,0 +1,16 @@ +# 0000. Title + +## Status +Proposed | Accepted | Superseded + +## Context +What problem or constraint forced this decision? + +## Decision +What was chosen? + +## Consequences +What improves, what tradeoffs are accepted, what risks remain? + +## Alternatives Considered +Option A, Option B, and why they were rejected. diff --git a/openapi/openapi.generated.json b/openapi/openapi.generated.json new file mode 100644 index 0000000..3e752f6 --- /dev/null +++ b/openapi/openapi.generated.json @@ -0,0 +1,9 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "API Contract", + "version": "1.0.0" + }, + "paths": {}, + "components": {} +} diff --git a/package.json b/package.json index 0869460..fcce13b 100644 --- a/package.json +++ b/package.json @@ -3,19 +3,22 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "vite", - "build": "tsc && vite build", + "dev": "node ./node_modules/vite/bin/vite.js", + "dev:lean": "bash ./scripts/lean-dev.sh", + "build": "node ./node_modules/typescript/bin/tsc && node ./node_modules/vite/bin/vite.js build", + "clean:heavy": "bash ./scripts/clean-heavy.sh", + "clean:all": "bash ./scripts/clean-all.sh", "check:performance-budget": "node ./scripts/check_performance_budget.mjs", "test:pack-qa": "node ./scripts/pack_qa.mjs", - "test": "vitest run", - "test:watch": "vitest", - "test:ui": "vitest --ui", - "test:coverage": "vitest run --coverage", + "test": "node ./node_modules/vitest/vitest.mjs run", + "test:watch": "node ./node_modules/vitest/vitest.mjs", + "test:ui": "node ./node_modules/vitest/vitest.mjs --ui", + "test:coverage": "node ./node_modules/vitest/vitest.mjs run --coverage", "test:rust": "cargo test --manifest-path src-tauri/Cargo.toml", "test:rust:release": "cargo test --manifest-path src-tauri/Cargo.toml --release", "test:all": "npm test && npm run test:rust", "test:smoke": "./scripts/tauri-smoke.sh", - "tauri": "tauri", + "tauri": "node ./node_modules/@tauri-apps/cli/tauri.js", "test:tauri-preflight": "./scripts/tauri-preflight.sh" }, "dependencies": { diff --git a/scripts/ci/require-tests-and-docs.mjs b/scripts/ci/require-tests-and-docs.mjs new file mode 100644 index 0000000..6509f6a --- /dev/null +++ b/scripts/ci/require-tests-and-docs.mjs @@ -0,0 +1,41 @@ +import { execSync } from 'node:child_process'; + +const defaultBaseRef = (() => { + try { + return execSync('git symbolic-ref refs/remotes/origin/HEAD', { encoding: 'utf8' }).trim().replace('refs/remotes/', ''); + } catch { + return 'origin/main'; + } +})(); + +const baseRef = process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : defaultBaseRef; +const diff = execSync(`git diff --name-only ${baseRef}...HEAD`, { encoding: 'utf8' }) + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + +const isProdCode = (file) => /^(src|app|server|api)\//.test(file) && !/\.(test|spec)\.[cm]?[jt]sx?$/.test(file); +const isTest = (file) => /^tests\//.test(file) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(file); +const isDoc = (file) => /^docs\//.test(file) || /^openapi\//.test(file) || file === 'README.md'; +const isApiSurface = (file) => /^(src|app|server|api)\/.*(route|controller|handler|webhook|api|command)/.test(file); +const isArchChange = (file) => /^src\/(auth|db|infra|queue|events|architecture)\//.test(file) || /^infra\//.test(file); +const isAdr = (file) => /^docs\/adr\/\d{4}-.*\.md$/.test(file); + +const prodChanged = diff.some(isProdCode); +const testsChanged = diff.some(isTest); +const apiChanged = diff.some(isApiSurface); +const docsChanged = diff.some(isDoc); +const archChanged = diff.some(isArchChange); +const adrChanged = diff.some(isAdr); + +const failures = []; +if (prodChanged && !testsChanged) failures.push('Policy failure: production code changed without test updates.'); +if (apiChanged && !docsChanged) failures.push('Policy failure: API/command changes without docs/OpenAPI updates.'); +if (archChanged && !adrChanged) failures.push('Policy failure: architecture-impacting change without ADR.'); + +if (failures.length > 0) { + for (const failure of failures) console.error(failure); + process.exit(1); +} + +console.log('Policy checks passed.'); diff --git a/scripts/clean-all.sh b/scripts/clean-all.sh new file mode 100755 index 0000000..2b4a246 --- /dev/null +++ b/scripts/clean-all.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +"$ROOT_DIR/scripts/clean-heavy.sh" + +ALL_TARGETS=( + "$ROOT_DIR/node_modules" +) + +for target in "${ALL_TARGETS[@]}"; do + if [ -e "$target" ]; then + rm -rf "$target" + echo "Removed $target" + else + echo "Skipped $target (not present)" + fi +done diff --git a/scripts/clean-heavy.sh b/scripts/clean-heavy.sh new file mode 100755 index 0000000..98d56f2 --- /dev/null +++ b/scripts/clean-heavy.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +TARGETS=( + "$ROOT_DIR/dist" + "$ROOT_DIR/artifacts" + "$ROOT_DIR/src-tauri/target" + "$ROOT_DIR/node_modules/.vite" +) + +for target in "${TARGETS[@]}"; do + if [ -e "$target" ]; then + rm -rf "$target" + echo "Removed $target" + else + echo "Skipped $target (not present)" + fi +done diff --git a/scripts/lean-dev.sh b/scripts/lean-dev.sh new file mode 100755 index 0000000..f8bbd0f --- /dev/null +++ b/scripts/lean-dev.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LEAN_CACHE_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/desktop-pet-lean.XXXXXX")" + +export CARGO_TARGET_DIR="$LEAN_CACHE_ROOT/cargo-target" +export DESKTOPPET_VITE_CACHE_DIR="$LEAN_CACHE_ROOT/vite-cache" +mkdir -p "$CARGO_TARGET_DIR" "$DESKTOPPET_VITE_CACHE_DIR" + +print_size_report() { + local label="$1" + echo + echo "[$label] Disk usage snapshot" + for path in \ + "$ROOT_DIR/node_modules" \ + "$ROOT_DIR/node_modules/.vite" \ + "$ROOT_DIR/src-tauri/target" \ + "$ROOT_DIR/dist" \ + "$ROOT_DIR/artifacts" \ + "$CARGO_TARGET_DIR" \ + "$DESKTOPPET_VITE_CACHE_DIR"; do + if [ -e "$path" ]; then + du -sh "$path" + else + echo "0B $path (missing)" + fi + done +} + +cleanup() { + local exit_code=$? + echo + echo "lean-dev: cleaning temporary caches" + rm -rf "$LEAN_CACHE_ROOT" + + if "$ROOT_DIR/scripts/clean-heavy.sh"; then + true + else + echo "lean-dev: warning - heavy cleanup encountered an issue" >&2 + fi + + print_size_report "After cleanup" + exit "$exit_code" +} +trap cleanup EXIT INT TERM + +print_size_report "Before start" +echo + +echo "lean-dev: using temporary cache root $LEAN_CACHE_ROOT" +cd "$ROOT_DIR" +npm run tauri dev diff --git a/vite.config.ts b/vite.config.ts index 1063ac2..43bccc2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,10 +4,12 @@ import tailwindcss from "@tailwindcss/vite"; import { resolve } from "path"; const host = process.env.TAURI_DEV_HOST; +const leanCacheDir = process.env.DESKTOPPET_VITE_CACHE_DIR; export default defineConfig({ plugins: [react(), tailwindcss()], clearScreen: false, + cacheDir: leanCacheDir || "node_modules/.vite", server: { port: 5173, strictPort: true,