feat(intake): size band derived from decomposition and known areas
This commit is contained in:
parent
95c4c6c8fa
commit
04f38ee114
2 changed files with 110 additions and 0 deletions
60
src/lib/intake/size.test.ts
Normal file
60
src/lib/intake/size.test.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { deriveSizeBand } from "@/lib/intake/size";
|
||||
|
||||
describe("deriveSizeBand", () => {
|
||||
it("calls a short decomposition small", () => {
|
||||
expect(
|
||||
deriveSizeBand({ subtaskCount: 2, knownScopes: ["stock"], suggestedArea: "stock" })
|
||||
).toBe("small");
|
||||
});
|
||||
|
||||
it("calls a middling decomposition medium", () => {
|
||||
expect(
|
||||
deriveSizeBand({ subtaskCount: 5, knownScopes: ["stock"], suggestedArea: "stock" })
|
||||
).toBe("medium");
|
||||
});
|
||||
|
||||
it("calls a long decomposition large", () => {
|
||||
expect(
|
||||
deriveSizeBand({ subtaskCount: 9, knownScopes: ["stock"], suggestedArea: "stock" })
|
||||
).toBe("large");
|
||||
});
|
||||
|
||||
it("raises the band when the area is new to the project", () => {
|
||||
// Work in an area with no commit history is less well understood than the
|
||||
// same-sized work somewhere the project has been before.
|
||||
expect(
|
||||
deriveSizeBand({ subtaskCount: 2, knownScopes: ["stock"], suggestedArea: "pos" })
|
||||
).toBe("medium");
|
||||
});
|
||||
|
||||
it("does not raise past large", () => {
|
||||
expect(
|
||||
deriveSizeBand({ subtaskCount: 12, knownScopes: ["stock"], suggestedArea: "pos" })
|
||||
).toBe("large");
|
||||
});
|
||||
|
||||
it("treats an unknown area as new", () => {
|
||||
expect(
|
||||
deriveSizeBand({ subtaskCount: 2, knownScopes: [], suggestedArea: "" })
|
||||
).toBe("medium");
|
||||
});
|
||||
|
||||
it("matches the area case-insensitively", () => {
|
||||
expect(
|
||||
deriveSizeBand({ subtaskCount: 2, knownScopes: ["Stock"], suggestedArea: "stock" })
|
||||
).toBe("small");
|
||||
});
|
||||
|
||||
it("never returns an empty band for a real brief", () => {
|
||||
for (const count of [0, 1, 3, 6, 20]) {
|
||||
const band = deriveSizeBand({ subtaskCount: count, knownScopes: [], suggestedArea: "" });
|
||||
expect(["small", "medium", "large"]).toContain(band);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not crash on a negative or absurd count", () => {
|
||||
expect(deriveSizeBand({ subtaskCount: -5, knownScopes: ["a"], suggestedArea: "a" })).toBe("small");
|
||||
expect(deriveSizeBand({ subtaskCount: 10_000, knownScopes: ["a"], suggestedArea: "a" })).toBe("large");
|
||||
});
|
||||
});
|
||||
50
src/lib/intake/size.ts
Normal file
50
src/lib/intake/size.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* A rough size band for a request. Small, medium or large — never a duration.
|
||||
*
|
||||
* Derived rather than guessed, from two things the system actually knows:
|
||||
*
|
||||
* - How many sub-tasks the request decomposed into. A request that becomes two
|
||||
* steps is not the same as one that becomes eleven.
|
||||
* - Whether it lands in an area the project already has commit history in.
|
||||
* Phase 2 knows every conventional-commit scope and how much work each holds.
|
||||
*
|
||||
* Stated plainly because it is the weakest thing in this phase: this is a
|
||||
* judgement made from a description collected minutes ago. Better than nothing,
|
||||
* worse than the developer's opinion, and the UI must never imply otherwise —
|
||||
* it is shown as a first impression and the developer sets the real one when
|
||||
* accepting from the Inbox.
|
||||
*
|
||||
* NEVER render this next to a date or a duration. The spec forbids attaching
|
||||
* time to it, and a band beside a date reads as an estimate no matter how it is
|
||||
* labelled.
|
||||
*/
|
||||
|
||||
export type SizeBand = "small" | "medium" | "large";
|
||||
|
||||
const ORDER: SizeBand[] = ["small", "medium", "large"];
|
||||
|
||||
export function deriveSizeBand(input: {
|
||||
subtaskCount: number;
|
||||
/** Conventional-commit scopes this project already has history in. */
|
||||
knownScopes: string[];
|
||||
/** The area the brief suggests, or "". */
|
||||
suggestedArea: string;
|
||||
}): SizeBand {
|
||||
const count = Number.isFinite(input.subtaskCount)
|
||||
? Math.max(0, input.subtaskCount)
|
||||
: 0;
|
||||
|
||||
let index = count <= 3 ? 0 : count <= 7 ? 1 : 2;
|
||||
|
||||
const area = (input.suggestedArea ?? "").trim().toLowerCase();
|
||||
const known = new Set(
|
||||
(input.knownScopes ?? []).map((scope) => scope.trim().toLowerCase())
|
||||
);
|
||||
|
||||
// New territory: the same amount of visible work carries more unknowns.
|
||||
if (!area || !known.has(area)) {
|
||||
index = Math.min(index + 1, ORDER.length - 1);
|
||||
}
|
||||
|
||||
return ORDER[index];
|
||||
}
|
||||
Loading…
Reference in a new issue