From dc26026ad76e5fda6920fa0f0ef905dab00ff0af Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:35:40 +0200 Subject: [PATCH] refactor(ci): adopt ai-kit, the package ai-ration became MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ai-ration` is now `ai-kit`, because the name described one of its five modules and hid the rest — and the person deciding whether to install it could not tell what it did. That is an adoption cost paid at every install decision, and this is the package with ONE adopter whose absence took five repos down together on 2026-08-26. The audit here was its second adopter, so it moves first. `AI_RATION_FROM` still works and the old paths stay in the resolver list: a rename must not turn a working daily audit into a silent exit 2 on the first machine that has not caught up. Installed from GitHub rather than npm because ai-kit v0.3.0 is tagged, built and verified but not on the registry — the repo has no NPM_TOKEN, which needs an account nobody here holds. Switch to `npm i ai-kit` once it does. SHARED.md records WHY the merge stopped where it did: ai-kit absorbed ai-ration and re-exports ai-forms, because to an app "which model do we call", "AI chat" and "AI form fill" are one feature. threadkit and limitkit stay out — neither is about AI, and an app throttling its login form should not install a model catalogue to do it. Merge by what a consumer needs together, never by "these are all shared utilities". Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/model-pins.yml | 18 +++++++++++------- SHARED.md | 14 +++++++++++--- scripts/ci/model-pin-audit.mjs | 26 ++++++++++++++++---------- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/.github/workflows/model-pins.yml b/.github/workflows/model-pins.yml index be1a042..945ad4c 100644 --- a/.github/workflows/model-pins.yml +++ b/.github/workflows/model-pins.yml @@ -53,21 +53,25 @@ jobs: - name: Self-test the detector run: node scripts/ci/test-model-pin-audit.mjs - # dotfiles has no package.json on purpose. Install ai-ration into a - # scratch dir and point the audit at it — the same shape ui-defects.yml - # uses for playwright. The vendor query lives in ai-ration precisely so - # this repo does not grow a second copy of it. - - name: Install ai-ration + # dotfiles has no package.json on purpose. Install ai-kit into a scratch + # dir and point the audit at it — the same shape ui-defects.yml uses for + # playwright. The vendor query lives in ai-kit precisely so this repo does + # not grow a second copy of it. + # + # Installed from GitHub rather than npm: ai-kit v0.3.0 is tagged and built + # but not on the registry, because the repo has no NPM_TOKEN. Switch this + # to `npm i ai-kit` once it does. + - name: Install ai-kit if: github.event_name != 'pull_request' run: | mkdir -p "$RUNNER_TEMP/air" && cd "$RUNNER_TEMP/air" npm init -y >/dev/null - npm i --no-audit --no-fund github:maonakamoto/ai-ration#v0.2.1 >/dev/null + npm i --no-audit --no-fund github:maonakamoto/ai-kit#v0.3.0 >/dev/null - name: Audit the fleet's pins if: github.event_name != 'pull_request' env: - AI_RATION_FROM: ${{ runner.temp }}/air + AI_KIT_FROM: ${{ runner.temp }}/air GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Absent keys are handled, not fatal: the vendor is reported UNCHECKED # rather than clean. "I could not look" is not "nothing is wrong". diff --git a/SHARED.md b/SHARED.md index c4f43ae..f9b29bd 100644 --- a/SHARED.md +++ b/SHARED.md @@ -23,14 +23,22 @@ the inventory underneath it is **generated**, and the number it produces is a | Package | Install | Replaces | |---|---|---| | [`ai-forms`](https://github.com/maonakamoto/ai-forms) | `npm i github:maonakamoto/ai-forms#v0.1.0` | per-app "fill this form from prose" + conversational refinement. Headless — ships **no markup**, so each app keeps its own styling. | -| [`ai-ration`](https://github.com/maonakamoto/ai-ration) | `npm i github:maonakamoto/ai-ration#v0.2.0` | LLM free-tier survival: multi-vendor fallback chain, the three kinds of 429, per-user fair-share rationing, `modelCost()` so a fallback can never silently bill. | +| [`ai-kit`](https://github.com/maonakamoto/ai-kit) | `npm i github:maonakamoto/ai-kit#v0.3.0` | **the AI layer, in one install** — which model to call, whether the vendor still lists it, the three kinds of 429, fair-share of a free tier, and (re-exported) `ai-forms`. Renamed from `ai-ration` 2026-08-26: the name described one of five modules, and the package had one adopter while five repos that skipped it went down together to a retired model id. | | [`threadkit`](https://github.com/maonakamoto/threadkit) | `npm i threadkit` | multi-participant message threads where *permission is participation*, not a role or an ownership column. Headless pure functions, so "who may read this" is unit-testable instead of buried in a `WHERE` clause. AI participants obey the same visibility rules. **ESM-only.** | | [`limitkit`](https://github.com/maonakamoto/limitkit) | `npm i github:maonakamoto/limitkit#v0.1.0` | the fleet's **12 hand-rolled rate limiters** (this file's own "next extraction" row). Sliding/fixed windows over an injectable two-method `Store`; **bounded** memory default (the unbounded-Map leak is impossible by construction); standard `X-RateLimit-*` + `Retry-After` headers — what orangecat's ADR-0002 specified seven months before anything enforced it; `clientIp()`. Refusals count nothing, so a hammered key recovers. Ships no middleware and **no limit values** — how many attempts a route allows is app semantics, asserted locally. | **Adopted:** `ai-forms` — fleetcrown, evig, aoz-housing, surf-your-life. -`ai-ration` — fleetcrown, **and this repo** (`model-pin-audit.mjs` calls +`ai-kit` — fleetcrown, **and this repo** (`model-pin-audit.mjs` calls `checkCatalog`; the audit needed exactly the vendor query the package owns, so -writing a second one here would have been this file's own sin). `threadkit` — **nobody yet**. +writing a second one here would have been this file's own sin). + +**On merging packages.** `ai-kit` absorbed `ai-ration` and re-exports +`ai-forms`, because to an app "which model do we call", "AI chat" and "AI form +fill" are ONE feature — AOZ adopted the form half, hand-rolled the rest, and was +taken down by the half it skipped. `threadkit` and `limitkit` are deliberately +NOT merged in: neither is about AI, and an app throttling its login form should +not install a model catalogue to do it. Merge by what a consumer needs together, +never by "these are all shared utilities". `threadkit` — **nobody yet**. `limitkit` — fleetcrown (proving consumer; its old limiter had the unbounded Map). **Next adopter should be orangecat** — it closes ADR-0002 by making its Upstash client a 12-line `Store` adapter and deleting three of its four diff --git a/scripts/ci/model-pin-audit.mjs b/scripts/ci/model-pin-audit.mjs index d80e4bc..296cc0d 100755 --- a/scripts/ci/model-pin-audit.mjs +++ b/scripts/ci/model-pin-audit.mjs @@ -27,8 +27,8 @@ * runs in repos somebody still touches, and the repos that rot quietly are * precisely the ones nobody touches. This one needs no adoption at all. * - * WHY IT REUSES ai-ration - * ----------------------- + * WHY IT REUSES ai-kit + * -------------------- * `checkCatalog` already answers this, already distinguishes the three states * that matter, and already carries the scars — its own docstring records four * of nine default pins gone and a consumer silently failing for eight days. @@ -46,7 +46,7 @@ * ---------------------- * That a listed model WORKS. Existence is cheap; capability is not. A model * can be listed and still refuse tool calls — of nine free models probed for - * ai-ration's default chain, five answered only via a text protocol. If the + * ai-kit's default chain, five answered only via a text protocol. If the * surface is a tool loop, probe with a real tool call before pinning. This * audit catches the retirement, not the mismatch. * @@ -61,7 +61,7 @@ * * Env: GH_OWNER (default maonakamoto), GH_LIMIT (default 100), * FLEET_ROOT (default ~/dev, --local only), - * AI_RATION_FROM (path to a repo that installs ai-ration), + * AI_KIT_FROM (path to a repo that installs ai-kit), * GROQ_API_KEY / OPENROUTER_API_KEY (to read the catalogues). */ import { execFile } from "node:child_process"; @@ -410,17 +410,23 @@ function localFiles(repo) { })); } -// ── The catalogue, via ai-ration ───────────────────────────────────────────── +// ── The catalogue, via ai-kit ───────────────────────────────────────────── -function loadAiRation() { +function loadAiKit() { const candidates = [ + process.env.AI_KIT_FROM, + // Renamed from ai-ration in v0.3.0. The old paths stay in the list so a + // checkout that has not been renamed still resolves — a rename should not + // turn a working audit into a silent exit 2. process.env.AI_RATION_FROM, join(homedir(), "dev", "fleetcrown"), + join(homedir(), "dev", "ai-kit"), join(homedir(), "dev", "ai-ration"), ].filter(Boolean); for (const root of candidates) { for (const entry of [ + join(root, "node_modules", "ai-kit", "dist", "index.js"), join(root, "node_modules", "ai-ration", "dist", "index.js"), join(root, "dist", "index.js"), ]) { @@ -428,8 +434,8 @@ function loadAiRation() { } } console.error( - "✗ ai-ration not found. Set AI_RATION_FROM=/path/to/a/repo that installs it,\n" + - " or build it once: (cd ~/dev/ai-ration && npm i && npm run build)", + "\u2717 ai-kit not found. Set AI_KIT_FROM=/path/to/a/repo that installs it,\n" + + " or build it once: (cd ~/dev/ai-kit && npm i && npm run build)", ); process.exit(2); } @@ -525,7 +531,7 @@ function report(judged) { if (gone.length) { lines.push(""); lines.push("A pin is a scheduled outage. The durable fix is a chain across VENDORS —"); - lines.push("see SHARED.md → ai-ration. Repinning buys time until the next retirement."); + lines.push("see SHARED.md → ai-kit. Repinning buys time until the next retirement."); } return lines.join("\n"); @@ -534,7 +540,7 @@ function report(judged) { // ── Main ───────────────────────────────────────────────────────────────────── export async function main() { - const { checkCatalog } = await loadAiRation(); + const { checkCatalog } = await loadAiKit(); const repos = LOCAL ? localRepos() : await remoteRepos(); const findings = [];