@mossbear/protocol 0.1.0-alpha.26
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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/bundleSlug.d.ts +42 -0
- package/dist/bundleSlug.d.ts.map +1 -0
- package/dist/bundleSlug.js +68 -0
- package/dist/bundleSlug.js.map +1 -0
- package/dist/bundleSlug.test.d.ts +2 -0
- package/dist/bundleSlug.test.d.ts.map +1 -0
- package/dist/bundleSlug.test.js +43 -0
- package/dist/bundleSlug.test.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas.d.ts +1782 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +1422 -0
- package/dist/schemas.js.map +1 -0
- package/dist/schemas.test.d.ts +2 -0
- package/dist/schemas.test.d.ts.map +1 -0
- package/dist/schemas.test.js +1018 -0
- package/dist/schemas.test.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +47 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +125 -0
- package/dist/version.js.map +1 -0
- package/dist/version.test.d.ts +2 -0
- package/dist/version.test.d.ts.map +1 -0
- package/dist/version.test.js +86 -0
- package/dist/version.test.js.map +1 -0
- package/package.json +48 -0
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,1422 @@
|
|
|
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
|
+
const NonEmptyStringSchema = v.pipe(v.string(), v.minLength(1));
|
|
33
|
+
const Sha256HexSchema = v.pipe(v.string(), v.regex(/^[a-f0-9]{64}$/i));
|
|
34
|
+
// No eval-provider schemas here. They described the LLM credentials the CLI
|
|
35
|
+
// needed to grade on-device; grading is server-side and the CLI holds no
|
|
36
|
+
// provider config (docs/plans/cli-local-grading-removal-plan.md). The server's
|
|
37
|
+
// own provider settings are environment variables read in
|
|
38
|
+
// `apps/dashboard/src/lib/eval/serverEvalAdapter.ts`, not a wire contract.
|
|
39
|
+
// Auth wire schemas used to live here (ValidateTokenRequest/Response,
|
|
40
|
+
// CliTokenMetadata). The validate-token route never adopted them — it checks
|
|
41
|
+
// `typeof body.token === 'string'` by hand — and nothing else imported them.
|
|
42
|
+
// Deleted rather than kept as a parallel contract that had already drifted
|
|
43
|
+
// from the route's `{ valid }` response.
|
|
44
|
+
// The guide snapshot schemas live further down, after
|
|
45
|
+
// `SafeRelativePosixPathSchema` and `GuideDiscoveryRootSchema`: the bundle now
|
|
46
|
+
// carries the same `(root, path)` pair the import payload does, and a `const`
|
|
47
|
+
// cannot be referenced above its own declaration.
|
|
48
|
+
/** Max serialized size (UTF-8 bytes) of an imported guide file's raw content. */
|
|
49
|
+
export const MAX_RAW_GUIDE_CONTENT_BYTES = 512 * 1024;
|
|
50
|
+
/** Max member files a single skill-md payload may carry. */
|
|
51
|
+
export const MAX_SKILL_MEMBER_FILES = 64;
|
|
52
|
+
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`));
|
|
53
|
+
// A path that is safe to re-create on disk and safe to store as identity:
|
|
54
|
+
// forward slashes only, never absolute (POSIX or Windows drive-letter form),
|
|
55
|
+
// no empty / `.` / `..` segments, no control characters. Every path this
|
|
56
|
+
// protocol carries is eventually written into a project tree by the export
|
|
57
|
+
// round-trip, so the rule belongs at the schema boundary rather than in each
|
|
58
|
+
// consumer — CLAUDE.md → Validation.
|
|
59
|
+
//
|
|
60
|
+
// Exported so the member-files read path (server response filtering, CLI
|
|
61
|
+
// defense-in-depth before writing to disk) enforces the identical rule.
|
|
62
|
+
export const SafeRelativePosixPathSchema = 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
|
|
63
|
+
.split('/')
|
|
64
|
+
.every((segment) => segment !== '' && segment !== '.' && segment !== '..'), 'path must not contain empty, "." or ".." segments'));
|
|
65
|
+
// Member paths are relative to the skill directory, and additionally may never
|
|
66
|
+
// name the skill's own SKILL.md (the payload itself).
|
|
67
|
+
export const SkillMemberPathSchema = v.pipe(SafeRelativePosixPathSchema, v.check((path) => path.toLowerCase() !== 'skill.md', 'path must not shadow the skill’s own SKILL.md'));
|
|
68
|
+
// A sibling file of a skill's SKILL.md (scripts/, references/, assets/,
|
|
69
|
+
// supporting docs), imported alongside it as a guide_file row.
|
|
70
|
+
export const RawSkillMemberFileSchema = v.object({
|
|
71
|
+
path: SkillMemberPathSchema,
|
|
72
|
+
rawContent: BoundedRawContentSchema,
|
|
73
|
+
lastModified: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
74
|
+
content_hash: Sha256HexSchema,
|
|
75
|
+
});
|
|
76
|
+
/** Max skipped-member paths a skill-md payload may report alongside `members`. */
|
|
77
|
+
export const MAX_SKILL_SKIPPED_MEMBER_PATHS = 256;
|
|
78
|
+
// Paths (relative to the skill directory) that discovery found but could not
|
|
79
|
+
// import this run — binary/non-UTF-8, oversized, unreadable, or beyond the
|
|
80
|
+
// member cap. Deliberately looser than SkillMemberPathSchema: these describe
|
|
81
|
+
// what is on the user's disk and are only ever string-matched against stored
|
|
82
|
+
// member paths server-side (never re-created as files), so an odd on-disk
|
|
83
|
+
// filename must not fail the whole payload.
|
|
84
|
+
const SkippedMemberPathSchema = v.pipe(v.string(), v.minLength(1), v.maxLength(512));
|
|
85
|
+
/**
|
|
86
|
+
* Which root a discovered guide file's `path` is relative to: the repo checkout
|
|
87
|
+
* (`project`) or the user's homedir (`global`, e.g. `.claude/CLAUDE.md`).
|
|
88
|
+
* Part of the import resolution key — see `RawGuideFilePayloadSchema.root`.
|
|
89
|
+
*/
|
|
90
|
+
export const GuideDiscoveryRootSchema = v.picklist(['project', 'global']);
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Guide snapshot schemas
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
export const GuideSnapshotSchema = v.object({
|
|
95
|
+
id: v.string(),
|
|
96
|
+
title: v.string(),
|
|
97
|
+
content: v.string(),
|
|
98
|
+
enabled: v.boolean(),
|
|
99
|
+
updatedAt: v.string(),
|
|
100
|
+
// origin is `file:{format}:{path}` for imported guides, null/absent
|
|
101
|
+
// otherwise. contentHash is the SHA-256 of the canonical bytes; the CLI uses
|
|
102
|
+
// the pair to round-trip imported files back to disk without re-encoding.
|
|
103
|
+
//
|
|
104
|
+
// **`origin` is not superseded by `(root, path)`.** Nothing on the write side
|
|
105
|
+
// reconciles on it after this slice, but `resolveRuleFileDelivery` still
|
|
106
|
+
// matches delivery evidence on `(origin, contentHash)`
|
|
107
|
+
// (`apps/dashboard/src/lib/eval/ruleFileDelivery.ts:395-430`), so dropping it
|
|
108
|
+
// would make every guide read as undelivered — silently, since runs still
|
|
109
|
+
// grade and verdicts still appear. It stays as provenance.
|
|
110
|
+
origin: v.optional(v.nullable(v.string())),
|
|
111
|
+
contentHash: v.optional(v.nullable(Sha256HexSchema)),
|
|
112
|
+
// Which scope root `path` is relative to, and the path itself — the write
|
|
113
|
+
// side's half of the identity the read side already stores on
|
|
114
|
+
// `guide_files.root` / `guide_files.path`
|
|
115
|
+
// (`guide-write-side-identity-plan.md` → "Carry `root` on every write path").
|
|
116
|
+
//
|
|
117
|
+
// Without them the bundle could not express "global": `origin` is
|
|
118
|
+
// `file:{format}:{path}` with no root component, so every writer resolved it
|
|
119
|
+
// against the repo and a guide imported from `~/.claude/CLAUDE.md` came back
|
|
120
|
+
// down as `<repo>/.claude/CLAUDE.md`.
|
|
121
|
+
//
|
|
122
|
+
// **Both optional and nullable, and their absence is not an error.** A guide
|
|
123
|
+
// authored in the dashboard has no path; a row imported before the columns
|
|
124
|
+
// existed has neither. Such a guide keeps resolving from `origin` alone —
|
|
125
|
+
// today's behaviour — rather than becoming undeliverable. A missing `root` is
|
|
126
|
+
// read as `project`, which is what every pre-existing row meant.
|
|
127
|
+
root: v.optional(v.nullable(GuideDiscoveryRootSchema)),
|
|
128
|
+
path: v.optional(v.nullable(SafeRelativePosixPathSchema)),
|
|
129
|
+
});
|
|
130
|
+
export const GuideBundleResponseSchema = v.object({
|
|
131
|
+
guides: v.array(GuideSnapshotSchema),
|
|
132
|
+
pulledAt: v.string(),
|
|
133
|
+
});
|
|
134
|
+
export const RawGuideFilePayloadSchema = v.pipe(v.object({
|
|
135
|
+
// Relative to the discovery root the CLI walked (`payloadPath`,
|
|
136
|
+
// `packages/cli/src/rules/discover.ts:29-30`). Two things depend on it
|
|
137
|
+
// being a safe relative path, which is why it is parsed here rather than
|
|
138
|
+
// guarded downstream:
|
|
139
|
+
//
|
|
140
|
+
// - it is embedded verbatim in the guide's `origin` (`file:{format}:{path}`)
|
|
141
|
+
// and parsed back out to place the file on disk. The writer that does so
|
|
142
|
+
// is now the docs phase's projection resolver
|
|
143
|
+
// (`packages/cli/src/docs/guide-projection.ts`), which rejects anything
|
|
144
|
+
// absolute, traversing or uncontained — so an unsafe path stores a guide
|
|
145
|
+
// that can never round-trip. (`mossbear guides export` was the original
|
|
146
|
+
// writer and skipped such a path *silently*; it was deleted by
|
|
147
|
+
// `unified-item-sync` Slice 5.);
|
|
148
|
+
// - it is stored as `guide_files.path`, the column import reconciles on
|
|
149
|
+
// (`guide-versioned-identity-plan.md` → Decision 4), and identity must
|
|
150
|
+
// not be keyable on a value the exporter refuses to honour.
|
|
151
|
+
path: SafeRelativePosixPathSchema,
|
|
152
|
+
// Which discovery root `path` is relative to — `guide-versioned-identity-plan.md`
|
|
153
|
+
// → Decision 6. Without it, `(repoId, path)` collapses a project skill onto
|
|
154
|
+
// its `~/.claude/skills` twin: `.claude/skills` is in both of the CLI's root
|
|
155
|
+
// lists and both scans emit the same relative string, while `repoId` is
|
|
156
|
+
// envelope-level so both files carry the same one.
|
|
157
|
+
//
|
|
158
|
+
// **Optional, and its absence is not an error** — a CLI predating this field
|
|
159
|
+
// sends none, exactly as it may send no `repoId`. A payload missing either
|
|
160
|
+
// does not participate in reconciliation and keeps the content-hash insert
|
|
161
|
+
// behaviour it was created under, rather than being defaulted to `'project'`:
|
|
162
|
+
// guessing would let an old client's `--global` file adopt a project row.
|
|
163
|
+
root: v.optional(GuideDiscoveryRootSchema),
|
|
164
|
+
format: GuideFileFormatSchema,
|
|
165
|
+
rawContent: BoundedRawContentSchema,
|
|
166
|
+
lastModified: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
167
|
+
content_hash: Sha256HexSchema,
|
|
168
|
+
// Only meaningful on `skill-md` payloads. `[]` asserts the skill has no
|
|
169
|
+
// member files (stale synced rows are removed server-side); omitting the
|
|
170
|
+
// field means "no member information" and leaves member rows untouched.
|
|
171
|
+
// No CLI caller omits it today — every sender reaches this schema through
|
|
172
|
+
// a directory scan that populates it — but the distinction is load-bearing
|
|
173
|
+
// on the wire and must not collapse: reading an absent `members` as an
|
|
174
|
+
// empty skill would hard-delete a client's member rows.
|
|
175
|
+
members: v.optional(v.pipe(v.array(RawSkillMemberFileSchema), v.maxLength(MAX_SKILL_MEMBER_FILES))),
|
|
176
|
+
/**
|
|
177
|
+
* The body-item version this machine last agreed with the server about for
|
|
178
|
+
* this `(scope, root, path)` — the machine-local baseline
|
|
179
|
+
* (`guide-versioned-identity-plan.md` → slice 2b-B, D10 in
|
|
180
|
+
* `context-bundle-authoring-plan.md`).
|
|
181
|
+
*
|
|
182
|
+
* It is the *client* half of the reconcile precondition, and it exists to
|
|
183
|
+
* answer a question the server cannot: whether the bytes on this disk were
|
|
184
|
+
* edited **from** the version the server currently holds, or from an older
|
|
185
|
+
* one. The server-only guard shipped in 2b-A can see that a dashboard edit
|
|
186
|
+
* exists, never that this machine has already seen it — so a guide the
|
|
187
|
+
* dashboard edited and the user then re-synced to disk forks forever.
|
|
188
|
+
*
|
|
189
|
+
* **Optional, and its absence is not an error.** A CLI predating the
|
|
190
|
+
* baseline store, a fresh machine, or a deleted `~/.mossbear/state/` sends
|
|
191
|
+
* none, and the payload falls back to the server-only guard exactly as it
|
|
192
|
+
* behaves today. Absence therefore degrades to "decline and fork", never to
|
|
193
|
+
* an overwrite; a *stale* value degrades to a reported conflict.
|
|
194
|
+
*/
|
|
195
|
+
expectedVersion: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
|
|
196
|
+
// Only alongside `members`: member paths present on disk but not imported
|
|
197
|
+
// this run. The server must NOT treat their absence from `members` as
|
|
198
|
+
// removal — a transiently unreadable or newly-oversized file would
|
|
199
|
+
// otherwise hard-delete its previously synced row.
|
|
200
|
+
skippedMemberPaths: v.optional(v.pipe(v.array(SkippedMemberPathSchema), v.maxLength(MAX_SKILL_SKIPPED_MEMBER_PATHS))),
|
|
201
|
+
}), 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'),
|
|
202
|
+
// Case-insensitive uniqueness: the export round-trip re-creates these files
|
|
203
|
+
// on disk, where the default macOS/Windows filesystems fold case — two
|
|
204
|
+
// members differing only by case would silently overwrite each other.
|
|
205
|
+
v.check((payload) => payload.members === undefined ||
|
|
206
|
+
new Set(payload.members.map((member) => member.path.toLowerCase())).size ===
|
|
207
|
+
payload.members.length, 'member paths must be unique (case-insensitive)'));
|
|
208
|
+
// A skill guide's member file as served by GET /api/v1/guides/:id/member-files
|
|
209
|
+
// — the export round-trip's read side. Lean by design: member bodies are NOT
|
|
210
|
+
// part of the guide bundle (the eval hot path pulls that every session), they
|
|
211
|
+
// are fetched lazily per skill guide at export time.
|
|
212
|
+
export const GuideMemberFileSchema = v.object({
|
|
213
|
+
path: SkillMemberPathSchema,
|
|
214
|
+
content: BoundedRawContentSchema,
|
|
215
|
+
contentHash: Sha256HexSchema,
|
|
216
|
+
});
|
|
217
|
+
export const GuideMemberFilesResponseSchema = v.pipe(v.object({
|
|
218
|
+
memberFiles: v.pipe(v.array(GuideMemberFileSchema), v.maxLength(MAX_SKILL_MEMBER_FILES)),
|
|
219
|
+
}),
|
|
220
|
+
// Mirrors the import payload's rule: on case-folding filesystems two
|
|
221
|
+
// entries differing only by case resolve to one file, and both planning as
|
|
222
|
+
// NEW would let the second silently clobber the first without the
|
|
223
|
+
// OVERWRITE diff prompt. A response that case-collides is malformed.
|
|
224
|
+
v.check((response) => new Set(response.memberFiles.map((file) => file.path.toLowerCase())).size ===
|
|
225
|
+
response.memberFiles.length, 'member file paths must be unique (case-insensitive)'));
|
|
226
|
+
/** Max number of rules accepted in a single `POST /api/rules/import` request. */
|
|
227
|
+
export const MAX_RULES_IMPORT_BATCH = 100;
|
|
228
|
+
/** Opaque checkout digest shared by run logs, manifests, and `runs.repo_id`. */
|
|
229
|
+
export const RepoIdSchema = v.pipe(v.string(), v.regex(/^repo_[0-9a-f]{32}$/, 'repoId must be repo_ followed by 32 hex chars'));
|
|
230
|
+
/** Longest accepted `projectId`. Generated ids are far shorter; this bounds a hand-declared one. */
|
|
231
|
+
const MAX_PROJECT_ID_LENGTH = 128;
|
|
232
|
+
/**
|
|
233
|
+
* A repo's declared portable identity. See `RulesImportRequestSchema.projectId`
|
|
234
|
+
* for why this is looser than {@link RepoIdSchema} — a declared id is
|
|
235
|
+
* human-authored and predates any prefix rule.
|
|
236
|
+
*/
|
|
237
|
+
export const ProjectIdSchema = v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(MAX_PROJECT_ID_LENGTH), v.regex(/^[A-Za-z0-9_.-]+$/, 'projectId must contain only letters, digits, underscore, dot or hyphen'));
|
|
238
|
+
// Envelope-only schema: caps request shape/size before the per-item
|
|
239
|
+
// parseRawGuideFilePayload loop validates each rule in detail (preserves
|
|
240
|
+
// index-aware error reporting — see apps/dashboard/app/api/rules/import+api.ts).
|
|
241
|
+
export const RulesImportRequestSchema = v.object({
|
|
242
|
+
rules: v.pipe(v.array(v.unknown()), v.maxLength(MAX_RULES_IMPORT_BATCH)),
|
|
243
|
+
/**
|
|
244
|
+
* Which checkout these files came from — the same opaque digest the hook and
|
|
245
|
+
* the rule manifest already carry (`packages/cli/src/repo-id.ts`,
|
|
246
|
+
* `RuleManifestSchema.repoId`). Envelope-level, not per-file: every file in
|
|
247
|
+
* one import comes from one repo, and the per-file payload stays lean (the
|
|
248
|
+
* "Import endpoint payload" resolved decision).
|
|
249
|
+
*
|
|
250
|
+
* It is what lets the server reconcile an edited file back onto the guide it
|
|
251
|
+
* already created instead of inserting a second one. `origin` alone cannot:
|
|
252
|
+
* it is `file:{format}:{path}` with no repo component, so two checkouts
|
|
253
|
+
* sharing `.agents/code-style.md` would collide.
|
|
254
|
+
*
|
|
255
|
+
* **Optional, and its absence is not an error.** A CLI predating this field
|
|
256
|
+
* sends none, and the server falls back to the content-hash dedupe it always
|
|
257
|
+
* used. Never send a repo id derived anywhere but `repoIdFromRoot` — the
|
|
258
|
+
* digest is keyed by a machine-local secret precisely so the server never
|
|
259
|
+
* learns a path.
|
|
260
|
+
*
|
|
261
|
+
* Shaped by the canonical `RepoIdSchema` (shared with run logs and manifests)
|
|
262
|
+
* rather than a looser local check: a row storing anything but
|
|
263
|
+
* a real digest could never match one, so a malformed identity has to be
|
|
264
|
+
* rejected at the boundary instead of persisted as a permanent orphan.
|
|
265
|
+
*/
|
|
266
|
+
repoId: v.optional(RepoIdSchema),
|
|
267
|
+
/**
|
|
268
|
+
* The repo's **portable** identity — `project.id` from the committed
|
|
269
|
+
* `<repo>/.mossbear/config.json` (`guide-write-side-identity` slice 1, shipped in
|
|
270
|
+
* #540). This is what makes `(scope, path)` reconciliation portable, and it is
|
|
271
|
+
* the field `unified-item-sync` slice 2 exists to put on the wire.
|
|
272
|
+
*
|
|
273
|
+
* **Why `repoId` is not enough, even though it is already here.**
|
|
274
|
+
* `repoIdFromRoot` is an HMAC keyed by a machine-local secret
|
|
275
|
+
* (`packages/cli/src/repo-id.ts`), so the same checkout on two machines — or
|
|
276
|
+
* one teammate's clone versus another's — produces two different digests. A
|
|
277
|
+
* guide reconciled on it therefore forks the moment anyone else edits the
|
|
278
|
+
* file, which is the one-way trip this item is closing. `projectId` is
|
|
279
|
+
* declared in a committed file, so every clone agrees on it.
|
|
280
|
+
*
|
|
281
|
+
* **Optional, and its absence is not an error.** A CLI predating this field
|
|
282
|
+
* sends none and the server falls back to `repoId` scoping, which is exactly
|
|
283
|
+
* the behaviour those clients already have. A repo that has never run
|
|
284
|
+
* `mossbear init` also has no declaration, and must keep importing.
|
|
285
|
+
*
|
|
286
|
+
* Deliberately looser than `RepoIdSchema`: a generated id is
|
|
287
|
+
* `proj_`+hex, but a *declared* one is whatever a human committed —
|
|
288
|
+
* `readRepoProjectId` accepts any non-empty trimmed string
|
|
289
|
+
* (`packages/cli/src/docs/repo-config.ts`), and rejecting those here would
|
|
290
|
+
* break a shipped contract. Constrained only as far as it must be to serve as
|
|
291
|
+
* a storage key: no whitespace, no path separators, no control characters,
|
|
292
|
+
* bounded length.
|
|
293
|
+
*/
|
|
294
|
+
projectId: v.optional(ProjectIdSchema),
|
|
295
|
+
});
|
|
296
|
+
/**
|
|
297
|
+
* What the server did with one payload, reported per file so the CLI can update
|
|
298
|
+
* its machine-local baseline (`guide-versioned-identity-plan.md` → slice 2b-B).
|
|
299
|
+
*
|
|
300
|
+
* The distinction that matters is not "did something change" but **do disk and
|
|
301
|
+
* server now agree**: `imported`, `updated` and `skipped` all mean they do, and
|
|
302
|
+
* only those license recording a new baseline. `conflict` means the disk bytes
|
|
303
|
+
* did *not* land — recording the server's version for one would hand the next
|
|
304
|
+
* import a matching `expectedVersion` and turn a refused overwrite into an
|
|
305
|
+
* automatic one on the following run, which is the failure this whole precondition
|
|
306
|
+
* exists to prevent.
|
|
307
|
+
*/
|
|
308
|
+
export const GuideImportOutcomeSchema = v.picklist([
|
|
309
|
+
'imported',
|
|
310
|
+
'updated',
|
|
311
|
+
'skipped',
|
|
312
|
+
'conflict',
|
|
313
|
+
]);
|
|
314
|
+
/**
|
|
315
|
+
* Per-payload import outcome, addressed by the same `(root, path)` pair the
|
|
316
|
+
* server reconciles on so the CLI can key its baseline identically without
|
|
317
|
+
* re-deriving anything.
|
|
318
|
+
*
|
|
319
|
+
* `version` is the body item's version **as the server holds it after this
|
|
320
|
+
* request** — the value a later import should send as `expectedVersion` once
|
|
321
|
+
* disk and server agree. It is reported for a `conflict` too, deliberately: the
|
|
322
|
+
* conflict report needs it to offer explicit resolution, and reporting it is not
|
|
323
|
+
* the same as recording it.
|
|
324
|
+
*
|
|
325
|
+
* Entries are omitted for a payload the server could not address at all (no
|
|
326
|
+
* `root`, or a pre-R0 row): there is no identity to key a baseline on, and
|
|
327
|
+
* inventing one would let a later import claim a precondition it never observed.
|
|
328
|
+
*/
|
|
329
|
+
export const GuideImportResultSchema = v.object({
|
|
330
|
+
root: GuideDiscoveryRootSchema,
|
|
331
|
+
path: SafeRelativePosixPathSchema,
|
|
332
|
+
outcome: GuideImportOutcomeSchema,
|
|
333
|
+
version: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
334
|
+
});
|
|
335
|
+
// Deletion signal for a previously imported guide file that disappeared from
|
|
336
|
+
// disk. The dashboard disables (sets active=false) any row whose origin matches
|
|
337
|
+
// `file:{format}:{path}` — the row is retained so historical actions are not
|
|
338
|
+
// orphaned, and so re-creating the file can flip it back on.
|
|
339
|
+
//
|
|
340
|
+
// **That last part is not true today.** The import endpoint's lookup has no
|
|
341
|
+
// `active` predicate, so restoring a deleted file matches the disabled row's
|
|
342
|
+
// hash and is skipped — the row stays off. Reactivation arrives with
|
|
343
|
+
// reconciliation (`guide-versioned-identity` slice 2), which resolves the row
|
|
344
|
+
// by `(userId, repoId, origin)` rather than by hash.
|
|
345
|
+
//
|
|
346
|
+
// **No first-party emitter as of 2026-08-11.** `mossbear rules watch` was this
|
|
347
|
+
// signal's only caller and was deleted (`guide-versioned-identity-plan.md` →
|
|
348
|
+
// "Watch removed"), so nothing in the CLI now derives a deletion from a
|
|
349
|
+
// vanished file. Schema and route stay: Decision 3 of that plan keeps exactly
|
|
350
|
+
// these `(userId, origin)` → `active=false` semantics for unshared
|
|
351
|
+
// repo-authored guides, and whatever re-introduces the signal must address it
|
|
352
|
+
// by item id rather than reviving the origin path.
|
|
353
|
+
export const RawGuideFileDeletionSchema = v.object({
|
|
354
|
+
path: v.pipe(v.string(), v.minLength(1)),
|
|
355
|
+
format: GuideFileFormatSchema,
|
|
356
|
+
});
|
|
357
|
+
/** Max number of deletions accepted in a single `POST /api/rules/deletions` request. */
|
|
358
|
+
export const MAX_RULE_DELETIONS_BATCH = 500;
|
|
359
|
+
// Envelope-only schema, matching RulesImportRequestSchema's shape: caps
|
|
360
|
+
// request shape/size before the per-item parseRawGuideFileDeletion loop in
|
|
361
|
+
// apps/dashboard/app/api/rules/deletions+api.ts validates each entry in
|
|
362
|
+
// detail (preserves its index-aware error reporting).
|
|
363
|
+
export const RuleDeletionsRequestSchema = v.object({
|
|
364
|
+
deletions: v.pipe(v.array(v.unknown()), v.maxLength(MAX_RULE_DELETIONS_BATCH)),
|
|
365
|
+
});
|
|
366
|
+
// No RuleDeletionsResponseSchema — the route returns
|
|
367
|
+
// `Response.json({ disabled, skipped })` directly and nothing parsed the
|
|
368
|
+
// response shape.
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// Context bundle schemas (docs/plans/context-layer-mcp-inbox-plan.md — Phase 0)
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// url-safe slug, unique per user. Lowercase alphanumerics + hyphens, must start
|
|
373
|
+
// with an alphanumeric so it reads cleanly in a URL/path.
|
|
374
|
+
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'));
|
|
375
|
+
export const BundleSchema = v.object({
|
|
376
|
+
id: v.string(),
|
|
377
|
+
slug: BundleSlugSchema,
|
|
378
|
+
name: v.string(),
|
|
379
|
+
createdAt: v.string(),
|
|
380
|
+
});
|
|
381
|
+
export const BundleListResponseSchema = v.object({
|
|
382
|
+
bundles: v.array(BundleSchema),
|
|
383
|
+
});
|
|
384
|
+
export const CreateBundleRequestSchema = v.object({
|
|
385
|
+
slug: BundleSlugSchema,
|
|
386
|
+
name: v.pipe(v.string(), v.minLength(1), v.maxLength(200)),
|
|
387
|
+
});
|
|
388
|
+
export const CreateBundleResponseSchema = v.object({
|
|
389
|
+
bundle: BundleSchema,
|
|
390
|
+
});
|
|
391
|
+
// Membership addressing matches the bundle_items composite key: itemType is
|
|
392
|
+
// part of identity, so detach must carry it too — an itemId alone cannot
|
|
393
|
+
// address a `guide` membership.
|
|
394
|
+
export const BundleMemberTypeSchema = v.picklist(['guide_file', 'guide']);
|
|
395
|
+
export const AttachBundleMemberRequestSchema = v.object({
|
|
396
|
+
itemId: v.pipe(v.string(), v.minLength(1)),
|
|
397
|
+
// Defaults to 'guide_file' server-side.
|
|
398
|
+
itemType: v.optional(BundleMemberTypeSchema),
|
|
399
|
+
});
|
|
400
|
+
export const AttachBundleMemberResponseSchema = v.object({
|
|
401
|
+
// False when the item was already a member — attach is idempotent, and the
|
|
402
|
+
// caller (multi-select menu applying a diff) treats both as success.
|
|
403
|
+
attached: v.boolean(),
|
|
404
|
+
});
|
|
405
|
+
export const DetachBundleMemberResponseSchema = v.object({
|
|
406
|
+
detached: v.boolean(),
|
|
407
|
+
});
|
|
408
|
+
// Rename changes the display name only. The slug is the bundle's identity —
|
|
409
|
+
// repos subscribe by slug in `.mossbear/config.json` → `docs.bundles`, and
|
|
410
|
+
// `mossbear pull <slug>` addresses it — so a slug change would silently break
|
|
411
|
+
// every subscribed repo (context-bundle-authoring-plan.md → D14).
|
|
412
|
+
export const RenameBundleRequestSchema = v.object({
|
|
413
|
+
name: v.pipe(v.string(), v.minLength(1), v.maxLength(200)),
|
|
414
|
+
});
|
|
415
|
+
export const RenameBundleResponseSchema = v.object({
|
|
416
|
+
bundle: BundleSchema,
|
|
417
|
+
});
|
|
418
|
+
export const DeleteBundleResponseSchema = v.object({
|
|
419
|
+
deleted: v.boolean(),
|
|
420
|
+
// `movedToInbox` was removed in `inbox-as-view` slice 2. Deleting a bundle no
|
|
421
|
+
// longer re-parents anything: an item that loses its last membership is
|
|
422
|
+
// unfiled, which is what the inbox view shows, rather than stranded. The
|
|
423
|
+
// field could only ever have reported 0 afterwards, and a number that can
|
|
424
|
+
// never be non-zero is a discriminator carrying no decision. Nothing on the
|
|
425
|
+
// CLI wire parsed it — `parseDeleteBundleResponse` has one caller, the
|
|
426
|
+
// dashboard's own `bundlesApi.ts`.
|
|
427
|
+
});
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// Context item + inbox schemas (context-layer-mcp-inbox-plan.md — Phase 1a)
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// The `type` of an item returned to an agent. A `guide` is a canonical guide
|
|
432
|
+
// row; the rest are guide_files rows (documents, references, assets).
|
|
433
|
+
export const ContextItemTypeSchema = v.picklist([
|
|
434
|
+
'guide',
|
|
435
|
+
'document',
|
|
436
|
+
'reference',
|
|
437
|
+
'asset',
|
|
438
|
+
]);
|
|
439
|
+
// The contentType a caller may *create* through the context layer. `guide` is
|
|
440
|
+
// deliberately absent, and stays absent after slice 2b-B made guides pushable:
|
|
441
|
+
// this schema is reached only by POST (create) and the MCP `push_context`
|
|
442
|
+
// tool's create branch — the PUT body carries no contentType at all
|
|
443
|
+
// (`UpdateContextItemRequestSchema` below). So admitting `guide` here could
|
|
444
|
+
// only legalize a *create*, which the endpoint cannot honor: POST always calls
|
|
445
|
+
// `insertBundleGuideFile`, which writes a standalone guide_files row with
|
|
446
|
+
// `guideId: null` and never the `guides` registration a guide needs, so the
|
|
447
|
+
// caller would get a 201 for something that is not a guide. Updating an
|
|
448
|
+
// existing guide needs nothing from this picklist — it is addressed by
|
|
449
|
+
// `itemId` alone (`guide-versioned-identity-plan.md` → Slice 2b-B, the third
|
|
450
|
+
// Codex constraint, resolved as "restrict to itemId-bearing updates").
|
|
451
|
+
export const PushContentTypeSchema = v.picklist(['document', 'reference', 'asset']);
|
|
452
|
+
// Inbox capture is text or an image today (image upload lands in Phase 2b).
|
|
453
|
+
export const InboxContentTypeSchema = v.picklist(['document', 'asset']);
|
|
454
|
+
/** Max serialized size (UTF-8 bytes) of a pushed or captured item's content. */
|
|
455
|
+
export const MAX_CONTEXT_CONTENT_BYTES = 256 * 1024;
|
|
456
|
+
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`));
|
|
457
|
+
const ContextItemNameSchema = v.pipe(v.string(), v.minLength(1), v.maxLength(200));
|
|
458
|
+
// The wire ceiling for an in-place update — see UpdateContextItemRequestSchema
|
|
459
|
+
// for why it is the guide bound rather than the context bound.
|
|
460
|
+
const UpdatableContentSchema = v.pipe(v.string(), v.check((value) => new TextEncoder().encode(value).length <= MAX_RAW_GUIDE_CONTENT_BYTES, `content must be at most ${MAX_RAW_GUIDE_CONTENT_BYTES} bytes`));
|
|
461
|
+
// The item shape returned to agents. `content` is nullable because an asset
|
|
462
|
+
// stores an object key elsewhere (Phase 2b), not inline text.
|
|
463
|
+
export const ContextItemSchema = v.object({
|
|
464
|
+
id: v.string(),
|
|
465
|
+
type: ContextItemTypeSchema,
|
|
466
|
+
name: v.string(),
|
|
467
|
+
contentType: v.string(),
|
|
468
|
+
content: v.nullable(v.string()),
|
|
469
|
+
origin: v.optional(v.nullable(v.string())),
|
|
470
|
+
// Monotonic content version of the underlying doc (guide_files), surfaced so a
|
|
471
|
+
// caller can echo it back as an expected-version precondition. See
|
|
472
|
+
// doc-version-history-plan.md.
|
|
473
|
+
//
|
|
474
|
+
// A `guide` item reports the version of its **body item** — the guide_files
|
|
475
|
+
// row holding its content (`guide-versioned-identity-plan.md` → Decision 1).
|
|
476
|
+
// That is the row a guide push updates, so the precondition a caller echoes
|
|
477
|
+
// back is checked against the same counter it read. Guides carried no version
|
|
478
|
+
// until slice 2b-B, which is why this stays optional: a guide whose
|
|
479
|
+
// `body_item_id` is still NULL (created by an old client mid-rollout, or
|
|
480
|
+
// imported before slice 1's backfill) has no version to report, and the CLI
|
|
481
|
+
// declines to push it rather than falling back to last-write-wins.
|
|
482
|
+
version: v.optional(v.number()),
|
|
483
|
+
// Scope root and path, mirroring `GuideSnapshotSchema` — the docs phase
|
|
484
|
+
// projects an item onto disk, and without a root it resolved every one of
|
|
485
|
+
// them against the repo checkout. Every member kind carries the pair: a
|
|
486
|
+
// `guide` since #543, and a `guide_file` member — documents included — since
|
|
487
|
+
// `unified-item-sync` put the columns on the wire and `docs-in-place` slice 0
|
|
488
|
+
// made the docs phase land a document at its own declared path rather than in
|
|
489
|
+
// the bundle directory. The same optional/nullable rule applies throughout:
|
|
490
|
+
// absent means "no declared path", which degrades to the origin-derived
|
|
491
|
+
// projection (guides) or the bundle-directory default (documents) rather than
|
|
492
|
+
// failing.
|
|
493
|
+
root: v.optional(v.nullable(GuideDiscoveryRootSchema)),
|
|
494
|
+
path: v.optional(v.nullable(SafeRelativePosixPathSchema)),
|
|
495
|
+
updatedAt: v.string(),
|
|
496
|
+
});
|
|
497
|
+
export const ContextItemsResponseSchema = v.object({
|
|
498
|
+
items: v.array(ContextItemSchema),
|
|
499
|
+
});
|
|
500
|
+
/** Max number of context item ids a push may cite as its transform sources. */
|
|
501
|
+
export const MAX_TRANSFORM_SOURCE_IDS = 100;
|
|
502
|
+
/**
|
|
503
|
+
* The ids of the caller-owned context items a pushed document was produced
|
|
504
|
+
* from (inbox captures, bundle items — any item the caller owns). Shared
|
|
505
|
+
* between the REST contract and the MCP tool's client-side arg validation so
|
|
506
|
+
* the two can't drift.
|
|
507
|
+
*/
|
|
508
|
+
export const SourceItemIdsSchema = v.pipe(v.array(v.pipe(v.string(), v.minLength(1))), v.minLength(1), v.maxLength(MAX_TRANSFORM_SOURCE_IDS));
|
|
509
|
+
// The update-side variant: same element and upper bound, but an empty array is
|
|
510
|
+
// legal and means "clear the stored provenance". See
|
|
511
|
+
// `UpdateContextItemRequestSchema.sourceItemIds` for why the create side keeps
|
|
512
|
+
// the non-empty bound.
|
|
513
|
+
export const UpdatableSourceItemIdsSchema = v.pipe(v.array(v.pipe(v.string(), v.minLength(1))), v.maxLength(MAX_TRANSFORM_SOURCE_IDS));
|
|
514
|
+
// Push a canonical item into a bundle. `itemId` present → update in place (the
|
|
515
|
+
// MCP push maps to PUT); absent → create (maps to POST). The dedicated update
|
|
516
|
+
// schema below is what the PUT route validates.
|
|
517
|
+
//
|
|
518
|
+
// `sourceItemIds` (optional, on create and update alike) cites the context
|
|
519
|
+
// items the pushed document was produced from. It is **provenance**: the server
|
|
520
|
+
// validates the ids against the caller's own items and stores them on the
|
|
521
|
+
// item's metadata. It no longer triggers a second grading pass — that path was
|
|
522
|
+
// removed in `transform-grading-removal`; work an agent does is graded once,
|
|
523
|
+
// through its run.
|
|
524
|
+
export const PushContextRequestSchema = v.object({
|
|
525
|
+
name: ContextItemNameSchema,
|
|
526
|
+
contentType: PushContentTypeSchema,
|
|
527
|
+
content: ContextContentSchema,
|
|
528
|
+
itemId: v.optional(v.string()),
|
|
529
|
+
origin: v.optional(v.nullable(v.string())),
|
|
530
|
+
sourceItemIds: v.optional(SourceItemIdsSchema),
|
|
531
|
+
});
|
|
532
|
+
export const PushContextResponseSchema = v.object({
|
|
533
|
+
item: ContextItemSchema,
|
|
534
|
+
created: v.boolean(),
|
|
535
|
+
});
|
|
536
|
+
// Update a canonical item in place — the edit propagates to every bundle that
|
|
537
|
+
// references it. Both fields are optional so a partial update parses; the route
|
|
538
|
+
// rejects a body that changes nothing.
|
|
539
|
+
export const UpdateContextItemRequestSchema = v.object({
|
|
540
|
+
itemId: v.pipe(v.string(), v.minLength(1)),
|
|
541
|
+
name: v.optional(ContextItemNameSchema),
|
|
542
|
+
// Bounded by the **guide** cap, not the context cap, because an update can
|
|
543
|
+
// target either kind and the two disagree: an imported guide body may be up
|
|
544
|
+
// to `MAX_RAW_GUIDE_CONTENT_BYTES` (512 KiB) while a pushed/captured item
|
|
545
|
+
// caps at `MAX_CONTEXT_CONTENT_BYTES` (256 KiB). Before slice 2b-B that gap
|
|
546
|
+
// was unreachable — guides could not be pushed — and closing it by lowering
|
|
547
|
+
// the import cap would reject guides that already import today, while raising
|
|
548
|
+
// the context cap would double what the context layer accepts for everything.
|
|
549
|
+
// So this is only the wire ceiling (the DoS bound); the *kind-specific* limit
|
|
550
|
+
// is enforced in `updateBundleGuideFileContent`, which is the one place that
|
|
551
|
+
// has already resolved which kind of row it is about to write. Enforcing it
|
|
552
|
+
// in the route instead would be exactly the rule a future caller forgets
|
|
553
|
+
// (Planning Principle 14).
|
|
554
|
+
content: v.optional(UpdatableContentSchema),
|
|
555
|
+
// Optimistic-concurrency precondition: the version the caller last read (from
|
|
556
|
+
// `ContextItemSchema.version`). When present and it no longer matches the
|
|
557
|
+
// current version, the update is rejected with 409 instead of silently
|
|
558
|
+
// clobbering a concurrent write. Omit it to keep last-write-wins behavior.
|
|
559
|
+
expectedVersion: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
|
|
560
|
+
// Replaces the item's stored provenance. An update re-derives the document,
|
|
561
|
+
// so the citation records where the item's *current* body came from; leaving
|
|
562
|
+
// the previous ids in place would describe content that no longer exists.
|
|
563
|
+
// This used to be rejected outright, because a transform was graded once at
|
|
564
|
+
// creation — that reason was deleted with the grader
|
|
565
|
+
// (`transform-grading-removal-plan.md`).
|
|
566
|
+
//
|
|
567
|
+
// Three states, which is why this is not `SourceItemIdsSchema`: **omitted**
|
|
568
|
+
// leaves the stored ids alone (a name-only or provenance-preserving edit),
|
|
569
|
+
// a **non-empty** list replaces them, and **`[]` clears them**. Without the
|
|
570
|
+
// empty case an item rewritten from scratch could never shed provenance that
|
|
571
|
+
// no longer describes it — the silently-wrong record this field's own
|
|
572
|
+
// rationale argues against. `POST` keeps the non-empty bound: there is
|
|
573
|
+
// nothing to clear on a create, so `[]` there would only be a second way to
|
|
574
|
+
// say "no sources" alongside omitting the field.
|
|
575
|
+
sourceItemIds: v.optional(UpdatableSourceItemIdsSchema),
|
|
576
|
+
});
|
|
577
|
+
export const UpdateContextItemResponseSchema = v.object({
|
|
578
|
+
item: ContextItemSchema,
|
|
579
|
+
updated: v.boolean(),
|
|
580
|
+
});
|
|
581
|
+
// The same edit addressed by URL instead of by body field. The item-scoped
|
|
582
|
+
// `PUT /api/v1/context/items/:itemId` takes the id from the path, so carrying it
|
|
583
|
+
// in the body as well would let the two disagree and force the route to pick a
|
|
584
|
+
// winner. Derived with `v.omit` rather than re-declared, so the updatable fields
|
|
585
|
+
// and their bounds cannot drift between the two shapes.
|
|
586
|
+
export const UpdateContextItemBodySchema = v.omit(UpdateContextItemRequestSchema, [
|
|
587
|
+
'itemId',
|
|
588
|
+
]);
|
|
589
|
+
/** A single canonical item, for the item-scoped read route. */
|
|
590
|
+
export const ContextItemResponseSchema = v.object({
|
|
591
|
+
item: ContextItemSchema,
|
|
592
|
+
});
|
|
593
|
+
// `POST /api/v1/context/items/:itemId/archive`. A boolean, not a count: the
|
|
594
|
+
// route addresses exactly one item and answers 404 when it does not resolve, so
|
|
595
|
+
// there is no "succeeded on 0 of 1" state left for a number to express. That
|
|
596
|
+
// ambiguity is the bug this route exists to remove — the batch inbox endpoint
|
|
597
|
+
// answered `200 {archived: 0}` for an item it declined to touch
|
|
598
|
+
// (`inbox-as-view-plan.md` → Evidence).
|
|
599
|
+
export const ArchiveContextItemResponseSchema = v.object({
|
|
600
|
+
archived: v.boolean(),
|
|
601
|
+
item: ContextItemSchema,
|
|
602
|
+
});
|
|
603
|
+
export const InboxCaptureRequestSchema = v.object({
|
|
604
|
+
contentType: InboxContentTypeSchema,
|
|
605
|
+
content: ContextContentSchema,
|
|
606
|
+
name: v.optional(ContextItemNameSchema),
|
|
607
|
+
source: v.optional(v.string()),
|
|
608
|
+
});
|
|
609
|
+
// An inbox item carries capture provenance on top of the base item shape.
|
|
610
|
+
// `capturedAt` is the underlying row's createdAt, so "last 24h" is a filter on it.
|
|
611
|
+
export const InboxItemSchema = v.object({
|
|
612
|
+
id: v.string(),
|
|
613
|
+
type: ContextItemTypeSchema,
|
|
614
|
+
name: v.string(),
|
|
615
|
+
contentType: v.string(),
|
|
616
|
+
content: v.nullable(v.string()),
|
|
617
|
+
source: v.optional(v.nullable(v.string())),
|
|
618
|
+
// Monotonic content version of the underlying doc, mirroring
|
|
619
|
+
// `ContextItemSchema.version` — an inbox item is a guide_file too, so a caller
|
|
620
|
+
// that edits it via PUT can echo this back as an expected-version precondition.
|
|
621
|
+
version: v.optional(v.number()),
|
|
622
|
+
capturedAt: v.string(),
|
|
623
|
+
updatedAt: v.string(),
|
|
624
|
+
});
|
|
625
|
+
export const InboxCaptureResponseSchema = v.object({
|
|
626
|
+
item: InboxItemSchema,
|
|
627
|
+
});
|
|
628
|
+
export const InboxListResponseSchema = v.object({
|
|
629
|
+
items: v.array(InboxItemSchema),
|
|
630
|
+
});
|
|
631
|
+
/** Max number of ids accepted in a single `POST /api/v1/context/inbox/archive` request. */
|
|
632
|
+
export const MAX_INBOX_ARCHIVE_BATCH = 500;
|
|
633
|
+
export const InboxArchiveRequestSchema = v.object({
|
|
634
|
+
ids: v.pipe(v.array(v.pipe(v.string(), v.minLength(1))), v.minLength(1), v.maxLength(MAX_INBOX_ARCHIVE_BATCH)),
|
|
635
|
+
});
|
|
636
|
+
export const InboxArchiveResponseSchema = v.object({
|
|
637
|
+
archived: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
638
|
+
});
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
// Doc version history (read API)
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
// One append-only saved version of a context doc (guide_file). Rows are served
|
|
643
|
+
// lazily over REST and never sync through Zero — syncing them would grow every
|
|
644
|
+
// client's initial-sync payload forever. See doc-version-history-plan.md.
|
|
645
|
+
//
|
|
646
|
+
// `authorType` is intentionally an open string, not a picklist: the server owns
|
|
647
|
+
// the closed set ('user' | 'agent' | 'cli'), but a deployed client parses this
|
|
648
|
+
// response strictly, and a picklist would turn any future author kind into a
|
|
649
|
+
// hard parse error on an otherwise valid history read.
|
|
650
|
+
export const ContextItemVersionSchema = v.object({
|
|
651
|
+
id: v.string(),
|
|
652
|
+
// Monotonic per doc; the newest version equals the item's current `version`.
|
|
653
|
+
version: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
654
|
+
contentHash: v.string(),
|
|
655
|
+
authorType: v.string(),
|
|
656
|
+
// Non-secret provenance the write path recorded (e.g. bundle slug, capture
|
|
657
|
+
// source). Never a bearer secret — these rows outlive tokens and ride export.
|
|
658
|
+
metadata: v.optional(v.nullable(v.record(v.string(), v.unknown()))),
|
|
659
|
+
createdAt: v.string(),
|
|
660
|
+
});
|
|
661
|
+
// The full version including the snapshotted body, returned by the per-version
|
|
662
|
+
// content fetch. `content` is nullable to stay blob-tier compatible: a future
|
|
663
|
+
// encrypted/blob version stores a hash pointer, not inline text.
|
|
664
|
+
export const ContextItemVersionDetailSchema = v.object({
|
|
665
|
+
id: v.string(),
|
|
666
|
+
version: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
667
|
+
content: v.nullable(v.string()),
|
|
668
|
+
contentHash: v.string(),
|
|
669
|
+
authorType: v.string(),
|
|
670
|
+
metadata: v.optional(v.nullable(v.record(v.string(), v.unknown()))),
|
|
671
|
+
createdAt: v.string(),
|
|
672
|
+
});
|
|
673
|
+
// Newest-first page of a doc's version summaries. `nextCursor` is the version
|
|
674
|
+
// number to pass back as `?before=` for the following (older) page, or null
|
|
675
|
+
// once the oldest version has been returned.
|
|
676
|
+
export const ContextItemVersionsResponseSchema = v.object({
|
|
677
|
+
versions: v.array(ContextItemVersionSchema),
|
|
678
|
+
nextCursor: v.nullable(v.number()),
|
|
679
|
+
});
|
|
680
|
+
export const ContextItemVersionResponseSchema = v.object({
|
|
681
|
+
version: ContextItemVersionDetailSchema,
|
|
682
|
+
});
|
|
683
|
+
/** Default and max page size for the version-history list endpoint. */
|
|
684
|
+
export const DEFAULT_VERSIONS_LIMIT = 50;
|
|
685
|
+
export const MAX_VERSIONS_LIMIT = 200;
|
|
686
|
+
// ---------------------------------------------------------------------------
|
|
687
|
+
// Sync payload schemas
|
|
688
|
+
// ---------------------------------------------------------------------------
|
|
689
|
+
const DashboardSyncTierSchema = v.picklist([
|
|
690
|
+
'pattern_match',
|
|
691
|
+
'cached',
|
|
692
|
+
'llm',
|
|
693
|
+
'batch',
|
|
694
|
+
'unguided',
|
|
695
|
+
]);
|
|
696
|
+
const DashboardGuideSuggestionStatusSchema = v.picklist([
|
|
697
|
+
'pending',
|
|
698
|
+
'accepted',
|
|
699
|
+
'dismissed',
|
|
700
|
+
]);
|
|
701
|
+
const DashboardFeedbackCandidateStatusSchema = v.picklist([
|
|
702
|
+
'inferred',
|
|
703
|
+
'confirmed',
|
|
704
|
+
'dismissed',
|
|
705
|
+
]);
|
|
706
|
+
const DashboardFeedbackSignalTypeSchema = v.picklist([
|
|
707
|
+
'correction',
|
|
708
|
+
'approval',
|
|
709
|
+
'implied_preference',
|
|
710
|
+
'stated_preference',
|
|
711
|
+
'skill_suggestion',
|
|
712
|
+
]);
|
|
713
|
+
export const ProposalCitationSchema = v.object({
|
|
714
|
+
sessionId: v.string(),
|
|
715
|
+
timestamp: v.optional(v.string()),
|
|
716
|
+
summary: v.string(),
|
|
717
|
+
});
|
|
718
|
+
/** Max serialized size of an action's `metadata` field — bounds storage bloat. */
|
|
719
|
+
export const MAX_ACTION_METADATA_BYTES = 16 * 1024;
|
|
720
|
+
/**
|
|
721
|
+
* Longest accepted CLI version string. Releases are `x.y.z` /
|
|
722
|
+
* `x.y.z-<channel>.<n>`, so this is generous — it exists because the value is
|
|
723
|
+
* client-supplied and lands on every action row, not to constrain the format
|
|
724
|
+
* (ordering already fails closed on anything unparseable).
|
|
725
|
+
*/
|
|
726
|
+
const MAX_CLI_VERSION_LENGTH = 64;
|
|
727
|
+
const CliVersionSchema = v.pipe(v.string(), v.nonEmpty(), v.maxLength(MAX_CLI_VERSION_LENGTH));
|
|
728
|
+
/**
|
|
729
|
+
* Identity of the skill an agent loaded, captured by the logger hook from a
|
|
730
|
+
* `Skill` tool call (docs/plans/run-level-grading-plan.md, Slice 0).
|
|
731
|
+
*
|
|
732
|
+
* Two fields because the hook can only ever be sure of one of them:
|
|
733
|
+
*
|
|
734
|
+
* - `name` is `tool_input.skill` verbatim — always present, never qualified.
|
|
735
|
+
* Two skills that share a directory name (`.claude/skills/review` and
|
|
736
|
+
* `.agents/skills/review`) are indistinguishable by it, which is review
|
|
737
|
+
* finding 5 on PR #387.
|
|
738
|
+
* - `path` is the skill's *directory*, relative to the discovery root the
|
|
739
|
+
* guide import walks and in the same posix form its `origin` carries
|
|
740
|
+
* (`.claude/skills/review`). This is the qualified identity: it is exactly
|
|
741
|
+
* `guides.origin`'s path with the trailing `/SKILL.md` removed, so the two
|
|
742
|
+
* sides of the join are the same string rather than two derivations of it.
|
|
743
|
+
* Optional because the hook resolves it by probing the filesystem: a skill
|
|
744
|
+
* that lives somewhere the probe does not look (a plugin skill, a
|
|
745
|
+
* `~/.claude/skills` skill, a session whose cwd is not the repo root) has a
|
|
746
|
+
* name and no path, and matching falls back to the name.
|
|
747
|
+
*/
|
|
748
|
+
export const ActionSkillLoadSchema = v.object({
|
|
749
|
+
name: NonEmptyStringSchema,
|
|
750
|
+
path: v.optional(NonEmptyStringSchema),
|
|
751
|
+
});
|
|
752
|
+
/**
|
|
753
|
+
* The `actions.action_type` a skill load is stored under.
|
|
754
|
+
*
|
|
755
|
+
* Deliberately **not** a member of `KNOWN_ACTION_TYPES` below, even though the
|
|
756
|
+
* CLI emits it on purpose. A skill load is evidence about what was in the
|
|
757
|
+
* agent's *context*, not a piece of work anyone should grade — it is an input to
|
|
758
|
+
* `inContextGuides`, never a row the grader judges (`gradeableActions.ts`
|
|
759
|
+
* filters it out). Keeping it out of the known-vocabulary list is what stops it
|
|
760
|
+
* being treated as gradeable work by anything that enumerates that list.
|
|
761
|
+
*/
|
|
762
|
+
export const SKILL_LOAD_ACTION_TYPE = 'skill_load';
|
|
763
|
+
/**
|
|
764
|
+
* A machine-shaped label for *why* a tool call failed — `ENOENT`, `TimeoutError`,
|
|
765
|
+
* `permission_denied`.
|
|
766
|
+
*
|
|
767
|
+
* Deliberately not free text. An error *message* is content: it quotes file
|
|
768
|
+
* contents, stderr and third-party data, which is exactly the class
|
|
769
|
+
* `content-sync-privacy` gates and `sanitizeCommand` cannot redact (it targets
|
|
770
|
+
* shell syntax, not prose). A class is metadata, so it rides the same
|
|
771
|
+
* unblocked path as the exit code. The character set admits identifiers and
|
|
772
|
+
* dotted/namespaced codes and nothing else; anything failing it is dropped
|
|
773
|
+
* rather than truncated, because half an error class is not a smaller fact, it
|
|
774
|
+
* is a different one.
|
|
775
|
+
*/
|
|
776
|
+
export const ActionErrorClassSchema = v.pipe(v.string(), v.regex(/^[A-Za-z0-9_.:-]{1,32}$/, 'errorClass must be 1-32 chars of [A-Za-z0-9_.:-]'));
|
|
777
|
+
/**
|
|
778
|
+
* Whether a logged tool call actually succeeded
|
|
779
|
+
* (docs/plans/action-evidence-sync-plan.md — the outcome-metadata slice).
|
|
780
|
+
*
|
|
781
|
+
* The logger hook parsed `tool_input` and dropped the `tool_response` beside
|
|
782
|
+
* it, so every action reached the grader looking like it had happened. A failed
|
|
783
|
+
* edit and a successful one rendered identically, and the run grader has no
|
|
784
|
+
* other way to tell them apart.
|
|
785
|
+
*
|
|
786
|
+
* **Absent means unknown, never success.** The hook emits this only from a
|
|
787
|
+
* response shape it recognizes (`extractActionOutcome`), and agent tool
|
|
788
|
+
* responses vary per tool and per platform; a guess here would be worse than
|
|
789
|
+
* silence, because the grader would weigh it. `status` is therefore the only
|
|
790
|
+
* required field, and the whole object is optional everywhere it appears.
|
|
791
|
+
*/
|
|
792
|
+
export const ActionOutcomeSchema = v.object({
|
|
793
|
+
status: v.picklist(['ok', 'error']),
|
|
794
|
+
errorClass: v.optional(ActionErrorClassSchema),
|
|
795
|
+
/**
|
|
796
|
+
* Bounded to a signed 16-bit range, which covers every real exit status
|
|
797
|
+
* (0-255, 128+signal, and Node's -1) with room to spare. The bound is a line
|
|
798
|
+
* length bound as much as a validity one: this renders into the grading
|
|
799
|
+
* prompt, where every field is capped.
|
|
800
|
+
*/
|
|
801
|
+
exitCode: v.optional(v.pipe(v.number(), v.integer(), v.minValue(-32_768), v.maxValue(32_767))),
|
|
802
|
+
});
|
|
803
|
+
/**
|
|
804
|
+
* {@link ActionOutcomeSchema} with unknown keys refused rather than ignored.
|
|
805
|
+
*
|
|
806
|
+
* Used only by `ActionMetadataSchema`'s boundary check, where the difference
|
|
807
|
+
* matters: see the comment there. Readers parse with the permissive schema,
|
|
808
|
+
* which strips unknown keys on output the way `v.object` always has.
|
|
809
|
+
*/
|
|
810
|
+
const StrictActionOutcomeSchema = v.strictObject(ActionOutcomeSchema.entries);
|
|
811
|
+
/**
|
|
812
|
+
* Longest narration field accepted on the wire.
|
|
813
|
+
*
|
|
814
|
+
* Matches `MAX_PROSE_CHARS` in `@mossbear/eval-core`, which is the cap the
|
|
815
|
+
* client-side redactor already applies. Duplicated as a literal rather than
|
|
816
|
+
* imported because the dependency runs the other way — `@mossbear/eval-core`
|
|
817
|
+
* depends on this package — and inverting it to share one constant would make
|
|
818
|
+
* the wire contract depend on the evaluation package. The number is a bound, so
|
|
819
|
+
* a drift between them fails safe in the direction that matters: the server
|
|
820
|
+
* refusing something the client would have sent, never the reverse.
|
|
821
|
+
*/
|
|
822
|
+
export const MAX_ACTION_NARRATION_CHARS = 2000;
|
|
823
|
+
/**
|
|
824
|
+
* A code fence at the start of a line, allowing Markdown container prefixes.
|
|
825
|
+
*
|
|
826
|
+
* The exact structural inverse of what `stripCodeBlocks` guarantees: it removes
|
|
827
|
+
* every fenced block by this same rule, so a narration field reaching the server
|
|
828
|
+
* with a line-initial fence in it *cannot* have been produced by a client that
|
|
829
|
+
* ran the pipeline. Rejecting is therefore free of false positives — inline
|
|
830
|
+
* `` `code` `` spans and a mid-sentence "wrap it in ```" are untouched, because
|
|
831
|
+
* neither begins a line.
|
|
832
|
+
*
|
|
833
|
+
* **What this does and does not buy.** It catches the realistic failure — a
|
|
834
|
+
* client whose stripping broke, or one built against the wire schema without
|
|
835
|
+
* reading the class table — before its output lands in a column. It is not a
|
|
836
|
+
* defense against a determined client, which would simply send the same source
|
|
837
|
+
* with no fence around it; the server cannot tell prose from code in general,
|
|
838
|
+
* which is exactly why the class table strips whole *structures* rather than
|
|
839
|
+
* asking a scrubber to recognize code. Claiming more than this would be the
|
|
840
|
+
* "alignment theater" the charter's fourth commitment names.
|
|
841
|
+
*/
|
|
842
|
+
const LINE_INITIAL_FENCE = /^[ \t]*(?:>[ \t]?)*(?:`{3,}|~{3,})/m;
|
|
843
|
+
const NarrationTextSchema = v.pipe(v.string(), v.maxLength(MAX_ACTION_NARRATION_CHARS), v.check((value) => !LINE_INITIAL_FENCE.test(value), 'narration must not contain a code block — code is stripped structurally on the client before upload'));
|
|
844
|
+
/**
|
|
845
|
+
* Transcript evidence attached to an action
|
|
846
|
+
* (docs/plans/transcript-content-classes-plan.md — the founder's class call of
|
|
847
|
+
* 2026-08-13).
|
|
848
|
+
*
|
|
849
|
+
* **Two fields, and the omissions are the specification.** The class table
|
|
850
|
+
* decides that user turns and assistant *prose* sync prose-redacted, assistant
|
|
851
|
+
* code blocks are stripped to a language-and-line-count placeholder, and
|
|
852
|
+
* tool-result bodies **never** sync. There is therefore no field here for a
|
|
853
|
+
* tool result, no field for a code-block body, and no field for a diff.
|
|
854
|
+
*
|
|
855
|
+
* **`strictObject`, so a body that carries one is rejected rather than
|
|
856
|
+
* trimmed.** This is the same decision `StrictActionOutcomeSchema` above makes
|
|
857
|
+
* and for a sharper version of the same reason. A permissive `v.object` strips
|
|
858
|
+
* unknown keys on output, which would turn a client sending
|
|
859
|
+
* `narration.toolResult` into a *silently accepted* request: the sender is told
|
|
860
|
+
* it worked, learns nothing, and keeps sending. Trimming is a policy that fails
|
|
861
|
+
* open — it makes an over-sharing client indistinguishable from a correct one.
|
|
862
|
+
* Rejecting makes the class table enforceable against a client the server does
|
|
863
|
+
* not control, which is the only kind of enforcement worth having here; the
|
|
864
|
+
* CLI's own choke point (`packages/cli/src/transcript/narration.ts`) is the
|
|
865
|
+
* first line, not the last.
|
|
866
|
+
*
|
|
867
|
+
* The forward-compatibility cost is the same as `outcome`'s, and so is the
|
|
868
|
+
* ordering that pays it: widen this schema and deploy the server before any
|
|
869
|
+
* CLI starts sending a new narration field.
|
|
870
|
+
*/
|
|
871
|
+
export const ActionNarrationSchema = v.strictObject({
|
|
872
|
+
/**
|
|
873
|
+
* The user turn this action followed — prose-redacted on the user's machine.
|
|
874
|
+
*
|
|
875
|
+
* **Not every message with `role: 'user'`.** A tool result is delivered as a
|
|
876
|
+
* user-role message whose content is a `tool_result` block, so the client
|
|
877
|
+
* selects on content-block type rather than role
|
|
878
|
+
* (`packages/cli/src/transcript/messages.ts`). Nothing on this side can tell
|
|
879
|
+
* the difference after the fact, which is why the client selects
|
|
880
|
+
* structurally and this schema constrains the shape rather than the content.
|
|
881
|
+
*/
|
|
882
|
+
userText: v.optional(NarrationTextSchema),
|
|
883
|
+
/**
|
|
884
|
+
* Assistant prose adjacent to the tool call — code blocks structurally
|
|
885
|
+
* removed, then prose-redacted, then windowed, all on the user's machine.
|
|
886
|
+
*/
|
|
887
|
+
assistantText: v.optional(NarrationTextSchema),
|
|
888
|
+
});
|
|
889
|
+
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`),
|
|
890
|
+
// `metadata` is an open record by design (platform-specific context), but the
|
|
891
|
+
// one key the server *reads* has to be trustworthy: `skill` is what resolves
|
|
892
|
+
// a run's loaded skills, so a malformed one must be rejected at the boundary
|
|
893
|
+
// rather than silently ignored downstream.
|
|
894
|
+
v.check((value) => value.skill === undefined || v.is(ActionSkillLoadSchema, value.skill), 'metadata.skill must be { name: string, path?: string }'), v.check((value) => value.repoId === undefined || v.is(RepoIdSchema, value.repoId), 'metadata.repoId must be repo_ followed by 32 hex chars'),
|
|
895
|
+
// Same reasoning as `skill` above: the server reads this one, so a malformed
|
|
896
|
+
// outcome is rejected at the boundary rather than reaching the grader as a
|
|
897
|
+
// half-parsed fact about whether the action succeeded.
|
|
898
|
+
//
|
|
899
|
+
// `v.is` only answers yes/no — it does not substitute the schema's stripped
|
|
900
|
+
// output — and the surrounding `metadata` is an open record that keeps what
|
|
901
|
+
// it was given. A permissive object check would therefore *store* an
|
|
902
|
+
// unrecognized `outcome.message`, which is exactly the free-text error prose
|
|
903
|
+
// this field exists to keep off the wire. `strictObject` is what makes the
|
|
904
|
+
// check mean what it says.
|
|
905
|
+
//
|
|
906
|
+
// The forward-compatibility cost is real and the ordering that pays it:
|
|
907
|
+
// widen this schema (server, deployed first) before any CLI starts sending a
|
|
908
|
+
// new outcome field, or an un-upgraded server rejects the whole action.
|
|
909
|
+
v.check((value) => value.outcome === undefined || v.is(StrictActionOutcomeSchema, value.outcome), 'metadata.outcome must be { status: "ok" | "error", errorClass?: string, exitCode?: number } with no other keys'));
|
|
910
|
+
// Token usage extracted from an agent's own transcript (e.g. Claude Code
|
|
911
|
+
// JSONL), keyed by model. See docs/plans/token-cost-tracking-plan.md — Phase 1
|
|
912
|
+
// stores raw token counts only; cost is derived server-side in Phase 2.
|
|
913
|
+
export const TokenUsageSchema = v.object({
|
|
914
|
+
model: NonEmptyStringSchema,
|
|
915
|
+
provider: v.optional(v.string()),
|
|
916
|
+
inputTokens: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
917
|
+
outputTokens: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
918
|
+
cacheReadTokens: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
919
|
+
cacheWriteTokens: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
920
|
+
});
|
|
921
|
+
export const DashboardRunUsageSchema = v.object({
|
|
922
|
+
runId: v.optional(v.string()),
|
|
923
|
+
sessionId: NonEmptyStringSchema,
|
|
924
|
+
usage: v.array(TokenUsageSchema),
|
|
925
|
+
});
|
|
926
|
+
// Per-skill Tier-1 "load overhead" token usage extracted from an agent's own
|
|
927
|
+
// transcript (docs/plans/token-cost-tracking-plan.md §13.3) — the precise part
|
|
928
|
+
// of skill-cost attribution: the tokens billed to load a Skill/SKILL.md into
|
|
929
|
+
// context, approximated in v1 as the first-load message's usage. Tier 2
|
|
930
|
+
// (heuristic guided-work spans) is not implemented yet.
|
|
931
|
+
export const SkillUsageEntrySchema = v.object({
|
|
932
|
+
skillName: NonEmptyStringSchema,
|
|
933
|
+
model: NonEmptyStringSchema,
|
|
934
|
+
inputTokens: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
935
|
+
cacheWriteTokens: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
936
|
+
});
|
|
937
|
+
export const DashboardSkillUsageSchema = v.object({
|
|
938
|
+
runId: v.optional(v.string()),
|
|
939
|
+
sessionId: NonEmptyStringSchema,
|
|
940
|
+
usage: v.array(SkillUsageEntrySchema),
|
|
941
|
+
});
|
|
942
|
+
/**
|
|
943
|
+
* Bounds on the action fields that reach an LLM prompt verbatim.
|
|
944
|
+
*
|
|
945
|
+
* Every one of these is interpolated into the run grader's action log
|
|
946
|
+
* (`apps/dashboard/src/lib/eval/runGradePrompt.ts`), so an unbounded value is
|
|
947
|
+
* not just storage bloat: it is an authenticated client deciding how much
|
|
948
|
+
* context one nominally-capped action consumes, or making the provider call
|
|
949
|
+
* fail on length over and over while the run stays retryable. The prompt caps
|
|
950
|
+
* them again on the way out — these stop the oversized value being *stored*,
|
|
951
|
+
* which is the only place a cap holds for rows the prompt never renders.
|
|
952
|
+
*
|
|
953
|
+
* Sized well above anything the CLI produces: summaries are capped at 200
|
|
954
|
+
* characters before they leave the machine (`MAX_SUMMARY_CHARS`,
|
|
955
|
+
* `packages/cli/src/action-queue.ts:31`), action types are derived from tool
|
|
956
|
+
* names, and `filePaths` carries at most one entry per action
|
|
957
|
+
* (`toSyncAction`, `packages/cli/src/action-queue.ts:135-147`).
|
|
958
|
+
*/
|
|
959
|
+
const MAX_ACTION_ID_LENGTH = 128;
|
|
960
|
+
const MAX_ACTION_TYPE_LENGTH = 128;
|
|
961
|
+
const MAX_ACTION_SUMMARY_LENGTH = 4_000;
|
|
962
|
+
const MAX_ACTION_FILE_PATHS = 100;
|
|
963
|
+
const MAX_ACTION_FILE_PATH_LENGTH = 1_024;
|
|
964
|
+
/**
|
|
965
|
+
* An action id as it may appear in a prompt: bounded, and free of the control
|
|
966
|
+
* characters that would let it break out of the line it is rendered on. Ids are
|
|
967
|
+
* the one field the grader reads back and cites, so they cannot be truncated or
|
|
968
|
+
* sanitized after the fact without silently detaching every citation that used
|
|
969
|
+
* them — the boundary is the only place to reject a bad one.
|
|
970
|
+
*/
|
|
971
|
+
const ActionIdSchema = v.pipe(v.string(), v.maxLength(MAX_ACTION_ID_LENGTH), v.regex(/^[^\p{Cc}\p{Cf}\s]*$/u, 'id must not contain whitespace or control characters'));
|
|
972
|
+
export const DashboardSyncActionSchema = v.object({
|
|
973
|
+
id: v.optional(ActionIdSchema),
|
|
974
|
+
adapterId: v.optional(v.string()),
|
|
975
|
+
runId: v.optional(v.string()),
|
|
976
|
+
/**
|
|
977
|
+
* Optional on the wire, but **required to be graded**: runs are the only
|
|
978
|
+
* grading unit, and an action with no session joins no run
|
|
979
|
+
* (docs/plans/run-level-grading-plan.md). The field stays optional because
|
|
980
|
+
* tightening it would 400 a whole batch — a poison pill for any queue file
|
|
981
|
+
* carrying one — and no shipped CLI omits it: the hook always writes a
|
|
982
|
+
* session id, falling back to the literal `'unknown'`
|
|
983
|
+
* (`packages/cli/src/hook-logger.ts:129`), which groups into a run like any
|
|
984
|
+
* other. A direct API caller that omits it gets its actions stored and
|
|
985
|
+
* ungraded, and the server logs `sync.actions_without_session` so the case is
|
|
986
|
+
* visible rather than silent.
|
|
987
|
+
*/
|
|
988
|
+
sessionId: v.optional(v.string()),
|
|
989
|
+
actionType: v.pipe(v.string(), v.maxLength(MAX_ACTION_TYPE_LENGTH)),
|
|
990
|
+
summary: v.pipe(v.string(), v.maxLength(MAX_ACTION_SUMMARY_LENGTH)),
|
|
991
|
+
filePaths: v.optional(v.pipe(v.array(v.pipe(v.string(), v.maxLength(MAX_ACTION_FILE_PATH_LENGTH))), v.maxLength(MAX_ACTION_FILE_PATHS))),
|
|
992
|
+
verdict: v.optional(VerdictSchema),
|
|
993
|
+
gradingTier: v.optional(DashboardSyncTierSchema),
|
|
994
|
+
guideIds: v.optional(v.array(v.string())),
|
|
995
|
+
violatedGuideId: v.optional(v.nullable(v.string())),
|
|
996
|
+
evalReasoning: v.optional(v.nullable(v.string())),
|
|
997
|
+
metadata: v.optional(v.nullable(ActionMetadataSchema)),
|
|
998
|
+
/**
|
|
999
|
+
* Transcript evidence for this action. See {@link ActionNarrationSchema}.
|
|
1000
|
+
*
|
|
1001
|
+
* A top-level field rather than a `metadata` key, and that is a decision
|
|
1002
|
+
* rather than a style choice (design review, 2026-08-15): `metadata` is an
|
|
1003
|
+
* open record holding *metadata* under one shared 16 KB budget, and this is
|
|
1004
|
+
* content — the first content an action carries. It lands in its own column
|
|
1005
|
+
* server-side for the same reason.
|
|
1006
|
+
*
|
|
1007
|
+
* **No shipped CLI sends this yet.** Slice 2 builds the extraction and both
|
|
1008
|
+
* halves of the enforcement; Slice 3 adds the column and the disclosure
|
|
1009
|
+
* surface and is what turns it on, so nothing begins leaving a user's machine
|
|
1010
|
+
* a release before the panel describing it.
|
|
1011
|
+
*/
|
|
1012
|
+
narration: v.optional(ActionNarrationSchema),
|
|
1013
|
+
/**
|
|
1014
|
+
* Version of the CLI that *produced* this action's verdict.
|
|
1015
|
+
*
|
|
1016
|
+
* **No shipped CLI sends this any more.** Its only writer was `mossbear verdict
|
|
1017
|
+
* record`, whose queue files could outlive an upgrade, which is why the
|
|
1018
|
+
* per-action version was distinguished from the request-level one at all.
|
|
1019
|
+
* That command is gone (docs/plans/cli-local-grading-removal-plan.md) and no
|
|
1020
|
+
* client-side verdict exists to attribute.
|
|
1021
|
+
*
|
|
1022
|
+
* Kept on the wire for one release so an un-upgraded CLI's queued verdicts
|
|
1023
|
+
* are not rejected mid-migration; the field and the `actions.cli_version`
|
|
1024
|
+
* column it feeds are dropped together (`action-cli-version-column-drop`).
|
|
1025
|
+
* For the CLI making the request — which is live, and what
|
|
1026
|
+
* `enforceCliVersionFloor` reads — see `DashboardSyncRequestSchema`.
|
|
1027
|
+
*/
|
|
1028
|
+
cliVersion: v.optional(CliVersionSchema),
|
|
1029
|
+
});
|
|
1030
|
+
export const DashboardGuideSuggestionSchema = v.object({
|
|
1031
|
+
id: v.optional(v.string()),
|
|
1032
|
+
content: v.string(),
|
|
1033
|
+
reasoning: v.optional(v.string()),
|
|
1034
|
+
triggerType: v.picklist([
|
|
1035
|
+
'uncovered',
|
|
1036
|
+
'ambiguous',
|
|
1037
|
+
'performance_pattern',
|
|
1038
|
+
'conversational_feedback',
|
|
1039
|
+
]),
|
|
1040
|
+
sourceActionIds: v.optional(v.array(v.union([v.string(), ProposalCitationSchema]))),
|
|
1041
|
+
status: v.optional(DashboardGuideSuggestionStatusSchema),
|
|
1042
|
+
});
|
|
1043
|
+
export const DashboardFeedbackCandidateSchema = v.object({
|
|
1044
|
+
id: v.optional(v.string()),
|
|
1045
|
+
signal: v.string(),
|
|
1046
|
+
signalType: DashboardFeedbackSignalTypeSchema,
|
|
1047
|
+
confidence: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(1))),
|
|
1048
|
+
occurrences: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
|
|
1049
|
+
status: v.optional(DashboardFeedbackCandidateStatusSchema),
|
|
1050
|
+
});
|
|
1051
|
+
/** Max number of actions accepted in a single `POST /api/sync` request. */
|
|
1052
|
+
export const MAX_SYNC_ACTIONS_BATCH = 500;
|
|
1053
|
+
/** Max number of guideSuggestions/feedbackCandidates/runUsage entries per sync request. */
|
|
1054
|
+
export const MAX_SYNC_AUX_BATCH = 100;
|
|
1055
|
+
/**
|
|
1056
|
+
* Max rule files one delivery manifest enumerates. Well above what a real repo
|
|
1057
|
+
* carries (this one: ~20), and bounded because the manifest rides every
|
|
1058
|
+
* action-carrying batch: an unbounded list would multiply across a chunked
|
|
1059
|
+
* sync. Overflow sets `truncated` rather than being dropped silently.
|
|
1060
|
+
*/
|
|
1061
|
+
export const MAX_RULE_MANIFEST_FILES = 500;
|
|
1062
|
+
/**
|
|
1063
|
+
* A rule file observed on disk in the repo `mossbear sync` ran from.
|
|
1064
|
+
*
|
|
1065
|
+
* `path` is relative to that repo root, in exactly the form `payloadPath`
|
|
1066
|
+
* takes at import time (`packages/cli/src/rules/discover.ts`), so
|
|
1067
|
+
* `file:{format}:{path}` reconstructs the `origin` the dashboard stored for
|
|
1068
|
+
* the guide this file produced. That reconstruction is the whole join; nothing
|
|
1069
|
+
* else about the file is needed and nothing else is sent.
|
|
1070
|
+
*
|
|
1071
|
+
* **Never any content.** `contentHash` is the same sha256 the import payload
|
|
1072
|
+
* carries, present so a later slice can tell "delivered" from "delivered but
|
|
1073
|
+
* edited since import" without shipping the bytes again.
|
|
1074
|
+
*/
|
|
1075
|
+
export const RuleManifestFileSchema = v.object({
|
|
1076
|
+
path: v.pipe(v.string(), v.minLength(1), v.maxLength(512)),
|
|
1077
|
+
format: GuideFileFormatSchema,
|
|
1078
|
+
contentHash: Sha256HexSchema,
|
|
1079
|
+
});
|
|
1080
|
+
/**
|
|
1081
|
+
* Which rule files were actually on disk in the repo this sync ran from —
|
|
1082
|
+
* observed delivery, replacing the assumption that every non-skill active
|
|
1083
|
+
* guide reached the agent (docs/plans/run-level-grading-plan.md, review
|
|
1084
|
+
* finding 3). Without it, an account with several repos, or a
|
|
1085
|
+
* dashboard-authored guide never exported anywhere, is graded against
|
|
1086
|
+
* instructions the agent never received.
|
|
1087
|
+
*
|
|
1088
|
+
* `repoId` is an opaque, machine-local digest — deliberately not a path, a
|
|
1089
|
+
* remote URL, or a repo name. Its only job is to keep two checkouts on the
|
|
1090
|
+
* same account distinguishable, which is the half of review finding 5 that
|
|
1091
|
+
* repo-root-relative origins cannot express: two repos each holding
|
|
1092
|
+
* `.claude/skills/review` produce byte-identical origins by construction. See
|
|
1093
|
+
* `repoIdFromRoot` (`packages/cli/src/rules/manifest.ts`) for how it is
|
|
1094
|
+
* derived and why that derivation is privacy-safe.
|
|
1095
|
+
*/
|
|
1096
|
+
export const RuleFileManifestSchema = v.object({
|
|
1097
|
+
repoId: RepoIdSchema,
|
|
1098
|
+
files: v.pipe(v.array(RuleManifestFileSchema), v.maxLength(MAX_RULE_MANIFEST_FILES)),
|
|
1099
|
+
/**
|
|
1100
|
+
* The repo held more rule files than the cap, so `files` is a prefix. Set
|
|
1101
|
+
* rather than silently trimmed: a manifest read as complete would mark the
|
|
1102
|
+
* omitted files' guides undelivered, which is the exact wrong-verdict
|
|
1103
|
+
* failure this field exists to prevent.
|
|
1104
|
+
*/
|
|
1105
|
+
truncated: v.optional(v.boolean()),
|
|
1106
|
+
});
|
|
1107
|
+
export const DashboardSyncRequestSchema = v.object({
|
|
1108
|
+
actions: v.optional(v.pipe(v.array(DashboardSyncActionSchema), v.maxLength(MAX_SYNC_ACTIONS_BATCH))),
|
|
1109
|
+
guideSuggestions: v.optional(v.pipe(v.array(DashboardGuideSuggestionSchema), v.maxLength(MAX_SYNC_AUX_BATCH))),
|
|
1110
|
+
feedbackCandidates: v.optional(v.pipe(v.array(DashboardFeedbackCandidateSchema), v.maxLength(MAX_SYNC_AUX_BATCH))),
|
|
1111
|
+
runUsage: v.optional(v.pipe(v.array(DashboardRunUsageSchema), v.maxLength(MAX_SYNC_AUX_BATCH))),
|
|
1112
|
+
skillUsage: v.optional(v.pipe(v.array(DashboardSkillUsageSchema), v.maxLength(MAX_SYNC_AUX_BATCH))),
|
|
1113
|
+
/**
|
|
1114
|
+
* Version of the CLI making *this request* — the live one, and the only
|
|
1115
|
+
* `cliVersion` a shipped CLI still sends (the per-action field is a
|
|
1116
|
+
* migration leftover; see `DashboardSyncActionSchema`). It sits on the
|
|
1117
|
+
* envelope because the server reads it before touching any row, to
|
|
1118
|
+
* compare against its version floor and reject an incompatible client with
|
|
1119
|
+
* 426 rather than half-ingesting the batch. Optional: clients predating this
|
|
1120
|
+
* field send none, and are never blocked for it.
|
|
1121
|
+
*/
|
|
1122
|
+
cliVersion: v.optional(CliVersionSchema),
|
|
1123
|
+
/**
|
|
1124
|
+
* Sessions whose agent context has ended, named by the CLI's `SessionEnd`
|
|
1125
|
+
* hook (`mossbear sync --session-complete`) — never by its per-turn `Stop` hook.
|
|
1126
|
+
* The server closes those sessions' runs; every other session in the payload
|
|
1127
|
+
* stays open.
|
|
1128
|
+
*
|
|
1129
|
+
* Session-keyed rather than a single envelope-level boolean, because one
|
|
1130
|
+
* `mossbear sync` uploads every session with pending actions and chunks them
|
|
1131
|
+
* across several requests. A boolean would close sessions that merely had a
|
|
1132
|
+
* backlog, and would close a session before the batches carrying the rest of
|
|
1133
|
+
* its actions had arrived. The CLI therefore names a session here only on the
|
|
1134
|
+
* final batch that carries that session's actions.
|
|
1135
|
+
*
|
|
1136
|
+
* Deliberately an assertion, not a fact: `SessionEnd` also fires on
|
|
1137
|
+
* `/clear`, `--resume` and `logout`, and a reclaimed cloud container may fire
|
|
1138
|
+
* nothing at all. Closing is therefore reversible — later actions for a
|
|
1139
|
+
* closed run reopen it — and the server's idle sweep, not this field, is what
|
|
1140
|
+
* runs actually depend on being closed by. See
|
|
1141
|
+
* docs/plans/run-level-grading-plan.md Slice 1.
|
|
1142
|
+
*/
|
|
1143
|
+
completedSessionIds: v.optional(v.pipe(v.array(NonEmptyStringSchema), v.maxLength(MAX_SYNC_AUX_BATCH))),
|
|
1144
|
+
/**
|
|
1145
|
+
* The rule files present in the repo this sync ran from — see
|
|
1146
|
+
* {@link RuleFileManifestSchema}.
|
|
1147
|
+
*
|
|
1148
|
+
* Envelope-level rather than per-action because it describes the machine and
|
|
1149
|
+
* the checkout, not any one tool call; the server attaches it to the runs it
|
|
1150
|
+
* materializes from this request. Present on every batch that carries
|
|
1151
|
+
* actions, so a session chunked across several requests has the manifest
|
|
1152
|
+
* available wherever its run is touched.
|
|
1153
|
+
*
|
|
1154
|
+
* Optional: a CLI predating this sends none, and the server must read its
|
|
1155
|
+
* absence as "delivery unobserved", never as "nothing was delivered".
|
|
1156
|
+
*/
|
|
1157
|
+
ruleManifest: v.optional(RuleFileManifestSchema),
|
|
1158
|
+
});
|
|
1159
|
+
export const ExtractedGuideSuggestionSchema = v.object({
|
|
1160
|
+
id: v.string(),
|
|
1161
|
+
content: v.string(),
|
|
1162
|
+
reasoning: v.string(),
|
|
1163
|
+
triggerType: v.picklist(['conversational_feedback']),
|
|
1164
|
+
confidence: v.pipe(v.number(), v.minValue(0), v.maxValue(1)),
|
|
1165
|
+
occurrences: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
1166
|
+
citations: v.array(ProposalCitationSchema),
|
|
1167
|
+
source: PlatformKindSchema,
|
|
1168
|
+
});
|
|
1169
|
+
export const ExtractedSkillSuggestionSchema = v.object({
|
|
1170
|
+
id: v.string(),
|
|
1171
|
+
signal: v.string(),
|
|
1172
|
+
confidence: v.pipe(v.number(), v.minValue(0), v.maxValue(1)),
|
|
1173
|
+
occurrences: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
1174
|
+
citations: v.array(ProposalCitationSchema),
|
|
1175
|
+
source: PlatformKindSchema,
|
|
1176
|
+
});
|
|
1177
|
+
export const ExtractedFeedbackBatchSchema = v.object({
|
|
1178
|
+
guideSuggestions: v.array(ExtractedGuideSuggestionSchema),
|
|
1179
|
+
skillSuggestions: v.array(ExtractedSkillSuggestionSchema),
|
|
1180
|
+
});
|
|
1181
|
+
// ---------------------------------------------------------------------------
|
|
1182
|
+
// Agent config schema
|
|
1183
|
+
// ---------------------------------------------------------------------------
|
|
1184
|
+
// AgentConfig is what `mossbear guides project` and `packages/projections` share.
|
|
1185
|
+
// EvalRequest/EvalResult/Finding and ProjectionRequest/ProjectionResult used
|
|
1186
|
+
// to wrap it for the CLI's on-device grader and a projection wire envelope;
|
|
1187
|
+
// both are gone (docs/plans/cli-local-grading-removal-plan.md Open questions)
|
|
1188
|
+
// and had zero consumers outside this package's own tests.
|
|
1189
|
+
export const AgentConfigSchema = v.object({
|
|
1190
|
+
id: v.string(),
|
|
1191
|
+
name: v.string(),
|
|
1192
|
+
description: v.string(),
|
|
1193
|
+
capabilities: v.array(v.string()),
|
|
1194
|
+
meta: v.optional(v.record(v.string(), v.unknown())),
|
|
1195
|
+
});
|
|
1196
|
+
// ---------------------------------------------------------------------------
|
|
1197
|
+
// Action log schemas
|
|
1198
|
+
// ---------------------------------------------------------------------------
|
|
1199
|
+
export const ActionEntrySchema = v.object({
|
|
1200
|
+
/**
|
|
1201
|
+
* Stable identity, assigned by the logger hook when the action happens.
|
|
1202
|
+
*
|
|
1203
|
+
* Serves two jobs at once (docs/plans/action-evidence-sync-plan.md): it is
|
|
1204
|
+
* the `actions.id` a sync sends, so re-syncing the same append-only run log
|
|
1205
|
+
* is a no-op against the server's `onConflictDoNothing`; and it is the
|
|
1206
|
+
* correlation key for joining transcript evidence to an action, which
|
|
1207
|
+
* adjacency cannot do once one assistant message emits several tool calls.
|
|
1208
|
+
*
|
|
1209
|
+
* Optional because run logs written before this field existed must stay
|
|
1210
|
+
* readable. Sync derives an id for those from `(sessionId, line index)`; see
|
|
1211
|
+
* `deriveLegacyActionId`.
|
|
1212
|
+
*/
|
|
1213
|
+
id: v.optional(v.string()),
|
|
1214
|
+
timestamp: v.string(),
|
|
1215
|
+
sessionId: v.string(),
|
|
1216
|
+
toolName: v.string(),
|
|
1217
|
+
filePath: v.optional(v.string()),
|
|
1218
|
+
command: v.optional(v.string()),
|
|
1219
|
+
/**
|
|
1220
|
+
* Set only for a `Skill` tool call: which skill the agent loaded.
|
|
1221
|
+
*
|
|
1222
|
+
* This is what makes a skill load ride the action stream instead of the
|
|
1223
|
+
* transcript. `skill_usage` — the other place a loaded skill name is
|
|
1224
|
+
* recorded — is populated by `extractRunUsage`, which returns empty unless
|
|
1225
|
+
* `mossbear sync` is given `--transcripts-dir`, and the hook `mossbear init`
|
|
1226
|
+
* installs never passes one. So the transcript path has no data in a normal
|
|
1227
|
+
* session, while an action definitionally syncs.
|
|
1228
|
+
*/
|
|
1229
|
+
skill: v.optional(ActionSkillLoadSchema),
|
|
1230
|
+
/**
|
|
1231
|
+
* Where this session's transcript lives on *this machine*, as the agent
|
|
1232
|
+
* itself reported it.
|
|
1233
|
+
*
|
|
1234
|
+
* **Local-only, and it must stay that way.** `toSyncAction`
|
|
1235
|
+
* (`packages/cli/src/action-queue.ts`) builds the wire payload field by
|
|
1236
|
+
* field and does not include this one; transcripts are disclosed as
|
|
1237
|
+
* local-only (`apps/dashboard/src/lib/privacy/whatSyncs.ts`), and a path is
|
|
1238
|
+
* still a fact about the user's filesystem. It exists so `mossbear sync` can
|
|
1239
|
+
* find the transcript for a session it is already uploading without
|
|
1240
|
+
* searching for it (docs/plans/default-transcript-discovery-plan.md →
|
|
1241
|
+
* "Session resolution").
|
|
1242
|
+
*
|
|
1243
|
+
* Optional three times over: run logs written before this field existed must
|
|
1244
|
+
* stay readable, a hook protocol other than Claude Code's may not send it,
|
|
1245
|
+
* and resolution falls back to the `<sessionId>.jsonl` filename convention
|
|
1246
|
+
* when it is absent or stale.
|
|
1247
|
+
*/
|
|
1248
|
+
transcriptPath: v.optional(v.string()),
|
|
1249
|
+
/**
|
|
1250
|
+
* Which checkout produced this action — the same digest as
|
|
1251
|
+
* {@link RuleFileManifestSchema}'s `repoId`, stamped by the logger hook from
|
|
1252
|
+
* the event's `cwd` so a sync in repo A does not attach A's manifest to
|
|
1253
|
+
* sessions that ran in repo B (`run-repo-attribution`).
|
|
1254
|
+
*/
|
|
1255
|
+
repoId: v.optional(RepoIdSchema),
|
|
1256
|
+
/**
|
|
1257
|
+
* Whether the tool call succeeded, when the hook could tell.
|
|
1258
|
+
*
|
|
1259
|
+
* Optional twice over: run logs written before this field existed must stay
|
|
1260
|
+
* readable, and a recognized response shape is not guaranteed even now. See
|
|
1261
|
+
* {@link ActionOutcomeSchema} — absent is "unknown", not "fine".
|
|
1262
|
+
*/
|
|
1263
|
+
outcome: v.optional(ActionOutcomeSchema),
|
|
1264
|
+
});
|
|
1265
|
+
/**
|
|
1266
|
+
* Action types the CLI maps agent tool calls onto.
|
|
1267
|
+
*
|
|
1268
|
+
* Deliberately **not** a closed schema. `DashboardSyncAction.actionType` stays
|
|
1269
|
+
* an open string so a tool the CLI has never seen still syncs as itself
|
|
1270
|
+
* (`packages/cli/src/action-queue.ts` normalizes an unknown tool name rather
|
|
1271
|
+
* than collapsing it into a catch-all) — a closed picklist would silently drop
|
|
1272
|
+
* those. This list is the *known* vocabulary: what the CLI produces on purpose.
|
|
1273
|
+
*
|
|
1274
|
+
* It lives here because it is a contract between two packages that never import
|
|
1275
|
+
* each other. It used to also be the vocabulary the guide-trigger picker
|
|
1276
|
+
* offered; that picker is gone (docs/plans/run-level-grading-plan.md, Slice 4)
|
|
1277
|
+
* and nothing matches against this list any more — relevance is observed from
|
|
1278
|
+
* what was in the agent's context, not asserted against an action type.
|
|
1279
|
+
*
|
|
1280
|
+
* `SKILL_LOAD_ACTION_TYPE` is the one type the CLI emits on purpose that is
|
|
1281
|
+
* deliberately absent here; see its own comment for why.
|
|
1282
|
+
*/
|
|
1283
|
+
const KNOWN_ACTION_TYPES = [
|
|
1284
|
+
'file_edit',
|
|
1285
|
+
'file_read',
|
|
1286
|
+
'shell_command',
|
|
1287
|
+
'search',
|
|
1288
|
+
'web_access',
|
|
1289
|
+
];
|
|
1290
|
+
/** True when `value` is one of the action types the CLI produces on purpose. */
|
|
1291
|
+
export function isKnownActionType(value) {
|
|
1292
|
+
return KNOWN_ACTION_TYPES.includes(value);
|
|
1293
|
+
}
|
|
1294
|
+
// No run-eval result schema here. It was the contract for the CLI's on-device
|
|
1295
|
+
// grader, which is gone (docs/plans/cli-local-grading-removal-plan.md). The
|
|
1296
|
+
// only grader left runs on the server and owns its own output contract in
|
|
1297
|
+
// `apps/dashboard/src/lib/eval/runGradeSchema.ts` — one shape of that fact, not
|
|
1298
|
+
// two packages holding copies that a comment has to keep in step.
|
|
1299
|
+
// ---------------------------------------------------------------------------
|
|
1300
|
+
// Parse wrappers (throw ValiError on invalid input)
|
|
1301
|
+
// ---------------------------------------------------------------------------
|
|
1302
|
+
export function parseAgentConfig(data) {
|
|
1303
|
+
return v.parse(AgentConfigSchema, data);
|
|
1304
|
+
}
|
|
1305
|
+
export function parseGuideSnapshot(data) {
|
|
1306
|
+
return v.parse(GuideSnapshotSchema, data);
|
|
1307
|
+
}
|
|
1308
|
+
export function parseGuideBundleResponse(data) {
|
|
1309
|
+
return v.parse(GuideBundleResponseSchema, data);
|
|
1310
|
+
}
|
|
1311
|
+
export function parseRawGuideFilePayload(data) {
|
|
1312
|
+
return v.parse(RawGuideFilePayloadSchema, data);
|
|
1313
|
+
}
|
|
1314
|
+
export function parseGuideMemberFilesResponse(data) {
|
|
1315
|
+
return v.parse(GuideMemberFilesResponseSchema, data);
|
|
1316
|
+
}
|
|
1317
|
+
export function parseRawGuideFileDeletion(data) {
|
|
1318
|
+
return v.parse(RawGuideFileDeletionSchema, data);
|
|
1319
|
+
}
|
|
1320
|
+
export function parseRuleDeletionsRequest(data) {
|
|
1321
|
+
return v.parse(RuleDeletionsRequestSchema, data);
|
|
1322
|
+
}
|
|
1323
|
+
export function parseRulesImportRequest(data) {
|
|
1324
|
+
return v.parse(RulesImportRequestSchema, data);
|
|
1325
|
+
}
|
|
1326
|
+
export function parseBundle(data) {
|
|
1327
|
+
return v.parse(BundleSchema, data);
|
|
1328
|
+
}
|
|
1329
|
+
export function parseBundleListResponse(data) {
|
|
1330
|
+
return v.parse(BundleListResponseSchema, data);
|
|
1331
|
+
}
|
|
1332
|
+
export function parseCreateBundleRequest(data) {
|
|
1333
|
+
return v.parse(CreateBundleRequestSchema, data);
|
|
1334
|
+
}
|
|
1335
|
+
export function parseCreateBundleResponse(data) {
|
|
1336
|
+
return v.parse(CreateBundleResponseSchema, data);
|
|
1337
|
+
}
|
|
1338
|
+
export function parseAttachBundleMemberRequest(data) {
|
|
1339
|
+
return v.parse(AttachBundleMemberRequestSchema, data);
|
|
1340
|
+
}
|
|
1341
|
+
export function parseAttachBundleMemberResponse(data) {
|
|
1342
|
+
return v.parse(AttachBundleMemberResponseSchema, data);
|
|
1343
|
+
}
|
|
1344
|
+
export function parseDetachBundleMemberResponse(data) {
|
|
1345
|
+
return v.parse(DetachBundleMemberResponseSchema, data);
|
|
1346
|
+
}
|
|
1347
|
+
export function parseRenameBundleRequest(data) {
|
|
1348
|
+
return v.parse(RenameBundleRequestSchema, data);
|
|
1349
|
+
}
|
|
1350
|
+
export function parseRenameBundleResponse(data) {
|
|
1351
|
+
return v.parse(RenameBundleResponseSchema, data);
|
|
1352
|
+
}
|
|
1353
|
+
export function parseDeleteBundleResponse(data) {
|
|
1354
|
+
return v.parse(DeleteBundleResponseSchema, data);
|
|
1355
|
+
}
|
|
1356
|
+
export function parseContextItem(data) {
|
|
1357
|
+
return v.parse(ContextItemSchema, data);
|
|
1358
|
+
}
|
|
1359
|
+
export function parseContextItemsResponse(data) {
|
|
1360
|
+
return v.parse(ContextItemsResponseSchema, data);
|
|
1361
|
+
}
|
|
1362
|
+
export function parsePushContextRequest(data) {
|
|
1363
|
+
return v.parse(PushContextRequestSchema, data);
|
|
1364
|
+
}
|
|
1365
|
+
export function parsePushContextResponse(data) {
|
|
1366
|
+
return v.parse(PushContextResponseSchema, data);
|
|
1367
|
+
}
|
|
1368
|
+
export function parseUpdateContextItemRequest(data) {
|
|
1369
|
+
return v.parse(UpdateContextItemRequestSchema, data);
|
|
1370
|
+
}
|
|
1371
|
+
export function parseUpdateContextItemResponse(data) {
|
|
1372
|
+
return v.parse(UpdateContextItemResponseSchema, data);
|
|
1373
|
+
}
|
|
1374
|
+
export function parseUpdateContextItemBody(data) {
|
|
1375
|
+
return v.parse(UpdateContextItemBodySchema, data);
|
|
1376
|
+
}
|
|
1377
|
+
export function parseContextItemResponse(data) {
|
|
1378
|
+
return v.parse(ContextItemResponseSchema, data);
|
|
1379
|
+
}
|
|
1380
|
+
export function parseArchiveContextItemResponse(data) {
|
|
1381
|
+
return v.parse(ArchiveContextItemResponseSchema, data);
|
|
1382
|
+
}
|
|
1383
|
+
export function parseInboxCaptureRequest(data) {
|
|
1384
|
+
return v.parse(InboxCaptureRequestSchema, data);
|
|
1385
|
+
}
|
|
1386
|
+
export function parseInboxCaptureResponse(data) {
|
|
1387
|
+
return v.parse(InboxCaptureResponseSchema, data);
|
|
1388
|
+
}
|
|
1389
|
+
export function parseInboxListResponse(data) {
|
|
1390
|
+
return v.parse(InboxListResponseSchema, data);
|
|
1391
|
+
}
|
|
1392
|
+
export function parseContextItemVersionsResponse(data) {
|
|
1393
|
+
return v.parse(ContextItemVersionsResponseSchema, data);
|
|
1394
|
+
}
|
|
1395
|
+
export function parseContextItemVersionResponse(data) {
|
|
1396
|
+
return v.parse(ContextItemVersionResponseSchema, data);
|
|
1397
|
+
}
|
|
1398
|
+
export function parseInboxArchiveRequest(data) {
|
|
1399
|
+
return v.parse(InboxArchiveRequestSchema, data);
|
|
1400
|
+
}
|
|
1401
|
+
export function parseInboxArchiveResponse(data) {
|
|
1402
|
+
return v.parse(InboxArchiveResponseSchema, data);
|
|
1403
|
+
}
|
|
1404
|
+
export function parseDashboardSyncRequest(data) {
|
|
1405
|
+
return v.parse(DashboardSyncRequestSchema, data);
|
|
1406
|
+
}
|
|
1407
|
+
export function parseProposalCitation(data) {
|
|
1408
|
+
return v.parse(ProposalCitationSchema, data);
|
|
1409
|
+
}
|
|
1410
|
+
export function parseExtractedGuideSuggestion(data) {
|
|
1411
|
+
return v.parse(ExtractedGuideSuggestionSchema, data);
|
|
1412
|
+
}
|
|
1413
|
+
export function parseExtractedSkillSuggestion(data) {
|
|
1414
|
+
return v.parse(ExtractedSkillSuggestionSchema, data);
|
|
1415
|
+
}
|
|
1416
|
+
export function parseExtractedFeedbackBatch(data) {
|
|
1417
|
+
return v.parse(ExtractedFeedbackBatchSchema, data);
|
|
1418
|
+
}
|
|
1419
|
+
export function parseActionEntry(data) {
|
|
1420
|
+
return v.parse(ActionEntrySchema, data);
|
|
1421
|
+
}
|
|
1422
|
+
//# sourceMappingURL=schemas.js.map
|