@oddington/sdk 0.1.37 → 0.1.39

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/README.md CHANGED
@@ -4,13 +4,20 @@ The importable client for OS apps, routines and response handlers.
4
4
  Node operations require Node.js 22.12.0 or later. The root import and hosted
5
5
  Data and Files APIs work in browsers. Installing the package grants no authority.
6
6
 
7
+ When developing OS alongside a customer repository, use the repository's
8
+ [`dev:customer` helper](../../README.md) to run Bun tests and TypeScript checks
9
+ against the built local SDK without changing the customer's released pin,
10
+ lockfile or installed package. Build the SDK and CLI together first; the same
11
+ helper can execute that built CLI from the customer checkout.
12
+
7
13
  Hosted Data and shared Files reach OS directly through `/api/data` and `/api/files`.
8
14
  Inbox owns workspace-hosted `/api/inbox/check`, `/api/inbox/publish`,
9
- `/api/inbox/resolve` and `/api/inbox/files`. Request-key recovery and captured
15
+ `/api/inbox/resolve`. Request-key recovery and captured
10
16
  publication validation are internal behavior of these operations. Retrying the
11
17
  same key and content returns the original result; changed content conflicts.
12
- Existing Inbox-retained files remain separate from shared Files, with their
13
- original identities, bytes and audience. The SDK exposes no raw-credential API.
18
+ Inbox File previews and downloads use package-root `files.read(fileId)` with
19
+ current workspace access. Item visibility does not grant File access. The SDK
20
+ exposes no raw-credential API.
14
21
 
15
22
  Hosted apps use `inbox.questions.list({ routines })` for `AnswerableQuestion[]`.
16
23
  Derive `routines` from the existing app declaration with
@@ -65,7 +72,7 @@ Each step runs at most once and returns `{ summary, data }`. Unknown or repeated
65
72
  steps fail the Run even if routine code catches the error; unreached steps are
66
73
  skipped. Omitted input normalizes to `{}`. A top-level schema can supply a validated
67
74
  immutable default with `.default(value)`, such as
68
- `schema.object({ files: schema.array(file(), { maxItems: 32 }) }).default({ files: [] })`.
75
+ `schema.object({ files: schema.array(file()) }).default({ files: [] })`.
69
76
  Nested defaults are unsupported and rejected. Omitted steps permit no step call;
70
77
  omitted result permits no returned value. `file()` declares a complete OS File descriptor.
71
78
  Its optional `storage` selects local or remote placement and its optional `path`
@@ -87,7 +94,7 @@ this metadata when absent. Setup and safe inspection are documented in the
87
94
 
88
95
  With `access: { ai: true }`, `os.ai.generateText` returns non-empty `{ text }`.
89
96
  `os.ai.generateObject` returns `{ object }` validated against the requested
90
- `schema`, with its inferred type. An invalid answer throws `RoutineAIError`
97
+ `schema`, with its inferred type. An invalid answer throws `AIError`
91
98
  with code `invalid_output`, a clear bounded `detail`, and the original final
92
99
  answer as optional `text` for routine-owned recovery. The text is bounded at
93
100
  256 KiB and excluded from the error message and ordinary JSON serialization.
@@ -95,20 +102,47 @@ The routine can inspect it explicitly, apply its own recovery rules, report
95
102
  an item failure and continue; it must not cast a rejected answer to the promised
96
103
  type. OS does not coerce, drop fields, repair or retry automatically.
97
104
 
105
+ `generateObject` also accepts one exact outer Markdown code fence (empty or
106
+ `json` language tag), then applies the same strict schema checks. It does not
107
+ extract JSON from explanatory prose or repair the answer. `RoutineAIError`
108
+ remains a compatibility alias of `AIError`, so existing `instanceof` checks work.
109
+
110
+ Preserve safe AI error facts when reporting a per-document failure. Keep the
111
+ original answer (`error.text`) out of step data, business records and logs:
112
+
113
+ ```ts
114
+ import { AIError } from '@oddington/sdk';
115
+
116
+ try {
117
+ const { object } = await os.ai.generateObject({ input, instructions, schema });
118
+ // Apply the routine's business validation before using the result.
119
+ } catch (error) {
120
+ if (!(error instanceof AIError)) throw error;
121
+ return { status: 'unavailable', error: {
122
+ code: error.code, message: error.message,
123
+ ...(error.detail ? { detail: error.detail } : {}),
124
+ } };
125
+ }
126
+ ```
127
+
98
128
  An AI call failure can be caught and handled without automatically failing the
99
129
  Run. Uncaught routine or step exceptions still fail execution, and outstanding
100
130
  AI calls drain before completion. The host retains authority, isolation,
101
131
  cancellation and byte limits. Local and remote Runs use the same API.
102
132
 
103
133
  The bounded grammar supports closed objects with required properties, arrays with
104
- an explicit `maxItems` from 1 to 64, booleans, strings (at most 4096 code points),
134
+ an optional positive safe-integer `maxItems` business bound, booleans, strings (at most 4096 code points),
105
135
  finite numbers and safe integers with optional inclusive bounds, and Files.
136
+ Omitting `maxItems` imposes no item-count ceiling; the complete serialized input
137
+ still shares the Run's byte bound, including every File descriptor and other field.
106
138
  Definitions allow at most 64 steps, 64 total object properties across their schemas,
107
- and eight schema levels. Input, each step input, each summary, each detail value,
108
- and the result accept at most 256 KiB of UTF-8 serialized JSON, without truncation.
139
+ and eight schema levels. Run input, each summary, each detail value, and the result
140
+ accept at most 256 KiB of UTF-8 serialized JSON, without truncation. Step inputs
141
+ remain closed, immutable JSON but have no separate byte ceiling because they are
142
+ transient in-process values rather than transported or retained Run evidence.
109
143
  Across 64 steps, retained summary plus detail is at most 32 MiB, plus 512 KiB for
110
- Run input and result; step inputs are transient and individually bounded. Schema
111
- defaults share the input bound. Titles contain at most 256 UTF-16 code units;
144
+ Run input and result. Schema defaults share the input bound. Titles contain at
145
+ most 256 UTF-16 code units;
112
146
  access dimensions and response handlers each have at most 64 entries. Functions,
113
147
  accessors, sparse arrays, cycles and non-JSON values reject. The canonical routine
114
148
  ID is a bounded lowercase alphanumeric/hyphen name, independent of its directory.
@@ -252,7 +286,7 @@ Local Data selects from the Host's exact published index; a warm selection
252
286
  neither initiates nor waits for renewal or synchronous hosted authority checks.
253
287
  Local Files returns `unsupported_context` after checking context. Neither path
254
288
  falls back to hosted services. Local app routines use the same local execution
255
- and explicit publication lifecycle as the CLI.
289
+ and live capability calls as the CLI.
256
290
 
257
291
  Apps can assess a declared routine without starting it:
258
292
 
@@ -268,19 +302,17 @@ const detail = await run.step('read'); // Inferred summary and data for this new
268
302
  const recent = await app.routines.receipts.runs.list();
269
303
  const saved = await app.routines.receipts.runs.get(recent[0].id);
270
304
  console.log(await saved.current());
271
- // await saved.publish(); // Only in response to an explicit publication action.
272
305
  ```
273
306
 
274
307
  The fixed build generates types from each routine definition; no app-authored
275
308
  contract map is needed. Descriptions combine safe structure with live readiness
276
309
  and connection availability. Starts validate the current definition and input;
277
- retained reads and publication recheck complete saved member/source/capability
278
- scope. No installation is needed. Bindings accept no endpoint, credential,
310
+ retained local reads check the established session and saved Run ownership. No installation is needed. Bindings accept no endpoint, credential,
279
311
  command, path or configuration selector. A local app uses its live Local Host;
280
312
  a hosted app uses its existing member session and workspace context to reach
281
313
  the current remote Run owner through the same bindings. Operations reject with
282
314
  `AppRoutineError.reason`: `authority_unavailable`, `execution_unavailable`, or
283
- `invalid_input`. Starts and publications are not automatically retried. A
315
+ `invalid_input`. Starts are not automatically retried. A
284
316
  disconnected watch ends and requires `current()`. History returns at most 20
285
317
  summaries without deleting older Runs. Each JSON value is bounded to 256 KiB.
286
318
  Transport envelopes allow 17 MiB per summary, 514 KiB per detail and 20 summaries
@@ -344,13 +376,18 @@ both shipped SDK inputs and every emitted helper, rejects unexpected inputs and
344
376
  imports, and retains the full credential/server/customer leak scan. Node retains
345
377
  the complete package-root exports; provider classes are never public.
346
378
 
347
- Current auth measurement is **171,346 minified bytes**, including **6,322 fixture
348
- bytes** and **165,024 SDK bytes**, with **45,466 total gzip bytes**. Current
349
- Data/Files fixture measures **47,254 total / 5,893 fixture / 41,361 SDK
350
- bytes**, with **14,906 total gzip bytes**. The shipped runtime now also carries
351
- the local Questions transport and transparent Local Host renewal.
352
- Both fit their explicitly reviewed ceilings: **42,000 SDK bytes** for the pinned
353
- Data/Files fixture and **166,000 SDK bytes** for the separately pinned auth fixture.
379
+ Current auth measurement is **186,220 minified bytes**, including **8,189 fixture
380
+ bytes** and **178,031 SDK bytes**, with **50,478 total gzip bytes**. Current
381
+ Data/Files fixture measures **58,294 total / 7,204 fixture / 51,090 SDK
382
+ bytes**, with **18,751 total gzip bytes**. The shipped runtime also carries
383
+ the local Questions transport, transparent Local Host renewal, and bounded
384
+ Data validation details and guidance.
385
+ Both fit their explicitly reviewed ceilings: **64,000 SDK bytes** for the pinned
386
+ Data/Files fixture and **256,000 SDK bytes** for the separately pinned auth fixture.
387
+ The combined runtime includes validation guidance and local Data recovery
388
+ without another dependency. These allowances leave room for ordinary feature growth while
389
+ catching substantial size regressions. Dependency, compatibility and private-code
390
+ audits remain separate checks.
354
391
  Those ceilings are fixed review decisions, not values regenerated from later
355
392
  measurements. The installed auth realm/host-handoff proof passes alongside the
356
393
  Node exports, declarations, tarball leak scan and browser compatibility audits.
@@ -364,17 +401,18 @@ bytes; generated helpers count toward the SDK. Gzip is diagnostic only.
364
401
 
365
402
  | Same named-import fixture | Legacy diagnostic | Current strict proof |
366
403
  | --- | ---: | ---: |
367
- | Total minified JavaScript bytes | 488,539 | 47,254 |
368
- | Fixture-attributed bytes | 5,954 | 5,893 |
369
- | SDK bytes | 482,585 | 41,361 |
370
- | Total gzip bytes | 198,612 | 14,906 |
404
+ | Total minified JavaScript bytes | 488,539 | 58,294 |
405
+ | Fixture-attributed bytes | 5,954 | 7,204 |
406
+ | SDK bytes | 482,585 | 51,090 |
407
+ | Total gzip bytes | 198,612 | 18,751 |
371
408
 
372
409
  The legacy diagnostic externalized three unresolved Node built-ins and failed
373
410
  browser compatibility. The current strict proof has no imports, shims or warnings
374
- and reduces SDK bytes by 91.4%. Its literal ceiling is **42,000 SDK bytes**:
375
- 639 bytes of deliberately limited headroom. The reviewed measurement includes
411
+ and reduces SDK bytes by 89.4%. Its literal ceiling is **64,000 SDK bytes**.
412
+ The measurement includes
376
413
  the Local Host bootstrap, local Questions transport, transparent renewal and
377
- opaque browser-auth handoff that `os.init` must select at runtime. The verifier pins the fixture
414
+ opaque browser-auth handoff that `os.init` must select at runtime, and bounded
415
+ Data validation errors. The verifier pins the fixture
378
416
  hash and bundler version; changing either requires a reviewed measurement
379
417
  comparison. Future ceiling increases require explicit review of added browser
380
418
  capability and must never be automatically regenerated from an oversized output.
@@ -402,33 +440,29 @@ await files.enrich({ file: report.id, name: 'read.md', content: revisedMarkdown,
402
440
  await files.upload(report.id); // Move a local File and every retained enrichment to remote storage.
403
441
  ```
404
442
 
405
- Hosted Files retains remote originals and caller-produced named enrichments;
406
- `storage: 'local'` requires a managed CLI parent. Configure that parent with
407
- `OS_FILES_ROOT` pointing to an empty or already OS-managed dedicated
408
- directory outside tracked Git content. `OS_FILES_DEFAULT=local|remote`
409
- selects its default; without it, remote remains the default. Per-File `storage`
410
- overrides it. The root stays private to the parent. Local creation without a
411
- configured root fails with `unsupported_context`. Files have one durable home;
412
- `both` is invalid. Local retention establishes no hosted registration.
413
- Fresh runs recover the same creator's File by its request key, and verify
414
- retained bytes before returning a descriptor or content. A dead parent fails
415
- closed. `os routine run <name>` stages newly created Files locally for that Run,
416
- including local-placement Files, and defers promotion, enrichments and explicit
417
- uploads until `os routine publish <run>`. Legacy Inbox file capture remains
418
- additive. Existing hosted Files are read-only during local execution.
419
- `upload(id)` preserves the File descriptor, creator access and existing Source
420
- references. Local bytes remain authoritative until all remote objects verify;
421
- retrying a completed upload returns the same File. Later reads and enrichments
422
- follow remote storage; any remaining managed local bytes are disposable cache.
423
- An unreferenced local File is disclosed remotely only when explicitly uploaded
424
- or successfully published as a Source. An authorized hosted caller receives
425
- non-retryable `local_only` (409) until upload succeeds; unrelated callers receive
426
- `access_denied`.
443
+ Hosted Files retains remote originals and caller-produced named enrichments.
444
+ A managed local CLI parent stores local Files beneath the signed-in active
445
+ workspace's `~/.os/<workspace>/files` root; callers cannot configure another
446
+ root. Local Runs default new Files to local storage, hosted Runs to remote.
447
+ An existing local or remote File keeps its placement. Every completed File has
448
+ one workspace registry row, including local Files whose bytes stay on this
449
+ device. Request-key replay and reusable enrichments preserve the same identity
450
+ across member calls, Runs and restarts.
451
+
452
+ Explicit remote creation, upload and enrichment complete during their calls.
453
+ `upload(id)` verifies the original and every enrichment before changing the same
454
+ File's placement; retrying a completed upload returns that File. Later reads
455
+ and enrichments follow its pinned remote store. A Source reference leaves a
456
+ local File local. Inbox publication uploads that same File before exposing the
457
+ Ask or Digest. Completed effects survive later Run failure. A runtime without
458
+ the local bytes reports `local_only` (409).
427
459
  Unnamed inputs require a safe `name`; streams also require declared `size`.
428
460
  Blob and byte-array declarations must match their intrinsic size. Originals are
429
- bounded to 16 MiB per object, metadata to 4 KiB, and descriptor/error bodies to 16 KiB.
430
- Operations share four concurrent slots and a 30-second deadline;
431
- transport retries at most once with the same prepared bytes and request key.
461
+ bounded to 5 GiB per original and 16 MiB per enrichment. Large originals stream
462
+ through bounded multipart transfers; local writes and reads use bounded disk
463
+ buffers. Operations share four concurrent slots and idle/request deadlines;
464
+ later calls wait in invocation order without reading caller content, and
465
+ transport retries preserve the same bytes and request key.
432
466
  Omitting a key generates one for that invocation. Supply a stable key to recover
433
467
  across processes or fresh routine runs. Caller inputs remain owned by the caller.
434
468
  Each request resolves current authority; hosted routine Files calls use the
@@ -441,24 +475,10 @@ versus received size. Streaming rejection marks partial received counts with
441
475
  failure evidence without raw names, keys, file content or provider text. Older
442
476
  errors without `detail` remain valid. Reads return the successful Web
443
477
  Response without consuming its body. Local transfer is bounded and buffered
444
- before constructing that Response. Creation grants private creator access;
445
- workspace membership alone never grants another creator's File.
446
-
447
- `await files.delete(fileId)` removes an unreferenced File's original and all
448
- retained enrichments under its current member or installed-routine creator.
449
- It returns `void` only after physical cleanup and durable completion; a completed
450
- retry succeeds. Independent retention returns `conflict` without changing the
451
- File. `access_denied` and `integrity` remain terminal; `unavailable` and
452
- `capacity` are retryable. Pending deletion denies reads, writes, new uses and
453
- creation-key replay while the same current creator retries cleanup. Deletion
454
- uses the authenticated parent for managed local Files and uploaded caches.
455
- R2 cleanup verifies the registered immutable key and native revision, removes
456
- only that object and confirms absence. Each physical writer owns a distinct
457
- candidate key, so a delayed writer cannot recreate the deleted representation.
458
- Pending admission is never success.
459
- When a completion reply is lost, retry `files.delete` with the same File ID and
460
- current creator authority. Keep stable creation keys separate from deletion
461
- recovery: a completed deleted key is denied and cannot recreate the File.
478
+ before constructing that Response. Current workspace members can use every File in that workspace. A
479
+ routine's `files: "read"` permits get/read; `files: "read-write"` also permits
480
+ create/enrich/upload. References in Sources, Runs and Inbox items grant no
481
+ additional access. Files has no public delete operation.
462
482
 
463
483
  `enrich` accepts a string encoded as UTF-8 or exact `Uint8Array` bytes, a safe
464
484
  flat filename, and an optional MIME hint. OS verifies text/Markdown/JSON or
@@ -491,7 +511,7 @@ const changes = data.repository('general').changes();
491
511
  changes.create({ ...statement, added: '2026-10-20' });
492
512
  changes.create({ $ref: 'transactions/one',
493
513
  sources: [source.csv.row(statement.$ref, parsedTransaction.recordPosition)] });
494
- await changes.publish();
514
+ await changes.apply();
495
515
  ```
496
516
 
497
517
  The caller's CSV parser supplies `recordPosition` from the retained statement:
@@ -508,7 +528,12 @@ guarded Data update. Known objects match by OS File ID, Gmail connection/message
508
528
  attachment, or Dropbox connection/File ID; newer Locations replace old path,
509
529
  revision and URL facts. Distinct objects and custom provider values remain.
510
530
  Helpers need no initialization and perform no I/O, choose no ref, and add no
511
- changes. File retention and Data validation/publication remain explicit.
531
+ changes. File retention and Data validation/application remain explicit.
532
+
533
+ File Sources use the OS-defined fields `filename`, `media_type`, `size_bytes`,
534
+ and `sha256`. Keep the customer repository's Source schema aligned with this
535
+ definition when updating it. `mime_type` is not an alias for `media_type`;
536
+ do not rename a field produced by `source.file()` to fit an incompatible schema.
512
537
 
513
538
  Microsoft helpers are also exported at the package root, with the corresponding
514
539
  `OutlookSourceLocation` and `SharePointSourceLocation` types:
@@ -526,7 +551,7 @@ const itemLocation = source.location.sharepoint({
526
551
 
527
552
  Construct these only after successful provider reads establish the coordinates
528
553
  for the retained bytes, and save them alongside `source.location.os(file)` using
529
- ordinary Data publication. Request `Prefer: IdType="ImmutableId"` consistently
554
+ an ordinary Data apply. Request `Prefer: IdType="ImmutableId"` consistently
530
555
  when obtaining Outlook message and attachment IDs. These IDs are case-sensitive
531
556
  and survive folder moves within the same mailbox, but not archive-mailbox moves
532
557
  or export/reimport. Subjects, filenames and `internetMessageId` are not substitutes.
@@ -570,12 +595,11 @@ Binding is inert. Each operation resolves current workspace, endpoint and
570
595
  short-lived authority from the trusted app host or managed routine context.
571
596
  Customer calls cannot configure those values. Missing or expired context
572
597
  rejects; a named handle does not expand repository access. During
573
- `os routine run <name>`, the same calls use the matched checkout's committed
574
- version through a private parent transport. Dirty files are ignored. An OS
575
- cache outside the checkout reuses derived data; reads require no remote access
576
- or preparation storage. Later accepted record effects advance the run's view
577
- to their exact proposed version. Login, installed grants and provider authority
578
- retain their normal checks. A dead local parent fails without hosted fallback.
598
+ `os routine run <name>`, the same calls read current schema-governed Markdown
599
+ in the trusted checkout, including staged, modified and untracked Records. Each
600
+ operation reads the files as it begins. Later calls see completed writes and
601
+ external edits. Current local Data acquires no hosted binding or index. A dead
602
+ local parent fails without hosted fallback.
579
603
 
580
604
  Each graph selection sends one request and returns only selected canonical
581
605
  values, expanded forward links and exact opaque public versions. Cursors
@@ -632,7 +656,9 @@ use `select` for a manual `after` cursor or explicit historical selection.
632
656
  `all` consumes the same iterator and returns only after successful exhaustion.
633
657
  Its required positive safe-integer `maxRecords` caps top-level Record occurrences
634
658
  across all collections; expanded links do not count toward that ceiling. Overflow
635
- rejects with nonretryable `capacity`, without a partial or truncated result.
659
+ names the queried collections and ceiling, tells the author to narrow the query
660
+ or increase `maxRecords`, and rejects with nonretryable `capacity` without a
661
+ partial or truncated result.
636
662
  Invalid helper arguments use `invalid_query`; malformed pages, cursor cycles,
637
663
  snapshot changes or inconsistent requested counts use nonretryable `unavailable`.
638
664
  Selection and authority failures propagate unchanged, with no retry or restart.
@@ -706,15 +732,11 @@ projection, highlighting, embedding or historical version option.
706
732
 
707
733
  Matches have deterministic order and complete canonical Records. Empty matches
708
734
  succeed with the exact searched version. Members search latest published data;
709
- routines search their captured version, advancing only through their own
710
- publication. Hosted and app children publish immediately. During
711
- `os routine run <name>`, search uses the same committed local Index as select,
712
- ignores dirty/untracked files and advances only to that run's exact proposed
713
- version after its own Changes publication. Validation does not advance reads;
714
- another publisher does not move the captured view. Search Records can supply
715
- guards for subsequent Changes. Missing or incomplete search data, including
716
- retained versions without it, fails retryably with `unavailable` without an
717
- alternate read path.
735
+ hosted routines search their admitted hosted version. Local routines search the
736
+ schema-governed Markdown currently in the checkout, including staged, modified
737
+ and untracked Records. Each call refreshes its operation-local view; a later call
738
+ sees completed Changes writes and external edits. Search Records supply captured
739
+ versions for subsequent Changes. Explicit History keeps its hosted contract.
718
740
 
719
741
  `where` combines field conditions with AND. Alongside exact scalar/null equality
720
742
  and `$in`, declared one-valued `integer`, `number` and `date` fields accept
@@ -753,37 +775,106 @@ changes.create({
753
775
  changes.update(first.invoices[0], { amount: 30 });
754
776
  changes.delete(first.invoices[1]);
755
777
  await changes.validate();
756
- const publication = await changes.publish();
778
+ const applied = await changes.apply();
757
779
  ```
758
780
 
781
+ Use reserved `$body` to author a Record's Markdown content independently of its
782
+ schema fields, in the same guarded change:
783
+
784
+ ```ts
785
+ changes.create({ $ref: 'emails/email-123', type: 'email', subject: 'Invoice',
786
+ $body: 'Hello,\n\nPlease find our invoice attached.\n' });
787
+ changes.update(existingEmail, { $body: revisedMarkdown }); // body-only change
788
+ const emails = await general.records.read(emailRefs, { body: true });
789
+ ```
790
+
791
+ Omitted `$body` on create means empty; omitted on update preserves the existing
792
+ body exactly. A string deliberately replaces it, `''` clears it, and `null` is
793
+ invalid. Body-only edits preserve front matter, field-only edits preserve the
794
+ body, and both share the Record's exact-byte version guard. Bodies round-trip
795
+ without Markdown interpretation, formatting, trimming, or fence semantics.
796
+ Ordinary reads, selections and search stay body-free; explicitly requested
797
+ bodies use the same ordered batch/null-slot shape. Local change batches support
798
+ 32 MiB of serialized JSON; hosted requests retain their separate 256 KiB limit.
799
+ Body-enabled read responses are bounded to 8 MiB, with an explicit error
800
+ instead of truncation. Hosted body reads use a protected immutable v3 index;
801
+ older indexes without body coverage are unavailable rather than falsely empty.
802
+
803
+ The customer routine owns content and authoring policy. For imported emails,
804
+ create with the prepared body, omit `$body` on ordinary reruns, and backfill
805
+ only bodies explicitly read as exactly empty. Existing notes and fenced blocks
806
+ remain ordinary authored content. A fresh version alone does not authorize
807
+ overwriting an existing nonempty body.
808
+
759
809
  Creation captures a complete Record and stable `$ref`, without I/O or a caller
760
810
  path. OS resolves exactly one collection and its schema-declared filing path.
761
811
  Optional `$filing` supplies only unresolved named safe components and is never
762
812
  stored or returned as Record data; missing components reject by name. Updates
763
813
  capture only `$ref`, `$version` and explicit fields. Deletion captures only the
764
- selected `$ref` and `$version`. Links within the deletion set are allowed;
765
- schema-declared links from any committed Record outside it refuse the whole group,
766
- even if a proposed update removes that link. Validation checks the complete
767
- candidate graph without effects; publication repeats authoritative validation
768
- and applies every create/update/delete in one commit.
814
+ selected `$ref` and `$version`. For hosted Data, links within the deletion set
815
+ are allowed; schema-declared links from any committed Record outside it refuse
816
+ the whole group, even if a proposed update removes that link. Hosted validation
817
+ checks the complete candidate graph without effects, and `apply()` repeats
818
+ authoritative validation before writing every create/update/delete together.
769
819
  Success returns `{ version, created: [{ $ref, $version }], updated: [{ $ref, $version }], deleted: [{ $ref, $version }] }`
770
820
  only after its exact index is ready. Later current reads discover that version.
771
821
  Deleted identities keep their captured versions; deletion creates no new Record
772
822
  version or tombstone. A Source deletion removes only that guarded Record, retaining
773
823
  its File, enrichments and existing durable uses.
774
- During `os routine run <name>`, publication retains the exact proposal and
775
- advances only run-local reads. `os routine publish <run>` later applies and indexes
776
- that immutable proposal; unrelated staged and unstaged work is preserved.
824
+ During a local Run, `changes.apply()` validates the touched Records, paths,
825
+ authored links and captured versions without auditing unrelated Record
826
+ contents, then writes individual complete files atomically. Completed paths
827
+ remain after a later filesystem failure. `validate()` grants no later
828
+ permission to write; subsequent operations read the current checkout. There is
829
+ no later Run publication, promotion or prepared repository state.
777
830
 
778
831
  `invalid_change` and known-unapplied `conflict` reject the batch.
779
- `publication_pending` is retryable and may report `uncertain` or `applied`;
832
+ Unknown, missing, or invalid field failures include the Record ref and
833
+ field name in the SDK error's `message`, with guidance to check the collection
834
+ schema and its relative file location when known. Use
835
+ `repository.collections.schema(name)` to inspect that schema.
836
+ The error code and effect remain available for programmatic handling; the
837
+ message does not include rejected field values.
838
+
839
+ Known field failures also expose optional `error.detail`, exported as
840
+ `DataValidationDetail`. It contains `record`, `field`, `reason`, and, when
841
+ known, `schemaFile` and `expected`. Reasons are `unknown_field`, `missing_field`,
842
+ `invalid_value`, and `source_schema_mismatch`. `expected` contains the field
843
+ `type`, `many`, and `nullable`, with `decimalPlaces` for a bounded number or
844
+ `format: 'month'` for a month date. Enum/constant values and rejected values
845
+ are excluded. Older errors without detail remain valid.
846
+ Other Data errors use this same optional `detail` field for local read/write
847
+ recovery or record-limit facts. Check `'reason' in error.detail` before reading
848
+ field-validation properties.
849
+
850
+ When handling a failure, preserve the SDK error's message. For example, a
851
+ routine that catches Data errors and reports them in its own result can keep
852
+ the explanation alongside the stable code:
853
+
854
+ ```ts
855
+ import { DataOperationError } from '@oddington/sdk';
856
+
857
+ try {
858
+ await changes.validate();
859
+ } catch (error) {
860
+ if (!(error instanceof DataOperationError)) throw error;
861
+ return { code: error.code, message: error.message,
862
+ ...(error.detail ? { detail: error.detail } : {}) };
863
+ }
864
+ ```
865
+
866
+ When the routine cannot continue, rethrow the original error to preserve its
867
+ type and guidance. A new generic `Error` loses the SDK's bounded failure facts.
868
+
869
+ `apply_pending` is retryable and may report `uncertain` or `applied`;
780
870
  retry the same change set to reconcile its retained identity. A transport failure
781
- also keeps that identity. Concurrent publication calls coalesce, confirmed calls
871
+ also keeps that identity. Concurrent `apply()` calls coalesce, confirmed calls
782
872
  replay the result, and changed intent requires a new change set. Updates are
783
- bounded with creates and deletions to 10,000 total operations and the 256 KiB Data
784
- request limit, with one operation per ref. Search and history are outside this
785
- Changes slice. Source Records use ordinary schema rules; Data also validates OS
786
- File identity/access and registers durable uses from its exact ready artifact.
873
+ bounded with creates and deletions to 10,000 total operations, with one operation
874
+ per ref. The complete serialized change request is limited to 32 MiB for local
875
+ file changes and 256 KiB for hosted writes. Search and history are outside this
876
+ Changes slice. Source Records use ordinary schema rules. Files checks access
877
+ when a consumer reads the referenced content.
787
878
 
788
879
  Bind a declared Airtable connection with `connections.airtable('project-management')`
789
880
  from the package root. Import and binding perform no network or credential work.
@@ -816,42 +907,41 @@ records separately. `AirtableOperationError` reports `unapplied`, `partial` or
816
907
  work; never repeat the whole change blindly. Credentials must never be retained
817
908
  in files, publication, logs, handler input, errors or receipts.
818
909
 
819
- Managed routine launches supply workspace, scoped capability and working root.
820
- The defaults read `OS_ROUTINE_WORKSPACE`, `OS_ROUTINE_RUN` and
821
- `OS_INBOX_WORKING_ROOT` (or the current directory). Callers may supply the
822
- existing `workspace`, `capability` and `workingRoot` options. Runtime context
910
+ Managed routine launches supply workspace and scoped capability.
911
+ The defaults read `OS_ROUTINE_WORKSPACE` and `OS_ROUTINE_RUN`. Hosts may supply
912
+ `workspace`, `capability` and `request` options. Runtime context
823
913
  provides authority; callers cannot select repository remotes or expand grants.
824
914
  `os routine run <name> [--input <file|->]` executes the conventional inert
825
- definition with declared JSON input and captures OS-owned changes.
826
- `os routine publish <run>` sends those exact changes and recovers confirmed
827
- receipts without invoking business code again. Filesystem, model and installed
828
- connection effects occur immediately, including before a later failure.
829
- No-change success retains Run evidence without requiring publish. `--json`
830
- returns typed Run state/counts/result, or confirmed publish receipts/links.
915
+ definition with declared JSON input. Data writes and hosted capability calls
916
+ complete live; successful earlier work remains after a later failure. The Run
917
+ retains exact local paths, safe hosted effects and Git provenance. `--json`
918
+ returns typed Run state, result/failure and retained local activity.
831
919
  The former `prepare`, `run --local`, command/argv and authority-selection
832
920
  flags are rejected. `os routine deploy <canonical-id>` explicitly delivers the
833
921
  complete definition, including its response handlers; `--yes` provides automation
834
922
  consent. `os routine run <canonical-id> --remote` runs the current successful
835
923
  deployment with the same input/defaults, ordered steps and result validation.
836
924
  Remote handles expose `current()`, `watch()` and `step(id)`; `runs.list()` and
837
- `runs.get(id)` read retained bounded history. Remote summaries omit publication.
838
- Checkout-backed app children retain immediate publication.
839
-
840
- Retain evidence with `inbox.files.add({ path: 'evidence.txt' })`. Paths stay within
841
- the invocation's working root and uploaded bytes are verified against the retained
842
- file. Use `data.repository(name)` for Record reads and `changes()` for guarded
843
- creates, updates and deletions. Local Runs read their own successive proposals
844
- and leave ordinary uncommitted files for review. `os routine publish` sends the
845
- retained logical changes explicitly. Inbox publication has no Record methods;
925
+ `runs.get(id)` read retained bounded history. Remote summaries omit local activity.
926
+ App-started local routines use the same current-checkout and live-effects owner.
927
+
928
+ Retain evidence with shared `files.create({ file: bytes, name: 'evidence.txt' })`
929
+ and put the returned File into `fileBlock({ label, files })`. Inbox has no byte
930
+ intake API. Local publication uploads that same File before exposing the item. Use `data.repository(name)` for Record reads and `changes()` for guarded
931
+ creates, updates and deletions. Local Runs read current checkout files and leave
932
+ completed Data writes as ordinary uncommitted files for review, even after later
933
+ failure. Inbox publication has no Record methods;
846
934
  managed Response handlers retain their separately injected repository capability.
847
935
 
848
936
  Hosts publishing as a member supply the existing `request` option:
849
937
 
850
938
  ```ts
851
- const inbox = routinePublicationClient({ workspace, workingRoot, request });
939
+ const inbox = routinePublicationClient({ workspace, request });
852
940
  ```
853
941
 
854
- Here `request` is the host's authorized member transport. A member can publish an
942
+ Here `request` is the host's authorized member transport and takes precedence
943
+ over ambient Run routing. The CLI uses its active workspace and shared Files to
944
+ upload referenced local Files before sending publication. A member can publish an
855
945
  initial handlerless Ask or Digest where allowed; a member login supplies no
856
946
  routine repository-write authority. Login, session storage and refresh belong
857
947
  to `@oddington/cli`.