Repository navigation
Optional change classifier on decision models (Clef, Jev), off by default - #77
Merged
Merged
Conversation
…ault
A decision model labels each new version of an own item typo, minor or
major before publish; a republish of the same words is refresh, decided
locally from content_hash with no model call. The Version N dialog shows
the label as an editable chip and, with change_category_in_note, writes it
into the note where the author sees it. Failures are a null category,
never an error.
- decide() in src/ai/provider.ts: the System One request shape, Clef on
the Workers AI binding, Jev over HTTP ({base_url}/systemone, bearer
TYPESAFE_API_KEY); responses parsed against Cloudflare's published
output schema and TypeSafe's API reference, defensively.
- models.json: kind "decision" (Clef, Clef-flash, jev-latest) and a
systemone provider; decision models only for ai_model_classify, never
for text functions, enforced in settings and complete().
- POST /items/{id}/classify (owner:draft), change_category on publish,
change_class on the owner's versions; migration 0030, studio-private.
- Nothing on the wire (blygger#19 / TN-1, blygger#77): asserted by test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vgururao
added a commit
that referenced
this pull request
Oct 20, 2026
# Conflicts: # src/read-api.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Optional change classifier on decision models (Clef, Jev)
Off by default. Nothing here does anything until Settings → AI → change classifier names a decision model. With no model chosen, no route calls a model, the publish dialog behaves as it does today, and no row is written.
Why
Venkat's post (f/4dkqht955z9aq1ekr7bekex980) asks every blyg to "use a Jev-class super cheap classifier to post category-based changelogs like 'minor', 'major', 'typo'", so readers can present feeds less noisily.
What it does
refresh,typo,minor,major.refreshis a republish whosecontent_hashequals the published one. It is decided locally with no model call. The model gets onechoicequestion over the other three, with both versions and the word-level change asstate. The instruction text isCLASSIFY_QUESTIONinsrc/change-class.ts. I left outaddition: whether text was added doesn't tell a reader how much to care.POST /items/{id}/classify(scopeowner:draft, likenote-draft) and shows the answer as a chip the author can change or clear. When the classifier is on, the dialog opens for every later version.change_category_in_note, on by default, matters only while the classifier is on):minor.minor: <note>.generatedonly if it is exactly the machine's text. That means a drafted note led by the classifier's own category, or the classifier's bare word on an empty note. If the author overrides the category, or a category is prefixed to the author's own words, the note is not generated. The UI now keys this onnoteIsGenerated()(src/change-category.ts) instead of comparing against the drafted text alone. With the classifier off, the behaviour is unchanged.409only when the classifier is off, for a first version, or when the prior version is a withdrawal.200 { category: null, reason }for a missing binding or key, a provider error, the daily AI budget, or an answer that doesn't parse.admitAilike other AI calls.refreshcosts nothing.migrations/0030_change_class.sqladdschange_class, one row per own version, withcategory,model,probability,classified_at,overriddenandcontent_hash.change_categoryonPOST /items/{id}/publish. Omitting it means the classification stands;nullclears it; a different value marks it overridden.change_classon each version inGET /items/{id}, and as a badge in the editor's history.items.Providers
decide(env, modelId, state, questions)insrc/ai/provider.tsis separate fromcomplete()/generate(). It returns{ answers: { [q]: { choice, probability } }, model }.bindingName(spec)). The body's requiredmodelselector (clef/clef-flash) comes from the model'sparams.systemoneprovider api:POST {base_url}/systemone, default basehttps://api.typesafe.ai/v1, with the key as a bearer token from secretTYPESAFE_API_KEY.models.json,"kind": "decision"(and everysystemonemodel) marks a decision model:GET /api/ai/modelsreturnskind: "decision"on those models.PATCH /settingsrefuses a mismatch (400), andcomplete()refuses a decision model.scripts/models-manifest.ts) checks the new api and kind.Nothing new on the wire
pageslugs (already legal), and why ids stay opaque #19 / TN-1 refuses a version-significance marker on the wire.blygger-spec/docs/v0.4-plan.md§12.5) makesignyra reader-side queue derived fromcontent_hash, not a flag.So this PR adds no member to item documents,
changelog[]entries,feed.xmlorblyg.json, andsrc/protocol.tsis untouched. The category reaches readers only as words in the note the author confirmed.test/change-class.test.tsasserts that a classified publish produces the same document key set (and the same changelog-entry keys) as an unclassified one. It also checks that the item document, feed and manifest carry no category member.Verification status of the two APIs
{ type: "choice", choice, probabilities, confidence }.{ result: … }envelope.choiceoutside the asked options and falls back to the argmax of the asked options.confidencewhen the chosen option has no probability.POST https://api.typesafe.ai/v1/systemone,Authorization: Bearer, modeljev-latest. This matches Cloudflare's schema. Not exercised against the live API: there is no key in this environment.Reader-side follow-up, for Venkat
climbFor()insrc/feeds/pace-layered.tsis unchanged; it gets only a comment pointing here. A reader-side version would classify imported versions locally, from the two versions this node already holds, and feed that into the climb. That stays consistent with #77: the reader derives significance and the wire never marks it. That is a decision to make with Venkat, not part of this PR.Tests
npm run typecheck(both configs): clean.npm test: 143 files, 1768 passed, 5 skipped. This includes the newtest/change-class.test.ts(22 tests):refreshwith no model call;classifyChangeunderowner:draft.npm run test:ui: 12 files, 97 passed.npm run test:e2efor the touched areas:e2e/change-class.spec.ts(new: chip, override, prefix, no-answer path), pluse2e/studio.spec.tsande2e/setup.spec.ts. 28 passed, desktop and mobile.npm run test:mutations(oracle) andnpm run test:polling-cache:mutationsboth pass, with every mutation caught. The new migration puts every suite inci-scope, but the auth-security mutation suites (about 13 min) were not run locally. Nothing in auth or OAuth changed.🤖 Generated with Claude Code