@valbuild/server 0.121.0 → 0.122.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,366 @@
1
+ import type { ExternalItemOf, ExternalLabelOf, ExternalReadonlyOf, ExternalRecordSrc, InferValModuleType, Json, JsonOf, ModuleFilePath, ValModule } from "@valbuild/core";
2
+ /**
3
+ * The type surface of an external record's adapter.
4
+ *
5
+ * Nothing here runs: phase 0 is the contract, and the registry that executes it
6
+ * arrives with the read endpoints. What the contract has to get right now is the
7
+ * shape, because every adapter written against it is a compatibility promise.
8
+ *
9
+ * Four kinds of method, and each is required or not for one reason:
10
+ *
11
+ * - **Read** — `keys`, `get`. Cannot be built out of anything else, so always
12
+ * required.
13
+ * - **Write** — `put`, `delete`. Likewise, and they move together: required on a
14
+ * writable record, forbidden on a `.readonly()` one.
15
+ * - **Derived** — `count`, `search`. Computable from the read methods, so
16
+ * omitting one costs performance, never capability. `false` declines the
17
+ * fallback; that is the only thing `false` ever means here.
18
+ * - **Media** — `putFile`, `getFile`. A pair, and required by the item SCHEMA
19
+ * rather than by this type (see `hasMediaSchema`).
20
+ */
21
+ /**
22
+ * Brands the result envelope so a bare value can be accepted alongside it.
23
+ *
24
+ * A symbol rather than a property name because `get` returns
25
+ * `Record<key, Item>` — a record that can perfectly well contain a key called
26
+ * `kind`. The envelope never crosses the wire (it travels in-process between
27
+ * adapter and Val), so there is no serialization cost to it.
28
+ */
29
+ export declare const EXTERNAL_RESULT: unique symbol;
30
+ export type ExternalIssue = {
31
+ message: string;
32
+ /**
33
+ * Whether repeating the operation could succeed.
34
+ *
35
+ * Only the adapter can tell a transient 429 from a permanent 404, so only the
36
+ * adapter sets this. A THROWN error is never retried: Val cannot tell a rate
37
+ * limit from a `TypeError`, and retrying a bug three times only fails slower.
38
+ */
39
+ retryable?: boolean;
40
+ /** Names the entry when the problem is one key's, not the whole call's. */
41
+ key?: string;
42
+ cause?: unknown;
43
+ };
44
+ export type ExternalResult<T> = {
45
+ readonly [EXTERNAL_RESULT]: true;
46
+ } & ({
47
+ kind: "ok";
48
+ value: T;
49
+ warnings?: ExternalIssue[];
50
+ } | {
51
+ kind: "err";
52
+ error: ExternalIssue;
53
+ });
54
+ /**
55
+ * What every adapter method may return: the value on its own, or the envelope.
56
+ *
57
+ * A bare value means "ok, nothing to report", which keeps the common path one
58
+ * line. Errors can be thrown instead of returned — the envelope is for a failure
59
+ * worth classifying, or a success worth annotating.
60
+ */
61
+ export type Returns<T> = T | ExternalResult<T>;
62
+ /**
63
+ * Wrap a value as a successful result, optionally with warnings.
64
+ *
65
+ * `NoInfer` is load-bearing, not decoration. Without it `T` is inferred FROM the
66
+ * argument, and an object literal that infers its own type is not contextually
67
+ * typed by the adapter's contract — so `title` inside `ok({ a: { title } })` is a
68
+ * fresh property that happens to be named `title`, with no declaration link back
69
+ * to `title: s.string()` in the schema. "Find all references" on the schema field
70
+ * then stops at the page that reads it and never reaches the adapter that
71
+ * produces it. `NoInfer` blocks that inference, leaving the contextual return
72
+ * type as the only source for `T`, which is what restores the link (and, as a
73
+ * bonus, makes a typo report the offending property rather than the whole
74
+ * return value).
75
+ *
76
+ * `externalNavigation.test.ts` guards this; nothing else would notice.
77
+ *
78
+ * The cost: `ok(...)` in a position with NO contextual type — a helper without a
79
+ * return type annotation — infers `unknown` instead of the argument's type, and
80
+ * fails where the helper is used rather than where it is written. Annotate the
81
+ * helper's return type, or inline it.
82
+ */
83
+ export declare function ok<T>(value: NoInfer<T>, warnings?: ExternalIssue[]): ExternalResult<T>;
84
+ export declare function err(issue: ExternalIssue): ExternalResult<never>;
85
+ export declare function isExternalResult<T>(value: Returns<T>): value is ExternalResult<T>;
86
+ export type ExternalAuthor = {
87
+ /** Always present. */
88
+ id: string;
89
+ /**
90
+ * Present when Val knows it — the profile shape has it optional, so an
91
+ * adapter keying only on email would break for a user without one.
92
+ */
93
+ email?: string;
94
+ };
95
+ /**
96
+ * What every adapter method is told about the call it is serving.
97
+ *
98
+ * `tx` is present only when the adapter declared an `around`; a store with no
99
+ * transaction seam has nothing to put there and nothing to ignore.
100
+ */
101
+ export type ExternalCtx<Tx> = {
102
+ moduleFilePath: ModuleFilePath;
103
+ /** Who is editing. Anything richer is the adapter's to resolve. */
104
+ author?: ExternalAuthor;
105
+ /**
106
+ * 1 on the first call, 2 on the first retry.
107
+ *
108
+ * Not only for logging: an adapter can widen a timeout, skip a read replica
109
+ * that may be lagging, bypass a cache, or give up on its own terms.
110
+ */
111
+ attempt: number;
112
+ } & ([Tx] extends [never] ? {
113
+ tx?: undefined;
114
+ } : {
115
+ tx: Tx;
116
+ });
117
+ export type ExternalKeyPage = {
118
+ keys: string[];
119
+ /** `null` means this was the last page. */
120
+ cursor: string | null;
121
+ };
122
+ /**
123
+ * How a page should be ordered.
124
+ *
125
+ * Records are unordered today and sorting is coming; the parameter lands now
126
+ * because adding one to `keys` later would break every adapter that exists by
127
+ * then. Val passes `undefined` until sorting ships, and an adapter may ignore it.
128
+ *
129
+ * Two rules for whoever implements it:
130
+ *
131
+ * - **A cursor is only valid for the sort that issued it.** Key-ordered paging
132
+ * is `where key > cursor`; sorted paging is `where (field, key) > (…, …)`.
133
+ * Val pairs the cursor with a hash of the sort and restarts rather than
134
+ * replaying a mismatched one.
135
+ * - **The key is always the last sort term.** Ordering by a non-unique field
136
+ * without a tiebreaker lets rows shift between pages, so page 2 can skip or
137
+ * repeat an entry while both pages look fine on their own.
138
+ */
139
+ export type ExternalSort = {
140
+ /**
141
+ * A path into the ITEM: `["publishedAt"]`, `["author", "name"]`. Absent means
142
+ * order by the entry key, which is the only order there is today.
143
+ */
144
+ path?: string[];
145
+ direction: "asc" | "desc";
146
+ };
147
+ export type ExternalSearchHit<Item> = {
148
+ key: string;
149
+ /**
150
+ * Where it matched, relative to the item. A store that only knows "this row
151
+ * matched" omits it; the Studio uses it to show the field, not just the row.
152
+ */
153
+ path?: string[];
154
+ /**
155
+ * The entry as the store has it.
156
+ *
157
+ * Val applies pending patches on top before deciding the hit still stands —
158
+ * which is the whole reason to return it. A delegated search sees only
159
+ * PUBLISHED content, so without the value there is no way to drop a hit whose
160
+ * draft edit removed the matching text.
161
+ */
162
+ value?: Item;
163
+ };
164
+ export type ExternalSearchPage<Item> = {
165
+ hits: ExternalSearchHit<Item>[];
166
+ cursor: string | null;
167
+ };
168
+ export type ExternalFile = {
169
+ /**
170
+ * The path Val chose — `${directory}/${createFilename(...)}` — which is the
171
+ * same name a local or remote file would have got.
172
+ *
173
+ * An INPUT, not something the adapter invents: the stored ref embeds this
174
+ * path, so a gallery can be moved back to local storage by writing the bytes
175
+ * where the ref already says they belong.
176
+ */
177
+ path: string;
178
+ bytes: Uint8Array;
179
+ filename: string;
180
+ mimeType: string;
181
+ /** Content address. Make `putFile` idempotent on it, so a retry re-uses. */
182
+ sha256: string;
183
+ };
184
+ type AnyExternalModule = ValModule<ExternalRecordSrc>;
185
+ /** Read methods. Always required. */
186
+ type ReadMethods<Item, Tx> = {
187
+ keys(args: {
188
+ cursor: string | null;
189
+ limit: number;
190
+ sort?: ExternalSort;
191
+ }, ctx: ExternalCtx<Tx>): Promise<Returns<ExternalKeyPage>>;
192
+ get(keys: string[], ctx: ExternalCtx<Tx>): Promise<Returns<Record<string, Item | null>>>;
193
+ };
194
+ /**
195
+ * Write methods. Required together, and forbidden together on `.readonly()`.
196
+ *
197
+ * `put` must be an UPSERT keyed by the entry key and `delete` must tolerate an
198
+ * absent key, because a publish may be replayed: retry re-runs the whole scope
199
+ * where there is a transaction, and the individual call where there is not.
200
+ */
201
+ type WriteMethods<Item, Tx> = {
202
+ put(entries: Record<string, Item>, ctx: ExternalCtx<Tx> & {
203
+ /**
204
+ * What `putFile` returned, keyed by the path it was given.
205
+ *
206
+ * Path rather than sha256 because the entry references its file by path
207
+ * and carries no hash — keying by hash would need a second map. The sha is
208
+ * a field, for an adapter doing content-addressed bookkeeping.
209
+ */
210
+ uploads: Record<string, {
211
+ sha256: string;
212
+ data?: Json;
213
+ }>;
214
+ }): Promise<Returns<void>>;
215
+ delete(keys: string[], ctx: ExternalCtx<Tx>): Promise<Returns<void>>;
216
+ };
217
+ /**
218
+ * Named so the compiler prints the reason. A bare `never` would report only
219
+ * "not assignable to type 'undefined'", which tells nobody anything.
220
+ */
221
+ export type ReadonlyRecordHasNoWrites = {
222
+ readonly __val_error: "this record is .readonly(): remove put and delete, or remove .readonly() from the schema";
223
+ };
224
+ /** Derived and media methods, and the sort declaration. */
225
+ type OptionalMethods<Item, Tx> = {
226
+ /**
227
+ * Three modes. A function delegates; `false` declines; omitted means Val
228
+ * counts by paging `keys` and summing, cached and bounded.
229
+ */
230
+ count?: false | ((ctx: ExternalCtx<Tx>) => Promise<Returns<number>>);
231
+ /**
232
+ * Three modes, as `count`. Omitted, Val answers from the entries it has
233
+ * already seen and labels the result partial — it does not fetch a whole store
234
+ * on the editor's behalf. `false` declines even that.
235
+ */
236
+ search?: false | ((query: {
237
+ text: string;
238
+ cursor: string | null;
239
+ limit: number;
240
+ sort?: ExternalSort;
241
+ }, ctx: ExternalCtx<Tx>) => Promise<Returns<ExternalSearchPage<Item>>>);
242
+ /**
243
+ * Which item paths this store can order by. `true` for any field. Absent
244
+ * means key order only — all any store can promise, and all a bucket can do.
245
+ */
246
+ sortable?: true | string[][];
247
+ /**
248
+ * The most keys this store will list per call, and the most one `get` may
249
+ * carry.
250
+ *
251
+ * Chunking hints for the fetcher, NOT ceilings the UI sees: if the Studio asks
252
+ * for 50 and the store lists 10 at a time, Val makes five calls and returns
253
+ * 50. Page size belongs to the Studio, which must not be able to tell how a
254
+ * record is stored.
255
+ */
256
+ maxPageSize?: number;
257
+ maxBatchSize?: number;
258
+ /**
259
+ * A token that changes when anything in this record changes — `max(updated_at)`,
260
+ * a sequence number, an ETag. Polled on the poll that already exists. Absent
261
+ * costs freshness, never function: Val re-reads on navigation.
262
+ */
263
+ version?(ctx: ExternalCtx<Tx>): Promise<Returns<string>>;
264
+ /** Stores bytes at the path Val chose. Paired with `getFile`. */
265
+ putFile?(file: ExternalFile, ctx: ExternalCtx<Tx>): Promise<Returns<{
266
+ data?: Json;
267
+ }>>;
268
+ /** Reads them back. If an adapter can store bytes it must return them. */
269
+ getFile?(path: string, ctx: ExternalCtx<Tx>): Promise<Returns<Uint8Array | null>>;
270
+ };
271
+ /** The item type an external module's entries hold, loosened as JSON allows. */
272
+ export type ItemOfModule<M extends AnyExternalModule> = JsonOf<ExternalItemOf<InferValModuleType<M>>>;
273
+ /**
274
+ * The adapter a given external module needs.
275
+ *
276
+ * Writes are required or forbidden by the module's own `.readonly()`, read off
277
+ * the source marker's phantom.
278
+ */
279
+ export type AdapterFor<M extends AnyExternalModule, Tx> = ReadMethods<ItemOfModule<M>, Tx> & OptionalMethods<ItemOfModule<M>, Tx> & (ExternalReadonlyOf<InferValModuleType<M>> extends true ? {
280
+ put?: ReadonlyRecordHasNoWrites;
281
+ delete?: ReadonlyRecordHasNoWrites;
282
+ } : WriteMethods<ItemOfModule<M>, Tx>);
283
+ declare const BoundTag: unique symbol;
284
+ /** What `entry()` returns: a module and its adapter, checked against each other. */
285
+ export type BoundExternalRecord<M> = {
286
+ readonly [BoundTag]: M;
287
+ };
288
+ export type ExternalRecords = {
289
+ readonly __brand: "ExternalRecords";
290
+ };
291
+ export type ExternalBuilder<Tx> = {
292
+ /**
293
+ * Bind an adapter to a module.
294
+ *
295
+ * A call rather than an object literal on purpose: `module` is inferred from
296
+ * the first argument, so `AdapterFor` RESOLVES for the second and TypeScript
297
+ * records the declaration link between a schema field and the adapter that
298
+ * produces it. Inside a single object literal the module stays a deferred type
299
+ * parameter, the adapter type never resolves, and "find all references" on a
300
+ * schema field stops reaching the adapter.
301
+ */
302
+ entry<M extends AnyExternalModule>(module: M, adapter: AdapterFor<M, Tx>): BoundExternalRecord<M>;
303
+ /**
304
+ * Collect the bindings. The key must equal the schema's own `.external(label)`.
305
+ */
306
+ modules<E extends Record<string, BoundExternalRecord<AnyExternalModule>>>(entries: E & {
307
+ [K in keyof E]: E[K] extends BoundExternalRecord<infer M extends AnyExternalModule> ? ExternalLabelOf<InferValModuleType<M>> extends K ? E[K] : BoundExternalRecord<ValModule<ExternalRecordSrc<unknown, K & string>>> : never;
308
+ }): ExternalRecords;
309
+ };
310
+ export type ExternalDefinition<Tx> = {
311
+ /**
312
+ * One scope per request, so a 50-key batch is one transaction and one query
313
+ * rather than fifty. Composes with any `withTransaction(cb)` API.
314
+ *
315
+ * Optional: a store with no transaction seam — a bucket, a REST API — simply
316
+ * omits it, and `ctx` then has no `tx`.
317
+ *
318
+ * Retry scope follows from this. WITH `around`, Val re-enters the whole scope,
319
+ * because a database aborts a transaction on its first error and answers
320
+ * everything after it with "current transaction is aborted". WITHOUT it, Val
321
+ * repeats the single operation, which is both correct and far cheaper.
322
+ */
323
+ around?: <R>(run: (tx: Tx) => Promise<R>) => Promise<R>;
324
+ /**
325
+ * Retry policy. Return the delay in ms, or `false` to give up.
326
+ *
327
+ * A policy function, not a re-implementation of the loop. Defaults to three
328
+ * attempts with exponential backoff. `false` disables retries entirely.
329
+ */
330
+ retry?: false | {
331
+ attempts: number;
332
+ backoff?: (attempt: number) => number;
333
+ } | ((attempt: number, issue: ExternalIssue) => number | false);
334
+ /** Fired after a publish, so an app that caches can purge what changed. */
335
+ onPublished?: (event: {
336
+ label: string;
337
+ keys: string[];
338
+ }) => Promise<void> | void;
339
+ /**
340
+ * How far the derived `count` walk may page before answering "200,000+".
341
+ * Keys only — it never fetches entry content.
342
+ */
343
+ countPageLimit?: number;
344
+ };
345
+ /**
346
+ * Declare the adapters for this project's external records.
347
+ *
348
+ * `Tx` is given explicitly: `around` offers the compiler no inference site,
349
+ * since `run` is a callback you CALL rather than one whose signature you write.
350
+ * One type argument, in one place, and every inline adapter then gets `tx`,
351
+ * `cursor`, `limit` and the row shape correctly typed.
352
+ *
353
+ * @example
354
+ * const { entry, modules } = defineExternal<typeof sql>({
355
+ * around: (run) => sql.begin(run),
356
+ * });
357
+ *
358
+ * export default modules({
359
+ * posts: entry(postsVal, { keys, get, put, delete: del, search: false }),
360
+ * });
361
+ *
362
+ * @example a store with no transaction
363
+ * const { entry, modules } = defineExternal();
364
+ */
365
+ export declare function defineExternal<Tx = never>(definition?: ExternalDefinition<Tx>): ExternalBuilder<Tx>;
366
+ export {};
@@ -164,6 +164,19 @@ export declare function handleRemoteFileCheck(): Promise<FixHandlerResult>;
164
164
  export declare function handleUniqueFolderCheck(ctx: FixHandlerContext): Promise<FixHandlerResult>;
165
165
  export declare function handleCheckAllFiles(ctx: FixHandlerContext): Promise<FixHandlerResult>;
166
166
  export declare function handleJsonValuesExtractEntry(ctx: FixHandlerContext): Promise<FixHandlerResult>;
167
+ /**
168
+ * `external:upload` under `val validate --fix`: refuse, and say what to run.
169
+ *
170
+ * Every other fix in this registry rewrites something inside the repository —
171
+ * reversible, visible in a diff, wrong by at most one commit. This one would
172
+ * write entries into a live external store: not in a diff, not undone by
173
+ * `git revert`, and against production indistinguishable from an editor's
174
+ * publish. So a blanket `--fix` must never apply it.
175
+ *
176
+ * `fixableErrorMessage` rather than a plain error, because the error IS fixable
177
+ * — just not by this command.
178
+ */
179
+ export declare function handleExternalUpload(): Promise<FixHandlerResult>;
167
180
  export declare const currentFixHandlers: Record<Exclude<ValidationFix, "keyof:check-keys" | "router:check-route">, FixHandler>;
168
181
  export declare const fixHandlers: Record<string, FixHandler>;
169
182
  export declare function createDefaultValFSHost(): IValFSHost;
@@ -0,0 +1,90 @@
1
+ import type { ModuleFilePath } from "@valbuild/core";
2
+ /**
3
+ * Everything that can go wrong reading, replaying or restoring history.
4
+ *
5
+ * There is a lot of it, which is the point of making it one closed union rather
6
+ * than strings: reading a commit means reaching a content host, finding the
7
+ * commit, reading what was recorded for each of its modules, and asking whether
8
+ * this version of Val can make sense of the schema it was stored under. Each of
9
+ * those fails differently, and a caller deciding what to SHOW needs to know
10
+ * which.
11
+ *
12
+ * Every member has a producer. The union once carried five more - a module
13
+ * removed from the project, an op that would not replay, a value that no longer
14
+ * fits today's schema, a field the schema no longer defines, and ops from a
15
+ * core version known to replay wrongly. All five belonged to reconstructing a
16
+ * commit by replaying patches against a parsed source; storing the data ended
17
+ * that, and a member nothing can produce is a state the Studio writes handling
18
+ * for and never sees.
19
+ *
20
+ * ## The rule
21
+ *
22
+ * Whole-commit failures are the `err` channel. Per-module and per-patch
23
+ * failures ride along inside the `ok` payload.
24
+ *
25
+ * A commit that cannot be found or read at all has nothing to show. But one
26
+ * unparseable module out of ten does not make the other nine unreadable, and
27
+ * collapsing the whole view because of it would hide exactly the information
28
+ * someone needs to see - that this module is the broken one.
29
+ */
30
+ export type HistoryError =
31
+ /** No such commit, or not one Val created. Nothing will make it readable. */
32
+ {
33
+ kind: "commit-not-found";
34
+ commitSha: string;
35
+ }
36
+ /**
37
+ * The commit says it has an archive and the archive is missing or malformed.
38
+ * Distinct from a commit that predates archiving, which is not an error.
39
+ */
40
+ | {
41
+ kind: "archive-unreadable";
42
+ commitSha: string;
43
+ message: string;
44
+ }
45
+ /**
46
+ * Nothing was stored for this module at this commit - a commit made before
47
+ * history was recorded, or by a Val too old to send it. NOT the same as an
48
+ * empty module, which is why it is reported rather than defaulted.
49
+ */
50
+ | {
51
+ kind: "source-unavailable";
52
+ moduleFilePath: ModuleFilePath;
53
+ }
54
+ /**
55
+ * The schema stored with this commit is not one this version of Val can read.
56
+ *
57
+ * Expected, and NOT anyone's mistake: schemas are stored as written, and Val's
58
+ * schema format is allowed to move. An older project opened in a newer Val -
59
+ * or the reverse - can hit this, and the honest thing is to say the commit
60
+ * cannot be shown HERE rather than to imply the data is damaged. Everything
61
+ * else about the commit still reads.
62
+ */
63
+ | {
64
+ kind: "schema-unreadable";
65
+ moduleFilePath: ModuleFilePath;
66
+ message: string;
67
+ }
68
+ /** A binary file or `*.val.json` entry could not be read at that commit. */
69
+ | {
70
+ kind: "file-unavailable";
71
+ gitPath: string;
72
+ message: string;
73
+ }
74
+ /** History needs the content host; local FS mode has git instead. */
75
+ | {
76
+ kind: "not-supported-in-fs-mode";
77
+ }
78
+ /** Could not reach the content host at all. */
79
+ | {
80
+ kind: "transport";
81
+ message: string;
82
+ };
83
+ export declare function historyErrorMessage(error: HistoryError): string;
84
+ /**
85
+ * Whether an error is about the whole commit rather than one part of it.
86
+ *
87
+ * The `err`/`ok`-payload split above, as a predicate, so callers do not
88
+ * re-derive it and disagree.
89
+ */
90
+ export declare function isWholeCommitError(error: HistoryError): boolean;
@@ -0,0 +1,126 @@
1
+ import type { JSONValue } from "@valbuild/core/patch";
2
+ import type { ModuleFilePath, PatchId, SerializedSchema, SourcePath } from "@valbuild/core";
3
+ import type { HistoryError } from "./HistoryError.js";
4
+ /** One commit, as history lists it. */
5
+ export type HistoricalCommit = {
6
+ commitSha: string;
7
+ parentCommitSha: string;
8
+ clientCommitSha: string;
9
+ branch: string;
10
+ createdBranch: string | null;
11
+ creator: string | null;
12
+ message: string | null;
13
+ createdAt: string;
14
+ seqNum: string;
15
+ patchCount: number;
16
+ /**
17
+ * Whether this commit's record was stored. False for commits made before
18
+ * history was recorded: their patches are still readable, but not the
19
+ * pre-commit sources a restore replays against.
20
+ */
21
+ hasArchive: boolean;
22
+ };
23
+ export type CommitPage = {
24
+ commits: HistoricalCommit[];
25
+ /** Pass as `cursor` for the next page. `null` when there are no more. */
26
+ nextCursor: string | null;
27
+ };
28
+ export type CommitPatch = {
29
+ patchId: PatchId;
30
+ moduleFilePath: ModuleFilePath;
31
+ patch: unknown;
32
+ authorId: string | null;
33
+ createdAt: string;
34
+ baseSha: string;
35
+ coreVersion: string;
36
+ };
37
+ export type FileChange = "added" | "modified" | "deleted";
38
+ export type AffectedFile = {
39
+ kind: "module-source" | "json-entry" | "binary";
40
+ gitPath: string;
41
+ change: FileChange;
42
+ } | {
43
+ kind: "remote-binary";
44
+ ref: string;
45
+ change: FileChange;
46
+ };
47
+ /**
48
+ * A binary file this commit touched - named, not fetched.
49
+ *
50
+ * `url` is where the bytes are IF something needs them. Nothing downloads a
51
+ * commit's images to show that the commit changed them; the descriptor is
52
+ * enough to render a row, and only an `<img src>` that actually mounts pays.
53
+ */
54
+ export type BinaryFileRef = {
55
+ gitPath: string;
56
+ change: FileChange;
57
+ remote: boolean;
58
+ url: string;
59
+ };
60
+ /**
61
+ * A module as one commit left it.
62
+ *
63
+ * Both halves are stored rather than derived. `source` is the module's data,
64
+ * recorded at the commit - not recovered by parsing the `.val.ts` git holds,
65
+ * because that is code and parsing code is best-effort and rots. `schema` is
66
+ * the schema the data was written against, which nothing in the current
67
+ * checkout has once the schema has moved on, and without which the module
68
+ * cannot be RENDERED as it was - only guessed at.
69
+ */
70
+ export type HistoricalModule = {
71
+ /** The module's data at this commit. `null` if deleted, or unreadable — see `failures`. */
72
+ source: JSONValue | null;
73
+ /**
74
+ * The schema at this commit, if this version of Val can read it.
75
+ *
76
+ * `null` with a `schema-unreadable` failure means the stored schema is from a
77
+ * Val whose schema format this one does not know. Expected rather than
78
+ * broken, and reported so the Studio can say so kindly.
79
+ */
80
+ schema: SerializedSchema | null;
81
+ patchIds: PatchId[];
82
+ /**
83
+ * What this commit changed here, taken from the ops themselves.
84
+ *
85
+ * The ops ARE the change, so this needs no before-state and no replay - which
86
+ * is what let the static parser and the patch replay go.
87
+ */
88
+ changedPaths: SourcePath[];
89
+ /** Per-module problems. Collected, never thrown - see HistoryError. */
90
+ failures: HistoryError[];
91
+ };
92
+ /**
93
+ * A commit, reconstructed.
94
+ *
95
+ * Deliberately says nothing about the CURRENT source or schema, so it can never
96
+ * change for a given commit sha - which is what lets it be cached forever. The
97
+ * comparison against today lives in `HistoricalComparison`.
98
+ */
99
+ export type HistoricalPatchSet = {
100
+ commit: HistoricalCommit;
101
+ modules: Record<ModuleFilePath, HistoricalModule>;
102
+ patches: CommitPatch[];
103
+ /** `*.val.json` entry contents at this commit, keyed by git path. */
104
+ jsonEntries: Record<string, JSONValue>;
105
+ binaryFiles: BinaryFileRef[];
106
+ /** Problems that are about the commit but did not stop it being read. */
107
+ warnings: HistoryError[];
108
+ };
109
+ /**
110
+ * One module's stored version, exactly as the content service returns it.
111
+ *
112
+ * `schema` is `unknown` on purpose: the content service stores it opaquely and
113
+ * cannot vouch for it, so it arrives unvalidated and is checked HERE, where the
114
+ * schema format is actually known. That check is what turns "a project written
115
+ * by a different Val" into a sentence rather than a crash.
116
+ */
117
+ export type StoredModuleVersion = {
118
+ moduleFilePath: ModuleFilePath;
119
+ commitSha: string;
120
+ sourceSha: string | null;
121
+ schemaSha: string;
122
+ source: JSONValue | null;
123
+ schema: unknown;
124
+ /** We hold the hash but not the object. Distinct from a deleted module. */
125
+ unavailable: boolean;
126
+ };
@@ -1,4 +1,6 @@
1
1
  export { createService, Service } from "./Service.js";
2
+ export { defineExternal, ok, err, isExternalResult, EXTERNAL_RESULT, } from "./externalRecords.js";
3
+ export type { AdapterFor, BoundExternalRecord, ExternalAuthor, ExternalBuilder, ExternalCtx, ExternalDefinition, ExternalFile, ExternalIssue, ExternalKeyPage, ExternalRecords, ExternalResult, ExternalSearchHit, ExternalSearchPage, ExternalSort, ItemOfModule, ReadonlyRecordHasNoWrites, Returns, } from "./externalRecords.js";
2
4
  export { createValApiRouter, createValServer, safeReadGit } from "./ValRouter.js";
3
5
  export { createValTools } from "./tools/index.js";
4
6
  export { VAL_SCOPE_READ, VAL_SCOPE_WRITE, authorIdFromVerifiedSubject, } from "./tools/index.js";
@@ -15,7 +17,7 @@ export { formatSyntaxErrorTree } from "./patch/ts/syntax.js";
15
17
  export { analyzeValModule } from "./patch/ts/valModule.js";
16
18
  export type { ValModuleAnalysis } from "./patch/ts/valModule.js";
17
19
  export { createFixPatch } from "./createFixPatch.js";
18
- export { fixHandlers, currentFixHandlers, createDefaultValFSHost, handleFileMetadata, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleRemoteFileDownload, handleRemoteFileCheck, handleUniqueFolderCheck, handleCheckAllFiles, handleJsonValuesExtractEntry, } from "./fixHandlers.js";
20
+ export { fixHandlers, currentFixHandlers, createDefaultValFSHost, handleFileMetadata, handleRemoteFileUpload, handleRemoteGalleryFileUpload, handleRemoteFileDownload, handleRemoteFileCheck, handleUniqueFolderCheck, handleCheckAllFiles, handleJsonValuesExtractEntry, handleExternalUpload, } from "./fixHandlers.js";
19
21
  export type { FixHandler, FixHandlerContext, FixHandlerResult, IValRemote, ValidationEvent, ValidationError, ValModule, } from "./fixHandlers.js";
20
22
  export * from "./jwt.js";
21
23
  export type { ValServer } from "./ValServer.js";