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
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
name: CI

# `schedule` and `workflow_dispatch` exist for the model-data-drift job only.
# Triggers are workflow-wide in Actions, so every job below carries an `if:`
# that scopes it to the events it is actually for.
on:
push:
branches: ["**"]
pull_request:
schedule:
- cron: "0 9 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
build:
name: typecheck · test · build (node ${{ matrix.node-version }})
# Code CI only. The weekly cron and manual dispatch exist for
# model-data-drift; re-running the matrix on them buys nothing.
if: github.event_name == 'push' || github.event_name == 'pull_request'
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down Expand Up @@ -45,8 +54,56 @@ jobs:
- name: Build
run: npm run build

model-data-drift:
name: model limits · drift vs Cursor docs
# Deliberately not on pull_request: this job reaches cursor.com, and PR CI
# stays hermetic.
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Set up Node
uses: actions/setup-node@v7
with:
node-version: "24.x"
cache: npm

- name: Install dependencies
run: npm ci

- name: Check committed model limits against Cursor docs
run: |
log="$RUNNER_TEMP/drift.log"
set +e
npm run sync:model-limits -- --check 2>&1 | tee "$log"
code=${PIPESTATUS[0]}
set -e
if [ "$code" = "1" ]; then
echo "::error::src/model-limits.ts is stale. Run 'npm run sync:model-limits' and commit."
exit 1
fi
if [ "$code" = "2" ]; then
echo "::error::Could not verify model data (docs unreachable, unparseable, or a model id matched nothing). The drift check did not run — treat this as unverified, not as passing."
exit 1
fi
if [ "$code" != "0" ]; then
echo "::error::Unexpected exit code $code from the drift check."
exit "$code"
fi
# Exit 0 is only trustworthy if the run also reported its summary. A
# bare 0 is what "did nothing at all" looks like, and that has
# happened: an entry-point guard once skipped main() entirely and the
# job passed for free. Demand the evidence, not just the code.
if ! grep -qE 'sync-model-limits: src/model-limits\.ts is up to date \(context: [0-9]+ from docs, [0-9]+ overridden \| cost: [0-9]+ from docs, [0-9]+ overridden \| [0-9]+ model ids\)' "$log"; then
echo "::error::Drift check exited 0 without printing a run summary — it did not actually verify anything."
exit 1
fi

integration:
name: e2e · opencode loads plugin & lists models
# Code CI only, same reason as `build`.
if: github.event_name == 'push' || github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "vitest run --config vitest.e2e.config.ts --passWithNoTests",
"sync:model-limits": "node scripts/sync-model-limits-cli.mjs",
"prepublishOnly": "npm run typecheck && npm test && npm run build"
},
"dependencies": {
Expand Down
17 changes: 17 additions & 0 deletions scripts/sync-model-limits-cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env node
/**
* CLI entry point for `scripts/sync-model-limits.mjs`.
*
* This file exists so the generator module stays import-pure (the tests import
* it) without needing an "am I the entry point?" guard inside it. Such a guard
* — comparing `process.argv[1]` against `import.meta.url` — is fail-open: on
* any invocation where the two differ (a symlinked path, a wrapper, an exec
* shim) `main()` never runs and the process exits 0 having done nothing, which
* makes the scheduled drift check permanently green and permanently useless.
* That already happened once, via a symlinked `/tmp` path on macOS.
*
* There is no guard here. Running this file always runs `main()`.
*/
import { main } from "./sync-model-limits.mjs";

process.exitCode = await main(process.argv.slice(2));
56 changes: 56 additions & 0 deletions scripts/sync-model-limits.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Types for `sync-model-limits.mjs`. The script is plain ESM JavaScript (it
* runs via `node` with no build step), but `tsconfig.json` includes `test`, so
* `test/sync-model-limits.test.ts` needs a declaration to import it.
*
* This mirror is hand-maintained. `allowJs: true` would remove the need for it,
* but it does not work in this repo: it pulls `src/sidecar/agent-host.mjs` into
* the program, and that file assigns to `console.log`, which strips `log`,
* `debug`, `info`, `warn`, and `error` off the global `Console` type and breaks
* 30 checks in existing `.ts` files. Measured, not assumed — see
* `.superpowers/sdd/task-6-report.md`. Two things keep this file honest in the
* meantime: `test/sync-model-limits.test.ts` asserts the module's runtime
* export names match the list declared here, and the tests call every declared
* signature, so a parameter that is declared but missing (or vice versa) fails
* `npm run typecheck`.
*/

export type DocsRow = Record<string, string>;

export type ModelCost = {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
};

export type MatchResult =
| { row: DocsRow; ambiguous?: never }
| { row?: never; ambiguous: string[] };

export declare const SOURCES: { context: string; pricing: string };
export declare const MODEL_IDS: string[];
export declare const OVERRIDES: Record<
string,
{ context?: number; cost?: ModelCost; why: string }
>;

export declare function parseDocsTable(md: string, columnNames: string[]): DocsRow[];
export declare function parseTokens(text: string): number | undefined;
export declare function parsePrice(text: string): number;
export declare function matchModelId(id: string, docRows: DocsRow[]): MatchResult | undefined;
export declare function normalizeForComparison(text: string): string;
export declare function generate(input: {
contextMd: string;
pricingMd: string;
modelIds?: readonly string[];
overrides?: Record<string, { context?: number; cost?: ModelCost; why?: string }>;
date?: string;
}): {
text: string;
stats: {
context: { matched: number; overridden: number };
cost: { matched: number; overridden: number };
};
};
export declare function main(argv: string[]): Promise<number>;
Loading