Merge branch 'browser-pass' into fast-push
9 files+57−170/9 viewed
| 63 | 63 | | **Active branches** | Branches other than the default, newest first. See [active branches](#active-branches). | | |
| 64 | 64 | | **Recent changes**, **Latest on main**, **Activity**, **Previews** | What landed, the default branch's latest commits, what happened (pull requests opened, readied and merged, issues opened and closed, checks, reviews, comments, pushes to the default branch and production deploys), and the previews that are up. | | |
| 65 | 65 | | **About** | Its description, its [links](#links), what it is, and its latest release (its newest tag). People who can change its settings edit the description and links from here. | | |
| 66 | − | | **Health**, **Clone** | How often checks pass, recent builds and open issues by age; the clone address. | | |
| 66 | + | | **Health**, **Clone** | How often checks pass (on pull requests, or the project's workflow runs when it ships without them), recent builds and open issues by age; the clone address. | | |
| 67 | 67 | ||
| 68 | 68 | ### The checklist | |
| 69 | 69 |
| 28 | 28 | nextSeen, | |
| 29 | 29 | pipelineStage, | |
| 30 | 30 | queuedNumbers, | |
| 31 | + | runHealth, | |
| 31 | 32 | rankNeeds, | |
| 32 | 33 | readCookie, | |
| 33 | 34 | sparkPoints, | |
| ⋯ | |||
| 179 | 180 | ]); | |
| 180 | 181 | assert.deepEqual(rate, { rate: 0.5, of: 2 }); | |
| 181 | 182 | assert.deepEqual(firstPassRate([]), { rate: null, of: 0 }); | |
| 183 | + | // Workflow runs: cancelled, skipped and unfinished ones don't count; a pass on a second attempt isn't a first-try pass. | |
| 184 | + | const runs = runHealth([ | |
| 185 | + | { attempt: 1, status: "completed", conclusion: "success" }, | |
| 186 | + | { attempt: 2, status: "completed", conclusion: "success" }, | |
| 187 | + | { attempt: 1, status: "completed", conclusion: "failure" }, | |
| 188 | + | { attempt: 1, status: "completed", conclusion: "cancelled" }, | |
| 189 | + | { attempt: 1, status: "in_progress", conclusion: null }, | |
| 190 | + | ]); | |
| 191 | + | assert.equal(runs.checkRuns, 3); | |
| 192 | + | assert.equal(runs.passRate, 2 / 3); | |
| 193 | + | assert.deepEqual(runs.firstPass, { rate: 1 / 3, of: 3 }); | |
| 194 | + | assert.deepEqual(runHealth([]), { passRate: null, firstPass: { rate: null, of: 0 }, checkRuns: 0 }); | |
| 182 | 195 | assert.equal(formatSpan(null), "—"); | |
| 183 | 196 | assert.equal(formatSpan(30 * MIN), "30m"); | |
| 184 | 197 | assert.equal(formatSpan(5 * HOUR), "5h"); | |
| 417 | 417 | return results.filter(Boolean).length / results.length; | |
| 418 | 418 | } | |
| 419 | 419 | ||
| 420 | + | /** | |
| 421 | + | * A project's health from its recent workflow runs, for one that ships | |
| 422 | + | * without pull requests (whose checks are what `checks.completed` counts). | |
| 423 | + | * Only runs that finished and passed or failed count: cancelled and skipped | |
| 424 | + | * say nothing about the code. A run passed on the first try when its first | |
| 425 | + | * attempt is the one that passed. | |
| 426 | + | */ | |
| 427 | + | export function runHealth(runs: { attempt: number; status: string; conclusion: string | null }[]): { passRate: number | null; firstPass: { rate: number | null; of: number }; checkRuns: number } { | |
| 428 | + | const decided = runs.filter((run) => run.status === "completed" && (run.conclusion === "success" || run.conclusion === "failure")); | |
| 429 | + | const passed = decided.map((run) => run.conclusion === "success"); | |
| 430 | + | const first = decided.filter((run) => run.conclusion === "success" && run.attempt <= 1).length; | |
| 431 | + | return { passRate: passRate(passed), firstPass: { rate: decided.length ? first / decided.length : null, of: decided.length }, checkRuns: decided.length }; | |
| 432 | + | } | |
| 433 | + | ||
| 420 | 434 | /** How long from an issue being opened to its change landing, for each that did, in ms. */ | |
| 421 | 435 | export function issueToMerge( | |
| 422 | 436 | opened: { repo: string; number: number; at: number }[], |
| 71 | 71 | projectFeed, | |
| 72 | 72 | queuedNumbers, | |
| 73 | 73 | rankNeeds, | |
| 74 | + | runHealth, | |
| 74 | 75 | stuckMinutes, | |
| 75 | 76 | } from "../../lib/mission"; | |
| 76 | 77 | import { agentWasAssigned, checklistPlan, hasInstructions, productionChecklist, releaseChecklist, startChecklist } from "../../lib/checklist"; | |
| ⋯ | |||
| 160 | 161 | const libraryRepoP = Promise.all([planP, repoP]).then(([plan, repo]) => (plan === "release" && repo?.ok ? repo.value : null)); | |
| 161 | 162 | const packagesP = libraryRepoP.then((repo) => (repo ? soft(packages.list(params.owner, viewer, { repoId: repo.id })) : null)); | |
| 162 | 163 | const workflowsP = planP.then((plan) => (plan && plan !== "production" ? forMembers(() => actions.workflows(path, viewer)) : null)); | |
| 164 | + | // Recent workflow runs, for Health when no pull request's checks have finished lately. | |
| 165 | + | const workflowRunsP = soft(actions.runs(path, viewer, { limit: 30 })); | |
| 163 | 166 | // Only the kinds the feed shows: the newest events are mostly session | |
| 164 | 167 | // steps and merge checks, which would otherwise crowd out everything. | |
| 165 | 168 | const eventsP = repoP.then((repo) => | |
| ⋯ | |||
| 231 | 234 | // The latest release, for About. | |
| 232 | 235 | soft(repos.tags(path, viewer)), | |
| 233 | 236 | ]); | |
| 237 | + | const workflowRuns = await workflowRunsP; | |
| 234 | 238 | const ok = <T,>(result: { ok: true; value: T } | { ok: false } | null): T | null => (result?.ok ? result.value : null); | |
| 235 | 239 | ||
| 236 | 240 | const openPulls = ok(open) ?? []; | |
| ⋯ | |||
| 458 | 462 | groups: groupActivity(items).slice(0, 30), | |
| 459 | 463 | eventsLoaded: recent != null, | |
| 460 | 464 | health: { | |
| 461 | − | passRate: passRate(checkEvents.map((c) => c.passed)), | |
| 462 | − | firstPass: firstPassRate(checkEvents), | |
| 463 | − | checkRuns: checkEvents.length, | |
| 465 | + | // A pull request's checks when any finished lately; otherwise the | |
| 466 | + | // project's workflow runs, for one that ships straight to its branch. | |
| 467 | + | ...(checkEvents.length > 0 || !workflowRuns?.ok | |
| 468 | + | ? { passRate: passRate(checkEvents.map((c) => c.passed)), firstPass: firstPassRate(checkEvents), checkRuns: checkEvents.length } | |
| 469 | + | : runHealth(workflowRuns.value)), | |
| 464 | 470 | ages: openIssues ? ageBuckets(openIssues.map((issue) => issue.createdAt), now) : null, | |
| 465 | 471 | }, | |
| 466 | 472 | knows: knows as Memory[], | |
| 16 | 16 | import { page } from "../../lib/meta"; | |
| 17 | 17 | import { useRefreshWhile } from "../../lib/refresh"; | |
| 18 | 18 | import { workspaceAgents } from "../../lib/services.server"; | |
| 19 | − | import { getViewer, roleIn } from "../../lib/session.server"; | |
| 19 | + | import { requireUser, roleIn } from "../../lib/session.server"; | |
| 20 | 20 | import { money } from "../../lib/usage"; | |
| 21 | 21 | ||
| 22 | 22 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| ⋯ | |||
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | 26 | /** Agents mode's front page: the budget, what is waiting and working, the roster, and where the month went. */ | |
| 27 | − | export async function loader({ params, context }: Route.LoaderArgs): Promise<{ overview: AgentsOverview | null }> { | |
| 28 | − | const viewer = getViewer(context); | |
| 27 | + | export async function loader({ params, context, request }: Route.LoaderArgs): Promise<{ overview: AgentsOverview | null }> { | |
| 28 | + | // Signed out, sign in first (as Chat and Docs do), rather than a 404. | |
| 29 | + | const viewer = requireUser(context, request); | |
| 29 | 30 | if (!viewer || !roleIn(viewer, params.owner)) throw data(null, { status: 404 }); | |
| 30 | 31 | return { overview: await readOrNull(workspaceAgents.overview(params.owner.toLowerCase(), viewer)) }; | |
| 31 | 32 | } | |
| 6 | 6 | ||
| 7 | 7 | import type { Route } from "./+types/layout"; | |
| 8 | 8 | import { workspaceAgents } from "../../../lib/services.server"; | |
| 9 | − | import { getViewer, roleIn } from "../../../lib/session.server"; | |
| 9 | + | import { requireUser, roleIn } from "../../../lib/session.server"; | |
| 10 | 10 | ||
| 11 | 11 | /** | |
| 12 | 12 | * Agents mode: the workspace's agents, for its sidebar, and how many live | |
| ⋯ | |||
| 14 | 14 | */ | |
| 15 | 15 | export type AgentsLayoutData = { slug: string; agents: WorkspaceAgent[] | null; live: Record<string, number> }; | |
| 16 | 16 | ||
| 17 | − | export async function loader({ params, context }: Route.LoaderArgs): Promise<AgentsLayoutData> { | |
| 18 | − | const viewer = getViewer(context); | |
| 17 | + | export async function loader({ params, context, request }: Route.LoaderArgs): Promise<AgentsLayoutData> { | |
| 18 | + | // Signed out, sign in first (as Chat and Docs do), rather than a 404. | |
| 19 | + | const viewer = requireUser(context, request); | |
| 19 | 20 | if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 }); | |
| 20 | 21 | const slug = params.owner.toLowerCase(); | |
| 21 | 22 | const [listed, live] = await Promise.all([ | |
| 10 | 10 | import { integrationsSection } from "../../lib/integration-sections"; | |
| 11 | 11 | import { page } from "../../lib/meta"; | |
| 12 | 12 | import { integrations, webhooks } from "../../lib/services.server"; | |
| 13 | − | import { getViewer, roleIn } from "../../lib/session.server"; | |
| 13 | + | import { requireUser, roleIn } from "../../lib/session.server"; | |
| 14 | 14 | ||
| 15 | 15 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 16 | 16 | return page(args, { title: `Integrations · ${params.owner} · g1t` }); | |
| ⋯ | |||
| 23 | 23 | * GitHub import, webhooks); this page only finds them. | |
| 24 | 24 | */ | |
| 25 | 25 | export async function loader({ params, context, request }: Route.LoaderArgs) { | |
| 26 | − | const viewer = getViewer(context); | |
| 26 | + | // Signed out, sign in first (as Chat and Docs do), rather than a 404. | |
| 27 | + | const viewer = requireUser(context, request); | |
| 27 | 28 | const role = roleIn(viewer, params.owner); | |
| 28 | 29 | if (!role || !viewer) throw new Response(null, { status: 404 }); | |
| 29 | 30 | const slug = params.owner.toLowerCase(); | |
| 46 | 46 | }); | |
| 47 | 47 | ||
| 48 | 48 | test("required spaces come first, then the rest", () => { | |
| 49 | − | const picked = pickPassages([c("a:0", "s1", 0.9), c("b:0", "s2", 0.65), c("c:0", "s1", 0.85)], { allowed: new Set(["s1", "s2"]), required: ["s2"], limit: 2 }); | |
| 49 | + | const picked = pickPassages([c("a:0", "s1", 0.9), c("b:0", "s2", 0.75), c("c:0", "s1", 0.85)], { allowed: new Set(["s1", "s2"]), required: ["s2"], limit: 2 }); | |
| 50 | 50 | assert.deepEqual( | |
| 51 | 51 | picked.map((p) => p.id), | |
| 52 | 52 | ["b:0", "a:0"], |
| 8 | 8 | ||
| 9 | 9 | /** | |
| 10 | 10 | * The least cosine similarity a passage needs to count as being about the | |
| 11 | − | * query. bge-base-en-v1.5 scores unrelated English around 0.4 to 0.55 and | |
| 12 | − | * a passage on the asked-about topic from about 0.65; 0.6 keeps recall | |
| 13 | − | * quiet when the docs say nothing about it. | |
| 11 | + | * query. Measured on g1t's own docs folder (606 passages, chunked and | |
| 12 | + | * embedded as here, bge-base-en-v1.5 with Workers AI's mean pooling): the | |
| 13 | + | * best passage for 16 questions the docs answer scored 0.72 to 0.84, while | |
| 14 | + | * the best for 16 they don't ("hello", a recipe, Postgres tuning, a | |
| 15 | + | * vacation policy, SSO with Okta) scored 0.54 to 0.67. At 0.6, 11 of those | |
| 16 | + | * 16 pulled in passages (up to 89 above it); 0.7 keeps every answer and | |
| 17 | + | * keeps recall quiet when the docs say nothing about the question. | |
| 14 | 18 | */ | |
| 15 | − | export const MEANING_FLOOR = 0.6; | |
| 19 | + | export const MEANING_FLOOR = 0.7; | |
| 16 | 20 | /** The score a passage found only by its words carries: below the floor, so callers can tell. */ | |
| 17 | 21 | export const WORDS_SCORE = 0.5; | |
| 18 | 22 | /** Nearest passages asked of the index. */ |