From f7c8251f665bfe72efcc1b3f0ea628430b16a636 Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Wed, 23 Sep 2026 15:42:45 +0530 Subject: [PATCH 1/6] chore: remove inherited tooling that no longer applies - build.yml pushed to GCP Artifact Registry on bh-main/bh-dev, branches this fork does not have, so it never ran; workflow_dispatch would have run and failed - the governance doc and script describe a CODEOWNERS gate whose owners file and workflow are already gone - desgin/ was a misspelled directory carried since the initial commit - .gitignore now covers keys, so the untracked oan/oan.pub keypair in the working tree cannot be committed by accident Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 130 ------------ .gitignore | 8 + desgin/desgin.xml | 293 ---------------------------- docs/repo-governance-enforcement.md | 53 ----- scripts/apply_repo_governance.sh | 67 ------- 5 files changed, 8 insertions(+), 543 deletions(-) delete mode 100644 .github/workflows/build.yml delete mode 100644 desgin/desgin.xml delete mode 100644 docs/repo-governance-enforcement.md delete mode 100755 scripts/apply_repo_governance.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 03f9aa6..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,130 +0,0 @@ -name: Build and Push to GCP Artifact Registry - -on: - workflow_dispatch: - inputs: - target_environment: - description: Branch/environment name to use for image tags and GitHub environment - required: true - default: bh-dev - type: choice - options: - - bh-dev - - bh-main - push_image: - description: Push the built Docker image to Artifact Registry - required: true - default: true - type: boolean - push: - branches: - - bh-main - - bh-dev - pull_request: - branches: - - bh-main - - bh-dev - -env: - BASE_IMAGE_NAME: oan-ui - -jobs: - build: - runs-on: ubuntu-latest - environment: ${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || github.base_ref || github.ref_name }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - - - name: Install dependencies - run: | - rm -rf node_modules package-lock.json - npm install --legacy-peer-deps --include=optional --force - - - name: Build project - run: npm run build - env: - VITE_API_URL: ${{ vars.VITE_API_URL }} - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: dist - path: dist/ - retention-days: 7 - - docker-push: - runs-on: ubuntu-latest - needs: build - if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push_image) - environment: ${{ github.event_name == 'workflow_dispatch' && inputs.target_environment || github.base_ref || github.ref_name }} - - permissions: - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Get version from package.json - id: package-version - run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT - - - name: Set image name and tags based on branch - id: set-tags - run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - BRANCH="${{ inputs.target_environment }}" - else - BRANCH="${{ github.ref_name }}" - fi - - ENV_PREFIX=$(echo "$BRANCH" | cut -d'-' -f1) - BUILD_TYPE=$(echo "$BRANCH" | cut -d'-' -f2) - BRANCH_TAG=$(echo "$BRANCH" | sed 's/\//-/g') - - echo "IMAGE_NAME=${{ env.BASE_IMAGE_NAME }}" >> $GITHUB_OUTPUT - echo "BUILD_TAG=${BUILD_TYPE}" >> $GITHUB_OUTPUT - echo "BRANCH_TAG=${BRANCH_TAG}" >> $GITHUB_OUTPUT - echo "LATEST_TAG=${BUILD_TYPE}-latest" >> $GITHUB_OUTPUT - - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: dist - path: dist/ - - - name: Authenticate to Google Cloud - id: auth - uses: google-github-actions/auth@v2 - with: - credentials_json: ${{ secrets.GCP_SA_KEY }} - - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v2 - - - name: Configure Docker for Artifact Registry - run: gcloud auth configure-docker ${{ secrets.GCP_REGION }}-docker.pkg.dev --quiet - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and Push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: | - ${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.GCP_ARTIFACT_REPO }}/${{ steps.set-tags.outputs.IMAGE_NAME }}:${{ steps.set-tags.outputs.LATEST_TAG }} - ${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.GCP_ARTIFACT_REPO }}/${{ steps.set-tags.outputs.IMAGE_NAME }}:${{ steps.package-version.outputs.VERSION }}-${{ steps.set-tags.outputs.BUILD_TAG }} - ${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.GCP_ARTIFACT_REPO }}/${{ steps.set-tags.outputs.IMAGE_NAME }}:${{ github.sha }} - ${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.GCP_ARTIFACT_REPO }}/${{ steps.set-tags.outputs.IMAGE_NAME }}:${{ steps.set-tags.outputs.BRANCH_TAG }} - ${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.GCP_ARTIFACT_REPO }}/${{ steps.set-tags.outputs.IMAGE_NAME }}:${{ steps.set-tags.outputs.BUILD_TAG }} - platforms: linux/amd64 - no-cache: true diff --git a/.gitignore b/.gitignore index e458663..b61bdcb 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,11 @@ coverage/ # TypeScript incremental build info tsconfig.tsbuildinfo + +# Keys and certificates. Nothing of this shape belongs in the repository. +*.pem +*.key +*.p12 +*.pfx +oan +oan.pub diff --git a/desgin/desgin.xml b/desgin/desgin.xml deleted file mode 100644 index ab5bed3..0000000 --- a/desgin/desgin.xml +++ /dev/null @@ -1,293 +0,0 @@ - - - - - - - - - - Upload weekly weather advisory file - PDF, Excel, JSON - - - - - - Extracts structured records from uploaded file - - PDF - Excel (.xlsx) - JSON - - - - - - - Validates required fields before processing - - location_name - from_date - to_date - advisory_body - - - - - - - - - - Raw location text extracted from file - Nashik, Maharashtra - - - - - - OpenStreetMap geocoding service - https://nominatim.openstreetmap.org - /search?q={location}&format=json&addressdetails=1 - - district - subdistrict - state - lat - lon - geo_code - - - - - - - Combines Nominatim response with advisory record - - district - subdistrict - lat - lon - geo_code - - - - - - - - - Merges geo data with advisory content into one record - - - - - - Stamps each record with active or expired status based on current date - - if today >= from_date AND today <= to_date then status = active - if today > to_date then status = expired - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Emits event after advisory is saved to DB - weather_advisory - On successful expiry-tagger completion in Pipeline A - - - - - - Admin manually creates and publishes notifications - feature_launch - Admin clicks publish in console - - - - - - Scheduled or automated system events - - maintenance - system_alert - - Scheduled job or system condition - - - - - - - - Decouples producers from consumers - - - - - - - null means broadcast to all users - - - - - - - - - - - Subscribes to all bus topics and routes by event type - - - - - - Core validation before delivery - - User geo_code must match event target_geo at state/district/subdistrict level - today >= from_date AND today <= to_date - If target_geo is null, skip location match — send to all users - - - - - - - - - Resolves current user location to a geo_code - - Device GPS - IP geolocation - Saved profile location - - - - - - - Persists a notification record per eligible user - - - - - - - - - - - - - - - - - - - - - - - Shows unread notification count badge - WebSocket preferred, HTTP polling fallback - - - - - - - Dropdown listing all notifications grouped by type - - Group by event type - Mark individual as read - Mark all as read - Dismiss / delete - - - - - - - Full content of the selected notification - - Renders advisory body with date range and location - Renders feature description with CTA button - Renders maintenance window details - Renders alert message with severity - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/repo-governance-enforcement.md b/docs/repo-governance-enforcement.md deleted file mode 100644 index cb37c74..0000000 --- a/docs/repo-governance-enforcement.md +++ /dev/null @@ -1,53 +0,0 @@ -# OpenAgriNet Governance Enforcement - -This repository enforces the policy in `Openagrinet-repo-ownership-and-pr-governance.pdf` with: - -1. `CODEOWNERS` ownership mapping. -2. Pull request template checklist. -3. Automated PR governance checks in GitHub Actions. -4. Scripted branch protection for protected branches. - -## What Is Enforced In-Repo - -- Owners are defined in `.github/CODEOWNERS`. -- PRs into protected branches are validated by `.github/workflows/pr-governance.yml` for: - - branch name pattern (`feature/`, `bugfix/`, `hotfix/`, `chore/`) - - no self-approval - - at least one peer approval - - at least one CODEOWNERS approval - - re-approval on latest commit - - all review threads resolved - -## Apply GitHub Branch Protection - -Run: - -```bash -bash scripts/apply_repo_governance.sh -``` - -Optional explicit target repo: - -```bash -bash scripts/apply_repo_governance.sh / -``` - -### Script Requirements - -- `gh` installed and authenticated (`gh auth login`) -- admin access to the target repository -- `jq` installed - -### Script Applies These Settings - -For branches that exist among `main`, `master`, `mh-main`, `bh-main`: - -- pull-request reviews required (minimum 1 approval) -- CODEOWNERS review required -- stale approvals dismissed on new commits -- last push requires approval from someone else -- all conversations must be resolved -- force pushes disabled -- branch deletions disabled -- admin enforcement enabled -- required status check: `PR Governance Gate / governance` diff --git a/scripts/apply_repo_governance.sh b/scripts/apply_repo_governance.sh deleted file mode 100755 index 9f72dbc..0000000 --- a/scripts/apply_repo_governance.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REPO="${1:-}" -if [[ -z "$REPO" ]]; then - REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)" -fi - -if ! command -v gh >/dev/null 2>&1; then - echo "Error: GitHub CLI (gh) is required." - exit 1 -fi - -if ! command -v jq >/dev/null 2>&1; then - echo "Error: jq is required." - exit 1 -fi - -if ! gh auth status >/dev/null 2>&1; then - echo "Error: gh is not authenticated. Run: gh auth login" - exit 1 -fi - -PROTECTED_BRANCHES=("main" "master" "mh-main" "bh-main") -STATUS_CONTEXTS='["PR Governance Gate / governance"]' - -echo "Applying OpenAgriNet governance to ${REPO}..." -for branch in "${PROTECTED_BRANCHES[@]}"; do - resolved_branch="$(gh api "repos/${REPO}/branches/${branch}" --jq .name 2>/dev/null || true)" - if [[ "${resolved_branch}" != "${branch}" ]]; then - echo "Skipping missing branch: ${branch}" - continue - fi - - payload="$( - jq -n \ - --argjson contexts "${STATUS_CONTEXTS}" \ - '{ - required_status_checks: { - strict: true, - contexts: $contexts - }, - enforce_admins: true, - required_pull_request_reviews: { - dismiss_stale_reviews: true, - require_code_owner_reviews: true, - required_approving_review_count: 1, - require_last_push_approval: true - }, - restrictions: null, - required_linear_history: false, - allow_force_pushes: false, - allow_deletions: false, - required_conversation_resolution: true - }' - )" - - gh api \ - --method PUT \ - -H "Accept: application/vnd.github+json" \ - "repos/${REPO}/branches/${branch}/protection" \ - --input - <<<"${payload}" >/dev/null - - echo "Protected branch updated: ${branch}" -done - -echo "Governance branch protection applied." From 1280f461db3d583236dd35858c2f0a8b817628a6 Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Wed, 23 Sep 2026 15:46:16 +0530 Subject: [PATCH 2/6] chore: run the full check set pre-commit and enforce the message format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pre-commit gains knip and the test run. All four finish in a few seconds, so the hook stays usable; build stays out of it. commit-msg is new. It checks the subject against the convention rather than leaving it described in a file nobody opens. The issue number is encouraged but not required — requiring it blocks commits that have no tracker issue, and what people write then is [#0]. Merge, revert and fixup subjects are skipped, since their messages are generated. Three things had to be fixed to make knip a gate rather than noise: - it was never a declared dependency; it had been run through bunx, so `bun run knip` could not find it - src/routes.ts is loaded by vite.config.ts as a string path, invisible to static analysis, so knip called it and @tanstack/virtual-file-routes dead. It is now declared as an entry point - knip does not follow CSS imports, so tailwindcss, tw-animate-css and the typography plugin looked unused despite being imported by global.css The gate covers dead files and dependency drift. Unused exports are reported by `bunx knip` but not gated: src/components/ui is a vendored component kit kept as a whole API, and 35 of its exports would fail every run. Both hooks now set -e themselves. Husky's shim runs them with `sh -e`, so this changed nothing under git — but run directly the script executed every check and returned only the last one's status, which is how a failing lint could have looked like a pass. README says how to check whether hooks are active at all. core.hooksPath is local to a clone and is not committed, so anyone who installed dependencies before a hook existed has none and is not told. Co-Authored-By: Claude Opus 5 (1M context) --- .husky/commit-msg | 37 +++++++++++++++ .husky/pre-commit | 11 ++++- README.md | 29 ++++++++++++ bun.lock | 115 ++++++++++++++++++++++++++++++++++++++++++++++ knip.json | 21 +++++++++ package.json | 3 ++ 6 files changed, 215 insertions(+), 1 deletion(-) create mode 100755 .husky/commit-msg mode change 100644 => 100755 .husky/pre-commit create mode 100644 knip.json diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..db91498 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,37 @@ +#!/bin/sh +set -e + +# Enforces the commit convention in CONVENTIONS.md: +# +# : [#] +# +# The issue number is encouraged but not required. Requiring it would block +# commits that have no tracker issue — tooling changes, typo fixes — and what +# people do then is write [#0], which is worse than nothing. + +subject=$(head -n 1 "$1") + +# Generated messages are not ours to police. +case "$subject" in + "Merge "*|"Revert "*|"fixup! "*|"squash! "*|"amend! "*) exit 0 ;; +esac + +if ! printf '%s' "$subject" | grep -Eq '^(feat|fix|refactor|chore|test|docs|build|ci|perf)(\(.+\))?: .{3,}$'; then + echo "commit-msg: the subject line does not match the convention." + echo + echo " got: $subject" + echo " expected: : [#]" + echo + echo " type is one of: feat fix refactor chore test docs build ci perf" + echo " scope is optional: fix(auth): ..." + echo " issue number is optional but encouraged: fix: stop the flicker [#42]" + echo + echo "See CONVENTIONS.md. To bypass deliberately: git commit --no-verify" + exit 1 +fi + +# Long subjects are a smell, not an error. +length=$(printf '%s' "$subject" | wc -c | tr -d ' ') +if [ "$length" -gt 72 ]; then + echo "commit-msg: subject is ${length} characters; 72 or fewer reads better in git log." +fi diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 index e152ee9..1733198 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,9 +1,18 @@ #!/bin/sh +# Husky's shim runs this with `sh -e`, but set it here too so the script is +# correct when run directly — without it every check runs and only the last +# one's exit status counts. +set -e + +# Fast feedback only. CI runs the same checks and is the actual gate, because +# a local hook can be skipped with --no-verify. if ! command -v bun >/dev/null 2>&1; then - echo "Bun not found, skipping checks" + echo "husky: bun not found, skipping checks" exit 0 fi bun run lint bun run typecheck +bun run knip +bun run test diff --git a/README.md b/README.md index a534d32..e347c6e 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,35 @@ labelled as stubbed so fabricated agricultural advice cannot be mistaken for real. Set `stubs.enabled` to `false` to talk to a real backend. See [`src/lib/api-stubs.ts`](src/lib/api-stubs.ts). +## Git hooks + +The hooks live in `.husky/` and are activated by a single local git setting, +`core.hooksPath`. Installing dependencies sets it, because `package.json` runs +`husky` from its `prepare` script: + +```bash +bun install +``` + +Check that yours are actually active: + +```bash +git config core.hooksPath # should print .husky/_ +``` + +If that prints nothing, the hooks are not running. Fix it with `bunx husky`. +This is worth checking rather than assuming: `core.hooksPath` is local to your +clone and is not committed, so anyone who installed dependencies before a hook +existed silently has no hooks. + +| Hook | Runs | +|---|---| +| `pre-commit` | lint, typecheck, knip, tests | +| `commit-msg` | checks the subject against the convention in [`CONVENTIONS.md`](CONVENTIONS.md) | + +Hooks are fast feedback, not the gate — they can be skipped with +`--no-verify`, so CI runs the same checks. + ## Deployment The image is a static build served by nginx. It expects to sit behind something diff --git a/bun.lock b/bun.lock index dd85d3e..f26e24d 100644 --- a/bun.lock +++ b/bun.lock @@ -54,6 +54,7 @@ "globals": "^16.5.0", "happy-dom": "^20.14.5", "husky": "^9.1.7", + "knip": "^6.37.0", "postcss": "^8.5.28", "prettier": "^3.9.8", "prettier-plugin-tailwindcss": "^0.7.4", @@ -115,6 +116,12 @@ "@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="], + "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], @@ -223,8 +230,86 @@ "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], + + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.150.0", "", { "os": "android", "cpu": "arm" }, "sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw=="], + + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.150.0", "", { "os": "android", "cpu": "arm64" }, "sha512-B6ofpoFiAUwIZ0MJ2IgHPvZK8FAtL4qzSZRwOkAKIfxEobE1mQC8nDdiFZj7pUaJiVUVCsfkMsBJKedzO8rZvA=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.150.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-J+9IHKzx/bSz1JetOfD4zKXSK9sOm4/a7+0qomJODcTL6JsRXGeV/ZdkAPSIDydfFmWzYCraMlQEosSZEyBYkQ=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.150.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-v6IPfcAcSYrBWXV5Tce1DmxDMXLhEYpIIWiRFPJopgCVscZdLaV4MRQOUxvL3usQlw+yrwYOjBFB9IwBx+jUiQ=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.150.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-AoR/4jD02HET0KO0yNT4nbTE+XJUxiO9jB1X/ycEjUE3WrIJ3IjV/Vf+gskhWLcqqeXfvNnkDtBR7epllAm88A=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.150.0", "", { "os": "linux", "cpu": "arm" }, "sha512-A/hycCFjLUrCmLoL5O/vBp40ohlaXO1Ta3v5hqYZxicYFs129wZmgZJggcW4mYGYbZebQLhup+N8JjeQl8qwyw=="], + + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.150.0", "", { "os": "linux", "cpu": "arm" }, "sha512-pbqahg1Pkz7J4RKmXm30s/iQu99hv64ayXCu8P4p75HcEH5azOdR4eGqiB6lFGkFR3mMpzaYsSKLkS8oJbjmeQ=="], + + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.150.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HV11aRbBQGwqv8bEo+K/6qr88uB4fEe1mhXncMhlVCrj+WpBSraxrUzHaPVmeQRU6x6x8v/3DuCbbo93rpp+fw=="], + + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.150.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-k6pVkJqALwtEuP4zukVhGRhdIy4+ofChUIUUsAHHyqDZlNsknmel3JjbggrJiWGaes6h/dIcWmEXsKW4SmK9oQ=="], + + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.150.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tEFg39mw/rHO5n8GK2DR4ZMFfsPQZtnQIPbsIpjoQRomJc/2tRfsCSmCT6gNit+NZGoeJG5aH9ATDH5bf0WnoQ=="], + + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.150.0", "", { "os": "linux", "cpu": "none" }, "sha512-0JO7IFkoek6HqV589Menx3hJySNXi6quRhO4tq2P7kpEHKEXoDATnoPVUpn5B7gDH2KLkdo751N0uIrGJFCTCw=="], + + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.150.0", "", { "os": "linux", "cpu": "none" }, "sha512-7XPzREnyAS5wHU9aB+49Uaqgoo1gVCklHc3DRlc3fJy2tXD/eAJFN1QFz/crj+Ulup5P1+axNREF+/RGaB/IRA=="], + + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.150.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-7n7ZxcbRWDFaTdyj8M8p0O9OfzoMKm8O6syBAIgcSutbOALq0Bb9G52Me+XH0anMLZV+NgXc726gqgG2q39vcQ=="], + + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.150.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Vx0GSA9ZCRTUiczoEQnIIyXkMvtEY8uc6IiwLQtMe5ci2sXPqjrry0Ek5fsPP1k2kCzkML3ow9UOOEgnEDi7Bw=="], + + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.150.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ii4/9m3viDLssMnfXU7/Pni/3nYplApuGayceX4qsVGaqqQJCx3tHIH9M/HWIeSty5i8LjS21zPYT6lVIkwLxw=="], + + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.150.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ZtoxX5rez36ZWkwtiEhjkCPnlTPoQ2I7lIL0IYZ1yjxMYohbvoOjBmUIZzrf9W/HouONq0omIfTeCWEY+R380w=="], + + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.150.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-VqeRb5JX/bKYBrff7TUDvJyKY0914UzuCkuXIGxJS4FUmvMNfqkCbc7Sq90iMz9DlmAtlggwaBYQ9eg3h3ltiw=="], + + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.150.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EPqJfeZ4Pgg2BJSsCV8GozxV0FDltRzpVtGVa1r2noJu1iu+opOw3gtBWXwIo9odCT/MdFfyHxdy0/CRqs3OqA=="], + + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.150.0", "", { "os": "win32", "cpu": "x64" }, "sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw=="], + "@oxc-project/types": ["@oxc-project/types@0.150.0", "", {}, "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw=="], + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="], + + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg=="], + + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w=="], + + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q=="], + + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw=="], + + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg=="], + + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA=="], + + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA=="], + + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw=="], + + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA=="], + + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw=="], + + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w=="], + + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ=="], + + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg=="], + + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw=="], + + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg=="], + + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.24.2", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q=="], + + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg=="], + + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw=="], + "@radix-ui/number": ["@radix-ui/number@1.1.3", "", {}, "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], @@ -495,6 +580,8 @@ "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], @@ -881,6 +968,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -899,6 +988,8 @@ "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], + "formatly": ["formatly@0.7.1", "", { "dependencies": { "fd-package-json": "^2.0.0", "package-manager-detector": "^1.8.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-ESuCEJE0SGgabl9q+zABI8LhJDmF+8nhpuqDzuM48HfJ9QTD5o6PqFJDFZY7w6f8xsh5lixuj5nJsQNvYOWvKQ=="], + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -921,6 +1012,8 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], @@ -1089,6 +1182,8 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "knip": ["knip@6.37.0", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.7.0", "get-tsconfig": "4.14.3", "jiti": "^2.7.0", "oxc-parser": "^0.150.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.7", "smol-toml": "^1.8.0", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^4.0.11", "yaml": "^2.9.1", "zod": "^4.4.3" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-NKCYuoGKJ+o2AuoCuV1mDvfMYnSSWfDXqxwmzxeHyd8fftjNRl8TBUg3fbSrtJjhm7Vj6sc1PX3WZnZhECs9EQ=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1289,10 +1384,16 @@ "own-keys": ["own-keys@1.0.2", "", { "dependencies": { "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg=="], + "oxc-parser": ["oxc-parser@0.150.0", "", { "dependencies": { "@oxc-project/types": "^0.150.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.150.0", "@oxc-parser/binding-android-arm64": "0.150.0", "@oxc-parser/binding-darwin-arm64": "0.150.0", "@oxc-parser/binding-darwin-x64": "0.150.0", "@oxc-parser/binding-freebsd-x64": "0.150.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.150.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.150.0", "@oxc-parser/binding-linux-arm64-gnu": "0.150.0", "@oxc-parser/binding-linux-arm64-musl": "0.150.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.150.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.150.0", "@oxc-parser/binding-linux-riscv64-musl": "0.150.0", "@oxc-parser/binding-linux-s390x-gnu": "0.150.0", "@oxc-parser/binding-linux-x64-gnu": "0.150.0", "@oxc-parser/binding-linux-x64-musl": "0.150.0", "@oxc-parser/binding-openharmony-arm64": "0.150.0", "@oxc-parser/binding-win32-arm64-msvc": "0.150.0", "@oxc-parser/binding-win32-ia32-msvc": "0.150.0", "@oxc-parser/binding-win32-x64-msvc": "0.150.0" } }, "sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg=="], + + "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -1443,6 +1544,8 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "ripemd160": ["ripemd160@2.0.3", "", { "dependencies": { "hash-base": "^3.1.2", "inherits": "^2.0.4" } }, "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA=="], "rolldown": ["rolldown@1.2.9", "", { "dependencies": { "@oxc-project/types": "=0.150.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.9", "@rolldown/binding-android-arm64": "1.2.9", "@rolldown/binding-darwin-arm64": "1.2.9", "@rolldown/binding-darwin-x64": "1.2.9", "@rolldown/binding-freebsd-x64": "1.2.9", "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", "@rolldown/binding-linux-arm64-gnu": "1.2.9", "@rolldown/binding-linux-arm64-musl": "1.2.9", "@rolldown/binding-linux-ppc64-gnu": "1.2.9", "@rolldown/binding-linux-s390x-gnu": "1.2.9", "@rolldown/binding-linux-x64-gnu": "1.2.9", "@rolldown/binding-linux-x64-musl": "1.2.9", "@rolldown/binding-openharmony-arm64": "1.2.9", "@rolldown/binding-win32-arm64-msvc": "1.2.9", "@rolldown/binding-win32-x64-msvc": "1.2.9" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg=="], @@ -1491,6 +1594,8 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "smol-toml": ["smol-toml@1.9.0", "", {}, "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ=="], + "solid-js": ["solid-js@1.9.15", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg=="], "solid-presence": ["solid-presence@0.2.0", "", { "dependencies": { "@corvu/utils": "~0.4.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-YM92o+jvpzX3XGaD4rLYmq/Kc2ZVh47GSCLEufHBFQQIurvZTs8SoGJxO8BJGNDxBKdcS8F3dYhW1SDXp4BNjA=="], @@ -1599,6 +1704,8 @@ "typescript-eslint": ["typescript-eslint@8.70.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.70.1", "@typescript-eslint/parser": "8.70.1", "@typescript-eslint/typescript-estree": "8.70.1", "@typescript-eslint/utils": "8.70.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA=="], + "unbash": ["unbash@4.0.11", "", {}, "sha512-FoSOKV7NEofQSkAefMVHam4ZPKYMxjAydxiV72UFEDNV/YofxjGfiZ2A9pZjdL/lRJzTjcu4PABo1JYJX8N5iQ=="], + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], @@ -1647,6 +1754,8 @@ "vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="], + "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], @@ -1671,6 +1780,8 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -1751,6 +1862,10 @@ "eslint-plugin-react-hooks/zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="], + "knip/strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + + "knip/zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="], + "make-dir/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "md5.js/hash-base": ["hash-base@3.1.2", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^2.3.8", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.1" } }, "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg=="], diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..0b92df8 --- /dev/null +++ b/knip.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://unjs.io/schema/knip.json", + + // src/routes.ts is loaded by vite.config.ts as a string path, so no static + // analysis reaches it. Declared as an entry point or knip calls it dead. + "entry": ["src/routes.ts", "src/__tests__/**/*.test.ts"], + "project": ["src/**/*.{ts,tsx}"], + + // knip does not follow CSS imports, so anything referenced only from + // global.css looks unused: tailwindcss, tw-animate-css and the typography + // plugin are all @import-ed or @plugin-ed there. The rest are tooling + // dependencies resolved by config files rather than by source. + "ignoreDependencies": [ + "tailwindcss", + "tw-animate-css", + "@tailwindcss/typography", + "eslint-config-prettier", + "baseline-browser-mapping", + "caniuse-lite" + ] +} diff --git a/package.json b/package.json index 684964e..8229649 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "dev": "vite --host 0.0.0.0", "build": "tsc -b && vite build", "lint": "eslint .", + "knip": "knip --include files,dependencies,unlisted,unresolved", "preview": "vite preview", + "test": "vitest run", "test:coverage": "vitest run --coverage", "prepare": "husky", "typecheck": "tsc -b", @@ -70,6 +72,7 @@ "globals": "^16.5.0", "happy-dom": "^20.14.5", "husky": "^9.1.7", + "knip": "^6.37.0", "postcss": "^8.5.28", "prettier": "^3.9.8", "prettier-plugin-tailwindcss": "^0.7.4", From 2fc91858e0d4db8cabffb78369df5c9165dd4684 Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Wed, 23 Sep 2026 15:52:12 +0530 Subject: [PATCH 3/6] ci: add check, security and image workflows Three workflows, matching the shape decision-support-system and knowledge-provider already use, so a reader moving between OAN repos finds the same layout. - ci.yml: install (frozen), lint, typecheck, knip, test with coverage, build - security.yml: Trivy scan of bun.lock, gating on fixable HIGH/CRITICAL. Passes clean today - build-and-push.yml: the same checks, then an amd64 image, a report-only Trivy image scan, and a push to ghcr.io/openagrinet/experience-ui. PRs build but never publish Two checks the sibling repos have no equivalent for, both for things that have already cost this repo: - the route tree must be current. The Docker build cannot regenerate it, because oven/bun:1-alpine has no Node for the TanStack Router plugin, so a stale committed copy would ship a wrong route tree silently - a bundle budget: 6 MB for dist, 2 MB for any single file. This client shipped an 8.66 MB SVG to every visitor for its whole life upstream, and a budget is what would have caught it on the commit that added it. dist is 3.3 MB today, largest asset 832 KB Coverage is printed, not uploaded. No threshold: the suite is small, and a number invented now would either be trivially met or fail on the first honest commit. The image scan is report-only on purpose. A base-image CVE with no fix available should not block a deploy; security.yml is where an actionable finding stops things. Verified locally: both guards pass, and the Trivy gate reports zero vulnerabilities against bun.lock. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build-and-push.yml | 121 +++++++++++++++++++++++++++ .github/workflows/ci.yml | 82 ++++++++++++++++++ .github/workflows/security.yml | 54 ++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 .github/workflows/build-and-push.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/security.yml diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml new file mode 100644 index 0000000..0322da5 --- /dev/null +++ b/.github/workflows/build-and-push.yml @@ -0,0 +1,121 @@ +name: Build and Push + +# Triggers on push to main, or on a release tag, per OAN convention: +# v{SemVer}, optionally with an -rcN pre-release suffix. +on: + pull_request: + push: + branches: + - main + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" + +permissions: + contents: read + packages: write + +jobs: + # The same checks as ci.yml, gating the image build directly rather than + # trusting that the other workflow happened to pass. + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + + - name: Typecheck + run: bun run typecheck + + - name: Dead code + run: bun run knip + + - name: Test + run: bun run test + + build: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Trivy cache date + id: trivy-date + run: echo "date=$(date -u +%F)" >> "$GITHUB_OUTPUT" + + - name: Cache Trivy binary and vulnerability DB + uses: actions/cache@v4 + with: + path: | + bin/trivy + ~/.cache/trivy + key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ steps.trivy-date.outputs.date }} + restore-keys: | + trivy-${{ runner.os }}-${{ runner.arch }}- + + - name: Install Trivy (if not cached) + run: | + if [ ! -x bin/trivy ]; then + mkdir -p bin + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b bin + fi + + - uses: docker/setup-buildx-action@v3 + + # Skipped on a PR: that leg only builds, for early Dockerfile feedback, + # and never publishes. Falls back to GITHUB_TOKEN if GHCR_PAT is unset. + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT || secrets.GITHUB_TOKEN }} + + - name: Set image owner + run: echo "OWNER=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + + - name: Set image tag + run: echo "TAG=$(echo '${{ github.ref_name }}' | tr '/' '-')" >> "$GITHUB_ENV" + + # Loaded locally first so Trivy scans the exact image that gets pushed. + - name: Build (local, unpushed) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64 + load: true + tags: experience-ui:${{ env.TAG }} + + # Report-only. It does not fail the build: a base-image CVE with no fix + # available should not block a deploy, and the dependency gate in + # security.yml is where an actionable finding stops things. + - name: Trivy image scan (report) + run: | + ./bin/trivy image \ + --exit-code 0 \ + --severity HIGH,CRITICAL \ + --ignore-unfixed \ + --no-progress \ + "experience-ui:${{ env.TAG }}" + + - name: Tag and push + if: github.event_name != 'pull_request' + run: | + BASE="ghcr.io/${OWNER}/experience-ui" + docker tag "experience-ui:${TAG}" "${BASE}:${TAG}" + docker tag "experience-ui:${TAG}" "${BASE}:${GITHUB_SHA}" + docker tag "experience-ui:${TAG}" "${BASE}:latest" + docker push "${BASE}:${TAG}" + docker push "${BASE}:${GITHUB_SHA}" + docker push "${BASE}:latest" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7e4faa2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,82 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + # --frozen-lockfile, so a dependency added without committing bun.lock + # fails here rather than resolving to something different in CI. + - name: Install + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + + - name: Typecheck + run: bun run typecheck + + # Dead files and dependency drift. Unused exports are deliberately not + # gated — see knip.json. + - name: Dead code + run: bun run knip + + # Coverage is printed here rather than uploaded anywhere. There is no + # threshold yet: the suite is small, and a number invented now would + # either be trivially met or fail on the first honest commit. + - name: Test + run: bunx vitest run --coverage + + - name: Build + run: bun run build + + # The Docker build cannot regenerate this file — oven/bun:1-alpine has no + # Node for the TanStack Router plugin to run under — so a stale committed + # copy would ship a wrong route tree with nothing to catch it. + - name: Route tree is current + run: | + if ! git diff --quiet -- src/routeTree.gen.ts; then + echo "::error file=src/routeTree.gen.ts::Route tree is stale. Run 'bun run build' and commit the result." + git diff -- src/routeTree.gen.ts + exit 1 + fi + + # This client shipped an 8.66 MB SVG to every visitor for its whole life + # upstream. The budget is what would have caught it on the commit that + # added it, rather than months later. + - name: Bundle budget + run: | + TOTAL_KB=$(du -sk dist | cut -f1) + MAX_TOTAL_KB=6144 + MAX_ASSET_KB=2048 + + echo "dist total: ${TOTAL_KB} KB (budget ${MAX_TOTAL_KB} KB)" + if [ "$TOTAL_KB" -gt "$MAX_TOTAL_KB" ]; then + echo "::error::dist is ${TOTAL_KB} KB, over the ${MAX_TOTAL_KB} KB budget." + du -ak dist | sort -rn | head -20 + exit 1 + fi + + OVERSIZE=$(find dist -type f -size +${MAX_ASSET_KB}k) + if [ -n "$OVERSIZE" ]; then + echo "::error::These files are over the ${MAX_ASSET_KB} KB per-asset budget:" + echo "$OVERSIZE" | xargs -I{} du -h {} + exit 1 + fi diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..47ee3aa --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,54 @@ +name: Security + +# Scans bun.lock for known CVEs, before anything is built into an image. +# build-and-push.yml has its own Trivy step that scans the built image. +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + dependency-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Trivy cache date + id: trivy-date + run: echo "date=$(date -u +%F)" >> "$GITHUB_OUTPUT" + + - name: Cache Trivy binary and vulnerability DB + uses: actions/cache@v4 + with: + path: | + bin/trivy + ~/.cache/trivy + key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ steps.trivy-date.outputs.date }} + restore-keys: | + trivy-${{ runner.os }}-${{ runner.arch }}- + + - name: Install Trivy (if not cached) + run: | + if [ ! -x bin/trivy ]; then + mkdir -p bin + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b bin + fi + + # --ignore-unfixed is what keeps this gate liveable: it only fires when an + # upgrade is actually available, so a red run is always actionable. + - name: Trivy dependency scan + run: | + ./bin/trivy fs \ + --scanners vuln \ + --exit-code 1 \ + --severity HIGH,CRITICAL \ + --ignore-unfixed \ + --no-progress \ + . From 2e9605fbdf5b26f7b671ae6953ec5655d026694d Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Wed, 23 Sep 2026 15:53:56 +0530 Subject: [PATCH 4/6] docs: add AGENTS.md and CONVENTIONS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches the layout decision-support-system and knowledge-provider already use, with CLAUDE.md a symlink to AGENTS.md, so someone moving between OAN repos finds the same two files. Both are adapted rather than copied. The cross-repo rules carry over — git workflow, commit format, PR shape, writing style. Everything Python is replaced: ruff becomes eslint and knip, the pytest tiers become the much smaller Vitest story, and the hexagonal layout becomes this repo's actual folder structure. The gotchas section is the part worth reading. Every entry in it has already cost time here: routes.ts being invisible to static analysis, the dynamic import in main.tsx that configuration ordering depends on, Vite refusing JS imports from public/, विस्तार meaning "area" so brand find-and-replace corrupts copy, and npm being unable to install this tree at all. Two sections are deliberately absent. There is no versioning section: package.json says 0.0.0, the settings drawer hardcodes 1.0.0, and a convention describing neither would be fiction. Formatting is documented as not enforced rather than quietly implied, since Prettier is installed but unconfigured. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 137 +++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + CONVENTIONS.md | 95 ++++++++++++++++++++++++++++++++++ README.md | 2 + 4 files changed, 235 insertions(+) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 CONVENTIONS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f2dbf27 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,137 @@ +# experience-ui + +## Service Overview +The reference web client for the OpenAgriNet (OAN) **Experience Layer**. The DSS +answers agricultural questions and holds no interface of its own; this is the +screen its capability reaches a person through. It talks to the Experience API, +which calls the DSS. It is meant to be read and copied by adopters, not deployed +as a product. + +## Architecture & decision reference +Do not duplicate architecture or decision detail here — it goes stale +immediately. The authoritative sources are: +- **`docs/ADR/`** — accepted architecture decisions. `0000-fork-oan-ui.md` covers + why this is a fork of OAN-UI, what the client is today, which features are + switched off and what each is waiting on. + +**Write an ADR for new tech-stack or design-direction decisions.** Any choice +with real trade-offs — a framework, a state library, an API shape, how identity +is carried — gets a new `docs/ADR/NNNN-title.md` in the existing format +(context, decision drivers, considered options, decision outcome, consequences). +Don't make load-bearing decisions silently in code or a PR description. + +**Keep ADR-0000 §6 current.** It lists what works and what is switched off. When +a feature flag flips, update it in the same change. + +## Tech Stack +- Language: TypeScript +- UI: React 19, Vite +- Routing: TanStack Router, virtual file routes (`src/routes.ts`) +- State: Zustand +- Styling: Tailwind CSS v4, shadcn/ui components in `src/components/ui` +- Package manager: **Bun only.** npm cannot resolve this dependency tree without + `--legacy-peer-deps`; Bun resolves it with no flags +- Tests: Vitest, with happy-dom where a test needs a real `document` +- Dead code: knip +- Serving: nginx in a container (`Dockerfile`, `nginx.conf`) + +## Build & Run +``` +bun install # also activates git hooks, via the prepare script +bun dev # http://localhost:3000 +bun run lint # eslint +bun run typecheck # tsc -b +bun run knip # dead files and dependency drift +bun run test # vitest +bun run build # tsc -b && vite build +docker compose up --build # the deployable image, http://localhost:8080 +``` + +There is no Experience API yet. The client ships with stubs on +(`stubs.enabled` in config), so chat works end to end with canned replies, each +labelled as stubbed. + +## Conventions +Naming, git workflow, commit format, PR shape and linting are in +[`CONVENTIONS.md`](./CONVENTIONS.md). Read it before naming anything, writing a +commit, or opening a PR. + +## Writing Style +Applies to ADRs, PR descriptions, code comments, and any other document. +- Simple words, short sentences. +- No verbosity — say only what's needed. +- Write for a non-technical reader — a product owner, anyone without a software + background. + +## Folder Structure + +``` +src/ +├── main.tsx # Entry. Loads config, applies branding, THEN dynamically +│ # imports bootstrap. See the gotcha below — this order is +│ # load-bearing. +├── bootstrap.tsx # Mounts React. Anything reachable from here may read config +│ # at import time. +├── routes.ts # Virtual route definitions, read by vite.config.ts. +├── routeTree.gen.ts # Generated AND committed. See gotchas. +│ +├── config/ # app-config.json — the bundled default, and the file the +│ # Vite plugin serves and emits as /config.json. +├── lib/ +│ ├── config/ # runtime-config.ts (fetch + merge + getConfig), +│ │ # features.ts (flags), branding.ts (title, favicon). +│ ├── auth/ # The identity seam. Returns a fixed local user and +│ │ # verifies nothing. How a credential reaches the API is +│ │ # deliberately undecided. +│ ├── api-service.ts # The client's entire server coupling. One file. +│ └── api-stubs.ts # Canned responses. Only chat is reachable today. +│ +├── components/ +│ ├── ui/ # shadcn/ui. Vendored, kept as a whole API — knip reports +│ │ # its unused exports and the gate deliberately ignores them. +│ └── screens-component/ +├── hooks/store/chat/ # The chat store. Zustand. +├── layouts/ +└── __tests__/ +``` + +## Testing Patterns +- Framework: Vitest. Tests live in `src/__tests__/`. +- Default environment is Node. A test needing a real `document` opts in with a + `@vitest-environment happy-dom` docblock. +- **Test behaviour, not configuration.** A test asserting that a config file + contains what the config file contains fails whenever a default legitimately + changes and teaches people to ignore red. Two such tests were written and + removed here; don't add more. +- What is worth a test: the coercion rules (anything other than boolean `true` + reads as off), the config merge, the fallback when `config.json` is + unreachable, and anything where storage and configuration disagree — the + English pinning test exists because `localStorage` beating a flag is a real + failure mode. + +## Known Gotchas + +- **`src/routes.ts` is loaded by `vite.config.ts` as a string path.** No static + analysis reaches it. It is declared as a knip entry point for this reason; + without that, knip calls it and `@tanstack/virtual-file-routes` dead. +- **`routeTree.gen.ts` is generated and committed, and must stay committed.** + The Docker build cannot regenerate it — `oven/bun:1-alpine` has no Node for + the router plugin. CI has a guard. +- **Configuration is read at module-init time**, so `main.tsx` loads config and + then *dynamically* imports `bootstrap.tsx`. A static import would evaluate the + whole graph first and `getConfig()` would throw. The `.then()` chain is not + stylistic. +- **Assets in `public/` cannot be imported from JavaScript.** That is why + `config.json` is emitted by a Vite plugin from `src/config/app-config.json`, + so one file serves as both the bundled fallback and the deployment override. +- **`विस्तार` / `વિસ્તાર` is the ordinary word for *area*** in Hindi, Marathi and + Gujarati. Find-and-replace on brand names corrupts legitimate copy. Read every + occurrence. +- **npm cannot install this repo** without `--legacy-peer-deps`; it crashes with + `Cannot read properties of null (reading 'edgesOut')`. Bun is the only + supported package manager, and `bun.lock` is the only lockfile. +- **knip's unused-export list is not a task list.** `src/components/ui` is a + vendored component kit kept whole. The gate covers files and dependencies only. +- **Verify with `bun dev` as well as `bun run build`.** A Vite rule about + `public/` imports once shipped in a PR that built and served correctly from + `dist/` but broke the dev server. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CONVENTIONS.md b/CONVENTIONS.md new file mode 100644 index 0000000..e4babea --- /dev/null +++ b/CONVENTIONS.md @@ -0,0 +1,95 @@ +# Conventions + +Repo-wide conventions for naming, git workflow, and linting. Referenced from +`AGENTS.md`. Aligned with the other OAN repositories where the rule is not +language-specific. + +## Naming Conventions + +### TypeScript +| Artifact | Convention | Example | +|---|---|---| +| Component file | `kebab-case.tsx` | `chat-input.tsx` | +| Component | `PascalCase` | `ChatInput` | +| Hook | `useCamelCase` | `useChatStore` | +| Function | `camelCase` verb | `loadRuntimeConfig` | +| Constant | `UPPER_SNAKE_CASE` | `APP_NAME`, `FEATURES` | +| Type / interface | `PascalCase`, no `I` prefix | `AppConfig` | + +### Configuration +- `config.json` keys are `camelCase`, matching the TypeScript that reads them. +- Feature flags are named for the capability, not the control: `voiceInput`, + not `showMicButton`. The flag outlives the button. +- A flag is `true` or `false`. Anything else reads as off — deliberately, so a + deployment writing `"true"` cannot enable a surface with no backend. + +### Repository naming +kebab-case, specific to the capability. No `oan-` or `dpg-` prefix. + +## Git Workflow + +### Branch naming +`{type}/{issue-no}-{short-description}`, with the issue number where one exists. + +``` +feat/42-runtime-config +chore/devx +``` + +### Commit messages +`: [#]` + +The issue number makes commits grep-able: `git log --grep="#42"`. It is +encouraged but not required — a tooling change or a typo fix often has no issue, +and requiring one produces `[#0]`. + +Scopes are optional. Don't define them speculatively. + +| Type | When | +|---|---| +| feat | New capability | +| fix | Bug fix | +| refactor | No behaviour change | +| chore | Tooling, dependencies | +| test | Tests only | +| docs | Docs only | +| build | Packaging, the container image | +| ci | Workflows | +| perf | Performance, no behaviour change | + +`.husky/commit-msg` enforces the subject line. To bypass deliberately: +`git commit --no-verify`. + +**Add a body only when *why* is not obvious from the diff.** The subject is +usually the whole message. When a change turned on something surprising — a +coupling, a failure that would have been silent, a decision with a real +alternative — say so, because `git log` is where the next person looks. Long +design reasoning belongs in an ADR, not here. + +### Pull requests +- Title follows the commit convention. +- Body says what changed, why, and how it was verified. +- End with `Relates to #` where an issue exists — it links without + auto-closing on merge. + +State what was actually run. "Verified by rendering the built image" and "should +work" are different claims and a reviewer cannot tell them apart afterwards. + +## Linting & Formatting +- `eslint` for linting, `tsc -b` for types, `knip` for dead code. +- Formatting is **not** currently enforced. Prettier is installed but + unconfigured; the codebase is tab-indented and Prettier defaults to spaces, so + turning it on means either a matching config or a repo-wide reformat. Until + that is decided, match the surrounding file. +- pre-commit runs lint, typecheck, knip and tests. Hooks can be bypassed with + `--no-verify`, so CI runs the same checks and is the real gate. +- CI additionally runs the build, a route-tree freshness guard and a bundle + budget. + +## Dependencies +- **Bun only.** One lockfile, `bun.lock`. npm cannot resolve this tree. +- `bun install --frozen-lockfile` in CI, so a dependency added without + committing the lockfile fails there rather than silently resolving to + something else. +- A new dependency needs a reason in the commit body. This repo is a reference + implementation; every dependency is one an adopter inherits. diff --git a/README.md b/README.md index e347c6e..e83deb7 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,8 @@ happens to expire. Content-hashed assets under `/assets/` are cached for a year; | Doc | Contents | | --- | --- | | [`docs/ADR/`](docs/ADR/) | Accepted architecture decisions | +| [`AGENTS.md`](AGENTS.md) | What this repo is, its stack, layout, testing patterns and gotchas | +| [`CONVENTIONS.md`](CONVENTIONS.md) | Naming, git workflow, commit format, linting | ## License From 2b42e4d4335c292a91aecb7e9acd859b5d697b42 Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Wed, 23 Sep 2026 15:58:14 +0530 Subject: [PATCH 5/6] ci: chain the jobs instead of running three workflows at once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split across ci.yml, security.yml and build-and-push.yml, all three fired simultaneously on every push, and build-and-push.yml re-ran the lint, typecheck, knip and test steps ci.yml had already run. That duplication came from copying the sibling repos' layout without questioning it. Now one workflow with a job graph: check ──┬──→ build ──┬──→ image └──→ security ┘ check is the fastest failure signal, so everything waits on it: a broken type no longer costs a Docker build. build and security run alongside each other because a vulnerable dependency and an oversized bundle are independent failures and both are worth seeing. Nothing is published until all of it passes. This diverges from decision-support-system and knowledge-provider, which keep three separate workflows and carry the same duplicate test job. The job graph seemed worth more than matching a layout. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build-and-push.yml | 121 --------------------- .github/workflows/ci.yml | 151 ++++++++++++++++++++++++++- .github/workflows/security.yml | 54 ---------- CONVENTIONS.md | 6 +- 4 files changed, 154 insertions(+), 178 deletions(-) delete mode 100644 .github/workflows/build-and-push.yml delete mode 100644 .github/workflows/security.yml diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml deleted file mode 100644 index 0322da5..0000000 --- a/.github/workflows/build-and-push.yml +++ /dev/null @@ -1,121 +0,0 @@ -name: Build and Push - -# Triggers on push to main, or on a release tag, per OAN convention: -# v{SemVer}, optionally with an -rcN pre-release suffix. -on: - pull_request: - push: - branches: - - main - tags: - - "v[0-9]+.[0-9]+.[0-9]+" - - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" - -permissions: - contents: read - packages: write - -jobs: - # The same checks as ci.yml, gating the image build directly rather than - # trusting that the other workflow happened to pass. - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install - run: bun install --frozen-lockfile - - - name: Lint - run: bun run lint - - - name: Typecheck - run: bun run typecheck - - - name: Dead code - run: bun run knip - - - name: Test - run: bun run test - - build: - needs: check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Trivy cache date - id: trivy-date - run: echo "date=$(date -u +%F)" >> "$GITHUB_OUTPUT" - - - name: Cache Trivy binary and vulnerability DB - uses: actions/cache@v4 - with: - path: | - bin/trivy - ~/.cache/trivy - key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ steps.trivy-date.outputs.date }} - restore-keys: | - trivy-${{ runner.os }}-${{ runner.arch }}- - - - name: Install Trivy (if not cached) - run: | - if [ ! -x bin/trivy ]; then - mkdir -p bin - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b bin - fi - - - uses: docker/setup-buildx-action@v3 - - # Skipped on a PR: that leg only builds, for early Dockerfile feedback, - # and never publishes. Falls back to GITHUB_TOKEN if GHCR_PAT is unset. - - name: Log in to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GHCR_PAT || secrets.GITHUB_TOKEN }} - - - name: Set image owner - run: echo "OWNER=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" - - - name: Set image tag - run: echo "TAG=$(echo '${{ github.ref_name }}' | tr '/' '-')" >> "$GITHUB_ENV" - - # Loaded locally first so Trivy scans the exact image that gets pushed. - - name: Build (local, unpushed) - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfile - platforms: linux/amd64 - load: true - tags: experience-ui:${{ env.TAG }} - - # Report-only. It does not fail the build: a base-image CVE with no fix - # available should not block a deploy, and the dependency gate in - # security.yml is where an actionable finding stops things. - - name: Trivy image scan (report) - run: | - ./bin/trivy image \ - --exit-code 0 \ - --severity HIGH,CRITICAL \ - --ignore-unfixed \ - --no-progress \ - "experience-ui:${{ env.TAG }}" - - - name: Tag and push - if: github.event_name != 'pull_request' - run: | - BASE="ghcr.io/${OWNER}/experience-ui" - docker tag "experience-ui:${TAG}" "${BASE}:${TAG}" - docker tag "experience-ui:${TAG}" "${BASE}:${GITHUB_SHA}" - docker tag "experience-ui:${TAG}" "${BASE}:latest" - docker push "${BASE}:${TAG}" - docker push "${BASE}:${GITHUB_SHA}" - docker push "${BASE}:latest" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e4faa2..c0fd26a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,20 @@ name: CI +# One workflow, one job graph: +# +# check ──┬──→ build ──┬──→ image +# └──→ security ┘ +# +# Split across three workflows these would all start at once, and the image +# workflow would repeat every check the first had already run. Chaining them +# means a broken type never costs a Docker build, and nothing is run twice. on: + pull_request: push: branches: [main] - pull_request: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -11,8 +22,10 @@ concurrency: permissions: contents: read + packages: write jobs: + # Fastest failure signal, so it goes first and everything else waits on it. check: runs-on: ubuntu-latest steps: @@ -44,6 +57,19 @@ jobs: - name: Test run: bunx vitest run --coverage + build: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install + run: bun install --frozen-lockfile + - name: Build run: bun run build @@ -80,3 +106,126 @@ jobs: echo "$OVERSIZE" | xargs -I{} du -h {} exit 1 fi + + # Scans bun.lock for known CVEs. Runs alongside build rather than after it: + # a vulnerable dependency and a broken bundle are independent failures and + # you want to see both, but neither is worth running if the code does not + # compile. + security: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Trivy cache date + id: trivy-date + run: echo "date=$(date -u +%F)" >> "$GITHUB_OUTPUT" + + - name: Cache Trivy binary and vulnerability DB + uses: actions/cache@v4 + with: + path: | + bin/trivy + ~/.cache/trivy + key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ steps.trivy-date.outputs.date }} + restore-keys: | + trivy-${{ runner.os }}-${{ runner.arch }}- + + - name: Install Trivy (if not cached) + run: | + if [ ! -x bin/trivy ]; then + mkdir -p bin + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b bin + fi + + # --ignore-unfixed is what keeps this gate liveable: it only fires when an + # upgrade is actually available, so a red run is always actionable. + - name: Trivy dependency scan + run: | + ./bin/trivy fs \ + --scanners vuln \ + --exit-code 1 \ + --severity HIGH,CRITICAL \ + --ignore-unfixed \ + --no-progress \ + . + + # Nothing is published until the code compiles, the bundle is within budget + # and the dependencies are clean. + image: + needs: [build, security] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Trivy cache date + id: trivy-date + run: echo "date=$(date -u +%F)" >> "$GITHUB_OUTPUT" + + - name: Cache Trivy binary and vulnerability DB + uses: actions/cache@v4 + with: + path: | + bin/trivy + ~/.cache/trivy + key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ steps.trivy-date.outputs.date }} + restore-keys: | + trivy-${{ runner.os }}-${{ runner.arch }}- + + - name: Install Trivy (if not cached) + run: | + if [ ! -x bin/trivy ]; then + mkdir -p bin + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b bin + fi + + - uses: docker/setup-buildx-action@v3 + + # Skipped on a PR: that leg only builds, for early Dockerfile feedback, + # and never publishes. Falls back to GITHUB_TOKEN if GHCR_PAT is unset. + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT || secrets.GITHUB_TOKEN }} + + - name: Set image owner + run: echo "OWNER=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + + - name: Set image tag + run: echo "TAG=$(echo '${{ github.ref_name }}' | tr '/' '-')" >> "$GITHUB_ENV" + + # Loaded locally first so Trivy scans the exact image that gets pushed. + - name: Build (local, unpushed) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + platforms: linux/amd64 + load: true + tags: experience-ui:${{ env.TAG }} + + # Report-only. It does not fail the build: a base-image CVE with no fix + # available should not block a deploy, and the dependency scan above is + # where an actionable finding stops things. + - name: Trivy image scan (report) + run: | + ./bin/trivy image \ + --exit-code 0 \ + --severity HIGH,CRITICAL \ + --ignore-unfixed \ + --no-progress \ + "experience-ui:${{ env.TAG }}" + + - name: Tag and push + if: github.event_name != 'pull_request' + run: | + BASE="ghcr.io/${OWNER}/experience-ui" + docker tag "experience-ui:${TAG}" "${BASE}:${TAG}" + docker tag "experience-ui:${TAG}" "${BASE}:${GITHUB_SHA}" + docker tag "experience-ui:${TAG}" "${BASE}:latest" + docker push "${BASE}:${TAG}" + docker push "${BASE}:${GITHUB_SHA}" + docker push "${BASE}:latest" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index 47ee3aa..0000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Security - -# Scans bun.lock for known CVEs, before anything is built into an image. -# build-and-push.yml has its own Trivy step that scans the built image. -on: - push: - branches: [main] - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - dependency-scan: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Trivy cache date - id: trivy-date - run: echo "date=$(date -u +%F)" >> "$GITHUB_OUTPUT" - - - name: Cache Trivy binary and vulnerability DB - uses: actions/cache@v4 - with: - path: | - bin/trivy - ~/.cache/trivy - key: trivy-${{ runner.os }}-${{ runner.arch }}-${{ steps.trivy-date.outputs.date }} - restore-keys: | - trivy-${{ runner.os }}-${{ runner.arch }}- - - - name: Install Trivy (if not cached) - run: | - if [ ! -x bin/trivy ]; then - mkdir -p bin - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b bin - fi - - # --ignore-unfixed is what keeps this gate liveable: it only fires when an - # upgrade is actually available, so a red run is always actionable. - - name: Trivy dependency scan - run: | - ./bin/trivy fs \ - --scanners vuln \ - --exit-code 1 \ - --severity HIGH,CRITICAL \ - --ignore-unfixed \ - --no-progress \ - . diff --git a/CONVENTIONS.md b/CONVENTIONS.md index e4babea..169d68d 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -83,8 +83,10 @@ work" are different claims and a reviewer cannot tell them apart afterwards. that is decided, match the surrounding file. - pre-commit runs lint, typecheck, knip and tests. Hooks can be bypassed with `--no-verify`, so CI runs the same checks and is the real gate. -- CI additionally runs the build, a route-tree freshness guard and a bundle - budget. +- CI additionally runs the build, a route-tree freshness guard, a bundle budget, + a dependency vulnerability scan and the container image. Its jobs are chained + — `check` first, then `build` and `security`, then `image` — so a broken type + never costs a Docker build and no check runs twice. ## Dependencies - **Bun only.** One lockfile, `bun.lock`. npm cannot resolve this tree. From ef8ea16ca6ef83b8c8ca80e865c4d8da36ae7bbb Mon Sep 17 00:00:00 2001 From: RohanReddy44 Date: Wed, 23 Sep 2026 15:59:06 +0530 Subject: [PATCH 6/6] ci: build the image only on main and release tags A pull request runs check, build and security. `bun run build` is what catches a build error there, and building a container image the run will never publish costs a couple of minutes on every push to every branch. The job-level condition replaces the two per-step guards on the login and push steps, which were the only thing stopping a PR from publishing. Gating the job says the same thing in one place, and makes it harder to add a publishing step later that forgets its own guard. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 18 ++++++++++++------ CONVENTIONS.md | 8 +++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0fd26a..226fb4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,12 +2,16 @@ name: CI # One workflow, one job graph: # -# check ──┬──→ build ──┬──→ image +# check ──┬──→ build ──┬──→ image (main and tags only) # └──→ security ┘ # # Split across three workflows these would all start at once, and the image # workflow would repeat every check the first had already run. Chaining them -# means a broken type never costs a Docker build, and nothing is run twice. +# means a broken type never costs a build, and nothing is run twice. +# +# A pull request runs check, build and security. `bun run build` is what +# catches a build error there; the container image is only built on main and +# on release tags, where it is also published. on: pull_request: push: @@ -150,9 +154,14 @@ jobs: --no-progress \ . + # Only on main and release tags. A pull request has already had its build + # checked by the `build` job, and building an image it will never publish + # costs a couple of minutes on every push to every branch. + # # Nothing is published until the code compiles, the bundle is within budget # and the dependencies are clean. image: + if: github.event_name != 'pull_request' needs: [build, security] runs-on: ubuntu-latest steps: @@ -181,10 +190,8 @@ jobs: - uses: docker/setup-buildx-action@v3 - # Skipped on a PR: that leg only builds, for early Dockerfile feedback, - # and never publishes. Falls back to GITHUB_TOKEN if GHCR_PAT is unset. + # Falls back to GITHUB_TOKEN if GHCR_PAT is unset. - name: Log in to GHCR - if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io @@ -220,7 +227,6 @@ jobs: "experience-ui:${{ env.TAG }}" - name: Tag and push - if: github.event_name != 'pull_request' run: | BASE="ghcr.io/${OWNER}/experience-ui" docker tag "experience-ui:${TAG}" "${BASE}:${TAG}" diff --git a/CONVENTIONS.md b/CONVENTIONS.md index 169d68d..e458914 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -84,9 +84,11 @@ work" are different claims and a reviewer cannot tell them apart afterwards. - pre-commit runs lint, typecheck, knip and tests. Hooks can be bypassed with `--no-verify`, so CI runs the same checks and is the real gate. - CI additionally runs the build, a route-tree freshness guard, a bundle budget, - a dependency vulnerability scan and the container image. Its jobs are chained - — `check` first, then `build` and `security`, then `image` — so a broken type - never costs a Docker build and no check runs twice. + a dependency vulnerability scan, and on main and release tags the container + image. Its jobs are chained — `check` first, then `build` and `security`, + then `image` — so a broken type never costs a build and no check runs twice. + A pull request stops after `build`: `bun run build` is what catches a build + error there, and an image nobody will publish is not worth the minutes. ## Dependencies - **Bun only.** One lockfile, `bun.lock`. npm cannot resolve this tree.