Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 4 additions & 1 deletion .agents/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# AI Guidance

This folder contains provider-neutral guidance for AI coding assistants working on `react-base`.
This folder contains provider-neutral guidance for AI coding assistants working on `kernel`.

The repository-root `AGENTS.md` is the discovery entry point. This directory
contains the detailed, provider-neutral rules and optional hook implementations.

## How to use these files

Expand Down
6 changes: 5 additions & 1 deletion .agents/hooks/post-edit-format.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#!/bin/bash
# Optional post-edit helper for AI tools that provide JSON input in $INPUT.
# Optional post-edit helper for AI tools that provide JSON on stdin or in $INPUT.
# It runs ESLint auto-fix on edited TypeScript/JavaScript files when available.

if [ -z "${INPUT:-}" ]; then
INPUT=$(cat)
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .file_path // empty' 2>/dev/null)

if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
Expand Down
9 changes: 7 additions & 2 deletions .agents/hooks/quality-gate.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash
# Optional quality gate for AI tools that support task-completion hooks.
# Runs lint and typecheck when tracked TypeScript files changed.
# Runs lint and typecheck when tracked or untracked TypeScript files changed.

PROJECT_DIR=${AI_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}

Expand All @@ -10,7 +10,12 @@ fi

cd "$PROJECT_DIR" || exit 0

MODIFIED=$(git diff --name-only --diff-filter=ACMR HEAD 2>/dev/null | grep -E '\.(ts|tsx)$' || true)
MODIFIED=$(
{
git diff --name-only --diff-filter=ACMR HEAD 2>/dev/null
git ls-files --others --exclude-standard 2>/dev/null
} | grep -E '\.(ts|tsx)$' | sort -u || true
)

if [ -z "$MODIFIED" ]; then
exit 0
Expand Down
4 changes: 3 additions & 1 deletion .agents/rules/code-quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ Run the smallest check that covers the files you changed, then broaden before ha
- Production build: `npm run build`
- Unused code check: `npm run knip`

For docs-only tasks, validation can usually be limited to `npm run lint` and `npm run knip`.
For docs-only tasks, run `npm run format:check` and validate links and referenced
commands. Code-only checks such as lint, typecheck, and Knip are unnecessary
unless configuration or source files also changed.

## Autofix workflow

Expand Down
28 changes: 14 additions & 14 deletions .agents/settings.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
{
"name": "react-base-ai-guidance",
"description": "Provider-neutral guidance for AI assistants working on react-base.",
"name": "kernel-ai-guidance",
"description": "Provider-neutral guidance for AI assistants working on Kernel.",
"rules": [
".ai/rules/code-quality.md",
".ai/rules/git.md",
".ai/rules/imports.md",
".ai/rules/ordering.md",
".ai/rules/react.md",
".ai/rules/safety.md",
".ai/rules/styling.md",
".ai/rules/testing.md",
".ai/rules/typescript.md"
".agents/rules/code-quality.md",
".agents/rules/git.md",
".agents/rules/imports.md",
".agents/rules/ordering.md",
".agents/rules/react.md",
".agents/rules/safety.md",
".agents/rules/styling.md",
".agents/rules/testing.md",
".agents/rules/typescript.md"
],
"optionalHooks": [
".ai/hooks/block-dangerous.sh",
".ai/hooks/post-edit-format.sh",
".ai/hooks/quality-gate.sh"
".agents/hooks/block-dangerous.sh",
".agents/hooks/post-edit-format.sh",
".agents/hooks/quality-gate.sh"
],
"recommendedChecks": [
"npm run typecheck",
Expand Down
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
*.css text eol=lf
*.html text eol=lf
*.js text eol=lf
*.mjs text eol=lf
*.json text eol=lf
*.md text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.sh text eol=lf
4 changes: 3 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Default reviewer for all repository changes.
* @sepaseh

# Require explicit review for automation and dependency policy changes.
# Suggest the repository owner for automation and dependency policy changes.
# Branch protection requires one approval but does not currently require that
# approval to come from a Code Owner.
/.github/ @sepaseh
/package.json @sepaseh
/package-lock.json @sepaseh
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ blob-report
*.sw?

# AI assistant local settings
.ai/settings.local.json
.agents/settings.local.json
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# AI assistant entry point

Before changing this repository, read `.agents/README.md` and the relevant files
under `.agents/rules/`. Treat `.agents/settings.json` as the machine-readable
index of those rules and optional hooks.

Use repository scripts for validation, preserve user changes, and never commit,
push, edit secrets, or contact production services without explicit user
authorization. Start with the smallest relevant check and follow
`CONTRIBUTING.md` before preparing a pull request.

The documentation map in `docs/README.md` identifies the source of truth for
each project subsystem. Update the corresponding document whenever behavior,
configuration, test coverage, or operational workflow changes.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ npm run knip # detect unused files, exports, and dependencies

## Environment

| Variable | Description | Fallback |
| ------------------- | ------------------------------------ | ----------------------- |
| `VITE_API_BASE_URL` | Backend HTTP API base URL | `http://<current-host>` |
| `VITE_APP_BASE_URL` | Router basename / deployed base path | empty string |
| Variable | Description | Fallback |
| ------------------------ | ----------------------------------------------- | ----------------------- |
| `VITE_API_BASE_URL` | Backend HTTP API base URL | `http://<current-host>` |
| `VITE_APP_BASE_URL` | Router basename / deployed base path | empty string |
| `VITE_OBSERVABILITY_URL` | Optional HTTPS event collector | disabled |
| `VITE_RELEASE_ID` | Immutable release identifier attached to events | `local` |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Example values are available in `.env.example`.

Expand Down Expand Up @@ -126,7 +128,9 @@ src/

## AI Guidance

Provider-neutral AI assistant guidance lives in `.agents/`. It documents project rules for code quality, imports, React, TypeScript, styling, testing, safety, and git workflow.
AI assistants should start with `AGENTS.md`; provider-neutral detailed guidance
lives in `.agents/`. The complete subsystem-to-document index is available in
[the documentation map](docs/README.md).

## Icons

Expand Down
27 changes: 27 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Documentation map

This page maps every maintained project area to its source of truth. A change
that introduces a new subsystem, workflow, environment variable, or public
contract must add or update an entry here.

| Area | Primary documentation | Implementation and configuration |
| -------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Setup, scripts, and local development | [Development](development.md) | `package.json`, `.nvmrc`, `.env.example`, Vite and TypeScript configs |
| Application structure and boundaries | [Architecture](architecture.md) | `src/app`, `src/features`, `src/layouts`, `src/shared`, `eslint.config.ts` |
| Routes, permissions, localization, theme, fonts, and storage | [Architecture](architecture.md) | `src/app`, `src/shared/config`, `src/shared/i18n`, `src/shared/storage`, `src/assets` |
| HTTP, authentication, token refresh, and endpoint contracts | [API client](api-client.md) | `src/shared/api`, feature `api.ts` and `types.ts` files |
| Unit, component, integration, browser, accessibility, visual, and Storybook testing | [Testing](testing.md) | Vitest, Playwright, `.storybook`, `e2e`, and `src/test` |
| Consumer contracts and mutation testing | [Mutation and contract testing](contract-testing.md) | `contract`, `vitest.contract.config.ts`, `stryker.config.json` |
| Build, environment validation, nginx, security headers, observability, and performance | [Deployment](deployment.md) | `vite.config.ts`, `nginx.conf`, `scripts`, `smoke` |
| Staging validation | [Staging](staging.md) | `staging`, `staging.config.ts`, staging workflow |
| CI checks and merge policy | [Branch protection](branch-protection.md) | CI and CodeQL workflows, `CODEOWNERS`, GitHub branch settings |
| Versioning and release automation | [Versioning and releases](releasing.md) | Release workflow, Release Please config, changelog and manifest |
| Production approval, promotion, rollback, and incident follow-up template | [Release operations](release-operations.md) | Deployment-smoke workflow and downstream environment controls |
| Static analysis | [SonarQube](sonarqube.md) | `sonar-project.properties` and advisory CI scan |
| Security policy and assurance | [Security policy](../SECURITY.md) and `security/` reviews | CodeQL and authorized DAST workflows, threat model |
| AI assistant rules and safety hooks | [AI guidance](../.agents/README.md) | `AGENTS.md`, `.agents/settings.json`, `.agents/rules`, `.agents/hooks` |

Generated output directories such as `dist`, `coverage`, `pacts`, Storybook,
Lighthouse, mutation, and Playwright reports are intentionally excluded from
version control. Their producers and retention behavior are documented in the
testing, contract-testing, deployment, and workflow documents above.
13 changes: 9 additions & 4 deletions docs/branch-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ Protection also:
- Prevents force-pushes.
- Prevents deletion of `main`.

An approving review is not required for ordinary changes while the repository
has only one active maintainer. Enable at least one approval and Code Owner
review when another maintainer can review pull requests without blocking all
development.
One approving review is required for every pull request. Code Owner review is
not currently a separate requirement, so any collaborator with review
permission may provide the approval. Enable required Code Owner review when
ownership must be enforced rather than used only for automatic reviewer
suggestions.

GitHub currently permits squash, merge-commit, and rebase merges. Squash merge
is preferred for ordinary changes because the pull-request title then becomes
the single Conventional Commit consumed by Release Please.
13 changes: 12 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ src/
server.ts # One shared Node interception server
e2e/
*.spec.ts # Playwright browser journeys
scripts/
*.test.mjs # Build-environment and bundle-budget gate tests
```

Keep tests next to production modules when they describe that module. Put
cross-cutting test infrastructure under `src/test`. Keep Playwright tests
separate because they run against the built application.
separate because they run against the built application. Tests beside Node
scripts exercise their exported validation logic without requiring a production
deployment or oversized generated artifacts.

## Commands

Expand All @@ -66,6 +70,13 @@ API and application base URLs for unit, component, and API integration tests.
These values take precedence over developer-specific `.env.local` settings, so
tests never depend on or contact a configured development backend.

Playwright builds its preview server with a separate deterministic environment
defined in `playwright.config.ts`: the application base is `/`, the release ID
is `e2e`, and the API origin is `https://api.example.com`. Browser tests
intercept that reserved example origin. These explicit values override
developer-specific `.env.local` settings and prevent local E2E runs from
contacting a configured backend.

Pull requests run the critical browser suite in Chromium. Firefox, WebKit,
mobile, and visual projects remain available for focused local checks when a
change warrants broader browser coverage. Install those browsers as needed:
Expand Down
6 changes: 6 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ export default defineConfig({
},
webServer: {
command: "npm run build && npm run preview -- --host 127.0.0.1 --port 4173",
env: {
...process.env,
VITE_API_BASE_URL: "https://api.example.com",
VITE_APP_BASE_URL: "/",
VITE_RELEASE_ID: "e2e",
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ignoreHTTPSErrors: true,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
Expand Down
85 changes: 54 additions & 31 deletions scripts/check-bundle-size.mjs
Original file line number Diff line number Diff line change
@@ -1,45 +1,68 @@
import console from "node:console";
import { readdir, stat } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const assetsDirectory = path.resolve("dist/assets");
const maximumChunkBytes = 450_000;
const maximumTotalBytes = 1_600_000;
export const maximumChunkBytes = 450_000;
export const maximumTotalBytes = 1_600_000;

const assetNames = await readdir(assetsDirectory);
const javascriptAssets = assetNames.filter((name) => name.endsWith(".js"));
export const evaluateBundleAssets = (assets) => {
if (assets.length === 0) throw new Error("No JavaScript assets found");

if (javascriptAssets.length === 0) {
throw new Error(`No JavaScript assets found in ${assetsDirectory}`);
}

const assets = await Promise.all(
javascriptAssets.map(async (name) => ({
bytes: (await stat(path.join(assetsDirectory, name))).size,
name,
})),
);
const oversizedAssets = assets.filter(({ bytes }) => bytes > maximumChunkBytes);
const totalBytes = assets.reduce((total, { bytes }) => total + bytes, 0);

if (oversizedAssets.length > 0 || totalBytes > maximumTotalBytes) {
const failures = oversizedAssets.map(
({ bytes, name }) =>
`${name} is ${bytes.toLocaleString()} bytes (limit ${maximumChunkBytes.toLocaleString()})`,
const oversizedAssets = assets.filter(
({ bytes }) => bytes > maximumChunkBytes,
);
const totalBytes = assets.reduce((total, { bytes }) => total + bytes, 0);

if (totalBytes > maximumTotalBytes) {
failures.push(
`Total JavaScript is ${totalBytes.toLocaleString()} bytes (limit ${maximumTotalBytes.toLocaleString()})`,
if (oversizedAssets.length > 0 || totalBytes > maximumTotalBytes) {
const failures = oversizedAssets.map(
({ bytes, name }) =>
`${name} is ${bytes.toLocaleString()} bytes (limit ${maximumChunkBytes.toLocaleString()})`,
);

if (totalBytes > maximumTotalBytes) {
failures.push(
`Total JavaScript is ${totalBytes.toLocaleString()} bytes (limit ${maximumTotalBytes.toLocaleString()})`,
);
}

throw new Error(`Bundle size budget exceeded:\n${failures.join("\n")}`);
}

throw new Error(`Bundle size budget exceeded:\n${failures.join("\n")}`);
}
return {
largestAsset: assets.toSorted((a, b) => b.bytes - a.bytes)[0],
totalBytes,
};
};

const largestAsset = assets.toSorted((a, b) => b.bytes - a.bytes)[0];
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const assetNames = await readdir(assetsDirectory);
const javascriptAssets = assetNames.filter((name) => name.endsWith(".js"));
const assets = await Promise.all(
javascriptAssets.map(async (name) => ({
bytes: (await stat(path.join(assetsDirectory, name))).size,
name,
})),
);
let result;

console.log(
`Bundle budgets passed: ${assets.length} chunks, ${totalBytes.toLocaleString()} bytes total, ` +
`${largestAsset.name} largest at ${largestAsset.bytes.toLocaleString()} bytes.`,
);
try {
result = evaluateBundleAssets(assets);
} catch (error) {
if (error.message === "No JavaScript assets found") {
throw new Error(`No JavaScript assets found in ${assetsDirectory}`, {
cause: error,
});
}
throw error;
}

const { largestAsset, totalBytes } = result;

console.log(
`Bundle budgets passed: ${assets.length} chunks, ${totalBytes.toLocaleString()} bytes total, ` +
`${largestAsset.name} largest at ${largestAsset.bytes.toLocaleString()} bytes.`,
);
}
40 changes: 40 additions & 0 deletions scripts/check-bundle-size.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";

import {
evaluateBundleAssets,
maximumChunkBytes,
maximumTotalBytes,
} from "./check-bundle-size.mjs";

describe("bundle size budgets", () => {
it("returns the total and largest JavaScript asset", () => {
expect(
evaluateBundleAssets([
{ bytes: 100, name: "small.js" },
{ bytes: 200, name: "large.js" },
]),
).toEqual({
largestAsset: { bytes: 200, name: "large.js" },
totalBytes: 300,
});
});

it("rejects an empty asset list", () => {
expect(() => evaluateBundleAssets([])).toThrow("No JavaScript assets");
});

it("rejects an oversized chunk", () => {
expect(() =>
evaluateBundleAssets([{ bytes: maximumChunkBytes + 1, name: "app.js" }]),
).toThrow("app.js");
});

it("rejects an oversized total", () => {
expect(() =>
evaluateBundleAssets([
{ bytes: maximumTotalBytes / 2 + 1, name: "a.js" },
{ bytes: maximumTotalBytes / 2, name: "b.js" },
]),
).toThrow("Total JavaScript");
});
});
Loading
Loading