Public: anyone can read this; the crew can change it.
/map loses the sections docs/design/map.md says to cut #2790
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/2770-map-loses-the-sections-docs-design-map-m → main 2 commits fork
10 files
changed +53
−195
docs/DESIGN.md +1 −1
| @@ -384,7 +384,7 @@ gone, so this list only shrinks. | ||
| 384 | 384 | - **`pages/board.ts`** — the board, the live wall of the crews at work: `.board` `.board-about` `.board-answer` `.board-app` `.board-app-mode` `.board-app-name` `.board-app-nums` `.board-app-week` `.board-asked` `.board-asks` `.board-bar` `.board-brand` `.board-card` `.board-clock` `.board-count` `.board-decision` `.board-decision-main` `.board-declined` `.board-declined-row` `.board-declined-says` `.board-dot` `.board-elapsed` `.board-fill` `.board-forge` `.board-grid` `.board-group` `.board-h` `.board-inferred` `.board-key` `.board-keys` `.board-kind` `.board-landed` `.board-landed-meta` `.board-landed-title` `.board-list` `.board-live` `.board-outcome` `.board-p` `.board-parked-row` `.board-pillar` `.board-pillars` `.board-pillars-h` `.board-quiet` `.board-region` `.board-run` `.board-run-meta` `.board-run-title` `.board-run-who` `.board-scope` `.board-spend` `.board-spend-detail` `.board-state` `.board-sub` `.board-sub-name` `.board-sub-scope` `.board-subs` `.board-thumb` `.board-top` `.board-when` `.board-window` `.board-window-name` `.board-windows` `.spend-table` `.spend-totals` |
| 385 | 385 | - **`pages/flow.ts`** — /flow's pipeline bars, one `held-${kind}` per reason a stage holds work: `.flow-bar` `.flow-out` `.held-bar` `.held-done_on_main` `.held-key` `.held-open_pull` `.held-parked_twice` `.held-planner` `.held-red_fix` `.held-run_cap` `.held-running` `.held-setting` `.held-stalled` `.held-startable` |
| 386 | 386 | - **`pages/limb.ts`** — acting on a whole limb from its milestone: `.limb-confirm` `.limb-rows` `.limb-why` |
| 387 | - **`pages/map.ts`** — the map of what waits for what: `.map-ask` `.map-autonomy` `.map-breath` `.map-canvas` `.map-card` `.map-chart` `.map-counts` `.map-dot` `.map-edge` `.map-edge-crit` `.map-edge-done` `.map-empty` `.map-flow` `.map-lane` `.map-lane-caret` `.map-lane-crew` `.map-lane-head` `.map-lane-name` `.map-lane-word` `.map-lanes` `.map-level` `.map-list` `.map-main` `.map-meta` `.map-node` `.map-pillars` `.map-rule` `.map-spend` `.map-spend-fill` `.map-spend-track` `.map-svg` `.map-title` `.map-tool` `.map-tools` `.map-who` `.map-who-i` `.map-who-n` `.map-word` `.map-wrap` | |
| 387 | - **`pages/map.ts`** — the map of what waits for what: `.map-ask` `.map-breath` `.map-canvas` `.map-card` `.map-counts` `.map-dot` `.map-edge` `.map-edge-crit` `.map-edge-done` `.map-empty` `.map-lane` `.map-lane-caret` `.map-lane-crew` `.map-lane-head` `.map-lane-name` `.map-lane-word` `.map-lanes` `.map-list` `.map-main` `.map-meta` `.map-node` `.map-pillars` `.map-rule` `.map-svg` `.map-title` `.map-tool` `.map-tools` `.map-who` `.map-who-i` `.map-who-n` `.map-word` `.map-wrap` | |
| 388 | 388 | - **`pages/milestones.ts`** — milestones and one limb opened: `.ms-actions` `.ms-bar` `.ms-count` `.ms-figures` `.ms-head` `.ms-limb` `.ms-limbs` `.ms-more` `.ms-move` `.ms-name` `.ms-planner` `.ms-rows` `.ms-section` `.ms-state` `.ms-top` `.ms-trim` `.ms-weight` |
| 389 | 389 | - **`studio/feed_style.ts`** — the home feed's cards, its composer and the apps rail: `.composer` `.composer-bar` `.fcard` `.fcard-acts` `.fcard-form` `.fcard-head` `.fcard-more` `.fcard-reason` `.fcard-reply` `.fcard-text` `.fcard-unblock` `.fcard-who` `.fcard-why` `.feed` `.feed-head` `.home-app` `.home-apps` `.home-apps-verbs` `.home-pick` `.home-rail` `.home-tabs` |
| 390 | 390 | - **`studio/style.ts`** — the studio's tell box, app head, word and shelf rows: `.fcard-app` `.shelf-desc` `.shelf-facts` `.shelf-meta` `.studio-ask-actions` `.studio-head` `.studio-tell` `.studio-word` |
docs/design/map.md +5 −2
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | |
| 3 | 3 | Part of #2695. A reading of `src/web/pages/map.ts`, section by section: does a |
| 4 | 4 | board or an issue's dependency list already show it, and keep or cut. This |
| 5 | document changes no code; each **cut** is a follow-up issue, not done here. | |
| 5 | document changed no code; #2770 made the cuts. | |
| 6 | 6 | |
| 7 | 7 | An issue's dependency list shows one issue's direct edges. A board shows |
| 8 | 8 | work by state, per app, with a "merged by pillar" line. Neither draws the |
| @@ -28,4 +28,7 @@ whole graph or follows a chain more than one hop. | ||
| 28 | 28 | |
| 29 | 29 | Cutting `Autonomy line`, `Critical path` list, `Two weeks` and `Spend bar` |
| 30 | 30 | leaves the canvas, counts, pillars, ready and blocked: what only the graph |
| 31 | can say. | |
| 31 | can say. Done in #2770: today's spend against each app's ceiling is read on | |
| 32 | `/board` (the home's "their ceiling" link goes there), the mode on | |
| 33 | `settings/autonomy`, and an issue's place in a chain on its dependency list. | |
| 34 | `GET /api/v1/map` still carries `critical` and each lane's spend for the crew. |
src/core/dependencies.ts −43
| @@ -621,46 +621,3 @@ export function criticalPath(graph: Graph): Issue[] { | ||
| 621 | 621 | ? best.map((id) => byId.get(id)).filter((i): i is Issue => i !== undefined) |
| 622 | 622 | : []; |
| 623 | 623 | } |
| 624 | ||
| 625 | // ---- level and flow ---- | |
| 626 | ||
| 627 | export interface DayPoint { | |
| 628 | readonly day: string; | |
| 629 | /** Open issues at the end of that day: the level. */ | |
| 630 | readonly open: number; | |
| 631 | /** Issues closed during that day: the flow. */ | |
| 632 | readonly closed: number; | |
| 633 | } | |
| 634 | ||
| 635 | /** The last `days` days of open count against closed-per-day, oldest first. */ | |
| 636 | export function levelAndFlow( | |
| 637 | db: Database, | |
| 638 | repoId: string, | |
| 639 | days = 14, | |
| 640 | now = new Date(), | |
| 641 | ): DayPoint[] { | |
| 642 | const out: DayPoint[] = []; | |
| 643 | for (let back = days - 1; back >= 0; back--) { | |
| 644 | const end = new Date(now); | |
| 645 | end.setUTCHours(24, 0, 0, 0); | |
| 646 | end.setUTCDate(end.getUTCDate() - back); | |
| 647 | const endIso = end.toISOString(); | |
| 648 | const start = new Date(end); | |
| 649 | start.setUTCDate(start.getUTCDate() - 1); | |
| 650 | const startIso = start.toISOString(); | |
| 651 | const open = db | |
| 652 | .query<{ n: number }, [string, string]>( | |
| 653 | `SELECT COUNT(*) AS n FROM issue WHERE repo_id = ?1 AND kind = 'issue' | |
| 654 | AND created_at < ?2 AND (closed_at IS NULL OR closed_at >= ?2)`, | |
| 655 | ) | |
| 656 | .get(repoId, endIso)!.n; | |
| 657 | const closed = db | |
| 658 | .query<{ n: number }, [string, string, string]>( | |
| 659 | `SELECT COUNT(*) AS n FROM issue WHERE repo_id = ?1 AND kind = 'issue' | |
| 660 | AND closed_at >= ?2 AND closed_at < ?3`, | |
| 661 | ) | |
| 662 | .get(repoId, startIso, endIso)!.n; | |
| 663 | out.push({ day: startIso.slice(0, 10), open, closed }); | |
| 664 | } | |
| 665 | return out; | |
| 666 | } |
src/web/pages/map.ts +3 −127
| @@ -3,9 +3,8 @@ | ||
| 3 | 3 | * a card one level below everything it is blocked by, so the top edge is |
| 4 | 4 | * what is ready now and the longest open chain runs down the page in the |
| 5 | 5 | * accent. Issues that wait for nothing and hold nothing up stand apart |
| 6 | * below, under a rule. Beside the drawing: the three lists the crew and a | |
| 7 | * person both read — ready now, blocked on, the critical path — and two | |
| 8 | * weeks of open count over closed-per-day. | |
| 6 | * below, under a rule. Beside the drawing: open work by pillar and the two | |
| 7 | * lists the crew and a person both read — ready now and blocked on. | |
| 9 | 8 | * |
| 10 | 9 | * One language for every map. A card says its title, who is on it, and one |
| 11 | 10 | * word: ready, working, blocked, done, parked, or waiting. Edges are calm |
| @@ -27,10 +26,8 @@ import { | ||
| 27 | 26 | blockedOn, |
| 28 | 27 | criticalPath, |
| 29 | 28 | idleFurniture, |
| 30 | levelAndFlow, | |
| 31 | 29 | readyNow, |
| 32 | 30 | repoGraph, |
| 33 | type DayPoint, | |
| 34 | 31 | type Edge, |
| 35 | 32 | type Graph, |
| 36 | 33 | type Ranked, |
| @@ -67,7 +64,6 @@ import { | ||
| 67 | 64 | type ParkedTwice, |
| 68 | 65 | } from "../../crew/parks.ts"; |
| 69 | 66 | import { |
| 70 | OUTSIDERS_NOTE, | |
| 71 | 67 | proposedWork, |
| 72 | 68 | READER_SUGGESTION, |
| 73 | 69 | } from "../../crew/direct.ts"; |
| @@ -1042,60 +1038,10 @@ function pillarList(repo: Repo, rows: readonly PillarRow[]): Html { | ||
| 1042 | 1038 | </section>`; |
| 1043 | 1039 | } |
| 1044 | 1040 | |
| 1045 | function chart(points: DayPoint[]): Html { | |
| 1046 | const w = 300; | |
| 1047 | const h = 84; | |
| 1048 | const pad = 6; | |
| 1049 | const maxOpen = Math.max(1, ...points.map((p) => p.open)); | |
| 1050 | const maxClosed = Math.max(1, ...points.map((p) => p.closed)); | |
| 1051 | const step = (w - pad * 2) / Math.max(1, points.length - 1); | |
| 1052 | const line = points | |
| 1053 | .map( | |
| 1054 | (p, i) => | |
| 1055 | `${i === 0 ? "M" : "L"}${(pad + i * step).toFixed(1)} ${( | |
| 1056 | pad + | |
| 1057 | (h - pad * 2) * (1 - p.open / maxOpen) | |
| 1058 | ).toFixed(1)}`, | |
| 1059 | ) | |
| 1060 | .join(" "); | |
| 1061 | const barW = Math.max(3, step * 0.55); | |
| 1062 | const bars = points.map((p, i) => { | |
| 1063 | const bh = (h - pad * 2) * (p.closed / maxClosed); | |
| 1064 | return html`<rect | |
| 1065 | class="map-flow" | |
| 1066 | x="${(pad + i * step - barW / 2).toFixed(1)}" | |
| 1067 | y="${(h - pad - bh).toFixed(1)}" | |
| 1068 | width="${barW.toFixed(1)}" | |
| 1069 | height="${bh.toFixed(1)}" | |
| 1070 | rx="1.5" | |
| 1071 | />`; | |
| 1072 | }); | |
| 1073 | const last = points.at(-1); | |
| 1074 | const closedTotal = points.reduce((s, p) => s + p.closed, 0); | |
| 1075 | return html`<section class="map-list"> | |
| 1076 | <h2>Two weeks</h2> | |
| 1077 | <svg | |
| 1078 | class="map-chart" | |
| 1079 | viewBox="0 0 ${w} ${h}" | |
| 1080 | role="img" | |
| 1081 | aria-label="Open issues as a line over issues closed per day as bars" | |
| 1082 | > | |
| 1083 | ${bars} | |
| 1084 | <path class="map-level" d="${line}" /> | |
| 1085 | </svg> | |
| 1086 | <p class="muted small"> | |
| 1087 | <span class="num">${last?.open ?? 0}</span> open now · | |
| 1088 | <span class="num">${closedTotal}</span> closed in 14 days | |
| 1089 | </p> | |
| 1090 | </section>`; | |
| 1091 | } | |
| 1092 | ||
| 1093 | 1041 | export const mapCss = /* css */ ` |
| 1094 | 1042 | .map-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 272px; gap: 24px; align-items: start; margin-top: 4px; } |
| 1095 | 1043 | @media (max-width: 860px) { .map-wrap { grid-template-columns: 1fr; } } |
| 1096 | 1044 | .map-main { min-width: 0; } |
| 1097 | .map-autonomy { flex-basis: 100%; color: var(--muted-fg); text-wrap: pretty; } | |
| 1098 | .map-autonomy .ico { color: var(--faint); vertical-align: -3px; margin-right: 2px; } | |
| 1099 | 1045 | |
| 1100 | 1046 | /* ---- the canvas ---- */ |
| 1101 | 1047 | .map-canvas { position: relative; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); touch-action: pan-y; user-select: none; -webkit-user-select: none; } |
| @@ -1174,9 +1120,6 @@ export const mapCss = /* css */ ` | ||
| 1174 | 1120 | .map-list + .map-list { margin-top: 18px; } |
| 1175 | 1121 | .map-ask { display: block; } |
| 1176 | 1122 | .map-pillars ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; font-size: 14px; } |
| 1177 | .map-chart { width: 100%; height: auto; display: block; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg); } | |
| 1178 | .map-level { fill: none; stroke: var(--accent); stroke-width: 1.5; } | |
| 1179 | .map-flow { fill: color-mix(in srgb, var(--fg) 14%, transparent); } | |
| 1180 | 1123 | .map-empty { display: grid; justify-items: center; gap: 8px; padding: 40px 16px; border: 1px dashed var(--border); border-radius: var(--radius); color: var(--muted-fg); text-align: center; } |
| 1181 | 1124 | |
| 1182 | 1125 | /* ---- lanes ---- */ |
| @@ -1191,11 +1134,6 @@ export const mapCss = /* css */ ` | ||
| 1191 | 1134 | .map-lane-name .muted { font-weight: 500; } |
| 1192 | 1135 | .map-lane-word { font-size: 12.5px; color: var(--muted-fg); padding: 1px 8px; border: 1px solid var(--border); border-radius: 999px; line-height: 18px; } |
| 1193 | 1136 | .map-lane-word[data-autonomy="autonomous"] { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 34%, transparent); } |
| 1194 | .map-spend { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--muted-fg); font-variant-numeric: tabular-nums; } | |
| 1195 | .map-spend svg { width: 72px; height: 4px; border-radius: 2px; overflow: hidden; } | |
| 1196 | .map-spend-track { fill: var(--muted); } | |
| 1197 | .map-spend-fill { fill: var(--muted-fg); } | |
| 1198 | .map-spend[data-near="1"] .map-spend-fill { fill: var(--attn); } | |
| 1199 | 1137 | .map-lane-crew { display: inline-flex; align-items: center; gap: 4px; } |
| 1200 | 1138 | .map-lane-crew .avatar { box-shadow: 0 0 0 2px var(--card); } |
| 1201 | 1139 | .map-lane-head .map-counts { margin: 0 0 0 auto; } |
| @@ -1363,28 +1301,6 @@ export const mapViewJs = /* js */ ` | ||
| 1363 | 1301 | })(); |
| 1364 | 1302 | `; |
| 1365 | 1303 | |
| 1366 | /** One line on how the repository runs itself, when it does. */ | |
| 1367 | function autonomyLine(ctx: Ctx, repo: Repo): Html | null { | |
| 1368 | if (repo.autonomy === "off") | |
| 1369 | return repo.crewOutsiders | |
| 1370 | ? html`<p class="small map-autonomy">${OUTSIDERS_NOTE}</p>` | |
| 1371 | : null; | |
| 1372 | const s = autonomyStatus(ctx.app, repo); | |
| 1373 | const paused = | |
| 1374 | s.paused === "ceiling" | |
| 1375 | ? " · paused at the ceiling until 00:00 UTC" | |
| 1376 | : s.paused === "no crew" | |
| 1377 | ? " · paused: no crew" | |
| 1378 | : s.paused === "nobody" | |
| 1379 | ? " · paused: nobody to act as" | |
| 1380 | : ""; | |
| 1381 | const line = `${repo.autonomy === "autonomous" ? "Autonomous" : "Assisted"} · $${s.spentTodayUsd.toFixed(2)} of $${s.ceilingUsd.toFixed(0)} today · ${s.active} of ${s.slots} slots${paused} · ${repo.crewOutsiders ? "anyone signed in directs the crew" : "only writers direct the crew"}`; | |
| 1382 | return html`<p class="small map-autonomy"> | |
| 1383 | ${icon("play")} ${line} · | |
| 1384 | <a href="${repoBase(repo)}/settings/autonomy">settings</a> | |
| 1385 | </p>`; | |
| 1386 | } | |
| 1387 | ||
| 1388 | 1304 | const LIVE_KINDS = "run.*,issue.*,pull.*"; |
| 1389 | 1305 | |
| 1390 | 1306 | /** The empty state in the studio's own words, pointing at the app page's |
| @@ -1423,7 +1339,6 @@ export async function mapPage(ctx: Ctx): Promise<Response> { | ||
| 1423 | 1339 | is the least work that still has to happen in order. |
| 1424 | 1340 | <a href="${repoBase(repo)}/milestones">How far each milestone is</a>. |
| 1425 | 1341 | </p> |
| 1426 | ${autonomyLine(ctx, repo)} | |
| 1427 | 1342 | </div> |
| 1428 | 1343 | <div |
| 1429 | 1344 | class="map-wrap" |
| @@ -1468,22 +1383,6 @@ export async function mapPage(ctx: Ctx): Promise<Response> { | ||
| 1468 | 1383 | "Nothing open holds anything up.", |
| 1469 | 1384 | issueHref, |
| 1470 | 1385 | )} |
| 1471 | ${critical.length > 1 | |
| 1472 | ? html`<section class="map-list"> | |
| 1473 | <h2>Critical path</h2> | |
| 1474 | <ol> | |
| 1475 | ${critical.map( | |
| 1476 | (i) => | |
| 1477 | html`<li> | |
| 1478 | <a href="${issueHref(repoBase(repo), i.number)}" | |
| 1479 | >${i.title}</a | |
| 1480 | > | |
| 1481 | </li>`, | |
| 1482 | )} | |
| 1483 | </ol> | |
| 1484 | </section>` | |
| 1485 | : null} | |
| 1486 | ${chart(levelAndFlow(db, repo.id))} | |
| 1487 | 1386 | </aside> |
| 1488 | 1387 | </div>`; |
| 1489 | 1388 | return page({ |
| @@ -1730,29 +1629,7 @@ export function globalLanes(app: App, viewer: User | undefined): Lane[] { | ||
| 1730 | 1629 | ); |
| 1731 | 1630 | } |
| 1732 | 1631 | |
| 1733 | /** "$1.20 of $10", as one string so nothing wraps inside it. */ | |
| 1734 | function spendWords(status: AutonomyStatus): string { | |
| 1735 | return `$${status.spentTodayUsd.toFixed(2)} of $${status.ceilingUsd.toFixed(0)}`; | |
| 1736 | } | |
| 1737 | ||
| 1738 | function spendBar(status: AutonomyStatus): Html { | |
| 1739 | const share = | |
| 1740 | status.ceilingUsd > 0 ? status.spentTodayUsd / status.ceilingUsd : 0; | |
| 1741 | const pct = Math.max(0, Math.min(100, share * 100)); | |
| 1742 | return html`<span | |
| 1743 | class="map-spend" | |
| 1744 | ${share >= 0.9 ? html`data-near="1"` : null} | |
| 1745 | title="Spent today against the day's ceiling" | |
| 1746 | > | |
| 1747 | <svg viewBox="0 0 100 4" preserveAspectRatio="none" aria-hidden="true"> | |
| 1748 | <rect class="map-spend-track" width="100" height="4" /> | |
| 1749 | <rect class="map-spend-fill" width="${pct.toFixed(1)}" height="4" /> | |
| 1750 | </svg> | |
| 1751 | <span>${spendWords(status)}</span> | |
| 1752 | </span>`; | |
| 1753 | } | |
| 1754 | ||
| 1755 | /** A lane: the repository's name, how it runs itself, who is on it, and its map. */ | |
| 1632 | /** A lane: the repository's name, its autonomy mode, who is on it, and its map. */ | |
| 1756 | 1633 | export function laneHtml(lane: Lane, width: number): Html { |
| 1757 | 1634 | const { repo } = lane; |
| 1758 | 1635 | const id = `lane-${repo.ownerName}-${repo.name}`; |
| @@ -1771,7 +1648,6 @@ export function laneHtml(lane: Lane, width: number): Html { | ||
| 1771 | 1648 | <span class="map-lane-word" data-autonomy="${repo.autonomy}" |
| 1772 | 1649 | >${repo.autonomy}</span |
| 1773 | 1650 | > |
| 1774 | ${lane.status === undefined ? null : spendBar(lane.status)} | |
| 1775 | 1651 | ${lane.workers.length === 0 |
| 1776 | 1652 | ? null |
| 1777 | 1653 | : html`<span |
src/web/studio/cost_line.ts +1 −1
| @@ -22,6 +22,6 @@ export function costLine(db: Database, writable: readonly Repo[]): Html | null { | ||
| 22 | 22 | const noun = `app${writable.length === 1 ? "" : "s"}`; |
| 23 | 23 | const sentence = `$${spentUsd.toFixed(2)} spent today across ${writable.length} ${noun} you can write`; |
| 24 | 24 | return html`<p class="small muted studio-cost"> |
| 25 | ${sentence} · ${paused} paused at <a href="/map">their ceiling</a> | |
| 25 | ${sentence} · ${paused} paused at <a href="/board">their ceiling</a> | |
| 26 | 26 | </p>`; |
| 27 | 27 | } |
test/autonomy.test.ts +8 −2
| @@ -581,11 +581,17 @@ describe("a repository on its own", () => { | ||
| 581 | 581 | }) |
| 582 | 582 | ).text(); |
| 583 | 583 | expect(crew).toContain("reviewer"); |
| 584 | // Today's spend against the new ceiling is the forge's crew page's; the | |
| 585 | // map only draws what waits for what. | |
| 586 | const forgeCrew = await ( | |
| 587 | await fetch(`${servers.url}/settings/crew`, { headers: { cookie } }) | |
| 588 | ).text(); | |
| 589 | expect(forgeCrew).toMatch(/alice\/engine[\s\S]*?assisted/); | |
| 590 | expect(forgeCrew).toMatch(/\/ \$25 today/); | |
| 584 | 591 | const map = await ( |
| 585 | 592 | await fetch(`${servers.url}/alice/engine/map`, { headers: { cookie } }) |
| 586 | 593 | ).text(); |
| 587 | expect(map).toContain("Assisted"); | |
| 588 | expect(map).toContain("of $25 today"); | |
| 594 | expect(map).not.toContain("of $25 today"); | |
| 589 | 595 | }); |
| 590 | 596 | |
| 591 | 597 | test("the forge's own ceiling holds every repository, whatever each one's says", async () => { |
test/dependencies.test.ts +2 −7
| @@ -16,7 +16,6 @@ import { | ||
| 16 | 16 | blockedOn, |
| 17 | 17 | criticalPath, |
| 18 | 18 | dependenciesOf, |
| 19 | levelAndFlow, | |
| 20 | 19 | readyNow, |
| 21 | 20 | removeDependency, |
| 22 | 21 | repoGraph, |
| @@ -154,7 +153,7 @@ describe("dependencies", () => { | ||
| 154 | 153 | expect(placed.islandFrom).toBe(4); |
| 155 | 154 | }); |
| 156 | 155 | |
| 157 | test("closing a blocker frees what waited; the level falls and the flow ticks", () => { | |
| 156 | test("closing a blocker frees what waited and shortens the critical path", () => { | |
| 158 | 157 | const closed = setIssueState(app, alice, repo, issue(1), "closed"); |
| 159 | 158 | expect(closed.ok).toBe(true); |
| 160 | 159 | const graph = repoGraph(app.db, repo.id); |
| @@ -163,10 +162,6 @@ describe("dependencies", () => { | ||
| 163 | 162 | expect(ready).not.toContain(4); // still waits for 2 |
| 164 | 163 | // 2 → 4 → 5 → 6 is now the longest open chain: 4 still waits for 2. |
| 165 | 164 | expect(criticalPath(graph).map((i) => i.number)).toEqual([2, 4, 5, 6]); |
| 166 | const points = levelAndFlow(app.db, repo.id, 3); | |
| 167 | expect(points).toHaveLength(3); | |
| 168 | expect(points.at(-1)!.open).toBe(6); | |
| 169 | expect(points.at(-1)!.closed).toBe(1); | |
| 170 | 165 | const closedNow = getIssue(app.db, repo.id, 1)!; |
| 171 | 166 | expect(setIssueState(app, alice, repo, closedNow, "open").ok).toBe(true); |
| 172 | 167 | expect(getIssue(app.db, repo.id, 1)?.state).toBe("open"); |
| @@ -293,7 +288,7 @@ describe("dependencies", () => { | ||
| 293 | 288 | await fetch(`${base}/alice/engine/map`, { headers: { cookie } }) |
| 294 | 289 | ).text(); |
| 295 | 290 | expect(map).toContain("Ready now"); |
| 296 | expect(map).toContain("Critical path"); | |
| 291 | expect(map).not.toContain("Critical path"); | |
| 297 | 292 | expect(map).toContain('data-state="ready"'); |
| 298 | 293 | expect(map).toContain("Extract renderer"); |
| 299 | 294 | expect(map).toContain("finishing it frees 4"); |
test/design-map.test.ts +22 −5
| @@ -1,7 +1,8 @@ | ||
| 1 | 1 | /** |
| 2 | * docs/design/map.md names every section of the map page, and each one is | |
| 3 | * still in the page source (#2768). Each entry is a backticked name leading a | |
| 4 | * bullet; this maps it to the text the page carries for it. | |
| 2 | * docs/design/map.md names every section of the map page and says keep or | |
| 3 | * cut (#2768): each one kept is in the page source, each one cut is gone | |
| 4 | * from it (#2770). Each entry is a backticked name leading a bullet; this | |
| 5 | * maps it to the text the page carried for it. | |
| 5 | 6 | */ |
| 6 | 7 | import { expect, test } from "bun:test"; |
| 7 | 8 | import { readFileSync } from "node:fs"; |
| @@ -28,8 +29,24 @@ test("the doc names every section it knows, and no other", () => { | ||
| 28 | 29 | expect(named.sort()).toEqual(Object.keys(MARKERS).sort()); |
| 29 | 30 | }); |
| 30 | 31 | |
| 31 | test("every section the doc names is in the page source", () => { | |
| 32 | for (const name of named) expect(src).toContain(MARKERS[name]!); | |
| 32 | const verdicts = new Map( | |
| 33 | [...doc.matchAll(/^- `([^`]+)` —.*\*\*(Keep|Cut)\*\*/gm)].map((m) => [ | |
| 34 | m[1]!, | |
| 35 | m[2]!, | |
| 36 | ]), | |
| 37 | ); | |
| 38 | ||
| 39 | test("every section the doc keeps is in the page source", () => { | |
| 40 | for (const name of named) | |
| 41 | if (verdicts.get(name) === "Keep") expect(src).toContain(MARKERS[name]!); | |
| 42 | }); | |
| 43 | ||
| 44 | test("every section the doc cuts is gone from the page source", () => { | |
| 45 | const cut = named.filter((name) => verdicts.get(name) === "Cut"); | |
| 46 | expect(cut.sort()).toEqual( | |
| 47 | ["Autonomy line", "Critical path", "Spend bar", "Two weeks"].sort(), | |
| 48 | ); | |
| 49 | for (const name of cut) expect(src).not.toContain(MARKERS[name]!); | |
| 33 | 50 | }); |
| 34 | 51 | |
| 35 | 52 | test("every section says keep or cut", () => { |
test/map.test.ts +10 −6
| @@ -299,7 +299,7 @@ describe("the map, per app and across the forge", () => { | ||
| 299 | 299 | const dormant = await repoOf(alice, "dormant", ["Rewrite the loader"]); |
| 300 | 300 | park(alice, dormant.issues[0]!); |
| 301 | 301 | // bob/secret: bob's own, which alice cannot see and bob can; assisted, |
| 302 | // with no crew, so the spend bar has a ceiling to draw against. | |
| 302 | // with no crew, so it has a ceiling its lane no longer draws. | |
| 303 | 303 | const secret = await repoOf(bob, "secret", ["Rotate the keys"]); |
| 304 | 304 | const on = await updateRepoSettings(app, bob, secret.repo, { |
| 305 | 305 | autonomy: "assisted", |
| @@ -351,9 +351,13 @@ describe("the map, per app and across the forge", () => { | ||
| 351 | 351 | expect(page).toContain('<i data-state="done"></i>done <b>1</b>'); |
| 352 | 352 | expect(page).toContain('<i data-state="waiting"></i>waiting <b>1</b>'); |
| 353 | 353 | expect(page).toContain('<i data-state="parked"></i>parked <b>1</b>'); |
| 354 | // The lists beside it still read the graph. | |
| 354 | // The lists beside it still read the graph; the critical path is the | |
| 355 | // accent on the canvas alone, and spend and flow live on their boards. | |
| 355 | 356 | expect(page).toContain("Ready now"); |
| 356 | expect(page).toContain("Critical path"); | |
| 357 | expect(page).toContain("Blocked on"); | |
| 358 | expect(page).not.toContain("Critical path"); | |
| 359 | expect(page).not.toContain("Two weeks"); | |
| 360 | expect(page).not.toContain("map-autonomy"); | |
| 357 | 361 | }); |
| 358 | 362 | |
| 359 | 363 | test("the drawing is a live region scoped to its repository, fits, and pans", async () => { |
| @@ -417,11 +421,11 @@ describe("the map, per app and across the forge", () => { | ||
| 417 | 421 | // A repository with nothing to draw is not a lane. |
| 418 | 422 | expect(page).not.toContain('href="/alice/bare/map"'); |
| 419 | 423 | // The forge's admin reads every repository, so bob's is a lane here, |
| 420 | // with today's spend drawn against its ceiling … | |
| 424 | // saying its mode; today's spend is the board's, not the lane's … | |
| 421 | 425 | expect(page).toContain('href="/bob/secret/map"'); |
| 422 | 426 | expect(page).toContain('data-autonomy="assisted"'); |
| 423 | expect(page).toContain('class="map-spend-fill" width="0.0"'); | |
| 424 | expect(page).toContain("$0.00 of $10"); | |
| 427 | expect(page).not.toContain("map-spend"); | |
| 428 | expect(page).not.toContain("$0.00 of $10"); | |
| 425 | 429 | |
| 426 | 430 | // … and bob, who is nobody's admin, sees his own and not alice's. |
| 427 | 431 | const bobs = await ( |
test/studio-search.test.ts +1 −1
| @@ -455,7 +455,7 @@ describe("the studio home says what the crew cost you today", () => { | ||
| 455 | 455 | const html = await home(aliceCookie); |
| 456 | 456 | expect(html).toContain("$8.00 spent today across 2 apps you can write"); |
| 457 | 457 | expect(html).toContain("1 paused at"); |
| 458 | expect(html).toMatch(/<a href="\/map">their ceiling<\/a>/); | |
| 458 | expect(html).toMatch(/<a href="\/board">their ceiling<\/a>/); | |
| 459 | 459 | }); |
| 460 | 460 | |
| 461 | 461 | test("a viewer who can only read sees no figure at all", async () => { |
new above
offline