From db3264c1b3de0bdaa7d8a17109f2e148e0872462 Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Sun, 9 Aug 2026 12:29:07 +0530 Subject: [PATCH 1/3] Improve grounded project discovery --- .fleet/design-review.json | 18 +- .../core-discovery-hardening/after-1440.png | Bin 0 -> 67877 bytes .../core-discovery-hardening/after-390.png | Bin 0 -> 53894 bytes .../core-discovery-hardening/after-768.png | Bin 0 -> 61633 bytes .../core-discovery-hardening/before-1440.png | Bin 0 -> 16202 bytes .../2026-08-09-projects-shared-app-shell.json | 59 ++ .github/workflows/ci.yml | 6 +- AGENTS.md | 10 + PROJECT_STATUS.md | 37 +- README.md | 5 +- docs/architecture/data-flow.md | 3 +- docs/development/commands.md | 1 + docs/development/testing.md | 12 +- docs/operations/ci-cd.md | 6 +- docs/operations/data-map.md | 2 +- docs/product/features.md | 19 +- docs/product/overview.md | 6 +- docs/product/surfaces.md | 2 +- e2e/landing-mobile.spec.ts | 41 +- e2e/public-app.spec.ts | 229 ++++++ landing-astro/src/pages/index.astro | 4 +- next.config.ts | 2 +- .../.openspec.yaml | 0 .../design.md | 52 +- .../proposal.md | 13 +- .../specs/github-project-connections/spec.md | 0 .../specs/public-discovery/spec.md | 44 ++ .../specs/public-project-preview/spec.md | 10 +- .../specs/repo-tool-intelligence/spec.md | 22 + .../tasks.md | 15 +- .../specs/public-discovery/spec.md | 19 - .../specs/github-project-connections/spec.md | 31 +- openspec/specs/public-discovery/spec.md | 39 +- openspec/specs/public-project-preview/spec.md | 63 ++ openspec/specs/repo-tool-intelligence/spec.md | 68 +- package.json | 1 + playwright.config.ts | 52 +- public/api-ai.json | 2 +- public/index.md | 2 +- public/llms-full.txt | 4 +- public/llms.txt | 2 +- public/project-preview.md | 11 +- src/__tests__/abortable-fetch.test.ts | 33 + src/__tests__/discover-api.test.ts | 38 + src/__tests__/project-activation-ui.test.ts | 24 +- src/__tests__/project-preview-api.test.ts | 25 + src/__tests__/project-preview.test.ts | 18 +- src/__tests__/project-recommendations.test.ts | 83 ++- src/__tests__/similar-repos-api.test.ts | 87 +++ src/__tests__/swr-fetcher.test.ts | 2 + src/__tests__/tools-api.test.ts | 89 +++ src/app/api/discover/route.ts | 37 +- src/app/api/health/route.ts | 26 +- src/app/api/project-preview/route.ts | 19 +- src/app/api/repos/[repoId]/route.ts | 26 - src/app/api/repos/[repoId]/similar/route.ts | 14 +- src/app/api/tools/route.ts | 44 +- src/app/discover/discover-client.tsx | 18 +- src/app/discover/layout.tsx | 5 + src/app/explore/[...slug]/page.tsx | 674 +++++++----------- src/app/explore/layout.tsx | 5 + src/app/privacy/page.tsx | 12 +- src/app/projects/[slug]/page.tsx | 7 + src/app/projects/page.tsx | 10 + src/app/stars/layout.tsx | 9 +- src/app/tools/[toolKey]/page.tsx | 169 +++-- src/app/tools/layout.tsx | 5 + src/app/tools/page.tsx | 122 +--- src/components/project-preview-workspace.tsx | 13 +- src/components/sync-animation.tsx | 10 +- src/components/top-bar.tsx | 33 +- src/hooks/use-discover-repos.ts | 28 +- src/hooks/use-repo-detail.ts | 50 +- src/hooks/use-starred-repos.ts | 28 +- src/lib/abortable-fetch.ts | 24 + src/lib/connected-projects.ts | 4 +- src/lib/project-intelligence.ts | 2 +- src/lib/project-preview.ts | 10 +- src/lib/project-recommendations.ts | 47 +- src/lib/swr-fetcher.ts | 9 +- src/middleware.ts | 3 + 81 files changed, 1922 insertions(+), 852 deletions(-) create mode 100644 .fleet/evidence/core-discovery-hardening/after-1440.png create mode 100644 .fleet/evidence/core-discovery-hardening/after-390.png create mode 100644 .fleet/evidence/core-discovery-hardening/after-768.png create mode 100644 .fleet/evidence/core-discovery-hardening/before-1440.png create mode 100644 .fleet/reviews/2026-08-09-projects-shared-app-shell.json create mode 100644 e2e/public-app.spec.ts rename openspec/changes/{prove-project-recommendation-value => archive/2026-08-09-prove-project-recommendation-value}/.openspec.yaml (100%) rename openspec/changes/{prove-project-recommendation-value => archive/2026-08-09-prove-project-recommendation-value}/design.md (72%) rename openspec/changes/{prove-project-recommendation-value => archive/2026-08-09-prove-project-recommendation-value}/proposal.md (78%) rename openspec/changes/{prove-project-recommendation-value => archive/2026-08-09-prove-project-recommendation-value}/specs/github-project-connections/spec.md (100%) create mode 100644 openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/public-discovery/spec.md rename openspec/changes/{prove-project-recommendation-value => archive/2026-08-09-prove-project-recommendation-value}/specs/public-project-preview/spec.md (83%) rename openspec/changes/{prove-project-recommendation-value => archive/2026-08-09-prove-project-recommendation-value}/specs/repo-tool-intelligence/spec.md (76%) rename openspec/changes/{prove-project-recommendation-value => archive/2026-08-09-prove-project-recommendation-value}/tasks.md (61%) delete mode 100644 openspec/changes/prove-project-recommendation-value/specs/public-discovery/spec.md create mode 100644 openspec/specs/public-project-preview/spec.md create mode 100644 src/__tests__/abortable-fetch.test.ts create mode 100644 src/__tests__/similar-repos-api.test.ts create mode 100644 src/__tests__/tools-api.test.ts create mode 100644 src/app/discover/layout.tsx create mode 100644 src/app/explore/layout.tsx create mode 100644 src/app/tools/layout.tsx create mode 100644 src/lib/abortable-fetch.ts diff --git a/.fleet/design-review.json b/.fleet/design-review.json index 21f2bbb..e386eea 100644 --- a/.fleet/design-review.json +++ b/.fleet/design-review.json @@ -2,7 +2,7 @@ "$schema": "fleet.design-review.v1", "version": 1, "project": "starboard", - "target": "projects shared app shell", + "target": "core discovery hardening and shared repository shell", "mode": "preserve", "register": "product", "context": { @@ -14,29 +14,29 @@ "probes": [], "selected": "existing-design", "approval": "not-required", - "before": ".fleet/evidence/projects-shell/before-1440.png" + "before": ".fleet/evidence/core-discovery-hardening/before-1440.png" }, "evidence": { "screenshots": [ { "width": 390, - "path": ".fleet/evidence/projects-shell/after-390.png" + "path": ".fleet/evidence/core-discovery-hardening/after-390.png" }, { "width": 768, - "path": ".fleet/evidence/projects-shell/after-768.png" + "path": ".fleet/evidence/core-discovery-hardening/after-768.png" }, { "width": 1440, - "path": ".fleet/evidence/projects-shell/after-1440.png" + "path": ".fleet/evidence/core-discovery-hardening/after-1440.png" } ], "projectCheck": { - "command": "pnpm build", + "command": "pnpm build:cf", "status": "pass" }, "critique": { - "score": 32, + "score": 35, "maximum": 40 }, "audit": { @@ -53,7 +53,7 @@ } }, "ownerFeedback": { - "decision": "keep", - "note": "Owner approved the shared-shell Projects correction on 2026-08-09." + "decision": "delegated", + "note": "Owner delegated the final implementation and testability judgment by asking to fix the full audited surface before their own test pass." } } diff --git a/.fleet/evidence/core-discovery-hardening/after-1440.png b/.fleet/evidence/core-discovery-hardening/after-1440.png new file mode 100644 index 0000000000000000000000000000000000000000..46f5e88171e03f08573ab5d62c652213bf9b0fda GIT binary patch literal 67877 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

*-z474@>?_dL&O+wbc7x2?My`W=2FOeEGsw>= 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 W`Th$FObj^)rpgxmyA^QSKmI@InSwn4 literal 0 HcmV?d00001 diff --git a/.fleet/evidence/core-discovery-hardening/after-390.png b/.fleet/evidence/core-discovery-hardening/after-390.png new file mode 100644 index 0000000000000000000000000000000000000000..dab4aad3760056907ba8293e633162ba24a02919 GIT binary patch literal 53894 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%0A}!HcIfd{ngAjZCREM8Lv?E%I?`(D2k<7@Xy~w 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 literal 0 HcmV?d00001 diff --git a/.fleet/evidence/core-discovery-hardening/after-768.png b/.fleet/evidence/core-discovery-hardening/after-768.png new file mode 100644 index 0000000000000000000000000000000000000000..af7b4f15206bfb9995f6ae28eef344b52496d652 GIT binary patch literal 61633 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 literal 0 HcmV?d00001 diff --git a/.fleet/evidence/core-discovery-hardening/before-1440.png b/.fleet/evidence/core-discovery-hardening/before-1440.png new file mode 100644 index 0000000000000000000000000000000000000000..dae08edc6065f9aa0fa7fd5c9cc151e93d8adbd7 GIT binary patch literal 16202 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 YkE29S-MtSjG6YdOr*$^_cg(GS0eEs4e*gdg literal 0 HcmV?d00001 diff --git a/.fleet/reviews/2026-08-09-projects-shared-app-shell.json b/.fleet/reviews/2026-08-09-projects-shared-app-shell.json new file mode 100644 index 0000000..21f2bbb --- /dev/null +++ b/.fleet/reviews/2026-08-09-projects-shared-app-shell.json @@ -0,0 +1,59 @@ +{ + "$schema": "fleet.design-review.v1", + "version": 1, + "project": "starboard", + "target": "projects shared app shell", + "mode": "preserve", + "register": "product", + "context": { + "product": "PRODUCT.md", + "design": "src/app/globals.css" + }, + "direction": { + "references": [], + "probes": [], + "selected": "existing-design", + "approval": "not-required", + "before": ".fleet/evidence/projects-shell/before-1440.png" + }, + "evidence": { + "screenshots": [ + { + "width": 390, + "path": ".fleet/evidence/projects-shell/after-390.png" + }, + { + "width": 768, + "path": ".fleet/evidence/projects-shell/after-768.png" + }, + { + "width": 1440, + "path": ".fleet/evidence/projects-shell/after-1440.png" + } + ], + "projectCheck": { + "command": "pnpm build", + "status": "pass" + }, + "critique": { + "score": 32, + "maximum": 40 + }, + "audit": { + "score": 18, + "maximum": 20 + }, + "unresolved": { + "p0": 0, + "p1": 0 + }, + "detector": { + "posture": "advisory", + "findings": [] + } + }, + "ownerFeedback": { + "decision": "keep", + "note": "Owner approved the shared-shell Projects correction on 2026-08-09." + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e97562..0a0bc04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,4 +26,8 @@ jobs: - run: pnpm test:coverage - - run: pnpm build + - run: pnpm build:e2e + + - run: pnpm exec playwright install --with-deps chromium + + - run: pnpm test:e2e diff --git a/AGENTS.md b/AGENTS.md index fad8b1f..19266ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -244,3 +244,13 @@ S69 Starboard — migrate deployment from Vercel/Turso to Cloudflare Workers via Access 284k tokens of past work via get_observations([IDs]) or mem-search skill. + + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index d6a2384..d92c235 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -32,13 +32,13 @@ installation, alerts, reports, digest email, and stack generation. **Local dev:** `pnpm install && cp .env.example .env.local && pnpm dev` → http://localhost:3000 -**Key checks:** `pnpm test:coverage` · `pnpm build` · `pnpm build:cf` (Cloudflare path) +**Key checks:** `pnpm test:coverage` · `pnpm test:e2e` · `pnpm build:cf` (Cloudflare path) ``` GitHub OAuth (NextAuth) │ ▼ -Star sync + public project connection ──► D1 (users, repos, user_repos, user_projects, lists, comments, votes) +Star sync + public project connection ──► D1 (users, repos, user_repos, user_projects, lists) │ ├── Full-text + facet search (GET /api/stars) ├── Semantic search: knowledgebase Worker; lexical-only when shared RAG is unavailable @@ -72,6 +72,15 @@ provenance. The workflow is free and has no billing or entitlement gate. ## Timeline +- **2026-08-09 (core discovery hardening, local)** — Stabilized aborted filter + requests, added hybrid public Discover relevance with lexical fallback, + tightened peer/tool evidence thresholds, protected uncataloged previews with + the user's GitHub token, paginated Tool Intelligence evidence, moved + repository intelligence into the shared shell, and retired its discussion + controls while retaining legacy data. The production-accurate Playwright + suite now covers the Astro landing and core public journeys at desktop and + mobile widths. This work is locally testable and not yet released. + - **2026-08-09 (shared lint baseline)** — Adopted the Fleet Ultracite baseline for core TypeScript, React, Next.js, and Vitest code. Explicit compatibility exceptions preserve current behavior while 206 files pass with zero @@ -177,7 +186,7 @@ provenance. The workflow is free and has no billing or entitlement gate. | Phase | Milestone | |-------|-----------| | Foundation | GitHub OAuth (NextAuth v5), OpenNext Cloudflare deploy, core dashboard with sync, tags, collections, full-text search, virtual scroll | -| Repo intelligence | Repo detail (`/explore`), comments/votes, public shared lists, legal/marketing shell | +| Repo intelligence | Shared-shell repository detail (`/explore`), similar projects, tool evidence, public shared lists, legal/marketing shell | | Semantic search | knowledgebase Worker integration for relevance search; README-backed sync ingest; local embeddings retained for non-RAG Starboard features | | Connected projects | Shipped public GitHub project connections, public preview, GitHub picker, and evidence-based repository and tool recommendations | | Discovery & tools | Public Discover, daily bounded seed/enrich/embed with manual dispatch, stored growth sorting, and Tool Intelligence | @@ -212,7 +221,9 @@ provenance. The workflow is free and has no billing or entitlement gate. - GitHub star-list ingestion via HTML scraping where no official API exists. - Main dashboard: smart categories, custom colored tags, named collections, full-text search, language/category/tag/collection filters, sort (recently starred, most stars, recently updated, A-Z), grid/list toggle, virtual scroll for 1000+ repos. - URL-shareable filter/sort state through nuqs. -- Repo detail (`/explore`): comments, votes, likes, similar repos, list assignment, tag picker. +- Repo detail (`/explore`): public similar repositories, detected-tool evidence, + and stored star history in the shared application shell. Legacy discussion + data is retained but has no active product controls. - Public shared lists at `/lists/[slug]` with SSR and `list.json` export route. - Legal/marketing shell: about, privacy, terms, sitemap, robots, OG image, security.txt, humans.txt, PWA manifest. @@ -225,9 +236,9 @@ provenance. The workflow is free and has no billing or entitlement gate. credentials in GitHub. ### Connected projects and public preview -- Guests can preview a public GitHub repository without sign-in or a user-data - write, then carry the normalized repository through sign-in for explicit - connection. +- Guests can preview a cataloged public GitHub repository without sign-in or a + user-data write. Uncataloged lookups require the existing GitHub session token + and preserve the repository through sign-in for explicit connection. - Authenticated users can connect and disconnect public GitHub repositories by URL or an on-demand public-repository picker without a broader OAuth scope. - `user_projects` isolates project connections per user while reusing shared @@ -238,13 +249,15 @@ provenance. The workflow is free and has no billing or entitlement gate. ### Discovery and intelligence surfaces - Public Discover page and `/api/discover` for the seeded popular repository - corpus; authentication adds saved state and collection controls but is not - required to browse, search, sort, filter, paginate, or open repo details. + corpus; relevance search fuses bounded semantic and lexical candidates and + falls back honestly to lexical search. Authentication adds saved state and + collection controls but is not required to browse, search, sort, filter, + paginate, or open repo details. - Discover supports paginated 30-day growth ordering and detected-tool facets from indexed local snapshot/tool tables. - Daily bounded GitHub Actions seed/enrich popular repos in D1 and embed through native Worker bindings; manual dispatch remains available for operator checks. - Star history and fastest-grower APIs/surfaces: `/api/repos/[repoId]/star-history`, `/api/growth`, Discover growth sorting, and repo-detail mini history from stored `repo_star_snapshots`. -- Tool Intelligence: additive `repo_tools` index, `/api/tools`, `/api/repos/[repoId]/tools`, `/tools`, and `pnpm db:enrich-tools` for bounded SBOM/tree/manifest-based detection with source/confidence labels. Accuracy disclaimer is shown in-product because manifest/SBOM evidence is stronger than README/topic/metadata inference and C/C++ monorepos vary. +- Tool Intelligence: additive `repo_tools` index, `/api/tools`, `/api/repos/[repoId]/tools`, `/tools`, and `pnpm db:enrich-tools` for bounded SBOM/tree/manifest-based detection with source/confidence labels. Repository evidence is server-filtered and paginated in 48-item pages. Accuracy disclaimer is shown in-product because manifest/SBOM evidence is stronger than README/topic/metadata inference and C/C++ monorepos vary. - SaaS Maker feedback widget integrated; product analytics run directly through PostHog. - First-run UX, sample prioritized stars board, semantic search, GitHub permission trust note, and public discovery paths remain. @@ -253,7 +266,9 @@ provenance. The workflow is free and has no billing or entitlement gate. - Shared Ultracite lint baseline with a clean 206-file check. - Checked-in `.env.example` documents required local variables without secrets. -- Vitest unit tests with v8 coverage thresholds (80% lines/functions/statements, 70% branches) on `github-projects`, `project-recommendations`, `search`, and `starboard-rag-documents`; Playwright e2e path documented in README. +- Vitest unit tests with v8 coverage thresholds (80% lines/functions/statements, + 70% branches) on selected core logic; production-accurate Playwright journeys + cover the Astro landing and public app at desktop and mobile widths. - Pre-push lint hook. - TypeScript config and Astro landing tooling made self-contained for green Cloudflare builds. diff --git a/README.md b/README.md index 0a6e5ba..34ecf9b 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,9 @@ matches, and keeps personal stars searchable with tags and collections. ## Features -- **Public Project Preview** — paste a public GitHub repository and inspect a - read-only recommendation sample before sign-in +- **Public Project Preview** — inspect a read-only recommendation sample for a + cataloged repository before sign-in; uncataloged lookups use the user's + existing GitHub session - **GitHub OAuth** — Sign in and sync your starred repos - **Smart Categories** — Auto-categorize repos (AI/ML, Frontend, DevOps, etc.) - **Custom Tags** — Create and assign colored tags to repos diff --git a/docs/architecture/data-flow.md b/docs/architecture/data-flow.md index 199982d..7c8305d 100644 --- a/docs/architecture/data-flow.md +++ b/docs/architecture/data-flow.md @@ -31,7 +31,8 @@ history and remains empty when insufficient samples exist. ```text public GitHub URL or owner/repository → normalize and check the local catalog - → resolve one public GitHub repository on a catalog miss + → require the existing GitHub session token on a catalog miss + → resolve one public GitHub repository with authenticated quota → public preview: no user-owned write → authenticated connection only after explicit confirmation → upsert shared repos row diff --git a/docs/development/commands.md b/docs/development/commands.md index 185d069..9cd3d4d 100644 --- a/docs/development/commands.md +++ b/docs/development/commands.md @@ -23,6 +23,7 @@ Source of truth: `scripts` in `package.json`. This page annotates intent; run | --- | --- | | `pnpm cf:build` | `next build --webpack` + inline critical CSS + `opennextjs-cloudflare build --skipNextBuild` + `populateCache local` + landing-astro build + overlay | | `pnpm build:cf` | Alias for `cf:build` | +| `pnpm build:e2e` | Credential-free OpenNext build plus Astro landing overlay for browser tests | | `pnpm preview:cf` | `build:cf` + `opennextjs-cloudflare preview` | | `pnpm deploy:cf` | `build:cf` + SHA-tagged `opennextjs-cloudflare deploy` (manual) | | `pnpm cf:typegen` | `wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts` | diff --git a/docs/development/testing.md b/docs/development/testing.md index bee6049..761c1df 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -34,8 +34,14 @@ pnpm test:coverage # vitest run --coverage ## End-to-end (Playwright) - **Runner:** Playwright. Config in `playwright.config.ts`. Tests in `e2e/`. -- `pnpm test:e2e` — desktop project. -- `pnpm test:e2e:mobile` — mobile project. +- `pnpm test:e2e` — runs `build:e2e` locally to produce a credential-free + OpenNext artifact, overlays the real Astro landing, then exercises landing + and mocked public-product journeys at desktop and mobile widths. CI builds + that artifact once before starting Playwright. +- Covered journeys: public project CTA, Discover search stability, bounded Tool + Intelligence pagination, the shared repository-intelligence shell, and the + uncataloged-preview sign-in boundary. +- `pnpm test:e2e:mobile` — the mobile Next.js application journeys only. ## Smoke checks @@ -47,7 +53,7 @@ pnpm test:coverage # vitest run --coverage ## CI - `.github/workflows/ci.yml` — push/PR: `pnpm install --frozen-lockfile` → - `lint` → `test:coverage` → `build`. + `lint` → `test:coverage` → `build:e2e` → Chromium install → `test:e2e`. - `.github/workflows/weekly.yml` — Mondays 09:00 UTC: lint, typecheck, test, build (catches drift that doesn't surface on push CI). - `.github/workflows/docs.yml` — push/PR on docs-touching paths: runs diff --git a/docs/operations/ci-cd.md b/docs/operations/ci-cd.md index 999ca14..64b1299 100644 --- a/docs/operations/ci-cd.md +++ b/docs/operations/ci-cd.md @@ -7,7 +7,11 @@ in [jobs.md](jobs.md); this page covers the push/PR and deploy pipelines. - **Triggers:** push to `main`/`master`; PRs against `main`/`master`. - **Steps:** checkout → pnpm setup → Node 22 → `pnpm install --frozen-lockfile` - → `pnpm lint` → `pnpm test:coverage` → `pnpm build`. + → `pnpm lint` → `pnpm test:coverage` → `pnpm build:e2e` → Chromium install + → `pnpm test:e2e`. `build:e2e` creates the OpenNext and Astro-overlaid + artifact without Cloudflare cache population or operator credentials. CI + previews that artifact so the browser suite verifies the landing and app + together. - **Permissions:** default. ## Deploy (`.github/workflows/deploy.yml`) diff --git a/docs/operations/data-map.md b/docs/operations/data-map.md index 0c46ff7..f3b4b04 100644 --- a/docs/operations/data-map.md +++ b/docs/operations/data-map.md @@ -32,7 +32,7 @@ retirement requires separate approval. | D1 `user_repos` (starred/saved state) | irreplaceable-user | GitHub sync via `/api/stars/sync` | Re-sync from GitHub starred list (ETag + HTML scrape) | ~seconds per user | 2026-08-02 | | D1 `user_projects` | irreplaceable-user | Project connection UI/API | Reconnect public GitHub repositories manually | ~seconds per user | 2026-08-08 | | D1 `user_lists`, `user_repo_lists` | irreplaceable-user | User UI actions | Not reconstructable — user-curated collections | n/a — export required | 2026-08-02 | -| D1 `comments`, `likes`, `comment_votes` | irreplaceable-user | User UI actions | Not reconstructable — user-generated content | n/a — export required | 2026-08-02 | +| D1 `comments`, `likes`, `comment_votes` | historical inactive user storage | Removed discussion UI; legacy user actions | Not reconstructable — retained for export until a separately approved retention migration | n/a — export required | 2026-08-09 | | D1 `repo_embeddings` hashes + Vectorize `starboard-repos` values | derived | Worker binding embedding jobs | Re-embed from `repos` + `repo_ai_metadata` text via Workers AI | ~minutes (Workers AI quota) | 2026-08-02 | | D1 `repo_ai_metadata` | derived | `scripts/enrich-repos.ts` (free-ai gateway) | Re-enrich from `repos` metadata via AI | ~minutes per batch | 2026-08-02 | | D1 `repo_tools` | derived | `scripts/enrich-tools.ts` | Re-detect from GH tree/manifest/SBOM | ~minutes per batch | 2026-08-02 | diff --git a/docs/product/features.md b/docs/product/features.md index 9462bf2..52c21d9 100644 --- a/docs/product/features.md +++ b/docs/product/features.md @@ -5,9 +5,10 @@ reasons, see [../architecture/decisions/](../architecture/decisions/). ## Project-aware discovery -- Guests can preview a public GitHub repository before sign-in. Cataloged - repositories use stored evidence; uncataloged repositories use one public - GitHub metadata lookup. Preview creates no user-owned row. +- Guests can preview a cataloged public GitHub repository before sign-in using + stored evidence. An uncataloged lookup requires the user's existing GitHub + session token so anonymous traffic cannot exhaust a shared quota. Preview + creates no user-owned row in either case. - Authenticated users can connect a public GitHub repository by URL or `owner/repository`, or choose from a bounded on-demand list of public GitHub repositories, without broadening the current OAuth scope. @@ -27,10 +28,13 @@ reasons, see [../architecture/decisions/](../architecture/decisions/). ## Public discovery and tool intelligence -- Discover is public and supports search, pagination, language facets, detected - tool facets, and stored 30-day growth ordering. +- Discover is public and supports hybrid semantic-plus-lexical relevance + search with lexical fallback, pagination, language facets, detected-tool + facets, and stored 30-day growth ordering. - Tool Intelligence aggregates normalized `repo_tools` records across the seeded corpus and, for authenticated users, the personal library. +- Tool evidence is paginated in bounded 48-repository pages and filtered on the + server, avoiding a 500-card initial render. - Tool detections preserve category, confidence, and source evidence. The UI states that detection is evidence-based but not guaranteed complete. - Repository detail pages include tool evidence, similar repositories, and @@ -59,8 +63,9 @@ reasons, see [../architecture/decisions/](../architecture/decisions/). - Daily and manually dispatched seed, metadata-enrichment, tool-enrichment, and embedding jobs with explicit per-run bounds. - Additive ordered D1 migrations; raw SQL with no ORM. -- Vitest unit/integration coverage, Playwright path, Biome checks, docs - validation, and OpenNext Cloudflare builds. +- Selective core-logic Vitest coverage, Playwright journeys against the actual + Astro-overlaid Cloudflare preview, Biome checks, docs validation, and + OpenNext Cloudflare builds. ## Deliberately removed diff --git a/docs/product/overview.md b/docs/product/overview.md index 3360043..6faf1bf 100644 --- a/docs/product/overview.md +++ b/docs/product/overview.md @@ -7,8 +7,10 @@ developer's public GitHub projects to a seeded open-source repository catalog, then explains which repositories and tools may fit the work being built. Similar repositories form the grounding layer: tool recommendations come only from normalized detections on those peers and name their repository sources. -Guests can preview this value with a public repository before sign-in; a durable -project connection remains an explicit authenticated action. +Guests can preview this value for repositories already in the public catalog. +An uncataloged repository lookup uses the user's existing authenticated GitHub +session rather than a shared anonymous quota. A durable project connection +remains a separate explicit action. The product also keeps a personal starred-repository library with search, lists, tags, saved state, and notes. Discover and Tool Intelligence remain diff --git a/docs/product/surfaces.md b/docs/product/surfaces.md index e93ff3d..0b9b965 100644 --- a/docs/product/surfaces.md +++ b/docs/product/surfaces.md @@ -5,7 +5,7 @@ | Route | Purpose | Access | | --- | --- | --- | | `/` | Product landing page | Public | -| `/project-preview` | Read-only project-context preview before sign-in | Public | +| `/project-preview` | Read-only project-context preview; catalog hits are public and uncataloged lookups require GitHub sign-in | Conditional public | | `/discover` | Search and filter the seeded repository corpus | Public | | `/explore/[...slug]` | Repository detail, tool evidence, history, and similar repos | Public read; signed-in actions | | `/tools` | Aggregate tool intelligence | Public | diff --git a/e2e/landing-mobile.spec.ts b/e2e/landing-mobile.spec.ts index 0fb2c92..386e2da 100644 --- a/e2e/landing-mobile.spec.ts +++ b/e2e/landing-mobile.spec.ts @@ -1,30 +1,24 @@ import { expect, test } from '@playwright/test'; -/** - * Mobile-viewport smoke test for the public landing page. - * - * Runs under both the `desktop` and `mobile` Playwright projects (see - * playwright.config.ts). The `mobile` project uses a 390px iPhone 13 - * viewport — the fleet mobile target — so layout regressions there fail CI. - * - * The primary signed-in flow (the virtualized repo grid) is verified manually - * against the mobile conventions doc. - */ -test.describe('landing page', () => { +test.describe('production Astro landing page', () => { test('renders the hero and key sections with no horizontal scroll', async ({ page }) => { await page.goto('/'); - // Hero value prop is visible. await expect( - page.getByRole('heading', { name: 'GitHub stars, ranked for the work you ship.', level: 1 }) + page.getByRole('heading', { + name: 'Start with your project. Find the tools that fit it.', + level: 1, + }) + ).toBeVisible(); + await expect( + page.getByText(/paste a public github repository\. starboard finds/i) ).toBeVisible(); - await expect(page.getByText(/connect a public github project/i)).toBeVisible(); - // The single primary CTA is reachable. - await expect(page.getByRole('button', { name: /continue with github/i })).toBeVisible(); + const repository = page.getByRole('textbox', { name: /public github repository/i }); + await expect(repository).toBeVisible(); + await expect(page.getByRole('button', { name: /preview project/i })).toBeVisible(); - // The guest alternative must lead to a genuinely public surface. - await expect(page.getByRole('link', { name: /browse public repos/i })).toHaveAttribute( + await expect(page.getByRole('link', { name: /browse the public catalog/i })).toHaveAttribute( 'href', '/discover' ); @@ -38,10 +32,17 @@ test.describe('landing page', () => { test('the primary CTA is a large enough touch target', async ({ page }) => { await page.goto('/'); - const cta = page.getByRole('button', { name: /continue with github/i }); + const cta = page.getByRole('button', { name: /preview project/i }); const box = await cta.boundingBox(); expect(box).not.toBeNull(); - // WCAG 2.5.5 / iOS HIG: tap targets must be at least 44x44px. expect(box!.height).toBeGreaterThanOrEqual(44); }); + + test('submits a repository through the public preview route', async ({ page }) => { + await page.goto('/'); + await page.getByRole('textbox', { name: /public github repository/i }).fill('acme/store'); + await page.getByRole('button', { name: /preview project/i }).click(); + + await expect(page).toHaveURL(/\/project-preview\?repository=acme%2Fstore$/); + }); }); diff --git a/e2e/public-app.spec.ts b/e2e/public-app.spec.ts new file mode 100644 index 0000000..84cb694 --- /dev/null +++ b/e2e/public-app.spec.ts @@ -0,0 +1,229 @@ +import { expect, type Page, test } from '@playwright/test'; + +const repo = { + id: 1, + name: 'starboard', + full_name: 'codevetter/starboard', + owner_login: 'codevetter', + owner_avatar: 'https://avatars.githubusercontent.com/u/1?v=4', + owner: { login: 'codevetter', avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4' }, + html_url: 'https://github.com/codevetter/starboard', + description: 'Project-aware open-source discovery and tool intelligence.', + language: 'TypeScript', + stargazers_count: 12_500, + archived: false, + topics: ['github', 'discovery'], + repo_created_at: '2026-01-01T00:00:00Z', + repo_updated_at: '2026-08-01T00:00:00Z', + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-08-01T00:00:00Z', + list_id: null, + collection_ids: [], + tags: [], + notes: null, + starred_at: null, + is_starred: false, + is_saved: false, + star_growth_30d: 420, +}; + +async function mockGuestSession(page: Page) { + await page.route('**/api/auth/session', (route) => + route.fulfill({ status: 200, contentType: 'application/json', body: 'null' }) + ); +} + +test.beforeEach(async ({ page }) => { + await mockGuestSession(page); +}); + +test('Discover keeps the first search request alive and shows matching repositories', async ({ + page, +}) => { + await page.route('**/discover/data**', (route) => + route.fulfill({ + json: { + repos: [repo], + total: 1, + facets: { + languages: [['TypeScript', 1]], + lists: [], + tags: [], + tools: [{ key: 'nextjs', name: 'Next.js', count: 1 }], + }, + minStars: 5000, + }, + }) + ); + + await page.goto('/discover'); + await page.getByPlaceholder('Search repos...').fill('tool intelligence'); + + await expect(page.getByRole('link', { name: /codevetter\/starboard/i })).toBeVisible(); + await expect(page.getByText(/couldn't load discover results/i)).toHaveCount(0); + await expect(page).toHaveURL(/q=tool\+intelligence|q=tool%20intelligence/); + + if ((page.viewportSize()?.width ?? 0) >= 1024) { + const repositoryCount = page.getByRole('button', { + name: /how discover selects repositories/i, + }); + await repositoryCount.hover(); + await expect( + page.getByText(/discover starts with public github repositories at 5,000\+ stars/i) + ).toBeVisible(); + } +}); + +test('tool intelligence renders one bounded page and loads more on demand', async ({ page }) => { + await page.route('**/api/tools?**', async (route) => { + const url = new URL(route.request().url()); + const offset = Number(url.searchParams.get('offset') ?? 0); + const count = offset === 0 ? 48 : 1; + const repos = Array.from({ length: count }, (_, index) => { + const id = offset + index + 1; + return { + ...repo, + id, + name: `repo-${id}`, + full_name: `acme/repo-${id}`, + html_url: `https://github.com/acme/repo-${id}`, + tool: { + toolKey: 'vitest', + toolName: 'Vitest', + category: 'testing', + url: 'https://vitest.dev', + confidence: 96, + sources: ['package.json'], + }, + }; + }); + await route.fulfill({ + json: { + scope: 'discover', + disclaimer: 'Detections come from repository manifests and may need verification.', + tool: { + toolKey: 'vitest', + toolName: 'Vitest', + category: 'testing', + url: 'https://vitest.dev', + repoCount: 49, + avgConfidence: 96, + maxConfidence: 100, + }, + repos, + page: { offset, limit: 48, hasMore: offset === 0 }, + }, + }); + }); + + await page.goto('/tools/vitest'); + await expect(page.getByRole('heading', { name: 'Vitest', level: 1 })).toBeVisible(); + await expect(page.getByText('Showing 48 of 49 matching repositories.')).toBeVisible(); + await expect(page.getByRole('link', { name: /^acme\/repo-/ })).toHaveCount(48); + + await page.getByRole('button', { name: 'Load 48 more' }).click(); + await expect(page.getByText('Showing 49 of 49 matching repositories.')).toBeVisible(); + await expect(page.getByRole('link', { name: /^acme\/repo-/ })).toHaveCount(49); +}); + +test('repository intelligence reuses the app shell and omits discussion controls', async ({ + page, +}, testInfo) => { + await page.route('**/api/repos/lookup?**', (route) => route.fulfill({ json: { repo } })); + await page.route('**/api/repos/1/similar?**', (route) => + route.fulfill({ + json: { + similar: [ + { + ...repo, + id: 2, + name: 'peer', + full_name: 'acme/peer', + html_url: 'https://github.com/acme/peer', + similarity: 0.88, + }, + ], + }, + }) + ); + await page.route('**/api/repos/1/tools', (route) => + route.fulfill({ + json: { + disclaimer: 'Manifest evidence', + tools: [ + { + toolKey: 'vitest', + toolName: 'Vitest', + category: 'testing', + url: 'https://vitest.dev', + confidence: 96, + sources: ['package.json'], + }, + ], + }, + }) + ); + await page.route('**/api/repos/1/star-history?**', (route) => + route.fulfill({ + json: { + points: [ + { stargazersCount: 12_000, capturedAt: '2026-07-01T00:00:00Z' }, + { stargazersCount: 12_500, capturedAt: '2026-08-01T00:00:00Z' }, + ], + growth: { starsGained: 500, percentGrowth: 4.2, enoughHistory: true }, + }, + }) + ); + + await page.goto('/explore/codevetter/starboard'); + await expect(page.getByRole('heading', { name: 'Repository Intelligence' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'starboard', level: 1 })).toBeVisible(); + if ((page.viewportSize()?.width ?? 0) < 640) { + await expect(page.getByRole('button', { name: 'Open product navigation' })).toBeVisible(); + } else { + await expect(page.getByRole('link', { name: 'Discover', exact: true })).toBeVisible(); + } + await expect(page.getByRole('heading', { name: 'Similar projects' })).toBeVisible(); + await expect(page.getByText('Discussion', { exact: true })).toHaveCount(0); + + const overflow = await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth + ); + expect(overflow).toBe(false); + + if (!process.env.CI && testInfo.project.name === 'desktop') { + for (const width of [390, 768, 1440]) { + await page.setViewportSize({ width, height: 900 }); + expect( + await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth + ) + ).toBe(false); + await page.screenshot({ + path: `.fleet/evidence/core-discovery-hardening/after-${width}.png`, + fullPage: true, + }); + } + } +}); + +test('uncataloged public preview asks for sign-in without presenting a rate-limit failure', async ({ + page, +}) => { + await page.route('**/api/project-preview?**', (route) => + route.fulfill({ + status: 401, + json: { + error: 'Sign in to preview a repository that is not in the Starboard catalog yet.', + loginRequired: true, + }, + }) + ); + + await page.goto('/project-preview?repository=acme%2Fprivate-to-catalog'); + await expect(page.locator('section[role="alert"]')).toContainText( + 'Sign in to preview a repository that is not in the Starboard catalog yet.' + ); + await expect(page.getByRole('link', { name: 'Sign in to preview' })).toBeVisible(); + await expect(page.getByText(/rate limit/i)).toHaveCount(0); +}); diff --git a/landing-astro/src/pages/index.astro b/landing-astro/src/pages/index.astro index 1f9b159..e0fd552 100644 --- a/landing-astro/src/pages/index.astro +++ b/landing-astro/src/pages/index.astro @@ -82,7 +82,7 @@ const DESCRIPTION =

- No sign-in to preview · public repositories only · nothing saved until you connect + Catalog matches preview without sign-in · uncataloged repositories use your signed-in GitHub session · nothing saved until you connect

@@ -194,7 +194,7 @@ const DESCRIPTION =
  • 1

    Preview a project

    -

    Paste any public GitHub repository. No account or write happens yet.

    +

    Paste a public GitHub repository. Catalog matches need no account; new lookups use your signed-in GitHub session. Nothing is saved yet.

  • 2 diff --git a/next.config.ts b/next.config.ts index d4e8e3f..296777a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -16,7 +16,7 @@ const securityHeaders = [ "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://us-assets.i.posthog.com", // unsafe-inline/eval required by Next.js "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https://avatars.githubusercontent.com https://github.com", - "connect-src 'self' https://api.github.com https://us.i.posthog.com https://us-assets.i.posthog.com", + "connect-src 'self' https://api.github.com https://us.i.posthog.com https://us-assets.i.posthog.com https://vitals.fleet.workers.dev", "frame-ancestors 'none'", ].join('; '), }, diff --git a/openspec/changes/prove-project-recommendation-value/.openspec.yaml b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/.openspec.yaml similarity index 100% rename from openspec/changes/prove-project-recommendation-value/.openspec.yaml rename to openspec/changes/archive/2026-08-09-prove-project-recommendation-value/.openspec.yaml diff --git a/openspec/changes/prove-project-recommendation-value/design.md b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/design.md similarity index 72% rename from openspec/changes/prove-project-recommendation-value/design.md rename to openspec/changes/archive/2026-08-09-prove-project-recommendation-value/design.md index 171ad66..bfff22f 100644 --- a/openspec/changes/prove-project-recommendation-value/design.md +++ b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/design.md @@ -66,11 +66,13 @@ not depend on Workers AI availability. The static landing submits a GET form to `/project-preview`. The Next.js preview surface calls a public GET API with the normalized public repository value. -Resolution checks the local catalog first; only a cache miss calls GitHub's -single-repository endpoint, with bounded caching and no token for guests. The -response never writes `user_projects` or other user-owned tables. The connect -CTA signs in and returns to `/projects?repository=...`, where the manual form is -prefilled for an explicit final connection. +Resolution checks the local catalog first. Catalog hits remain public. A cache +miss no longer calls GitHub anonymously from the shared Worker egress path: an +authenticated session token may resolve the repository, while a guest receives +a sign-in handoff that preserves the repository value. The response never +writes `user_projects` or other user-owned tables. The connect CTA signs in and +returns to `/projects?repository=...`, where the manual form is prefilled for an +explicit final connection. Alternative: connect automatically after OAuth. Rejected because the user should still see and confirm the durable write boundary. @@ -104,11 +106,11 @@ dashboard. - **Workers AI or Vectorize is temporarily unavailable** → Continue through FTS and language candidates, report the retrieval mode, and never turn the error into a failed project connection. -- **Unauthenticated GitHub quota is exhausted or preview is abused** → Resolve - cataloged repositories without GitHub, cache successful public resolution, - make exactly one repository request on a miss, and return a retryable state. - A broader edge rate limiter is explicitly outside this change because it - previously affected unrelated product requests. +- **Unauthenticated GitHub quota is exhausted or preview is abused** → Never + spend shared anonymous GitHub quota on a guest request. Resolve cataloged + repositories locally and require the existing minimal GitHub session for an + uncataloged lookup. A broader edge rate limiter remains outside this change + because it previously affected unrelated product requests. - **Minimal OAuth token returns fewer repositories than expected** → Present the picker as a convenience, label it public-only, and keep manual URL entry primary and fully functional. @@ -127,3 +129,33 @@ dashboard. and responsive visual review before release. 4. Roll back by reverting this change; it adds no schema, dependency, OAuth scope, secret, or production binding migration. + +## Hardening decisions after production audit + +### 7. Search requests own only their request lifetime + +The SWR fetcher aborts the previous request before installing a new controller. +Filter effects reset pagination but never abort the request that SWR just +started. Discover fuses bounded Vectorize matches with FTS matches and degrades +to lexical results when Workers AI or Vectorize is unavailable. + +### 8. Recommendation evidence has an admission floor + +Primary-language equality remains contextual evidence but cannot admit a peer +by itself. A peer needs a meaningful shared topic, exact tool, tool-area, or +multiple contextual signals. Tool additions exclude language metadata and +low-confidence detections, and need corroboration from at least two admitted +peers. + +### 9. Large evidence collections paginate at the API boundary + +Tool detail requests use a small bounded page with offset metadata and an +explicit Load more action. Filtering is server-side so pagination does not make +search incomplete. The DOM therefore grows only when the user asks for more. + +### 10. Core product routes share one shell + +Repository detail uses `AppShell` and `TopBar`, exposes a real main landmark and +page heading, and returns guests to Discover. The discussion UI is removed so +the surface focuses on repository evidence, similar projects, and detected +tools; existing database records are preserved. diff --git a/openspec/changes/prove-project-recommendation-value/proposal.md b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/proposal.md similarity index 78% rename from openspec/changes/prove-project-recommendation-value/proposal.md rename to openspec/changes/archive/2026-08-09-prove-project-recommendation-value/proposal.md index 56b6238..54c3946 100644 --- a/openspec/changes/prove-project-recommendation-value/proposal.md +++ b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/proposal.md @@ -25,6 +25,17 @@ and make the grounding set representative of the full eligible catalog. recommendation sentiment. No repository identity or query text is emitted. - Clarify tool cards with support strength, source confidence, and a direct path to the existing Tool Intelligence detail surface. +- Harden the shipped first-value path: prevent client searches from cancelling + themselves, combine semantic and lexical Discover candidates, and keep an + uncataloged guest preview from consuming shared unauthenticated GitHub quota. +- Tighten the recommendation contract so primary-language overlap alone cannot + admit a peer and language or weak one-source detections cannot become tool + recommendations. +- Paginate repository evidence on Tool Intelligence detail pages and bring the + core repository-detail route into the shared application shell. +- Remove the peripheral repository discussion UI, repair production telemetry, + and add browser journeys for the actual Astro landing plus core public app + surfaces. - Update active product/status documentation and add a deterministic retrieval evaluation fixture for the core recommendation path. @@ -53,4 +64,4 @@ and make the grounding set representative of the full eligible catalog. - Reuses the existing D1 full-text indexes, Cloudflare Vectorize binding, Workers AI embedding adapter, GitHub OAuth token, and UI components. - Adds no production dependency, billing/entitlement gate, private-repository - scope, raw-SQL proxy, deployment, or production configuration change. + scope, raw-SQL proxy, deployment, secret, or production binding change. diff --git a/openspec/changes/prove-project-recommendation-value/specs/github-project-connections/spec.md b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/github-project-connections/spec.md similarity index 100% rename from openspec/changes/prove-project-recommendation-value/specs/github-project-connections/spec.md rename to openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/github-project-connections/spec.md diff --git a/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/public-discovery/spec.md b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/public-discovery/spec.md new file mode 100644 index 0000000..52a8469 --- /dev/null +++ b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/public-discovery/spec.md @@ -0,0 +1,44 @@ +## MODIFIED Requirements + +### Requirement: Public landing handoff + +The Starboard landing page SHALL make a public project preview the primary +first-value action and SHALL preserve direct public Discover browsing as an +alternative. + +#### Scenario: Guest submits a cataloged public project + +- **WHEN** a visitor enters a cataloged GitHub URL or `owner/repository` on the + landing page +- **THEN** the visitor reaches a working read-only project preview without + first being required to sign in + +#### Scenario: Guest submits an uncataloged public project + +- **WHEN** a visitor enters a repository that is not yet in Starboard's catalog +- **THEN** Starboard preserves the repository and asks for GitHub sign-in before + making the external lookup + +#### Scenario: Guest follows the public CTA + +- **WHEN** a visitor selects the landing-page public browsing action +- **THEN** the visitor reaches `/discover` and can use the shared corpus + +## ADDED Requirements + +### Requirement: Public search is stable and meaning-aware + +Discover SHALL combine bounded semantic and lexical candidates for relevance +search and SHALL NOT surface an error merely because its own filter state +changed. + +#### Scenario: Visitor changes a search or filter + +- **WHEN** a new first-page request replaces an older Discover request +- **THEN** Starboard cancels only the stale request and renders the newest result + without requiring a manual retry + +#### Scenario: Semantic retrieval is unavailable + +- **WHEN** Workers AI or Vectorize cannot serve a Discover query +- **THEN** Starboard returns bounded lexical results and does not fail the page diff --git a/openspec/changes/prove-project-recommendation-value/specs/public-project-preview/spec.md b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/public-project-preview/spec.md similarity index 83% rename from openspec/changes/prove-project-recommendation-value/specs/public-project-preview/spec.md rename to openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/public-project-preview/spec.md index b93829d..df0dc4e 100644 --- a/openspec/changes/prove-project-recommendation-value/specs/public-project-preview/spec.md +++ b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/public-project-preview/spec.md @@ -20,8 +20,14 @@ preview without creating user-owned data. #### Scenario: Uncataloged public repository - **WHEN** a guest previews a valid public repository not yet in the catalog -- **THEN** Starboard resolves its public GitHub metadata and shows the best - available explained preview without persisting a project connection +- **THEN** Starboard asks for the existing minimal GitHub sign-in, preserves the + repository value, and performs no anonymous GitHub API lookup + +#### Scenario: Signed-in uncataloged public repository + +- **WHEN** a signed-in user previews a public repository not yet in the catalog +- **THEN** Starboard resolves it with the user's existing GitHub token and shows + the best available explained preview without persisting a project connection #### Scenario: Invalid or unavailable repository diff --git a/openspec/changes/prove-project-recommendation-value/specs/repo-tool-intelligence/spec.md b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/repo-tool-intelligence/spec.md similarity index 76% rename from openspec/changes/prove-project-recommendation-value/specs/repo-tool-intelligence/spec.md rename to openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/repo-tool-intelligence/spec.md index 3f84dbc..4d833dc 100644 --- a/openspec/changes/prove-project-recommendation-value/specs/repo-tool-intelligence/spec.md +++ b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/specs/repo-tool-intelligence/spec.md @@ -25,6 +25,17 @@ recommended tool only from normalized detections on those ranked peers. - **THEN** Starboard recommends those tools with the exact peer repositories and detection confidence that support the recommendation +#### Scenario: Only primary language overlaps + +- **WHEN** a candidate shares only the connected project's primary language +- **THEN** Starboard does not admit it as a grounded similar project + +#### Scenario: Tool evidence is weak or metadata-only + +- **WHEN** a peer detection is repository language metadata, below the grounded + confidence floor, or supported by only one peer +- **THEN** Starboard does not present it as a tool to evaluate + #### Scenario: Tool is already used by the connected project - **WHEN** a peer repository contains a tool already detected in the connected @@ -66,3 +77,14 @@ not useful without sending repository identity or free-form text to analytics. - **WHEN** a user selects useful or not useful - **THEN** Starboard records only recommendation kind, sentiment, rank bucket, retrieval mode, and aggregate evidence buckets + +### Requirement: Tool evidence collections remain bounded + +Tool Intelligence SHALL paginate repository evidence and SHALL apply repository +filtering before pagination. + +#### Scenario: A tool appears in hundreds of repositories + +- **WHEN** a user opens that tool's detail page +- **THEN** Starboard renders one bounded page and offers an explicit way to load + more without placing the full collection in the DOM diff --git a/openspec/changes/prove-project-recommendation-value/tasks.md b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/tasks.md similarity index 61% rename from openspec/changes/prove-project-recommendation-value/tasks.md rename to openspec/changes/archive/2026-08-09-prove-project-recommendation-value/tasks.md index ea51f25..8d8d4d1 100644 --- a/openspec/changes/prove-project-recommendation-value/tasks.md +++ b/openspec/changes/archive/2026-08-09-prove-project-recommendation-value/tasks.md @@ -27,4 +27,17 @@ - [x] 5.1 Update current product, status, architecture, route, and analytics documentation; remove remaining shipped-state and digest-era inconsistencies. - [x] 5.2 Run targeted tests, typecheck, lint, docs checks, strict OpenSpec validation, and the production Cloudflare build. -- [ ] 5.3 Complete preserve-lane responsive review at 390, 768, and 1440 pixels, critique, polish, audit, detector review, and the design receipt. +- [x] 5.3 Complete preserve-lane responsive review at 390, 768, and 1440 pixels, critique, polish, audit, detector review, and the design receipt. + +## 6. Production-audit hardening + +- [x] 6.1 Fix Discover and Library request cancellation and add a regression guard for stable filter changes. +- [x] 6.2 Fuse bounded semantic and lexical candidates for public Discover with an honest lexical fallback. +- [x] 6.3 Require meaningful peer evidence, exclude language and weak detections, and require corroboration for grounded tool recommendations. +- [x] 6.4 Keep catalog previews public while requiring the existing GitHub session token for uncataloged lookups; preserve the post-login repository handoff. +- [x] 6.5 Add bounded API pagination and server-side filtering to Tool Intelligence repository evidence. +- [x] 6.6 Move repository detail into the shared shell, repair landmarks/headings/back navigation, and remove the peripheral discussion UI without deleting stored data. +- [x] 6.7 Prevent the signed-out Projects workspace flash and repair Web Vitals CSP/version observability. +- [x] 6.8 Expand Playwright coverage to the actual Astro landing and mocked core public journeys at desktop and mobile widths. +- [x] 6.9 Remove strict-Knip findings and the non-compliant bounce easing advisory. +- [x] 6.10 Run targeted tests after each lane, then full lint, typecheck, coverage, E2E, docs, strict Knip, Cloudflare build, diff validation, and production-profile browser review. diff --git a/openspec/changes/prove-project-recommendation-value/specs/public-discovery/spec.md b/openspec/changes/prove-project-recommendation-value/specs/public-discovery/spec.md deleted file mode 100644 index 902d85a..0000000 --- a/openspec/changes/prove-project-recommendation-value/specs/public-discovery/spec.md +++ /dev/null @@ -1,19 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Public landing handoff - -The Starboard landing page SHALL make a public project preview the primary -first-value action and SHALL preserve direct public Discover browsing as an -alternative. - -#### Scenario: Guest submits a public project - -- **WHEN** a visitor enters a GitHub URL or `owner/repository` on the landing - page -- **THEN** the visitor reaches a working read-only project preview without - first being required to sign in - -#### Scenario: Guest follows the public browsing alternative - -- **WHEN** a visitor selects the landing-page public browsing action -- **THEN** the visitor reaches `/discover` and can use the shared corpus diff --git a/openspec/specs/github-project-connections/spec.md b/openspec/specs/github-project-connections/spec.md index d1e510b..68ee4f3 100644 --- a/openspec/specs/github-project-connections/spec.md +++ b/openspec/specs/github-project-connections/spec.md @@ -4,9 +4,7 @@ Define how developers connect public GitHub repositories as durable project context for project-aware repository and tool discovery. - ## Requirements - ### Requirement: Connect a public GitHub project Starboard SHALL allow an authenticated user to connect a public GitHub @@ -88,3 +86,32 @@ entitlement checks. - **WHEN** an authenticated user connects a supported public repository - **THEN** the complete project-intelligence workflow is available without a checkout, plan selection, usage credit, or premium lock + +### Requirement: Choose a public repository from GitHub + +Starboard SHALL let an authenticated user load and select public repositories +available through the existing GitHub token while retaining manual URL entry. + +#### Scenario: User opens the repository picker + +- **WHEN** a signed-in user asks to choose from GitHub +- **THEN** Starboard fetches a bounded list of public repositories using the + existing OAuth token and does not request a broader scope + +#### Scenario: Picker is unavailable + +- **WHEN** GitHub cannot return the repository list +- **THEN** Starboard keeps manual public URL entry usable and shows a recovery + message for the picker + +### Requirement: Privacy-safe project activation measurement + +Starboard SHALL measure the project-connection and recommendation activation +funnel without emitting repository names, repository identifiers, query text, +or GitHub access tokens. + +#### Scenario: Project is connected + +- **WHEN** a signed-in user successfully connects a project +- **THEN** Starboard records the connection source as picker or manual entry + without recording repository identity diff --git a/openspec/specs/public-discovery/spec.md b/openspec/specs/public-discovery/spec.md index 65771e0..fc2643b 100644 --- a/openspec/specs/public-discovery/spec.md +++ b/openspec/specs/public-discovery/spec.md @@ -4,9 +4,7 @@ Define the unauthenticated Starboard discovery experience and the boundary between public repository browsing and signed-in personalization. - ## Requirements - ### Requirement: Public Discover browsing Starboard SHALL allow unauthenticated visitors to browse, search, sort, filter, @@ -54,12 +52,26 @@ authenticated while preserving the same public Discover corpus for guests. ### Requirement: Public landing handoff -The Starboard landing page SHALL link guests directly to the working public -Discover surface as the alternative to connecting GitHub. +The Starboard landing page SHALL make a public project preview the primary +first-value action and SHALL preserve direct public Discover browsing as an +alternative. + +#### Scenario: Guest submits a cataloged public project + +- **WHEN** a visitor enters a cataloged GitHub URL or `owner/repository` on the + landing page +- **THEN** the visitor reaches a working read-only project preview without + first being required to sign in + +#### Scenario: Guest submits an uncataloged public project + +- **WHEN** a visitor enters a repository that is not yet in Starboard's catalog +- **THEN** Starboard preserves the repository and asks for GitHub sign-in before + making the external lookup #### Scenario: Guest follows the public CTA -- **WHEN** a visitor selects the landing-page public browsing CTA +- **WHEN** a visitor selects the landing-page public browsing action - **THEN** the visitor reaches `/discover` and can use the shared corpus ### Requirement: Connected project discovery handoff @@ -78,3 +90,20 @@ visible. - **WHEN** a user selects a project recommendation - **THEN** Starboard opens the existing public repository detail surface + +### Requirement: Public search is stable and meaning-aware + +Discover SHALL combine bounded semantic and lexical candidates for relevance +search and SHALL NOT surface an error merely because its own filter state +changed. + +#### Scenario: Visitor changes a search or filter + +- **WHEN** a new first-page request replaces an older Discover request +- **THEN** Starboard cancels only the stale request and renders the newest result + without requiring a manual retry + +#### Scenario: Semantic retrieval is unavailable + +- **WHEN** Workers AI or Vectorize cannot serve a Discover query +- **THEN** Starboard returns bounded lexical results and does not fail the page diff --git a/openspec/specs/public-project-preview/spec.md b/openspec/specs/public-project-preview/spec.md new file mode 100644 index 0000000..cdafc05 --- /dev/null +++ b/openspec/specs/public-project-preview/spec.md @@ -0,0 +1,63 @@ +# public-project-preview Specification + +## Purpose +Let a visitor test Starboard's project-aware discovery with public repository +context before creating an account or persisting a project connection. +## Requirements +### Requirement: Preview a public GitHub project before sign-in + +Starboard SHALL accept a public GitHub URL or `owner/repository` from an +unauthenticated visitor and return a bounded, read-only project-intelligence +preview without creating user-owned data. + +#### Scenario: Cataloged public repository + +- **WHEN** a guest previews a public repository already present in the catalog +- **THEN** Starboard uses the cataloged metadata and evidence to show explained + similar projects without requiring sign-in + +#### Scenario: Uncataloged public repository + +- **WHEN** a guest previews a valid public repository not yet in the catalog +- **THEN** Starboard asks for the existing minimal GitHub sign-in, preserves the + repository value, and performs no anonymous GitHub API lookup + +#### Scenario: Signed-in uncataloged public repository + +- **WHEN** a signed-in user previews a public repository not yet in the catalog +- **THEN** Starboard resolves it with the user's existing GitHub token and shows + the best available explained preview without persisting a project connection + +#### Scenario: Invalid or unavailable repository + +- **WHEN** the supplied value is invalid, private, missing, or unavailable +- **THEN** Starboard returns a recovery message that states the accepted public + repository format and does not imply private-repository access + +### Requirement: Preview preserves an honest connection boundary + +Starboard MUST distinguish transient preview context from a durable connected +project and SHALL require GitHub sign-in before saving the project. + +#### Scenario: Guest wants to keep the previewed project + +- **WHEN** a guest selects the connect action from a successful preview +- **THEN** Starboard sends the guest through GitHub sign-in and returns them to + Projects with the public repository value ready to connect + +#### Scenario: Guest only inspects the preview + +- **WHEN** a guest views recommendations and leaves without signing in +- **THEN** Starboard creates no user project, list, note, or saved-repository row + +### Requirement: Preview work is bounded and degradable + +Starboard SHALL bound external resolution and recommendation work so a preview +cannot trigger bulk GitHub or unbounded catalog operations. + +#### Scenario: Recommendation infrastructure is partially unavailable + +- **WHEN** semantic retrieval or GitHub resolution is temporarily unavailable +- **THEN** Starboard either uses available catalog evidence or returns a clear + retry state without fabricating recommendations + diff --git a/openspec/specs/repo-tool-intelligence/spec.md b/openspec/specs/repo-tool-intelligence/spec.md index 6bb45ae..bb2efa9 100644 --- a/openspec/specs/repo-tool-intelligence/spec.md +++ b/openspec/specs/repo-tool-intelligence/spec.md @@ -82,9 +82,21 @@ recommendation context while preserving confidence and evidence limitations. ### Requirement: Similar repositories ground tool recommendations -The system SHALL rank similar repositories before recommending tools and SHALL -derive each recommended tool only from normalized detections on those ranked -peer repositories. +The system SHALL retrieve candidate peers from the eligible public catalog, +rank similar repositories before recommending tools, and derive each +recommended tool only from normalized detections on those ranked peers. + +#### Scenario: Semantic and lexical evidence are available + +- **WHEN** a project has enough public context for both retrieval paths +- **THEN** Starboard combines bounded semantic and lexical candidates from the + eligible catalog before deterministic evidence reranking + +#### Scenario: Semantic retrieval is unavailable + +- **WHEN** the semantic index or embedding path cannot serve a project +- **THEN** Starboard uses bounded lexical and structured catalog candidates and + labels any broad fallback honestly #### Scenario: Peer repositories contain additional tools @@ -93,6 +105,17 @@ peer repositories. - **THEN** Starboard recommends those tools with the exact peer repositories and detection confidence that support the recommendation +#### Scenario: Only primary language overlaps + +- **WHEN** a candidate shares only the connected project's primary language +- **THEN** Starboard does not admit it as a grounded similar project + +#### Scenario: Tool evidence is weak or metadata-only + +- **WHEN** a peer detection is repository language metadata, below the grounded + confidence floor, or supported by only one peer +- **THEN** Starboard does not present it as a tool to evaluate + #### Scenario: Tool is already used by the connected project - **WHEN** a peer repository contains a tool already detected in the connected @@ -104,3 +127,42 @@ peer repositories. - **WHEN** no project-specific similarity signal exists - **THEN** Starboard may show broad repository discovery but SHALL NOT present tools from those repositories as grounded recommendations + +### Requirement: Explain retrieval and recommendation strength + +Starboard SHALL expose enough non-sensitive context for a user to distinguish a +specific grounded recommendation from a degraded or broad result. + +#### Scenario: Grounded tool is shown + +- **WHEN** Starboard recommends a tool from similar repositories +- **THEN** the interface shows peer support, source confidence, and a direct + path to inspect the existing Tool Intelligence detail + +#### Scenario: Candidate retrieval degrades + +- **WHEN** one retrieval source is unavailable +- **THEN** the result describes the retrieval mode without claiming a stronger + semantic match than the system produced + +### Requirement: Privacy-safe recommendation feedback + +Starboard SHALL let a user mark repository and tool recommendations useful or +not useful without sending repository identity or free-form text to analytics. + +#### Scenario: User rates a recommendation + +- **WHEN** a user selects useful or not useful +- **THEN** Starboard records only recommendation kind, sentiment, rank bucket, + retrieval mode, and aggregate evidence buckets + +### Requirement: Tool evidence collections remain bounded + +Tool Intelligence SHALL paginate repository evidence and SHALL apply repository +filtering before pagination. + +#### Scenario: A tool appears in hundreds of repositories + +- **WHEN** a user opens that tool's detail page +- **THEN** Starboard renders one bounded page and offers an explicit way to load + more without placing the full collection in the DOM diff --git a/package.json b/package.json index 10c7c3d..4429d91 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "db:enrich-tools": "tsx scripts/enrich-tools.ts", "cf:build": "next build --webpack && node scripts/run-inline-critical-css.mjs && opennextjs-cloudflare build --skipNextBuild && opennextjs-cloudflare populateCache local && pnpm --filter ./landing-astro build && node scripts/run-overlay-astro-landing.mjs", "build:cf": "next build --webpack && node scripts/run-inline-critical-css.mjs && opennextjs-cloudflare build --skipNextBuild && opennextjs-cloudflare populateCache local && pnpm --filter ./landing-astro build && node scripts/run-overlay-astro-landing.mjs", + "build:e2e": "next build --webpack && node scripts/run-inline-critical-css.mjs && opennextjs-cloudflare build --skipNextBuild && pnpm --filter ./landing-astro build && node scripts/run-overlay-astro-landing.mjs", "preview:cf": "pnpm build:cf && opennextjs-cloudflare preview", "deploy:cf": "pnpm build:cf && opennextjs-cloudflare deploy --tag \"$(git rev-parse HEAD)\"", "cf:typegen": "wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts", diff --git a/playwright.config.ts b/playwright.config.ts index 4598fd3..c9f2c61 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,37 +1,47 @@ /** - * Playwright config — desktop + mobile-viewport projects. - * - * The `mobile` project uses the iPhone 13 device descriptor (390px wide — the - * fleet Wave 4 mobile target) so mobile-layout regressions are caught in CI - * alongside the `desktop` baseline. - * - * Run only the mobile project: pnpm exec playwright test --project=mobile - * - * NOTE: `@playwright/test` is not yet a dependency. Install it before running: - * pnpm add -D @playwright/test && pnpm exec playwright install --with-deps + * Browser coverage for both production surfaces: + * - the Astro landing page that is overlaid at `/` during `build:cf` + * - the Next.js application shell and public product journeys */ import { defineConfig, devices } from '@playwright/test'; +const appURL = 'http://127.0.0.1:8787'; +const browserPreview = + 'pnpm exec opennextjs-cloudflare preview --var AUTH_SECRET:starboard-browser-test-secret-at-least-32-characters --var AUTH_GITHUB_ID:browser-test-client --var AUTH_GITHUB_SECRET:browser-test-secret'; + export default defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, reporter: 'list', - use: { - baseURL: 'http://localhost:3000', - trace: 'on-first-retry', - }, + use: { trace: 'on-first-retry' }, webServer: { - command: 'pnpm dev', - url: 'http://localhost:3000', + command: process.env.CI ? browserPreview : `pnpm build:e2e && ${browserPreview}`, + url: appURL, reuseExistingServer: !process.env.CI, - timeout: 120_000, + timeout: 240_000, }, projects: [ - // Desktop baseline. - { name: 'desktop', use: { ...devices['Desktop Chrome'] } }, - // Mobile-viewport project — iPhone 13 is 390px wide, the mobile target. - { name: 'mobile', use: { ...devices['iPhone 13'] } }, + { + name: 'desktop', + testMatch: /public-app\.spec\.ts/, + use: { ...devices['Desktop Chrome'], baseURL: appURL }, + }, + { + name: 'mobile', + testMatch: /public-app\.spec\.ts/, + use: { ...devices['iPhone 13'], baseURL: appURL }, + }, + { + name: 'landing-desktop', + testMatch: /landing-mobile\.spec\.ts/, + use: { ...devices['Desktop Chrome'], baseURL: appURL }, + }, + { + name: 'landing-mobile', + testMatch: /landing-mobile\.spec\.ts/, + use: { ...devices['iPhone 13'], baseURL: appURL }, + }, ], }); diff --git a/public/api-ai.json b/public/api-ai.json index 048b72b..7a10f31 100644 --- a/public/api-ai.json +++ b/public/api-ai.json @@ -30,7 +30,7 @@ "url": "https://starboard.codevetter.com/project-preview", "md": "https://starboard.codevetter.com/project-preview.md", "kind": "static", - "description": "Try project-aware recommendations before sign-in without saving data" + "description": "Preview cataloged recommendations before sign-in without saving data; uncataloged lookups require GitHub sign-in" }, { "id": "tools", diff --git a/public/index.md b/public/index.md index 3bb605a..5afad98 100644 --- a/public/index.md +++ b/public/index.md @@ -4,7 +4,7 @@ Project-aware GitHub repository discovery and tool intelligence. ## Public without sign-in -- Preview a public GitHub project without creating an account or saving data +- Preview a cataloged public GitHub project without creating an account or saving data - Browse and search the seeded Discover corpus - Inspect detected tool and framework intelligence - Read the product history, scope, privacy policy, and terms diff --git a/public/llms-full.txt b/public/llms-full.txt index b011225..1b63f40 100644 --- a/public/llms-full.txt +++ b/public/llms-full.txt @@ -10,7 +10,7 @@ Project-aware GitHub repository discovery and tool intelligence. ## Public without sign-in -- Preview a public GitHub project without creating an account or saving data +- Preview a cataloged public GitHub project without creating an account or saving data - Browse and search the seeded Discover corpus - Inspect detected tool and framework intelligence - Read the product history, scope, privacy policy, and terms @@ -30,7 +30,7 @@ Project-aware GitHub repository discovery and tool intelligence. ## Product links - Home: https://starboard.codevetter.com/ — Product -- Project Preview: https://starboard.codevetter.com/project-preview — Try project-aware recommendations before sign-in +- Project Preview: https://starboard.codevetter.com/project-preview — Preview cataloged recommendations before sign-in; new lookups require GitHub sign-in - Discover: https://starboard.codevetter.com/discover — Browse and search the seeded public repository corpus - Tools: https://starboard.codevetter.com/tools — Detected tool and framework intelligence - Changelog: https://starboard.codevetter.com/changelog — Verified product history diff --git a/public/llms.txt b/public/llms.txt index e2200ed..1d87fd9 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -5,7 +5,7 @@ ## Product - [Home](https://starboard.codevetter.com/): Product -- [Project Preview](https://starboard.codevetter.com/project-preview): Try project-aware recommendations before sign-in +- [Project Preview](https://starboard.codevetter.com/project-preview): Preview cataloged recommendations before sign-in; new lookups require GitHub sign-in - [Discover](https://starboard.codevetter.com/discover): Browse and search the seeded public repository corpus - [Tools](https://starboard.codevetter.com/tools): Detected tool and framework intelligence - [Changelog](https://starboard.codevetter.com/changelog): Verified product history diff --git a/public/project-preview.md b/public/project-preview.md index 979f782..be21d8c 100644 --- a/public/project-preview.md +++ b/public/project-preview.md @@ -1,13 +1,14 @@ # Public Project Preview Paste a public GitHub URL or `owner/repository` at - to inspect a bounded sample of -similar projects and peer-grounded tools before sign-in. + to inspect a bounded sample +of similar projects and peer-grounded tools. Cataloged repositories preview +before sign-in; uncataloged lookups use the user's existing GitHub session. The preview checks Starboard's public catalog first and may resolve one public -GitHub repository on a catalog miss. It creates no project connection, list, -note, or saved repository. GitHub sign-in is required only when the visitor -chooses to connect the project. +GitHub repository on an authenticated catalog miss. It creates no project +connection, list, note, or saved repository. Connecting remains a separate +explicit action. Recommendations combine available semantic, catalog text, and language evidence before deterministic reranking. Degraded and broad-fallback states are labeled. diff --git a/src/__tests__/abortable-fetch.test.ts b/src/__tests__/abortable-fetch.test.ts new file mode 100644 index 0000000..65ae7c9 --- /dev/null +++ b/src/__tests__/abortable-fetch.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { replaceAbortableJsonRequest } from '@/lib/abortable-fetch'; + +describe('replaceAbortableJsonRequest', () => { + it('aborts only the stale request and leaves the newest request alive', async () => { + const pending: Array<{ + signal: AbortSignal; + resolve: (value: { ok: boolean; json: () => Promise }) => void; + }> = []; + vi.stubGlobal( + 'fetch', + vi.fn( + (_url: string, init?: RequestInit) => + new Promise((resolve) => pending.push({ signal: init!.signal as AbortSignal, resolve })) + ) + ); + const ref = { current: null as AbortController | null }; + + const first = replaceAbortableJsonRequest(ref, '/first'); + const second = replaceAbortableJsonRequest<{ page: number }>(ref, '/second'); + + expect(pending[0].signal.aborted).toBe(true); + expect(pending[1].signal.aborted).toBe(false); + + pending[1].resolve({ ok: true, json: async () => ({ page: 2 }) }); + await expect(second).resolves.toEqual({ page: 2 }); + expect(ref.current).toBeNull(); + + pending[0].resolve({ ok: true, json: async () => ({ page: 1 }) }); + await expect(first).resolves.toEqual({ page: 1 }); + }); +}); diff --git a/src/__tests__/discover-api.test.ts b/src/__tests__/discover-api.test.ts index 2233476..0151375 100644 --- a/src/__tests__/discover-api.test.ts +++ b/src/__tests__/discover-api.test.ts @@ -3,8 +3,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const mocks = vi.hoisted(() => ({ auth: vi.fn(), + embed: vi.fn(), execute: vi.fn(), batch: vi.fn(), + vectorQuery: vi.fn(), })); vi.mock('@/lib/auth', () => ({ auth: mocks.auth })); @@ -14,6 +16,10 @@ vi.mock('@/db', () => ({ batch: mocks.batch, }, })); +vi.mock('@/lib/embeddings', () => ({ generateEmbeddings: mocks.embed })); +vi.mock('@/lib/repo-vectors', () => ({ + repoVectors: () => ({ query: mocks.vectorQuery }), +})); import { GET } from '@/app/api/discover/route'; @@ -21,6 +27,8 @@ describe('GET /api/discover', () => { beforeEach(() => { vi.clearAllMocks(); mocks.auth.mockResolvedValue({ user: { githubId: 'user-1' } }); + mocks.embed.mockResolvedValue([[0.1, 0.2]]); + mocks.vectorQuery.mockResolvedValue([]); mocks.execute.mockResolvedValue({ rows: [ { @@ -118,6 +126,36 @@ describe('GET /api/discover', () => { expect(mocks.batch).not.toHaveBeenCalled(); }); + it('fuses semantic candidates ahead of lexical-only matches for relevance search', async () => { + mocks.vectorQuery.mockResolvedValue([{ repoId: 2, distance: 0.2 }]); + + const response = await GET( + new NextRequest('http://localhost/api/discover?q=vector%20database&sort=relevance') + ); + + expect(response.status).toBe(200); + expect(mocks.embed).toHaveBeenCalledWith(['vector database vector database']); + const mainQuery = mocks.execute.mock.calls[1]?.[0] as { sql: string; args: unknown[] }; + expect(mainQuery.sql).toContain('CASE r.id WHEN 2 THEN 0 WHEN 1 THEN 1'); + expect(mainQuery.args).toContain(JSON.stringify([2, 1])); + }); + + it('degrades to lexical Discover results when semantic retrieval fails', async () => { + mocks.embed.mockRejectedValueOnce(new Error('AI unavailable')); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + const response = await GET(new NextRequest('http://localhost/api/discover?q=database')); + + expect(response.status).toBe(200); + const mainQuery = mocks.execute.mock.calls[1]?.[0] as { sql: string; args: unknown[] }; + expect(mainQuery.args).toContain(JSON.stringify([1])); + expect(warn).toHaveBeenCalledWith( + 'Discover semantic retrieval unavailable; using lexical search', + expect.any(Error) + ); + warn.mockRestore(); + }); + // Regression guard: the eligibility filter must use the index-friendly // IN (UNION) form, not the OR EXISTS correlated subquery that forced // O(|repos| × |user_repos|) row reads per request (the 500M-row burn). diff --git a/src/__tests__/project-activation-ui.test.ts b/src/__tests__/project-activation-ui.test.ts index 0e7b36f..15a935d 100644 --- a/src/__tests__/project-activation-ui.test.ts +++ b/src/__tests__/project-activation-ui.test.ts @@ -12,7 +12,8 @@ describe('project-first activation wiring', () => { expect(landing).toContain('action="/project-preview"'); expect(landing).toContain('name="repository"'); - expect(landing).toContain('No sign-in to preview'); + expect(landing).toContain('Catalog matches preview without sign-in'); + expect(landing).toContain('uncataloged repositories use your signed-in GitHub session'); expect(landing).not.toContain('Library · this week'); expect(landing).not.toContain('>Hot<'); expect(landing).not.toContain('>Watch<'); @@ -41,6 +42,27 @@ describe('project-first activation wiring', () => { expect(topBar).toContain('className="size-11 shrink-0 sm:hidden"'); }); + it('reuses the shared shell and navigation across discovery surfaces', () => { + const discover = source('src/app/discover/discover-client.tsx'); + const discoverLayout = source('src/app/discover/layout.tsx'); + const tools = source('src/app/tools/page.tsx'); + const toolDetail = source('src/app/tools/[toolKey]/page.tsx'); + const toolsLayout = source('src/app/tools/layout.tsx'); + const topBar = source('src/components/top-bar.tsx'); + + expect(discoverLayout).toContain(''); + expect(toolsLayout).toContain(''); + expect(tools).toContain(' { const preview = source('src/components/project-preview-workspace.tsx'); const login = source('src/app/login/page.tsx'); diff --git a/src/__tests__/project-preview-api.test.ts b/src/__tests__/project-preview-api.test.ts index e040c33..1c0e57b 100644 --- a/src/__tests__/project-preview-api.test.ts +++ b/src/__tests__/project-preview-api.test.ts @@ -2,18 +2,21 @@ import { NextRequest } from 'next/server'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const mocks = vi.hoisted(() => ({ + auth: vi.fn(), resolve: vi.fn(), retrieve: vi.fn(), })); vi.mock('@/lib/project-preview', () => ({ resolveProjectPreview: mocks.resolve })); vi.mock('@/lib/project-intelligence', () => ({ retrieveProjectIntelligence: mocks.retrieve })); +vi.mock('@/lib/auth', () => ({ auth: mocks.auth })); import { GET } from '@/app/api/project-preview/route'; describe('public project preview API', () => { beforeEach(() => { vi.clearAllMocks(); + mocks.auth.mockResolvedValue(null); mocks.retrieve.mockResolvedValue({ similarProjects: [], recommendedTools: [], @@ -54,4 +57,26 @@ describe('public project preview API', () => { expect(payload).toMatchObject({ project, source: 'catalog', retrieval: { mode: 'hybrid' } }); expect(mocks.retrieve).toHaveBeenCalledWith(project, 12); }); + + it('does not spend anonymous GitHub quota on an uncataloged guest preview', async () => { + mocks.resolve.mockResolvedValue({ status: 'auth-required' }); + + const response = await GET( + new NextRequest('http://localhost/api/project-preview?repository=acme%2Fnew-repo') + ); + + expect(response.status).toBe(401); + expect(await response.json()).toMatchObject({ loginRequired: true }); + expect(mocks.resolve).toHaveBeenCalledWith('acme/new-repo', undefined); + expect(mocks.retrieve).not.toHaveBeenCalled(); + }); + + it('passes the existing GitHub token to an uncataloged lookup', async () => { + mocks.auth.mockResolvedValue({ accessToken: 'github-token' }); + mocks.resolve.mockResolvedValue({ status: 'unavailable' }); + + await GET(new NextRequest('http://localhost/api/project-preview?repository=acme%2Fmissing')); + + expect(mocks.resolve).toHaveBeenCalledWith('acme/missing', 'github-token'); + }); }); diff --git a/src/__tests__/project-preview.test.ts b/src/__tests__/project-preview.test.ts index 65e35e3..3c1a0db 100644 --- a/src/__tests__/project-preview.test.ts +++ b/src/__tests__/project-preview.test.ts @@ -49,7 +49,16 @@ describe('public project preview resolution', () => { expect(fetchProject).not.toHaveBeenCalled(); }); - it('resolves one public GitHub repository on a catalog miss without persisting it', async () => { + it('requires authentication before an uncataloged GitHub request', async () => { + const execute = vi.fn().mockResolvedValue(result([])); + const fetchProject = vi.fn(); + const resolve = createProjectPreviewResolver({ database: { execute }, fetchProject }); + + await expect(resolve('acme/sdk')).resolves.toEqual({ status: 'auth-required' }); + expect(fetchProject).not.toHaveBeenCalled(); + }); + + it('resolves one public GitHub repository with the existing session token', async () => { const execute = vi.fn().mockResolvedValue(result([])); const fetchProject = vi.fn().mockResolvedValue({ id: 2, @@ -68,12 +77,15 @@ describe('public project preview resolution', () => { }); const resolve = createProjectPreviewResolver({ database: { execute }, fetchProject }); - await expect(resolve('acme/sdk')).resolves.toMatchObject({ + await expect(resolve('acme/sdk', 'github-token')).resolves.toMatchObject({ status: 'resolved', source: 'github', project: { id: 2, fullName: 'acme/sdk', tools: [] }, }); expect(execute).toHaveBeenCalledTimes(1); - expect(fetchProject).toHaveBeenCalledTimes(1); + expect(fetchProject).toHaveBeenCalledWith( + { owner: 'acme', repo: 'sdk', fullName: 'acme/sdk' }, + 'github-token' + ); }); }); diff --git a/src/__tests__/project-recommendations.test.ts b/src/__tests__/project-recommendations.test.ts index a5fb70c..edec69c 100644 --- a/src/__tests__/project-recommendations.test.ts +++ b/src/__tests__/project-recommendations.test.ts @@ -79,17 +79,35 @@ describe('rankProjectRecommendations', () => { ).toEqual([]); }); + it('does not admit a peer from primary-language overlap alone', () => { + const project = repo({ id: 1, fullName: 'acme/app', language: 'TypeScript' }); + const unrelated = repo({ + id: 2, + fullName: 'oss/unrelated', + language: 'TypeScript', + stargazersCount: 100_000, + tools: [{ key: 'java', name: 'Java', category: 'language', confidence: 98 }], + }); + + const result = rankProjectRecommendations(project, [unrelated]); + + expect(result.fallback).toBe(true); + expect(result.recommendedTools).toEqual([]); + }); + it('derives new tools from similar repositories with exact provenance', () => { const project = repo({ id: 1, fullName: 'acme/webapp', language: 'TypeScript', + topics: ['testing'], tools: [{ key: 'nextjs', name: 'Next.js', category: 'framework', confidence: 98 }], }); const peerOne = repo({ id: 2, fullName: 'oss/peer-one', language: 'TypeScript', + topics: ['testing'], tools: [ { key: 'nextjs', name: 'Next.js', category: 'framework', confidence: 96 }, { key: 'vitest', name: 'Vitest', category: 'testing', confidence: 94 }, @@ -99,6 +117,7 @@ describe('rankProjectRecommendations', () => { id: 3, fullName: 'oss/peer-two', language: 'TypeScript', + topics: ['testing'], tools: [{ key: 'vitest', name: 'Vitest', category: 'testing', confidence: 88 }], }); @@ -158,6 +177,7 @@ describe('rankProjectRecommendations', () => { fullName: 'acme/app', language: 'TypeScript', topics: ['payments'], + tools: [{ key: 'stripe', name: 'Stripe', category: 'payments', confidence: 98 }], }); const strongest = repo({ id: 2, @@ -165,17 +185,20 @@ describe('rankProjectRecommendations', () => { language: 'TypeScript', topics: ['payments'], stargazersCount: 1, + tools: [{ key: 'stripe', name: 'Stripe', category: 'payments', confidence: 95 }], }); const popular = repo({ id: 3, fullName: 'oss/popular', language: 'TypeScript', + topics: ['payments'], stargazersCount: 100, }); const alphabetical = repo({ id: 4, fullName: 'oss/alphabetical', language: 'TypeScript', + topics: ['payments'], stargazersCount: 100, }); @@ -186,17 +209,24 @@ describe('rankProjectRecommendations', () => { ).toEqual(['oss/strongest', 'oss/alphabetical', 'oss/popular']); }); - it('orders grounded tools by peer support, score, and name while deduplicating evidence', () => { - const project = repo({ id: 1, fullName: 'acme/app', language: 'TypeScript' }); + it('requires corroborated, confident, non-language tool evidence', () => { + const project = repo({ + id: 1, + fullName: 'acme/app', + language: 'TypeScript', + topics: ['testing'], + }); const peerOne = repo({ id: 2, fullName: 'oss/one', language: 'TypeScript', + topics: ['testing'], tools: [ { key: 'alpha', name: 'Alpha', category: 'testing', confidence: 150 }, { key: 'alpha', name: 'Alpha', category: 'testing', confidence: 150 }, { key: 'beta', name: 'Beta', category: 'testing', confidence: 90 }, { key: 'delta', name: 'Delta', category: 'testing', confidence: 90 }, + { key: 'java', name: 'Java', category: 'language', confidence: 100 }, { key: '', name: 'Invalid', category: 'testing', confidence: 100 }, ], }); @@ -204,19 +234,64 @@ describe('rankProjectRecommendations', () => { id: 3, fullName: 'oss/two', language: 'TypeScript', + topics: ['testing'], tools: [ - { key: 'alpha', name: 'Alpha', category: 'testing', confidence: -5 }, + { key: 'alpha', name: 'Alpha', category: 'testing', confidence: 70 }, { key: 'gamma', name: 'Gamma', category: 'testing', confidence: 10 }, ], }); const tools = rankProjectRecommendations(project, [peerOne, peerTwo]).recommendedTools; - expect(tools.map((tool) => tool.name)).toEqual(['Alpha', 'Beta', 'Delta', 'Gamma']); + expect(tools.map((tool) => tool.name)).toEqual(['Alpha']); expect(tools[0].supportCount).toBe(2); expect(tools[0].sources).toHaveLength(2); }); + it('does not recommend a competing framework when the project already uses one', () => { + const project = repo({ + id: 1, + fullName: 'acme/react-app', + topics: ['frontend'], + tools: [{ key: 'react', name: 'React', category: 'framework', confidence: 98 }], + }); + const peers = [2, 3].map((id) => + repo({ + id, + fullName: `oss/peer-${id}`, + topics: ['frontend'], + tools: [{ key: 'angular', name: 'Angular', category: 'framework', confidence: 98 }], + }) + ); + + expect(rankProjectRecommendations(project, peers).recommendedTools).toEqual([]); + }); + + it('orders grounded tools by support, evidence score, then stable name', () => { + const project = repo({ id: 1, fullName: 'acme/app', topics: ['testing'] }); + const peers = [2, 3, 4].map((id) => + repo({ + id, + fullName: `oss/peer-${id}`, + topics: ['testing'], + tools: [ + { key: 'zulu', name: 'Zulu', category: 'testing', confidence: 90 }, + ...(id < 4 + ? [ + { key: 'beta', name: 'Beta', category: 'testing', confidence: 90 }, + { key: 'gamma', name: 'Gamma', category: 'testing', confidence: 80 }, + { key: 'alpha', name: 'Alpha', category: 'testing', confidence: 80 }, + ] + : []), + ], + }) + ); + + expect( + rankProjectRecommendations(project, peers).recommendedTools.map((tool) => tool.name) + ).toEqual(['Zulu', 'Beta', 'Alpha', 'Gamma']); + }); + it('handles empty metadata and clamps the requested peer limit', () => { const project = repo({ id: 1, fullName: 'acme/empty', name: '' }); const peers = Array.from({ length: 60 }, (_, index) => diff --git a/src/__tests__/similar-repos-api.test.ts b/src/__tests__/similar-repos-api.test.ts new file mode 100644 index 0000000..a8b0587 --- /dev/null +++ b/src/__tests__/similar-repos-api.test.ts @@ -0,0 +1,87 @@ +import { NextRequest } from 'next/server'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + auth: vi.fn(), + execute: vi.fn(), + queryByRepoId: vi.fn(), +})); + +vi.mock('@/lib/auth', () => ({ auth: mocks.auth })); +vi.mock('@/db', () => ({ db: { execute: mocks.execute } })); +vi.mock('@/lib/repo-vectors', () => ({ + repoVectors: () => ({ queryByRepoId: mocks.queryByRepoId }), +})); + +import { GET } from '@/app/api/repos/[repoId]/similar/route'; + +describe('GET /api/repos/[repoId]/similar', () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.auth.mockResolvedValue(null); + mocks.queryByRepoId.mockResolvedValue([{ repoId: 2, distance: 0.2 }]); + mocks.execute + .mockResolvedValueOnce({ + rows: [ + { + name: 'app', + full_name: 'acme/app', + description: 'A TypeScript commerce application', + language: 'TypeScript', + topics: '["commerce","nextjs"]', + }, + ], + }) + .mockResolvedValueOnce({ + rows: [ + { + id: 2, + name: 'store', + full_name: 'peer/store', + owner_login: 'peer', + owner_avatar: 'https://example.com/peer.png', + html_url: 'https://github.com/peer/store', + description: 'A TypeScript commerce platform', + language: 'TypeScript', + stargazers_count: 12_000, + archived: 0, + topics: '["commerce"]', + repo_updated_at: '2026-08-01T00:00:00Z', + }, + ], + }); + }); + + it('serves global similarity to a guest', async () => { + const response = await GET( + new NextRequest('http://localhost/api/repos/1/similar?scope=global'), + { params: Promise.resolve({ repoId: '1' }) } + ); + + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ + similar: [{ id: 2, full_name: 'peer/store', similarity: 0.8 }], + }); + expect(mocks.queryByRepoId).toHaveBeenCalledWith(1, 100); + }); + + it('fails closed only when a guest explicitly requests user scope', async () => { + const response = await GET(new NextRequest('http://localhost/api/repos/1/similar?scope=user'), { + params: Promise.resolve({ repoId: '1' }), + }); + + expect(response.status).toBe(401); + expect(mocks.execute).not.toHaveBeenCalled(); + expect(mocks.queryByRepoId).not.toHaveBeenCalled(); + }); + + it('keeps the public path available when optional auth lookup fails', async () => { + mocks.auth.mockRejectedValueOnce(new Error('Auth unavailable')); + + const response = await GET(new NextRequest('http://localhost/api/repos/1/similar'), { + params: Promise.resolve({ repoId: '1' }), + }); + + expect(response.status).toBe(200); + }); +}); diff --git a/src/__tests__/swr-fetcher.test.ts b/src/__tests__/swr-fetcher.test.ts index ce4cf95..6db115d 100644 --- a/src/__tests__/swr-fetcher.test.ts +++ b/src/__tests__/swr-fetcher.test.ts @@ -21,12 +21,14 @@ describe('jsonFetcher', () => { ok: false, status: 429, headers: { get: (name: string) => (name === 'Retry-After' ? '3' : null) }, + json: async () => ({ error: 'Try again shortly.' }), }) ); await expect(jsonFetcher('/x')).rejects.toMatchObject({ name: 'FetchHttpError', status: 429, retryAfterMs: 3000, + message: 'Try again shortly.', }); }); }); diff --git a/src/__tests__/tools-api.test.ts b/src/__tests__/tools-api.test.ts new file mode 100644 index 0000000..84441cb --- /dev/null +++ b/src/__tests__/tools-api.test.ts @@ -0,0 +1,89 @@ +import { NextRequest } from 'next/server'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + auth: vi.fn(), + execute: vi.fn(), +})); + +vi.mock('@/lib/auth', () => ({ auth: mocks.auth })); +vi.mock('@/db', () => ({ db: { execute: mocks.execute } })); + +import { GET } from '@/app/api/tools/route'; + +describe('GET /api/tools tool evidence pagination', () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.auth.mockResolvedValue(null); + mocks.execute + .mockResolvedValueOnce({ + rows: [ + { + tool_key: 'react', + tool_name: 'React', + category: 'framework', + repo_count: 120, + avg_confidence: 95, + max_confidence: 98, + }, + ], + }) + .mockResolvedValueOnce({ + rows: [ + { + id: 51, + name: 'app', + full_name: 'acme/app', + owner_login: 'acme', + owner_avatar: '', + html_url: 'https://github.com/acme/app', + description: 'A dashboard', + language: 'TypeScript', + stargazers_count: 20_000, + archived: 0, + topics: '[]', + repo_created_at: '2025-01-01T00:00:00Z', + repo_updated_at: '2026-01-01T00:00:00Z', + tool_key: 'react', + tool_name: 'React', + category: 'framework', + confidence: 98, + sources: '["package.json"]', + }, + ], + }); + }); + + it('filters before applying a bounded limit and offset', async () => { + const response = await GET( + new NextRequest('http://localhost/api/tools?tool=react&q=TypeScript&limit=500&offset=48') + ); + + expect(response.status).toBe(200); + const resultQuery = mocks.execute.mock.calls[1][0] as { sql: string; args: unknown[] }; + expect(resultQuery.sql).toContain("LOWER(r.full_name) LIKE ? ESCAPE '\\'"); + expect(resultQuery.sql).toContain('LIMIT ? OFFSET ?'); + expect(resultQuery.args.slice(-5)).toEqual([ + '%typescript%', + '%typescript%', + '%typescript%', + 100, + 48, + ]); + + expect(await response.json()).toMatchObject({ + tool: { repoCount: 120 }, + repos: [{ id: 51 }], + page: { offset: 48, limit: 100, hasMore: true }, + }); + }); + + it('keeps personal tool evidence protected', async () => { + mocks.execute.mockReset(); + + const response = await GET(new NextRequest('http://localhost/api/tools?tool=react&scope=user')); + + expect(response.status).toBe(401); + expect(mocks.execute).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/api/discover/route.ts b/src/app/api/discover/route.ts index 43676bd..e78a92f 100644 --- a/src/app/api/discover/route.ts +++ b/src/app/api/discover/route.ts @@ -3,9 +3,13 @@ import { type NextRequest, NextResponse } from 'next/server'; import { db } from '@/db'; import { auth } from '@/lib/auth'; -import { ftsSearchQuery } from '@/lib/search'; +import { generateEmbeddings } from '@/lib/embeddings'; +import { repoVectors } from '@/lib/repo-vectors'; +import { expandedSearchQuery, ftsSearchQuery, rrfFuse } from '@/lib/search'; const MIN_STARS_FLOOR = 5000; +const SEMANTIC_TOP_K = 100; +const SEMANTIC_DISTANCE_MAX = 0.7; // Index-friendly eligibility filter. The previous form // (r.stargazers_count >= ? OR EXISTS (SELECT 1 FROM user_repos ...)) // forced SQLite to evaluate a correlated subquery against user_repos for @@ -51,7 +55,7 @@ export async function GET(request: NextRequest) { .map((value) => value.trim().toLowerCase()) .filter((value) => /^[a-z0-9][a-z0-9-]{0,63}$/.test(value)) || []; const listId = params.get('list_id'); - const sort = params.get('sort') || 'stars'; + const sort = params.get('sort') || (q ? 'relevance' : 'stars'); const limit = Math.min(Math.max(parseInt(params.get('limit') || '50', 10) || 50, 1), 200); const offset = Math.max(parseInt(params.get('offset') || '0', 10) || 0, 0); @@ -61,8 +65,8 @@ export async function GET(request: NextRequest) { let rankedRepoIds: number[] | null = null; if (q) { const lexicalQuery = ftsSearchQuery(q); - const lexIds = lexicalQuery - ? await db + const lexIdsPromise = lexicalQuery + ? db .execute({ sql: `SELECT r.id, MIN(rank) AS best_rank @@ -85,12 +89,29 @@ export async function GET(request: NextRequest) { args: [lexicalQuery, lexicalQuery, MIN_STARS_FLOOR], }) .then((result) => result.rows.map((r) => r.id as number)) - : []; + : Promise.resolve([] as number[]); - if (lexIds.length > 0) { - rankedRepoIds = lexIds; + const semanticIdsPromise = (async () => { + try { + const [embedding] = await generateEmbeddings([expandedSearchQuery(q)]); + if (!embedding) return []; + const matches = await repoVectors().query(embedding, SEMANTIC_TOP_K); + return matches + .filter((match) => match.distance <= SEMANTIC_DISTANCE_MAX) + .map((match) => match.repoId); + } catch (error) { + console.warn('Discover semantic retrieval unavailable; using lexical search', error); + return []; + } + })(); + + const [lexIds, semanticIds] = await Promise.all([lexIdsPromise, semanticIdsPromise]); + const searchIds = rrfFuse([semanticIds, lexIds]); + + if (searchIds.length > 0) { + rankedRepoIds = searchIds; whereClauses.push('r.id IN (SELECT CAST(value AS INTEGER) FROM json_each(?))'); - whereArgs.push(JSON.stringify(lexIds)); + whereArgs.push(JSON.stringify(searchIds)); } else { whereClauses.push('0 = 1'); } diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index afec577..4453d84 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,7 +1,23 @@ +import { getCloudflareContext } from '@opennextjs/cloudflare'; import { NextResponse } from 'next/server'; import { db } from '@/db'; +interface WorkerVersionMetadata { + id?: string; + tag?: string; + timestamp?: string; +} + +function workerVersionMetadata(): WorkerVersionMetadata | null { + try { + const { env } = getCloudflareContext(); + return (env as { CF_VERSION_METADATA?: WorkerVersionMetadata }).CF_VERSION_METADATA ?? null; + } catch { + return null; + } +} + /** * Public health endpoint for the Starboard Cloudflare Worker. * @@ -38,7 +54,13 @@ export async function GET() { console.error('Starboard search health probe failed', err); } - const revision = process.env.CF_PAGES_COMMIT_SHA || process.env.NEXT_PUBLIC_REVISION || 'unknown'; + const version = workerVersionMetadata(); + const revision = + process.env.CF_PAGES_COMMIT_SHA || + process.env.NEXT_PUBLIC_REVISION || + version?.tag || + version?.id || + 'unknown'; const ragConfigured = Boolean(process.env.RAG_SERVICE_KEY && process.env.STARBOARD_RAG_INDEX_ID); @@ -48,7 +70,7 @@ export async function GET() { build: { name: 'starboard', revision, - branch: process.env.CF_PAGES_BRANCH ?? 'unknown', + branch: process.env.CF_PAGES_BRANCH ?? (version ? 'worker' : 'unknown'), }, live: true, revision, diff --git a/src/app/api/project-preview/route.ts b/src/app/api/project-preview/route.ts index f8b6507..fffae82 100644 --- a/src/app/api/project-preview/route.ts +++ b/src/app/api/project-preview/route.ts @@ -1,13 +1,21 @@ import { type NextRequest, NextResponse } from 'next/server'; import { GitHubProjectApiError } from '@/lib/github-projects'; +import { auth } from '@/lib/auth'; import { retrieveProjectIntelligence } from '@/lib/project-intelligence'; import { resolveProjectPreview } from '@/lib/project-preview'; export async function GET(request: NextRequest) { const repository = request.nextUrl.searchParams.get('repository') ?? ''; try { - const resolution = await resolveProjectPreview(repository); + let accessToken: string | undefined; + try { + accessToken = (await auth())?.accessToken; + } catch { + // Catalog previews are public and must remain available when optional + // session hydration is unavailable. + } + const resolution = await resolveProjectPreview(repository, accessToken); if (resolution.status === 'invalid') { return NextResponse.json( { error: 'Enter a public GitHub URL or owner/repository.' }, @@ -20,6 +28,15 @@ export async function GET(request: NextRequest) { { status: 404 } ); } + if (resolution.status === 'auth-required') { + return NextResponse.json( + { + error: 'Sign in with GitHub to preview a repository outside the public catalog.', + loginRequired: true, + }, + { status: 401 } + ); + } const recommendations = await retrieveProjectIntelligence(resolution.project, 12); return NextResponse.json( diff --git a/src/app/api/repos/[repoId]/route.ts b/src/app/api/repos/[repoId]/route.ts index 097a4c3..a413a5a 100644 --- a/src/app/api/repos/[repoId]/route.ts +++ b/src/app/api/repos/[repoId]/route.ts @@ -1,8 +1,6 @@ -import type { InStatement } from '@/db/client'; import { type NextRequest, NextResponse } from 'next/server'; import { db } from '@/db'; -import { auth } from '@/lib/auth'; import { resolveRepoId } from '../resolve'; @@ -111,27 +109,6 @@ export async function GET( const row = repoResult.rows[0]; - const session = await auth(); - const userId = session?.user?.githubId ?? null; - - const countQueries: InStatement[] = [ - { sql: 'SELECT COUNT(*) as count FROM likes WHERE repo_id = ?', args: [repoId] }, - { sql: 'SELECT COUNT(*) as count FROM comments WHERE repo_id = ?', args: [repoId] }, - ]; - - if (userId) { - countQueries.push({ - sql: 'SELECT 1 as liked FROM likes WHERE user_id = ? AND repo_id = ?', - args: [userId, repoId], - }); - } - - const batchResults = await db.batch(countQueries); - - const likeCount = batchResults[0].rows[0].count as number; - const commentCount = batchResults[1].rows[0].count as number; - const userLiked = userId ? batchResults[2].rows.length > 0 : false; - return NextResponse.json({ repo: { id: row.id as number, @@ -148,9 +125,6 @@ export async function GET( repo_created_at: row.repo_created_at as string | null, repo_updated_at: row.repo_updated_at as string | null, }, - likeCount, - commentCount, - userLiked, }); } catch (error) { console.error('Failed to fetch repo detail:', error); diff --git a/src/app/api/repos/[repoId]/similar/route.ts b/src/app/api/repos/[repoId]/similar/route.ts index 4c789fb..4b697e8 100644 --- a/src/app/api/repos/[repoId]/similar/route.ts +++ b/src/app/api/repos/[repoId]/similar/route.ts @@ -34,11 +34,12 @@ export async function GET( request: NextRequest, { params }: { params: Promise<{ repoId: string }> } ) { - const session = await auth(); - if (!session?.user?.githubId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + let userId: string | null = null; + try { + userId = (await auth())?.user?.githubId ?? null; + } catch { + // Global similarity is public. Only the explicit user scope fails closed. } - const userId = session.user.githubId; const { repoId: rawId } = await params; const repoId = parseInt(rawId, 10); @@ -49,6 +50,9 @@ export async function GET( const limitParam = request.nextUrl.searchParams.get('limit'); const limit = Math.min(Math.max(parseInt(limitParam || '', 10) || DEFAULT_LIMIT, 1), 30); const scope = request.nextUrl.searchParams.get('scope') || 'global'; // "user" | "global" + if (scope === 'user' && !userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } try { // 1. Confirm the relational vector metadata exists and load lightweight @@ -104,7 +108,7 @@ export async function GET( FROM user_repos ur JOIN repos r ON r.id = ur.repo_id WHERE ur.user_id = ? AND r.id IN (${placeholders})`; - return db.execute({ sql, args: scope === 'global' ? chunk : [userId, ...chunk] }); + return db.execute({ sql, args: scope === 'global' ? chunk : [userId!, ...chunk] }); }) ); const hydratedRows = hydratedResults.flatMap((result) => result.rows); diff --git a/src/app/api/tools/route.ts b/src/app/api/tools/route.ts index 39d3208..d7751ea 100644 --- a/src/app/api/tools/route.ts +++ b/src/app/api/tools/route.ts @@ -20,6 +20,10 @@ function parseScope(value: string | null): ToolScope { return value === 'user' || value === 'all' ? value : 'discover'; } +function likePattern(value: string): string { + return `%${value.toLowerCase().replace(/[\\%_]/g, '\\$&')}%`; +} + function scopeClause(scope: ToolScope, userId: string | null, minStars: number) { if (scope === 'user') { if (!userId) return null; @@ -66,8 +70,14 @@ export async function GET(request: NextRequest) { 100 ); const minStars = Math.max(parseInt(params.get('min_stars') || '10000', 10) || 10000, 0); - const limit = Math.min(Math.max(parseInt(params.get('limit') || '80', 10) || 80, 1), 500); const tool = params.get('tool')?.trim() || null; + const limitCeiling = tool ? 100 : 500; + const limit = Math.min( + Math.max(parseInt(params.get('limit') || '80', 10) || 80, 1), + limitCeiling + ); + const offset = Math.max(parseInt(params.get('offset') || '0', 10) || 0, 0); + const query = params.get('q')?.trim().slice(0, 120) || null; const scopeSql = scopeClause(scope, session?.user?.githubId ?? null, minStars); if (!scopeSql) { @@ -75,6 +85,14 @@ export async function GET(request: NextRequest) { } if (tool) { + const querySql = query + ? `AND ( + LOWER(r.full_name) LIKE ? ESCAPE '\\' + OR LOWER(COALESCE(r.description, '')) LIKE ? ESCAPE '\\' + OR LOWER(COALESCE(r.language, '')) LIKE ? ESCAPE '\\' + )` + : ''; + const queryArgs = query ? Array(3).fill(likePattern(query)) : []; const summary = await db.execute({ sql: `SELECT rt.tool_key, rt.tool_name, @@ -89,8 +107,9 @@ export async function GET(request: NextRequest) { AND rt.confidence >= ? AND rt.category != 'language' AND ${scopeSql.where} + ${querySql} GROUP BY rt.tool_key, rt.tool_name, rt.category`, - args: [...scopeSql.joinArgs, tool, minConfidence, ...scopeSql.whereArgs], + args: [...scopeSql.joinArgs, tool, minConfidence, ...scopeSql.whereArgs, ...queryArgs], }); const summaryRow = summary.rows[0]; const definition = getToolDefinition(tool); @@ -120,10 +139,20 @@ export async function GET(request: NextRequest) { AND rt.confidence >= ? AND rt.category != 'language' AND ${scopeSql.where} + ${querySql} ORDER BY rt.confidence DESC, r.stargazers_count DESC - LIMIT ?`, - args: [...scopeSql.joinArgs, tool, minConfidence, ...scopeSql.whereArgs, limit], + LIMIT ? OFFSET ?`, + args: [ + ...scopeSql.joinArgs, + tool, + minConfidence, + ...scopeSql.whereArgs, + ...queryArgs, + limit, + offset, + ], }); + const repoCount = (summaryRow?.repo_count as number | undefined) ?? 0; return json({ scope, @@ -135,7 +164,7 @@ export async function GET(request: NextRequest) { toolName: (summaryRow?.tool_name as string | undefined) ?? definition?.name ?? tool, category: (summaryRow?.category as string | undefined) ?? definition?.category ?? 'library', url: getToolUrl((summaryRow?.tool_key as string | undefined) ?? definition?.key ?? tool), - repoCount: (summaryRow?.repo_count as number | undefined) ?? 0, + repoCount, avgConfidence: summaryRow ? Math.round(summaryRow.avg_confidence as number) : 0, maxConfidence: (summaryRow?.max_confidence as number | undefined) ?? 0, }, @@ -169,6 +198,11 @@ export async function GET(request: NextRequest) { sources: JSON.parse((row.sources as string) || '[]') as string[], }, })), + page: { + offset, + limit, + hasMore: offset + result.rows.length < repoCount, + }, }); } diff --git a/src/app/discover/discover-client.tsx b/src/app/discover/discover-client.tsx index 859e638..aeceba4 100644 --- a/src/app/discover/discover-client.tsx +++ b/src/app/discover/discover-client.tsx @@ -116,7 +116,9 @@ function DiscoverContent({ const [searchQuery, setSearchQuery] = useQueryState('q', parseAsString.withDefault('')); const [sortBy, setSortBy] = useQueryState( 'sort', - parseAsStringLiteral(sortOptions).withDefault('most-stars') + parseAsStringLiteral(sortOptions).withDefault( + initialUrl.includes('q=') ? 'relevance' : 'most-stars' + ) ); const [selectedLanguages, setSelectedLanguages] = useQueryState( 'lang', @@ -216,6 +218,17 @@ function DiscoverContent({ setSelectedListId(null); }, [setSearchQuery, setSelectedLanguages, setSelectedTools, setSelectedListId]); + const handleSearchChange = useCallback( + (value: string) => { + const wasEmpty = searchQuery.trim().length === 0; + const isEmpty = value.trim().length === 0; + setSearchQuery(value); + if (wasEmpty && !isEmpty && sortBy === 'most-stars') setSortBy('relevance'); + if (!wasEmpty && isEmpty && sortBy === 'relevance') setSortBy('most-stars'); + }, + [searchQuery, setSearchQuery, setSortBy, sortBy] + ); + const handleLanguageToggle = useCallback( (language: string) => { setSelectedLanguages((prev) => @@ -293,7 +306,7 @@ function DiscoverContent({ <> { if (sort !== 'recently-starred') setSortBy(sort); @@ -303,6 +316,7 @@ function DiscoverContent({ onViewModeChange={setViewMode} onMenuClick={() => setSidebarOpen(true)} repoCount={total} + repoCountDescription="This count shows repositories matching your current search and filters. Discover starts with public GitHub repositories at 5,000+ stars, added and refreshed by a bounded daily job." hasActiveFilters={hasActiveFilters} onClearFilters={clearFilters} /> diff --git a/src/app/discover/layout.tsx b/src/app/discover/layout.tsx new file mode 100644 index 0000000..1ef186f --- /dev/null +++ b/src/app/discover/layout.tsx @@ -0,0 +1,5 @@ +import { AppShell } from '@/components/app-shell'; + +export default function DiscoverLayout({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/src/app/explore/[...slug]/page.tsx b/src/app/explore/[...slug]/page.tsx index c0fbb4d..d36752d 100644 --- a/src/app/explore/[...slug]/page.tsx +++ b/src/app/explore/[...slug]/page.tsx @@ -7,11 +7,8 @@ import { ExternalLink, GitFork, Info, - MessageSquare, Sparkles, Star, - ThumbsDown, - ThumbsUp, TrendingUp, Wrench, } from 'lucide-react'; @@ -24,6 +21,7 @@ import useSWR from 'swr'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; +import { TopBar } from '@/components/top-bar'; import { useRepoDetail } from '@/hooks/use-repo-detail'; import { useSimilarRepos } from '@/hooks/use-similar-repos'; import { getAvatarImageAttrs } from '@/lib/avatar'; @@ -154,45 +152,51 @@ function MiniStarHistory({ history }: { history: StarHistoryResponse }) { function PageSkeleton() { return ( -
    - -
    -
    -
    - -
    - - +
    + +
    + +
    +
    +
    + +
    + + +
    + +
    + + +
    + + +
    +
    + + +
    - -
    - - -
    - - -
    -
    - - -
    -
    -
    - {[1, 2].map((i) => ( -
    -
    - - - +
    + {[1, 2].map((i) => ( +
    +
    + + + +
    + +
    - - -
    - ))} + ))} +
    -
    +
    ); } @@ -203,8 +207,7 @@ export default function RepoDetailPage() { const { status } = useSession(); const isAuthenticated = status === 'authenticated'; - const { repo, commentCount, comments, isLoading, error, addComment, voteComment } = - useRepoDetail(repoSlug); + const { repo, isLoading, error } = useRepoDetail(repoSlug); // Stagger secondary requests after the main repo payload so a single page // open does not fire 4 concurrent /api/* calls (Cloudflare edge 429s). @@ -230,37 +233,17 @@ export default function RepoDetailPage() { { revalidateOnFocus: false, dedupingInterval: 60_000 } ); - const [commentBody, setCommentBody] = useState(''); - const [submitting, setSubmitting] = useState(false); - const [votingId, setVotingId] = useState(null); - - const handleSubmitComment = async (e: React.FormEvent) => { - e.preventDefault(); - if (!commentBody.trim() || submitting) return; - setSubmitting(true); - try { - await addComment(commentBody.trim()); - setCommentBody(''); - } finally { - setSubmitting(false); - } - }; - - const handleVote = async (commentId: number, value: 1 | -1) => { - if (!isAuthenticated || votingId === commentId) return; - setVotingId(commentId); - try { - await voteComment(commentId, value); - } finally { - setVotingId(null); - } - }; - if (!repoSlug) { return ( -
    -

    Invalid repository path. Use /explore/owner/repo

    -
    +
    + +
    +

    Invalid repository path. Use /explore/owner/repo

    +
    +
    ); } @@ -268,370 +251,261 @@ export default function RepoDetailPage() { if (error || !repo) { return ( -
    - - - Back - -
    -

    - {error ? 'Failed to load repository.' : 'Repository not found.'} -

    +
    + +
    + + + Back + +
    +

    + {error ? 'Failed to load repository.' : 'Repository not found.'} +

    +
    -
    + ); } const langColor = repo.language ? (languageColors[repo.language] ?? '#8b8b8b') : null; const ownerAvatar = getAvatarImageAttrs(repo.owner_avatar, 40); + const backHref = isAuthenticated ? '/stars' : '/discover'; + const backLabel = isAuthenticated ? 'Back to Library' : 'Back to Discover'; return ( -
    - {/* Back link */} - - - Back to stars - +
    + +
    + {/* Back link */} + + + {backLabel} + - {/* Repo header card */} -
    -
    - {/* Owner + name */} -
    - {/* eslint-disable-next-line @next/next/no-img-element */} - {repo.owner_login} -
    -

    {repo.owner_login}

    -

    {repo.name}

    + {/* Repo header card */} +
    +
    + {/* Owner + name */} +
    + {/* eslint-disable-next-line @next/next/no-img-element */} + {repo.owner_login} +
    +

    {repo.owner_login}

    +

    {repo.name}

    +
    -
    - {/* GitHub CTA */} - - - -
    - - {/* Description */} - {repo.description && ( -

    {repo.description}

    - )} - - {/* Topics */} - {repo.topics.length > 0 && ( -
    - {repo.topics.map((topic) => ( - - {topic} - - ))} + {/* GitHub CTA */} + + +
    - )} - {/* Stats row */} -
    -
    - - - {formatStarCount(repo.stargazers_count)} - - stars -
    - {repo.language && ( -
    - - {repo.language} -
    + {/* Description */} + {repo.description && ( +

    {repo.description}

    )} -
    - - - {commentCount} {commentCount === 1 ? 'comment' : 'comments'} - -
    - {repo.archived && ( -
    - - Archived -
    - )} - {repo.repo_updated_at && ( -
    - - Updated {timeAgo(repo.repo_updated_at)} + + {/* Topics */} + {repo.topics.length > 0 && ( +
    + {repo.topics.map((topic) => ( + + {topic} + + ))}
    )} - {repo.repo_created_at && ( + + {/* Stats row */} +
    - - Created {formatDate(repo.repo_created_at)} + + + {formatStarCount(repo.stargazers_count)} + + stars
    - )} + {repo.language && ( +
    + + {repo.language} +
    + )} + {repo.archived && ( +
    + + Archived +
    + )} + {repo.repo_updated_at && ( +
    + + Updated {timeAgo(repo.repo_updated_at)} +
    + )} + {repo.repo_created_at && ( +
    + + Created {formatDate(repo.repo_created_at)} +
    + )} +
    -
    - {(starHistory || (repoTools?.tools.length ?? 0) > 0) && ( -
    -
    -

    - - Star history -

    - {starHistory ? ( - - ) : ( -

    No snapshots available yet.

    - )} + {(starHistory || (repoTools?.tools.length ?? 0) > 0) && ( +
    +
    +

    + + Star history +

    + {starHistory ? ( + + ) : ( +

    No snapshots available yet.

    + )} +
    + +
    +

    + + Detected tools +

    + {repoTools?.tools.length ? ( + <> +
    + {repoTools.tools.slice(0, 12).map((tool) => ( + + + {tool.toolName} + + + ))} +
    +
    + + {repoTools.disclaimer} +
    + + ) : ( +

    No detected tools yet.

    + )} +
    + )} -
    + {/* Similar repos */} + {(similarLoading || similar.length > 0) && ( +

    - - Detected tools + + Similar projects

    - {repoTools?.tools.length ? ( - <> -
    - {repoTools.tools.slice(0, 12).map((tool) => ( - - - {tool.toolName} - - - ))} -
    -
    - - {repoTools.disclaimer} -
    - + {similarLoading ? ( +
    + {[1, 2, 3, 4].map((i) => ( + + ))} +
    ) : ( -

    No detected tools yet.

    - )} -
    -
    - )} - - {/* Similar repos */} - {(similarLoading || similar.length > 0) && ( -
    -

    - - Similar in your stars -

    - {similarLoading ? ( -
    - {[1, 2, 3, 4].map((i) => ( - - ))} -
    - ) : ( -
    - {similar.map((s) => { - const langColor = s.language ? (languageColors[s.language] ?? '#8b8b8b') : null; - const avatar = getAvatarImageAttrs(s.owner.avatar_url, 24); - return ( - -
    - {/* eslint-disable-next-line @next/next/no-img-element */} - {s.owner.login} -
    -

    - {s.owner.login}/ - {s.name} -

    - {s.description && ( -

    - {s.description} +

    + {similar.map((s) => { + const langColor = s.language ? (languageColors[s.language] ?? '#8b8b8b') : null; + const avatar = getAvatarImageAttrs(s.owner.avatar_url, 24); + return ( + +
    + {/* eslint-disable-next-line @next/next/no-img-element */} + {s.owner.login} +
    +

    + {s.owner.login}/ + {s.name}

    - )} -
    - {s.language && ( - - - {s.language} - + {s.description && ( +

    + {s.description} +

    )} - - - {formatStarCount(s.stargazers_count)} - - - {Math.round(s.similarity * 100)}% - +
    + {s.language && ( + + + {s.language} + + )} + + + {formatStarCount(s.stargazers_count)} + + + {Math.round(s.similarity * 100)}% + +
    -
    - - ); - })} -
    - )} -
    - )} - - {/* Comments section */} -
    -

    - Discussion - {commentCount > 0 && ( - ({commentCount}) - )} -

    - - {/* Comment input at top */} -
    - {isAuthenticated ? ( -
    -