orizu 0.5.39 → 0.5.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.
Files changed (85) hide show
  1. package/README.md +77 -0
  2. package/dist/dataset-commands.d.ts +3 -1
  3. package/dist/dataset-commands.d.ts.map +1 -1
  4. package/dist/dataset-commands.js +101 -62
  5. package/dist/dataset-delete-workflow.d.ts +4 -0
  6. package/dist/dataset-delete-workflow.d.ts.map +1 -0
  7. package/dist/dataset-delete-workflow.js +75 -0
  8. package/dist/dataset-lock-workflow.d.ts +12 -0
  9. package/dist/dataset-lock-workflow.d.ts.map +1 -0
  10. package/dist/dataset-lock-workflow.js +76 -0
  11. package/dist/dataset-readme-workflow.d.ts +13 -0
  12. package/dist/dataset-readme-workflow.d.ts.map +1 -0
  13. package/dist/dataset-readme-workflow.js +82 -0
  14. package/dist/dataset-row-workflow.d.ts +14 -0
  15. package/dist/dataset-row-workflow.d.ts.map +1 -0
  16. package/dist/dataset-row-workflow.js +202 -0
  17. package/dist/dataset-split-reads.d.ts +28 -0
  18. package/dist/dataset-split-reads.d.ts.map +1 -0
  19. package/dist/dataset-split-reads.js +47 -0
  20. package/dist/help.d.ts.map +1 -1
  21. package/dist/help.js +29 -3
  22. package/dist/hosted-boot.d.ts +1 -0
  23. package/dist/hosted-boot.d.ts.map +1 -1
  24. package/dist/hosted-boot.js +17 -0
  25. package/dist/hosted-harness-opencode.d.ts +4 -5
  26. package/dist/hosted-harness-opencode.d.ts.map +1 -1
  27. package/dist/hosted-harness-opencode.js +66 -52
  28. package/dist/hosted-loop-lifecycle.d.ts +4 -10
  29. package/dist/hosted-loop-lifecycle.d.ts.map +1 -1
  30. package/dist/hosted-loop-lifecycle.js +4 -10
  31. package/dist/hosted-loop.d.ts +2 -0
  32. package/dist/hosted-loop.d.ts.map +1 -1
  33. package/dist/hosted-loop.js +5 -4
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +9 -1
  36. package/dist/instruction-delivery.d.ts +14 -0
  37. package/dist/instruction-delivery.d.ts.map +1 -0
  38. package/dist/instruction-delivery.js +103 -0
  39. package/dist/instruction-set-sync/index.d.ts +17 -0
  40. package/dist/instruction-set-sync/index.d.ts.map +1 -1
  41. package/dist/instruction-set-sync/index.js +44 -17
  42. package/dist/instruction-set-update/index.d.ts +2 -2
  43. package/dist/instruction-set-update/index.d.ts.map +1 -1
  44. package/dist/instruction-set-update/index.js +2 -2
  45. package/dist/instruction-sets-cli.d.ts.map +1 -1
  46. package/dist/instruction-sets-cli.js +19 -27
  47. package/dist/json-response.d.ts +1 -0
  48. package/dist/json-response.d.ts.map +1 -1
  49. package/dist/json-response.js +7 -0
  50. package/dist/preview-runtime.d.ts +2 -2
  51. package/dist/preview-runtime.d.ts.map +1 -1
  52. package/dist/preview-runtime.js +37 -7
  53. package/dist/task-export-workflow.d.ts +4 -0
  54. package/dist/task-export-workflow.d.ts.map +1 -0
  55. package/dist/task-export-workflow.js +46 -0
  56. package/dist/workbench-cli.js +1 -1
  57. package/package.json +1 -1
  58. package/src/dataset-commands.ts +92 -92
  59. package/src/dataset-delete-workflow.ts +72 -0
  60. package/src/dataset-lock-workflow.ts +74 -0
  61. package/src/dataset-readme-workflow.ts +80 -0
  62. package/src/dataset-row-workflow.ts +248 -0
  63. package/src/dataset-split-reads.ts +43 -0
  64. package/src/help.ts +29 -3
  65. package/src/hosted-boot.ts +16 -1
  66. package/src/hosted-harness-opencode.ts +67 -65
  67. package/src/hosted-loop-lifecycle.ts +4 -10
  68. package/src/hosted-loop.ts +6 -4
  69. package/src/hosted-model-cache.d.mts +2 -0
  70. package/src/hosted-model-cache.mjs +53 -0
  71. package/src/index.ts +10 -0
  72. package/src/instruction-delivery.ts +123 -0
  73. package/src/instruction-set-sync/index.ts +61 -14
  74. package/src/instruction-set-update/index.ts +4 -3
  75. package/src/instruction-sets-cli.ts +19 -27
  76. package/src/json-response.ts +8 -0
  77. package/src/preview-runtime.ts +40 -10
  78. package/src/task-export-workflow.ts +56 -0
  79. package/src/workbench-cli.ts +1 -1
  80. package/vendor/skills/orizu/references/cli-reference.md +34 -0
  81. package/vendor/skills/orizu/references/dataset-design.md +1 -1
  82. package/dist/dataset-edit-rows.d.ts +0 -10
  83. package/dist/dataset-edit-rows.d.ts.map +0 -1
  84. package/dist/dataset-edit-rows.js +0 -90
  85. package/src/dataset-edit-rows.ts +0 -124
@@ -1,9 +1,9 @@
1
1
  import { authedFetch } from './http.js'
2
2
  import { INSTRUCTION_SET_COMPONENT_KEY } from './instruction-set-lock/index.js'
3
3
  import { loadInstructionSetManifest, MAX_INSTRUCTION_SET_COMPONENT_BYTES, type InstructionSetManifest } from './instruction-set-manifest.js'
4
- import { sanitizeHumanInlineText, sanitizeTerminalText } from './json-response.js'
5
- import { parseSyncTarget, planSyncRequest, syncPayloadToDisk, type SyncPayload, type SyncTarget } from './instruction-set-sync/index.js'
6
- import { applyPrune, applyUpdate, lockedProfileIdentity, makeUpdatePlan, planPrune, PruneKeepUnresolvedError, readUpdateLock } from './instruction-set-update/index.js'
4
+ import { responsePayload, sanitizeHumanInlineText, sanitizeTerminalText } from './json-response.js'
5
+ import { parseSyncTarget, planSyncRequest, type SyncTarget } from './instruction-set-sync/index.js'
6
+ import { applyPrune, planPrune, PruneKeepUnresolvedError } from './instruction-set-update/index.js'
7
7
  import { printVerifyReport, verifyInstructionSetTree } from './instruction-set-verify/index.js'
8
8
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
9
9
  import { join } from 'node:path'
@@ -74,12 +74,8 @@ function lifecycleOptionValues(args: string[], command: 'update' | 'prune', flag
74
74
  return values
75
75
  }
76
76
 
77
- async function responsePayload(response: Response, action: string): Promise<Record<string, unknown>> {
78
- const payload = await response.json().catch(() => ({})) as Record<string, unknown>
79
- if (!response.ok) {
80
- throw new Error(`${action} failed (${response.status}): ${typeof payload.error === 'string' ? payload.error : response.statusText}`)
81
- }
82
- return payload
77
+ function printInterrupted(io: InstructionSetsCommandIo, message: string) {
78
+ io.print(io.json ? JSON.stringify({ error: message }) : message)
83
79
  }
84
80
 
85
81
  const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/u
@@ -340,23 +336,14 @@ export async function instructionSetsCommand(args: string[], io: InstructionSets
340
336
  const project = await io.resolveProjectSlug(projectOption)
341
337
  if (subcommand === 'update') {
342
338
  const output = lifecycleOptionValues(args, 'update', '--out')[0] ?? '.'
343
- const lock = readUpdateLock(output, project)
344
- const payloads: SyncPayload[] = []
345
- for (const [setSlug, set] of Object.entries(lock.instructionSets)) {
346
- const barePath = `/api/cli/instruction-sets/${encodeURIComponent(setSlug)}/sync?project=${encodeURIComponent(project)}`
347
- payloads.push(await responsePayload(await authedFetch(barePath, { method: 'GET' }), 'Instruction sets update') as unknown as SyncPayload)
348
- for (const profileSlugValue of Object.keys(set.profiles)) {
349
- const identity = lockedProfileIdentity(output, setSlug, profileSlugValue)
350
- const query = new URLSearchParams({ project, profile: identity })
351
- const path = `/api/cli/instruction-sets/${encodeURIComponent(setSlug)}/sync?${query.toString()}`
352
- payloads.push(await responsePayload(await authedFetch(path, { method: 'GET' }), 'Instruction sets update') as unknown as SyncPayload)
353
- }
354
- }
355
- const plan = makeUpdatePlan(project, lock, payloads)
356
339
  const isApproved = args.includes('--yes')
357
- const result = isApproved
358
- ? await applyUpdate(output, project, plan, args.includes('--no-sync'))
359
- : { absent: [], warnings: [] }
340
+ const { updateInstructionSets } = await import('./instruction-delivery.js')
341
+ const updated = await updateInstructionSets(output, project, isApproved, args.includes('--no-sync'))
342
+ if ('interrupted' in updated) {
343
+ printInterrupted(io, 'Instruction sets update interrupted; some sets may already be updated. Run orizu instructions update again, then orizu instructions verify.')
344
+ return updated.interrupted
345
+ }
346
+ const { plan, result } = updated
360
347
  if (io.json) io.print(JSON.stringify({ plan: plan.lines, applied: isApproved, absent: result.absent, warnings: result.warnings }))
361
348
  else {
362
349
  for (const line of plan.lines) io.print(line)
@@ -527,11 +514,16 @@ export async function instructionSetsCommand(args: string[], io: InstructionSets
527
514
  if (unknownOption) throw new Error(`instruction_set_sync_option_unknown:${unknownOption}`)
528
515
  const output = requiredOptionValue(args, subcommand, '--out') ?? '.'
529
516
  const plan = planSyncRequest(reference, requiredOptionValue(args, subcommand, '--version'), output, project)
530
- const payload = await responsePayload(await authedFetch(plan.path, { method: 'GET' }), 'Instruction sets sync') as unknown as SyncPayload
531
- const result = syncPayloadToDisk(output, project, plan, payload, {
517
+ const { syncInstructionSet } = await import('./instruction-delivery.js')
518
+ const delivered = await syncInstructionSet(output, project, plan, {
532
519
  forceHelpers: args.includes('--force-helpers'),
533
520
  target: syncTarget!,
534
521
  })
522
+ if ('interrupted' in delivered) {
523
+ printInterrupted(io, 'Instruction set sync interrupted; any unpublished staged version was removed. Run orizu instructions sync again.')
524
+ return delivered.interrupted
525
+ }
526
+ const { payload, result } = delivered
535
527
  if (io.json) io.print(JSON.stringify({ ...payload, warnings: result.warnings }))
536
528
  else {
537
529
  io.print(`Synced ${reference} to ${result.destination}`)
@@ -35,3 +35,11 @@ export async function parseJsonResponse<T>(
35
35
  )
36
36
  }
37
37
  }
38
+
39
+ export async function responsePayload(response: Response, action: string): Promise<Record<string, unknown>> {
40
+ const payload = await response.json().catch(() => ({})) as Record<string, unknown>
41
+ if (!response.ok) {
42
+ throw new Error(`${action} failed (${response.status}): ${typeof payload.error === 'string' ? payload.error : response.statusText}`)
43
+ }
44
+ return payload
45
+ }
@@ -112,8 +112,8 @@ interface PreviewPage {
112
112
  screenshot: (options: { path: string; fullPage: boolean }) => Promise<unknown>
113
113
  waitForTimeout: (timeout: number) => Promise<unknown>
114
114
  once?: (event: string, listener: () => void) => unknown
115
- on?: (event: string, listener: () => void) => unknown
116
- off?: (event: string, listener: () => void) => unknown
115
+ on?: (event: string, listener: (error: Error) => void) => unknown
116
+ off?: (event: string, listener: (error: Error) => void) => unknown
117
117
  }
118
118
 
119
119
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
@@ -256,19 +256,32 @@ export async function runLocalAppPreview(options: AppPreviewOptions): Promise<Ap
256
256
  headless: !options.headed,
257
257
  ...(chromiumExecutablePath ? { executablePath: chromiumExecutablePath } : {}),
258
258
  })
259
- const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
260
-
259
+ let page: PreviewPage | null = null
260
+ let handleRenderError: ((error: Error) => void) | undefined
261
+ let hasFinishedKeepingOpen = false
261
262
  try {
262
- await page.goto(url, { waitUntil: 'networkidle' })
263
- await page.waitForSelector('[data-orizu-preview-ready="true"]', { timeout: 15_000 })
263
+ page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
264
+ const renderError = new Promise<never>((_, reject) => {
265
+ handleRenderError = error => reject(new Error(`App preview render failed: ${error.message}`))
266
+ page?.on?.('pageerror', handleRenderError)
267
+ })
268
+ // Listen before navigation: React can fail before the ready marker mounts.
269
+ await Promise.race([
270
+ page.goto(url, { waitUntil: 'networkidle' }).then(() =>
271
+ page!.waitForSelector('[data-orizu-preview-ready="true"]', { timeout: 15_000 })
272
+ ),
273
+ renderError,
274
+ ])
264
275
  if (options.screenshotPath) {
265
276
  await page.screenshot({ path: options.screenshotPath, fullPage: true })
266
277
  }
267
278
  if (options.keepOpen) {
268
279
  await waitForPreviewClose(page, browser)
280
+ hasFinishedKeepingOpen = true
269
281
  }
270
282
  } finally {
271
- if (!options.keepOpen) {
283
+ if (handleRenderError) page?.off?.('pageerror', handleRenderError)
284
+ if (!hasFinishedKeepingOpen) {
272
285
  await browser.close()
273
286
  }
274
287
  }
@@ -826,7 +839,8 @@ export default TextContent
826
839
  import React from 'react'
827
840
  function cn(...values) { return values.filter(Boolean).join(' ') }
828
841
  export function CodeBlock({ code, children, className, ...props }) {
829
- return <pre {...props} className={cn('overflow-auto rounded-md border bg-muted p-3 font-mono text-xs', className)}><code>{children ?? code}</code></pre>
842
+ const codeText = Array.isArray(code) ? code.map(line => line.content).join('\\n') : code
843
+ return <pre {...props} className={cn('overflow-auto rounded-md border bg-muted p-3 font-mono text-xs', className)}><code>{children ?? codeText}</code></pre>
830
844
  }
831
845
  export default CodeBlock
832
846
  `
@@ -843,8 +857,24 @@ export default Prose
843
857
  const contentRendererSource = `
844
858
  import React from 'react'
845
859
  function cn(...values) { return values.filter(Boolean).join(' ') }
846
- export function ContentRenderer({ content, children, className, ...props }) {
847
- return <div {...props} className={cn('whitespace-pre-wrap text-sm leading-relaxed', className)}>{children ?? content}</div>
860
+ export function ContentRenderer({ contentType, content, maxHeight = '400px', customRenderer, className }) {
861
+ let renderedContent
862
+ switch (contentType) {
863
+ case 'image':
864
+ renderedContent = <div className="flex items-center justify-center p-2"><img src={content} alt="Content" width={800} height={600} className="max-w-full object-contain" style={{ maxHeight, height: 'auto', width: 'auto' }} /></div>
865
+ break
866
+ case 'video':
867
+ renderedContent = <div className="flex h-full justify-center p-2"><video src={content} controls className="max-h-full max-w-full" /></div>
868
+ break
869
+ case 'custom':
870
+ renderedContent = customRenderer ? customRenderer({ content }) : <p className="text-destructive">Custom renderer required for custom content type</p>
871
+ break
872
+ default:
873
+ renderedContent = <p>Invalid content type</p>
874
+ }
875
+ return maxHeight
876
+ ? <div className={cn('overflow-auto', className)} style={{ maxHeight }}>{renderedContent}</div>
877
+ : renderedContent
848
878
  }
849
879
  export default ContentRenderer
850
880
  `
@@ -0,0 +1,56 @@
1
+ import { Effect, Exit, Result } from 'effect'
2
+ import { renameSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { basename, dirname, join } from 'node:path'
4
+
5
+ import { authedFetch } from './http.js'
6
+
7
+ export type TaskExportFormat = 'csv' | 'json' | 'jsonl'
8
+
9
+ // Node's "CODE: description, syscall 'path'" without the syscall and path, so a
10
+ // failed save names the user's target rather than the temp file.
11
+ function saveFailureReason(cause: unknown): string {
12
+ const message = cause instanceof Error ? cause.message : String(cause)
13
+ return message.replace(/, \w+( '.*')?$/, '')
14
+ }
15
+
16
+ /**
17
+ * Writes the export beside `target` and renames it over the target, so a failed
18
+ * or interrupted save never touches the file the user already has. The temp file
19
+ * is removed whenever the save does not finish.
20
+ */
21
+ function saveExportFile(target: string, bytes: Uint8Array) {
22
+ const temp = join(dirname(target), `.${basename(target)}.${process.pid}.tmp`)
23
+ return Effect.acquireUseRelease(
24
+ Effect.succeed(temp),
25
+ path => Effect.try({
26
+ try: () => {
27
+ writeFileSync(path, bytes, { flag: 'wx' })
28
+ renameSync(path, target)
29
+ },
30
+ catch: cause => new Error(`Could not save export to ${target}: ${saveFailureReason(cause)}`, { cause }),
31
+ }),
32
+ (path, exit) => Exit.isSuccess(exit) ? Effect.void : Effect.sync(() => rmSync(path, { force: true }))
33
+ )
34
+ }
35
+
36
+ /**
37
+ * `orizu tasks export`: one GET of the export, never retried, then a safe save.
38
+ * A refused export fails with `Download failed: <body>` before anything is written.
39
+ */
40
+ function downloadTaskExport(taskId: string, format: TaskExportFormat, target: string) {
41
+ return Effect.gen(function* () {
42
+ const response = yield* Effect.promise(() => authedFetch(`/api/cli/tasks/${taskId}/export?format=${format}`))
43
+ if (!response.ok) {
44
+ const body = yield* Effect.promise(() => response.text())
45
+ return yield* Effect.fail(new Error(`Download failed: ${body}`))
46
+ }
47
+ const bytes = new Uint8Array(yield* Effect.promise(() => response.arrayBuffer()))
48
+ yield* saveExportFile(target, bytes)
49
+ })
50
+ }
51
+
52
+ /** Promise runner for the command: rejects with the message the CLI prints. */
53
+ export async function runTaskExportDownload(taskId: string, format: TaskExportFormat, target: string): Promise<void> {
54
+ const result = await Effect.runPromise(Effect.result(downloadTaskExport(taskId, format, target)))
55
+ if (Result.isFailure(result)) throw result.failure
56
+ }
@@ -1176,7 +1176,7 @@ function formatSessionFinishOutcome(result: SessionFinishResult): string {
1176
1176
  // The web page's failed line, with the reason the server mapped (ORI-2264).
1177
1177
  function failedRunLine(run: { status: string; failureReason?: unknown }): string | null {
1178
1178
  return run.status === 'failed' && typeof run.failureReason === 'string' && run.failureReason
1179
- ? `The agent stopped: ${run.failureReason}.`
1179
+ ? run.failureReason === 'The selected model is unavailable in this session.' ? run.failureReason : `The agent stopped: ${run.failureReason}.`
1180
1180
  : null
1181
1181
  }
1182
1182
 
@@ -351,6 +351,40 @@ orizu datasets lock --dataset <datasetId|datasetUrl> --reason "Finalize for labe
351
351
  orizu datasets clone --dataset <datasetId|datasetUrl> --name "Batch 1 Copy"
352
352
  ```
353
353
 
354
+ For validation-first enrichment, start with an explicit immutable version ID
355
+ returned by upload or version creation. Discover the saved names, export the
356
+ assignments, then download only the selected partition:
357
+
358
+ ```bash
359
+ orizu datasets splits list DATASET_VERSION_ID --json
360
+ orizu datasets splits show SPLIT_SET_ID --json > split.json
361
+ orizu datasets download --dataset-version DATASET_VERSION_ID --split-set SPLIT_SET_ID --split validation --format jsonl --out validation.jsonl
362
+ ```
363
+
364
+ The download retains canonical `id` values and existing fields in saved partition
365
+ order; `--format json` is also supported. Custom partition names and empty splits
366
+ are valid. Pinned reads remain unchanged after live row edits and require no
367
+ prompt, runner, scorer or optimization. Unknown/inaccessible or mismatched
368
+ selectors and incomplete artifacts fail without live-data fallback.
369
+
370
+ For saved split names beginning with `--`, use the unambiguous equals spelling:
371
+ `orizu datasets download --dataset-version DATASET_VERSION_ID --split-set SPLIT_SET_ID --split=--validation --format jsonl --out validation.jsonl`.
372
+ The same spelling supports `--split=--json` and `--split=--help`; all pinned selectors remain required.
373
+
374
+ Enrich rows outside Orizu, preserving every canonical `id`. Assemble the complete
375
+ enriched dataset, including other partitions, and upload or version it. Import
376
+ assignments into the explicit new version:
377
+
378
+ ```bash
379
+ orizu datasets splits create TARGET_DATASET_VERSION_ID --from-file split.json --json
380
+ ```
381
+
382
+ Show JSON preserves `name`, `strategy`, `seed`, `metadata`, and
383
+ `partitions: [{name, row_ids, metadata}]`. Set and partition metadata retain
384
+ their JSON values, including arrays, scalars and null. Source identity never overrides the command's
385
+ target. Every referenced ID must exist in the target; missing IDs fail rather
386
+ than being dropped or remapped. Reads follow existing project permissions.
387
+
354
388
  Dataset version and split-set commands used by prompt runs are covered in `prompt-control-plane.md`.
355
389
 
356
390
  ### Model configs
@@ -73,7 +73,7 @@ orizu datasets splits create <upload-returned-dataset-version-id> --from-file ./
73
73
 
74
74
  Reserve `orizu datasets versions create` for a subsequent snapshot, and give it a new label such as `v2`; labels are unique within a dataset. The dataset README should record source approval, source locators, sampling decisions, deduplication rules, the coverage table, ground-truth status, and the split rationale.
75
75
 
76
- The dataset version freezes its rows. Server-side split membership is mutable, but the CLI exposes no split-set read operation, so membership cannot be re-read through the CLI. Treat the independently reviewed local split file plus the returned split-set ID as the canonical record available to this workflow. Do not mutate that server-side split set; pass the recorded `--dataset-version-id` and `--split-set-id` to `orizu optimizations run-gepa`, and record the CLI verification gap explicitly rather than claiming the server membership was re-verified.
76
+ The dataset version freezes its rows. After creating the split set, re-read its saved membership with `orizu datasets splits show <split-set-id> --json` and confirm it matches the reviewed local split file before relying on it; the saved split set, not the local file, is the canonical record. Do not mutate that server-side split set; pass the recorded `--dataset-version-id` and `--split-set-id` to `orizu optimizations run-gepa`.
77
77
 
78
78
  ## Guard live rows before annotation and version later data
79
79
 
@@ -1,10 +0,0 @@
1
- export interface DatasetEditRowsOptions {
2
- datasetId: string;
3
- file: string;
4
- json: boolean;
5
- printJson: (value: Record<string, unknown>) => void;
6
- printLine: (value: string) => void;
7
- sanitize: (value: string) => string;
8
- }
9
- export declare function editDatasetRows(options: DatasetEditRowsOptions): Promise<void>;
10
- //# sourceMappingURL=dataset-edit-rows.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dataset-edit-rows.d.ts","sourceRoot":"","sources":["../src/dataset-edit-rows.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IACnD,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;CACpC;AAsDD,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+CpF"}
@@ -1,90 +0,0 @@
1
- import { extractErrorMessage } from './error-response.js';
2
- import { parseDatasetFile } from './file-parser.js';
3
- import { authedFetch } from './http.js';
4
- const MAX_REQUEST_BYTES = 3 * 1024 * 1024;
5
- const MAX_CHUNK_ROWS = 10;
6
- const EMPTY_REQUEST_BYTES = Buffer.byteLength('{"rows":[]}', 'utf8');
7
- function normalizeRows(rows) {
8
- if (rows.length === 0) {
9
- throw new Error('Dataset edit file must contain at least one row');
10
- }
11
- const seenRowIds = new Set();
12
- return rows.map((row, index) => {
13
- const rowId = typeof row.id === 'string' ? row.id.trim() : '';
14
- if (!rowId) {
15
- throw new Error(`Dataset edit file rows[${index}] must include a non-empty string id`);
16
- }
17
- if (seenRowIds.has(rowId)) {
18
- throw new Error(`Dataset edit file rows[${index}].id duplicates a previous row id`);
19
- }
20
- seenRowIds.add(rowId);
21
- return { ...row, id: rowId };
22
- });
23
- }
24
- function chunkRows(rows, sanitize) {
25
- const chunks = [];
26
- let currentChunk = [];
27
- let currentBytes = EMPTY_REQUEST_BYTES;
28
- for (const row of rows) {
29
- const rowBytes = Buffer.byteLength(JSON.stringify(row), 'utf8');
30
- const framedRowBytes = rowBytes + (currentChunk.length > 0 ? 1 : 0);
31
- if (currentChunk.length > 0 &&
32
- (currentChunk.length >= MAX_CHUNK_ROWS || currentBytes + framedRowBytes > MAX_REQUEST_BYTES)) {
33
- chunks.push(currentChunk);
34
- currentChunk = [];
35
- currentBytes = EMPTY_REQUEST_BYTES;
36
- }
37
- const singleRowRequestBytes = EMPTY_REQUEST_BYTES + rowBytes;
38
- if (singleRowRequestBytes > MAX_REQUEST_BYTES) {
39
- throw new Error(`Dataset edit row ${sanitize(row.id)} is ${singleRowRequestBytes} bytes, ` +
40
- `which exceeds the ${MAX_REQUEST_BYTES}-byte edit request limit`);
41
- }
42
- currentChunk.push(row);
43
- currentBytes += rowBytes + (currentChunk.length > 1 ? 1 : 0);
44
- }
45
- if (currentChunk.length > 0)
46
- chunks.push(currentChunk);
47
- return chunks;
48
- }
49
- export async function editDatasetRows(options) {
50
- const { rows } = parseDatasetFile(options.file);
51
- const chunks = chunkRows(normalizeRows(rows), options.sanitize);
52
- let totalUpdated = 0;
53
- let lastResult = null;
54
- for (let index = 0; index < chunks.length; index += 1) {
55
- const chunk = chunks[index];
56
- if (!options.json && chunks.length > 1) {
57
- options.printLine(`Updating chunk ${index + 1}/${chunks.length} (${chunk.length} rows)...`);
58
- }
59
- try {
60
- const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(options.datasetId)}/rows`, {
61
- method: 'PATCH',
62
- headers: { 'Content-Type': 'application/json' },
63
- body: JSON.stringify({ rows: chunk }),
64
- });
65
- if (!response.ok) {
66
- throw new Error(`HTTP ${response.status}: ${await extractErrorMessage(response)}`);
67
- }
68
- const data = await response.json();
69
- if (!Number.isInteger(data.updatedCount) || data.updatedCount !== chunk.length) {
70
- throw new Error(`Server reported ${String(data.updatedCount)} updated rows for a ${chunk.length}-row chunk`);
71
- }
72
- totalUpdated += data.updatedCount;
73
- lastResult = data;
74
- }
75
- catch (error) {
76
- const message = error instanceof Error ? error.message : String(error);
77
- throw new Error(`Chunk ${index + 1}/${chunks.length} failed: ${message}\n` +
78
- `${totalUpdated} rows from ${index} chunk(s) were already updated. ` +
79
- 'Retry with a file containing only the remaining row edits.');
80
- }
81
- }
82
- if (!lastResult)
83
- throw new Error('Dataset edit file must contain at least one row');
84
- if (options.json) {
85
- options.printJson({ dataset: lastResult.dataset, updatedCount: totalUpdated });
86
- return;
87
- }
88
- options.printLine(`Updated ${totalUpdated} rows in dataset ${options.sanitize(lastResult.dataset.name)} ` +
89
- `(${options.sanitize(lastResult.dataset.id)}). Current row count: ${lastResult.dataset.rowCount}`);
90
- }
@@ -1,124 +0,0 @@
1
- import { extractErrorMessage } from './error-response.js'
2
- import { parseDatasetFile } from './file-parser.js'
3
- import { authedFetch } from './http.js'
4
-
5
- const MAX_REQUEST_BYTES = 3 * 1024 * 1024
6
- const MAX_CHUNK_ROWS = 10
7
- const EMPTY_REQUEST_BYTES = Buffer.byteLength('{"rows":[]}', 'utf8')
8
-
9
- type DatasetRowEdit = Record<string, unknown> & { id: string }
10
-
11
- type EditRowsResponse = {
12
- dataset: { id: string; name: string; rowCount: number }
13
- updatedCount: number
14
- }
15
-
16
- export interface DatasetEditRowsOptions {
17
- datasetId: string
18
- file: string
19
- json: boolean
20
- printJson: (value: Record<string, unknown>) => void
21
- printLine: (value: string) => void
22
- sanitize: (value: string) => string
23
- }
24
-
25
- function normalizeRows(rows: Array<Record<string, unknown>>): DatasetRowEdit[] {
26
- if (rows.length === 0) {
27
- throw new Error('Dataset edit file must contain at least one row')
28
- }
29
-
30
- const seenRowIds = new Set<string>()
31
- return rows.map((row, index) => {
32
- const rowId = typeof row.id === 'string' ? row.id.trim() : ''
33
- if (!rowId) {
34
- throw new Error(`Dataset edit file rows[${index}] must include a non-empty string id`)
35
- }
36
- if (seenRowIds.has(rowId)) {
37
- throw new Error(`Dataset edit file rows[${index}].id duplicates a previous row id`)
38
- }
39
- seenRowIds.add(rowId)
40
- return { ...row, id: rowId }
41
- })
42
- }
43
-
44
- function chunkRows(rows: DatasetRowEdit[], sanitize: (value: string) => string): DatasetRowEdit[][] {
45
- const chunks: DatasetRowEdit[][] = []
46
- let currentChunk: DatasetRowEdit[] = []
47
- let currentBytes = EMPTY_REQUEST_BYTES
48
-
49
- for (const row of rows) {
50
- const rowBytes = Buffer.byteLength(JSON.stringify(row), 'utf8')
51
- const framedRowBytes = rowBytes + (currentChunk.length > 0 ? 1 : 0)
52
- if (
53
- currentChunk.length > 0 &&
54
- (currentChunk.length >= MAX_CHUNK_ROWS || currentBytes + framedRowBytes > MAX_REQUEST_BYTES)
55
- ) {
56
- chunks.push(currentChunk)
57
- currentChunk = []
58
- currentBytes = EMPTY_REQUEST_BYTES
59
- }
60
-
61
- const singleRowRequestBytes = EMPTY_REQUEST_BYTES + rowBytes
62
- if (singleRowRequestBytes > MAX_REQUEST_BYTES) {
63
- throw new Error(
64
- `Dataset edit row ${sanitize(row.id)} is ${singleRowRequestBytes} bytes, ` +
65
- `which exceeds the ${MAX_REQUEST_BYTES}-byte edit request limit`
66
- )
67
- }
68
-
69
- currentChunk.push(row)
70
- currentBytes += rowBytes + (currentChunk.length > 1 ? 1 : 0)
71
- }
72
-
73
- if (currentChunk.length > 0) chunks.push(currentChunk)
74
- return chunks
75
- }
76
-
77
- export async function editDatasetRows(options: DatasetEditRowsOptions): Promise<void> {
78
- const { rows } = parseDatasetFile(options.file)
79
- const chunks = chunkRows(normalizeRows(rows), options.sanitize)
80
- let totalUpdated = 0
81
- let lastResult: EditRowsResponse | null = null
82
-
83
- for (let index = 0; index < chunks.length; index += 1) {
84
- const chunk = chunks[index]
85
- if (!options.json && chunks.length > 1) {
86
- options.printLine(`Updating chunk ${index + 1}/${chunks.length} (${chunk.length} rows)...`)
87
- }
88
-
89
- try {
90
- const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(options.datasetId)}/rows`, {
91
- method: 'PATCH',
92
- headers: { 'Content-Type': 'application/json' },
93
- body: JSON.stringify({ rows: chunk }),
94
- })
95
- if (!response.ok) {
96
- throw new Error(`HTTP ${response.status}: ${await extractErrorMessage(response)}`)
97
- }
98
-
99
- const data = await response.json() as EditRowsResponse
100
- if (!Number.isInteger(data.updatedCount) || data.updatedCount !== chunk.length) {
101
- throw new Error(`Server reported ${String(data.updatedCount)} updated rows for a ${chunk.length}-row chunk`)
102
- }
103
- totalUpdated += data.updatedCount
104
- lastResult = data
105
- } catch (error) {
106
- const message = error instanceof Error ? error.message : String(error)
107
- throw new Error(
108
- `Chunk ${index + 1}/${chunks.length} failed: ${message}\n` +
109
- `${totalUpdated} rows from ${index} chunk(s) were already updated. ` +
110
- 'Retry with a file containing only the remaining row edits.'
111
- )
112
- }
113
- }
114
-
115
- if (!lastResult) throw new Error('Dataset edit file must contain at least one row')
116
- if (options.json) {
117
- options.printJson({ dataset: lastResult.dataset, updatedCount: totalUpdated })
118
- return
119
- }
120
- options.printLine(
121
- `Updated ${totalUpdated} rows in dataset ${options.sanitize(lastResult.dataset.name)} ` +
122
- `(${options.sanitize(lastResult.dataset.id)}). Current row count: ${lastResult.dataset.rowCount}`
123
- )
124
- }