@justai/cuts 0.38.0 → 0.39.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "A persona's named parts — the page shell that holds them, and the cuts themselves.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -116,8 +116,21 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
116
116
  import atrium from "@justai/atrium";
117
117
  import { PRESENTATION_PARAM, PRESENTATION_EMBEDDED } from "@justai/core";
118
118
 
119
+ // queries: file id or path · which one should a persona remember · does renaming break a
120
+ // reference · the durable handle for a file
121
+ //
122
+ // A **path** is where a file is standing right now, and it is how a PERSON says where something
123
+ // is. Rename it and every path to it is a dangling string that still looks perfectly valid. An
124
+ // **id** is the file: the kernel has stamped a `crypto.randomUUID()` on every node since the
125
+ // first write, and `mv` never touches it.
126
+ //
127
+ // So a caller reacting to something a person just did may use a path. A caller REMEMBERING a
128
+ // file across time — a project pointing at its logo, a document being saved back — must use the
129
+ // id, and both are handed over here so that choice is available rather than argued for.
119
130
  interface Picked {
120
131
  path: string;
132
+ /** WHAT IT IS, not where it is standing. Survives a rename and a move — remember this one. */
133
+ id: string;
121
134
  name: string;
122
135
  mime: string;
123
136
  bytes: ArrayBuffer;
@@ -131,8 +144,19 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
131
144
  * What a persona hands over to be kept. `name` is a PROPOSAL — the person may rewrite it and
132
145
  * will choose the folder — so give it the best default you have, extension included.
133
146
  */
134
- interface SaveRequest { name: string; bytes: ArrayBuffer | Uint8Array; mime?: string; title?: string; timeout?: number }
135
- interface Saved { path: string; name: string }
147
+ interface SaveRequest {
148
+ /** A PROPOSAL. The person may rewrite it — the extension is shown beside the field, not in it. */
149
+ name?: string;
150
+ /** The persona's format. Kept out of the name box, the way a desktop save dialog keeps it out. */
151
+ ext?: string;
152
+ /** Save BACK to a file already saved once. NO QUESTION IS ASKED: it was answered the first time. */
153
+ id?: string;
154
+ bytes: ArrayBuffer | Uint8Array;
155
+ mime?: string;
156
+ title?: string;
157
+ timeout?: number;
158
+ }
159
+ interface Saved { path: string; id: string; name: string }
136
160
 
137
161
  for (const root of document.querySelectorAll<HTMLElement>("[data-picker]")) {
138
162
  const id = root.dataset.picker!;
@@ -180,20 +204,26 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
180
204
  // for a file and for handing one over — the lazy frame, the presentation declaration, the
181
205
  // heading, the two ways a sheet can end. Extracted so the second question inherits the first
182
206
  // one's scars instead of being written again and re-earning them.
207
+ // Set the frame's src on first use. THREE callers need it now, and the presentation declaration
208
+ // must be identical in all three — a fourth that copied it slightly wrong is how a page ends up
209
+ // announcing itself as standalone on the one path that forgot to say otherwise.
210
+ function ensureFrame() {
211
+ if (frame.getAttribute("src")) return;
212
+ // DECLARE THE PRESENTATION. A page opened by a person and a page opened by another persona
213
+ // are two different situations, and the same controls do not belong in both — standalone
214
+ // there is nothing to cancel. The frame is the slower truth (a handshake takes a beat), so
215
+ // the declaration rides in the URL and is available in the first paint. It is presentation
216
+ // only: nothing that must not be forged is decided by it.
217
+ const src = root.dataset.src!;
218
+ frame.setAttribute("src", src + (src.includes("?") ? "&" : "?") + PRESENTATION_PARAM + "=" + PRESENTATION_EMBEDDED);
219
+ }
220
+
183
221
  async function ask<T>(
184
222
  req: { title?: string },
185
223
  invoke: (bus: any) => Promise<T>,
186
224
  ): Promise<T | null> {
187
225
  const bus = await router();
188
- if (!frame.getAttribute("src")) {
189
- // DECLARE THE PRESENTATION. A page opened by a person and a page opened by another persona
190
- // are two different situations, and the same controls do not belong in both — standalone
191
- // there is nothing to cancel. The frame is the slower truth (a handshake takes a beat), so
192
- // the declaration rides in the URL and is available in the first paint. It is presentation
193
- // only: nothing that must not be forged is decided by it.
194
- const src = root.dataset.src!;
195
- frame.setAttribute("src", src + (src.includes("?") ? "&" : "?") + PRESENTATION_PARAM + "=" + PRESENTATION_EMBEDDED);
196
- }
226
+ ensureFrame();
197
227
  // ONE HEADING, and it is the sheet's. A per-call title retitles the sheet rather than being
198
228
  // forwarded into the frame: the kernel renders a heading of its own for callers that have no
199
229
  // sheet around them, and sending it one here printed "Upload logo" twice, one under the other.
@@ -275,13 +305,24 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
275
305
  const b: any = req.bytes;
276
306
  const bytes: ArrayBuffer =
277
307
  b instanceof ArrayBuffer ? b : b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
308
+ const args = { name: req.name, ext: req.ext, id: req.id, bytes, mime: req.mime };
309
+ // SAVING BACK OPENS NO SHEET, and that is the point rather than an optimisation. The person
310
+ // named this file the first time; asking again on every save is not care, it is a persona
311
+ // that cannot remember what it is working on. So this path never touches `ask`.
312
+ //
313
+ // BY ID AND NOT BY PATH: between opening a document and saving it, the person may have
314
+ // renamed or moved it in the very list they are looking at. A path captured at open time
315
+ // would then either miss — a second copy appearing under the old name — or land on whatever
316
+ // has since taken that name, which is worse and completely silent.
317
+ if (req.id) {
318
+ const bus = await router();
319
+ ensureFrame();
320
+ return arrived!
321
+ .then(() => bus.proxy({ service: "files", method: "save", args, options: { timeout: req.timeout ?? 60000 } }))
322
+ .catch(() => null) as Promise<Saved | null>;
323
+ }
278
324
  return ask(req, (bus) =>
279
- bus.proxy({
280
- service: "files",
281
- method: "save",
282
- args: { name: req.name, bytes, mime: req.mime },
283
- options: { timeout: req.timeout ?? 300000 },
284
- }));
325
+ bus.proxy({ service: "files", method: "save", args, options: { timeout: req.timeout ?? 300000 } }));
285
326
  }
286
327
 
287
328
  // queries: read a file the persona already has a path to · FilePicker read · restore an asset a
@@ -302,16 +343,16 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
302
343
  // It opens no sheet, because there is no question. The policy still answers "who is asking" —
303
344
  // same-origin only, exactly as every other capability — so this widens no door: a caller that
304
345
  // could not read here a moment ago still cannot.
305
- async function read(path: string): Promise<{ bytes: ArrayBuffer; blob(): Blob; dataUrl(): Promise<string> } | null> {
346
+ async function read(ref: string | { path?: string; id?: string }): Promise<{ bytes: ArrayBuffer; blob(): Blob; dataUrl(): Promise<string> } | null> {
347
+ // A bare string is a PATH, because that is what a string reads as. An id must be said out
348
+ // loud — `read({ id })` — so a caller holding one kind can never pass it as the other.
349
+ const where = typeof ref === "string" ? { path: ref } : ref;
306
350
  const bus = await router();
307
- if (!frame.getAttribute("src")) {
308
- const src = root.dataset.src!;
309
- frame.setAttribute("src", src + (src.includes("?") ? "&" : "?") + PRESENTATION_PARAM + "=" + PRESENTATION_EMBEDDED);
310
- }
351
+ ensureFrame();
311
352
  const [bytes, stat] = await arrived!
312
353
  .then(() => Promise.all([
313
- bus.proxy({ service: "files", method: "read", args: { path }, options: { timeout: 30000 } }),
314
- bus.proxy({ service: "files", method: "stat", args: { path }, options: { timeout: 30000 } }),
354
+ bus.proxy({ service: "files", method: "read", args: where, options: { timeout: 30000 } }),
355
+ bus.proxy({ service: "files", method: "stat", args: where, options: { timeout: 30000 } }),
315
356
  ]))
316
357
  .catch(() => [null, null]);
317
358
  // A file the person deleted since is simply gone. `null` rather than a throw: a caller