@auden.to/protocol 0.1.0-alpha.2

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.
@@ -0,0 +1,750 @@
1
+ /**
2
+ * Runtime validation schemas using Valibot.
3
+ *
4
+ * Schemas are the single source of truth — TypeScript types in types.ts are
5
+ * derived from these via `v.InferOutput<typeof Schema>`.
6
+ *
7
+ * Callers can use the exported `parse*` wrappers (throw ValiError on invalid
8
+ * input) or call `v.safeParse(Schema, data)` directly for non-throwing checks.
9
+ */
10
+ import * as v from 'valibot';
11
+ // ---------------------------------------------------------------------------
12
+ // Core enum schemas
13
+ // ---------------------------------------------------------------------------
14
+ export const VerdictSchema = v.picklist(['aligned', 'unguided', 'misaligned']);
15
+ export const GuidingTierSchema = v.picklist(['pattern_match', 'cached', 'llm', 'batch']);
16
+ export const PlatformKindSchema = v.picklist([
17
+ 'claude_code',
18
+ 'cursor',
19
+ 'copilot',
20
+ 'windsurf',
21
+ 'other',
22
+ ]);
23
+ export const GuideFileFormatSchema = v.picklist([
24
+ 'agents-md',
25
+ 'agents-dir',
26
+ 'claude-md',
27
+ 'claude-rules',
28
+ 'cursorrules',
29
+ 'cursor-rules',
30
+ 'skill-md',
31
+ ]);
32
+ export const EvalProviderKindSchema = v.picklist([
33
+ 'openrouter',
34
+ 'anthropic',
35
+ 'openai-compatible',
36
+ 'google',
37
+ 'ollama',
38
+ ]);
39
+ const NonEmptyStringSchema = v.pipe(v.string(), v.minLength(1));
40
+ export const PartialEvalProviderSettingsSchema = v.object({
41
+ provider: v.optional(EvalProviderKindSchema),
42
+ model: v.optional(NonEmptyStringSchema),
43
+ apiKey: v.optional(NonEmptyStringSchema),
44
+ baseUrl: v.optional(NonEmptyStringSchema),
45
+ siteUrl: v.optional(NonEmptyStringSchema),
46
+ siteName: v.optional(NonEmptyStringSchema),
47
+ });
48
+ export const EvalProviderConfigSchema = v.object({
49
+ provider: EvalProviderKindSchema,
50
+ model: NonEmptyStringSchema,
51
+ apiKey: v.optional(NonEmptyStringSchema),
52
+ baseUrl: NonEmptyStringSchema,
53
+ siteUrl: v.optional(NonEmptyStringSchema),
54
+ siteName: v.optional(NonEmptyStringSchema),
55
+ });
56
+ // ---------------------------------------------------------------------------
57
+ // Auth schemas
58
+ // ---------------------------------------------------------------------------
59
+ export const ValidateTokenRequestSchema = v.object({
60
+ token: v.string(),
61
+ });
62
+ export const ValidateTokenResponseSchema = v.object({
63
+ valid: v.boolean(),
64
+ userId: v.optional(v.string()),
65
+ expiresAt: v.optional(v.string()),
66
+ });
67
+ export const CliTokenMetadataSchema = v.object({
68
+ token: v.string(),
69
+ userId: v.string(),
70
+ createdAt: v.string(),
71
+ expiresAt: v.string(),
72
+ });
73
+ // ---------------------------------------------------------------------------
74
+ // Guide snapshot schemas
75
+ // ---------------------------------------------------------------------------
76
+ export const GuideSnapshotSchema = v.object({
77
+ id: v.string(),
78
+ title: v.string(),
79
+ content: v.string(),
80
+ enabled: v.boolean(),
81
+ updatedAt: v.string(),
82
+ // origin is `file:{format}:{path}` for imported guides, null/absent
83
+ // otherwise. contentHash is the SHA-256 of the canonical bytes; the CLI uses
84
+ // the pair to round-trip imported files back to disk without re-encoding.
85
+ origin: v.optional(v.nullable(v.string())),
86
+ contentHash: v.optional(v.nullable(v.pipe(v.string(), v.regex(/^[a-f0-9]{64}$/i)))),
87
+ });
88
+ export const GuideBundleResponseSchema = v.object({
89
+ guides: v.array(GuideSnapshotSchema),
90
+ pulledAt: v.string(),
91
+ });
92
+ /** Max serialized size (UTF-8 bytes) of an imported guide file's raw content. */
93
+ export const MAX_RAW_GUIDE_CONTENT_BYTES = 512 * 1024;
94
+ /** Max member files a single skill-md payload may carry. */
95
+ export const MAX_SKILL_MEMBER_FILES = 64;
96
+ const BoundedRawContentSchema = v.pipe(v.string(), v.check((value) => new TextEncoder().encode(value).length <= MAX_RAW_GUIDE_CONTENT_BYTES, `rawContent must be at most ${MAX_RAW_GUIDE_CONTENT_BYTES} bytes`));
97
+ const Sha256HexSchema = v.pipe(v.string(), v.regex(/^[a-f0-9]{64}$/i));
98
+ // Member paths are relative to the skill directory and will be re-created on
99
+ // disk by the export round-trip, so they are constrained to safe relative
100
+ // paths at the schema boundary: forward slashes only, never absolute (POSIX
101
+ // or Windows drive-letter form), no empty / `.` / `..` segments, no control
102
+ // characters, and never the skill's own SKILL.md (the payload itself).
103
+ // Exported so the member-files read path (server response filtering, CLI
104
+ // defense-in-depth before writing to disk) enforces the identical rule.
105
+ export const SkillMemberPathSchema = v.pipe(v.string(), v.minLength(1), v.maxLength(512), v.check((path) => !path.includes('\\'), 'path must use forward slashes'), v.check((path) => !path.startsWith('/') && !/^[a-zA-Z]:/.test(path), 'path must be relative'), v.check((path) => !/[\u0000-\u001f\u007f]/.test(path), 'path must not contain control characters'), v.check((path) => path
106
+ .split('/')
107
+ .every((segment) => segment !== '' && segment !== '.' && segment !== '..'), 'path must not contain empty, "." or ".." segments'), v.check((path) => path.toLowerCase() !== 'skill.md', 'path must not shadow the skill’s own SKILL.md'));
108
+ // A sibling file of a skill's SKILL.md (scripts/, references/, assets/,
109
+ // supporting docs), imported alongside it as a guide_file row.
110
+ export const RawSkillMemberFileSchema = v.object({
111
+ path: SkillMemberPathSchema,
112
+ rawContent: BoundedRawContentSchema,
113
+ lastModified: v.pipe(v.number(), v.integer(), v.minValue(0)),
114
+ content_hash: Sha256HexSchema,
115
+ });
116
+ /** Max skipped-member paths a skill-md payload may report alongside `members`. */
117
+ export const MAX_SKILL_SKIPPED_MEMBER_PATHS = 256;
118
+ // Paths (relative to the skill directory) that discovery found but could not
119
+ // import this run — binary/non-UTF-8, oversized, unreadable, or beyond the
120
+ // member cap. Deliberately looser than SkillMemberPathSchema: these describe
121
+ // what is on the user's disk and are only ever string-matched against stored
122
+ // member paths server-side (never re-created as files), so an odd on-disk
123
+ // filename must not fail the whole payload.
124
+ const SkippedMemberPathSchema = v.pipe(v.string(), v.minLength(1), v.maxLength(512));
125
+ export const RawGuideFilePayloadSchema = v.pipe(v.object({
126
+ path: v.string(),
127
+ format: GuideFileFormatSchema,
128
+ rawContent: BoundedRawContentSchema,
129
+ lastModified: v.pipe(v.number(), v.integer(), v.minValue(0)),
130
+ content_hash: Sha256HexSchema,
131
+ // Only meaningful on `skill-md` payloads. `[]` asserts the skill has no
132
+ // member files (stale synced rows are removed server-side); omitting the
133
+ // field means "no member information" and leaves member rows untouched —
134
+ // `auden rules watch` re-sends a changed SKILL.md without re-reading the
135
+ // directory, so it must not be interpreted as an empty skill.
136
+ members: v.optional(v.pipe(v.array(RawSkillMemberFileSchema), v.maxLength(MAX_SKILL_MEMBER_FILES))),
137
+ // Only alongside `members`: member paths present on disk but not imported
138
+ // this run. The server must NOT treat their absence from `members` as
139
+ // removal — a transiently unreadable or newly-oversized file would
140
+ // otherwise hard-delete its previously synced row.
141
+ skippedMemberPaths: v.optional(v.pipe(v.array(SkippedMemberPathSchema), v.maxLength(MAX_SKILL_SKIPPED_MEMBER_PATHS))),
142
+ }), v.check((payload) => payload.members === undefined || payload.format === 'skill-md', 'members are only supported on skill-md payloads'), v.check((payload) => payload.skippedMemberPaths === undefined || payload.members !== undefined, 'skippedMemberPaths requires members'),
143
+ // Case-insensitive uniqueness: the export round-trip re-creates these files
144
+ // on disk, where the default macOS/Windows filesystems fold case — two
145
+ // members differing only by case would silently overwrite each other.
146
+ v.check((payload) => payload.members === undefined ||
147
+ new Set(payload.members.map((member) => member.path.toLowerCase())).size ===
148
+ payload.members.length, 'member paths must be unique (case-insensitive)'));
149
+ // A skill guide's member file as served by GET /api/v1/guides/:id/member-files
150
+ // — the export round-trip's read side. Lean by design: member bodies are NOT
151
+ // part of the guide bundle (the eval hot path pulls that every session), they
152
+ // are fetched lazily per skill guide at export time.
153
+ export const GuideMemberFileSchema = v.object({
154
+ path: SkillMemberPathSchema,
155
+ content: BoundedRawContentSchema,
156
+ contentHash: Sha256HexSchema,
157
+ });
158
+ export const GuideMemberFilesResponseSchema = v.pipe(v.object({
159
+ memberFiles: v.pipe(v.array(GuideMemberFileSchema), v.maxLength(MAX_SKILL_MEMBER_FILES)),
160
+ }),
161
+ // Mirrors the import payload's rule: on case-folding filesystems two
162
+ // entries differing only by case resolve to one file, and both planning as
163
+ // NEW would let the second silently clobber the first without the
164
+ // OVERWRITE diff prompt. A response that case-collides is malformed.
165
+ v.check((response) => new Set(response.memberFiles.map((file) => file.path.toLowerCase())).size ===
166
+ response.memberFiles.length, 'member file paths must be unique (case-insensitive)'));
167
+ /** Max number of rules accepted in a single `POST /api/rules/import` request. */
168
+ export const MAX_RULES_IMPORT_BATCH = 100;
169
+ // Envelope-only schema: caps request shape/size before the per-item
170
+ // parseRawGuideFilePayload loop validates each rule in detail (preserves
171
+ // index-aware error reporting — see apps/dashboard/app/api/rules/import+api.ts).
172
+ export const RulesImportRequestSchema = v.object({
173
+ rules: v.pipe(v.array(v.unknown()), v.maxLength(MAX_RULES_IMPORT_BATCH)),
174
+ });
175
+ // Deletion signal emitted by `auden rules watch` when a previously imported
176
+ // guide file disappears from disk. The dashboard disables (sets active=false)
177
+ // any row whose origin matches `file:{format}:{path}` — the row is retained so
178
+ // re-creating the file flips it back on, and so historical actions are not
179
+ // orphaned.
180
+ export const RawGuideFileDeletionSchema = v.object({
181
+ path: v.pipe(v.string(), v.minLength(1)),
182
+ format: GuideFileFormatSchema,
183
+ });
184
+ /** Max number of deletions accepted in a single `POST /api/rules/deletions` request. */
185
+ export const MAX_RULE_DELETIONS_BATCH = 500;
186
+ // Envelope-only schema, matching RulesImportRequestSchema's shape: caps
187
+ // request shape/size before the per-item parseRawGuideFileDeletion loop in
188
+ // apps/dashboard/app/api/rules/deletions+api.ts validates each entry in
189
+ // detail (preserves its index-aware error reporting).
190
+ export const RuleDeletionsRequestSchema = v.object({
191
+ deletions: v.pipe(v.array(v.unknown()), v.maxLength(MAX_RULE_DELETIONS_BATCH)),
192
+ });
193
+ export const RuleDeletionsResponseSchema = v.object({
194
+ disabled: v.pipe(v.number(), v.integer(), v.minValue(0)),
195
+ skipped: v.pipe(v.number(), v.integer(), v.minValue(0)),
196
+ });
197
+ // ---------------------------------------------------------------------------
198
+ // Context bundle schemas (docs/plans/context-layer-mcp-inbox-plan.md — Phase 0)
199
+ // ---------------------------------------------------------------------------
200
+ export const BundleKindSchema = v.picklist(['user', 'inbox', 'system']);
201
+ // url-safe slug, unique per user. Lowercase alphanumerics + hyphens, must start
202
+ // with an alphanumeric so it reads cleanly in a URL/path.
203
+ const BundleSlugSchema = v.pipe(v.string(), v.minLength(1), v.maxLength(64), v.regex(/^[a-z0-9][a-z0-9-]*$/, 'slug must be lowercase alphanumeric with hyphens'));
204
+ export const BundleSchema = v.object({
205
+ id: v.string(),
206
+ slug: BundleSlugSchema,
207
+ name: v.string(),
208
+ kind: BundleKindSchema,
209
+ createdAt: v.string(),
210
+ });
211
+ export const BundleListResponseSchema = v.object({
212
+ bundles: v.array(BundleSchema),
213
+ });
214
+ export const CreateBundleRequestSchema = v.object({
215
+ slug: BundleSlugSchema,
216
+ name: v.pipe(v.string(), v.minLength(1), v.maxLength(200)),
217
+ // Defaults to 'user' server-side. 'inbox' is reserved/auto-created; callers
218
+ // should not create it explicitly, but the schema stays permissive and the
219
+ // endpoint enforces reservation.
220
+ kind: v.optional(BundleKindSchema),
221
+ });
222
+ export const CreateBundleResponseSchema = v.object({
223
+ bundle: BundleSchema,
224
+ });
225
+ // ---------------------------------------------------------------------------
226
+ // Context item + inbox schemas (context-layer-mcp-inbox-plan.md — Phase 1a)
227
+ // ---------------------------------------------------------------------------
228
+ // The `type` of an item returned to an agent. A `guide` is a canonical guide
229
+ // row; the rest are guide_files rows (documents, references, assets).
230
+ export const ContextItemTypeSchema = v.picklist([
231
+ 'guide',
232
+ 'document',
233
+ 'reference',
234
+ 'asset',
235
+ ]);
236
+ // The contentType a caller may write through the context layer. `guide` items
237
+ // own their own lifecycle, so pushes/captures only create guide_files rows.
238
+ export const PushContentTypeSchema = v.picklist(['document', 'reference', 'asset']);
239
+ // Inbox capture is text or an image today (image upload lands in Phase 2b).
240
+ export const InboxContentTypeSchema = v.picklist(['document', 'asset']);
241
+ /** Max serialized size (UTF-8 bytes) of a pushed or captured item's content. */
242
+ export const MAX_CONTEXT_CONTENT_BYTES = 256 * 1024;
243
+ const ContextContentSchema = v.pipe(v.string(), v.check((value) => new TextEncoder().encode(value).length <= MAX_CONTEXT_CONTENT_BYTES, `content must be at most ${MAX_CONTEXT_CONTENT_BYTES} bytes`));
244
+ const ContextItemNameSchema = v.pipe(v.string(), v.minLength(1), v.maxLength(200));
245
+ // The item shape returned to agents. `content` is nullable because an asset
246
+ // stores an object key elsewhere (Phase 2b), not inline text.
247
+ export const ContextItemSchema = v.object({
248
+ id: v.string(),
249
+ type: ContextItemTypeSchema,
250
+ name: v.string(),
251
+ contentType: v.string(),
252
+ content: v.nullable(v.string()),
253
+ origin: v.optional(v.nullable(v.string())),
254
+ // Monotonic content version of the underlying doc (guide_files), surfaced so a
255
+ // caller can echo it back as an expected-version precondition. Absent for
256
+ // `guide` items, which carry no version. See doc-version-history-plan.md.
257
+ version: v.optional(v.number()),
258
+ updatedAt: v.string(),
259
+ });
260
+ export const ContextItemsResponseSchema = v.object({
261
+ items: v.array(ContextItemSchema),
262
+ });
263
+ /** Max number of context item ids a push may cite as its transform sources. */
264
+ export const MAX_TRANSFORM_SOURCE_IDS = 100;
265
+ /**
266
+ * The ids of the caller-owned context items a pushed document was produced
267
+ * from (inbox captures, bundle items — any item the caller owns). Shared
268
+ * between the REST contract and the MCP tool's client-side arg validation so
269
+ * the two can't drift.
270
+ */
271
+ export const SourceItemIdsSchema = v.pipe(v.array(v.pipe(v.string(), v.minLength(1))), v.minLength(1), v.maxLength(MAX_TRANSFORM_SOURCE_IDS));
272
+ // Push a canonical item into a bundle. `itemId` present → update in place (the
273
+ // MCP push maps to PUT); absent → create (maps to POST). The dedicated update
274
+ // schema below is what the PUT route validates.
275
+ //
276
+ // `sourceItemIds` (create only, optional) cites the context items the pushed
277
+ // document was produced from. When the caller's own guidance covers the
278
+ // transform (an active guide whose trigger matches, with eval criteria), the
279
+ // server grades the transform against that guidance and records a
280
+ // `context_transform` action with a verdict (context-layer plan, Phase 3c).
281
+ // Auden ships no criteria of its own — no matching guidance means the
282
+ // transform is recorded as 'unguided'.
283
+ export const PushContextRequestSchema = v.object({
284
+ name: ContextItemNameSchema,
285
+ contentType: PushContentTypeSchema,
286
+ content: ContextContentSchema,
287
+ itemId: v.optional(v.string()),
288
+ origin: v.optional(v.nullable(v.string())),
289
+ sourceItemIds: v.optional(SourceItemIdsSchema),
290
+ });
291
+ // The grade the server recorded for a sourced push. `gradingTier` is 'llm'
292
+ // when the caller's guidance was evaluated against the transform, or
293
+ // 'pattern_match' when verdict is 'unguided' (no active guide covered the
294
+ // transform — the signal to write one). When grading infrastructure couldn't
295
+ // run (no LLM configured, over quota, eval failure) NO grade is recorded and
296
+ // the push response omits `transform` entirely — an ungraded round-trip is
297
+ // not a verdict.
298
+ // `gradingTier` is deliberately an open string, not a picklist: a deployed CLI
299
+ // parses this response strictly, and a picklist would turn a future
300
+ // server-side tier (e.g. 'cached') into a hard client error on a successful
301
+ // push.
302
+ export const TransformGradeSchema = v.object({
303
+ actionId: v.string(),
304
+ verdict: VerdictSchema,
305
+ gradingTier: v.string(),
306
+ reasoning: v.nullable(v.string()),
307
+ });
308
+ export const PushContextResponseSchema = v.object({
309
+ item: ContextItemSchema,
310
+ created: v.boolean(),
311
+ transform: v.optional(TransformGradeSchema),
312
+ });
313
+ // Update a canonical item in place — the edit propagates to every bundle that
314
+ // references it. Both fields are optional so a partial update parses; the route
315
+ // rejects a body that changes nothing.
316
+ export const UpdateContextItemRequestSchema = v.object({
317
+ itemId: v.pipe(v.string(), v.minLength(1)),
318
+ name: v.optional(ContextItemNameSchema),
319
+ content: v.optional(ContextContentSchema),
320
+ // Optimistic-concurrency precondition: the version the caller last read (from
321
+ // `ContextItemSchema.version`). When present and it no longer matches the
322
+ // current version, the update is rejected with 409 instead of silently
323
+ // clobbering a concurrent write. Omit it to keep last-write-wins behavior.
324
+ expectedVersion: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
325
+ });
326
+ export const UpdateContextItemResponseSchema = v.object({
327
+ item: ContextItemSchema,
328
+ updated: v.boolean(),
329
+ });
330
+ export const InboxCaptureRequestSchema = v.object({
331
+ contentType: InboxContentTypeSchema,
332
+ content: ContextContentSchema,
333
+ name: v.optional(ContextItemNameSchema),
334
+ source: v.optional(v.string()),
335
+ });
336
+ // An inbox item carries capture provenance on top of the base item shape.
337
+ // `capturedAt` is the underlying row's createdAt, so "last 24h" is a filter on it.
338
+ export const InboxItemSchema = v.object({
339
+ id: v.string(),
340
+ type: ContextItemTypeSchema,
341
+ name: v.string(),
342
+ contentType: v.string(),
343
+ content: v.nullable(v.string()),
344
+ source: v.optional(v.nullable(v.string())),
345
+ // Monotonic content version of the underlying doc, mirroring
346
+ // `ContextItemSchema.version` — an inbox item is a guide_file too, so a caller
347
+ // that edits it via PUT can echo this back as an expected-version precondition.
348
+ version: v.optional(v.number()),
349
+ capturedAt: v.string(),
350
+ updatedAt: v.string(),
351
+ });
352
+ export const InboxCaptureResponseSchema = v.object({
353
+ item: InboxItemSchema,
354
+ });
355
+ export const InboxListResponseSchema = v.object({
356
+ items: v.array(InboxItemSchema),
357
+ });
358
+ /** Max number of ids accepted in a single `POST /api/v1/context/inbox/archive` request. */
359
+ export const MAX_INBOX_ARCHIVE_BATCH = 500;
360
+ export const InboxArchiveRequestSchema = v.object({
361
+ ids: v.pipe(v.array(v.pipe(v.string(), v.minLength(1))), v.minLength(1), v.maxLength(MAX_INBOX_ARCHIVE_BATCH)),
362
+ });
363
+ export const InboxArchiveResponseSchema = v.object({
364
+ archived: v.pipe(v.number(), v.integer(), v.minValue(0)),
365
+ });
366
+ // ---------------------------------------------------------------------------
367
+ // Doc version history (read API)
368
+ // ---------------------------------------------------------------------------
369
+ // One append-only saved version of a context doc (guide_file). Rows are served
370
+ // lazily over REST and never sync through Zero — syncing them would grow every
371
+ // client's initial-sync payload forever. See doc-version-history-plan.md.
372
+ //
373
+ // `authorType` is intentionally an open string, not a picklist: the server owns
374
+ // the closed set ('user' | 'agent' | 'cli'), but a deployed client parses this
375
+ // response strictly, and a picklist would turn any future author kind into a
376
+ // hard parse error on an otherwise valid history read (same reasoning as
377
+ // `TransformGradeSchema.gradingTier`).
378
+ export const ContextItemVersionSchema = v.object({
379
+ id: v.string(),
380
+ // Monotonic per doc; the newest version equals the item's current `version`.
381
+ version: v.pipe(v.number(), v.integer(), v.minValue(1)),
382
+ contentHash: v.string(),
383
+ authorType: v.string(),
384
+ // Non-secret provenance the write path recorded (e.g. bundle slug, capture
385
+ // source). Never a bearer secret — these rows outlive tokens and ride export.
386
+ metadata: v.optional(v.nullable(v.record(v.string(), v.unknown()))),
387
+ createdAt: v.string(),
388
+ });
389
+ // The full version including the snapshotted body, returned by the per-version
390
+ // content fetch. `content` is nullable to stay blob-tier compatible: a future
391
+ // encrypted/blob version stores a hash pointer, not inline text.
392
+ export const ContextItemVersionDetailSchema = v.object({
393
+ id: v.string(),
394
+ version: v.pipe(v.number(), v.integer(), v.minValue(1)),
395
+ content: v.nullable(v.string()),
396
+ contentHash: v.string(),
397
+ authorType: v.string(),
398
+ metadata: v.optional(v.nullable(v.record(v.string(), v.unknown()))),
399
+ createdAt: v.string(),
400
+ });
401
+ // Newest-first page of a doc's version summaries. `nextCursor` is the version
402
+ // number to pass back as `?before=` for the following (older) page, or null
403
+ // once the oldest version has been returned.
404
+ export const ContextItemVersionsResponseSchema = v.object({
405
+ versions: v.array(ContextItemVersionSchema),
406
+ nextCursor: v.nullable(v.number()),
407
+ });
408
+ export const ContextItemVersionResponseSchema = v.object({
409
+ version: ContextItemVersionDetailSchema,
410
+ });
411
+ /** Default and max page size for the version-history list endpoint. */
412
+ export const DEFAULT_VERSIONS_LIMIT = 50;
413
+ export const MAX_VERSIONS_LIMIT = 200;
414
+ // ---------------------------------------------------------------------------
415
+ // Normalized guide schemas
416
+ // ---------------------------------------------------------------------------
417
+ export const NormalizedGuideRuleSchema = v.object({
418
+ text: NonEmptyStringSchema,
419
+ section: v.nullable(v.string()),
420
+ });
421
+ export const NormalizedGuideSchema = v.object({
422
+ rules: v.array(NormalizedGuideRuleSchema),
423
+ summary: v.string(),
424
+ });
425
+ // ---------------------------------------------------------------------------
426
+ // Sync payload schemas
427
+ // ---------------------------------------------------------------------------
428
+ const DashboardSyncTierSchema = v.picklist([
429
+ 'pattern_match',
430
+ 'cached',
431
+ 'llm',
432
+ 'batch',
433
+ 'unguided',
434
+ ]);
435
+ const DashboardGuideSuggestionStatusSchema = v.picklist([
436
+ 'pending',
437
+ 'accepted',
438
+ 'dismissed',
439
+ ]);
440
+ const DashboardFeedbackCandidateStatusSchema = v.picklist([
441
+ 'inferred',
442
+ 'confirmed',
443
+ 'dismissed',
444
+ ]);
445
+ const DashboardFeedbackSignalTypeSchema = v.picklist([
446
+ 'correction',
447
+ 'approval',
448
+ 'implied_preference',
449
+ 'stated_preference',
450
+ 'skill_suggestion',
451
+ ]);
452
+ export const ProposalCitationSchema = v.object({
453
+ sessionId: v.string(),
454
+ timestamp: v.optional(v.string()),
455
+ summary: v.string(),
456
+ });
457
+ /** Max serialized size of an action's `metadata` field — bounds storage bloat. */
458
+ export const MAX_ACTION_METADATA_BYTES = 16 * 1024;
459
+ const ActionMetadataSchema = v.pipe(v.record(v.string(), v.unknown()), v.check((value) => new TextEncoder().encode(JSON.stringify(value)).length <= MAX_ACTION_METADATA_BYTES, `metadata must serialize to at most ${MAX_ACTION_METADATA_BYTES} bytes`));
460
+ // Token usage extracted from an agent's own transcript (e.g. Claude Code
461
+ // JSONL), keyed by model. See docs/plans/token-cost-tracking-plan.md — Phase 1
462
+ // stores raw token counts only; cost is derived server-side in Phase 2.
463
+ export const TokenUsageSchema = v.object({
464
+ model: NonEmptyStringSchema,
465
+ provider: v.optional(v.string()),
466
+ inputTokens: v.pipe(v.number(), v.integer(), v.minValue(0)),
467
+ outputTokens: v.pipe(v.number(), v.integer(), v.minValue(0)),
468
+ cacheReadTokens: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
469
+ cacheWriteTokens: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
470
+ });
471
+ export const DashboardRunUsageSchema = v.object({
472
+ runId: v.optional(v.string()),
473
+ sessionId: NonEmptyStringSchema,
474
+ usage: v.array(TokenUsageSchema),
475
+ });
476
+ // Per-skill Tier-1 "load overhead" token usage extracted from an agent's own
477
+ // transcript (docs/plans/token-cost-tracking-plan.md §13.3) — the precise part
478
+ // of skill-cost attribution: the tokens billed to load a Skill/SKILL.md into
479
+ // context, approximated in v1 as the first-load message's usage. Tier 2
480
+ // (heuristic guided-work spans) is not implemented yet.
481
+ export const SkillUsageEntrySchema = v.object({
482
+ skillName: NonEmptyStringSchema,
483
+ model: NonEmptyStringSchema,
484
+ inputTokens: v.pipe(v.number(), v.integer(), v.minValue(0)),
485
+ cacheWriteTokens: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
486
+ });
487
+ export const DashboardSkillUsageSchema = v.object({
488
+ runId: v.optional(v.string()),
489
+ sessionId: NonEmptyStringSchema,
490
+ usage: v.array(SkillUsageEntrySchema),
491
+ });
492
+ export const DashboardSyncActionSchema = v.object({
493
+ id: v.optional(v.string()),
494
+ adapterId: v.optional(v.string()),
495
+ runId: v.optional(v.string()),
496
+ sessionId: v.optional(v.string()),
497
+ actionType: v.string(),
498
+ summary: v.string(),
499
+ filePaths: v.optional(v.array(v.string())),
500
+ verdict: v.optional(VerdictSchema),
501
+ gradingTier: v.optional(DashboardSyncTierSchema),
502
+ guideIds: v.optional(v.array(v.string())),
503
+ violatedGuideId: v.optional(v.nullable(v.string())),
504
+ evalReasoning: v.optional(v.nullable(v.string())),
505
+ metadata: v.optional(v.nullable(ActionMetadataSchema)),
506
+ });
507
+ export const DashboardGuideSuggestionSchema = v.object({
508
+ id: v.optional(v.string()),
509
+ content: v.string(),
510
+ reasoning: v.optional(v.string()),
511
+ triggerType: v.picklist([
512
+ 'uncovered',
513
+ 'ambiguous',
514
+ 'performance_pattern',
515
+ 'conversational_feedback',
516
+ ]),
517
+ sourceActionIds: v.optional(v.array(v.union([v.string(), ProposalCitationSchema]))),
518
+ status: v.optional(DashboardGuideSuggestionStatusSchema),
519
+ });
520
+ export const DashboardFeedbackCandidateSchema = v.object({
521
+ id: v.optional(v.string()),
522
+ signal: v.string(),
523
+ signalType: DashboardFeedbackSignalTypeSchema,
524
+ confidence: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(1))),
525
+ occurrences: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
526
+ status: v.optional(DashboardFeedbackCandidateStatusSchema),
527
+ });
528
+ /** Max number of actions accepted in a single `POST /api/sync` request. */
529
+ export const MAX_SYNC_ACTIONS_BATCH = 500;
530
+ /** Max number of guideSuggestions/feedbackCandidates/runUsage entries per sync request. */
531
+ export const MAX_SYNC_AUX_BATCH = 100;
532
+ export const DashboardSyncRequestSchema = v.object({
533
+ actions: v.optional(v.pipe(v.array(DashboardSyncActionSchema), v.maxLength(MAX_SYNC_ACTIONS_BATCH))),
534
+ guideSuggestions: v.optional(v.pipe(v.array(DashboardGuideSuggestionSchema), v.maxLength(MAX_SYNC_AUX_BATCH))),
535
+ feedbackCandidates: v.optional(v.pipe(v.array(DashboardFeedbackCandidateSchema), v.maxLength(MAX_SYNC_AUX_BATCH))),
536
+ runUsage: v.optional(v.pipe(v.array(DashboardRunUsageSchema), v.maxLength(MAX_SYNC_AUX_BATCH))),
537
+ skillUsage: v.optional(v.pipe(v.array(DashboardSkillUsageSchema), v.maxLength(MAX_SYNC_AUX_BATCH))),
538
+ });
539
+ export const ExtractedGuideSuggestionSchema = v.object({
540
+ id: v.string(),
541
+ content: v.string(),
542
+ reasoning: v.string(),
543
+ triggerType: v.picklist(['conversational_feedback']),
544
+ confidence: v.pipe(v.number(), v.minValue(0), v.maxValue(1)),
545
+ occurrences: v.pipe(v.number(), v.integer(), v.minValue(1)),
546
+ citations: v.array(ProposalCitationSchema),
547
+ source: PlatformKindSchema,
548
+ });
549
+ export const ExtractedSkillSuggestionSchema = v.object({
550
+ id: v.string(),
551
+ signal: v.string(),
552
+ confidence: v.pipe(v.number(), v.minValue(0), v.maxValue(1)),
553
+ occurrences: v.pipe(v.number(), v.integer(), v.minValue(1)),
554
+ citations: v.array(ProposalCitationSchema),
555
+ source: PlatformKindSchema,
556
+ });
557
+ export const ExtractedFeedbackBatchSchema = v.object({
558
+ guideSuggestions: v.array(ExtractedGuideSuggestionSchema),
559
+ skillSuggestions: v.array(ExtractedSkillSuggestionSchema),
560
+ });
561
+ // ---------------------------------------------------------------------------
562
+ // Agent / eval schemas
563
+ // ---------------------------------------------------------------------------
564
+ export const FindingSchema = v.object({
565
+ level: v.picklist(['info', 'warning', 'error']),
566
+ message: v.string(),
567
+ data: v.optional(v.unknown()),
568
+ });
569
+ export const AgentConfigSchema = v.object({
570
+ id: v.string(),
571
+ name: v.string(),
572
+ description: v.string(),
573
+ capabilities: v.array(v.string()),
574
+ meta: v.optional(v.record(v.string(), v.unknown())),
575
+ });
576
+ export const EvalRequestSchema = v.object({
577
+ agent: AgentConfigSchema,
578
+ context: v.optional(v.record(v.string(), v.unknown())),
579
+ });
580
+ export const EvalResultSchema = v.object({
581
+ agentId: v.string(),
582
+ passed: v.boolean(),
583
+ summary: v.string(),
584
+ findings: v.array(FindingSchema),
585
+ });
586
+ export const ProjectionRequestSchema = v.object({
587
+ agents: v.array(AgentConfigSchema),
588
+ outputDir: v.string(),
589
+ });
590
+ export const ProjectionResultSchema = v.object({
591
+ agentsMd: v.string(),
592
+ agentsDir: v.record(v.string(), v.string()),
593
+ });
594
+ // ---------------------------------------------------------------------------
595
+ // Run-based eval schemas
596
+ // ---------------------------------------------------------------------------
597
+ export const RuleOutcomeSchema = v.picklist(['aligned', 'misaligned', 'not_applicable']);
598
+ export const ActionEntrySchema = v.object({
599
+ timestamp: v.string(),
600
+ sessionId: v.string(),
601
+ toolName: v.string(),
602
+ filePath: v.optional(v.string()),
603
+ command: v.optional(v.string()),
604
+ });
605
+ export const RuleVerdictSchema = v.object({
606
+ rule: v.string(),
607
+ verdict: RuleOutcomeSchema,
608
+ reasoning: v.string(),
609
+ });
610
+ export const RunEvalResultSchema = v.object({
611
+ verdicts: v.array(RuleVerdictSchema),
612
+ summary: v.string(),
613
+ score: v.pipe(v.number(), v.minValue(0), v.maxValue(1)),
614
+ });
615
+ // ---------------------------------------------------------------------------
616
+ // Parse wrappers (throw ValiError on invalid input)
617
+ // ---------------------------------------------------------------------------
618
+ export function parseAgentConfig(data) {
619
+ return v.parse(AgentConfigSchema, data);
620
+ }
621
+ export function parseEvalRequest(data) {
622
+ return v.parse(EvalRequestSchema, data);
623
+ }
624
+ export function parseEvalResult(data) {
625
+ return v.parse(EvalResultSchema, data);
626
+ }
627
+ export function parseEvalProviderSettings(data) {
628
+ return v.parse(PartialEvalProviderSettingsSchema, data);
629
+ }
630
+ export function parseEvalProviderConfig(data) {
631
+ return v.parse(EvalProviderConfigSchema, data);
632
+ }
633
+ export function parseValidateTokenRequest(data) {
634
+ return v.parse(ValidateTokenRequestSchema, data);
635
+ }
636
+ export function parseValidateTokenResponse(data) {
637
+ return v.parse(ValidateTokenResponseSchema, data);
638
+ }
639
+ export function parseCliTokenMetadata(data) {
640
+ return v.parse(CliTokenMetadataSchema, data);
641
+ }
642
+ export function parseGuideSnapshot(data) {
643
+ return v.parse(GuideSnapshotSchema, data);
644
+ }
645
+ export function parseGuideBundleResponse(data) {
646
+ return v.parse(GuideBundleResponseSchema, data);
647
+ }
648
+ export function parseRawGuideFilePayload(data) {
649
+ return v.parse(RawGuideFilePayloadSchema, data);
650
+ }
651
+ export function parseGuideMemberFilesResponse(data) {
652
+ return v.parse(GuideMemberFilesResponseSchema, data);
653
+ }
654
+ export function parseRawGuideFileDeletion(data) {
655
+ return v.parse(RawGuideFileDeletionSchema, data);
656
+ }
657
+ export function parseRuleDeletionsRequest(data) {
658
+ return v.parse(RuleDeletionsRequestSchema, data);
659
+ }
660
+ export function parseRulesImportRequest(data) {
661
+ return v.parse(RulesImportRequestSchema, data);
662
+ }
663
+ export function parseRuleDeletionsResponse(data) {
664
+ return v.parse(RuleDeletionsResponseSchema, data);
665
+ }
666
+ export function parseBundle(data) {
667
+ return v.parse(BundleSchema, data);
668
+ }
669
+ export function parseBundleListResponse(data) {
670
+ return v.parse(BundleListResponseSchema, data);
671
+ }
672
+ export function parseCreateBundleRequest(data) {
673
+ return v.parse(CreateBundleRequestSchema, data);
674
+ }
675
+ export function parseCreateBundleResponse(data) {
676
+ return v.parse(CreateBundleResponseSchema, data);
677
+ }
678
+ export function parseContextItem(data) {
679
+ return v.parse(ContextItemSchema, data);
680
+ }
681
+ export function parseContextItemsResponse(data) {
682
+ return v.parse(ContextItemsResponseSchema, data);
683
+ }
684
+ export function parsePushContextRequest(data) {
685
+ return v.parse(PushContextRequestSchema, data);
686
+ }
687
+ export function parsePushContextResponse(data) {
688
+ return v.parse(PushContextResponseSchema, data);
689
+ }
690
+ export function parseUpdateContextItemRequest(data) {
691
+ return v.parse(UpdateContextItemRequestSchema, data);
692
+ }
693
+ export function parseUpdateContextItemResponse(data) {
694
+ return v.parse(UpdateContextItemResponseSchema, data);
695
+ }
696
+ export function parseInboxCaptureRequest(data) {
697
+ return v.parse(InboxCaptureRequestSchema, data);
698
+ }
699
+ export function parseInboxCaptureResponse(data) {
700
+ return v.parse(InboxCaptureResponseSchema, data);
701
+ }
702
+ export function parseInboxListResponse(data) {
703
+ return v.parse(InboxListResponseSchema, data);
704
+ }
705
+ export function parseContextItemVersionsResponse(data) {
706
+ return v.parse(ContextItemVersionsResponseSchema, data);
707
+ }
708
+ export function parseContextItemVersionResponse(data) {
709
+ return v.parse(ContextItemVersionResponseSchema, data);
710
+ }
711
+ export function parseInboxArchiveRequest(data) {
712
+ return v.parse(InboxArchiveRequestSchema, data);
713
+ }
714
+ export function parseInboxArchiveResponse(data) {
715
+ return v.parse(InboxArchiveResponseSchema, data);
716
+ }
717
+ export function parseNormalizedGuide(data) {
718
+ return v.parse(NormalizedGuideSchema, data);
719
+ }
720
+ export function parseDashboardSyncRequest(data) {
721
+ return v.parse(DashboardSyncRequestSchema, data);
722
+ }
723
+ export function parseProposalCitation(data) {
724
+ return v.parse(ProposalCitationSchema, data);
725
+ }
726
+ export function parseExtractedGuideSuggestion(data) {
727
+ return v.parse(ExtractedGuideSuggestionSchema, data);
728
+ }
729
+ export function parseExtractedSkillSuggestion(data) {
730
+ return v.parse(ExtractedSkillSuggestionSchema, data);
731
+ }
732
+ export function parseExtractedFeedbackBatch(data) {
733
+ return v.parse(ExtractedFeedbackBatchSchema, data);
734
+ }
735
+ export function parseProjectionRequest(data) {
736
+ return v.parse(ProjectionRequestSchema, data);
737
+ }
738
+ export function parseProjectionResult(data) {
739
+ return v.parse(ProjectionResultSchema, data);
740
+ }
741
+ export function parseActionEntry(data) {
742
+ return v.parse(ActionEntrySchema, data);
743
+ }
744
+ export function parseRuleVerdict(data) {
745
+ return v.parse(RuleVerdictSchema, data);
746
+ }
747
+ export function parseRunEvalResult(data) {
748
+ return v.parse(RunEvalResultSchema, data);
749
+ }
750
+ //# sourceMappingURL=schemas.js.map