Skip to content

Commit 89061f0

Browse files
docs: make catalog release notes scannable with collapsed model lists
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent fe02413 commit 89061f0

7 files changed

Lines changed: 275 additions & 0 deletions

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- GitHub flow: PR-gated `main`, CI matrix (`check (test|typecheck|pack)`), semantic-release on merge, catalog updates as PRs.
13+
- Release notes open with a catalog summary table and collapsed lists of fallback / unpriced models.
1314
- `manifest.json` describing the bundled catalog (status, cost sources, command-code version).
1415
- First publish of `@brainervirus/commandcode-go-opencode-provider`.
1516
- Last-good catalog cache and silent `startup.json` diagnostics under the plugin state dir.

‎release.config.cjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module.exports = {
22
branches: ["main"],
33
plugins: [
44
"@semantic-release/commit-analyzer",
5+
"./scripts/semantic-release-catalog-notes.cjs",
56
"@semantic-release/release-notes-generator",
67
"@semantic-release/npm",
78
"@semantic-release/github",

‎scripts/catalog-release-notes.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bun
2+
import { readFileSync } from "fs"
3+
import { join } from "path"
4+
import { FALLBACK_COSTS } from "../src/catalog.js"
5+
import { catalogReleaseNotesFromFiles } from "../src/catalog-release-notes.js"
6+
import type { CatalogManifest } from "../src/manifest.js"
7+
import type { PricedModel } from "../src/catalog-release-notes.js"
8+
9+
const ROOT = join(import.meta.dir, "..")
10+
const manifest = JSON.parse(readFileSync(join(ROOT, "manifest.json"), "utf-8")) as CatalogManifest
11+
const models = JSON.parse(readFileSync(join(ROOT, "models.json"), "utf-8")) as PricedModel[]
12+
process.stdout.write(
13+
catalogReleaseNotesFromFiles({
14+
manifest,
15+
models,
16+
fallbackCosts: FALLBACK_COSTS,
17+
}),
18+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const { execFileSync } = require("node:child_process")
2+
const { join } = require("node:path")
3+
4+
module.exports = {
5+
generateNotes() {
6+
return execFileSync("bun", [join(__dirname, "catalog-release-notes.ts")], {
7+
encoding: "utf8",
8+
})
9+
},
10+
}

‎src/catalog-release-notes.ts‎

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import type { CatalogManifest, CatalogStatus, CostSources } from "./manifest.js"
2+
3+
export type PricedModel = {
4+
id: string
5+
name: string
6+
cost: { input: number; output: number }
7+
}
8+
9+
export type CatalogReleaseInput = {
10+
pluginVersion: string
11+
commandCodeVersion: string
12+
modelCount: number
13+
reasoningModelCount: number
14+
status: CatalogStatus
15+
costSources: CostSources
16+
models: PricedModel[]
17+
fallbackCosts: Record<string, { input: number; output: number }>
18+
}
19+
20+
const DEFAULT_COST = { input: 0.5, output: 2 }
21+
22+
export function partitionPrices(
23+
models: PricedModel[],
24+
fallbackCosts: Record<string, { input: number; output: number }>,
25+
): { official: PricedModel[]; fallback: PricedModel[]; unmatched: PricedModel[] } {
26+
const official: PricedModel[] = []
27+
const fallback: PricedModel[] = []
28+
const unmatched: PricedModel[] = []
29+
for (const model of models) {
30+
const known = fallbackCosts[model.id]
31+
if (known && known.input === model.cost.input && known.output === model.cost.output) {
32+
fallback.push(model)
33+
} else if (model.cost.input === DEFAULT_COST.input && model.cost.output === DEFAULT_COST.output) {
34+
unmatched.push(model)
35+
} else {
36+
official.push(model)
37+
}
38+
}
39+
return { official, fallback, unmatched }
40+
}
41+
42+
function n(count: number, one: string, many: string): string {
43+
return `${count} ${count === 1 ? one : many}`
44+
}
45+
46+
function headline(status: CatalogStatus): string {
47+
if (status === "healthy") {
48+
return "**Catalog is complete.** All listed prices come from Command Code or official docs."
49+
}
50+
if (status === "broken") {
51+
return "**Do not use this catalog.** Model extraction failed; OpenCode would not get a current model list."
52+
}
53+
return "**Safe to use.** Every model is listed. Some prices are estimates — expand the sections below to review them."
54+
}
55+
56+
function priceSummary(sources: CostSources): string {
57+
const parts: string[] = []
58+
if (sources.cli > 0) parts.push(`${sources.cli} CLI`)
59+
if (sources.officialDocs > 0) parts.push(`${sources.officialDocs} official docs`)
60+
if (sources.thirdParty > 0) parts.push(`${sources.thirdParty} third-party`)
61+
if (sources.fallback > 0) parts.push(`${sources.fallback} known fallback${sources.fallback === 1 ? "" : "s"}`)
62+
if (sources.unmatched > 0) parts.push(`${sources.unmatched} no listed price`)
63+
return parts.join(" · ") || "none"
64+
}
65+
66+
function money(n: number): string {
67+
return `$${n}`
68+
}
69+
70+
function modelTable(models: PricedModel[]): string {
71+
const rows = models
72+
.map((m) => `| ${m.name} | \`${m.id}\` | ${money(m.cost.input)} / ${money(m.cost.output)} |`)
73+
.join("\n")
74+
return `| Model | Id | In / out per 1M |\n| --- | --- | --- |\n${rows}`
75+
}
76+
77+
function details(summary: string, body: string): string {
78+
return `<details>\n<summary>${summary}</summary>\n\n${body}\n\n</details>`
79+
}
80+
81+
export function renderCatalogReleaseNotes(input: CatalogReleaseInput): string {
82+
const { fallback, unmatched } = partitionPrices(input.models, input.fallbackCosts)
83+
const sections = [
84+
headline(input.status),
85+
"",
86+
"| | |",
87+
"| --- | --- |",
88+
`| Plugin | ${input.pluginVersion} |`,
89+
`| Command Code | ${input.commandCodeVersion} |`,
90+
`| Models | ${input.modelCount} (${input.reasoningModelCount} with reasoning) |`,
91+
`| Prices | ${priceSummary(input.costSources)} |`,
92+
]
93+
94+
if (fallback.length > 0) {
95+
sections.push(
96+
"",
97+
details(
98+
n(fallback.length, "model with a known fallback price", "models with known fallback prices"),
99+
modelTable(fallback),
100+
),
101+
)
102+
}
103+
if (unmatched.length > 0) {
104+
sections.push(
105+
"",
106+
details(
107+
n(unmatched.length, "model with no official price ($0.50 / $2)", "models with no official price ($0.50 / $2)"),
108+
modelTable(unmatched),
109+
),
110+
)
111+
}
112+
113+
sections.push(
114+
"",
115+
details(
116+
"Machine-readable catalog",
117+
"```json\n" +
118+
JSON.stringify(
119+
{
120+
status: input.status,
121+
pluginVersion: input.pluginVersion,
122+
commandCodeVersion: input.commandCodeVersion,
123+
modelCount: input.modelCount,
124+
reasoningModelCount: input.reasoningModelCount,
125+
costSources: input.costSources,
126+
},
127+
null,
128+
2,
129+
) +
130+
"\n```",
131+
),
132+
)
133+
134+
return sections.join("\n") + "\n"
135+
}
136+
137+
export function catalogReleaseNotesFromFiles(input: {
138+
manifest: CatalogManifest
139+
models: PricedModel[]
140+
fallbackCosts: Record<string, { input: number; output: number }>
141+
}): string {
142+
return renderCatalogReleaseNotes({
143+
pluginVersion: input.manifest.pluginVersion,
144+
commandCodeVersion: input.manifest.commandCodeVersion,
145+
modelCount: input.manifest.modelCount,
146+
reasoningModelCount: input.manifest.reasoningModelCount,
147+
status: input.manifest.status,
148+
costSources: input.manifest.costSources,
149+
models: input.models,
150+
fallbackCosts: input.fallbackCosts,
151+
})
152+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { expect, test, describe } from "bun:test"
2+
import { renderCatalogReleaseNotes, type CatalogReleaseInput } from "../../src/catalog-release-notes.ts"
3+
4+
const fallbackCosts = {
5+
"google/gemini-3.5-flash": { input: 1.5, output: 9 },
6+
}
7+
8+
function input(partial: Partial<CatalogReleaseInput> = {}): CatalogReleaseInput {
9+
return {
10+
pluginVersion: "0.5.0",
11+
commandCodeVersion: "1.38.1",
12+
modelCount: 3,
13+
reasoningModelCount: 2,
14+
status: "degraded",
15+
costSources: { cli: 0, officialDocs: 1, thirdParty: 0, fallback: 1, unmatched: 1 },
16+
models: [
17+
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", cost: { input: 3, output: 15 } },
18+
{ id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash", cost: { input: 1.5, output: 9 } },
19+
{ id: "tencent/Hy3", name: "Tencent Hy3 (Free)", cost: { input: 0.5, output: 2 } },
20+
],
21+
fallbackCosts,
22+
...partial,
23+
}
24+
}
25+
26+
describe("renderCatalogReleaseNotes", () => {
27+
test("leads with a usable headline and hides the raw status word", () => {
28+
const md = renderCatalogReleaseNotes(input())
29+
expect(md).toContain("Safe to use")
30+
expect(md).not.toMatch(/^#.*degraded/m)
31+
expect(md.startsWith("degraded")).toBe(false)
32+
})
33+
34+
test("puts counts in a compact table", () => {
35+
const md = renderCatalogReleaseNotes(input())
36+
expect(md).toContain("| Command Code | 1.38.1 |")
37+
expect(md).toContain("| Models | 3 (2 with reasoning) |")
38+
expect(md).toContain("1 official docs")
39+
expect(md).toContain("1 known fallback")
40+
expect(md).toContain("1 no listed price")
41+
})
42+
43+
test("collapses fallback and unmatched model lists", () => {
44+
const md = renderCatalogReleaseNotes(input())
45+
expect(md).toContain("<details>")
46+
expect(md).toContain("<summary>1 model with a known fallback price</summary>")
47+
expect(md).toContain("<summary>1 model with no official price ($0.50 / $2)</summary>")
48+
expect(md).toContain("Gemini 3.5 Flash")
49+
expect(md).toContain("Tencent Hy3 (Free)")
50+
expect(md).toContain("tencent/Hy3")
51+
})
52+
53+
test("healthy catalog skips the extra lists", () => {
54+
const md = renderCatalogReleaseNotes(
55+
input({
56+
status: "healthy",
57+
costSources: { cli: 0, officialDocs: 3, thirdParty: 0, fallback: 0, unmatched: 0 },
58+
models: [
59+
{ id: "a", name: "A", cost: { input: 1, output: 2 } },
60+
{ id: "b", name: "B", cost: { input: 1, output: 2 } },
61+
{ id: "c", name: "C", cost: { input: 1, output: 2 } },
62+
],
63+
}),
64+
)
65+
expect(md).toContain("All listed prices come from Command Code or official docs")
66+
expect(md).not.toContain("known fallback")
67+
expect(md).not.toContain("no official price")
68+
})
69+
70+
test("broken catalog is explicit", () => {
71+
const md = renderCatalogReleaseNotes(
72+
input({
73+
status: "broken",
74+
modelCount: 0,
75+
reasoningModelCount: 0,
76+
models: [],
77+
costSources: { cli: 0, officialDocs: 0, thirdParty: 0, fallback: 0, unmatched: 0 },
78+
}),
79+
)
80+
expect(md).toContain("Do not use this catalog")
81+
})
82+
83+
test("includes a collapsed machine-readable block for agents", () => {
84+
const md = renderCatalogReleaseNotes(input())
85+
expect(md).toContain("<summary>Machine-readable catalog</summary>")
86+
expect(md).toContain('"status": "degraded"')
87+
expect(md).toContain('"unmatched": 1')
88+
})
89+
})

‎tests/unit/release-workflow.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,12 @@ describe("release.config.cjs", () => {
109109
test("publishes the root package then GitHub Release", () => {
110110
const cfg = read("release.config.cjs")
111111
expect(cfg).toContain("@semantic-release/commit-analyzer")
112+
expect(cfg).toContain("./scripts/semantic-release-catalog-notes.cjs")
112113
expect(cfg).toContain("@semantic-release/npm")
113114
expect(cfg).toContain("@semantic-release/github")
115+
expect(cfg.indexOf("semantic-release-catalog-notes.cjs")).toBeLessThan(
116+
cfg.indexOf("@semantic-release/release-notes-generator"),
117+
)
114118
expect(cfg.indexOf("@semantic-release/npm")).toBeLessThan(cfg.indexOf("@semantic-release/github"))
115119
})
116120
})

0 commit comments

Comments
 (0)