@warmhub/sdk-ts 0.139.0 → 0.140.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,465 @@
2
2
 
3
3
  <!-- warmhub-changelog-artifact:v1 -->
4
4
 
5
+ ## 0.140.0
6
+
7
+ ### Changed
8
+
9
+ A resumed repository export now sends the `formatVersion` of the header the download opened with, and the server refuses a resume whose format is not the one it writes: HTTP 412 `client-incompatible` with the message "Restart the export; a resume must use the format it started with." A download started before a server upgrade that changes the export format can no longer be silently finished under its old header; the resume fails and the export must be restarted.
10
+
11
+ A resume of a file that holds only the header (interrupted before the first row) sends no cursor, so the server cannot check it; the session now compares the new header's format to the saved one and raises `RepositoryExportError` (`header_invalid`) with the same restart message rather than appending new-format rows under the old header.
12
+
13
+ **Upgrade with the server.** An older SDK sends no `formatVersion` on a resume, so a server with this change refuses every resume from it, not only cross-format ones. Upgrade the SDK in the same release as the server.
14
+
15
+ ### Added
16
+
17
+ `isAllStreamOperationsFailedError(error)` reports whether a thrown value is an all-failed stream error. Unlike `error instanceof AllStreamOperationsFailedError`, it also recognizes the error after `toWarmHubError` normalization and when a different copy of the SDK threw it.
18
+
19
+ ### Breaking
20
+
21
+ #### `NAME_TAKEN` names the holder by durable id only and offers no replacement name
22
+
23
+ **What it was.** A `name_taken` error's `details.takenBy` was `{ shape, durableId }`, `details.suggestedName` could carry a replacement name, and the message said `by a <Shape> Thing` and `for example "<name>"`.
24
+
25
+ **What it is now.** `details.takenBy` is `{ durableId }`; `shape` is removed. `details.suggestedName` is removed and no example name appears in the message or hint. The message names no Shape: `Name "acme" is already taken in this repository by another Thing; choose another name`. A caller who cannot read the holder still gets no `takenBy` and no `by another Thing`. The `name_taken` decoder in the streaming submission protocol requires only `takenBy.durableId`.
26
+
27
+ **Why it breaks.** This is a client-visible breaking change, accepted. Released sdk-ts 0.135.0 to 0.139.0 decode `takenBy` with `shape` required, so they refuse a `name_taken` result from a server that carries this change. It lands before the v2 floor release (0.140.0), so no v2 client sees `shape`.
28
+
29
+ **Migration.** Read the holder's Shapes with `get` on `details.takenBy.durableId`; a Thing can have several Shapes or none. Choose the replacement name yourself.
30
+
31
+ ```ts
32
+ // Before
33
+ const { takenBy, suggestedName } = details
34
+ console.log(takenBy?.shape, suggestedName)
35
+
36
+ // After
37
+ const holder = details.takenBy && (await client.thing.get(org, repo, details.takenBy.durableId))
38
+ ```
39
+
40
+ ### Added
41
+
42
+ The `restore` operation and `OperationBuilder.restore()` make a retracted Thing active again, by its durable ID. Pass `newName` to restore it under another name.
43
+
44
+ In these notes, "old grammar" means the reference form used before this release (`Deal/acme`, operations with `kind`), and "new grammar" the form this release introduces (`acme`, operations with `shapes`).
45
+
46
+ ### Breaking
47
+
48
+ #### Write operations take `name` and `shapes`
49
+
50
+ **What it was.** Operations carried `kind`, an add name spelled
51
+ `Shape/name`, a top-level `about`, or collection `type`/`members`, and the
52
+ SDK converted them.
53
+
54
+ **What it is now.** `Operation`, `AddOperation`, `ReviseOperation`,
55
+ `RetractOperation`, `ReaffirmOperation` and `RenameOperation` keep their
56
+ names but have the new shape. An add or revise requires `name`, `shapes`
57
+ and `data`. `kind` and `shape` are typed `never`. The new `WriteOperation`
58
+ type is one operation of that form.
59
+
60
+ - A Shape is the Thing `hub/shapes/X` (`shapeName('X')`), written with
61
+ `shapes: []`.
62
+ - An assertion is a Thing one of whose declared Shapes composes `Assertion`,
63
+ with its target in `data.about`.
64
+ - A collection is a Thing of its collection Shape:
65
+ `{ shapes: ['Set'], data: { members } }`.
66
+
67
+ **Migration.**
68
+
69
+ ```ts
70
+ // Before
71
+ await client.commit.apply('acme', 'world', 'seed', [
72
+ { operation: 'add', kind: 'thing', name: 'Deal/acme', data: { company: 'Acme' } },
73
+ ])
74
+
75
+ // After
76
+ await client.commit.apply('acme', 'world', 'seed', [
77
+ { operation: 'add', name: 'acme', shapes: ['Deal'], data: { company: 'Acme' } },
78
+ ])
79
+ ```
80
+
81
+ #### Old-grammar operations are refused before any request
82
+
83
+ **What it was.** The SDK converted old-grammar operations.
84
+
85
+ **What it is now.** An operation with `kind`, `shape`, `wref`,
86
+ `declaredShapes`, `about`, collection `type` or `members`, or with no
87
+ `operation`, is refused with a `WriteOperationError` before anything is
88
+ sent. The message shows a suggested new form, with data values replaced by
89
+ `"..."`. Names and `about` targets are copied as written, so check them: a
90
+ suggested `name: 'Deal/acme'` would address a Thing literally named
91
+ `Deal/acme`. An add or revise with no `shapes` is refused with a message
92
+ that it states no Shapes.
93
+
94
+ Old-grammar input no longer type-checks, so it reaches this refusal only
95
+ through a cast.
96
+
97
+ **Migration.**
98
+
99
+ ```ts
100
+ // Before: converted and sent
101
+ await client.commit.apply('acme', 'world', 'rate', [
102
+ { operation: 'add', kind: 'assertion', name: 'Rating/r1', about: 'Deal/acme', data: { score: 5 } },
103
+ ])
104
+
105
+ // After
106
+ await client.commit.apply('acme', 'world', 'rate', [
107
+ { operation: 'add', name: 'r1', shapes: ['Rating'], data: { about: 'acme', score: 5 } },
108
+ ])
109
+ ```
110
+
111
+ Other operations refused locally:
112
+
113
+ - a pinned Shape in `shapes` (`Deal@v2`), or an entry that is not a bare
114
+ Shape name (`hub/shapes/Deal`, anything with `/`);
115
+ - `shapeless` on a raw operation (spell it `shapes: []`);
116
+ - non-empty `shapes` on a `hub/shapes/X` name;
117
+ - an add or revise without `data`, and `keepShapes` on an add;
118
+ - `affirmedTargets` on a shapeless write;
119
+ - `expectedVersion` on an add, and `active` on a revise;
120
+ - renaming a Shape to a non-Shape name, or the reverse;
121
+ - `+` in an added or renamed-to name;
122
+ - an added or renamed-to name under the reserved `hub` root
123
+ (`RESERVED_NAME`);
124
+ - the same name added twice in one submission (`ILLEGAL_OP_SEQUENCE`);
125
+ - errors found by the local Shape-definition checks.
126
+
127
+ #### `OperationBuilder` methods take the name first
128
+
129
+ **What it was.** `add({ name, data })`, `revise({ name, data })`,
130
+ `retract(target)`, `reaffirm({ name, kind, ... })`; no `rename`.
131
+
132
+ **What it is now.**
133
+
134
+ | Method | After |
135
+ |---|---|
136
+ | `add` | `add(name, { shapes \| shapeless, data, ... })` |
137
+ | `revise` | `revise(name, { shapes \| shapeless \| keepShapes, data, ... })` |
138
+ | `retract` | `retract(name, { reason? })` |
139
+ | `reaffirm` | `reaffirm(name, { add?, remove? })` |
140
+ | `rename` | `rename(name, newName)` (new) |
141
+
142
+ Each add and revise states its Shapes in exactly one way: `shapes: [...]`,
143
+ `shapeless: true`, or (revise only) `keepShapes: true`. The builder refuses
144
+ zero ways or more than one, `keepShapes` on an add, `shapeless` on a
145
+ `hub/shapes/X` name, and `shapes: []` on a non-Shape name (spell that
146
+ `shapeless: true`).
147
+
148
+ **Migration.**
149
+
150
+ ```ts
151
+ // Before
152
+ import { OperationBuilder } from '@warmhub/sdk-ts'
153
+ const ob = new OperationBuilder()
154
+ ob.add({ name: 'Deal/acme', data: { company: 'Acme' } })
155
+ ob.retract('Deal/old')
156
+ await ob.commit({ client, orgName: 'acme', repoName: 'world', message: 'seed' })
157
+
158
+ // After
159
+ import { createOperationEventRequestId } from '@warmhub/sdk-ts'
160
+
161
+ await client.shape.create('acme', 'world', 'Deal', { company: 'string' }, {
162
+ eventRequestId: createOperationEventRequestId(),
163
+ })
164
+
165
+ await client.commit.apply(
166
+ 'acme',
167
+ 'world',
168
+ 'seed',
169
+ client
170
+ .ops()
171
+ .add('acme', { shapes: ['Deal'], data: { company: 'Acme' } })
172
+ .add('scratch', { shapeless: true, data: { note: 'free' } })
173
+ .rename('scratch', 'scratch-2')
174
+ .retract('old'),
175
+ )
176
+
177
+ // A later submission: keepShapes revises a Thing that already exists
178
+ await client.commit.apply(
179
+ 'acme',
180
+ 'world',
181
+ 'update',
182
+ client.ops().revise('acme', { keepShapes: true, data: { company: 'Acme 2' } }),
183
+ )
184
+ ```
185
+
186
+ A `keepShapes` revise of a Thing added or renamed earlier in the same
187
+ submission is refused; state `shapes` instead.
188
+
189
+ #### Builder errors are `WriteOperationError`, raised when an operation is queued
190
+
191
+ **What it was.** Some refusals surfaced only at commit.
192
+
193
+ **What it is now.** A per-operation refusal throws at the `.add(...)` or
194
+ `.revise(...)` call. Whole-batch refusals ("already submitted", "in flight")
195
+ have `operationIndex: undefined`. The code is usually `VALIDATION_ERROR`;
196
+ it can also be `RESERVED_NAME`, `ILLEGAL_OP_SEQUENCE`, a Shape-name check
197
+ code, or (for `keepShapes`, at submit) `NOT_FOUND`.
198
+
199
+ **Migration.**
200
+
201
+ ```ts
202
+ import { WriteOperationError, type AddInput } from '@warmhub/sdk-ts'
203
+
204
+ try {
205
+ client.ops().add('acme', { data: {} } as AddInput) // no Shapes stated
206
+ } catch (e) {
207
+ if (e instanceof WriteOperationError) console.log(e.code, e.operationIndex) // VALIDATION_ERROR 0
208
+ }
209
+ ```
210
+
211
+ #### Every reference argument uses the new grammar
212
+
213
+ **What it was.** Read and write calls took `Deal/acme`, and a Shape was
214
+ `Deal`.
215
+
216
+ **What it is now.** Pass the Thing name (`acme`) and `hub/shapes/Deal` for a
217
+ Shape where any Thing can appear. An old spelling is looked up literally: a
218
+ single read gets `NOT_FOUND`, and `thing.getMany` lists it in `missing`.
219
+ Globs (`match`) match Thing names, and `*` does not cross `/`.
220
+
221
+ **Migration.**
222
+
223
+ ```ts
224
+ // Before
225
+ await client.thing.get('acme', 'world', 'Deal/acme')
226
+ // After
227
+ await client.thing.get('acme', 'world', 'acme')
228
+ ```
229
+
230
+ #### `thing.rename` takes full names
231
+
232
+ **What it was.** `rename(org, repo, shapeName, oldName, newName, opts)`.
233
+
234
+ **What it is now.** `rename(org, repo, name, newName, opts)`. A call in the
235
+ old argument order is refused locally with `VALIDATION_ERROR`.
236
+
237
+ **Migration.**
238
+
239
+ ```ts
240
+ // Before
241
+ await client.thing.rename('acme', 'world', 'Deal', 'acme', 'acme-corp', { eventRequestId })
242
+ // After
243
+ await client.thing.rename('acme', 'world', 'acme', 'acme-corp', { eventRequestId })
244
+ ```
245
+
246
+ #### Read results drop `shape`, `shapeName` and `validatedShape`
247
+
248
+ **What it was.** Results carried a single Shape.
249
+
250
+ **What it is now.** Use `declaredShapes` (the Shapes the version states)
251
+ and `validatedShapes` (the Shapes it satisfies, including composed ones).
252
+ Both hold version-pinned names such as `Deal@v3`. The removal covers
253
+ `ThingDetail`, `ThingItem`, `thing.getMany` items, `thing.getWithLease`,
254
+ history versions and history's `thing`, refs items, component owned Things
255
+ and `view.evaluate` items. `SynthesizedRepoContent.shape` becomes
256
+ `declaredShapes` / `validatedShapes`.
257
+
258
+ `ThingDetail` allows extra keys, so `detail.shapeName` still compiles; it is
259
+ `undefined` at run time. Search your code for these field names.
260
+
261
+ `shape` as an input filter (`thing.head(org, repo, { shape: 'Deal' })`) is
262
+ unchanged.
263
+
264
+ **Migration.**
265
+
266
+ ```ts
267
+ // Before
268
+ const shape = detail.shapeName
269
+ // After
270
+ const shapes = (detail.declaredShapes ?? []).map((w) => w.replace(/@v\d+$/, ''))
271
+ ```
272
+
273
+ #### Token scopes use `grants`, on input and output
274
+
275
+ **What it was.** A scope entry carried `allowedMatches` and `allowedShapes`.
276
+
277
+ **What it is now.** Restrictions are a list of (paths, Shapes) pairs in
278
+ `grants`.
279
+
280
+ - Input (`token.create`): the entry-level fields are still typed `unknown`,
281
+ so old code compiles, but the server refuses them with "allowedMatches and
282
+ allowedShapes are no longer scope-entry fields: give each (paths, Shapes)
283
+ pair in grants…".
284
+ - Output (`TokenCreateResult`, token metadata, whoami scopes): the
285
+ entry-level fields are removed, and reading them is a compile error. Read
286
+ `grants`.
287
+ - `org.setMemberScopes` takes entries with `resource` and `permissions`
288
+ only; restrictions on member scopes are refused.
289
+ - A pair must carry a non-empty `allowedMatches`, or an `allowedShapes`
290
+ with at least one key. `grants: []`, `{}` and `{ allowedShapes: {} }` are
291
+ refused.
292
+ - `shapeless: true` in a clause is not a restriction of its own: it lets
293
+ Things with no Shape through. Alone it admits every Thing the pair's paths
294
+ match, shaped or shapeless; beside `shapes` it adds shapeless Things to
295
+ the Things the clause covers. No clause restricts a pair to shapeless
296
+ Things only.
297
+
298
+ **Migration.**
299
+
300
+ ```ts
301
+ // Before
302
+ await client.token.create({ name: 'ci', scopes: [
303
+ { resource: 'acme/world', permissions: ['read'], allowedMatches: ['Deal/**'] },
304
+ ] })
305
+ // After
306
+ await client.token.create({ name: 'ci', scopes: [
307
+ { resource: 'acme/world', permissions: ['read'],
308
+ grants: [{ allowedMatches: ['**'], allowedShapes: { shapes: ['Deal'] } }] },
309
+ ] })
310
+ ```
311
+
312
+ #### Repository exports are format 6, and the delta fold needs formats
313
+
314
+ **What it was.** Exports were format 5, and
315
+ `applyRepositoryExportDelta(base, delta)` returned rows keyed by durable id.
316
+
317
+ **What it is now.**
318
+
319
+ - The server writes format 6. This SDK reads 5 and 6; older SDKs read only
320
+ 5, so they cannot read exports made after this release reaches
321
+ production.
322
+ - Format 6 uses new-grammar references, has one row per Shape version, and
323
+ may carry `retracted: true` on Shape rows. A Shape tombstone in format 6
324
+ is rejected as `row_invalid`.
325
+ - `applyRepositoryExportDelta` takes a third argument, the formats. Mixing
326
+ format 5 and format 6 throws `RepositoryExportError` with reason
327
+ `format_mismatch`.
328
+ - In the returned map, format 6 Shape rows are keyed `durableId@vN`, not
329
+ `durableId`. Code that looks a Shape up by durable id, or keys its own map
330
+ by durable id, must account for versions.
331
+
332
+ **Migration.**
333
+
334
+ ```ts
335
+ // Before
336
+ const current = applyRepositoryExportDelta(base, delta)
337
+ // After
338
+ const current = applyRepositoryExportDelta(base, delta, {
339
+ base: baseSession.header!.formatVersion,
340
+ delta: deltaSession.header!.formatVersion,
341
+ })
342
+ ```
343
+
344
+ #### `stream.append` takes new-grammar operations only
345
+
346
+ **What it was.** `stream.append` took the old operation form.
347
+
348
+ **What it is now.** It takes new-grammar operations and does not accept
349
+ `keepShapes` (use `createKeepShapesResolver`). Its only local check is a
350
+ pinned Shape; everything else is checked by the server.
351
+
352
+ **Migration.** Send `{ operation, name, shapes, data }` operations.
353
+
354
+ #### Writes need a server running this release
355
+
356
+ **What it was.** The SDK wrote to any current server.
357
+
358
+ **What it is now.** Against an older server, writes fail the compatibility
359
+ check with "@warmhub/sdk-ts requires write contract 3, but the backend
360
+ provides 2."
361
+
362
+ **Migration.** Upgrade the SDK after the server.
363
+
364
+ ### Added
365
+
366
+ - **`client.ops(options?)`** — returns a new `OperationBuilder`.
367
+ - **`shapeName(bare)`** — `'Deal'` to `'hub/shapes/Deal'`; idempotent.
368
+ - **`isShapeReference(value)`** — true for a bare, unpinned Shape name.
369
+ - **`WriteOperationError`** (extends `WarmHubError`) — with
370
+ `operationIndex`; its message starts `Invalid operation at index N:`.
371
+ - **Types** — `WriteOperation`, `KeepShapesRevise`, `AddInput`,
372
+ `ReviseInput`, `RetractInput`, `ReaffirmInput`, `ShapesChoice`,
373
+ `CommitValidateOperations`, `RepositoryExportDeltaFormats`,
374
+ `ThingShapesReader`.
375
+ - **`keepShapes: true` on a revise** — at submit the SDK reads the Thing's
376
+ current Shapes and version and sends both, the version as
377
+ `expectedVersion` (a supplied `expectedVersion` wins). Shapes are sent
378
+ bare, so the revise is checked against each Shape's current version. A
379
+ missing Thing is refused with `NOT_FOUND`. A Thing added, or renamed to or
380
+ from, earlier in the same submission is refused. A Thing revised earlier in
381
+ the same submission reuses that revise's Shapes and sends no version.
382
+ Because the version is pinned, resubmitting the same `submissionId` from
383
+ another process can conflict; explicit `shapes` is the retry-safe form.
384
+ ```ts
385
+ await client.commit.apply('acme', 'world', 'm',
386
+ client.ops().revise('acme', { keepShapes: true, data: { company: 'Acme 2' } }))
387
+ ```
388
+ - **`createKeepShapesResolver(read)`** — resolves `keepShapes` revises for
389
+ `stream.append`. It keeps state across calls, so use one resolver per
390
+ submission.
391
+ ```ts
392
+ const resolve = createKeepShapesResolver((names) => client.thing.getMany('acme', 'world', names))
393
+ await client.stream.append({ /* ... */ operations: await resolve([
394
+ { operation: 'revise', name: 'acme', keepShapes: true, data: {} },
395
+ ]) })
396
+ ```
397
+ - **Builders everywhere** — `commit.validate`, `commit.apply` and
398
+ `commit.applyStreaming` accept an `OperationBuilder` as well as an array.
399
+ - **`shapeless: true` read filter** — on `thing.head`, `thing.query`,
400
+ `thing.count` and `live.thingHead`: only Things with no Shape. It
401
+ cannot be combined with `shape`, `declaredShape`, `match`, a token
402
+ restricted by paths, or `sinceRepoSeq`, and is not available on search or
403
+ the changes feeds.
404
+ ```ts
405
+ await client.thing.head('acme', 'world', { shapeless: true })
406
+ ```
407
+ - **`warnings.shapeless?: true`** — on per-operation write results.
408
+ - **`ThingGetManyResult.missingHints`** — for a missing reference that is an
409
+ old `Shape/name` spelling of a Thing you can read, the text naming its new
410
+ form.
411
+ - **`declaredShapes` on refs items and history's `thing`** (history also
412
+ gains `validatedShapes`).
413
+ - **`redactedResource: true`** — may appear on `TokenCreateResult` scopes.
414
+ - **`details.reason` values** — `'legacy-wrefs-window-ended'` (from the
415
+ server) and `'legacy-wrefs-sdk'` (local).
416
+ - **`DEPENDENCY_FAILED`** — exported as a constant.
417
+ - **`LEGACY_WREFS_REFUSAL` and `WarmHubClient.clientFlags`**.
418
+ - **Export** — `RepositoryExportErrorReason` gains `'format_mismatch'`;
419
+ `RepositoryExportSession.lastShapeVersion`; an `onRow` option on
420
+ `restoreRepositoryExportSession`. A resume that breaks between two versions
421
+ of one Shape continues from the right version.
422
+
423
+ ### Behavior worth knowing
424
+
425
+ - **`clientFlags: ['legacy-wrefs']` refuses every call.** The SDK does not
426
+ send old-grammar requests. With the flag set, reads, writes, streaming,
427
+ export and the compatibility check all throw `WarmHubError` with code
428
+ `CLIENT_INCOMPATIBLE`, `details.reason: 'legacy-wrefs-sdk'`, and the
429
+ message "`legacy-wrefs` is set; the SDKs are v2-only. Unset it, or use the
430
+ `wh` CLI for v1." Use the CLI to submit old-grammar files during the
431
+ compatibility window.
432
+
433
+ ### Changed
434
+
435
+ - **Local checks before sending.** `commit.apply`, `commit.validate` and
436
+ the builder check every operation before sending when given an array or a
437
+ builder. An iterable, an async iterable, or `commit.applyStreaming` is
438
+ checked chunk by chunk, so a bad operation late in the source is refused
439
+ after earlier chunks were committed (`PartialStreamSubmissionError`).
440
+ `stream.append` checks only pinned Shapes.
441
+ - **Content-limit error labels.**
442
+ ```text
443
+ Before: Field "Content/Readme.content" is N bytes; WarmHub content fields are limited to 65536 bytes. ...
444
+ After: Field "Readme.content" is N bytes; WarmHub content fields are limited to 65536 bytes. ...
445
+ ```
446
+ `Agents.content` changes the same way.
447
+
448
+ ### Removed
449
+
450
+ - **`AddOp`, `ReviseOp`, `RetractOp`, `CollectionAddOp`,
451
+ `CollectionAddType`, `OperationBuilderAddInput`** — use `WriteOperation`
452
+ and the builder input types.
453
+ ```ts
454
+ // Before
455
+ const op: AddOp = { name: 'Deal/acme', data: {} }
456
+ // After
457
+ const op: WriteOperation = { operation: 'add', name: 'acme', shapes: ['Deal'], data: {} }
458
+ ```
459
+ - **Automatic old-grammar conversion** (`kind`, `about`, `type`/`members`,
460
+ `Shape/name` splitting).
461
+ - **The local "Selector-backed revise requires a Set/<name> target wref"
462
+ check in `collection.revise`.** The server decides.
463
+
5
464
  ## 0.139.0
6
465
 
7
466
  No new release notes.
package/README.md CHANGED
@@ -65,20 +65,23 @@ const client = new WarmHubClient({
65
65
  // One-time setup. Skip these three calls (or catch CONFLICT) on re-runs.
66
66
  await client.org.create('acme')
67
67
  await client.repo.create('acme', 'world', 'Game world')
68
- await client.shape.create('acme', 'world', 'Location', {
69
- x: 'number',
70
- y: 'number',
71
- label: 'string',
72
- })
68
+ await client.shape.create(
69
+ 'acme',
70
+ 'world',
71
+ 'Location',
72
+ { x: 'number', y: 'number', label: 'string' },
73
+ { eventRequestId: crypto.randomUUID() },
74
+ )
73
75
 
74
- await client.commit.apply('acme', 'world', 'seed cave', [
75
- {
76
- operation: 'add',
77
- kind: 'thing',
78
- name: 'Location/cave',
76
+ await client.commit.apply(
77
+ 'acme',
78
+ 'world',
79
+ 'seed cave',
80
+ client.ops().add('cave', {
81
+ shapes: ['Location'],
79
82
  data: { x: 0, y: 0, label: 'Dark Cave' },
80
- },
81
- ])
83
+ }),
84
+ )
82
85
 
83
86
  const head = await client.thing.head('acme', 'world', { shape: 'Location' })
84
87
  ```
@@ -99,7 +102,7 @@ const client = new WarmHubClient({
99
102
 
100
103
  ## Entrypoints
101
104
 
102
- - `@warmhub/sdk-ts`: core client, types, errors, `OperationBuilder`
105
+ - `@warmhub/sdk-ts`: core client, types, errors, `OperationBuilder` (`client.ops()`)
103
106
 
104
107
  ## Error Handling
105
108
 
@@ -119,24 +122,67 @@ try {
119
122
  }
120
123
  ```
121
124
 
122
- ## OperationBuilder
125
+ ## Writing
123
126
 
124
- `OperationBuilder` is single-use: after a successful `commit()`, the builder
125
- is sealed and cannot be modified or reused.
127
+ A write names a Thing by its full name (taken literally, `/` included) and
128
+ states its Shapes. Build writes with `client.ops()`; every add and revise
129
+ states its Shapes in exactly one way, or the builder throws a
130
+ `WriteOperationError` (`VALIDATION_ERROR`, with `operationIndex`) before any
131
+ request:
126
132
 
127
133
  ```ts
128
- import { OperationBuilder } from '@warmhub/sdk-ts'
129
-
130
- const ob = new OperationBuilder()
131
- ob.add({ name: 'Location/cave', data: { x: 0, y: 0 } })
132
- const result = await ob.commit({
133
- client,
134
- orgName: 'acme',
135
- repoName: 'world',
136
- message: 'seed cave',
137
- })
134
+ import { shapeName } from '@warmhub/sdk-ts'
135
+
136
+ const ops = client
137
+ .ops()
138
+ .add('acme', { shapes: ['Deal'], data: { company: 'Acme' } })
139
+ .add('scratch', { shapeless: true, data: { note: 'free' } })
140
+ .revise('acme', { shapes: ['Deal'], data: { company: 'Acme 2' } })
141
+ .add('r1', { shapes: ['Rating'], data: { about: 'acme', score: 5 } })
142
+ .add('crew', { shapes: ['Set'], data: { members: ['acme'] } })
143
+ .rename('scratch', 'scratch-2')
144
+ .retract('crew', { reason: 'disbanded' })
145
+ // A Shape is the Thing hub/shapes/X, written with shapes: [].
146
+ .add(shapeName('Deal'), { shapes: [], data: { fields: { company: 'string' } } })
147
+
148
+ await client.commit.apply('acme', 'world', 'seed', ops)
138
149
  ```
139
150
 
151
+ `commit.apply`, `commit.validate` and `commit.applyStreaming` also take plain
152
+ `WriteOperation[]` (`{ operation: 'add', name, shapes, data }`). A v1-shaped
153
+ operation (`kind`, `Shape/name` adds, top-level `about`, collection
154
+ `type`/`members`) is refused, and the error shows its v2 form. Raw v1 input
155
+ from typed TypeScript needs a cast: the v1 types are gone.
156
+
157
+ - An assertion (`r1` above) is a Thing one of whose declared Shapes composes
158
+ `Assertion`, with its target in `data.about`.
159
+ - `shapes: ['Deal', 'Contactable']`: bare Shape names. A pin (`Deal@v2`) is
160
+ refused: a write always certifies under each Shape's head version.
161
+ - `shapeless: true` (builder option): no Shape; the data is stored but not
162
+ validated, and the operation result carries `warnings.shapeless`. A raw
163
+ operation spells it `shapes: []`. A Shape (`hub/shapes/X`) is written with
164
+ `shapes: []`, never `shapeless`.
165
+ - `keepShapes: true` (revise only): the SDK reads the Thing's current Shapes
166
+ and version and sends both, the version as `expectedVersion` (a supplied
167
+ `expectedVersion` wins). A Thing revised earlier in the same submission
168
+ carries that revise's Shapes and no version; one added, or renamed to or
169
+ from, earlier in the same submission is refused before anything is sent
170
+ (submit it in a later request, or pass `shapes`). **keepShapes pins the version
171
+ it read; a cross-process resubmit of the same submissionId can conflict;
172
+ explicit shapes is the retry-safe form.** The Shapes are sent bare, so the
173
+ revise re-certifies under each Shape's head version.
174
+
175
+ A builder can also submit itself with `ops.commit({ client, orgName,
176
+ repoName })`. Either way it is single-use: once submitted it is sealed. Every
177
+ local refusal, per operation or for the whole batch ("already submitted",
178
+ "in flight"), is a `WriteOperationError` with code `VALIDATION_ERROR`
179
+ unless it names another; `operationIndex` is unset for a whole-batch one.
180
+
181
+ **`legacy-wrefs`:** the SDK is v2-only. With `clientFlags: ['legacy-wrefs']`
182
+ it refuses every call, read or write, before any request (the compatibility
183
+ probe included) with `LEGACY_WREFS_REFUSAL`. To write v1 during the
184
+ compatibility window, use the `wh` CLI.
185
+
140
186
  ## API Docs
141
187
 
142
188
  Generated API reference is published at [docs.warmhub.ai/sdk-reference/readme/](https://docs.warmhub.ai/sdk-reference/readme/) — the landing page lists every public class, interface, type alias, variable, and function exported from the main `@warmhub/sdk-ts` entrypoint.
package/changelog.json CHANGED
@@ -150,6 +150,32 @@
150
150
  "version": "0.139.0",
151
151
  "sourceSha": "01575d928d1214ed8556301179ef2a4509c28383",
152
152
  "notes": []
153
+ },
154
+ {
155
+ "version": "0.140.0",
156
+ "sourceSha": "6d99197d6e84edc1e7842634e0f5e4b810c80824",
157
+ "notes": [
158
+ {
159
+ "id": "export-resume-format-guard",
160
+ "body": "### Changed\n\nA resumed repository export now sends the `formatVersion` of the header the download opened with, and the server refuses a resume whose format is not the one it writes: HTTP 412 `client-incompatible` with the message \"Restart the export; a resume must use the format it started with.\" A download started before a server upgrade that changes the export format can no longer be silently finished under its old header; the resume fails and the export must be restarted.\n\nA resume of a file that holds only the header (interrupted before the first row) sends no cursor, so the server cannot check it; the session now compares the new header's format to the saved one and raises `RepositoryExportError` (`header_invalid`) with the same restart message rather than appending new-format rows under the old header.\n\n**Upgrade with the server.** An older SDK sends no `formatVersion` on a resume, so a server with this change refuses every resume from it, not only cross-format ones. Upgrade the SDK in the same release as the server."
161
+ },
162
+ {
163
+ "id": "is-all-stream-operations-failed-error",
164
+ "body": "### Added\n\n`isAllStreamOperationsFailedError(error)` reports whether a thrown value is an all-failed stream error. Unlike `error instanceof AllStreamOperationsFailedError`, it also recognizes the error after `toWarmHubError` normalization and when a different copy of the SDK threw it."
165
+ },
166
+ {
167
+ "id": "name-taken-durable-id-only",
168
+ "body": "### Breaking\n\n#### `NAME_TAKEN` names the holder by durable id only and offers no replacement name\n\n**What it was.** A `name_taken` error's `details.takenBy` was `{ shape, durableId }`, `details.suggestedName` could carry a replacement name, and the message said `by a <Shape> Thing` and `for example \"<name>\"`.\n\n**What it is now.** `details.takenBy` is `{ durableId }`; `shape` is removed. `details.suggestedName` is removed and no example name appears in the message or hint. The message names no Shape: `Name \"acme\" is already taken in this repository by another Thing; choose another name`. A caller who cannot read the holder still gets no `takenBy` and no `by another Thing`. The `name_taken` decoder in the streaming submission protocol requires only `takenBy.durableId`.\n\n**Why it breaks.** This is a client-visible breaking change, accepted. Released sdk-ts 0.135.0 to 0.139.0 decode `takenBy` with `shape` required, so they refuse a `name_taken` result from a server that carries this change. It lands before the v2 floor release (0.140.0), so no v2 client sees `shape`.\n\n**Migration.** Read the holder's Shapes with `get` on `details.takenBy.durableId`; a Thing can have several Shapes or none. Choose the replacement name yourself.\n\n```ts\n// Before\nconst { takenBy, suggestedName } = details\nconsole.log(takenBy?.shape, suggestedName)\n\n// After\nconst holder = details.takenBy && (await client.thing.get(org, repo, details.takenBy.durableId))\n```"
169
+ },
170
+ {
171
+ "id": "restore-operation",
172
+ "body": "### Added\n\nThe `restore` operation and `OperationBuilder.restore()` make a retracted Thing active again, by its durable ID. Pass `newName` to restore it under another name."
173
+ },
174
+ {
175
+ "id": "v2-write-form",
176
+ "body": "In these notes, \"old grammar\" means the reference form used before this release (`Deal/acme`, operations with `kind`), and \"new grammar\" the form this release introduces (`acme`, operations with `shapes`).\n\n### Breaking\n\n#### Write operations take `name` and `shapes`\n\n**What it was.** Operations carried `kind`, an add name spelled\n`Shape/name`, a top-level `about`, or collection `type`/`members`, and the\nSDK converted them.\n\n**What it is now.** `Operation`, `AddOperation`, `ReviseOperation`,\n`RetractOperation`, `ReaffirmOperation` and `RenameOperation` keep their\nnames but have the new shape. An add or revise requires `name`, `shapes`\nand `data`. `kind` and `shape` are typed `never`. The new `WriteOperation`\ntype is one operation of that form.\n\n- A Shape is the Thing `hub/shapes/X` (`shapeName('X')`), written with\n `shapes: []`.\n- An assertion is a Thing one of whose declared Shapes composes `Assertion`,\n with its target in `data.about`.\n- A collection is a Thing of its collection Shape:\n `{ shapes: ['Set'], data: { members } }`.\n\n**Migration.**\n\n```ts\n// Before\nawait client.commit.apply('acme', 'world', 'seed', [\n { operation: 'add', kind: 'thing', name: 'Deal/acme', data: { company: 'Acme' } },\n])\n\n// After\nawait client.commit.apply('acme', 'world', 'seed', [\n { operation: 'add', name: 'acme', shapes: ['Deal'], data: { company: 'Acme' } },\n])\n```\n\n#### Old-grammar operations are refused before any request\n\n**What it was.** The SDK converted old-grammar operations.\n\n**What it is now.** An operation with `kind`, `shape`, `wref`,\n`declaredShapes`, `about`, collection `type` or `members`, or with no\n`operation`, is refused with a `WriteOperationError` before anything is\nsent. The message shows a suggested new form, with data values replaced by\n`\"...\"`. Names and `about` targets are copied as written, so check them: a\nsuggested `name: 'Deal/acme'` would address a Thing literally named\n`Deal/acme`. An add or revise with no `shapes` is refused with a message\nthat it states no Shapes.\n\nOld-grammar input no longer type-checks, so it reaches this refusal only\nthrough a cast.\n\n**Migration.**\n\n```ts\n// Before: converted and sent\nawait client.commit.apply('acme', 'world', 'rate', [\n { operation: 'add', kind: 'assertion', name: 'Rating/r1', about: 'Deal/acme', data: { score: 5 } },\n])\n\n// After\nawait client.commit.apply('acme', 'world', 'rate', [\n { operation: 'add', name: 'r1', shapes: ['Rating'], data: { about: 'acme', score: 5 } },\n])\n```\n\nOther operations refused locally:\n\n- a pinned Shape in `shapes` (`Deal@v2`), or an entry that is not a bare\n Shape name (`hub/shapes/Deal`, anything with `/`);\n- `shapeless` on a raw operation (spell it `shapes: []`);\n- non-empty `shapes` on a `hub/shapes/X` name;\n- an add or revise without `data`, and `keepShapes` on an add;\n- `affirmedTargets` on a shapeless write;\n- `expectedVersion` on an add, and `active` on a revise;\n- renaming a Shape to a non-Shape name, or the reverse;\n- `+` in an added or renamed-to name;\n- an added or renamed-to name under the reserved `hub` root\n (`RESERVED_NAME`);\n- the same name added twice in one submission (`ILLEGAL_OP_SEQUENCE`);\n- errors found by the local Shape-definition checks.\n\n#### `OperationBuilder` methods take the name first\n\n**What it was.** `add({ name, data })`, `revise({ name, data })`,\n`retract(target)`, `reaffirm({ name, kind, ... })`; no `rename`.\n\n**What it is now.**\n\n| Method | After |\n|---|---|\n| `add` | `add(name, { shapes \\| shapeless, data, ... })` |\n| `revise` | `revise(name, { shapes \\| shapeless \\| keepShapes, data, ... })` |\n| `retract` | `retract(name, { reason? })` |\n| `reaffirm` | `reaffirm(name, { add?, remove? })` |\n| `rename` | `rename(name, newName)` (new) |\n\nEach add and revise states its Shapes in exactly one way: `shapes: [...]`,\n`shapeless: true`, or (revise only) `keepShapes: true`. The builder refuses\nzero ways or more than one, `keepShapes` on an add, `shapeless` on a\n`hub/shapes/X` name, and `shapes: []` on a non-Shape name (spell that\n`shapeless: true`).\n\n**Migration.**\n\n```ts\n// Before\nimport { OperationBuilder } from '@warmhub/sdk-ts'\nconst ob = new OperationBuilder()\nob.add({ name: 'Deal/acme', data: { company: 'Acme' } })\nob.retract('Deal/old')\nawait ob.commit({ client, orgName: 'acme', repoName: 'world', message: 'seed' })\n\n// After\nimport { createOperationEventRequestId } from '@warmhub/sdk-ts'\n\nawait client.shape.create('acme', 'world', 'Deal', { company: 'string' }, {\n eventRequestId: createOperationEventRequestId(),\n})\n\nawait client.commit.apply(\n 'acme',\n 'world',\n 'seed',\n client\n .ops()\n .add('acme', { shapes: ['Deal'], data: { company: 'Acme' } })\n .add('scratch', { shapeless: true, data: { note: 'free' } })\n .rename('scratch', 'scratch-2')\n .retract('old'),\n)\n\n// A later submission: keepShapes revises a Thing that already exists\nawait client.commit.apply(\n 'acme',\n 'world',\n 'update',\n client.ops().revise('acme', { keepShapes: true, data: { company: 'Acme 2' } }),\n)\n```\n\nA `keepShapes` revise of a Thing added or renamed earlier in the same\nsubmission is refused; state `shapes` instead.\n\n#### Builder errors are `WriteOperationError`, raised when an operation is queued\n\n**What it was.** Some refusals surfaced only at commit.\n\n**What it is now.** A per-operation refusal throws at the `.add(...)` or\n`.revise(...)` call. Whole-batch refusals (\"already submitted\", \"in flight\")\nhave `operationIndex: undefined`. The code is usually `VALIDATION_ERROR`;\nit can also be `RESERVED_NAME`, `ILLEGAL_OP_SEQUENCE`, a Shape-name check\ncode, or (for `keepShapes`, at submit) `NOT_FOUND`.\n\n**Migration.**\n\n```ts\nimport { WriteOperationError, type AddInput } from '@warmhub/sdk-ts'\n\ntry {\n client.ops().add('acme', { data: {} } as AddInput) // no Shapes stated\n} catch (e) {\n if (e instanceof WriteOperationError) console.log(e.code, e.operationIndex) // VALIDATION_ERROR 0\n}\n```\n\n#### Every reference argument uses the new grammar\n\n**What it was.** Read and write calls took `Deal/acme`, and a Shape was\n`Deal`.\n\n**What it is now.** Pass the Thing name (`acme`) and `hub/shapes/Deal` for a\nShape where any Thing can appear. An old spelling is looked up literally: a\nsingle read gets `NOT_FOUND`, and `thing.getMany` lists it in `missing`.\nGlobs (`match`) match Thing names, and `*` does not cross `/`.\n\n**Migration.**\n\n```ts\n// Before\nawait client.thing.get('acme', 'world', 'Deal/acme')\n// After\nawait client.thing.get('acme', 'world', 'acme')\n```\n\n#### `thing.rename` takes full names\n\n**What it was.** `rename(org, repo, shapeName, oldName, newName, opts)`.\n\n**What it is now.** `rename(org, repo, name, newName, opts)`. A call in the\nold argument order is refused locally with `VALIDATION_ERROR`.\n\n**Migration.**\n\n```ts\n// Before\nawait client.thing.rename('acme', 'world', 'Deal', 'acme', 'acme-corp', { eventRequestId })\n// After\nawait client.thing.rename('acme', 'world', 'acme', 'acme-corp', { eventRequestId })\n```\n\n#### Read results drop `shape`, `shapeName` and `validatedShape`\n\n**What it was.** Results carried a single Shape.\n\n**What it is now.** Use `declaredShapes` (the Shapes the version states)\nand `validatedShapes` (the Shapes it satisfies, including composed ones).\nBoth hold version-pinned names such as `Deal@v3`. The removal covers\n`ThingDetail`, `ThingItem`, `thing.getMany` items, `thing.getWithLease`,\nhistory versions and history's `thing`, refs items, component owned Things\nand `view.evaluate` items. `SynthesizedRepoContent.shape` becomes\n`declaredShapes` / `validatedShapes`.\n\n`ThingDetail` allows extra keys, so `detail.shapeName` still compiles; it is\n`undefined` at run time. Search your code for these field names.\n\n`shape` as an input filter (`thing.head(org, repo, { shape: 'Deal' })`) is\nunchanged.\n\n**Migration.**\n\n```ts\n// Before\nconst shape = detail.shapeName\n// After\nconst shapes = (detail.declaredShapes ?? []).map((w) => w.replace(/@v\\d+$/, ''))\n```\n\n#### Token scopes use `grants`, on input and output\n\n**What it was.** A scope entry carried `allowedMatches` and `allowedShapes`.\n\n**What it is now.** Restrictions are a list of (paths, Shapes) pairs in\n`grants`.\n\n- Input (`token.create`): the entry-level fields are still typed `unknown`,\n so old code compiles, but the server refuses them with \"allowedMatches and\n allowedShapes are no longer scope-entry fields: give each (paths, Shapes)\n pair in grants…\".\n- Output (`TokenCreateResult`, token metadata, whoami scopes): the\n entry-level fields are removed, and reading them is a compile error. Read\n `grants`.\n- `org.setMemberScopes` takes entries with `resource` and `permissions`\n only; restrictions on member scopes are refused.\n- A pair must carry a non-empty `allowedMatches`, or an `allowedShapes`\n with at least one key. `grants: []`, `{}` and `{ allowedShapes: {} }` are\n refused.\n- `shapeless: true` in a clause is not a restriction of its own: it lets\n Things with no Shape through. Alone it admits every Thing the pair's paths\n match, shaped or shapeless; beside `shapes` it adds shapeless Things to\n the Things the clause covers. No clause restricts a pair to shapeless\n Things only.\n\n**Migration.**\n\n```ts\n// Before\nawait client.token.create({ name: 'ci', scopes: [\n { resource: 'acme/world', permissions: ['read'], allowedMatches: ['Deal/**'] },\n] })\n// After\nawait client.token.create({ name: 'ci', scopes: [\n { resource: 'acme/world', permissions: ['read'],\n grants: [{ allowedMatches: ['**'], allowedShapes: { shapes: ['Deal'] } }] },\n] })\n```\n\n#### Repository exports are format 6, and the delta fold needs formats\n\n**What it was.** Exports were format 5, and\n`applyRepositoryExportDelta(base, delta)` returned rows keyed by durable id.\n\n**What it is now.**\n\n- The server writes format 6. This SDK reads 5 and 6; older SDKs read only\n 5, so they cannot read exports made after this release reaches\n production.\n- Format 6 uses new-grammar references, has one row per Shape version, and\n may carry `retracted: true` on Shape rows. A Shape tombstone in format 6\n is rejected as `row_invalid`.\n- `applyRepositoryExportDelta` takes a third argument, the formats. Mixing\n format 5 and format 6 throws `RepositoryExportError` with reason\n `format_mismatch`.\n- In the returned map, format 6 Shape rows are keyed `durableId@vN`, not\n `durableId`. Code that looks a Shape up by durable id, or keys its own map\n by durable id, must account for versions.\n\n**Migration.**\n\n```ts\n// Before\nconst current = applyRepositoryExportDelta(base, delta)\n// After\nconst current = applyRepositoryExportDelta(base, delta, {\n base: baseSession.header!.formatVersion,\n delta: deltaSession.header!.formatVersion,\n})\n```\n\n#### `stream.append` takes new-grammar operations only\n\n**What it was.** `stream.append` took the old operation form.\n\n**What it is now.** It takes new-grammar operations and does not accept\n`keepShapes` (use `createKeepShapesResolver`). Its only local check is a\npinned Shape; everything else is checked by the server.\n\n**Migration.** Send `{ operation, name, shapes, data }` operations.\n\n#### Writes need a server running this release\n\n**What it was.** The SDK wrote to any current server.\n\n**What it is now.** Against an older server, writes fail the compatibility\ncheck with \"@warmhub/sdk-ts requires write contract 3, but the backend\nprovides 2.\"\n\n**Migration.** Upgrade the SDK after the server.\n\n### Added\n\n- **`client.ops(options?)`** — returns a new `OperationBuilder`.\n- **`shapeName(bare)`** — `'Deal'` to `'hub/shapes/Deal'`; idempotent.\n- **`isShapeReference(value)`** — true for a bare, unpinned Shape name.\n- **`WriteOperationError`** (extends `WarmHubError`) — with\n `operationIndex`; its message starts `Invalid operation at index N:`.\n- **Types** — `WriteOperation`, `KeepShapesRevise`, `AddInput`,\n `ReviseInput`, `RetractInput`, `ReaffirmInput`, `ShapesChoice`,\n `CommitValidateOperations`, `RepositoryExportDeltaFormats`,\n `ThingShapesReader`.\n- **`keepShapes: true` on a revise** — at submit the SDK reads the Thing's\n current Shapes and version and sends both, the version as\n `expectedVersion` (a supplied `expectedVersion` wins). Shapes are sent\n bare, so the revise is checked against each Shape's current version. A\n missing Thing is refused with `NOT_FOUND`. A Thing added, or renamed to or\n from, earlier in the same submission is refused. A Thing revised earlier in\n the same submission reuses that revise's Shapes and sends no version.\n Because the version is pinned, resubmitting the same `submissionId` from\n another process can conflict; explicit `shapes` is the retry-safe form.\n ```ts\n await client.commit.apply('acme', 'world', 'm',\n client.ops().revise('acme', { keepShapes: true, data: { company: 'Acme 2' } }))\n ```\n- **`createKeepShapesResolver(read)`** — resolves `keepShapes` revises for\n `stream.append`. It keeps state across calls, so use one resolver per\n submission.\n ```ts\n const resolve = createKeepShapesResolver((names) => client.thing.getMany('acme', 'world', names))\n await client.stream.append({ /* ... */ operations: await resolve([\n { operation: 'revise', name: 'acme', keepShapes: true, data: {} },\n ]) })\n ```\n- **Builders everywhere** — `commit.validate`, `commit.apply` and\n `commit.applyStreaming` accept an `OperationBuilder` as well as an array.\n- **`shapeless: true` read filter** — on `thing.head`, `thing.query`,\n `thing.count` and `live.thingHead`: only Things with no Shape. It\n cannot be combined with `shape`, `declaredShape`, `match`, a token\n restricted by paths, or `sinceRepoSeq`, and is not available on search or\n the changes feeds.\n ```ts\n await client.thing.head('acme', 'world', { shapeless: true })\n ```\n- **`warnings.shapeless?: true`** — on per-operation write results.\n- **`ThingGetManyResult.missingHints`** — for a missing reference that is an\n old `Shape/name` spelling of a Thing you can read, the text naming its new\n form.\n- **`declaredShapes` on refs items and history's `thing`** (history also\n gains `validatedShapes`).\n- **`redactedResource: true`** — may appear on `TokenCreateResult` scopes.\n- **`details.reason` values** — `'legacy-wrefs-window-ended'` (from the\n server) and `'legacy-wrefs-sdk'` (local).\n- **`DEPENDENCY_FAILED`** — exported as a constant.\n- **`LEGACY_WREFS_REFUSAL` and `WarmHubClient.clientFlags`**.\n- **Export** — `RepositoryExportErrorReason` gains `'format_mismatch'`;\n `RepositoryExportSession.lastShapeVersion`; an `onRow` option on\n `restoreRepositoryExportSession`. A resume that breaks between two versions\n of one Shape continues from the right version.\n\n### Behavior worth knowing\n\n- **`clientFlags: ['legacy-wrefs']` refuses every call.** The SDK does not\n send old-grammar requests. With the flag set, reads, writes, streaming,\n export and the compatibility check all throw `WarmHubError` with code\n `CLIENT_INCOMPATIBLE`, `details.reason: 'legacy-wrefs-sdk'`, and the\n message \"`legacy-wrefs` is set; the SDKs are v2-only. Unset it, or use the\n `wh` CLI for v1.\" Use the CLI to submit old-grammar files during the\n compatibility window.\n\n### Changed\n\n- **Local checks before sending.** `commit.apply`, `commit.validate` and\n the builder check every operation before sending when given an array or a\n builder. An iterable, an async iterable, or `commit.applyStreaming` is\n checked chunk by chunk, so a bad operation late in the source is refused\n after earlier chunks were committed (`PartialStreamSubmissionError`).\n `stream.append` checks only pinned Shapes.\n- **Content-limit error labels.**\n ```text\n Before: Field \"Content/Readme.content\" is N bytes; WarmHub content fields are limited to 65536 bytes. ...\n After: Field \"Readme.content\" is N bytes; WarmHub content fields are limited to 65536 bytes. ...\n ```\n `Agents.content` changes the same way.\n\n### Removed\n\n- **`AddOp`, `ReviseOp`, `RetractOp`, `CollectionAddOp`,\n `CollectionAddType`, `OperationBuilderAddInput`** — use `WriteOperation`\n and the builder input types.\n ```ts\n // Before\n const op: AddOp = { name: 'Deal/acme', data: {} }\n // After\n const op: WriteOperation = { operation: 'add', name: 'acme', shapes: ['Deal'], data: {} }\n ```\n- **Automatic old-grammar conversion** (`kind`, `about`, `type`/`members`,\n `Shape/name` splitting).\n- **The local \"Selector-backed revise requires a Set/<name> target wref\"\n check in `collection.revise`.** The server decides.\n"
177
+ }
178
+ ]
153
179
  }
154
180
  ]
155
181
  }