@valbuild/shared 0.120.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.
@@ -897,6 +897,12 @@ function _asyncToGenerator(n) {
897
897
  };
898
898
  }
899
899
 
900
+ /**
901
+ * JSON, recursively — the shape of a Source over the wire.
902
+ *
903
+ * Exported because history reads stored Sources back and needs the same
904
+ * definition; a second copy is a second thing to keep in step.
905
+ */
900
906
  var JSONValue = z.lazy(function () {
901
907
  return z.union([z.string(), z.number(), z["boolean"](), z["null"](), z.array(JSONValue), z.record(z.string(), JSONValue)]);
902
908
  });
@@ -1275,7 +1281,26 @@ var SerializedRouteSchema = z.object({
1275
1281
  readonly: z["boolean"]().optional(),
1276
1282
  hidden: z["boolean"]().optional()
1277
1283
  });
1278
- var SerializedSchema = z.union([SerializedStringSchema, SerializedLiteralSchema, SerializedBooleanSchema, SerializedNumberSchema, SerializedObjectSchema, SerializedArraySchema, SerializedUnionSchema, SerializedRichTextSchema, SerializedRecordSchema, SerializedKeyOfSchema, SerializedRouteSchema, SerializedFileSchema, SerializedDateSchema, SerializedDateTimeSchema, SerializedColorSchema, SerializedCodeSchema, SerializedImageSchema]);
1284
+
1285
+ // A settings module, and each section inside one, serialize as this - the
1286
+ // shape is recursive because `items` holds sections which are themselves
1287
+ // settings schemas. `s.settings()` never writes render/preview/customValidate
1288
+ // (see core/src/schema/settings.ts), but they are accepted here because they
1289
+ // are part of the declared type.
1290
+ var SerializedSettingsSchema = z.lazy(function () {
1291
+ return z.object({
1292
+ type: z.literal("settings"),
1293
+ render: FieldRender,
1294
+ preview: z.literal(true).optional(),
1295
+ items: z.record(z.string(), SerializedSchema),
1296
+ opt: z["boolean"](),
1297
+ customValidate: z["boolean"]().optional(),
1298
+ readonly: z["boolean"]().optional(),
1299
+ hidden: z["boolean"]().optional(),
1300
+ description: z.string().optional()
1301
+ });
1302
+ });
1303
+ var SerializedSchema = z.union([SerializedStringSchema, SerializedLiteralSchema, SerializedBooleanSchema, SerializedNumberSchema, SerializedObjectSchema, SerializedArraySchema, SerializedUnionSchema, SerializedRichTextSchema, SerializedRecordSchema, SerializedKeyOfSchema, SerializedRouteSchema, SerializedFileSchema, SerializedDateSchema, SerializedDateTimeSchema, SerializedColorSchema, SerializedCodeSchema, SerializedSettingsSchema, SerializedImageSchema]);
1279
1304
 
1280
1305
  var ValCommit = z.object({
1281
1306
  commitSha: z.string(),
@@ -1287,6 +1312,125 @@ var ValCommit = z.object({
1287
1312
  commitMessage: z.string().nullable()
1288
1313
  });
1289
1314
 
1315
+ /**
1316
+ * The wire shapes for reading history.
1317
+ *
1318
+ * Here rather than in `@valbuild/server` because both sides of `/api/val` need
1319
+ * them: the server builds these and the Studio parses them, and a second
1320
+ * hand-written copy in the client is how the two drift.
1321
+ *
1322
+ * `HistoryError` is a closed union of everything that can go wrong
1323
+ * reconstructing a commit - see `packages/server/src/history/HistoryError.ts`
1324
+ * for what each one means. It travels as data rather than as a message because
1325
+ * the Studio decides what to OFFER from it: a schema mismatch means "cannot
1326
+ * restore this field", a missing source means "cannot restore this module at
1327
+ * all", and a rendered string cannot be told apart.
1328
+ */
1329
+ var HistoryError = z.discriminatedUnion("kind", [z.object({
1330
+ kind: z.literal("commit-not-found"),
1331
+ commitSha: z.string()
1332
+ }), z.object({
1333
+ kind: z.literal("archive-unreadable"),
1334
+ commitSha: z.string(),
1335
+ message: z.string()
1336
+ }), z.object({
1337
+ kind: z.literal("source-unavailable"),
1338
+ moduleFilePath: z.string()
1339
+ }), z.object({
1340
+ kind: z.literal("schema-unreadable"),
1341
+ moduleFilePath: z.string(),
1342
+ message: z.string()
1343
+ }), z.object({
1344
+ kind: z.literal("file-unavailable"),
1345
+ gitPath: z.string(),
1346
+ message: z.string()
1347
+ }), z.object({
1348
+ kind: z.literal("not-supported-in-fs-mode")
1349
+ }), z.object({
1350
+ kind: z.literal("transport"),
1351
+ message: z.string()
1352
+ })]);
1353
+ var HistoricalCommit = z.object({
1354
+ commitSha: z.string(),
1355
+ parentCommitSha: z.string(),
1356
+ clientCommitSha: z.string(),
1357
+ branch: z.string(),
1358
+ createdBranch: z.string().nullable(),
1359
+ creator: z.string().nullable(),
1360
+ message: z.string().nullable(),
1361
+ createdAt: z.string(),
1362
+ seqNum: z.string(),
1363
+ patchCount: z.number(),
1364
+ /** False for commits made before history was recorded. */
1365
+ hasArchive: z["boolean"]()
1366
+ });
1367
+ var FileChange = z.union([z.literal("added"), z.literal("modified"), z.literal("deleted")]);
1368
+
1369
+ /**
1370
+ * A binary file the commit touched - named, with a URL, and nothing fetched.
1371
+ * Only an `<img src>` that mounts pays for the bytes.
1372
+ */
1373
+ var BinaryFileRef = z.object({
1374
+ gitPath: z.string(),
1375
+ change: FileChange,
1376
+ remote: z["boolean"](),
1377
+ url: z.string()
1378
+ });
1379
+ var HistoricalModule = z.object({
1380
+ /** The module's data at this commit. Null if deleted, or unreadable. */
1381
+ source: JSONValue.nullable(),
1382
+ /**
1383
+ * The schema at this commit, already validated against THIS Val.
1384
+ *
1385
+ * Null with a `schema-unreadable` failure means the stored schema came from a
1386
+ * Val whose schema format this one does not know — expected, not damage.
1387
+ */
1388
+ schema: SerializedSchema.nullable(),
1389
+ patchIds: z.array(PatchId),
1390
+ changedPaths: z.array(z.string()),
1391
+ failures: z.array(HistoryError)
1392
+ });
1393
+ var HistoricalPatchSet = z.object({
1394
+ commit: HistoricalCommit,
1395
+ modules: z.record(z.string(), HistoricalModule),
1396
+ patches: z.array(z.object({
1397
+ patchId: PatchId,
1398
+ moduleFilePath: z.string(),
1399
+ patch: z.unknown(),
1400
+ authorId: z.string().nullable(),
1401
+ createdAt: z.string(),
1402
+ baseSha: z.string(),
1403
+ coreVersion: z.string()
1404
+ })),
1405
+ jsonEntries: z.record(z.string(), z.unknown()),
1406
+ binaryFiles: z.array(BinaryFileRef),
1407
+ warnings: z.array(HistoryError)
1408
+ });
1409
+ /**
1410
+ * Whether the historical value can be written back as the project stands.
1411
+ *
1412
+ * Computed in the Studio, not on the server: the verdict depends on the current
1413
+ * source and the current schema, both of which the client already holds, and it
1414
+ * runs against the same worker-thread validator that validates ordinary edits.
1415
+ * The types stay here because they are what a restore preview hands to the UI.
1416
+ *
1417
+ * `blocked` carries its reasons so a refusal can name the field and say why,
1418
+ * rather than being an unexplained disabled button.
1419
+ */
1420
+ var RestoreVerdict = z.union([z.object({
1421
+ status: z.literal("restorable")
1422
+ }), z.object({
1423
+ status: z.literal("blocked"),
1424
+ reasons: z.array(HistoryError)
1425
+ })]);
1426
+ var ModuleComparison = z.object({
1427
+ current: z.unknown().nullable(),
1428
+ /** Paths where the commit's result and the current source differ: what a
1429
+ * restore would undo. */
1430
+ changedVsCurrent: z.array(z.string()),
1431
+ verdict: RestoreVerdict
1432
+ });
1433
+
1290
1434
  var ModuleFilePath = z.string().refine(function (_path) {
1291
1435
  return true;
1292
1436
  } // TODO: validation
@@ -1329,6 +1473,21 @@ var notFoundResponse = z.object({
1329
1473
  var GenericError = z.object({
1330
1474
  message: z.string()
1331
1475
  });
1476
+
1477
+ /**
1478
+ * A patch group: the set of patches one user has chosen to publish.
1479
+ *
1480
+ * Not a patch *set* — a patch set is computed from the schema and says which
1481
+ * patches must move together; a patch group is curated and says which ones this
1482
+ * user wants live. See `docs/independent-publish/PLAN.md`.
1483
+ */
1484
+ var PatchGroup = z.object({
1485
+ patchGroupId: z.string(),
1486
+ authorId: z.string().nullable(),
1487
+ createdAt: z.string(),
1488
+ publishedAt: z.string().nullable(),
1489
+ patchIds: z.array(PatchId)
1490
+ });
1332
1491
  var GenericPatchError = z.union([z.object({
1333
1492
  patchId: PatchId,
1334
1493
  message: z.string()
@@ -1541,48 +1700,6 @@ var Api = {
1541
1700
  })
1542
1701
  }
1543
1702
  },
1544
- "/session": {
1545
- GET: {
1546
- req: {
1547
- cookies: {
1548
- val_session: z.string().optional()
1549
- }
1550
- },
1551
- res: z.union([z.object({
1552
- status: z.literal(200),
1553
- json: z.object({
1554
- mode: z.union([z.literal("local"), z.literal("proxy")]),
1555
- enabled: z["boolean"]()
1556
- })
1557
- }), z.object({
1558
- status: z.union([
1559
- // TODO: Remove the ones we don't need.
1560
- z.literal(400), z.literal(401), z.literal(403), z.literal(404), z.literal(500), z.literal(501)]),
1561
- json: z.object({
1562
- message: z.string()
1563
- })
1564
- }), z.object({
1565
- status: z.literal(401),
1566
- json: z.object({
1567
- message: z.string(),
1568
- details: z.union([z.string(), z.object({
1569
- reason: z.string()
1570
- }), z.object({
1571
- sub: z.string(),
1572
- exp: z.number(),
1573
- token: z.string(),
1574
- org: z.string(),
1575
- project: z.string()
1576
- })])
1577
- })
1578
- }), z.object({
1579
- status: z.literal(500),
1580
- json: z.object({
1581
- message: z.string()
1582
- })
1583
- })])
1584
- }
1585
- },
1586
1703
  "/logout": {
1587
1704
  GET: {
1588
1705
  req: {
@@ -1710,6 +1827,30 @@ var Api = {
1710
1827
  schemaSha: z.string(),
1711
1828
  sourcesSha: z.string(),
1712
1829
  patches: z.array(PatchId),
1830
+ /**
1831
+ * Of `patches`, the ones that have already SHIPPED.
1832
+ *
1833
+ * A published patch stays in the chain with `appliedAt` set until
1834
+ * the next deployment moves the base, so the id list alone cannot
1835
+ * say which is which — and a client never re-fetches a record it
1836
+ * already holds, so it never finds out on its own. Another
1837
+ * author's publish then left that patch pending in your scope,
1838
+ * your prefix gate read a hole in front of it, and Publish
1839
+ * refused for a reason that had stopped being true.
1840
+ *
1841
+ * Optional: `fs` mode forgets published patches outright, and a
1842
+ * client that ignores this behaves exactly as it did before.
1843
+ */
1844
+ appliedPatches: z.array(PatchId).optional(),
1845
+ /**
1846
+ * The newest commit, which is the PUBLISH head.
1847
+ *
1848
+ * Not `commitSha`: that is the commit this deployment serves and
1849
+ * moves only when a new build lands. This moves the instant
1850
+ * somebody publishes, which is what makes it the thing a client
1851
+ * carries back to `/save` to say which world it decided against.
1852
+ */
1853
+ headCommitSha: z.string().optional(),
1713
1854
  /**
1714
1855
  * Unpublished changes the store threw away because it could not
1715
1856
  * read them.
@@ -1738,6 +1879,30 @@ var Api = {
1738
1879
  sourcesSha: z.string(),
1739
1880
  commitSha: z.string(),
1740
1881
  patches: z.array(PatchId),
1882
+ /**
1883
+ * Of `patches`, the ones that have already SHIPPED.
1884
+ *
1885
+ * A published patch stays in the chain with `appliedAt` set until
1886
+ * the next deployment moves the base, so the id list alone cannot
1887
+ * say which is which — and a client never re-fetches a record it
1888
+ * already holds, so it never finds out on its own. Another
1889
+ * author's publish then left that patch pending in your scope,
1890
+ * your prefix gate read a hole in front of it, and Publish
1891
+ * refused for a reason that had stopped being true.
1892
+ *
1893
+ * Optional: `fs` mode forgets published patches outright, and a
1894
+ * client that ignores this behaves exactly as it did before.
1895
+ */
1896
+ appliedPatches: z.array(PatchId).optional(),
1897
+ /**
1898
+ * The newest commit, which is the PUBLISH head.
1899
+ *
1900
+ * Not `commitSha`: that is the commit this deployment serves and
1901
+ * moves only when a new build lands. This moves the instant
1902
+ * somebody publishes, which is what makes it the thing a client
1903
+ * carries back to `/save` to say which world it decided against.
1904
+ */
1905
+ headCommitSha: z.string().optional(),
1741
1906
  commits: z.array(ValCommit),
1742
1907
  config: ValConfig,
1743
1908
  profileId: z.string().nullable(),
@@ -1807,6 +1972,26 @@ var Api = {
1807
1972
  query: {
1808
1973
  id: z.array(PatchId).min(1, "At least one patch id is required")
1809
1974
  },
1975
+ body: z.object({
1976
+ /*
1977
+ * Patches that are NOT being deleted but must lose their patch
1978
+ * group membership because these are.
1979
+ *
1980
+ * Deleting a patch out of the middle of a patch set leaves every
1981
+ * group holding the rest with a non-prefix intersection, and a
1982
+ * prefix is the one invariant a group has. Which patches those are
1983
+ * needs the content schema, so the client computes the forward
1984
+ * closure and sends it; the content API drops the memberships
1985
+ * without touching the patches.
1986
+ *
1987
+ * In the BODY while the ids to delete are in the query, because
1988
+ * this list is unbounded in the same way and the query string is
1989
+ * already chunked to fit. Optional throughout: `fs` mode has no
1990
+ * groups, and a client that predates this is deleting correctly,
1991
+ * just not repairing.
1992
+ */
1993
+ unstagePatchIds: z.array(PatchId).optional()
1994
+ }).optional(),
1810
1995
  cookies: {
1811
1996
  val_session: z.string().optional()
1812
1997
  }
@@ -1831,7 +2016,16 @@ var Api = {
1831
2016
  path: ModuleFilePath,
1832
2017
  patchId: PatchId,
1833
2018
  patch: z.any() // TODO: this should be Patch instead - we got a weird validation error: although input looks good, it still does not accept objects as values... Which it should do via the z.record(JSONValue) type
1834
- }))
2019
+ })),
2020
+ // Patch group membership. Sent with the patch itself, in one request, so
2021
+ // there is no window in which a patch exists but belongs to no group.
2022
+ // Absent means "the caller does not know about groups" - the server then
2023
+ // behaves exactly as before.
2024
+ patchGroupId: z.string().nullish(),
2025
+ // What has to come with the patch: other patch ids that must join the
2026
+ // same group for it to stay applicable. Computed on the client, which
2027
+ // is the only side that has the schema needed to derive patch sets.
2028
+ withPatchIds: z.array(PatchId).optional()
1835
2029
  }),
1836
2030
  cookies: {
1837
2031
  val_session: z.string().optional()
@@ -1857,7 +2051,8 @@ var Api = {
1857
2051
  status: z.literal(200),
1858
2052
  json: z.object({
1859
2053
  newPatchIds: z.array(PatchId),
1860
- parentRef: ParentRef
2054
+ parentRef: ParentRef,
2055
+ patchGroupId: z.string().optional()
1861
2056
  })
1862
2057
  })])
1863
2058
  },
@@ -1865,7 +2060,16 @@ var Api = {
1865
2060
  req: {
1866
2061
  query: {
1867
2062
  patch_id: z.array(PatchId).optional(),
1868
- exclude_patch_ops: onlyOneBooleanQueryParam.optional()
2063
+ exclude_patch_ops: onlyOneBooleanQueryParam.optional(),
2064
+ /**
2065
+ * Include the patch group annotation in the answer.
2066
+ *
2067
+ * Off by default because the client CHUNKS this request — a long
2068
+ * chain is several round trips — and the groups are a property of the
2069
+ * branch, identical in every chunk. Asking on each one is N identical
2070
+ * lookups against the content API to learn the same thing.
2071
+ */
2072
+ include_patch_groups: onlyOneBooleanQueryParam.optional()
1869
2073
  },
1870
2074
  cookies: {
1871
2075
  val_session: z.string().optional()
@@ -1894,8 +2098,12 @@ var Api = {
1894
2098
  authorId: z.string().nullable(),
1895
2099
  appliedAt: z.object({
1896
2100
  commitSha: z.string()
1897
- }).nullable()
2101
+ }).nullable(),
2102
+ // Which patch groups hold this patch. Optional so that FS mode, and
2103
+ // any server that predates patch groups, can leave it out.
2104
+ patchGroupIds: z.array(z.string()).optional()
1898
2105
  })),
2106
+ patchGroups: z.array(PatchGroup).optional(),
1899
2107
  baseSha: z.string(),
1900
2108
  error: GenericError.optional(),
1901
2109
  errors: z.record(PatchId, GenericError).optional()
@@ -1903,6 +2111,91 @@ var Api = {
1903
2111
  })])
1904
2112
  }
1905
2113
  },
2114
+ // Patch group membership. Both are idempotent set operations, and both take an
2115
+ // already-closed set: the client computes the prefix closure (staging) or the
2116
+ // forward closure (unstaging) because only the client has the schema needed to
2117
+ // derive patch sets. See `docs/independent-publish/PLAN.md`.
2118
+ "/patch-groups/~/patches": {
2119
+ PUT: {
2120
+ req: {
2121
+ body: z.object({
2122
+ patchGroupId: z.string(),
2123
+ /** What the user asked to stage. */
2124
+ patchIds: z.array(PatchId),
2125
+ /*
2126
+ * What has to come with it, because the staged patches are written on
2127
+ * top of it.
2128
+ *
2129
+ * The content API records membership as `explicit` or `dependency`
2130
+ * and reads what it is not told about as a dependency — so folding
2131
+ * the two halves into `patchIds` files the patch someone clicked as
2132
+ * something the closure dragged in.
2133
+ */
2134
+ withPatchIds: z.array(PatchId).optional()
2135
+ }).refine(function (body) {
2136
+ var _body$withPatchIds$le, _body$withPatchIds;
2137
+ return body.patchIds.length > 0 || ((_body$withPatchIds$le = (_body$withPatchIds = body.withPatchIds) === null || _body$withPatchIds === void 0 ? void 0 : _body$withPatchIds.length) !== null && _body$withPatchIds$le !== void 0 ? _body$withPatchIds$le : 0) > 0;
2138
+ },
2139
+ // Either half alone is a real request: a stage of only what came with
2140
+ // it happens when the deferred queue's scope filter drops everything
2141
+ // the user originally clicked.
2142
+ "At least one patch id is required"),
2143
+ cookies: {
2144
+ val_session: z.string().optional()
2145
+ }
2146
+ },
2147
+ res: z.union([unauthorizedResponse, z.object({
2148
+ status: z.literal(403),
2149
+ json: GenericError
2150
+ }), z.object({
2151
+ // The group has already been published, so its membership is frozen.
2152
+ status: z.literal(409),
2153
+ json: GenericError
2154
+ }), z.object({
2155
+ status: z.literal(500),
2156
+ json: GenericError
2157
+ }), z.object({
2158
+ status: z.literal(200),
2159
+ json: z.object({
2160
+ patchGroupId: z.string(),
2161
+ patchIds: z.array(PatchId)
2162
+ })
2163
+ })])
2164
+ },
2165
+ DELETE: {
2166
+ req: {
2167
+ body: z.object({
2168
+ patchGroupId: z.string(),
2169
+ /** What the user asked to unstage. */
2170
+ patchIds: z.array(PatchId),
2171
+ /** What has to go with it: everything built on top of it. */
2172
+ withPatchIds: z.array(PatchId).optional()
2173
+ }).refine(function (body) {
2174
+ var _body$withPatchIds$le2, _body$withPatchIds2;
2175
+ return body.patchIds.length > 0 || ((_body$withPatchIds$le2 = (_body$withPatchIds2 = body.withPatchIds) === null || _body$withPatchIds2 === void 0 ? void 0 : _body$withPatchIds2.length) !== null && _body$withPatchIds$le2 !== void 0 ? _body$withPatchIds$le2 : 0) > 0;
2176
+ }, "At least one patch id is required"),
2177
+ cookies: {
2178
+ val_session: z.string().optional()
2179
+ }
2180
+ },
2181
+ res: z.union([unauthorizedResponse, z.object({
2182
+ status: z.literal(403),
2183
+ json: GenericError
2184
+ }), z.object({
2185
+ status: z.literal(409),
2186
+ json: GenericError
2187
+ }), z.object({
2188
+ status: z.literal(500),
2189
+ json: GenericError
2190
+ }), z.object({
2191
+ status: z.literal(200),
2192
+ json: z.object({
2193
+ patchGroupId: z.string(),
2194
+ patchIds: z.array(PatchId)
2195
+ })
2196
+ })])
2197
+ }
2198
+ },
1906
2199
  "/schema": {
1907
2200
  GET: {
1908
2201
  req: {
@@ -1939,7 +2232,34 @@ var Api = {
1939
2232
  validate_sources: onlyOneBooleanQueryParam.optional(),
1940
2233
  validate_binary_files: onlyOneBooleanQueryParam.optional(),
1941
2234
  exclude_patches: onlyOneBooleanQueryParam.optional(),
1942
- apply_patches: onlyOneBooleanQueryParam.optional()
2235
+ apply_patches: onlyOneBooleanQueryParam.optional(),
2236
+ /**
2237
+ * Apply ONLY these patches, rather than everything pending.
2238
+ *
2239
+ * This is what makes a draft-mode render match the caller's staged
2240
+ * view. Without it `/sources/~` replays every pending patch on the
2241
+ * branch, so a server-rendered preview shows other people's
2242
+ * unpublished work — the one thing independent publish is for.
2243
+ *
2244
+ * Omitted means everything pending, which is the behaviour every
2245
+ * existing caller has and must keep. An EMPTY array is a different
2246
+ * answer and is honoured as one: a group holding nothing renders
2247
+ * base.
2248
+ */
2249
+ patch_id: z.array(PatchId).optional(),
2250
+ /**
2251
+ * Apply only the patches in the CALLER'S OWN patch groups.
2252
+ *
2253
+ * The server resolves them from the session, so a draft render does
2254
+ * not have to know its own group ids — which it cannot, since it has
2255
+ * no client state. This is what makes `fetchVal` in draft mode show
2256
+ * the person their own staged work rather than everybody's.
2257
+ *
2258
+ * Ignored when `patch_id` is given: an explicit list is a caller that
2259
+ * already knows what it wants, and silently intersecting the two
2260
+ * would make the narrower one invisible.
2261
+ */
2262
+ own_patch_groups_only: onlyOneBooleanQueryParam.optional()
1943
2263
  },
1944
2264
  cookies: {
1945
2265
  val_session: z.string().optional()
@@ -2018,7 +2338,35 @@ var Api = {
2018
2338
  req: {
2019
2339
  body: z.object({
2020
2340
  message: z.string().optional(),
2021
- patchIds: z.array(PatchId)
2341
+ patchIds: z.array(PatchId),
2342
+ /*
2343
+ * The patch group this commit EMPTIES, if it empties one.
2344
+ *
2345
+ * Forwarded to the content API, which closes the group it names —
2346
+ * and closes it unconditionally, without checking that the commit
2347
+ * shipped all of it. So the client sends it only when the publish
2348
+ * accounts for every patch the group still holds; a partial publish
2349
+ * omits it and the group stays open with the rest in it.
2350
+ *
2351
+ * Optional, and absent in `fs` mode, which has no groups.
2352
+ */
2353
+ patchGroupId: z.string().optional(),
2354
+ /*
2355
+ * The newest commit this client knew about when it decided.
2356
+ *
2357
+ * Answered with 409 when the server sees a newer one, because then
2358
+ * somebody else published between the review screen being read and
2359
+ * Save being clicked, and what is about to ship was decided against a
2360
+ * world that no longer exists.
2361
+ *
2362
+ * Git's own not-fast-forward guard does not cover this: `ValServer`
2363
+ * fetches the chain and commits fresh at publish time, so the parent
2364
+ * commit it sends is always the server's current one.
2365
+ *
2366
+ * Optional, so a client that does not track it publishes exactly as
2367
+ * it did before.
2368
+ */
2369
+ expectedHeadCommitSha: z.string().optional()
2022
2370
  }),
2023
2371
  cookies: {
2024
2372
  val_session: z.string().optional()
@@ -2027,6 +2375,24 @@ var Api = {
2027
2375
  res: z.union([unauthorizedResponse, z.object({
2028
2376
  status: z.literal(200),
2029
2377
  json: z.object({
2378
+ /**
2379
+ * The commit this publish just made, which becomes the new publish
2380
+ * head.
2381
+ *
2382
+ * Sent back because nothing else tells this client about its own
2383
+ * commit in time. `headCommitSha` moves on a `/stat` response, and
2384
+ * until the next poll lands the client still believes the
2385
+ * PRE-publish head — so a second publish inside that window carried
2386
+ * a stale `expectedHeadCommitSha`, the server compared it against
2387
+ * the commit this same client had just made, and answered 409
2388
+ * "someone else published". With auto-publish, which publishes on
2389
+ * every pause in typing, that window is hit routinely and the
2390
+ * message blames a colleague for the user's own commit.
2391
+ *
2392
+ * Optional: `fs` mode has no publish head, and neither does a
2393
+ * server that predates this.
2394
+ */
2395
+ commitSha: z.string().optional(),
2030
2396
  /**
2031
2397
  * Unpublished changes the save threw away because they could not be
2032
2398
  * applied.
@@ -2043,12 +2409,61 @@ var Api = {
2043
2409
  message: z.string()
2044
2410
  })).optional()
2045
2411
  })
2412
+ }),
2413
+ /*
2414
+ * The named patch group is not this caller's, or the lookup that would
2415
+ * say so failed.
2416
+ *
2417
+ * Publishing may CLOSE a group, and closing somebody else's leaves
2418
+ * their pending work in a closed group and in no open one. So `/save`
2419
+ * runs the same ownership check stage and unstage do, and refuses the
2420
+ * same way — see `refuseUnlessOwn`.
2421
+ */
2422
+ z.object({
2423
+ status: z.literal(403),
2424
+ json: GenericError
2425
+ }), z.object({
2426
+ status: z.literal(500),
2427
+ json: GenericError
2046
2428
  }), z.object({
2047
2429
  status: z.literal(409),
2048
- json: z.object({
2430
+ json: z.union([/** A group that has already shipped. See the 403 above. */
2431
+ z.object({
2432
+ message: z.string()
2433
+ }), z.object({
2049
2434
  message: z.string(),
2050
2435
  isNotFastForward: z.literal(true)
2051
- })
2436
+ }),
2437
+ /*
2438
+ * Somebody else published between this being decided and Save.
2439
+ *
2440
+ * Its own shape rather than another `isNotFastForward`, because
2441
+ * they happen at different moments and mean different things to the
2442
+ * user: that one is git refusing a commit, this one is refused
2443
+ * BEFORE a commit is attempted, on a head the client named. Nothing
2444
+ * was written either way, but only this one is answered by looking
2445
+ * at the review screen again.
2446
+ */
2447
+ z.object({
2448
+ message: z.string(),
2449
+ headMoved: z.literal(true)
2450
+ }),
2451
+ /**
2452
+ * The group this commit named has already been published.
2453
+ *
2454
+ * A third thing sharing 409, and flagged for the same reason
2455
+ * `headMoved` is: read as a plain not-fast-forward it looks
2456
+ * retryable, and it is the opposite — the id will never be writable
2457
+ * again, so retrying reproduces it forever.
2458
+ *
2459
+ * It means this author published from somewhere else. The client
2460
+ * forgets the id and falls back to the annotation, which is the
2461
+ * same recovery a 409 on stage or unstage triggers.
2462
+ */
2463
+ z.object({
2464
+ message: z.string(),
2465
+ patchGroupPublished: z.literal(true)
2466
+ })])
2052
2467
  }), z.object({
2053
2468
  status: z.literal(400),
2054
2469
  json: z.union([z.object({
@@ -2076,6 +2491,144 @@ var Api = {
2076
2491
  })])
2077
2492
  }
2078
2493
  },
2494
+ // #region history
2495
+ //
2496
+ // Reading the past. Split across three routes on purpose, because they cache
2497
+ // differently: a reconstructed commit cannot change and is immutable
2498
+ // forever, the comparison against current source changes with every edit, and
2499
+ // a file at a fixed commit is immutable too. Folding them together would mean
2500
+ // the cheapest thing dragging the most expensive one's cache headers down to
2501
+ // no-store.
2502
+ "/history/commits": {
2503
+ GET: {
2504
+ req: {
2505
+ query: {
2506
+ branch: onlyOneStringQueryParam,
2507
+ limit: onlyOneIntQueryParam.optional(),
2508
+ /** The previous page's `nextCursor`. */
2509
+ cursor: onlyOneStringQueryParam.optional()
2510
+ },
2511
+ cookies: {
2512
+ val_session: z.string().optional()
2513
+ }
2514
+ },
2515
+ res: z.union([unauthorizedResponse, notFoundResponse, z.object({
2516
+ status: z.literal(400),
2517
+ json: GenericError.and(z.object({
2518
+ kind: z.string().optional()
2519
+ }))
2520
+ }), z.object({
2521
+ status: z.literal(500),
2522
+ json: GenericError.and(z.object({
2523
+ kind: z.string().optional()
2524
+ }))
2525
+ }), z.object({
2526
+ status: z.literal(200),
2527
+ json: z.object({
2528
+ commits: z.array(HistoricalCommit),
2529
+ nextCursor: z.string().nullable()
2530
+ })
2531
+ })])
2532
+ }
2533
+ },
2534
+ /**
2535
+ * One commit, reconstructed. IMMUTABLE for a given sha - it says nothing
2536
+ * about the current source or schema, which is what lets it be cached
2537
+ * forever and what makes comparing many commits cheap.
2538
+ */
2539
+ "/history/commit": {
2540
+ GET: {
2541
+ req: {
2542
+ query: {
2543
+ commit_sha: onlyOneStringQueryParam
2544
+ },
2545
+ cookies: {
2546
+ val_session: z.string().optional()
2547
+ }
2548
+ },
2549
+ res: z.union([unauthorizedResponse, notFoundResponse, z.object({
2550
+ status: z.literal(400),
2551
+ json: GenericError
2552
+ }), z.object({
2553
+ status: z.literal(500),
2554
+ json: GenericError.and(z.object({
2555
+ kind: z.string().optional()
2556
+ }))
2557
+ }), z.object({
2558
+ status: z.literal(200),
2559
+ json: HistoricalPatchSet
2560
+ })])
2561
+ }
2562
+ },
2563
+ /**
2564
+ * ONE module, as of a commit — including a commit that did not change it.
2565
+ *
2566
+ * `/history/commit` covers what a commit changed, which is the narrow case:
2567
+ * navigate the history pane to anything else and there would be nothing to
2568
+ * show. This answers "how did this module look at that point", which is the
2569
+ * question someone comparing two panes is actually asking. Immutable for the
2570
+ * same reason: what a commit left behind cannot change.
2571
+ */
2572
+ "/history/module": {
2573
+ GET: {
2574
+ req: {
2575
+ query: {
2576
+ commit_sha: onlyOneStringQueryParam,
2577
+ module_file_path: onlyOneStringQueryParam
2578
+ },
2579
+ cookies: {
2580
+ val_session: z.string().optional()
2581
+ }
2582
+ },
2583
+ res: z.union([unauthorizedResponse, notFoundResponse, z.object({
2584
+ status: z.literal(400),
2585
+ json: GenericError
2586
+ }), z.object({
2587
+ status: z.literal(500),
2588
+ json: GenericError.and(z.object({
2589
+ kind: z.string().optional()
2590
+ }))
2591
+ }), z.object({
2592
+ status: z.literal(200),
2593
+ json: z.object({
2594
+ moduleFilePath: z.string(),
2595
+ /**
2596
+ * Null when history has no record of this module at or before the
2597
+ * commit — it was last edited before recording started, or never.
2598
+ * Distinct from a module the commit deleted.
2599
+ */
2600
+ module: HistoricalModule.nullable()
2601
+ })
2602
+ })])
2603
+ }
2604
+ },
2605
+ /**
2606
+ * One file as it was at one commit.
2607
+ *
2608
+ * The lazy half: `/history/commit` NAMES a commit's binary files and this is
2609
+ * where their bytes come from, so nothing is fetched until an `<img>`
2610
+ * actually mounts. A file at a fixed commit cannot change, so the response is
2611
+ * immutable and the browser's own cache handles flipping between commits.
2612
+ */
2613
+ "/history/files": {
2614
+ GET: {
2615
+ req: {
2616
+ query: {
2617
+ commit_sha: onlyOneStringQueryParam,
2618
+ path: onlyOneStringQueryParam,
2619
+ remote: onlyOneStringQueryParam.optional()
2620
+ }
2621
+ },
2622
+ res: z.union([unauthorizedResponse, notFoundResponse, z.object({
2623
+ status: z.literal(400),
2624
+ json: GenericError
2625
+ }), z.object({
2626
+ status: z.literal(200),
2627
+ body: z["instanceof"](ReadableStream)
2628
+ })])
2629
+ }
2630
+ },
2631
+ // #endregion history
2079
2632
  "/files": {
2080
2633
  GET: {
2081
2634
  req: {
@@ -2129,7 +2682,16 @@ var Api = {
2129
2682
  limit: onlyOneIntQueryParam.refine(function (arg) {
2130
2683
  return arg > 0 && arg <= JSON_ENTRIES_BATCH_MAX;
2131
2684
  }, "limit must be between 1 and ".concat(JSON_ENTRIES_BATCH_MAX)).optional(),
2132
- apply_patches: onlyOneBooleanQueryParam.optional()
2685
+ apply_patches: onlyOneBooleanQueryParam.optional(),
2686
+ /**
2687
+ * The caller's own staged work, and nobody else's.
2688
+ *
2689
+ * The same flag `/sources/~` takes, and for the same reason — a draft
2690
+ * page renders both, so a scoped module beside an unscoped
2691
+ * `jsonValues` entry showed one person's view and everybody's pending
2692
+ * work on the same screen.
2693
+ */
2694
+ own_patch_groups_only: onlyOneBooleanQueryParam.optional()
2133
2695
  },
2134
2696
  cookies: _defineProperty({}, VAL_SESSION_COOKIE, z.string().optional())
2135
2697
  },
@@ -2703,6 +3265,392 @@ var urlOf = function urlOf() {
2703
3265
  return "".concat(route);
2704
3266
  };
2705
3267
 
3268
+ /**
3269
+ * Can this value, taken from one schema, be written into another?
3270
+ *
3271
+ * The question restore asks, and it is NOT the question validation asks.
3272
+ * Validation checks a value against the schema it belongs to. This compares two
3273
+ * schemas — the one a value was stored under and the one it is being written
3274
+ * into — because a restore moves a value across a schema change, and possibly
3275
+ * across paths.
3276
+ *
3277
+ * Three answers, not two. `unknown` is where the comparison genuinely cannot
3278
+ * decide, and it exists so that an editor is told "we are not sure" rather than
3279
+ * being given a confident answer we made up. Treating unknown as compatible
3280
+ * writes values that break; treating it as incompatible blocks restores that
3281
+ * would have been fine.
3282
+ */
3283
+
3284
+ var compatible = {
3285
+ status: "compatible"
3286
+ };
3287
+
3288
+ /**
3289
+ * `Array.isArray` cannot narrow `JsonArray` out of `Json`, because it is
3290
+ * `readonly Json[]` — so the check that reads naturally does not type. This
3291
+ * predicate is that check, said once.
3292
+ */
3293
+ function isJsonObject(value) {
3294
+ return _typeof(value) === "object" && value !== null && !Array.isArray(value);
3295
+ }
3296
+ function incompatible(reason) {
3297
+ return {
3298
+ status: "incompatible",
3299
+ reason: reason
3300
+ };
3301
+ }
3302
+
3303
+ /**
3304
+ * "an object", "a string".
3305
+ *
3306
+ * Schema type names are data — `object`, `array`, `image` — so an interpolated
3307
+ * "a" is wrong for three of them, in a sentence shown to editors the moment a
3308
+ * restore is refused.
3309
+ */
3310
+ function withArticle(type) {
3311
+ return /^[aeiou]/i.test(type) ? "an ".concat(type) : "a ".concat(type);
3312
+ }
3313
+
3314
+ /**
3315
+ * A sentence an editor can act on, for the popup that explains a refusal.
3316
+ *
3317
+ * Kept beside the reasons rather than in the UI so that adding a reason without
3318
+ * wording for it is a type error rather than a blank dialog.
3319
+ */
3320
+ function explainIncompatible(reason) {
3321
+ switch (reason.kind) {
3322
+ case "type-changed":
3323
+ return "This was ".concat(withArticle(reason.from), " and the field here is ").concat(withArticle(reason.to), ".");
3324
+ case "no-matching-variant":
3325
+ return "This does not match any of the shapes allowed here: ".concat(reason.variants.join(", "), ".");
3326
+ case "unknown-field":
3327
+ return "The old value has ".concat(reason.keys.length === 1 ? "a field" : "fields", " that no longer exist here: ").concat(reason.keys.join(", "), ".");
3328
+ case "missing-required":
3329
+ return "The field here needs ".concat(reason.keys.join(", "), ", which the old value does not have.");
3330
+ case "not-optional":
3331
+ return "The old value was empty, and this field cannot be empty.";
3332
+ case "literal-changed":
3333
+ return "This has to be exactly \"".concat(reason.to, "\", and the old value was \"").concat(reason.from, "\".");
3334
+ case "different-module":
3335
+ return "This points into ".concat(reason.to, ", and the old value pointed into ").concat(reason.from, ".");
3336
+ }
3337
+ }
3338
+
3339
+ /**
3340
+ * The variant of a union that a value actually is.
3341
+ *
3342
+ * `undefined` when the value does not identify itself — which is a real answer,
3343
+ * not a failure: it is what makes the caller return `unknown` rather than guess.
3344
+ */
3345
+ function variantOf(schema, value) {
3346
+ if (schema.type !== "union") {
3347
+ return undefined;
3348
+ }
3349
+ // String union: the value IS the discriminator.
3350
+ if (typeof schema.key !== "string") {
3351
+ if (typeof value !== "string") {
3352
+ return undefined;
3353
+ }
3354
+ return schema.items.find(function (item) {
3355
+ return item.type === "literal" && item.value === value;
3356
+ });
3357
+ }
3358
+ // Object union: the discriminator is a literal at `key`.
3359
+ var discriminator = schema.key;
3360
+ if (!isJsonObject(value)) {
3361
+ return undefined;
3362
+ }
3363
+ var tag = value[discriminator];
3364
+ if (typeof tag !== "string") {
3365
+ return undefined;
3366
+ }
3367
+ return schema.items.find(function (item) {
3368
+ if (item.type !== "object") {
3369
+ return false;
3370
+ }
3371
+ var keySchema = item.items[discriminator];
3372
+ return (keySchema === null || keySchema === void 0 ? void 0 : keySchema.type) === "literal" && keySchema.value === tag;
3373
+ });
3374
+ }
3375
+ function describeVariant(schema, key) {
3376
+ if (schema.type === "literal") {
3377
+ return schema.value;
3378
+ }
3379
+ if (schema.type === "object") {
3380
+ var tag = schema.items[key];
3381
+ if ((tag === null || tag === void 0 ? void 0 : tag.type) === "literal") {
3382
+ return tag.value;
3383
+ }
3384
+ }
3385
+ return schema.type;
3386
+ }
3387
+ function checkCompatibility(from, to) {
3388
+ // Emptiness first: a null that lands in a field which cannot be empty is a
3389
+ // break regardless of what the two schemas otherwise say.
3390
+ if (from.value === null) {
3391
+ return to.schema.opt ? compatible : incompatible({
3392
+ kind: "not-optional"
3393
+ });
3394
+ }
3395
+
3396
+ // A changed union is not itself disqualifying. What matters is whether the
3397
+ // variant being restored still has somewhere to go — comparing the unions
3398
+ // wholesale would refuse a restore that is perfectly safe.
3399
+ if (to.schema.type === "union") {
3400
+ var key = typeof to.schema.key === "string" ? to.schema.key : "";
3401
+ var _iterator = _createForOfIteratorHelper(to.schema.items),
3402
+ _step;
3403
+ try {
3404
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
3405
+ var variant = _step.value;
3406
+ if (checkCompatibility(from, {
3407
+ schema: variant
3408
+ }).status === "compatible") {
3409
+ return compatible;
3410
+ }
3411
+ }
3412
+ } catch (err) {
3413
+ _iterator.e(err);
3414
+ } finally {
3415
+ _iterator.f();
3416
+ }
3417
+ return incompatible({
3418
+ kind: "no-matching-variant",
3419
+ variants: to.schema.items.map(function (item) {
3420
+ return describeVariant(item, key);
3421
+ })
3422
+ });
3423
+ }
3424
+
3425
+ // Coming FROM a union into something that is not one: only the variant this
3426
+ // value actually is matters, so narrow before comparing.
3427
+ if (from.schema.type === "union") {
3428
+ var _variant = variantOf(from.schema, from.value);
3429
+ if (_variant === undefined) {
3430
+ return {
3431
+ status: "unknown",
3432
+ message: "The old value does not say which shape it is, so we cannot tell whether it fits here."
3433
+ };
3434
+ }
3435
+ return checkCompatibility({
3436
+ schema: _variant,
3437
+ value: from.value
3438
+ }, to);
3439
+ }
3440
+ if (from.schema.type !== to.schema.type) {
3441
+ return incompatible({
3442
+ kind: "type-changed",
3443
+ from: from.schema.type,
3444
+ to: to.schema.type
3445
+ });
3446
+ }
3447
+ switch (to.schema.type) {
3448
+ case "literal":
3449
+ {
3450
+ var fromValue = from.schema.type === "literal" ? from.schema.value : String(from.value);
3451
+ return fromValue === to.schema.value ? compatible : incompatible({
3452
+ kind: "literal-changed",
3453
+ from: fromValue,
3454
+ to: to.schema.value
3455
+ });
3456
+ }
3457
+ case "object":
3458
+ {
3459
+ if (from.schema.type !== "object" || !isJsonObject(from.value)) {
3460
+ return {
3461
+ status: "unknown",
3462
+ message: "The old value is not shaped like an object."
3463
+ };
3464
+ }
3465
+ var _fromValue = from.value;
3466
+ var target = to.schema.items;
3467
+ var source = from.schema.items;
3468
+
3469
+ // Keys the old value carries that the field no longer has. Writing them
3470
+ // would put data into a schema with nowhere to hold it.
3471
+ var extra = Object.keys(source).filter(function (key) {
3472
+ return !(key in target);
3473
+ });
3474
+ if (extra.length > 0) {
3475
+ return incompatible({
3476
+ kind: "unknown-field",
3477
+ keys: extra
3478
+ });
3479
+ }
3480
+
3481
+ // Keys the field requires that the old value cannot supply.
3482
+ var missing = Object.keys(target).filter(function (key) {
3483
+ return !target[key].opt && !(key in source);
3484
+ });
3485
+ if (missing.length > 0) {
3486
+ return incompatible({
3487
+ kind: "missing-required",
3488
+ keys: missing
3489
+ });
3490
+ }
3491
+ var unknown = null;
3492
+ for (var _i = 0, _Object$entries = Object.entries(target); _i < _Object$entries.length; _i++) {
3493
+ var _fromValue$_key;
3494
+ var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
3495
+ _key = _Object$entries$_i[0],
3496
+ targetItem = _Object$entries$_i[1];
3497
+ var sourceItem = source[_key];
3498
+ if (sourceItem === undefined) {
3499
+ continue; // optional and absent — already allowed above
3500
+ }
3501
+ var res = checkCompatibility({
3502
+ schema: sourceItem,
3503
+ value: (_fromValue$_key = _fromValue[_key]) !== null && _fromValue$_key !== void 0 ? _fromValue$_key : null
3504
+ }, {
3505
+ schema: targetItem
3506
+ });
3507
+ if (res.status === "incompatible") {
3508
+ return res;
3509
+ }
3510
+ if (res.status === "unknown" && unknown === null) {
3511
+ unknown = res;
3512
+ }
3513
+ }
3514
+ return unknown !== null && unknown !== void 0 ? unknown : compatible;
3515
+ }
3516
+ case "array":
3517
+ {
3518
+ if (from.schema.type !== "array" || !Array.isArray(from.value)) {
3519
+ return {
3520
+ status: "unknown",
3521
+ message: "The old value is not a list."
3522
+ };
3523
+ }
3524
+ // An empty list has nothing to probe the item schema with. The item
3525
+ // TYPES still have to agree, and that much can be decided without a
3526
+ // value - so decide it rather than passing an empty list as proof.
3527
+ if (from.value.length === 0) {
3528
+ return from.schema.item.type === to.schema.item.type ? compatible : incompatible({
3529
+ kind: "type-changed",
3530
+ from: from.schema.item.type,
3531
+ to: to.schema.item.type
3532
+ });
3533
+ }
3534
+ var _unknown = null;
3535
+ var _iterator2 = _createForOfIteratorHelper(from.value),
3536
+ _step2;
3537
+ try {
3538
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
3539
+ var item = _step2.value;
3540
+ var _res = checkCompatibility({
3541
+ schema: from.schema.item,
3542
+ value: item
3543
+ }, {
3544
+ schema: to.schema.item
3545
+ });
3546
+ if (_res.status === "incompatible") {
3547
+ return _res;
3548
+ }
3549
+ if (_res.status === "unknown" && _unknown === null) {
3550
+ _unknown = _res;
3551
+ }
3552
+ }
3553
+ } catch (err) {
3554
+ _iterator2.e(err);
3555
+ } finally {
3556
+ _iterator2.f();
3557
+ }
3558
+ return _unknown !== null && _unknown !== void 0 ? _unknown : compatible;
3559
+ }
3560
+ case "record":
3561
+ {
3562
+ if (from.schema.type !== "record" || !isJsonObject(from.value)) {
3563
+ return {
3564
+ status: "unknown",
3565
+ message: "The old value is not a record."
3566
+ };
3567
+ }
3568
+ var _unknown2 = null;
3569
+ for (var _i2 = 0, _Object$values = Object.values(from.value); _i2 < _Object$values.length; _i2++) {
3570
+ var entry = _Object$values[_i2];
3571
+ var _res2 = checkCompatibility({
3572
+ schema: from.schema.item,
3573
+ value: entry !== null && entry !== void 0 ? entry : null
3574
+ }, {
3575
+ schema: to.schema.item
3576
+ });
3577
+ if (_res2.status === "incompatible") {
3578
+ return _res2;
3579
+ }
3580
+ if (_res2.status === "unknown" && _unknown2 === null) {
3581
+ _unknown2 = _res2;
3582
+ }
3583
+ }
3584
+ return _unknown2 !== null && _unknown2 !== void 0 ? _unknown2 : compatible;
3585
+ }
3586
+ case "keyOf":
3587
+ {
3588
+ // A key is only meaningful against the module it indexes.
3589
+ if (from.schema.type !== "keyOf") {
3590
+ return compatible;
3591
+ }
3592
+ return from.schema.path === to.schema.path ? compatible : incompatible({
3593
+ kind: "different-module",
3594
+ from: from.schema.path,
3595
+ to: to.schema.path
3596
+ });
3597
+ }
3598
+ case "richtext":
3599
+ {
3600
+ // The options decide which nodes are legal, and comparing them properly
3601
+ // means comparing node trees against them. Not yet done, and saying so is
3602
+ // better than a confident answer we cannot back.
3603
+ return {
3604
+ status: "unknown",
3605
+ message: "Rich text can be restored, but we cannot yet confirm every mark and block is still allowed here."
3606
+ };
3607
+ }
3608
+ default:
3609
+ // Primitives and media: same type is the whole test. Constraints
3610
+ // (maxLength, accept, …) are validated on write, which reports them
3611
+ // better than a schema comparison could.
3612
+ return compatible;
3613
+ }
3614
+ }
3615
+
3616
+ /**
3617
+ * The newest commit the client has been told about, or `null` if none.
3618
+ *
3619
+ * This is the **publish head**: unlike `baseSha`, which only moves once a
3620
+ * deployment lands, it moves the instant somebody publishes. That makes it the
3621
+ * one thing a client can carry to `/save` to say "this is the world I decided
3622
+ * against" — and the only way the server can tell that somebody else has
3623
+ * published since the review screen was read.
3624
+ *
3625
+ * In `shared` because both sides need the SAME answer: the client names a head
3626
+ * and the server compares against one, so two implementations of "newest" is two
3627
+ * ways to disagree about whether a publish is stale.
3628
+ *
3629
+ * By `createdAt` rather than by position, because the list's order is the
3630
+ * content API's business and nothing here should depend on it. Ties keep the
3631
+ * first seen, which is arbitrary and does not matter: two commits sharing a
3632
+ * timestamp are equally "since", and either answer refuses the same publishes.
3633
+ */
3634
+ function newestCommitSha(commits) {
3635
+ var _newest$commitSha, _newest;
3636
+ var newest = null;
3637
+ var _iterator = _createForOfIteratorHelper(commits !== null && commits !== void 0 ? commits : []),
3638
+ _step;
3639
+ try {
3640
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
3641
+ var commit = _step.value;
3642
+ if (newest === null || commit.createdAt > newest.createdAt) {
3643
+ newest = commit;
3644
+ }
3645
+ }
3646
+ } catch (err) {
3647
+ _iterator.e(err);
3648
+ } finally {
3649
+ _iterator.f();
3650
+ }
3651
+ return (_newest$commitSha = (_newest = newest) === null || _newest === void 0 ? void 0 : _newest.commitSha) !== null && _newest$commitSha !== void 0 ? _newest$commitSha : null;
3652
+ }
3653
+
2706
3654
  var VAL_CONFIG_SESSION_STORAGE_KEY = "val-config";
2707
3655
  var VAL_THEME_SESSION_STORAGE_KEY = "val-theme";
2708
3656
  var VAL_AI_SESSION_STORAGE_KEY = "val-ai-session";
@@ -2718,14 +3666,6 @@ var SharedValConfig = z.object({
2718
3666
  ai: z.object({
2719
3667
  commitMessages: z.object({
2720
3668
  disabled: z["boolean"]().optional()
2721
- }).optional(),
2722
- chat: z.object({
2723
- experimental: z.object({
2724
- enable: z["boolean"]().optional()
2725
- }).optional(),
2726
- suggestions: z.array(z.string()).optional(),
2727
- title: z.string().optional(),
2728
- description: z.string().optional()
2729
3669
  }).optional()
2730
3670
  }).optional()
2731
3671
  });
@@ -3510,6 +4450,11 @@ function emptyOf(schema) {
3510
4450
  return [];
3511
4451
  } else if (schema.type === "record") {
3512
4452
  return {};
4453
+ } else if (schema.type === "settings") {
4454
+ // Not an object of empty sections: every settings key is optional, and
4455
+ // absent IS the empty value. Filling the sections in would write a shape
4456
+ // the project never asked for.
4457
+ return {};
3513
4458
  } else if (schema.opt) {
3514
4459
  return null;
3515
4460
  } else if (schema.type === "richtext") {
@@ -4173,6 +5118,13 @@ function isSkippableFixCode(fix) {
4173
5118
  // rewriting the module) and saving does not repair it either, so surface
4174
5119
  // it: the message names the command (`val validate --fix`) that does.
4175
5120
  return false;
5121
+ case "external:upload":
5122
+ // Same shape as `jsonValues:extract-entry` — an entry inlined in a
5123
+ // `.val.ts` that belongs elsewhere — and more emphatically not skippable.
5124
+ // Fixing it writes to a live store, which is why even the CLI does it only
5125
+ // under `val external upload` and never under a blanket `--fix`. Surfacing
5126
+ // it is the whole point: someone has to decide.
5127
+ return false;
4176
5128
  default:
4177
5129
  {
4178
5130
  return false;
@@ -4212,4 +5164,4 @@ function filterBlockingValidationErrors(validationErrors, schemas, sources) {
4212
5164
  return partitionValidationErrors(resolved).surfaced;
4213
5165
  }
4214
5166
 
4215
- export { Api, CodeOptions, ColorOptions, DEFAULT_ERROR_MESSAGE, DateOptions, FileOptions, ImageEncodeOption, ImageOptions, JSON_ENTRIES_BATCH_MAX, ParentRef$1 as ParentRef, Patch, PatchBlock, PatchId, PatchJSON, RemirrorBr, RemirrorBulletList, RemirrorHeading, RemirrorImage, RemirrorJSON, RemirrorLinkMark, RemirrorListItem, RemirrorOrderedList, RemirrorParagraph, RemirrorText, RemirrorTextMark, RichTextOptions, SerializedArraySchema, SerializedBooleanSchema, SerializedCodeSchema, SerializedColorSchema, SerializedDateSchema, SerializedDateTimeSchema, SerializedFileSchema, SerializedImageSchema, SerializedKeyOfSchema, SerializedLiteralSchema, SerializedNumberSchema, SerializedObjectSchema, SerializedRecordSchema, SerializedRichTextSchema, SerializedRouteSchema, SerializedSchema, SerializedStringSchema, SerializedUnionSchema, SharedValConfig, VAL_AI_SESSION_STORAGE_KEY, VAL_CANVAS_MESSAGE, VAL_CANVAS_PARAM, VAL_CONFIG_SESSION_STORAGE_KEY, VAL_ENABLE_COOKIE_NAME, VAL_SESSION_COOKIE, VAL_STATE_COOKIE, VAL_THEME_SESSION_STORAGE_KEY, ValCommit, ValDeployment, buildDuplicatePatch, buildEmptyAtPathPatch, buildFileRefValue, buildImageGalleryPatch, buildRemoveImageGalleryEntryPatch, createRegExpFromPattern, createValClient, describeContainerAtPath, emptyOf, filterBlockingValidationErrors, filterRoutesByPatterns, findSimilar, getErrorMessageFromUnknownJson, getInlineImgInfo, getNextAppRouterSitemapTree, getNextAppRouterSourceFolder, getPatternFromModuleFilePath, getRoutesOf, getRoutesWithModulePaths, getSourceAt, isRemoteSchema, isValCanvasFrame, isValCanvasPageMessage, isValCanvasStudioMessage, levenshtein, parseRoutePattern, partitionValidationErrors, remirrorToRichTextSource, resolveSchemaSourceFixForError, resolveSchemaSourceFixes, resolveSerializedSchemaAtPath, richTextToRemirror, safeParsePatch, toolNames, urlOf, validateRoutePatterns, withValCanvasParam };
5167
+ export { Api, BinaryFileRef, CodeOptions, ColorOptions, DEFAULT_ERROR_MESSAGE, DateOptions, FileOptions, HistoricalCommit, HistoricalModule, HistoricalPatchSet, HistoryError, ImageEncodeOption, ImageOptions, JSONValue, JSON_ENTRIES_BATCH_MAX, ModuleComparison, ParentRef$1 as ParentRef, Patch, PatchBlock, PatchGroup, PatchId, PatchJSON, RemirrorBr, RemirrorBulletList, RemirrorHeading, RemirrorImage, RemirrorJSON, RemirrorLinkMark, RemirrorListItem, RemirrorOrderedList, RemirrorParagraph, RemirrorText, RemirrorTextMark, RestoreVerdict, RichTextOptions, SerializedArraySchema, SerializedBooleanSchema, SerializedCodeSchema, SerializedColorSchema, SerializedDateSchema, SerializedDateTimeSchema, SerializedFileSchema, SerializedImageSchema, SerializedKeyOfSchema, SerializedLiteralSchema, SerializedNumberSchema, SerializedObjectSchema, SerializedRecordSchema, SerializedRichTextSchema, SerializedRouteSchema, SerializedSchema, SerializedSettingsSchema, SerializedStringSchema, SerializedUnionSchema, SharedValConfig, VAL_AI_SESSION_STORAGE_KEY, VAL_CANVAS_MESSAGE, VAL_CANVAS_PARAM, VAL_CONFIG_SESSION_STORAGE_KEY, VAL_ENABLE_COOKIE_NAME, VAL_SESSION_COOKIE, VAL_STATE_COOKIE, VAL_THEME_SESSION_STORAGE_KEY, ValCommit, ValDeployment, buildDuplicatePatch, buildEmptyAtPathPatch, buildFileRefValue, buildImageGalleryPatch, buildRemoveImageGalleryEntryPatch, checkCompatibility, createRegExpFromPattern, createValClient, describeContainerAtPath, emptyOf, explainIncompatible, filterBlockingValidationErrors, filterRoutesByPatterns, findSimilar, getErrorMessageFromUnknownJson, getInlineImgInfo, getNextAppRouterSitemapTree, getNextAppRouterSourceFolder, getPatternFromModuleFilePath, getRoutesOf, getRoutesWithModulePaths, getSourceAt, isRemoteSchema, isValCanvasFrame, isValCanvasPageMessage, isValCanvasStudioMessage, levenshtein, newestCommitSha, parseRoutePattern, partitionValidationErrors, remirrorToRichTextSource, resolveSchemaSourceFixForError, resolveSchemaSourceFixes, resolveSerializedSchemaAtPath, richTextToRemirror, safeParsePatch, toolNames, urlOf, validateRoutePatterns, withValCanvasParam };