Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions evals/corpus/fixtures/fixtures.spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
id: fixtures
title: Entity fixture factories (one module per entity)
verify: bun test
mode: scratch
---

## Acceptance criteria

A1. For each of the six entities — `user`, `order`, `product`, `invoice`, `payment`, `shipment` — a module `<entity>.ts` exports a factory named `make<Entity>` (e.g. `user.ts` → `makeUser`, `order.ts` → `makeOrder`). Each returns `IEntity`, imported from `./types` (`{ id: string; kind: string }`).

A2. The factory returns an object whose `kind` is the entity name (e.g. `"user"`) and whose `id` is a non-empty string. The six modules are identical in shape — only the name and the `kind` value differ.

## Tasks

1. [fixtures] Create the six entity fixture modules
accept: bun test fixtures.test.ts
files: user.ts, order.ts, product.ts, invoice.ts, payment.ts, shipment.ts
context: fixtures.test.ts, types.ts
27 changes: 27 additions & 0 deletions evals/corpus/fixtures/fixtures.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { test, expect } from "bun:test";
import type { IEntity } from "./types";
import { makeUser } from "./user";
import { makeOrder } from "./order";
import { makeProduct } from "./product";
import { makeInvoice } from "./invoice";
import { makePayment } from "./payment";
import { makeShipment } from "./shipment";

const cases: ReadonlyArray<[string, () => IEntity]> = [
["user", makeUser],
["order", makeOrder],
["product", makeProduct],
["invoice", makeInvoice],
["payment", makePayment],
["shipment", makeShipment],
];

for (const [kind, make] of cases) {
test(`make ${kind} returns a tagged entity with a non-empty id`, () => {
const e = make();

expect(e.kind).toBe(kind);
expect(typeof e.id).toBe("string");
expect(e.id.length).toBeGreaterThan(0);
});
}
5 changes: 5 additions & 0 deletions evals/corpus/fixtures/invoice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { IEntity } from "./types";

export function makeInvoice(): IEntity {
return { id: "invoice-1", kind: "invoice" };
}
5 changes: 5 additions & 0 deletions evals/corpus/fixtures/order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { IEntity } from "./types";

export function makeOrder(): IEntity {
return { id: "order-1", kind: "order" };
}
5 changes: 5 additions & 0 deletions evals/corpus/fixtures/payment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { IEntity } from "./types";

export function makePayment(): IEntity {
return { id: "payment-1", kind: "payment" };
}
5 changes: 5 additions & 0 deletions evals/corpus/fixtures/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { IEntity } from "./types";

export function makeProduct(): IEntity {
return { id: "product-1", kind: "product" };
}
5 changes: 5 additions & 0 deletions evals/corpus/fixtures/shipment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { IEntity } from "./types";

export function makeShipment(): IEntity {
return { id: "shipment-1", kind: "shipment" };
}
4 changes: 4 additions & 0 deletions evals/corpus/fixtures/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IEntity {
id: string;
kind: string;
}
5 changes: 5 additions & 0 deletions evals/corpus/fixtures/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { IEntity } from "./types";

export function makeUser(): IEntity {
return { id: "user-1", kind: "user" };
}
3 changes: 3 additions & 0 deletions evals/corpus/handlers/created.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function handleCreated(): { status: number; body: string } {
return { status: 201, body: "created" };
}
3 changes: 3 additions & 0 deletions evals/corpus/handlers/gone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function handleGone(): { status: number; body: string } {
return { status: 410, body: "gone" };
}
25 changes: 25 additions & 0 deletions evals/corpus/handlers/handlers.spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
id: handlers
title: Route handlers (one module per route)
verify: bun test
mode: scratch
---

## Acceptance criteria

Each route is its own module exporting `handle<Name>(): { status: number; body: string }`. All seven share the same shape — only the status and body differ.

A1. `health.ts` → `handleHealth` → `{ status: 200, body: "ok" }`
A2. `version.ts` → `handleVersion` → `{ status: 200, body: "v1" }`
A3. `ping.ts` → `handlePing` → `{ status: 200, body: "pong" }`
A4. `teapot.ts` → `handleTeapot` → `{ status: 418, body: "teapot" }`
A5. `notFound.ts` → `handleNotFound` → `{ status: 404, body: "not found" }`
A6. `gone.ts` → `handleGone` → `{ status: 410, body: "gone" }`
A7. `created.ts` → `handleCreated` → `{ status: 201, body: "created" }`

## Tasks

1. [handlers] Create the seven route handler modules
accept: bun test handlers.test.ts
files: health.ts, version.ts, ping.ts, teapot.ts, notFound.ts, gone.ts, created.ts
context: handlers.test.ts
29 changes: 29 additions & 0 deletions evals/corpus/handlers/handlers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test, expect } from "bun:test";
import { handleHealth } from "./health";
import { handleVersion } from "./version";
import { handlePing } from "./ping";
import { handleTeapot } from "./teapot";
import { handleNotFound } from "./notFound";
import { handleGone } from "./gone";
import { handleCreated } from "./created";

interface IReply {
status: number;
body: string;
}

const cases: ReadonlyArray<[() => IReply, number, string]> = [
[handleHealth, 200, "ok"],
[handleVersion, 200, "v1"],
[handlePing, 200, "pong"],
[handleTeapot, 418, "teapot"],
[handleNotFound, 404, "not found"],
[handleGone, 410, "gone"],
[handleCreated, 201, "created"],
];

for (const [handle, status, body] of cases) {
test(`${body} handler returns ${String(status)}`, () => {
expect(handle()).toEqual({ status, body });
});
}
3 changes: 3 additions & 0 deletions evals/corpus/handlers/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function handleHealth(): { status: number; body: string } {
return { status: 200, body: "ok" };
}
3 changes: 3 additions & 0 deletions evals/corpus/handlers/notFound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function handleNotFound(): { status: number; body: string } {
return { status: 404, body: "not found" };
}
3 changes: 3 additions & 0 deletions evals/corpus/handlers/ping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function handlePing(): { status: number; body: string } {
return { status: 200, body: "pong" };
}
3 changes: 3 additions & 0 deletions evals/corpus/handlers/teapot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function handleTeapot(): { status: number; body: string } {
return { status: 418, body: "teapot" };
}
3 changes: 3 additions & 0 deletions evals/corpus/handlers/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function handleVersion(): { status: number; body: string } {
return { status: 200, body: "v1" };
}
7 changes: 7 additions & 0 deletions evals/corpus/migrate/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function oldApi(payload: string): string {
return payload;
}

export function newApi(payload: string, tier: string): string {
return `${tier}:${payload}`;
}
19 changes: 19 additions & 0 deletions evals/corpus/migrate/migrate.spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
id: migrate
title: Migrate every service from oldApi to newApi (per-file tier)
verify: bun test
mode: existing
---

## Acceptance criteria

A1. Every `svc<N>.ts` currently calls the deprecated `oldApi(payload)`. Migrate each to `newApi(payload, tier)`, where `tier` is the string from that file's `// tier: <name>` header comment (e.g. `svc1.ts` is `// tier: gold` → `newApi("ping", "gold")`). The tier differs per file, so each edit is distinct — you must read each file to know its tier.

A2. Import `newApi` from `./api` and remove the now-unused `oldApi` import (the gate forbids unused imports). Do not change `api.ts` or the payload string.

## Tasks

1. [migrate] Migrate all eight services to newApi with their per-file tier
accept: bun test migrate.test.ts
files: svc1.ts, svc2.ts, svc3.ts, svc4.ts, svc5.ts, svc6.ts, svc7.ts, svc8.ts
context: migrate.test.ts, api.ts
26 changes: 26 additions & 0 deletions evals/corpus/migrate/migrate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test, expect } from "bun:test";
import { run as r1 } from "./svc1";
import { run as r2 } from "./svc2";
import { run as r3 } from "./svc3";
import { run as r4 } from "./svc4";
import { run as r5 } from "./svc5";
import { run as r6 } from "./svc6";
import { run as r7 } from "./svc7";
import { run as r8 } from "./svc8";

const cases: ReadonlyArray<[() => string, string]> = [
[r1, "gold:ping"],
[r2, "silver:ping"],
[r3, "bronze:ping"],
[r4, "platinum:ping"],
[r5, "diamond:ping"],
[r6, "copper:ping"],
[r7, "iron:ping"],
[r8, "steel:ping"],
];

for (const [run, expected] of cases) {
test(`migrated service returns ${expected}`, () => {
expect(run()).toBe(expected);
});
}
6 changes: 6 additions & 0 deletions evals/corpus/migrate/svc1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// tier: gold
import { oldApi } from "./api";

export function run(): string {
return oldApi("ping");
}
6 changes: 6 additions & 0 deletions evals/corpus/migrate/svc2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// tier: silver
import { oldApi } from "./api";

export function run(): string {
return oldApi("ping");
}
6 changes: 6 additions & 0 deletions evals/corpus/migrate/svc3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// tier: bronze
import { oldApi } from "./api";

export function run(): string {
return oldApi("ping");
}
6 changes: 6 additions & 0 deletions evals/corpus/migrate/svc4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// tier: platinum
import { oldApi } from "./api";

export function run(): string {
return oldApi("ping");
}
6 changes: 6 additions & 0 deletions evals/corpus/migrate/svc5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// tier: diamond
import { oldApi } from "./api";

export function run(): string {
return oldApi("ping");
}
6 changes: 6 additions & 0 deletions evals/corpus/migrate/svc6.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// tier: copper
import { oldApi } from "./api";

export function run(): string {
return oldApi("ping");
}
6 changes: 6 additions & 0 deletions evals/corpus/migrate/svc7.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// tier: iron
import { oldApi } from "./api";

export function run(): string {
return oldApi("ping");
}
6 changes: 6 additions & 0 deletions evals/corpus/migrate/svc8.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// tier: steel
import { oldApi } from "./api";

export function run(): string {
return oldApi("ping");
}
3 changes: 3 additions & 0 deletions evals/corpus/validators/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isEmail(v: string): boolean {
return /^[^@\s]+@[^@\s]+\.[^@\s]+$/u.test(v);
}
3 changes: 3 additions & 0 deletions evals/corpus/validators/hexColor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isHexColor(v: string): boolean {
return /^#?[0-9a-f]{6}$/iu.test(v);
}
3 changes: 3 additions & 0 deletions evals/corpus/validators/nonEmpty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isNonEmpty(v: string): boolean {
return v.trim().length > 0;
}
5 changes: 5 additions & 0 deletions evals/corpus/validators/positive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function isPositive(v: string): boolean {
const n = Number(v);

return Number.isFinite(n) && n > 0;
}
3 changes: 3 additions & 0 deletions evals/corpus/validators/slug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isSlug(v: string): boolean {
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(v);
}
3 changes: 3 additions & 0 deletions evals/corpus/validators/uuid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isUuid(v: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(v);
}
24 changes: 24 additions & 0 deletions evals/corpus/validators/validators.spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
id: validators
title: Field validators (one predicate module per rule)
verify: bun test
mode: scratch
---

## Acceptance criteria

Each rule lives in its own module exporting a single predicate `(v: string) => boolean`. All six share the same shape — only the rule differs.

A1. `nonEmpty.ts` → `isNonEmpty`: true iff `v` has at least one non-whitespace character.
A2. `positive.ts` → `isPositive`: true iff `v` parses to a finite number greater than 0.
A3. `email.ts` → `isEmail`: true iff `v` looks like `local@domain.tld` (non-empty local, domain, and TLD; no spaces or stray `@`).
A4. `slug.ts` → `isSlug`: true iff `v` is lowercase alphanumeric words joined by single hyphens (e.g. `my-post-1`), no leading/trailing/double hyphens, no spaces or uppercase.
A5. `hexColor.ts` → `isHexColor`: true iff `v` is a 6-digit hex color, optional leading `#` (e.g. `#a1b2c3` or `a1b2c3`), case-insensitive.
A6. `uuid.ts` → `isUuid`: true iff `v` is a canonical 8-4-4-4-12 hex UUID.

## Tasks

1. [validators] Create the six predicate modules
accept: bun test validators.test.ts
files: nonEmpty.ts, positive.ts, email.ts, slug.ts, hexColor.ts, uuid.ts
context: validators.test.ts
37 changes: 37 additions & 0 deletions evals/corpus/validators/validators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { test, expect } from "bun:test";
import { isNonEmpty } from "./nonEmpty";
import { isPositive } from "./positive";
import { isEmail } from "./email";
import { isSlug } from "./slug";
import { isHexColor } from "./hexColor";
import { isUuid } from "./uuid";

const valid: ReadonlyArray<[string, (v: string) => boolean, string]> = [
["nonEmpty", isNonEmpty, "x"],
["positive", isPositive, "3"],
["email", isEmail, "a@b.co"],
["slug", isSlug, "my-post-1"],
["hexColor", isHexColor, "#a1b2c3"],
["uuid", isUuid, "123e4567-e89b-12d3-a456-426614174000"],
];

const invalid: ReadonlyArray<[string, (v: string) => boolean, string]> = [
["nonEmpty", isNonEmpty, " "],
["positive", isPositive, "-2"],
["email", isEmail, "nope"],
["slug", isSlug, "Not A Slug"],
["hexColor", isHexColor, "#zzz"],
["uuid", isUuid, "123"],
];

for (const [name, fn, ok] of valid) {
test(`${name} accepts a valid value`, () => {
expect(fn(ok)).toBe(true);
});
}

for (const [name, fn, bad] of invalid) {
test(`${name} rejects an invalid value`, () => {
expect(fn(bad)).toBe(false);
});
}
9 changes: 9 additions & 0 deletions packages/core/scripts/sweep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const DIM_ENV: Record<string, string> = {
hashline: "TSFORGE_HASHLINE",
lsp_write_feedback: "TSFORGE_LSP_WRITE_FEEDBACK",
simplicity: "TSFORGE_SIMPLICITY",
web: "TSFORGE_WEB",
};

/** Map feature variant to env vars. Most dims set their var to the state; `git`
Expand All @@ -96,6 +97,14 @@ function variantToEnvVars(variant: IFeatureVariant): Record<string, string> {
continue;
}

// `script` is default-ON; like `git` it gates a NO_ flag, so script=on →
// the tool is available (NO_SCRIPT unset), script=off → withheld.
if (dim === "script") {
envVars.TSFORGE_NO_SCRIPT = state === "1" ? "0" : "1";

continue;
}

const varName = DIM_ENV[dim];

if (varName !== undefined) {
Expand Down
Loading