sanity-plugin-jev-fields 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,30 +2,43 @@
2
2
 
3
3
  > Beta: expect `0.x` releases to change until the stored value shapes are frozen at `1.0.0`.
4
4
 
5
- Jev signals for Sanity Studio: questions about a field's content, answered by
6
- [TypeSafe's Jev](https://typesafe.ai) decision model through
7
- [Vercel AI Gateway](https://vercel.com/ai-gateway) as editors write. Each signal is a chip under
8
- the field it judges; click it for the details.
5
+ Editorial checks that run while editors write. Ask a question about a field ("Is this easy to
6
+ read?", "How well are the claims backed up?") and the answer shows up under the field, is stored
7
+ next to it for you to query, and can warn or block publishing.
8
+
9
+ The questions are answered by [TypeSafe's Jev](https://typesafe.ai), a decision model that
10
+ answers a question with a probability instead of writing text, through
11
+ [Vercel AI Gateway](https://vercel.com/ai-gateway). Each question shows as a chip under the field
12
+ it judges; click it for the details. There are three kinds:
9
13
 
10
14
  - `noul`: a yes/no question, answered with the probability that the answer is yes
11
15
  - `score`: a position on an ordered scale you define
12
16
  - `choice`: one option from a named set, with a probability for each option
13
17
 
14
- Signals re-evaluate shortly after the field is edited. Opening a document never writes to it.
18
+ Questions are re-evaluated shortly after the field is edited. Opening a document never writes to it.
15
19
 
16
- ![A Body field in Sanity Studio with three signal chips under it: Readable 90%, Evidence 1.5 of 3 and Tone Casual. The Evidence details are open: a four-step bar from None to Cited, filled to Anecdotal, and the hint "To move up: add a source or figure."](https://raw.githubusercontent.com/frederikvonsperling/sanity-plugin-jev-fields/main/docs/images/signals.png)
20
+ <img src="https://raw.githubusercontent.com/frederikvonsperling/sanity-plugin-jev-fields/main/docs/images/readable.png" width="612" alt="A Body field in Sanity Studio with three question chips under it: Readable 90%, Evidence 1.5/3 and Tone Casual. The Readable details are open: a High badge, 90% likely to read easily, a nearly full green bar, and the hint &quot;Short sentences, plain words, clear structure&quot;.">
17
21
 
18
22
  ## Install
19
23
 
24
+ ```sh
25
+ npm install sanity-plugin-jev-fields
26
+ ```
27
+
20
28
  ```sh
21
29
  pnpm add sanity-plugin-jev-fields
22
30
  ```
23
31
 
24
- Requires Sanity Studio 6.
32
+ ```sh
33
+ yarn add sanity-plugin-jev-fields
34
+ ```
35
+
36
+ Requires Sanity Studio 6. Its peer dependencies, `react` and `react-dom` 19.2 or later and
37
+ `styled-components` 6.1 or later, come with any Studio 6 project.
25
38
 
26
39
  ## Usage
27
40
 
28
- Add the plugin, and wrap your schema types with `withJevAnswers` so every signal gets a field to
41
+ Add the plugin, and wrap your schema types with `withJevAnswers` so every question gets a field to
29
42
  store its answer in:
30
43
 
31
44
  ```ts
@@ -40,7 +53,7 @@ export default defineConfig({
40
53
  })
41
54
  ```
42
55
 
43
- Then attach signals to any field with `options.jev`:
56
+ Then attach questions to any field with `options.jev`:
44
57
 
45
58
  ```ts
46
59
  import {defineArrayMember, defineField} from 'sanity'
@@ -70,41 +83,54 @@ defineField({
70
83
  }),
71
84
  tone: choice({
72
85
  instructions: 'What is the tone of this article?',
73
- criteria: {formal: 'Professional and reserved', casual: 'Conversational and relaxed'},
86
+ criteria: {
87
+ formal: 'Professional and reserved',
88
+ casual: 'Conversational and relaxed',
89
+ playful: 'Light-hearted and witty',
90
+ },
74
91
  }),
75
92
  },
76
93
  },
77
94
  })
78
95
  ```
79
96
 
80
- A signal reads only the field it is attached to. Portable Text, slugs and nested objects are
97
+ A question reads only the field it is attached to. Portable Text, slugs and nested objects are
81
98
  flattened to plain text. Each key (`readable`, `evidence`, `tone`) becomes the name of a field
82
99
  next to it that stores the answer; `withJevAnswers` adds those fields and stops with an error if a
83
- name is already taken.
100
+ name is already taken. A `score` takes 2 to 10 criteria and a `choice` 2 to 255 options; see
101
+ [Question options](#question-options) for the rest.
102
+
103
+ <img src="https://raw.githubusercontent.com/frederikvonsperling/sanity-plugin-jev-fields/main/docs/images/tone.png" width="612" alt="A Title field with a Tone Casual chip under it. The Tone details are open: bars for Formal 3%, Casual 97% and Playful 0%, the meaning &quot;Conversational and relaxed&quot;, and the note &quot;Out of date: the field changed since this was evaluated.&quot;">
104
+
105
+ When a field changes, its answers are marked out of date until they are evaluated again.
84
106
 
85
107
  ## Rules
86
108
 
87
- Give a signal a `warn` or `require` rule to act on its answer: `warn` shows a warning on the
109
+ Give a question a `warn` or `require` rule to act on its answer: `warn` shows a warning on the
88
110
  attached field, `require` an error that blocks publishing.
89
111
 
90
112
  ```ts
91
113
  options: {
92
114
  jev: {
93
- readable: noul({...signal, warn: {atLeast: 0.6}}), // probability, 0–1
94
- evidence: score({...signal, require: {atLeast: 2}}), // position on the scale
95
- tone: choice({...signal, warn: {oneOf: ['formal', 'casual']}}),
115
+ readable: noul({...question, warn: {atLeast: 0.6}}), // probability, 0–1
116
+ evidence: score({...question, require: {atLeast: 2}}), // position on the scale
117
+ risk: score({...question, colors: 'reverse', warn: {atMost: 1}}), // lower is better
118
+ tone: choice({...question, warn: {oneOf: ['formal', 'casual']}}),
96
119
  },
97
120
  }
98
121
  ```
99
122
 
100
- Rules judge the stored answer, so they say nothing until a signal has been evaluated, and an
123
+ `noul` and `score` rules take `atLeast`, `atMost` or both. `choice` rules take `oneOf`, the options
124
+ the answer must be one of.
125
+
126
+ Rules judge the stored answer, so they say nothing until a question has been evaluated, and an
101
127
  answer that is out of date is still judged as it is.
102
128
 
103
129
  ## What Jev is good at
104
130
 
105
131
  Jev judges meaning in text: tone, clarity, whether claims are backed up. It reads only text, so
106
132
  images and other media in a field are ignored. It is not built for counting or arithmetic: ask
107
- "Is this under 150 words?" in a validation rule instead. Test signals on content in your own
133
+ "Is this under 150 words?" in a validation rule instead. Test questions on content in your own
108
134
  languages before relying on them. A field's text may be up to about 32k tokens; the Studio warns
109
135
  when a field gets close.
110
136
 
@@ -113,36 +139,51 @@ when a field gets close.
113
139
  Open the **Jev** tool in the Studio, click **Set key** and paste an AI Gateway API key. The tool
114
140
  shows whether a key is set, when it last changed, and can test the connection.
115
141
 
116
- The key is stored in the document `secrets.jev` in your dataset. Unauthenticated queries can't
117
- read it, but anyone who can read the dataset can: every Studio user, and every API token with read
118
- access, such as a frontend's read or preview token. It is also included in dataset exports and
119
- backups. Use a dedicated key with a spend limit.
142
+ > **Anyone who can read the dataset can read the key.** It is stored in the document
143
+ > `secrets.jev`. Unauthenticated queries can't read it, but every Studio user can, and so can
144
+ > every API token with read access, such as a frontend's read or preview token. It is also
145
+ > included in dataset exports and backups. Use a dedicated key with a spend limit.
146
+
147
+ You can also pass the key as the plugin's `apiKey` option, but then it is bundled into the
148
+ Studio's JavaScript, where anyone who can load the Studio can read it.
120
149
 
121
150
  If editors and tokens must never see the key, send requests through your own server instead:
122
151
 
123
152
  ```ts
124
153
  jev({
125
154
  transport: (request, {signal}) =>
126
- fetch('/api/jev', {method: 'POST', body: JSON.stringify(request), signal}),
155
+ fetch('/api/jev', {
156
+ method: 'POST',
157
+ headers: {'Content-Type': 'application/json'},
158
+ body: JSON.stringify(request),
159
+ signal,
160
+ }),
127
161
  })
128
162
  ```
129
163
 
130
164
  Your endpoint forwards the body unchanged to `https://ai-gateway.vercel.sh/v1/evaluate` with an
131
- `Authorization: Bearer <key>` header, and returns the Gateway's response.
165
+ `Authorization: Bearer <key>` header and a `Content-Type: application/json` header, and returns
166
+ the Gateway's response with its status.
167
+
168
+ When more than one is set, `transport` wins over `apiKey`, and `apiKey` over the stored key.
132
169
 
133
170
  ## Stored values
134
171
 
135
172
  ```groq
136
173
  *[_type == "article"]{
137
174
  title,
138
- "readable": readable.probability, // 0–1
139
- "evidence": evidence{score, max, label}, // score runs from 0 to max
140
- "tone": tone.choice
175
+ // noul: probability that the answer is yes, 0–1
176
+ "readable": readable.probability,
177
+ // score: score runs from 0 to max; label is the nearest criterion; confidence is 0–1
178
+ "evidence": evidence{score, max, label, confidence},
179
+ // choice: the chosen option, confidence 0–1, and a probability for every option
180
+ "tone": tone{choice, confidence, probabilities[]{option, probability}}
141
181
  }
142
182
  ```
143
183
 
144
184
  Every value also stores `evaluatedAt`, `model` (e.g. `typesafe-ai/jev`) and a `sourceHash` the
145
- Studio uses to show when a value is out of date.
185
+ Studio uses to show when a value is out of date. The types `NoulValue`, `ScoreValue` and
186
+ `ChoiceValue` describe the full shapes.
146
187
 
147
188
  ## Translations
148
189
 
@@ -163,22 +204,50 @@ export const jevNorwegian = defineLocaleResourceBundle({
163
204
  Rule messages and config problems stay in English: Sanity's validation has no translation hook,
164
205
  and config problems are meant for schema authors.
165
206
 
166
- ## Development
207
+ ## Options reference
167
208
 
168
- ```sh
169
- pnpm install
170
- pnpm test
171
- pnpm build
172
- ```
209
+ ### Plugin options
210
+
211
+ Passed to `jev({...})`. All are optional.
212
+
213
+ | Option | Default | Description |
214
+ | ------------ | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
215
+ | `transport` | | `(request, {signal}) => Promise<Response>`. Sends requests yourself, e.g. through your own server. See [API key](#api-key). |
216
+ | `apiKey` | | AI Gateway key. Bundled into the Studio's JavaScript; prefer the Jev tool or `transport`. |
217
+ | `endpoint` | `https://ai-gateway.vercel.sh/v1/evaluate` | Where requests go. Ignored when `transport` is set. |
218
+ | `model` | `typesafe-ai/jev` | Decision model to call. |
219
+ | `debounceMs` | `500` | Delay after the last edit before re-evaluating, in milliseconds. |
220
+ | `tool` | `true` | Adds the Jev tool for setting, testing and removing the stored key. |
221
+ | `tags` | `['feature:jev-fields']` | AI Gateway reporting tags, for cost attribution. Each request also gets a tag for its question, e.g. `jev.noul:article.readable`. |
222
+
223
+ ### Question options
224
+
225
+ Every question takes:
226
+
227
+ | Option | Description |
228
+ | -------------- | ----------------------------------------------------------------------------------- |
229
+ | `instructions` | Required. The question Jev answers about the attached field. |
230
+ | `title` | Chip and detail heading. Defaults to the question's key: `readable` → "Readable". |
231
+ | `warn` | Shows a warning on the field when the answer breaks this rule. See [Rules](#rules). |
232
+ | `require` | Blocks publishing when the answer breaks this rule. |
233
+
234
+ And, per kind:
235
+
236
+ | Kind | Option | Description |
237
+ | -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
238
+ | `noul` | `true` | Required. What a yes means. Shown when the probability is 50% or higher. |
239
+ | `noul` | `false` | Required. What a no means. Shown when the probability is below 50%. |
240
+ | `noul` | `label` | Short phrase after the percentage, e.g. "likely to read easily". |
241
+ | `score` | `criteria` | Required. 2 to 10 criteria, lowest first. Text before a colon is the short label. |
242
+ | `score` | `colors` | `'traffic'` (default): red at the bottom, green at the top. `'reverse'`: green at the bottom, for scales where lower is better. `'neutral'`: one colour throughout. |
243
+ | `choice` | `criteria` | Required. 2 to 255 options, as option name → what it means. |
244
+
245
+ Rules on a `noul` take probabilities (0–1), rules on a `score` take positions on the scale (0 to
246
+ the number of criteria minus one), and rules on a `choice` take option names.
173
247
 
174
- `pnpm record-fixtures` re-records the real Gateway responses in `src/__fixtures__` (needs
175
- `AI_GATEWAY_API_KEY` in `.env`). [CONTEXT.md](./CONTEXT.md) defines the vocabulary and
176
- [docs/adr](./docs/adr) records the main decisions.
248
+ ## Contributing
177
249
 
178
- To release a change, add a changeset to its PR with `pnpm changeset`. The release workflow keeps a
179
- "Version packages" PR up to date; merging it stages the new version on npm (trusted publishing,
180
- no token) and creates the GitHub release. A maintainer then approves the staged version on
181
- npmjs.com, with 2FA, to publish it.
250
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
182
251
 
183
252
  ## License
184
253
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { SchemaTypeDefinition } from "sanity";
2
2
  import "react";
3
3
  /** @public */
4
- type JevQuestion = {
4
+ type GatewayQuestion = {
5
5
  type: 'boolean';
6
6
  instructions: string;
7
7
  criteria: {
@@ -18,7 +18,7 @@ type JevQuestion = {
18
18
  criteria: Record<string, string>;
19
19
  };
20
20
  /** @public */
21
- type JevAnswer = {
21
+ type GatewayAnswer = {
22
22
  type: 'boolean';
23
23
  probability: number;
24
24
  } | {
@@ -41,7 +41,7 @@ interface JevRequest {
41
41
  model: string;
42
42
  state: string | Record<string, string>;
43
43
  questions: {
44
- q: JevQuestion;
44
+ q: GatewayQuestion;
45
45
  };
46
46
  providerOptions: {
47
47
  gateway: {
@@ -78,12 +78,12 @@ interface JevPluginConfig {
78
78
  tool?: boolean;
79
79
  /**
80
80
  * AI Gateway reporting tags, for cost attribution. Defaults to `['feature:jev-fields']`.
81
- * Each request also gets a per-signal tag, e.g. `jev.noul:article.readable`.
81
+ * Each request also gets a per-question tag, e.g. `jev.noul:article.readable`.
82
82
  */
83
83
  tags?: string[];
84
84
  }
85
85
  /** @public */
86
- interface ChoiceSignal extends SignalBase {
86
+ interface ChoiceQuestion extends QuestionBase {
87
87
  type: 'choice';
88
88
  /** Two to 255 options: option name → what it means. */
89
89
  criteria: Record<string, string>;
@@ -97,7 +97,7 @@ interface ChoiceRule {
97
97
  oneOf: string[];
98
98
  }
99
99
  /** One option from a named set. @public */
100
- export declare const choice: (signal: Omit<ChoiceSignal, "type">) => ChoiceSignal;
100
+ export declare const choice: (question: Omit<ChoiceQuestion, "type">) => ChoiceQuestion;
101
101
  interface ChoiceProbability {
102
102
  _key: string;
103
103
  _type?: 'jev.choiceProbability';
@@ -113,7 +113,7 @@ interface ChoiceValue extends EvaluatedValue {
113
113
  probabilities?: ChoiceProbability[];
114
114
  }
115
115
  /** @public */
116
- interface NoulSignal extends SignalBase {
116
+ interface NoulQuestion extends QuestionBase {
117
117
  type: 'noul';
118
118
  /** What a "yes" means. Shown when the probability is 50% or higher. */
119
119
  true: string;
@@ -126,8 +126,8 @@ interface NoulSignal extends SignalBase {
126
126
  /** Block publishing when the probability is outside these bounds (0–1). */
127
127
  require?: RangeRule;
128
128
  }
129
- /** A yes/no signal: the chip shows the probability that the answer is yes. @public */
130
- export declare const noul: (signal: Omit<NoulSignal, "type">) => NoulSignal;
129
+ /** A yes/no question: the chip shows the probability that the answer is yes. @public */
130
+ export declare const noul: (question: Omit<NoulQuestion, "type">) => NoulQuestion;
131
131
  /** @public */
132
132
  interface NoulValue extends EvaluatedValue {
133
133
  _type?: 'jev.noul';
@@ -135,7 +135,7 @@ interface NoulValue extends EvaluatedValue {
135
135
  probability?: number;
136
136
  }
137
137
  /** @public */
138
- interface ScoreSignal extends SignalBase {
138
+ interface ScoreQuestion extends QuestionBase {
139
139
  type: 'score';
140
140
  /**
141
141
  * Two to ten criteria, lowest first. Text before a colon becomes the short label,
@@ -154,7 +154,7 @@ interface ScoreSignal extends SignalBase {
154
154
  require?: RangeRule;
155
155
  }
156
156
  /** A position on an ordered scale. @public */
157
- export declare const score: (signal: Omit<ScoreSignal, "type">) => ScoreSignal;
157
+ export declare const score: (question: Omit<ScoreQuestion, "type">) => ScoreQuestion;
158
158
  /** @public */
159
159
  interface ScoreValue extends EvaluatedValue {
160
160
  _type?: 'jev.score';
@@ -167,8 +167,8 @@ interface ScoreValue extends EvaluatedValue {
167
167
  /** The model's confidence in this score (0–1). */
168
168
  confidence?: number;
169
169
  }
170
- interface SignalBase {
171
- /** Chip and detail heading. Defaults to the signal's key, e.g. `readable` → "Readable". */
170
+ interface QuestionBase {
171
+ /** Chip and detail heading. Defaults to the question's key, e.g. `readable` → "Readable". */
172
172
  title?: string;
173
173
  /** The question Jev answers about the attached field. */
174
174
  instructions: string;
@@ -186,11 +186,11 @@ interface RangeRule {
186
186
  atMost?: number;
187
187
  }
188
188
  /** @public */
189
- type JevSignal = NoulSignal | ScoreSignal | ChoiceSignal;
190
- /** Signals attached to a field, keyed by the name of the field that stores each answer. */
191
- type JevSignals = Record<string, JevSignal>;
189
+ type JevQuestion = NoulQuestion | ScoreQuestion | ChoiceQuestion;
190
+ /** Questions attached to a field, keyed by the name of the field that stores each answer. */
191
+ type JevQuestions = Record<string, JevQuestion>;
192
192
  /**
193
- * Adds a field that stores each signal's answer next to every field with `options.jev`.
193
+ * Adds a field that stores each question's answer next to every field with `options.jev`.
194
194
  * Plugins can't see the Studio's own schema types, so wrap them in `sanity.config`:
195
195
  *
196
196
  * ```ts
@@ -209,7 +209,7 @@ export declare const JEV_NAMESPACE: "jev";
209
209
  declare const resources: {
210
210
  readonly 'strip.label': "Jev";
211
211
  readonly 'strip.set-up': "Set up Jev";
212
- readonly 'strip.evaluate-all': "Evaluate all signals now";
212
+ readonly 'strip.evaluate-all': "Evaluate all questions now";
213
213
  readonly 'chip.empty': "–";
214
214
  readonly 'chip.error': "Error";
215
215
  readonly 'chip.out-of-date': "Out of date";
@@ -239,7 +239,7 @@ declare const resources: {
239
239
  readonly 'key-dialog.saving': "Saving…";
240
240
  readonly 'key-dialog.save-failed': "Could not save the key: {{error}}";
241
241
  readonly 'tool.title': "Jev";
242
- readonly 'tool.intro': "Jev signals are answered by TypeSafe’s Jev model through Vercel AI Gateway.";
242
+ readonly 'tool.intro': "Jev questions are answered by TypeSafe’s Jev model through Vercel AI Gateway.";
243
243
  readonly 'tool.key.heading': "AI Gateway API key";
244
244
  readonly 'tool.key.status.config': "From plugin config";
245
245
  readonly 'tool.key.status.checking': "Checking";
@@ -250,11 +250,11 @@ declare const resources: {
250
250
  readonly 'tool.key.ends-with': "Key ending in <Code>{{last4}}</Code>";
251
251
  readonly 'tool.key.changed': "Last changed {{date}}";
252
252
  readonly 'tool.key.checking': "Checking for a stored key…";
253
- readonly 'tool.key.none': "No key is stored, so Jev signals cannot evaluate yet.";
253
+ readonly 'tool.key.none': "No key is stored, so Jev questions can’t be evaluated yet.";
254
254
  readonly 'tool.key.set': "Set key";
255
255
  readonly 'tool.key.change': "Change key";
256
256
  readonly 'tool.key.remove': "Remove key";
257
- readonly 'tool.key.remove-confirm': "Remove the key? Jev signals stop evaluating until a new one is set.";
257
+ readonly 'tool.key.remove-confirm': "Remove the key? Jev questions stop being evaluated until a new one is set.";
258
258
  readonly 'tool.key.remove-cancel': "Cancel";
259
259
  readonly 'tool.key.remove-confirm-button': "Remove";
260
260
  readonly 'tool.key.remove-failed': "Could not remove the key: {{error}}";
@@ -268,16 +268,16 @@ declare const resources: {
268
268
  type JevTranslationKey = keyof typeof resources;
269
269
  declare module 'sanity' {
270
270
  interface BaseSchemaTypeOptions {
271
- /** Jev signals shown on this field, keyed by the name of the field storing each answer. */
272
- jev?: JevSignals;
271
+ /** Jev questions shown on this field, keyed by the name of the field storing each answer. */
272
+ jev?: JevQuestions;
273
273
  }
274
274
  }
275
275
  /**
276
- * Jev signals for Sanity Studio: yes/no, score and choice questions attached to a field with
276
+ * Jev questions for Sanity Studio: yes/no, score and choice questions attached to a field with
277
277
  * `options.jev`, answered by TypeSafe's Jev model through Vercel AI Gateway. Pair it with
278
278
  * `withJevAnswers(schemaTypes)` in `schema.types`.
279
279
  * @public
280
280
  */
281
281
  export declare const jev: import("sanity").Plugin<void | JevPluginConfig>;
282
- export type { ChoiceRule, ChoiceSignal, ChoiceValue, JevAnswer, JevPluginConfig, JevQuestion, JevRequest, JevSignal, JevSignals, JevTranslationKey, JevTransport, NoulSignal, NoulValue, RangeRule, ScoreSignal, ScoreValue };
282
+ export type { ChoiceQuestion, ChoiceRule, ChoiceValue, GatewayAnswer, GatewayQuestion, JevPluginConfig, JevQuestion, JevQuestions, JevRequest, JevTranslationKey, JevTransport, NoulQuestion, NoulValue, RangeRule, ScoreQuestion, ScoreValue };
283
283
  //# sourceMappingURL=index.d.ts.map