@oddington/sdk 0.1.3 → 0.1.5

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  The importable client for Oddington apps, routines and response handlers.
4
4
  Node operations require Node.js 22.12.0 or later. The root import and hosted
5
- Data API work in browsers. Installing the package grants no authority.
5
+ Data and Files APIs work in browsers. Installing the package grants no authority.
6
6
 
7
7
  ```sh
8
8
  npm install @oddington/sdk
@@ -29,6 +29,84 @@ session subscription. Workspace existence and member access are checked by the
29
29
  OS service. App modules use capability namespaces; routines and handlers receive
30
30
  runtime context without calling `os.init`.
31
31
 
32
+ Create and recover a private immutable original through the same host context:
33
+
34
+ ```ts
35
+ import { files } from '@oddington/sdk';
36
+
37
+ const report = await files.create({
38
+ file: selectedFile, // Web File, path-backed Blob, Uint8Array or byte stream
39
+ name: 'September statement.csv',
40
+ path: 'HSC/2026/09',
41
+ }, { requestKey: 'statement/2026/09' });
42
+ const descriptor = await files.get(report.id);
43
+ const response = await files.read(descriptor.id);
44
+ await response.text(); // Also supports blob(), arrayBuffer() and body streaming.
45
+ const reading = await files.enrich({ file: report.id, name: 'read.md', content: readingMarkdown });
46
+ await files.enrich({ file: report.id, name: 'extracted.json', content: JSON.stringify(extractedValue) });
47
+ const current = await files.get(report.id); // Discover retained enrichment descriptors.
48
+ const context = await files.read(current.id, ['read.md', 'extracted.json']);
49
+ await context['read.md'].text();
50
+ await context['extracted.json'].json(); // Each body is independently consumed.
51
+ await files.enrich({ file: report.id, name: 'read.md', content: revisedMarkdown, replace: reading.sha256 });
52
+ await files.upload(report.id); // Move a local File and every retained enrichment to remote storage.
53
+ ```
54
+
55
+ Hosted Files retains remote originals and caller-produced named enrichments;
56
+ `storage: 'local'` requires a managed CLI parent. Configure that parent with
57
+ `ODDINGTON_FILES_ROOT` pointing to an empty or already OS-managed dedicated
58
+ directory outside tracked Git content. `ODDINGTON_FILES_DEFAULT=local|remote`
59
+ selects its default; without it, remote remains the default. Per-File `storage`
60
+ overrides it. The root stays private to the parent. Local creation without a
61
+ configured root fails with `unsupported_context`. Files have one durable home;
62
+ `both` is invalid. Local retention establishes no hosted registration.
63
+ Fresh runs recover the same creator's File by its request key, and verify
64
+ retained bytes before returning a descriptor or content. A dead parent fails
65
+ closed. `os routine prepare` seals checked local File references and defers
66
+ new remote Files, their enrichments and explicit uploads until `os routine publish`; legacy Inbox
67
+ file preparation remains additive. Existing hosted Files are read-only during
68
+ preparation.
69
+ `upload(id)` preserves the File descriptor, creator access and existing Source
70
+ references. Local bytes remain authoritative until all remote objects verify;
71
+ retrying a completed upload returns the same File. Later reads and enrichments
72
+ follow remote storage; any remaining managed local bytes are disposable cache.
73
+ An unreferenced local File is disclosed remotely only when explicitly uploaded
74
+ or successfully published as a Source. An authorized hosted caller receives
75
+ non-retryable `local_only` (409) until upload succeeds; unrelated callers receive
76
+ `access_denied`.
77
+ Unnamed inputs require a safe `name`; streams also require declared `size`.
78
+ Blob and byte-array declarations must match their intrinsic size. Originals are
79
+ bounded to 16 MiB per object, metadata to 4 KiB, and descriptor/error bodies to 16 KiB.
80
+ Operations share four concurrent slots and a 30-second deadline;
81
+ transport retries at most once with the same prepared bytes and request key.
82
+ Omitting a key generates one for that invocation. Supply a stable key to recover
83
+ across processes or fresh routine runs. Caller inputs remain owned by the caller.
84
+ Each request resolves current authority; hosted routine Files calls use the
85
+ selected workspace's HTTPS OS host, and managed calls use the authenticated
86
+ private parent transport. `FileOperationError` exposes a bounded code,
87
+ status and retryability without server details. Reads return the successful Web
88
+ Response without consuming its body. Local transfer is bounded and buffered
89
+ before constructing that Response. Creation grants private creator access;
90
+ workspace membership alone never grants another creator's File.
91
+
92
+ `enrich` accepts a string encoded as UTF-8 or exact `Uint8Array` bytes, a safe
93
+ flat filename, and an optional MIME hint. OS verifies text/Markdown/JSON or
94
+ reduces the content to generic binary metadata; it performs no extraction or
95
+ model work. The first enrichment atomically promotes the storage bundle while
96
+ preserving original bytes, File identity, name, path and access. An identical
97
+ retry returns the retained descriptor. Different content conflicts unless
98
+ `replace` matches the current enrichment hash; repeating a completed replacement
99
+ still succeeds with its preceding hash. There is no enrichment request key.
100
+
101
+ `read(id)` returns the original; `read(id, name)` returns one enrichment.
102
+ `read(id, names)` validates at most 16 distinct safe names, deduplicates repeats,
103
+ and returns an object of unconsumed Responses after all reads succeed. An empty
104
+ list returns an empty object in a valid runtime context. Missing authorized names
105
+ return `invalid_input`; a failed keyed request cancels obtained bodies. Each name
106
+ is read independently with fresh authority; there is no cross-name revision snapshot.
107
+ Names cannot contain separators or use the reserved `original` basename with
108
+ or without an extension. Enrichments share their parent File's access and retention.
109
+
32
110
  Read a named repository through the authenticated host context:
33
111
 
34
112
  ```ts
@@ -51,7 +129,7 @@ Binding is inert. Each operation resolves current workspace, endpoint and
51
129
  short-lived authority from the trusted app host or managed routine context.
52
130
  Customer calls cannot configure those values. Missing or expired context
53
131
  rejects; a named handle does not expand repository access. During explicit
54
- `os routine run --local`, the same calls use the matched checkout's committed
132
+ `os routine prepare`, the same calls use the matched checkout's committed
55
133
  version through a private parent transport. Dirty files are ignored. An OS
56
134
  cache outside the checkout reuses derived data; reads require no remote access
57
135
  or preparation storage. Later accepted record effects advance the run's view
@@ -62,13 +140,78 @@ Each graph selection sends one request and returns only selected canonical
62
140
  values, expanded forward links and exact opaque public versions. Cursors
63
141
  continue the exact retained artifact and collection selection, even after
64
142
  replacement; repeat its options and links while omitting unrelated collections.
65
- No automatic retry, separate version fetch, search, history or changes operation
66
- is included. Collection schemas omit Markdown storage hints.
143
+ No automatic retry, separate version fetch, search or history operation is
144
+ included. Collection schemas omit Markdown storage hints.
145
+
146
+ `where` combines field conditions with AND. Alongside exact scalar/null equality
147
+ and `$in`, declared one-valued `integer`, `number` and `date` fields accept
148
+ `$gt`, `$gte`, `$lt` and `$lte`:
149
+
150
+ ```ts
151
+ await general.records.select({ invoices: { $: { where: {
152
+ amount: { $gte: 10, $lte: 20 },
153
+ date: { $gte: '2026-10-01', $lt: '2026-11-01' },
154
+ month: { $gte: '2026-10', $lte: '2026-10' },
155
+ }, count: true } } });
156
+ ```
157
+
158
+ Supply one lower bound, one upper bound, or one of each. Bounds cannot combine
159
+ `$gt` with `$gte`, `$lt` with `$lte`, or any bound with `$in`. Operands must be
160
+ valid for the declared field, including numeric precision and real day/month
161
+ dates; values are never coerced. Missing and null values never match bounds.
162
+ A valid interval without matches returns an empty array and requested zero
163
+ count. V0 types check expression structure; runtime schema validation owns field
164
+ eligibility. Repeat the same bounds when continuing a cursor.
165
+
67
166
  `DataOperationError.code` distinguishes `context_unavailable`, `unauthenticated`,
68
167
  `access_denied`, `invalid_query`, `request_too_large`, `capacity` and `unavailable`.
69
- Only `capacity` and `unavailable` are retryable; an error never includes private
168
+ For reads, only `capacity` and `unavailable` are retryable; an error never includes private
70
169
  server details. Server boundaries own query bounds and authorization.
71
170
 
171
+ Schema-driven creates, guarded updates and guarded deletions use the same handle
172
+ in immediate and prepared routines:
173
+
174
+ ```ts
175
+ const changes = general.changes();
176
+ changes.create({
177
+ $ref: 'invoices/INV-124', $filing: { year: '2026' },
178
+ type: 'invoice', number: 'INV-124', status: 'draft', amount: 1200,
179
+ });
180
+ changes.update(first.invoices[0], { amount: 30 });
181
+ changes.delete(first.invoices[1]);
182
+ await changes.validate();
183
+ const publication = await changes.publish();
184
+ ```
185
+
186
+ Creation captures a complete Record and stable `$ref`, without I/O or a caller
187
+ path. OS resolves exactly one collection and its schema-declared filing path.
188
+ Optional `$filing` supplies only unresolved named safe components and is never
189
+ stored or returned as Record data; missing components reject by name. Updates
190
+ capture only `$ref`, `$version` and explicit fields. Deletion captures only the
191
+ selected `$ref` and `$version`. Links within the deletion set are allowed;
192
+ schema-declared links from any committed Record outside it refuse the whole group,
193
+ even if a proposed update removes that link. Validation checks the complete
194
+ candidate graph without effects; publication repeats authoritative validation
195
+ and applies every create/update/delete in one commit.
196
+ Success returns `{ version, created: [{ $ref, $version }], updated: [{ $ref, $version }], deleted: [{ $ref, $version }] }`
197
+ only after its exact index is ready. Later current reads discover that version.
198
+ Deleted identities keep their captured versions; deletion creates no new Record
199
+ version or tombstone. A Source deletion removes only that guarded Record, retaining
200
+ its File, enrichments and existing durable uses.
201
+ During `os routine prepare`, publication retains the exact proposal and
202
+ advances only run-local reads. `os routine publish` later applies and indexes
203
+ that immutable proposal; unrelated staged and unstaged work is preserved.
204
+
205
+ `invalid_change` and known-unapplied `conflict` reject the batch.
206
+ `publication_pending` is retryable and may report `uncertain` or `applied`;
207
+ retry the same change set to reconcile its retained identity. A transport failure
208
+ also keeps that identity. Concurrent publication calls coalesce, confirmed calls
209
+ replay the result, and changed intent requires a new change set. Updates are
210
+ bounded with creates and deletions to 10,000 total operations and the 256 KiB Data
211
+ request limit, with one operation per ref. Search and history are outside this
212
+ Changes slice. Source Records use ordinary schema rules; Data also validates OS
213
+ File identity/access and registers durable uses from its exact ready artifact.
214
+
72
215
  Bind a declared Airtable connection with `connections.airtable('project-management')`
73
216
  from the package root. Import and binding perform no network or credential work.
74
217
  The client exposes `tables.list()` and `records.list`, `get`, `create`, `update`
@@ -105,8 +248,14 @@ The defaults read `ODDINGTON_ROUTINE_WORKSPACE`, `ODDINGTON_ROUTINE_RUN` and
105
248
  `ODDINGTON_INBOX_WORKING_ROOT` (or the current directory). Callers may supply the
106
249
  existing `workspace`, `capability` and `workingRoot` options. Runtime context
107
250
  provides authority; callers cannot select repository remotes or expand grants.
108
- The same calls prepare locally when launched with `os routine run --local`, and
109
- the CLI owns explicit publication and recovery of that prepared run.
251
+ `os routine run` publishes as capabilities are called. The same calls prepare
252
+ work when launched with `os routine prepare`, and `os routine publish` owns
253
+ explicit publication and recovery of that prepared run. Preparation may contact
254
+ OS for authentication and reads, and declared connections may contact their
255
+ providers; it does not promise offline execution. Execution location and File
256
+ storage are independent of the publication lifecycle. `os routine run --local`
257
+ remains a deprecated compatibility alias for `os routine prepare` and prints
258
+ the replacement command.
110
259
 
111
260
  Retain evidence with `inbox.files.add({ path: 'evidence.txt' })`. Paths stay within
112
261
  the invocation's working root and uploaded bytes are verified against the retained