monday-cli 0.7.0 → 0.8.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/README.md +87 -45
  3. package/dist/api/assets.d.ts +3 -3
  4. package/dist/api/board-metadata.d.ts +7 -4
  5. package/dist/api/board-metadata.d.ts.map +1 -1
  6. package/dist/api/board-metadata.js +21 -6
  7. package/dist/api/board-metadata.js.map +1 -1
  8. package/dist/api/column-types.d.ts +14 -7
  9. package/dist/api/column-types.d.ts.map +1 -1
  10. package/dist/api/column-types.js +14 -7
  11. package/dist/api/column-types.js.map +1 -1
  12. package/dist/api/error-decoration.d.ts +124 -0
  13. package/dist/api/error-decoration.d.ts.map +1 -0
  14. package/dist/api/error-decoration.js +161 -0
  15. package/dist/api/error-decoration.js.map +1 -0
  16. package/dist/api/fetch-transport-helpers.d.ts +97 -0
  17. package/dist/api/fetch-transport-helpers.d.ts.map +1 -0
  18. package/dist/api/fetch-transport-helpers.js +175 -0
  19. package/dist/api/fetch-transport-helpers.js.map +1 -0
  20. package/dist/api/file-column-set.d.ts +388 -82
  21. package/dist/api/file-column-set.d.ts.map +1 -1
  22. package/dist/api/file-column-set.js +466 -88
  23. package/dist/api/file-column-set.js.map +1 -1
  24. package/dist/api/multipart-transport.d.ts +95 -60
  25. package/dist/api/multipart-transport.d.ts.map +1 -1
  26. package/dist/api/multipart-transport.js +102 -120
  27. package/dist/api/multipart-transport.js.map +1 -1
  28. package/dist/api/transport.d.ts.map +1 -1
  29. package/dist/api/transport.js +2 -99
  30. package/dist/api/transport.js.map +1 -1
  31. package/dist/cli/program.js +1 -1
  32. package/dist/cli/program.js.map +1 -1
  33. package/dist/commands/auth/login.js +1 -1
  34. package/dist/commands/auth/login.js.map +1 -1
  35. package/dist/commands/auth/logout.js +1 -1
  36. package/dist/commands/auth/logout.js.map +1 -1
  37. package/dist/commands/board/column-create.d.ts +20 -2
  38. package/dist/commands/board/column-create.d.ts.map +1 -1
  39. package/dist/commands/board/column-create.js +191 -20
  40. package/dist/commands/board/column-create.js.map +1 -1
  41. package/dist/commands/board/describe.d.ts +5 -3
  42. package/dist/commands/board/describe.d.ts.map +1 -1
  43. package/dist/commands/board/describe.js +12 -4
  44. package/dist/commands/board/describe.js.map +1 -1
  45. package/dist/commands/completion.js +1 -1
  46. package/dist/commands/completion.js.map +1 -1
  47. package/dist/commands/dev/configure.js +1 -1
  48. package/dist/commands/dev/configure.js.map +1 -1
  49. package/dist/commands/dev/discover.js +1 -1
  50. package/dist/commands/dev/discover.js.map +1 -1
  51. package/dist/commands/dev/doctor.js +1 -1
  52. package/dist/commands/dev/doctor.js.map +1 -1
  53. package/dist/commands/dev/epic/items.js +2 -2
  54. package/dist/commands/dev/epic/items.js.map +1 -1
  55. package/dist/commands/dev/epic/list.js +2 -2
  56. package/dist/commands/dev/epic/list.js.map +1 -1
  57. package/dist/commands/dev/release/list.js +2 -2
  58. package/dist/commands/dev/release/list.js.map +1 -1
  59. package/dist/commands/dev/sprint/current.js +2 -2
  60. package/dist/commands/dev/sprint/current.js.map +1 -1
  61. package/dist/commands/dev/sprint/items.js +2 -2
  62. package/dist/commands/dev/sprint/items.js.map +1 -1
  63. package/dist/commands/dev/sprint/list.js +2 -2
  64. package/dist/commands/dev/sprint/list.js.map +1 -1
  65. package/dist/commands/dev/task/block.js +2 -2
  66. package/dist/commands/dev/task/block.js.map +1 -1
  67. package/dist/commands/dev/task/done.js +2 -2
  68. package/dist/commands/dev/task/done.js.map +1 -1
  69. package/dist/commands/dev/task/list.js +2 -2
  70. package/dist/commands/dev/task/list.js.map +1 -1
  71. package/dist/commands/dev/task/start.js +2 -2
  72. package/dist/commands/dev/task/start.js.map +1 -1
  73. package/dist/commands/doc/get.js +1 -1
  74. package/dist/commands/doc/get.js.map +1 -1
  75. package/dist/commands/doc/list.js +1 -1
  76. package/dist/commands/doc/list.js.map +1 -1
  77. package/dist/commands/emit.d.ts.map +1 -1
  78. package/dist/commands/emit.js +19 -16
  79. package/dist/commands/emit.js.map +1 -1
  80. package/dist/commands/item/clear.d.ts.map +1 -1
  81. package/dist/commands/item/clear.js +15 -41
  82. package/dist/commands/item/clear.js.map +1 -1
  83. package/dist/commands/item/create.d.ts +93 -1
  84. package/dist/commands/item/create.d.ts.map +1 -1
  85. package/dist/commands/item/create.js +474 -53
  86. package/dist/commands/item/create.js.map +1 -1
  87. package/dist/commands/item/search.js +7 -7
  88. package/dist/commands/item/search.js.map +1 -1
  89. package/dist/commands/item/set.d.ts +1 -0
  90. package/dist/commands/item/set.d.ts.map +1 -1
  91. package/dist/commands/item/set.js +94 -1
  92. package/dist/commands/item/set.js.map +1 -1
  93. package/dist/commands/item/time-track/start.js +2 -2
  94. package/dist/commands/item/time-track/start.js.map +1 -1
  95. package/dist/commands/item/time-track/stop.js +2 -2
  96. package/dist/commands/item/time-track/stop.js.map +1 -1
  97. package/dist/commands/item/update.d.ts +128 -11
  98. package/dist/commands/item/update.d.ts.map +1 -1
  99. package/dist/commands/item/update.js +784 -82
  100. package/dist/commands/item/update.js.map +1 -1
  101. package/dist/commands/item/upload.js +5 -5
  102. package/dist/commands/item/upload.js.map +1 -1
  103. package/dist/commands/item/watch.js +2 -2
  104. package/dist/commands/item/watch.js.map +1 -1
  105. package/dist/commands/notification/send.js +1 -1
  106. package/dist/commands/notification/send.js.map +1 -1
  107. package/dist/commands/update/upload.js +3 -3
  108. package/dist/commands/update/upload.js.map +1 -1
  109. package/dist/commands/user/team-add-members.js +1 -1
  110. package/dist/commands/user/team-add-members.js.map +1 -1
  111. package/dist/commands/user/team-create.js +1 -1
  112. package/dist/commands/user/team-create.js.map +1 -1
  113. package/dist/commands/user/team-remove-members.js +1 -1
  114. package/dist/commands/user/team-remove-members.js.map +1 -1
  115. package/dist/commands/webhook/create.js +1 -1
  116. package/dist/commands/webhook/create.js.map +1 -1
  117. package/dist/commands/webhook/delete.js +1 -1
  118. package/dist/commands/webhook/delete.js.map +1 -1
  119. package/dist/commands/webhook/list.js +1 -1
  120. package/dist/commands/webhook/list.js.map +1 -1
  121. package/dist/utils/file-source.d.ts +109 -0
  122. package/dist/utils/file-source.d.ts.map +1 -1
  123. package/dist/utils/file-source.js +123 -0
  124. package/dist/utils/file-source.js.map +1 -1
  125. package/dist/utils/output/select.d.ts +22 -0
  126. package/dist/utils/output/select.d.ts.map +1 -1
  127. package/dist/utils/output/select.js +30 -0
  128. package/dist/utils/output/select.js.map +1 -1
  129. package/dist/utils/output/table.d.ts +10 -0
  130. package/dist/utils/output/table.d.ts.map +1 -1
  131. package/dist/utils/output/table.js +40 -3
  132. package/dist/utils/output/table.js.map +1 -1
  133. package/package.json +2 -1
@@ -52,9 +52,10 @@ import { parseArgv } from '../parse-argv.js';
52
52
  import { ApiError, MondayCliError, UsageError } from '../../utils/errors.js';
53
53
  import { selectMutation, } from '../../api/column-values.js';
54
54
  import { executeItemMutation } from '../../api/item-mutation-execute.js';
55
- import { executeFileColumnSet, fileColumnSetOutputSchema, preCheckM38FileDispatch, } from '../../api/file-column-set.js';
56
- import { precheckLocalFile } from '../../utils/file-source.js';
55
+ import { executeFileColumnSet, dispatchFileLegsSequentially, fileColumnSetOutputSchema, fileColumnSetMultiOutputSchema, preCheckM38FileDispatch, } from '../../api/file-column-set.js';
56
+ import { precheckLocalFile, isStdinFileSetSource, readStdinFileSource, resolveStdinFilename, STDIN_FILE_SENTINEL, } from '../../utils/file-source.js';
57
57
  import { invalidateBoard } from '../../api/cache.js';
58
+ import { addFileToColumn } from '../../api/assets.js';
58
59
  import { dispatchSequential, } from '../../api/partial-success-mutation.js';
59
60
  import { dispatchParallel } from '../../api/parallel-dispatch.js';
60
61
  import { parseSetRawExpression, } from '../../api/raw-write.js';
@@ -64,6 +65,7 @@ import { resolveBoardId } from '../../api/item-board-lookup.js';
64
65
  import { SourceAggregator, mergeCacheAge, mergeSource, } from '../../api/source-aggregator.js';
65
66
  import { resolveAndTranslate } from '../../api/resolution-pass.js';
66
67
  import { foldAndRemap, mergeResolverWarningsIntoError, } from '../../api/resolver-error-fold.js';
68
+ import { projectCauseForEnvelope, reThrowDecorated, } from '../../api/error-decoration.js';
67
69
  import { planChanges } from '../../api/dry-run.js';
68
70
  import { buildQueryParams } from '../../api/filters.js';
69
71
  import { loadBoardMetadata, refreshBoardMetadata, } from '../../api/board-metadata.js';
@@ -77,19 +79,25 @@ import { MIN_CONCURRENCY, MAX_CONCURRENCY, } from '../../api/parallel-dispatch.j
77
79
  /**
78
80
  * Output envelope union — projected-item for the JSON translator
79
81
  * path (text / status / dropdown / date / people / etc.) +
80
- * file-dispatch envelope for the friendly `--set <file-col>=<path>`
81
- * path. The file-dispatch shape ships across v0.6-M38 (single-item
82
- * — `operation: 'add_file_to_column'`) and v0.7-M42 (bulk
83
- * `operation: 'item_update_bulk_file_set'`; the per-item fan-out's
84
- * `data.results[i].asset` slots wrap M31's `Asset` projection). The
85
- * union below admits only the single-item shape because the bulk
86
- * variant is emitted via its own `bulkFileSetDataSchema` at the
87
- * `runItemUpdateBulkFileDispatch` helper; agents discriminate on
88
- * `operation` (present + literal value identifies the variant).
82
+ * single-item file-dispatch envelopes for the friendly `--set
83
+ * <file-col>=<path>` path: v0.6-M38 single-file (`operation:
84
+ * 'add_file_to_column'`) + v0.8-M46 single-item multi-file
85
+ * (`operation: 'add_files_to_columns'`; `assets[]` wraps M31's
86
+ * `Asset` projection per file column).
87
+ *
88
+ * The union admits only the SINGLE-ITEM shapes. The BULK file
89
+ * variants — v0.7-M42 (`item_update_bulk_file_set`) + v0.8-M46
90
+ * (`item_update_bulk_file_set_multi`) are emitted via their own
91
+ * `bulkFileSet*DataSchema` at the `runItemUpdateBulk*FileDispatch`
92
+ * helpers and stay OUT of this union (the bulk per-item-results
93
+ * shape is structurally distinct from the single-resource `data`
94
+ * payload this schema describes); agents discriminate on `operation`
95
+ * (present + literal value identifies the variant).
89
96
  */
90
97
  export const itemUpdateOutputSchema = z.union([
91
98
  projectedItemSchema,
92
99
  fileColumnSetOutputSchema,
100
+ fileColumnSetMultiOutputSchema,
93
101
  ]);
94
102
  /**
95
103
  * Input shape — supports both single-item and bulk shapes.
@@ -112,6 +120,17 @@ const inputSchema = z
112
120
  // cli-design §5.3 line 961-972 (resolution-time, not parse-time).
113
121
  setRaw: z.array(z.string()).default([]),
114
122
  name: z.string().min(1).optional(),
123
+ // v0.8-M47 (D7 fold): companion to a stdin file `--set
124
+ // <file-col>=-` source — sets Monday's wire `Asset.name`. OPTIONAL
125
+ // (probe: `add_file_to_column` accepts any non-empty filename;
126
+ // only an EMPTY one `500`s — `.min(1)` rejects `--filename ""` at
127
+ // the parse boundary as `usage_error`). Default when omitted on a
128
+ // stdin source: `DEFAULT_STDIN_FILENAME` (`"blob"`). Consulted ONLY
129
+ // on a `<file-col>=-` stdin dispatch; ignored otherwise (whether a
130
+ // stdin source exists is only knowable after column resolution, so
131
+ // a no-stdin `--filename` is a harmless no-op rather than a
132
+ // resolution-coupled reject).
133
+ filename: z.string().min(1).optional(),
115
134
  board: BoardIdSchema.optional(),
116
135
  where: z.array(z.string()).default([]),
117
136
  // Empty `--filter-json ''` would slip through `buildQueryParams`
@@ -259,6 +278,7 @@ export const itemUpdateCommand = {
259
278
  .option('--set <expr>', 'repeatable <col>=<val> column write', (value, prev) => [...prev, value], [])
260
279
  .option('--set-raw <expr>', 'repeatable <col>=<json> raw write (escape hatch — bypasses friendly translator)', (value, prev) => [...prev, value], [])
261
280
  .option('--name <n>', 'rename the item')
281
+ .option('--filename <name>', "Asset.name for a stdin file `--set <file-col>=-` source (default \"blob\")")
262
282
  .option('--board <bid>', 'board ID (required for bulk; skip lookup for single-item)')
263
283
  .option('--where <expr>', 'repeatable bulk filter (cli-design §10.2): <col><op><val>', (value, prev) => [...prev, value], [])
264
284
  .option('--filter-json <json>', 'literal Monday query_params for bulk')
@@ -332,6 +352,29 @@ export const itemUpdateCommand = {
332
352
  // the pre-check's resolver warnings + source aggregation
333
353
  // into the final envelope (P3-1 — IMPL round-1 fix).
334
354
  await runItemUpdateSingleFileDispatch({
355
+ client,
356
+ multipart,
357
+ ctx,
358
+ programOpts: program.opts(),
359
+ apiVersion,
360
+ boardId,
361
+ itemId: dispatch.itemId,
362
+ m38,
363
+ isDryRun: globalFlags.dryRun,
364
+ retries: globalFlags.retry,
365
+ filename: parsed.filename,
366
+ toEmit,
367
+ });
368
+ return;
369
+ }
370
+ if (m38.kind === 'file_multi') {
371
+ // v0.8-M46 D2 carve-out fold — single-item multi-file
372
+ // dispatch path. argv parse + pre-check (which returned
373
+ // `kind: 'file_multi'` here) have already run as live
374
+ // contract; the per-item multi-leg fan-out body runs N
375
+ // sequential `add_file_to_column` legs against the single
376
+ // item (runtime body shipped at v0.8-M46 IMPL).
377
+ await runItemUpdateSingleFileMultiDispatch({
335
378
  client,
336
379
  multipart,
337
380
  ctx,
@@ -641,10 +684,18 @@ const runBulk = async (inputs) => {
641
684
  // boundary, BEFORE the items_page walker + confirmation
642
685
  // gate. The pre-check resolves setEntries against the now-
643
686
  // warm metadata cache and runs
644
- // `enforceSingleFileColumnSet({callShape: 'item_update_bulk'})`:
687
+ // `routeFileColumnDispatch({callShape: 'item_update_bulk'})`:
645
688
  //
646
- // - Multi-file `--set` throws `'multi_file_set_unsupported'`
647
- // (universal rule; single-column-per-wire-call).
689
+ // - Multi-file `--set` with distinct file columns →
690
+ // returns `kind: 'file_bulk_multi'` (v0.8-M46 D2
691
+ // carve-out fold; action body branches into the
692
+ // per-item multi-leg fan-out helper
693
+ // `runItemUpdateBulkFileMultiDispatch` — runtime body
694
+ // shipped at v0.8-M46 IMPL).
695
+ // - Multi-file `--set` with duplicate resolved file
696
+ // columns → throws `'duplicate_resolved_file_columns'`
697
+ // (mirrors JSON path's cross-token duplicate-resolved-
698
+ // ID contract; M46 R1 P2-1 fix).
648
699
  // - File `--set` + value `--set` / `--set-raw` / `--name`
649
700
  // → throws `'mixed_file_and_value_sets'` (universal rule;
650
701
  // mixing forces non-atomic multi-leg dispatch).
@@ -668,6 +719,7 @@ const runBulk = async (inputs) => {
668
719
  // unchanged).
669
720
  let m38Warnings = [];
670
721
  let m38FileBulk;
722
+ let m38FileBulkMulti;
671
723
  if (setEntries.length > 0) {
672
724
  const m38 = await preCheckM38FileDispatch({
673
725
  client,
@@ -693,6 +745,14 @@ const runBulk = async (inputs) => {
693
745
  // `executeFileColumnSet` across matched items.
694
746
  m38FileBulk = m38;
695
747
  }
748
+ if (m38.kind === 'file_bulk_multi') {
749
+ // v0.8-M46 D2 carve-out fold. Hold the file_bulk_multi slot
750
+ // for the multi-leg per-item fan-out below. items_page walker
751
+ // + confirmation gate still apply; the dispatch helper fans N
752
+ // sequential file legs per matched item (runtime body shipped
753
+ // at v0.8-M46 IMPL).
754
+ m38FileBulkMulti = m38;
755
+ }
696
756
  }
697
757
  const onColumnNotFound = meta.source === 'cache'
698
758
  ? async () => {
@@ -848,6 +908,34 @@ const runBulk = async (inputs) => {
848
908
  });
849
909
  return;
850
910
  }
911
+ // v0.8-M46 D2 carve-out fold — bulk multi-file `--set` dispatch.
912
+ // The per-item multi-leg fan-out body runs N sequential file legs
913
+ // per matched item (cross-item parallel under `--concurrency` ×
914
+ // within-item sequential; runtime body shipped at v0.8-M46 IMPL).
915
+ // Fires AFTER the items_page walker + confirmation gate (so an
916
+ // agent sees the matched-count via `confirmation_required` before
917
+ // any dispatch fans out) and BEFORE the JSON translator path's
918
+ // `resolveAndTranslate` call.
919
+ if (m38FileBulkMulti !== undefined) {
920
+ await runItemUpdateBulkFileMultiDispatch({
921
+ parsed,
922
+ client,
923
+ multipart,
924
+ ctx,
925
+ programOpts,
926
+ apiVersion,
927
+ boardId,
928
+ matchedItemIds,
929
+ m38: m38FileBulkMulti,
930
+ metaSource: meta.source,
931
+ metaCacheAgeSeconds: meta.cacheAgeSeconds,
932
+ filterWarnings: filterResult.warnings,
933
+ retries: globalFlags.retry,
934
+ isDryRun: globalFlags.dryRun,
935
+ noCache: globalFlags.noCache,
936
+ });
937
+ return;
938
+ }
851
939
  const { dateResolution, peopleResolution, tagResolution, relationResolution } = buildResolutionContexts({ client, ctx, globalFlags });
852
940
  // 5) Dry-run path: per-item planChanges. Column resolution is
853
941
  // cached after the first call; per-item state read fires per
@@ -1143,34 +1231,16 @@ const runBulk = async (inputs) => {
1143
1231
  resolutionSource: remapSource,
1144
1232
  });
1145
1233
  // Decorate with bulk-progress details so agents can see how
1146
- // many items mutated successfully before the failure.
1234
+ // many items mutated successfully before the failure, then
1235
+ // delegate the typed split + wire-metadata spreads to the
1236
+ // shared `reThrowDecorated` helper (R-v0.7-NEW-5).
1147
1237
  const existing = remapped.details ?? {};
1148
- if (remapped.code === 'usage_error') {
1149
- throw new UsageError(remapped.message, {
1150
- ...(remapped.cause === undefined ? {} : { cause: remapped.cause }),
1151
- details: {
1152
- ...existing,
1153
- applied_count: appliedItems.length,
1154
- applied_to: appliedItems.map((i) => i.id),
1155
- failed_at_item: itemId,
1156
- matched_count: matchedItemIds.length,
1157
- },
1158
- });
1159
- }
1160
- throw new ApiError(remapped.code, remapped.message, {
1161
- ...(remapped.cause === undefined ? {} : { cause: remapped.cause }),
1162
- ...(remapped.httpStatus === undefined ? {} : { httpStatus: remapped.httpStatus }),
1163
- ...(remapped.mondayCode === undefined ? {} : { mondayCode: remapped.mondayCode }),
1164
- ...(remapped.requestId === undefined ? {} : { requestId: remapped.requestId }),
1165
- retryable: remapped.retryable,
1166
- ...(remapped.retryAfterSeconds === undefined ? {} : { retryAfterSeconds: remapped.retryAfterSeconds }),
1167
- details: {
1168
- ...existing,
1169
- applied_count: appliedItems.length,
1170
- applied_to: appliedItems.map((i) => i.id),
1171
- failed_at_item: itemId,
1172
- matched_count: matchedItemIds.length,
1173
- },
1238
+ reThrowDecorated(remapped, {
1239
+ ...existing,
1240
+ applied_count: appliedItems.length,
1241
+ applied_to: appliedItems.map((i) => i.id),
1242
+ failed_at_item: itemId,
1243
+ matched_count: matchedItemIds.length,
1174
1244
  });
1175
1245
  }
1176
1246
  throw err;
@@ -1210,6 +1280,79 @@ const runBulk = async (inputs) => {
1210
1280
  });
1211
1281
  };
1212
1282
  const runItemUpdateSingleFileDispatch = async (inputs) => {
1283
+ // v0.8-M47 (D7 fold): stdin file `--set <file-col>=-` source. The
1284
+ // pre-check (`routeFileColumnDispatch` stdin scope gate) already
1285
+ // confirmed this is the sole file entry on the single-item callShape;
1286
+ // source the Blob from stdin (via `readStdinFileSource`) instead of a
1287
+ // local path. Dry-run emits the D4 size-less echo WITHOUT consuming
1288
+ // stdin; live buffers stdin once + dispatches the pre-built Blob via
1289
+ // M31's `addFileToColumn` fetcher (the path leg's
1290
+ // `executeFileColumnSet` builds its Blob from a path — stdin's is
1291
+ // already in hand) + emits the same M31-shaped envelope.
1292
+ if (isStdinFileSetSource(inputs.m38.rawValue)) {
1293
+ const filename = resolveStdinFilename(inputs.filename);
1294
+ if (inputs.isDryRun) {
1295
+ emitDryRun({
1296
+ ctx: inputs.ctx,
1297
+ programOpts: inputs.programOpts,
1298
+ plannedChanges: [
1299
+ {
1300
+ operation: 'add_file_to_column',
1301
+ item_id: inputs.itemId,
1302
+ column_id: inputs.m38.columnId,
1303
+ file_path: STDIN_FILE_SENTINEL,
1304
+ filename,
1305
+ },
1306
+ ],
1307
+ source: 'none',
1308
+ cacheAgeSeconds: null,
1309
+ warnings: inputs.m38.warnings,
1310
+ apiVersion: inputs.apiVersion,
1311
+ });
1312
+ return;
1313
+ }
1314
+ const stdinSource = await readStdinFileSource(inputs.ctx.stdin, filename);
1315
+ const stdinResult = await addFileToColumn({
1316
+ client: inputs.client,
1317
+ multipart: inputs.multipart,
1318
+ itemId: inputs.itemId,
1319
+ columnId: inputs.m38.columnId,
1320
+ file: stdinSource.blob,
1321
+ filename: stdinSource.filename,
1322
+ signal: inputs.ctx.signal,
1323
+ retries: inputs.retries,
1324
+ });
1325
+ await invalidateBoard(inputs.boardId, inputs.ctx.env);
1326
+ const stdinData = {
1327
+ operation: 'add_file_to_column',
1328
+ item_id: inputs.itemId,
1329
+ column_id: inputs.m38.columnId,
1330
+ filename: stdinSource.filename,
1331
+ file_size_bytes: stdinSource.fileSizeBytes,
1332
+ asset: stdinResult.asset,
1333
+ };
1334
+ emitMutation({
1335
+ ctx: inputs.ctx,
1336
+ data: stdinData,
1337
+ schema: fileColumnSetOutputSchema,
1338
+ programOpts: inputs.programOpts,
1339
+ warnings: inputs.m38.warnings.map((w) => ({
1340
+ code: w.code,
1341
+ message: w.message,
1342
+ details: w.details,
1343
+ })),
1344
+ ...inputs.toEmit({
1345
+ data: stdinResult.asset,
1346
+ complexity: stdinResult.complexity,
1347
+ stats: { attempts: 1, totalBackoffMs: 0 },
1348
+ }),
1349
+ source: 'live',
1350
+ cacheAgeSeconds: null,
1351
+ complexity: stdinResult.complexity,
1352
+ resolvedIds: { [inputs.m38.token]: inputs.m38.columnId },
1353
+ });
1354
+ return;
1355
+ }
1213
1356
  const precheck = await precheckLocalFile(inputs.m38.rawValue);
1214
1357
  if (inputs.isDryRun) {
1215
1358
  emitDryRun({
@@ -1361,13 +1504,15 @@ const runItemUpdateSingleFileDispatch = async (inputs) => {
1361
1504
  // - R-NEW-72 (post-fix-up cross-doc grep) — apply at every
1362
1505
  // Codex IMPL fix-up round that flips a contract surface.
1363
1506
  //
1364
- // **Future lift candidate.** The fail-fast error-decoration block
1365
- // (`if (err.code === 'usage_error') { throw new UsageError(...) } else
1366
- // { throw new ApiError(...) }`) is byte-equivalent across this
1367
- // helper and the JSON-bulk action body (R-NEW-58 2-consumer
1368
- // trigger). Lift candidate fires at the 3rd consumer (M43 create-
1369
- // time fold may add one if its rollback / orphan-warn shape
1370
- // re-uses the same decoration).
1507
+ // **R-v0.7-NEW-5 SHIPPED (v0.8 refactor cluster).** The fail-fast
1508
+ // error-decoration block (the `usage_error UsageError` / else
1509
+ // `ApiError` typed split + the conditional wire-metadata spreads)
1510
+ // reached 4 consumers (this helper + the JSON-bulk action body + the
1511
+ // M46 file-bulk-multi path + bulk clear) and was lifted to
1512
+ // `reThrowDecorated` in `src/api/error-decoration.ts`. Each site now
1513
+ // assembles its own `details` decoration and delegates the typed
1514
+ // split; the helper carries the focused unit test that recovers the
1515
+ // conditional-spread branch coverage.
1371
1516
  // ============================================================
1372
1517
  /**
1373
1518
  * Per-item dispatch result for v0.7-M42 bulk file `--set` carve-out
@@ -1422,6 +1567,95 @@ export const bulkFileSetDataSchema = z.object({
1422
1567
  }),
1423
1568
  results: z.array(bulkFileSetResultSchema),
1424
1569
  });
1570
+ /**
1571
+ * v0.8-M46 bulk multi-file `--set` per-item result schema (D6
1572
+ * closure — separate envelope shape from M42's single-file
1573
+ * `bulkFileSetResultSchema`). Per-item record carries the
1574
+ * per-leg asset projections + an `applied_file_columns` echo
1575
+ * slot (length 1..N reflecting which file columns landed
1576
+ * successfully on this item). On per-item partial failure mid-
1577
+ * multi-leg under `--continue-on-error`, `applied_file_columns`
1578
+ * length is 0..N-1 (reflecting columns that landed before the
1579
+ * failing leg) + `failed_file_column` carries the failing
1580
+ * column ID + `error` carries the leg's underlying error.
1581
+ *
1582
+ * **Status: schema landed at v0.8-M46 pre-flight contract diff
1583
+ * (Codex R1 P2-2 fix); runtime emit shipped at v0.8-M46 IMPL.**
1584
+ * `runItemUpdateBulkFileMultiDispatch` (below) emits against this
1585
+ * schema. Mirrors M42's per-item partial-success shape extended
1586
+ * with the multi-leg slots.
1587
+ */
1588
+ export const bulkFileSetMultiResultSchema = z.object({
1589
+ item_id: z.string().min(1),
1590
+ ok: z.boolean(),
1591
+ /**
1592
+ * Per-leg asset projections — one per file column that landed
1593
+ * on this item. Length N on success; length 0..N-1 on per-item
1594
+ * partial failure (`ok: false` with `error` populated). Each
1595
+ * entry carries the column ID + asset shape (mirrors M31's
1596
+ * Asset projection inside a per-leg context).
1597
+ */
1598
+ assets: z
1599
+ .array(z
1600
+ .object({
1601
+ column_id: z.string().min(1),
1602
+ filename: z.string().min(1),
1603
+ file_size_bytes: z.number().int().nonnegative(),
1604
+ asset: z
1605
+ .object({
1606
+ id: z.string().min(1),
1607
+ name: z.string().min(1),
1608
+ })
1609
+ .loose(),
1610
+ })
1611
+ .strict())
1612
+ .optional(),
1613
+ /**
1614
+ * Echo of file column IDs that landed successfully on this
1615
+ * item, in dispatch order. Length 1..N on success; length
1616
+ * 0..N-1 on per-item partial failure (reflecting columns that
1617
+ * landed before the failing leg).
1618
+ */
1619
+ applied_file_columns: z.array(z.string().min(1)).optional(),
1620
+ /** Column ID of the failing leg on per-item partial failure. */
1621
+ failed_file_column: z.string().min(1).optional(),
1622
+ error: z
1623
+ .object({
1624
+ code: z.string().min(1),
1625
+ message: z.string().min(1),
1626
+ })
1627
+ .optional(),
1628
+ });
1629
+ /**
1630
+ * v0.8-M46 bulk multi-file `--set` envelope `data` shape.
1631
+ * Discriminate from M42's single-file `bulkFileSetDataSchema`
1632
+ * via `operation: 'item_update_bulk_file_set_multi'` (plural).
1633
+ * Agents reading `data.operation` branch uniformly across
1634
+ * single + multi shapes. Aggregate `summary` extends M42's
1635
+ * shape with `file_count: number` (the N file columns per item
1636
+ * the call attempted).
1637
+ *
1638
+ * Invariant: `matched_count === applied_count + failed_count`
1639
+ * (per-item rollup; mirrors M42 + M25 invariant). Per-item
1640
+ * partial failure mid-multi-leg counts toward `failed_count`
1641
+ * with the per-item record's `applied_file_columns` reflecting
1642
+ * the partial-leg success.
1643
+ *
1644
+ * **Status: schema landed at v0.8-M46 pre-flight contract diff
1645
+ * (Codex R1 P2-2 fix); runtime emit shipped at v0.8-M46 IMPL.**
1646
+ */
1647
+ export const bulkFileSetMultiDataSchema = z.object({
1648
+ operation: z.literal('item_update_bulk_file_set_multi'),
1649
+ summary: z.object({
1650
+ matched_count: z.number().int().nonnegative(),
1651
+ applied_count: z.number().int().nonnegative(),
1652
+ failed_count: z.number().int().nonnegative(),
1653
+ board_id: z.string().min(1),
1654
+ file_count: z.number().int().min(2),
1655
+ file_column_ids: z.array(z.string().min(1)).min(2),
1656
+ }),
1657
+ results: z.array(bulkFileSetMultiResultSchema),
1658
+ });
1425
1659
  /**
1426
1660
  * Bulk file `--set` per-item dispatch helper (v0.7-M42 D5 carve-out
1427
1661
  * fold).
@@ -1622,9 +1856,9 @@ export const runItemUpdateBulkFileDispatch = async (inputs) => {
1622
1856
  // fail-fast error so a follow-up read doesn't serve stale
1623
1857
  // metadata. Mirrors the M38 single-item invalidate-on-
1624
1858
  // success pattern; the JSON-bulk fail-fast path has the
1625
- // same gap (unchanged by this commit — separate lift
1626
- // candidate per the future-lift-candidate note in the
1627
- // module docstring).
1859
+ // same cache-invalidation gap (still-open lift candidate,
1860
+ // unrelated to the now-shipped R-v0.7-NEW-5 error-decoration
1861
+ // lift).
1628
1862
  if (appliedAssets.length > 0) {
1629
1863
  await invalidateBoard(inputs.boardId, inputs.ctx.env);
1630
1864
  }
@@ -1647,10 +1881,11 @@ export const runItemUpdateBulkFileDispatch = async (inputs) => {
1647
1881
  noCache: inputs.noCache,
1648
1882
  resolutionSource: remapSource,
1649
1883
  });
1650
- // Same decoration shape as the JSON-bulk fail-fast path
1651
- // (lines ~1334-1361 above). Preserves the existing error
1652
- // class' fields and grafts `applied_count` / `applied_to`
1653
- // / `failed_at_item` / `matched_count` onto `details`.
1884
+ // Same decoration shape as the JSON-bulk fail-fast path in
1885
+ // `runBulk` above. Grafts `applied_count` / `applied_to` /
1886
+ // `failed_at_item` / `matched_count` onto `details`, then
1887
+ // delegates the typed split to `reThrowDecorated` (which
1888
+ // preserves the existing error class' wire-metadata fields).
1654
1889
  const existing = remapped.details ?? {};
1655
1890
  const decoration = {
1656
1891
  ...existing,
@@ -1659,33 +1894,7 @@ export const runItemUpdateBulkFileDispatch = async (inputs) => {
1659
1894
  failed_at_item: itemId,
1660
1895
  matched_count: inputs.matchedItemIds.length,
1661
1896
  };
1662
- if (remapped.code === 'usage_error') {
1663
- throw new UsageError(remapped.message, {
1664
- ...(remapped.cause === undefined
1665
- ? {}
1666
- : { cause: remapped.cause }),
1667
- details: decoration,
1668
- });
1669
- }
1670
- throw new ApiError(remapped.code, remapped.message, {
1671
- ...(remapped.cause === undefined
1672
- ? {}
1673
- : { cause: remapped.cause }),
1674
- ...(remapped.httpStatus === undefined
1675
- ? {}
1676
- : { httpStatus: remapped.httpStatus }),
1677
- ...(remapped.mondayCode === undefined
1678
- ? {}
1679
- : { mondayCode: remapped.mondayCode }),
1680
- ...(remapped.requestId === undefined
1681
- ? {}
1682
- : { requestId: remapped.requestId }),
1683
- retryable: remapped.retryable,
1684
- ...(remapped.retryAfterSeconds === undefined
1685
- ? {}
1686
- : { retryAfterSeconds: remapped.retryAfterSeconds }),
1687
- details: decoration,
1688
- });
1897
+ reThrowDecorated(remapped, decoration);
1689
1898
  }
1690
1899
  // Non-CliError programmer bug — re-throw to the runner's
1691
1900
  // catch-all (surfaces as `internal_error` whole-call;
@@ -1879,4 +2088,497 @@ export const runItemUpdateBulkFileDispatch = async (inputs) => {
1879
2088
  resolvedIds,
1880
2089
  });
1881
2090
  };
2091
+ /**
2092
+ * Single-item multi-file `--set` dispatch helper (v0.8-M46 D2
2093
+ * carve-out fold). Runtime body shipped at v0.8-M46 IMPL.
2094
+ *
2095
+ * **Execution shape:**
2096
+ *
2097
+ * 1. Single upfront `precheckLocalFile` per file path (N pre-checks
2098
+ * in argv order; D3 closure). Any pre-check failure aborts the
2099
+ * whole call with `usage_error` (`file_not_readable` /
2100
+ * `file_empty`) BEFORE any wire round-trip fires — atomicity-
2101
+ * before-wire per cli-design §5.8.
2102
+ * 2. Dry-run branch — N `add_file_to_column` planned_changes,
2103
+ * `source: 'none'` (pure-local, mirrors M38 single-item
2104
+ * dry-run); no multipart wire fires.
2105
+ * 3. Live branch — sequential N legs via
2106
+ * {@link dispatchFileLegsSequentially} (R-v0.8-NEW-1 shared
2107
+ * helper) against the single `inputs.itemId`. On partial
2108
+ * failure, `invalidateBoard` (any landed legs mutated the
2109
+ * board's asset state wire-side) then `internal_error` with
2110
+ * `details.reason: 'multi_file_update_partial_failure'` +
2111
+ * `details.item_id` + `details.applied_file_columns` (length
2112
+ * 0..N-1) + `details.failed_file_column` + `details.cause`
2113
+ * (M31 wire-failure surface, JSON projection) + `details.hint`.
2114
+ * Unlike the bulk + create paths, the single-item path does NOT
2115
+ * run `foldAndRemap` on the failing leg's cause — it mirrors the
2116
+ * M38 single-item precedent (`runItemUpdateSingleFileDispatch`
2117
+ * doesn't remap either); an archived file column is already
2118
+ * rejected at the pre-check's `includeArchived` archived-column
2119
+ * guard before dispatch.
2120
+ * 4. Full success — single `invalidateBoard` then envelope emit
2121
+ * per `fileColumnSetMultiOutputSchema`
2122
+ * (`operation: 'add_files_to_columns'` + `assets: [...]` +
2123
+ * `applied_file_columns: [...]`, both length N).
2124
+ */
2125
+ const runItemUpdateSingleFileMultiDispatch = async (inputs) => {
2126
+ // 1) Upfront pre-check per file path, in argv order. A bad path
2127
+ // surfaces `usage_error` (exit 1) whole-call-abort before any
2128
+ // multipart wire leg fires (cli-design §5.8). R-v0.6-NEW-1
2129
+ // `precheckLocalFile` called N times (one per file column).
2130
+ const legEntries = [];
2131
+ for (const entry of inputs.m38.entries) {
2132
+ const precheck = await precheckLocalFile(entry.rawValue);
2133
+ legEntries.push({
2134
+ columnId: entry.columnId,
2135
+ rawValue: entry.rawValue,
2136
+ filePath: precheck.filePath,
2137
+ filename: precheck.filename,
2138
+ fileSizeBytes: precheck.fileSizeBytes,
2139
+ });
2140
+ }
2141
+ const warnings = inputs.m38.warnings.map((w) => ({
2142
+ code: w.code,
2143
+ message: w.message,
2144
+ details: w.details,
2145
+ }));
2146
+ const resolvedIds = Object.fromEntries(inputs.m38.entries.map((e) => [e.token, e.columnId]));
2147
+ // 2) Dry-run branch — N planned_changes; pure-local, `source: 'none'`
2148
+ // (mirrors M38 single-item dry-run).
2149
+ if (inputs.isDryRun) {
2150
+ emitDryRun({
2151
+ ctx: inputs.ctx,
2152
+ programOpts: inputs.programOpts,
2153
+ plannedChanges: legEntries.map((leg) => ({
2154
+ operation: 'add_file_to_column',
2155
+ item_id: inputs.itemId,
2156
+ column_id: leg.columnId,
2157
+ file_path: leg.rawValue,
2158
+ filename: leg.filename,
2159
+ file_size_bytes: leg.fileSizeBytes,
2160
+ })),
2161
+ source: 'none',
2162
+ cacheAgeSeconds: null,
2163
+ warnings,
2164
+ apiVersion: inputs.apiVersion,
2165
+ });
2166
+ return;
2167
+ }
2168
+ // 3) Live branch — sequential N-leg fan-out against the single item.
2169
+ const dispatch = await dispatchFileLegsSequentially({
2170
+ client: inputs.client,
2171
+ multipart: inputs.multipart,
2172
+ itemId: inputs.itemId,
2173
+ entries: legEntries,
2174
+ signal: inputs.ctx.signal,
2175
+ retries: inputs.retries,
2176
+ });
2177
+ if (dispatch.failure !== undefined) {
2178
+ // Partial failure. If any leg landed, the board's asset state
2179
+ // mutated wire-side — invalidate before re-throwing so a
2180
+ // follow-up read doesn't serve stale metadata (mirrors M42's
2181
+ // fail-fast invalidate-on-partial-apply).
2182
+ if (dispatch.appliedColumns.length > 0) {
2183
+ await invalidateBoard(inputs.boardId, inputs.ctx.env);
2184
+ }
2185
+ const cause = dispatch.failure.cause;
2186
+ const causeProjection = projectCauseForEnvelope(cause);
2187
+ throw new ApiError('internal_error', `Multi-file \`--set\` on item ${inputs.itemId} partially failed: ` +
2188
+ `${String(dispatch.appliedColumns.length)} of ` +
2189
+ `${String(legEntries.length)} file column(s) landed before column ` +
2190
+ `${dispatch.failure.failedColumn} failed ` +
2191
+ `(${cause.code}: ${cause.message}). The applied file columns ` +
2192
+ `persist on Monday; retry only the unfailed columns with ` +
2193
+ `\`monday item set ${inputs.itemId} <file-col>=<path>\`, or reissue ` +
2194
+ `all ${String(legEntries.length)} \`--set\` entries.`, {
2195
+ cause,
2196
+ details: {
2197
+ reason: 'multi_file_update_partial_failure',
2198
+ item_id: inputs.itemId,
2199
+ applied_file_columns: dispatch.appliedColumns,
2200
+ failed_file_column: dispatch.failure.failedColumn,
2201
+ cause: causeProjection,
2202
+ hint: `${String(dispatch.appliedColumns.length)} file column(s) ` +
2203
+ `already landed on item ${inputs.itemId}; retry the unfailed ` +
2204
+ `columns alone with \`monday item set ${inputs.itemId} ` +
2205
+ `<file-col>=<path>\`, or reissue every \`--set\` entry (the ` +
2206
+ `landed columns will be overwritten with the same files).`,
2207
+ },
2208
+ });
2209
+ }
2210
+ // 4) Full success — single board-cache invalidate before emit
2211
+ // (mirrors M38 single-leg invalidate timing; one board covers
2212
+ // every leg's mutated asset slot).
2213
+ await invalidateBoard(inputs.boardId, inputs.ctx.env);
2214
+ const data = {
2215
+ operation: 'add_files_to_columns',
2216
+ item_id: inputs.itemId,
2217
+ assets: dispatch.assets.map((a) => ({
2218
+ column_id: a.column_id,
2219
+ filename: a.filename,
2220
+ file_size_bytes: a.file_size_bytes,
2221
+ asset: a.asset,
2222
+ })),
2223
+ applied_file_columns: [...dispatch.appliedColumns],
2224
+ };
2225
+ emitMutation({
2226
+ ctx: inputs.ctx,
2227
+ data,
2228
+ schema: fileColumnSetMultiOutputSchema,
2229
+ programOpts: inputs.programOpts,
2230
+ warnings,
2231
+ ...inputs.toEmit({
2232
+ data: dispatch.assets,
2233
+ complexity: dispatch.lastComplexity,
2234
+ stats: { attempts: 1, totalBackoffMs: 0 },
2235
+ }),
2236
+ source: 'live',
2237
+ cacheAgeSeconds: null,
2238
+ complexity: dispatch.lastComplexity,
2239
+ resolvedIds,
2240
+ });
2241
+ };
2242
+ /**
2243
+ * Bulk multi-file `--set` per-item dispatch helper (v0.8-M46 D2
2244
+ * carve-out fold). Runtime body shipped at v0.8-M46 IMPL.
2245
+ *
2246
+ * **Execution shape (extends M42's `runItemUpdateBulkFileDispatch`
2247
+ * single-file bulk path to N file legs per item):**
2248
+ *
2249
+ * 1. Single upfront `precheckLocalFile` pass over ALL N file paths
2250
+ * (D3 — N pre-checks total, shared across the M matched items,
2251
+ * NOT N×M). Any failure aborts the whole call with `usage_error`
2252
+ * regardless of `--continue-on-error` per cli-design §5.8.
2253
+ * 2. Dry-run branch — N×M `add_file_to_column` planned_changes
2254
+ * (one per (item, file column) pair); source aggregates the
2255
+ * upstream metadata + items_page legs per M42's pattern.
2256
+ * 3. Live branch — cross-item parallel via M42's `dispatchParallel`
2257
+ * / `dispatchSequential` (under `--concurrency`) × within-item
2258
+ * sequential N legs via {@link dispatchFileLegsSequentially}
2259
+ * (D1). Two shapes per `parsed.continueOnError`:
2260
+ * - **Fail-fast (default)** — sequential over matched items;
2261
+ * first per-item failure aborts whole-call with
2262
+ * `details.applied_to` (items where ALL legs landed) +
2263
+ * `details.applied_file_columns_per_item` (the failed item's
2264
+ * partial-leg map) + `details.failed_at_item` +
2265
+ * `details.failed_file_column`.
2266
+ * - **`--continue-on-error`** — per-item failures land as
2267
+ * `data.results[i].error` extended with
2268
+ * `applied_file_columns` + `failed_file_column`; partially-
2269
+ * landed assets carry on `data.results[i].assets`.
2270
+ * 4. Post-dispatch `invalidateBoard` + emit per
2271
+ * `bulkFileSetMultiDataSchema`
2272
+ * (`operation: 'item_update_bulk_file_set_multi'` +
2273
+ * `summary.{file_count, file_column_ids}`).
2274
+ *
2275
+ * Per-item failures route through `foldAndRemap` BEFORE the per-item
2276
+ * record / fail-fast decoration so the stable-code rule (cli-design
2277
+ * §6.5) surfaces `column_archived` for cache-served file-column
2278
+ * resolution against an archived column — mirrors M42's bulk
2279
+ * single-file remap.
2280
+ */
2281
+ const runItemUpdateBulkFileMultiDispatch = async (inputs) => {
2282
+ // 1) Single upfront pre-check pass over ALL N file paths (D3).
2283
+ // Shared across the M matched items — N pre-checks, NOT N×M.
2284
+ // Whole-call abort regardless of `--continue-on-error` per
2285
+ // cli-design §5.8 (`precheckLocalFile` throws `usage_error`
2286
+ // with `file_not_readable` / `file_empty`).
2287
+ const legEntries = [];
2288
+ for (const entry of inputs.m38.entries) {
2289
+ const precheck = await precheckLocalFile(entry.rawValue);
2290
+ legEntries.push({
2291
+ columnId: entry.columnId,
2292
+ rawValue: entry.rawValue,
2293
+ filePath: precheck.filePath,
2294
+ filename: precheck.filename,
2295
+ fileSizeBytes: precheck.fileSizeBytes,
2296
+ });
2297
+ }
2298
+ const fileColumnIds = legEntries.map((leg) => leg.columnId);
2299
+ const combinedWarnings = dedupeWarnings([
2300
+ ...inputs.filterWarnings,
2301
+ ...inputs.m38.warnings,
2302
+ ]);
2303
+ const resolvedIds = Object.fromEntries(inputs.m38.entries.map((e) => [e.token, e.columnId]));
2304
+ // Source aggregator — mirrors M42's bulk single-file pattern.
2305
+ // Seeds the metadata leg, folds the M38 pre-check leg + a synthetic
2306
+ // 'live' leg for the items_page walker (always fired upstream).
2307
+ const sourceAgg = new SourceAggregator({
2308
+ source: inputs.metaSource,
2309
+ cacheAgeSeconds: inputs.metaCacheAgeSeconds,
2310
+ });
2311
+ if (inputs.m38.source !== undefined) {
2312
+ sourceAgg.record(inputs.m38.source, inputs.m38.cacheAgeSeconds);
2313
+ }
2314
+ sourceAgg.record('live', null);
2315
+ // 2) Dry-run branch — N×M planned_changes (one `add_file_to_column`
2316
+ // per (item, file column) pair). No file bytes loaded, no
2317
+ // multipart wire. Source carries the aggregated upstream legs
2318
+ // (mirrors M42's bulk dry-run, which is NOT pure-local).
2319
+ if (inputs.isDryRun) {
2320
+ const plannedChanges = inputs.matchedItemIds.flatMap((itemId) => legEntries.map((leg) => ({
2321
+ operation: 'add_file_to_column',
2322
+ item_id: itemId,
2323
+ column_id: leg.columnId,
2324
+ file_path: leg.rawValue,
2325
+ filename: leg.filename,
2326
+ file_size_bytes: leg.fileSizeBytes,
2327
+ })));
2328
+ const dryRunAgg = sourceAgg.result();
2329
+ emitDryRun({
2330
+ ctx: inputs.ctx,
2331
+ programOpts: inputs.programOpts,
2332
+ plannedChanges,
2333
+ source: dryRunAgg.source,
2334
+ cacheAgeSeconds: dryRunAgg.cacheAgeSeconds,
2335
+ warnings: combinedWarnings,
2336
+ apiVersion: inputs.apiVersion,
2337
+ });
2338
+ return;
2339
+ }
2340
+ // 3) Live dispatch. Record the dispatch leg as 'live'.
2341
+ sourceAgg.record('live', null);
2342
+ const liveAgg = sourceAgg.result();
2343
+ const remapSource = inputs.m38.source ?? 'live';
2344
+ const fileCount = legEntries.length;
2345
+ const continueOnError = inputs.parsed.continueOnError === true;
2346
+ if (!continueOnError) {
2347
+ // Fail-fast bulk multi-file dispatch. Sequential over matched
2348
+ // items (M30 D2: `--concurrency requires --continue-on-error`).
2349
+ // Each item fans out N sequential file legs; first per-item
2350
+ // failure aborts whole-call.
2351
+ const fullyApplied = [];
2352
+ for (const itemId of inputs.matchedItemIds) {
2353
+ const dispatch = await dispatchFileLegsSequentially({
2354
+ client: inputs.client,
2355
+ multipart: inputs.multipart,
2356
+ itemId,
2357
+ entries: legEntries,
2358
+ signal: inputs.ctx.signal,
2359
+ retries: inputs.retries,
2360
+ });
2361
+ if (dispatch.failure !== undefined) {
2362
+ // Any prior item fully applied OR this item landed ≥1 leg →
2363
+ // the board's asset state mutated wire-side; invalidate
2364
+ // before re-throwing (mirrors M42 fail-fast invalidate).
2365
+ if (fullyApplied.length > 0 || dispatch.appliedColumns.length > 0) {
2366
+ await invalidateBoard(inputs.boardId, inputs.ctx.env);
2367
+ }
2368
+ const remapped = await foldAndRemap({
2369
+ err: dispatch.failure.cause,
2370
+ warnings: inputs.m38.warnings,
2371
+ client: inputs.client,
2372
+ boardId: inputs.boardId,
2373
+ columnIds: fileColumnIds,
2374
+ env: inputs.ctx.env,
2375
+ noCache: inputs.noCache,
2376
+ resolutionSource: remapSource,
2377
+ });
2378
+ const existing = remapped.details ?? {};
2379
+ const decoration = {
2380
+ ...existing,
2381
+ applied_count: fullyApplied.length,
2382
+ applied_to: fullyApplied.map((a) => a.itemId),
2383
+ applied_file_columns_per_item: {
2384
+ [itemId]: dispatch.appliedColumns,
2385
+ },
2386
+ failed_at_item: itemId,
2387
+ failed_file_column: dispatch.failure.failedColumn,
2388
+ matched_count: inputs.matchedItemIds.length,
2389
+ file_count: fileCount,
2390
+ file_column_ids: fileColumnIds,
2391
+ };
2392
+ reThrowDecorated(remapped, decoration);
2393
+ }
2394
+ fullyApplied.push({ itemId, assets: dispatch.assets });
2395
+ }
2396
+ // Every item applied all N legs — single board-cache invalidate
2397
+ // before emit.
2398
+ await invalidateBoard(inputs.boardId, inputs.ctx.env);
2399
+ const results = fullyApplied.map(({ itemId, assets }) => ({
2400
+ item_id: itemId,
2401
+ ok: true,
2402
+ assets: assets.map((a) => ({
2403
+ column_id: a.column_id,
2404
+ filename: a.filename,
2405
+ file_size_bytes: a.file_size_bytes,
2406
+ asset: a.asset,
2407
+ })),
2408
+ applied_file_columns: [...fileColumnIds],
2409
+ }));
2410
+ const data = {
2411
+ operation: 'item_update_bulk_file_set_multi',
2412
+ summary: {
2413
+ matched_count: inputs.matchedItemIds.length,
2414
+ applied_count: fullyApplied.length,
2415
+ failed_count: 0,
2416
+ board_id: inputs.boardId,
2417
+ file_count: fileCount,
2418
+ file_column_ids: [...fileColumnIds],
2419
+ },
2420
+ results,
2421
+ };
2422
+ emitMutation({
2423
+ ctx: inputs.ctx,
2424
+ data,
2425
+ schema: bulkFileSetMultiDataSchema,
2426
+ programOpts: inputs.programOpts,
2427
+ warnings: combinedWarnings,
2428
+ source: liveAgg.source,
2429
+ cacheAgeSeconds: liveAgg.cacheAgeSeconds,
2430
+ apiVersion: inputs.apiVersion,
2431
+ resolvedIds,
2432
+ });
2433
+ return;
2434
+ }
2435
+ // `--continue-on-error` path. Per-item failures land as per-record
2436
+ // slots; the envelope is `ok: true` regardless of how many items
2437
+ // failed (universal partial-success rule). Each item's within-item
2438
+ // fan-out is sequential; a partial mid-multi-leg failure records
2439
+ // the landed assets + applied_file_columns + failed_file_column on
2440
+ // the per-item record. `internal_error` (or non-CliError) re-throws
2441
+ // whole-call via the shared dispatcher's escape hatch.
2442
+ const successById = new Map();
2443
+ const partialById = new Map();
2444
+ const perTargetDispatch = async ({ targetId, }) => {
2445
+ const dispatch = await dispatchFileLegsSequentially({
2446
+ client: inputs.client,
2447
+ multipart: inputs.multipart,
2448
+ itemId: targetId,
2449
+ entries: legEntries,
2450
+ signal: inputs.ctx.signal,
2451
+ retries: inputs.retries,
2452
+ });
2453
+ if (dispatch.failure !== undefined) {
2454
+ // Record the partial state (landed assets + applied columns +
2455
+ // failing column) keyed by item_id, then re-throw the remapped
2456
+ // error so the shared dispatcher captures it as `ok: false` +
2457
+ // `error: {code, message}`. `foldAndRemap` NEVER converts a
2458
+ // non-`internal_error` into `internal_error`, so the
2459
+ // dispatcher's `internal_error` escape hatch stays intact.
2460
+ partialById.set(targetId, {
2461
+ assets: dispatch.assets,
2462
+ appliedColumns: dispatch.appliedColumns,
2463
+ failedColumn: dispatch.failure.failedColumn,
2464
+ });
2465
+ const remapped = await foldAndRemap({
2466
+ err: dispatch.failure.cause,
2467
+ warnings: inputs.m38.warnings,
2468
+ client: inputs.client,
2469
+ boardId: inputs.boardId,
2470
+ columnIds: fileColumnIds,
2471
+ env: inputs.ctx.env,
2472
+ noCache: inputs.noCache,
2473
+ resolutionSource: remapSource,
2474
+ });
2475
+ throw remapped;
2476
+ }
2477
+ successById.set(targetId, dispatch.assets);
2478
+ };
2479
+ let dispatchResults;
2480
+ if (inputs.parsed.concurrency !== undefined &&
2481
+ inputs.parsed.concurrency > 1) {
2482
+ dispatchResults = await dispatchParallel(inputs.matchedItemIds, 'item_id', perTargetDispatch, inputs.parsed.concurrency, inputs.ctx.signal);
2483
+ }
2484
+ else {
2485
+ dispatchResults = await dispatchSequential(inputs.matchedItemIds, 'item_id', perTargetDispatch, inputs.ctx.signal);
2486
+ }
2487
+ // Single post-dispatch invalidate (fires even when every item
2488
+ // failed — wire calls still fired against Monday).
2489
+ await invalidateBoard(inputs.boardId, inputs.ctx.env);
2490
+ const results = dispatchResults.map((row) => {
2491
+ const itemIdSlot = row.item_id;
2492
+ /* c8 ignore next 8 — dispatcher contract: every result row carries
2493
+ the id-field slot; this guard catches a contract violation that
2494
+ would surface as a programmer bug, not a Monday-side failure. */
2495
+ if (typeof itemIdSlot !== 'string' || itemIdSlot.length === 0) {
2496
+ throw new ApiError('internal_error', 'bulk multi-file dispatch result row is missing the `item_id` field — dispatcher contract violation.', { details: { record_keys: Object.keys(row) } });
2497
+ }
2498
+ if (row.ok) {
2499
+ const assets = successById.get(itemIdSlot);
2500
+ /* c8 ignore next 8 — side-map invariant: every successful
2501
+ per-target dispatch records its assets; this guard catches a
2502
+ wrapper-layer miss (programmer bug). */
2503
+ if (assets === undefined) {
2504
+ throw new ApiError('internal_error', `bulk multi-file dispatch result row for item_id ${itemIdSlot} reported ok: true but no assets were captured — wrapper-layer side-map miss.`, { details: { item_id: itemIdSlot } });
2505
+ }
2506
+ return {
2507
+ item_id: itemIdSlot,
2508
+ ok: true,
2509
+ assets: assets.map((a) => ({
2510
+ column_id: a.column_id,
2511
+ filename: a.filename,
2512
+ file_size_bytes: a.file_size_bytes,
2513
+ asset: a.asset,
2514
+ })),
2515
+ applied_file_columns: [...fileColumnIds],
2516
+ };
2517
+ }
2518
+ /* c8 ignore next 8 — dispatcher contract: every `ok: false` row
2519
+ carries the `error` slot (populated by the shared dispatcher's
2520
+ per-target error decoration). */
2521
+ if (row.error === undefined) {
2522
+ throw new ApiError('internal_error', `bulk multi-file dispatch result row for item_id ${itemIdSlot} reported ok: false but no error payload was captured — dispatcher contract violation.`, { details: { item_id: itemIdSlot } });
2523
+ }
2524
+ const partial = partialById.get(itemIdSlot);
2525
+ /* c8 ignore next 8 — side-map invariant: every per-target failure
2526
+ routes through `perTargetDispatch`'s `partialById.set` before
2527
+ re-throwing; a miss is a wrapper-layer programmer bug. */
2528
+ if (partial === undefined) {
2529
+ throw new ApiError('internal_error', `bulk multi-file dispatch result row for item_id ${itemIdSlot} reported ok: false but no partial-leg state was captured — wrapper-layer side-map miss.`, { details: { item_id: itemIdSlot } });
2530
+ }
2531
+ return {
2532
+ item_id: itemIdSlot,
2533
+ ok: false,
2534
+ assets: partial.assets.map((a) => ({
2535
+ column_id: a.column_id,
2536
+ filename: a.filename,
2537
+ file_size_bytes: a.file_size_bytes,
2538
+ asset: a.asset,
2539
+ })),
2540
+ applied_file_columns: [...partial.appliedColumns],
2541
+ failed_file_column: partial.failedColumn,
2542
+ error: { code: row.error.code, message: row.error.message },
2543
+ };
2544
+ });
2545
+ const appliedCount = results.filter((r) => r.ok).length;
2546
+ const failedCount = results.filter((r) => !r.ok).length;
2547
+ /* c8 ignore next 11 — invariant: every matched item produces exactly
2548
+ one result row under both dispatchers; mismatch indicates a
2549
+ programmer bug in the dispatcher or the fold. */
2550
+ if (appliedCount + failedCount !== inputs.matchedItemIds.length) {
2551
+ throw new ApiError('internal_error', `bulk multi-file dispatch summary invariant violated — matched_count (${String(inputs.matchedItemIds.length)}) !== applied_count (${String(appliedCount)}) + failed_count (${String(failedCount)}).`, {
2552
+ details: {
2553
+ matched_count: inputs.matchedItemIds.length,
2554
+ applied_count: appliedCount,
2555
+ failed_count: failedCount,
2556
+ board_id: inputs.boardId,
2557
+ },
2558
+ });
2559
+ }
2560
+ const data = {
2561
+ operation: 'item_update_bulk_file_set_multi',
2562
+ summary: {
2563
+ matched_count: inputs.matchedItemIds.length,
2564
+ applied_count: appliedCount,
2565
+ failed_count: failedCount,
2566
+ board_id: inputs.boardId,
2567
+ file_count: fileCount,
2568
+ file_column_ids: [...fileColumnIds],
2569
+ },
2570
+ results,
2571
+ };
2572
+ emitMutation({
2573
+ ctx: inputs.ctx,
2574
+ data,
2575
+ schema: bulkFileSetMultiDataSchema,
2576
+ programOpts: inputs.programOpts,
2577
+ warnings: combinedWarnings,
2578
+ source: liveAgg.source,
2579
+ cacheAgeSeconds: liveAgg.cacheAgeSeconds,
2580
+ apiVersion: inputs.apiVersion,
2581
+ resolvedIds,
2582
+ });
2583
+ };
1882
2584
  //# sourceMappingURL=update.js.map