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
5 changes: 4 additions & 1 deletion scripts/analyze-release-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,18 @@ export function latestTag(root = process.cwd()): string | null {

type Level = "major" | "minor" | "patch";
const LEVEL_RANK: Record<Level, number> = { patch: 1, minor: 2, major: 3 };
// "chore(scope)" covers routine product refreshes that still ship (e.g.
// chore(catalog): sync models.json); "chore" without a scope stays inert.
const TYPE_LEVEL: Record<string, Level> = { fix: "patch", perf: "patch", feat: "minor" };

const subjectLevel = (commit: string): Level | null => {
const firstLine = commit.split("\n")[0] ?? "";
const m = /^(?:fix|perf|feat)(?:\([^)]*\))?!?:/.exec(firstLine);
const m = /^(?:(?:fix|perf|feat)|chore\([^)]*\))(?:\([^)]*\))?!?:/.exec(firstLine);
if (!m) return null;
if (m[0].includes("!")) return "major";
const body = commit.split("\n").slice(1).join("\n");
const type = m[0].replace(/\(.*$/, "").replace(/!$/, "").replace(/:$/, "");
if (type === "chore") return "patch";
return /BREAKING[- ]CHANGE:/.test(body) ? "major" : (TYPE_LEVEL[type] ?? null);
};

Expand Down
3 changes: 2 additions & 1 deletion src/catalog-break.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export function catalogBreakTitle(commandCodeVersion: string): string {
return `[catalog-break] command-code@${commandCodeVersion} — model extraction failed`;
const version = commandCodeVersion.trim().replace(/^v/, "");
return `[catalog-break] command-code@${version} — model extraction failed`;
}

export function renderCatalogBreakBody(input: {
Expand Down
6 changes: 4 additions & 2 deletions src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,15 +279,17 @@ export function extractStringBindings(
): Record<string, string> {
const before = source.slice(Math.max(0, endIdx - window), endIdx);
const bindings: Record<string, string> = {};
const strRe = /\b([A-Za-z_$][\w$]*)="([^"]*)"/g;
// \b does not fire before "$" (not a word char); minified bundles use names like
// $R="vercel-ai-gateway". A lookbehind boundary handles both $ and letter names.
const strRe = /(?<![A-Za-z0-9_$])([A-Za-z_$][\w$]*)="([^"]*)"/g;
let m: RegExpExecArray | null;
while ((m = strRe.exec(before))) {
const name = m[1];
const value = m[2];
if (name !== undefined && value !== undefined) bindings[name] = value;
}
for (let pass = 0; pass < 4; pass++) {
const aliasRe = /\b([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)\b/g;
const aliasRe = /(?<![A-Za-z0-9_$])([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)(?![A-Za-z0-9_$])/g;
while ((m = aliasRe.exec(before))) {
const alias = m[1];
const target = m[2];
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/analyze-release-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,27 @@ describe("analyzeReleaseScope", () => {
r.cleanup();
}
});

test("chore(scope) commits that only touch non-product files yield no release", () => {
const r = repo();
r.tag("v0.6.0");
try {
r.commit("chore(catalog): tweak CI", { ".github/workflows/catalog-sync.yml": "cron: 0 *\n" });
expect(analyzeReleaseScope(r.root)).toEqual({ level: null });
} finally {
r.cleanup();
}
});

test("chore(scope) with product-file change still releases at patch", () => {
const r = repo();
r.tag("v0.6.0");
try {
r.commit("chore(catalog): refresh", { "models.json": "[]\n" });
// prod file touched + chore(scope): treat as the scope's default patch release
expect(analyzeReleaseScope(r.root).level).toBe("patch");
} finally {
r.cleanup();
}
});
});
20 changes: 20 additions & 0 deletions tests/unit/catalog-break.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ describe("catalogBreakTitle", () => {
"[catalog-break] command-code@1.39.0 — model extraction failed",
);
});

test("survives npm version tags with leading v", () => {
expect(catalogBreakTitle("v1.39.0")).toBe(
"[catalog-break] command-code@1.39.0 — model extraction failed",
);
});
});

describe("renderCatalogBreakBody", () => {
Expand All @@ -27,6 +33,20 @@ describe("renderCatalogBreakBody", () => {
expect(body).toContain("1.38.1");
expect(body).toContain("src/catalog.ts");
});

test("normalizes whitespace inside the embedded error and bundled version", () => {
const body = renderCatalogBreakBody({
commandCodeVersion: "1.40.1",
error: "SyntaxError: unexpected token\n at foo",
workflowUrl: "https://example.com/run",
bundledCommandCodeVersion: "0.7.4",
});
// error code block preserved verbatim
expect(body).toContain("SyntaxError: unexpected token");
// no double blank lines or stray leading spaces collapse the markdown
expect(body).not.toMatch(/\n{3,}/);
expect(body).toContain("command-code@1.40.1");
});
});

describe("catalogBreakResolvedComment", () => {
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,20 @@ describe("loadCatalogFromBundle", () => {
expect(gpt!.modalities).toEqual({ input: ["text"], output: ["text"] });
});

test("binds string vars prefixed with $ (minifier shape) for catalog eval", () => {
// command-code 1.40 bundles provider/spec constants into $R="..."-style vars;
// a \b boundary skips "$" and breaks evaluation of the model catalog object.
const source = [
'var $R="vercel-ai-gateway",KR="chatComplete",qR="responses";',
'var Sn=($R=>({SONNET_4_6:{id:"claude-sonnet-4-6",provider:$R,spec:KR,label:"Sonnet",name:"Claude Sonnet 4.6",description:"d",reasoning:!0,reasoningEfforts:["low","high"],contextWindow:2e5},GPT_X:{id:"gpt-5.5",provider:"openai",spec:qR,label:"GPT",name:"GPT-5.5",description:"d",inputModalities:["text"]}}))($R);',
].join("");

const entries = loadCatalogFromBundle(source);
const sonnet = entries.find((e) => e.id === "claude-sonnet-4-6");
expect(sonnet).toBeDefined();
expect(sonnet!.reasoningEfforts).toEqual(["low", "high"]);
});

test("returns models when cost extraction fails", () => {
const source = [
'(Wt={ANTHROPIC:"anthropic",OPENAI:"openai",VERCEL_AI_GATEWAY:"vercel-ai-gateway"});',
Expand Down