@oddington/sdk 0.1.38 → 0.1.40

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,6 +4,12 @@ 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
15
  `/api/inbox/resolve`. Request-key recovery and captured
@@ -88,7 +94,7 @@ this metadata when absent. Setup and safe inspection are documented in the
88
94
 
89
95
  With `access: { ai: true }`, `os.ai.generateText` returns non-empty `{ text }`.
90
96
  `os.ai.generateObject` returns `{ object }` validated against the requested
91
- `schema`, with its inferred type. An invalid answer throws `RoutineAIError`
97
+ `schema`, with its inferred type. An invalid answer throws `AIError`
92
98
  with code `invalid_output`, a clear bounded `detail`, and the original final
93
99
  answer as optional `text` for routine-owned recovery. The text is bounded at
94
100
  256 KiB and excluded from the error message and ordinary JSON serialization.
@@ -96,6 +102,29 @@ The routine can inspect it explicitly, apply its own recovery rules, report
96
102
  an item failure and continue; it must not cast a rejected answer to the promised
97
103
  type. OS does not coerce, drop fields, repair or retry automatically.
98
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
+
99
128
  An AI call failure can be caught and handled without automatically failing the
100
129
  Run. Uncaught routine or step exceptions still fail execution, and outstanding
101
130
  AI calls drain before completion. The host retains authority, isolation,
@@ -107,11 +136,13 @@ finite numbers and safe integers with optional inclusive bounds, and Files.
107
136
  Omitting `maxItems` imposes no item-count ceiling; the complete serialized input
108
137
  still shares the Run's byte bound, including every File descriptor and other field.
109
138
  Definitions allow at most 64 steps, 64 total object properties across their schemas,
110
- and eight schema levels. Input, each step input, each summary, each detail value,
111
- 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.
112
143
  Across 64 steps, retained summary plus detail is at most 32 MiB, plus 512 KiB for
113
- Run input and result; step inputs are transient and individually bounded. Schema
114
- 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;
115
146
  access dimensions and response handlers each have at most 64 entries. Functions,
116
147
  accessors, sparse arrays, cycles and non-JSON values reject. The canonical routine
117
148
  ID is a bounded lowercase alphanumeric/hyphen name, independent of its directory.
@@ -345,13 +376,18 @@ both shipped SDK inputs and every emitted helper, rejects unexpected inputs and
345
376
  imports, and retains the full credential/server/customer leak scan. Node retains
346
377
  the complete package-root exports; provider classes are never public.
347
378
 
348
- Current auth measurement is **171,346 minified bytes**, including **6,322 fixture
349
- bytes** and **165,024 SDK bytes**, with **45,466 total gzip bytes**. Current
350
- Data/Files fixture measures **47,254 total / 5,893 fixture / 41,361 SDK
351
- bytes**, with **14,906 total gzip bytes**. The shipped runtime now also carries
352
- the local Questions transport and transparent Local Host renewal.
353
- Both fit their explicitly reviewed ceilings: **42,000 SDK bytes** for the pinned
354
- 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.
355
391
  Those ceilings are fixed review decisions, not values regenerated from later
356
392
  measurements. The installed auth realm/host-handoff proof passes alongside the
357
393
  Node exports, declarations, tarball leak scan and browser compatibility audits.
@@ -365,17 +401,18 @@ bytes; generated helpers count toward the SDK. Gzip is diagnostic only.
365
401
 
366
402
  | Same named-import fixture | Legacy diagnostic | Current strict proof |
367
403
  | --- | ---: | ---: |
368
- | Total minified JavaScript bytes | 488,539 | 47,254 |
369
- | Fixture-attributed bytes | 5,954 | 5,893 |
370
- | SDK bytes | 482,585 | 41,361 |
371
- | 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 |
372
408
 
373
409
  The legacy diagnostic externalized three unresolved Node built-ins and failed
374
410
  browser compatibility. The current strict proof has no imports, shims or warnings
375
- and reduces SDK bytes by 91.4%. Its literal ceiling is **42,000 SDK bytes**:
376
- 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
377
413
  the Local Host bootstrap, local Questions transport, transparent renewal and
378
- 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
379
416
  hash and bundler version; changing either requires a reviewed measurement
380
417
  comparison. Future ceiling increases require explicit review of added browser
381
418
  capability and must never be automatically regenerated from an oversized output.
@@ -424,6 +461,7 @@ Blob and byte-array declarations must match their intrinsic size. Originals are
424
461
  bounded to 5 GiB per original and 16 MiB per enrichment. Large originals stream
425
462
  through bounded multipart transfers; local writes and reads use bounded disk
426
463
  buffers. Operations share four concurrent slots and idle/request deadlines;
464
+ later calls wait in invocation order without reading caller content, and
427
465
  transport retries preserve the same bytes and request key.
428
466
  Omitting a key generates one for that invocation. Supply a stable key to recover
429
467
  across processes or fresh routine runs. Caller inputs remain owned by the caller.
@@ -473,7 +511,7 @@ const changes = data.repository('general').changes();
473
511
  changes.create({ ...statement, added: '2026-10-20' });
474
512
  changes.create({ $ref: 'transactions/one',
475
513
  sources: [source.csv.row(statement.$ref, parsedTransaction.recordPosition)] });
476
- await changes.publish();
514
+ await changes.apply();
477
515
  ```
478
516
 
479
517
  The caller's CSV parser supplies `recordPosition` from the retained statement:
@@ -490,7 +528,12 @@ guarded Data update. Known objects match by OS File ID, Gmail connection/message
490
528
  attachment, or Dropbox connection/File ID; newer Locations replace old path,
491
529
  revision and URL facts. Distinct objects and custom provider values remain.
492
530
  Helpers need no initialization and perform no I/O, choose no ref, and add no
493
- 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.
494
537
 
495
538
  Microsoft helpers are also exported at the package root, with the corresponding
496
539
  `OutlookSourceLocation` and `SharePointSourceLocation` types:
@@ -508,7 +551,7 @@ const itemLocation = source.location.sharepoint({
508
551
 
509
552
  Construct these only after successful provider reads establish the coordinates
510
553
  for the retained bytes, and save them alongside `source.location.os(file)` using
511
- ordinary Data publication. Request `Prefer: IdType="ImmutableId"` consistently
554
+ an ordinary Data apply. Request `Prefer: IdType="ImmutableId"` consistently
512
555
  when obtaining Outlook message and attachment IDs. These IDs are case-sensitive
513
556
  and survive folder moves within the same mailbox, but not archive-mailbox moves
514
557
  or export/reimport. Subjects, filenames and `internetMessageId` are not substitutes.
@@ -613,7 +656,9 @@ use `select` for a manual `after` cursor or explicit historical selection.
613
656
  `all` consumes the same iterator and returns only after successful exhaustion.
614
657
  Its required positive safe-integer `maxRecords` caps top-level Record occurrences
615
658
  across all collections; expanded links do not count toward that ceiling. Overflow
616
- 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.
617
662
  Invalid helper arguments use `invalid_query`; malformed pages, cursor cycles,
618
663
  snapshot changes or inconsistent requested counts use nonretryable `unavailable`.
619
664
  Selection and authority failures propagate unchanged, with no retry or restart.
@@ -730,38 +775,106 @@ changes.create({
730
775
  changes.update(first.invoices[0], { amount: 30 });
731
776
  changes.delete(first.invoices[1]);
732
777
  await changes.validate();
733
- const publication = await changes.publish();
778
+ const applied = await changes.apply();
734
779
  ```
735
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
+
736
809
  Creation captures a complete Record and stable `$ref`, without I/O or a caller
737
810
  path. OS resolves exactly one collection and its schema-declared filing path.
738
811
  Optional `$filing` supplies only unresolved named safe components and is never
739
812
  stored or returned as Record data; missing components reject by name. Updates
740
813
  capture only `$ref`, `$version` and explicit fields. Deletion captures only the
741
- selected `$ref` and `$version`. Links within the deletion set are allowed;
742
- schema-declared links from any committed Record outside it refuse the whole group,
743
- even if a proposed update removes that link. Validation checks the complete
744
- candidate graph without effects; publication repeats authoritative validation
745
- 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.
746
819
  Success returns `{ version, created: [{ $ref, $version }], updated: [{ $ref, $version }], deleted: [{ $ref, $version }] }`
747
820
  only after its exact index is ready. Later current reads discover that version.
748
821
  Deleted identities keep their captured versions; deletion creates no new Record
749
822
  version or tombstone. A Source deletion removes only that guarded Record, retaining
750
823
  its File, enrichments and existing durable uses.
751
- During a local Run, `changes.publish()` validates the complete set once before
752
- writing individual complete files atomically. Completed paths remain after a later
753
- filesystem failure. `validate()` grants no later permission to write; subsequent
754
- operations read the current checkout. No later Run publication is required.
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.
755
830
 
756
831
  `invalid_change` and known-unapplied `conflict` reject the batch.
757
- `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`;
758
870
  retry the same change set to reconcile its retained identity. A transport failure
759
- also keeps that identity. Concurrent publication calls coalesce, confirmed calls
871
+ also keeps that identity. Concurrent `apply()` calls coalesce, confirmed calls
760
872
  replay the result, and changed intent requires a new change set. Updates are
761
- bounded with creates and deletions to 10,000 total operations and the 256 KiB Data
762
- request limit, with one operation per ref. Search and history are outside this
763
- Changes slice. Source Records use ordinary schema rules; Data also validates OS
764
- 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.
765
878
 
766
879
  Bind a declared Airtable connection with `connections.airtable('project-management')`
767
880
  from the package root. Import and binding perform no network or credential work.