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/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..226fb4f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,237 @@ +name: CI + +# One workflow, one job graph: +# +# 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 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: + branches: [main] + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +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: + - 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 + + 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 + + # 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 + + # 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 \ + . + + # 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: + - 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 + + # Falls back to GITHUB_TOKEN if GHCR_PAT is unset. + - name: Log in to GHCR + 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 + 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/.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/.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/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..e458914 --- /dev/null +++ b/CONVENTIONS.md @@ -0,0 +1,99 @@ +# 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, a bundle budget, + 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. +- `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 a534d32..e83deb7 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 @@ -126,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 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/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/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", 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."