@myvillage/cli 1.73.0 → 1.74.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": "@myvillage/cli",
3
- "version": "1.73.0",
3
+ "version": "1.74.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,8 @@ import {
19
19
  ingestAsset,
20
20
  readRows,
21
21
  freezeVersion,
22
+ deleteRow,
23
+ updateRow,
22
24
  } from '../utils/dataset-workspace.js';
23
25
 
24
26
  // ── create ─────────────────────────────────────────────
@@ -176,6 +178,55 @@ export async function datasetFreezeCommand(slug, options) {
176
178
  }
177
179
  }
178
180
 
181
+ // ── delete a row ───────────────────────────────────────
182
+
183
+ export async function datasetRowDeleteCommand(options) {
184
+ try {
185
+ const slug = options.dataset;
186
+ if (!slug) throw new Error('--dataset <slug> is required.');
187
+ if (!options.id) throw new Error('--id <rowId> is required.');
188
+ const result = deleteRow(slug, options.id);
189
+ if (options.json) {
190
+ console.log(JSON.stringify(result, null, 2));
191
+ return;
192
+ }
193
+ success(`Deleted row ${options.id} from ${slug}`);
194
+ info(` total: ${result.stats.rowCount} (train ${result.stats.bySplit.train} / val ${result.stats.bySplit.val})`);
195
+ } catch (e) {
196
+ error(e.message);
197
+ process.exitCode = 1;
198
+ }
199
+ }
200
+
201
+ // ── update a row ───────────────────────────────────────
202
+
203
+ export async function datasetRowUpdateCommand(options) {
204
+ try {
205
+ const slug = options.dataset;
206
+ if (!slug) throw new Error('--dataset <slug> is required.');
207
+ if (!options.id) throw new Error('--id <rowId> is required.');
208
+ const updates = {};
209
+ if (options.features !== undefined) updates.features = parseFeatures(options.features);
210
+ else if (options.text !== undefined) updates.features = options.text;
211
+ if (options.label !== undefined) updates.label = options.label;
212
+ if (options.target !== undefined) updates.target = Number(options.target);
213
+ if (options.split !== undefined) updates.split = options.split;
214
+ if (Object.keys(updates).length === 0) {
215
+ throw new Error('Nothing to update. Pass --text/--features, --label, --target, or --split.');
216
+ }
217
+ const result = updateRow(slug, options.id, updates);
218
+ if (options.json) {
219
+ console.log(JSON.stringify(result, null, 2));
220
+ return;
221
+ }
222
+ success(`Updated row ${options.id} in ${slug}`);
223
+ info(` total: ${result.stats.rowCount} (train ${result.stats.bySplit.train} / val ${result.stats.bySplit.val})`);
224
+ } catch (e) {
225
+ error(e.message);
226
+ process.exitCode = 1;
227
+ }
228
+ }
229
+
179
230
  // ── helpers ────────────────────────────────────────────
180
231
 
181
232
  function readRowsFromFile(path) {
package/src/index.js CHANGED
@@ -124,6 +124,8 @@ import {
124
124
  datasetAppendCommand,
125
125
  datasetShowCommand,
126
126
  datasetFreezeCommand,
127
+ datasetRowDeleteCommand,
128
+ datasetRowUpdateCommand,
127
129
  } from './commands/dataset.js';
128
130
  import {
129
131
  modelTrainCommand,
@@ -900,6 +902,27 @@ export function run() {
900
902
  .option('--json', 'Output raw JSON')
901
903
  .action(datasetFreezeCommand);
902
904
 
905
+ datasetCmd
906
+ .command('rm-row')
907
+ .description('Delete a row from the working set (by id)')
908
+ .requiredOption('--dataset <slug>', 'Target dataset slug')
909
+ .requiredOption('--id <rowId>', 'Row id to delete')
910
+ .option('--json', 'Output raw JSON')
911
+ .action(datasetRowDeleteCommand);
912
+
913
+ datasetCmd
914
+ .command('update-row')
915
+ .description('Update a working-set row (by id)')
916
+ .requiredOption('--dataset <slug>', 'Target dataset slug')
917
+ .requiredOption('--id <rowId>', 'Row id to update')
918
+ .option('--text <text>', 'New text features')
919
+ .option('--features <value>', 'New features (string, or numeric vector for tabular)')
920
+ .option('--label <label>', 'New class label (classification)')
921
+ .option('--target <number>', 'New numeric target (regression)')
922
+ .option('--split <split>', 'train | val')
923
+ .option('--json', 'Output raw JSON')
924
+ .action(datasetRowUpdateCommand);
925
+
903
926
  // ── Studio Lab: model training (local, TF.js CPU engine) ───────────────
904
927
  const modelCmd = program
905
928
  .command('model')
@@ -308,6 +308,93 @@ export function readRows(slug, { version, split, limit } = {}) {
308
308
  return out;
309
309
  }
310
310
 
311
+ // ── Edit / delete working-set rows ─────────────────────
312
+ //
313
+ // Only the working set (current.jsonl) is mutable; frozen versions (v*.jsonl)
314
+ // are immutable snapshots and are never touched here. Both ops rewrite the
315
+ // whole working-set file and recompute manifest stats from the result, so the
316
+ // manifest can never drift from the rows on disk.
317
+
318
+ /** Recompute manifest stats (rowCount / bySplit / byLabel) from a row array. */
319
+ function recomputeStats(rows) {
320
+ const stats = emptyStats();
321
+ for (const row of rows) {
322
+ stats.rowCount += 1;
323
+ if (stats.bySplit[row.split] != null) stats.bySplit[row.split] += 1;
324
+ if (row.label != null) {
325
+ stats.byLabel[row.label] = (stats.byLabel[row.label] || 0) + 1;
326
+ }
327
+ }
328
+ return stats;
329
+ }
330
+
331
+ /** Overwrite the working set with `rows` and refresh the manifest's stats. */
332
+ function writeWorkingSet(slug, manifest, rows) {
333
+ const body = rows.map((r) => JSON.stringify(r)).join('\n');
334
+ writeFileSync(getCurrentDataFile(slug), rows.length ? body + '\n' : '');
335
+ manifest.stats = recomputeStats(rows);
336
+ return writeManifest(slug, manifest);
337
+ }
338
+
339
+ /**
340
+ * Delete one working-set row by id. Throws if the id isn't in current.jsonl
341
+ * (e.g. it only exists in a frozen version, which can't be edited).
342
+ * @returns {{datasetId:string, deleted:string, stats:object}}
343
+ */
344
+ export function deleteRow(slug, rowId) {
345
+ const manifest = readManifest(slug);
346
+ if (!manifest) throw new Error(`Dataset "${slug}" not found.`);
347
+ if (!rowId) throw new Error('A row id is required.');
348
+ const rows = readRows(slug);
349
+ const idx = rows.findIndex((r) => r.id === rowId);
350
+ if (idx === -1) {
351
+ throw new Error(`Row "${rowId}" not found in the working set of "${slug}".`);
352
+ }
353
+ const [removed] = rows.splice(idx, 1);
354
+ writeWorkingSet(slug, manifest, rows);
355
+ return { datasetId: slug, deleted: removed.id, stats: manifest.stats };
356
+ }
357
+
358
+ /**
359
+ * Update one working-set row by id. `updates` is a partial
360
+ * {features, label, target, split}; the merged row is re-validated against the
361
+ * dataset's modality/task (same rules as append). The row id is preserved and
362
+ * provenance.editedAt is stamped. Image/audio asset refs can't be edited here —
363
+ * delete the row and re-add the asset instead.
364
+ * @returns {{datasetId:string, updated:string, stats:object}}
365
+ */
366
+ export function updateRow(slug, rowId, updates = {}) {
367
+ const manifest = readManifest(slug);
368
+ if (!manifest) throw new Error(`Dataset "${slug}" not found.`);
369
+ if (!rowId) throw new Error('A row id is required.');
370
+ const rows = readRows(slug);
371
+ const idx = rows.findIndex((r) => r.id === rowId);
372
+ if (idx === -1) {
373
+ throw new Error(`Row "${rowId}" not found in the working set of "${slug}".`);
374
+ }
375
+ const existing = rows[idx];
376
+ if ((manifest.modality === 'image' || manifest.modality === 'audio') && updates.features !== undefined) {
377
+ throw new Error(`Can't edit the asset of an ${manifest.modality} row — delete it and re-add.`);
378
+ }
379
+ const merged = normalizeRow(
380
+ {
381
+ features: updates.features !== undefined ? updates.features : existing.features,
382
+ split: updates.split !== undefined ? updates.split : existing.split,
383
+ label: updates.label !== undefined ? updates.label : existing.label,
384
+ target: updates.target !== undefined ? updates.target : existing.target,
385
+ },
386
+ manifest,
387
+ );
388
+ merged.id = existing.id;
389
+ merged.provenance = { ...(existing.provenance || {}), editedAt: nowISO() };
390
+ if (merged.label != null && !manifest.labels.includes(merged.label)) {
391
+ manifest.labels.push(merged.label);
392
+ }
393
+ rows[idx] = merged;
394
+ writeWorkingSet(slug, manifest, rows);
395
+ return { datasetId: slug, updated: merged.id, stats: manifest.stats };
396
+ }
397
+
311
398
  // ── Freeze a version ───────────────────────────────────
312
399
 
313
400
  /**