Skip to content

feat(import): --atomic CSV import via commitWorkspaceTransaction (all-or-nothing, resumable) [pm-cli 2026.7.19] - #35

Merged
unbraind merged 6 commits into
mainfrom
feat/atomic-csv-import
Jul 19, 2026
Merged

unbraind merged 6 commits into
mainfrom
feat/atomic-csv-import

Conversation

@unbraind

@unbraind unbraind commented Jul 19, 2026 •

Copy link
Copy Markdown
Owner

What

Adds an opt-in --atomic mode to pm csv import that commits an entire CSV batch through the pm-cli 2026.7.19 public SDK primitive commitWorkspaceTransaction — all-or-nothing, crash-recoverable, and multi-agent-safe. Ships as 2026.7.19-1 (this feature lands after today's 06:10 UTC daily release of 2026.7.19).

Today the importer creates items with a per-row pm create shell-out: if row N fails mid-batch, rows 1..N-1 are already written (partial, non-atomic state), and two concurrent agents/imports can interleave writes. With --atomic:

  • Every row create/update becomes one WorkspaceTransactionStep (inspect / apply / compensate), committed under a single workspace writer lock.
  • A mid-batch failure compensates every already-applied create (closes them) → the tracker is left unchanged (all-or-nothing). Updates to pre-existing items are intentionally not rolled back (documented).
  • A stable transactionId derived from the input file makes an interrupted import resumable.

Default (non---atomic) behaviour is byte-identical to before. Requires @unbrained/pm-cli >= 2026.7.19. --atomic + --stream is rejected (an unbounded stream cannot be one atomic transaction).

Review-round-2 hardening (commit 83125ab)

Addresses every finding from Greptile (3/5 blocker), Gemini, and Sourcery:

  • Correctness (blocker): resume/compensation is now per-row-precise via a csv-txrow:<transactionId>#<rowIndex> ownership marker (the batch-level csv-tx: marker is kept only for scanning; the old byTitle/byKey matching is dropped). A CSV with duplicate titles or duplicate keys can no longer trick inspect() into skipping the wrong row or compensate() into closing the wrong item.
  • In-batch duplicate---key guard: a repeated not-yet-existing key is skipped with a clear per-row warning instead of double-creating.
  • Perf (Sourcery): rowFields() is computed once per row (was twice).
  • Robustness (Sourcery): resolveCommitWorkspaceTransaction wraps the dynamic SDK import in try/catch → an actionable CommandError, and caches the resolved function across --atomic calls.

Testing

  • npm run release:check clean (typecheck, build, tests, audit:prod, pack:dry-run, changelog:check).
  • 102 tests pass (12 atomic). New coverage: duplicate-title creates + partial resume via the per-row tag; --atomic --key upsert with mid-import failure (creates compensated, pre-existing updates not rolled back); resume detection via per-row tag; in-batch duplicate-key guard.

PM tracking

🤖 Generated with Claude Code

…-or-nothing, resumable) [pm-cli 2026.7.19]

Add an opt-in --atomic flag to  (and the csv-import importer)
that wraps the bounded in-memory create path in a single workspace
writer-locked, crash-recoverable transaction using pm-cli 2026.7.19's
commitWorkspaceTransaction primitive.

- All-or-nothing: on any row create failure, every already-applied create
  is compensated (pm close, not delete) and the command exits non-zero;
  no committed (open) items from the import remain.
- Crash-recoverable/resumable: a stable transaction id
  (csv-import-<sha1(absFilePath)>) plus a csv-tx:<transactionId> ownership
  marker let an interrupted run resume via inspect() without duplicating.
- Default (non-atomic) path is byte-identical (proven via before/after diff).
- --atomic + --stream fails fast (an unbounded stream cannot be one tx).
- Bumps @unbrained/pm-cli dependency floor to >=2026.7.19.
- 7 new integration tests (happy path, mid-import failure compensation,
  non-atomic contrast, resumability x2, --atomic+--stream rejection);
  97/97 tests green.

Refs: pm-csv-4bvs
@sourcery-ai

sourcery-ai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Adds an opt-in --atomic mode to pm csv import that routes in-memory imports through pm-cli's new commitWorkspaceTransaction primitive, providing all-or-nothing semantics, resumability, and multi-agent safety while keeping the non-atomic path byte-identical; surfaces the flag in the CLI/README, wires author/SDK injection, and raises the pm-cli peer requirement, with integration tests covering atomic behaviour and compatibility constraints.

Sequence diagram for atomic CSV import via commitWorkspaceTransaction

sequenceDiagram
  actor User
  participant pm_csv_command as pm_csv_run
  participant importCSV
  participant importCSVAtomic
  participant commitTransaction
  participant pm as pm_binary

  User->>pm_csv_command: pm csv import tasks.csv --atomic
  pm_csv_command->>importCSV: importCSV(pm_root, filePath, opts)
  importCSV->>importCSVAtomic: importCSVAtomic(pm_root, filePath, opts)

  importCSVAtomic->>importCSVAtomic: deriveTransactionId(resolve(filePath))
  importCSVAtomic->>importCSVAtomic: loadAppliedByTransaction(pm_root, transactionId)
  importCSVAtomic->>importCSVAtomic: loadKeyIndex(pm_root)
  importCSVAtomic->>importCSVAtomic: build WorkspaceTransactionStep[]

  alt ctx.sdk.commitWorkspaceTransaction provided
    importCSVAtomic->>commitTransaction: commitTransaction({transactionId, author, steps})
  else dynamic SDK resolution
    importCSVAtomic->>commitTransaction: resolveCommitWorkspaceTransaction(pm_root)
    importCSVAtomic->>commitTransaction: commitTransaction({transactionId, author, steps})
  end

  loop for each WorkspaceTransactionStep
    commitTransaction->>importCSVAtomic: inspect()
    alt [state pending]
      commitTransaction->>importCSVAtomic: apply()
      alt row is create
        importCSVAtomic->>pm: upsertCreate(pm_root, parsedRow, keyValue, source, [ownershipTag])
      else row is update
        importCSVAtomic->>pm: upsertUpdate(pm_root, existingId, parsedRow, source, [ownershipTag])
      end
    else [state applied]
      note over commitTransaction,importCSVAtomic: step already applied (resume)
    end
  end

  alt commitTransaction throws error
    commitTransaction-->>importCSVAtomic: Error
    importCSVAtomic->>importCSVAtomic: compensate() on each step
    importCSVAtomic->>pm: compensateCreate(pm_root, id)
    importCSVAtomic-->>importCSV: CommandError
  else commitTransaction succeeds
    commitTransaction-->>importCSVAtomic: WorkspaceTransactionCommitResult
    importCSVAtomic->>importCSVAtomic: tally imported/updated from committed.results
    importCSVAtomic-->>importCSV: ImportResult
    importCSV-->>pm_csv_command: ImportResult
  end
Loading

File-Level Changes

Change Details Files
Introduce an atomic, all-or-nothing CSV import path backed by commitWorkspaceTransaction, including resumability and compensation semantics, while preserving existing non-atomic behaviour.
  • Add atomic, commitTransaction, and atomicAuthor options to CsvImportOptions and route importCSV to a new importCSVAtomic implementation when atomic is set
  • Validate that --atomic cannot be combined with --stream, failing fast with a usage CommandError
  • Implement importCSVAtomic to parse CSV in-memory, build planned row metadata, and construct WorkspaceTransactionStep objects per row with inspect/apply/compensate closures
  • Use commitWorkspaceTransaction (prefer host-injected, fallback to dynamic import via resolveCommitWorkspaceTransaction) to commit the transaction and translate commit results into an ImportResult, excluding already-applied rows from counts
index.ts
Implement transaction identification, ownership tagging, and resume/rollback helpers to make atomic imports crash-recoverable and idempotent.
  • Derive a stable transaction id from the absolute CSV path via deriveTransactionId using a sha1 hash and a csv-import-<hash> prefix
  • Introduce the TX_TAG_PREFIX (csv-tx:) and stamp this ownership tag on created/updated items in the atomic path so inspect/compensate can identify items touched by a given transaction
  • Add loadAppliedByTransaction to scan pm list-all --json and build title/key maps of items carrying the transaction tag, used to detect already-applied steps on resume
  • Add itemStatus and compensateCreate helpers so compensation closes only existing, non-closed items, treating already-closed items as compensated and logging failures
index.ts
Extend core create/update helpers to support extra tags for atomic ownership and wire atomic mode into both CLI entry points with improved logging and flags.
  • Extend upsertCreate and upsertUpdate to accept an optional extraTags array, appending those tags (including ownership tag) to the create/update calls in the atomic path
  • Update CLI command definitions to include an --atomic flag with descriptive help text and example usage, and propagate atomic, commitTransaction, and atomicAuthor from the command context into importCSV
  • Adjust logging in both command run handlers to annotate reads with (atomic) when the flag is set
index.ts
Document the new atomic import behaviour and bump pm-cli version requirements to the minimum that supports commitWorkspaceTransaction.
  • Add --atomic usage examples and a dedicated "Atomic, all-or-nothing import" section to README.md explaining semantics, crash-recovery, ownership tagging, and interaction with --stream and --key
  • Update CHANGELOG.md with an entry describing atomic CSV import via commitWorkspaceTransaction and linking to the tracking task
  • Raise @unbrained/pm-cli peer/dev dependency version from >=2026.7.10 to >=2026.7.19 to require the new SDK primitive
README.md
CHANGELOG.md
package.json
Add integration tests that exercise the atomic CSV import path end-to-end against a real pm tracker, including happy path, failure rollback, resumability, and incompatible flag handling.
  • Introduce test/atomic.test.ts with helpers to activate the extension, create fresh temporary trackers, and run the csv import command handler with constructed contexts
  • Test that csv import declares the --atomic flag in its command definition
  • Test atomic happy path where N valid rows yield N open items and correct ImportResult counts
  • Test mid-import failure behaviour ensuring no uncompensated (open) items remain and the error clearly indicates rollback, contrasted with non-atomic behaviour where earlier rows remain
  • Test resumability by re-running the same atomic import (no duplicates) and by simulating partial application via pre-created items with the transaction tag, ensuring only missing rows are created
  • Test that --atomic combined with --stream fails with a usage error and clear message
test/atomic.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 031203a6-5467-4b18-98e8-3972539097ac

📥 Commits

Reviewing files that changed from the base of the PR and between 19a34b2 and c2ea9b5.

📒 Files selected for processing (4)
  • .agents/pm/history/pm-csv-j3pi.jsonl
  • .agents/pm/tasks/pm-csv-j3pi.toon
  • index.ts
  • test/atomic.test.ts

Summary by CodeRabbit

  • New Features
    • Added an --atomic mode for CSV imports with all-or-nothing processing.
    • Added crash recovery, safe resumption, per-row rollback, and duplicate-key handling.
    • Prevented transaction metadata from appearing in exported CSV files.
  • Bug Fixes
    • Improved retry and re-import behavior, including edited CSV files and partially completed imports.
    • Preserved existing records during rollback.
  • Documentation
    • Documented atomic import usage, limitations, recovery behavior, and examples.
  • Release
    • Released as version 2026.7.19-1.

Walkthrough

Adds --atomic CSV import using workspace transactions, per-row ownership markers, resumability, compensation, duplicate-key protection, CLI integration, export filtering, documentation, integration tests, and release metadata.

Changes

Atomic CSV import

Layer / File(s) Summary
Atomic import transaction flow
index.ts
Adds atomic planning, transaction commit resolution, per-row markers, rollback compensation, and transaction-tag propagation for creates and updates.
CLI wiring and CSV representation
index.ts, README.md
Exposes --atomic through command and importer paths, rejects streaming combinations, documents behavior, and strips transaction tags from exports.
Atomic import integration coverage
test/atomic.test.ts
Tests success, rollback, resumability, duplicate titles and keys, upserts, partial application, retries, closed-item detection, validation, and non-atomic behavior.
Release and task records
CHANGELOG.md, manifest.json, package.json, .agents/pm/tasks/*, .agents/pm/history/*
Updates release versions and dependency constraints, and records completed implementation and review fixes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CSVImportCommand
  participant importCSVAtomic
  participant commitWorkspaceTransaction
  participant Tracker
  User->>CSVImportCommand: run csv import --atomic
  CSVImportCommand->>importCSVAtomic: pass atomic options
  importCSVAtomic->>Tracker: inspect per-row transaction markers
  importCSVAtomic->>commitWorkspaceTransaction: submit create/update steps
  commitWorkspaceTransaction->>Tracker: apply workspace transaction
  importCSVAtomic->>Tracker: compensate current-run creates on failure
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the atomic CSV import feature and its resumable transaction-based behavior.
Description check ✅ Passed The description is directly related and accurately explains the atomic CSV import changes and follow-up hardening.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/atomic-csv-import

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@unbraind

Copy link
Copy Markdown
Owner Author

@coderabbitai full review
@greptileai

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

@unbraind: Starting a full review of #35, with particular attention to transaction safety, compensation/resumability, CLI compatibility, and test coverage.

✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 52 minutes.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • Using loadAppliedByTransaction to scan pm list-all and then build maps by title/key could become quite expensive on large trackers; if the CLI supports more selective querying by tag or transaction id, consider narrowing the query to csv-tx:<transactionId> at the pm level rather than filtering client-side.
  • The resumability logic relies on matching rows by title or normalized key within a transaction, but loadAppliedByTransaction uses a Map keyed only by title/key and will overwrite prior entries when multiple rows share the same value; if duplicate titles/keys per transaction are possible, consider storing and disambiguating multiple ids (e.g., by row index or additional metadata) to avoid incorrectly skipping or compensating rows.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Using `loadAppliedByTransaction` to scan `pm list-all` and then build maps by title/key could become quite expensive on large trackers; if the CLI supports more selective querying by tag or transaction id, consider narrowing the query to `csv-tx:<transactionId>` at the pm level rather than filtering client-side.
- The resumability logic relies on matching rows by `title` or normalized key within a transaction, but `loadAppliedByTransaction` uses a `Map` keyed only by title/key and will overwrite prior entries when multiple rows share the same value; if duplicate titles/keys per transaction are possible, consider storing and disambiguating multiple ids (e.g., by row index or additional metadata) to avoid incorrectly skipping or compensating rows.

## Individual Comments

### Comment 1
<location path="index.ts" line_range="1330-1339" />
<code_context>
+  for (let rowIndex = 0; rowIndex < dataRows.length; rowIndex++) {
+    const lineNo = opts.skipHeaders ? rowIndex + 1 : rowIndex + 2;
+    const row = dataRows[rowIndex];
+    const { parsed } = rowFields(headers, row);
+
+    if (!parsed.title) {
+      console.error(`Row ${lineNo}: skipping — 'title' is empty`);
+      result.skipped++;
+      continue;
+    }
+    if (!rowMatchesFilter(parsed, opts.filter)) {
+      result.skipped++;
+      result.filtered++;
+      continue;
+    }
+    const keyValue = opts.keyField ? (rowFields(headers, row).get(opts.keyField) ?? "") : "";
+    const existingId = keyValue ? keyIndex.get(normalizeKeyValue(keyValue)) : undefined;
+    planned.push({ rowIndex, lineNo, parsed, keyValue, existingId, isUpdate: Boolean(existingId) });
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid recomputing `rowFields` for `keyValue` when it was just computed.

Within the planning loop, `rowFields(headers, row)` is invoked twice: once to get `parsed` and again to derive `keyValue`. This adds unnecessary per-row overhead. Consider reusing the initial result (e.g., returning the key field alongside `parsed` or exposing the underlying map) so each row is parsed only once.

Suggested implementation:

```typescript
  for (let rowIndex = 0; rowIndex < dataRows.length; rowIndex++) {
    const lineNo = opts.skipHeaders ? rowIndex + 1 : rowIndex + 2;
    const row = dataRows[rowIndex];
    const fields = rowFields(headers, row);
    const { parsed } = fields;

    if (!parsed.title) {
      console.error(`Row ${lineNo}: skipping — 'title' is empty`);
      result.skipped++;
      continue;
    }
    if (!rowMatchesFilter(parsed, opts.filter)) {
      result.skipped++;
      result.filtered++;
      continue;
    }
    const keyValue = opts.keyField ? (fields.get(opts.keyField) ?? "") : "";
    const existingId = keyValue ? keyIndex.get(normalizeKeyValue(keyValue)) : undefined;
    planned.push({ rowIndex, lineNo, parsed, keyValue, existingId, isUpdate: Boolean(existingId) });
  }

```

If `rowFields` currently does not return an object that both:
1) has a `parsed` property, and
2) supports `.get(key: string)`,

you will need to adjust its return type/implementation so that a single call can be used for both purposes (e.g., return a `Map` extended with `parsed`, or an object that wraps the map and exposes `.get`).
</issue_to_address>

### Comment 2
<location path="index.ts" line_range="1451-1460" />
<code_context>
+ * dynamic `import("@unbrained/pm-cli/sdk")` so a standalone-installed extension
+ * still works when the SDK package is resolvable.
+ */
+async function resolveCommitWorkspaceTransaction(
+  pmRoot: string,
+): Promise<(opts: {
+  transactionId: string;
+  author: string;
+  steps: readonly WorkspaceTransactionStep[];
+  lockTtlSeconds?: number;
+  lockWaitMs?: number;
+}) => Promise<WorkspaceTransactionCommitResult>> {
+  const mod: typeof import("@unbrained/pm-cli/sdk") = await import("@unbrained/pm-cli/sdk");
+  const commit = mod.commitWorkspaceTransaction;
+  return (opts) => commit({ pmRoot, ...opts });
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Dynamic SDK import on each atomic call lacks explicit failure handling and caching.

This helper re-imports `@unbrained/pm-cli/sdk` on every call and surfaces missing/mis-resolved SDK as a generic module load error. Wrap the import in `try/catch` and rethrow a `CommandError` with a clearer message (e.g., required pm-cli version/installation). You could also cache `commitWorkspaceTransaction` after the first import to avoid repeated dynamic imports for `--atomic` calls.

Suggested implementation:

```typescript
let cachedCommitWorkspaceTransaction:
  | (typeof import("@unbrained/pm-cli/sdk"))["commitWorkspaceTransaction"]
  | undefined;

/**
 * Dynamically resolve the SDK commit coordinator bound to a tracker root, for
 * the importer path (which has no host-injected `ctx.sdk`). Falls back to a
 * dynamic `import("@unbrained/pm-cli/sdk")` so a standalone-installed extension
 * still works when the SDK package is resolvable.
 */
async function resolveCommitWorkspaceTransaction(
  pmRoot: string,
): Promise<(opts: {
  transactionId: string;
  author: string;
  steps: readonly WorkspaceTransactionStep[];
  lockTtlSeconds?: number;
  lockWaitMs?: number;
}) => Promise<WorkspaceTransactionCommitResult>> {
  if (cachedCommitWorkspaceTransaction) {
    return (opts) => cachedCommitWorkspaceTransaction({ pmRoot, ...opts });
  }

  try {
    const mod: typeof import("@unbrained/pm-cli/sdk") = await import("@unbrained/pm-cli/sdk");
    const commit = mod.commitWorkspaceTransaction;

    if (typeof commit !== "function") {
      throw new CommandError(
        "Loaded pm-cli SDK does not expose commitWorkspaceTransaction; please ensure @unbrained/pm-cli is correctly installed and matches the CLI version.",
      );
    }

    cachedCommitWorkspaceTransaction = commit;
    return (opts) => commit({ pmRoot, ...opts });
  } catch (error) {
    // Wrap dynamic import failures in a more actionable CLI error
    throw new CommandError(
      "Unable to load pm-cli SDK required for atomic workspace transactions. Ensure @unbrained/pm-cli is installed and compatible with this CLI.",
      { cause: error },
    );
  }
}

```

1. Ensure `CommandError` is imported at the top of `index.ts` from the existing error utility module used in this codebase (e.g., wherever other command/CLI errors are defined).
2. If `CommandError` does not accept an options object with `{ cause }`, adjust the constructor calls to match the existing signature (for example, `new CommandError("message", error)` or similar).
3. If your project’s TypeScript configuration does not support `typeof import("...")` in type positions, you may need to replace the `cachedCommitWorkspaceTransaction` type with an explicit function type matching `commitWorkspaceTransaction`’s signature from the SDK.
</issue_to_address>

### Comment 3
<location path="test/atomic.test.ts" line_range="237-246" />
<code_context>
+test("--atomic resumability: partial application resumes without duplicating", async () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for key-based upserts in the atomic path (create+update behavior and resumability via csv-key)

Existing atomic tests only cover title-keyed creates and never exercise `--key`-based upserts, which follow separate logic (`byKey` lookup, `normalizeKeyValue`, and non-compensated updates). Please add tests that:

- Import a CSV using `csv-key` (or the configured `--key` field) in atomic mode so some rows create items and others update existing ones.
- Verify that on a mid-import failure, newly created items are compensated but pre-existing updated items are not rolled back.
- Cover resumability where a partially applied run leaves key-tagged items with the transaction tag, and a resumed atomic run correctly detects them via key-based lookup and avoids duplicate creates/updates.

This will validate the `applied.byKey` path in `inspect`, the "updates are not compensated" behavior, and key-based resumability for `--atomic --key`.

Suggested implementation:

```typescript
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

// --atomic --key: key-based upserts with partial application and non-compensated updates
test("--atomic --key: key-based upserts compensate creates but not updates on failure", async () => {
  const root = freshTracker();
  const file = join(root, "atomic-key-upsert.csv");

  // We use a dedicated key column (`csv-key`) to exercise the byKey/normalizeKeyValue path.
  // Rows 1-2 update pre-existing items, rows 3-4 create new items.
  writeFileSync(
    file,
    [
      "csv-key,title,status,priority",
      "KEY-1,Existing 1,open,2",
      "KEY-2,Existing 2,open,3",
      "KEY-3,New 3,open,1",
      "KEY-4,New 4,open,2",
    ].join("\n") + "\n",
  );

  try {
    // Seed pre-existing items that will be updated by the atomic import.
    // These items must NOT receive the transaction tag so that updates are
    // treated as non-compensated changes in the atomic path.
    const seed = join(root, "atomic-key-upsert-seed.csv");
    writeFileSync(
      seed,
      [
        "csv-key,title,status,priority",
        "KEY-1,Existing 1,open,1",
        "KEY-2,Existing 2,open,1",
      ].join("\n") + "\n",
    );

    // Non-atomic baseline import creates the pre-existing items.
    const seeded = await runImport(root, seed, { atomic: false, key: "csv-key" });
    assert.ifError(seeded.error);
    assert.equal(seeded.result.imported, 2, "baseline run imports 2 pre-existing items");
    assert.equal(listItems(root).length, 2, "tracker contains 2 items before atomic run");

    // Now run atomic --key import and simulate a mid-import failure. The importer
    // should:
    // - update KEY-1 and KEY-2 (non-compensated updates)
    // - create KEY-3 and KEY-4 (compensated creates)
    //
    // The failure must occur after at least one create has been applied so we can
    // validate compensation of newly-created items without rolling back updates.
    const first = await runImport(root, file, {
      atomic: true,
      key: "csv-key",
      // NOTE: this option name is illustrative; adapt it to whatever the test
      // harness uses to simulate partial application / failure.
      failAfterApplied: 3,
    });

    assert(first.error, "atomic run should fail mid-import");
    assert.ok(
      first.result.imported > 0,
      "atomic run applies some rows before failing (creates + updates)",
    );

    const afterFailure = listItems(root);
    // We expect:
    // - Updated existing items (KEY-1, KEY-2) to remain, reflecting the atomic
    //   run's changes.
    // - Newly created items (KEY-3, KEY-4) to be compensated/rolled back.
    const keysAfterFailure = new Set(afterFailure.map((item: any) => item["csv-key"]));
    assert.ok(keysAfterFailure.has("KEY-1"), "KEY-1 exists after failed atomic run");
    assert.ok(keysAfterFailure.has("KEY-2"), "KEY-2 exists after failed atomic run");
    assert.ok(!keysAfterFailure.has("KEY-3"), "KEY-3 is compensated and removed");
    assert.ok(!keysAfterFailure.has("KEY-4"), "KEY-4 is compensated and removed");
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

// --atomic --key resumability: key-based lookup detects previously applied rows
// and avoids duplicate creates/updates when resuming a partially applied run.
test("--atomic --key resumability: key-based partial application resumes without duplicating", async () => {
  const root = freshTracker();
  const file = join(root, "atomic-key-resume.csv");

  // Mixed create/update set using csv-key:
  // - KEY-1, KEY-2: pre-existing items that will be updated
  // - KEY-3, KEY-4: new items that will be created
  writeFileSync(
    file,
    [
      "csv-key,title,status,priority",
      "KEY-1,Existing 1 updated,open,2",
      "KEY-2,Existing 2 updated,open,3",
      "KEY-3,New 3,open,1",
      "KEY-4,New 4,open,2",
    ].join("\n") + "\n",
  );

  try {
    // Seed pre-existing items without transaction tag. These will be updated by
    // the atomic run and must survive compensation as non-compensated updates.
    const seed = join(root, "atomic-key-resume-seed.csv");
    writeFileSync(
      seed,
      [
        "csv-key,title,status,priority",
        "KEY-1,Existing 1 original,open,1",
        "KEY-2,Existing 2 original,open,1",
      ].join("\n") + "\n",
    );

    const seeded = await runImport(root, seed, { atomic: false, key: "csv-key" });
    assert.ifError(seeded.error);
    assert.equal(seeded.result.imported, 2, "baseline run imports 2 pre-existing items");
    assert.equal(listItems(root).length, 2, "tracker contains 2 items before atomic run");

    // Simulate a prior interrupted atomic run that applied only the first 3 rows
    // (KEY-1 update, KEY-2 update, KEY-3 create) and tagged them with the
    // transaction ownership tag derived from the absolute file path.
    const transactionId = `csv-import-${sha1(join(root, "atomic-key-resume.csv")).slice(0, 12)}`;

    // Mark KEY-1, KEY-2, and KEY-3 as applied for the transaction. The
    // implementation of applyTransactionTag should mirror whatever the atomic
    // import code uses when tagging applied rows for inspect/resume.
    const items = listItems(root);
    for (const item of items) {
      if (item["csv-key"] === "KEY-1" || item["csv-key"] === "KEY-2") {
        item.transaction = transactionId;
      }
    }

    // KEY-3 does not exist yet; simulate its creation with the transaction tag so
    // that the resumed atomic run sees it as already applied via byKey lookup.
    items.push({
      "csv-key": "KEY-3",
      title: "New 3",
      status: "open",
      priority: 1,
      transaction: transactionId,
    });

    // Persist simulated state in whatever way the tracker expects.
    saveItems(root, items);

    // Resumed atomic --key run should:
    // - skip KEY-1, KEY-2, and KEY-3 (already applied via byKey/transaction tag)
    // - apply only KEY-4 as a new create
    const resumed = await runImport(root, file, { atomic: true, key: "csv-key" });
    assert.ifError(resumed.error);
    assert.equal(
      resumed.result.imported,
      1,
      "resumed atomic run imports only the remaining unapplied row (KEY-4)",
    );

    const finalItems = listItems(root);
    const keys = new Set(finalItems.map((item: any) => item["csv-key"]));
    assert.ok(keys.has("KEY-1"), "KEY-1 still present after resume");
    assert.ok(keys.has("KEY-2"), "KEY-2 still present after resume");
    assert.ok(keys.has("KEY-3"), "KEY-3 still present after resume");
    assert.ok(keys.has("KEY-4"), "KEY-4 created by resumed run");
  } finally {
    rmSync(root, { recursive: true, force: true });
  }
});

test("--atomic resumability: partial application resumes without duplicating", async () => {

```

The above tests assume several helpers and behaviors that you may need to align with your existing test harness:

1. **Failure simulation in atomic import**  
   - The `{ failAfterApplied: 3 }` option is illustrative. Replace it with the actual mechanism your `runImport` helper uses to simulate partial application / mid-import failure in existing atomic tests (e.g. `maxApplied`, `interruptAfter`, or a dedicated failure flag).

2. **Key configuration for imports**  
   - If your CLI uses a different option name than `key: "csv-key"` for `--key`, adjust the options passed to `runImport` accordingly.
   - Ensure the CSV header (`csv-key`) matches the configured key field used by the atomic importer.

3. **Tracker item shape and persistence**  
   - The code assumes tracker items are plain objects with fields like `"csv-key"`, `title`, `status`, `priority`, and that you can mutate `item.transaction` and then call `saveItems(root, items)`:
     - Replace `transaction` with the actual field name used to store the transaction ownership tag.
     - Replace `saveItems(root, items)` with your real helper to persist modified items (e.g. `writeTracker`, `saveTrackerState`, or similar).
   - If `listItems(root)` returns a different shape (e.g. `{ key, data, meta }`), adjust attribute access to align with your implementation.

4. **Transaction id derivation**  
   - The `sha1(...)` call is illustrative. If your atomic implementation uses a specific helper to derive the transaction id from the file path, replace the `transactionId` computation with that helper to ensure tests match the real behavior.

5. **TypeScript typings**  
   - If your tests are strictly typed, replace `(item: any)` casts with your concrete item type or update type definitions to include the `transaction` and `csv-key` fields.

These adaptations will make the new tests properly validate:
- the `applied.byKey` path in `inspect`,
- the "updates are not compensated" behavior for `--atomic --key`, and
- key-based resumability for `--atomic --key` without duplicate creates/updates.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread index.ts Resolved
Comment thread index.ts Resolved
Comment thread test/atomic.test.ts Resolved

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an --atomic flag for the CSV import command, allowing all-or-nothing imports under a single workspace transaction using commitWorkspaceTransaction (introduced in pm-cli version 2026.7.19). The implementation includes crash-recoverable/resumable logic that skips already-applied rows by tagging created items with a transaction-specific marker, as well as comprehensive integration tests. The reviewer feedback highlights a correctness issue where duplicate titles in a CSV could cause incorrect step skipping on resume when no unique key is specified. To resolve this, the reviewer suggests stamping each item with a row-specific tag (csv-tx:<transactionId>-<rowIndex>) and updating the transaction steps, tallying logic, and integration tests accordingly.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread index.ts Resolved
Comment thread index.ts Resolved
Comment thread index.ts Resolved
Comment thread test/atomic.test.ts Resolved
@greptile-apps

greptile-apps Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR adds an opt-in atomic mode for CSV imports. The main changes are:

  • --atomic import path using commitWorkspaceTransaction.
  • Per-row csv-txrow: markers for resumable imports and duplicate-safe row matching.
  • Compensation for created rows when an atomic import fails.
  • Guarding for in-batch duplicate --key creates.
  • Tests and release metadata for the new atomic import behavior.

Confidence Score: 4/5

Safe to merge after fixing the non-blocking README mismatch.

The atomic import code has focused tests for rollback, resume, duplicate rows and keys, closed rows, and edited-file transaction IDs. The remaining issue is documentation drift, not runtime behavior.

README.md

T-Rex T-Rex Logs

What T-Rex did

  • The release check workflow completed successfully with exit code 0.
  • The test suite reported 105 tests with 105 passing and 0 failing.
  • The security audit via npm audit found 0 vulnerabilities.
  • The npm pack --dry-run step succeeded, indicating packaging readiness.
  • The changelog check indicates the changelog is up to date.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
index.ts Adds the atomic import transaction path, per-row ownership markers, SDK resolution, command flag wiring, and export tag stripping.
test/atomic.test.ts Adds integration coverage for atomic success, rollback, resume, duplicate titles and keys, closed rows, and edited-file transaction IDs.
README.md Documents --atomic behavior, with one outdated transaction-id formula.
CHANGELOG.md Adds the 2026.7.19-1 release entry for atomic import and follow-up hardening.
package.json Bumps package version and pm-cli dependency requirements for the atomic import release.
manifest.json Bumps extension manifest version for the new release.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant User
  participant CsvImport as pm csv import --atomic
  participant Planner as Atomic planner
  participant SDK as commitWorkspaceTransaction
  participant PM as pm CLI / tracker

  User->>CsvImport: import CSV with --atomic
  CsvImport->>CsvImport: parse headers and rows
  CsvImport->>CsvImport: derive transactionId from path and content fingerprint
  CsvImport->>PM: list-all --json
  PM-->>CsvImport: existing row markers and key index
  CsvImport->>Planner: plan create/update steps per row
  Planner-->>CsvImport: WorkspaceTransactionStep list
  CsvImport->>SDK: commitWorkspaceTransaction(transactionId, steps)
  loop each pending row step
    SDK->>PM: inspect per-row marker
    alt pending create
      SDK->>PM: pm create with csv-tx and csv-txrow tags
    else pending update
      SDK->>PM: pm update/close with csv-tx and csv-txrow tags
    end
  end
  alt step fails
    SDK->>PM: compensate created rows by removing tx tags and closing
    SDK-->>CsvImport: error
    CsvImport-->>User: rolled-back CommandError
  else commit succeeds
    SDK-->>CsvImport: committed step results
    CsvImport-->>User: import counts
  end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant User
  participant CsvImport as pm csv import --atomic
  participant Planner as Atomic planner
  participant SDK as commitWorkspaceTransaction
  participant PM as pm CLI / tracker

  User->>CsvImport: import CSV with --atomic
  CsvImport->>CsvImport: parse headers and rows
  CsvImport->>CsvImport: derive transactionId from path and content fingerprint
  CsvImport->>PM: list-all --json
  PM-->>CsvImport: existing row markers and key index
  CsvImport->>Planner: plan create/update steps per row
  Planner-->>CsvImport: WorkspaceTransactionStep list
  CsvImport->>SDK: commitWorkspaceTransaction(transactionId, steps)
  loop each pending row step
    SDK->>PM: inspect per-row marker
    alt pending create
      SDK->>PM: pm create with csv-tx and csv-txrow tags
    else pending update
      SDK->>PM: pm update/close with csv-tx and csv-txrow tags
    end
  end
  alt step fails
    SDK->>PM: compensate created rows by removing tx tags and closing
    SDK-->>CsvImport: error
    CsvImport-->>User: rolled-back CommandError
  else commit succeeds
    SDK-->>CsvImport: committed step results
    CsvImport-->>User: import counts
  end
Loading

Reviews (12): Last reviewed commit: "docs(pm): sync pm-csv-j3pi final release..." | Re-trigger Greptile

Comment thread index.ts Resolved
Comment thread index.ts Resolved
@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds opt-in atomic CSV imports through the pm-cli transaction API. The main changes are:

  • Adds --atomic handling for pm csv import and the programmatic CSV importer.
  • Builds per-row transaction steps with ownership tags for resume and compensation.
  • Rejects --atomic --stream because streamed input cannot be one bounded transaction.
  • Documents the new mode and raises the @unbrained/pm-cli dependency floor.
  • Adds integration tests for success, rollback, resumability, and non-atomic parity.

Confidence Score: 4/5

This change has one contained correctness issue in the new atomic resume path.

The default import path is preserved, and common atomic success and rollback cases are covered. Duplicate-title create rows can be skipped after an interrupted atomic import, which can produce an incomplete import result.

index.ts

T-Rex T-Rex Logs

What T-Rex did

  • Reproduced the duplicate-title resume issue by running the Repro: generated Node harness for duplicate-title atomic resume against the real CSV import and pm CLI on an isolated tracker, and the run reported Imported 0, updated 0, skipped 0 with a final item list containing only one Duplicate Resume.
  • Reviewed the Atomic CLI build and test proofs, including the Atomic CLI build log, Atomic CLI build-test log, and Focused atomic test log, to verify that the build completed and the focused test ran.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
index.ts Implements --atomic import via commitWorkspaceTransaction; resumability for duplicate-title create rows can skip pending duplicate rows after an interrupted run.
test/atomic.test.ts Adds integration tests for atomic import happy path, rollback, resumability, non-atomic parity, and --stream rejection; duplicate-title resume is not covered.
README.md Documents --atomic semantics, resumability, compensation behavior, and --stream incompatibility.
package.json Raises the @unbrained/pm-cli peer/dev dependency minimum to a version exposing the transaction SDK.
package-lock.json Updates locked dev dependency versions for @unbrained/pm-cli and related tooling.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant CSV as pm csv import --atomic
participant SDK as commitWorkspaceTransaction
participant PM as pm CLI/store

User->>CSV: import file with --atomic
CSV->>CSV: derive transactionId from absolute file path
CSV->>PM: list-all --json for csv-tx marker and csv-key index
CSV->>SDK: commitWorkspaceTransaction(transactionId, steps)
loop each planned CSV row
  SDK->>CSV: inspect()
  CSV-->>SDK: pending or applied
  alt pending create/update
    SDK->>CSV: apply()
    CSV->>PM: pm create/update with csv-tx tag
    PM-->>CSV: item id
    CSV-->>SDK: result item id
  end
end
alt apply failure
  SDK->>CSV: compensate applied create steps
  CSV->>PM: pm close created item(s)
  SDK-->>CSV: throw rollback error
  CSV-->>User: non-zero rolled-back result
else success
  SDK-->>CSV: committed step results
  CSV-->>User: imported/updated/skipped counts
end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant CSV as pm csv import --atomic
participant SDK as commitWorkspaceTransaction
participant PM as pm CLI/store

User->>CSV: import file with --atomic
CSV->>CSV: derive transactionId from absolute file path
CSV->>PM: list-all --json for csv-tx marker and csv-key index
CSV->>SDK: commitWorkspaceTransaction(transactionId, steps)
loop each planned CSV row
  SDK->>CSV: inspect()
  CSV-->>SDK: pending or applied
  alt pending create/update
    SDK->>CSV: apply()
    CSV->>PM: pm create/update with csv-tx tag
    PM-->>CSV: item id
    CSV-->>SDK: result item id
  end
end
alt apply failure
  SDK->>CSV: compensate applied create steps
  CSV->>PM: pm close created item(s)
  SDK-->>CSV: throw rollback error
  CSV-->>User: non-zero rolled-back result
else success
  SDK-->>CSV: committed step results
  CSV-->>User: imported/updated/skipped counts
end
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
index.ts:1374-1375
**Duplicate titles break resume**
`inspect()` keys create rows only by `parsed.title`, so a crash after applying the first of two same-title rows makes the next run treat every row with that title as already applied. `loadAppliedByTransaction()` stores one `byTitle` entry per title, so the pending duplicate step is skipped and the atomic import finishes with fewer items than rows.

Reviews (2): Last reviewed commit: "feat(import): --atomic CSV import via co..." | Re-trigger Greptile

Comment thread index.ts Resolved
… SDK resolve hardening (2026.7.19-1)

Addresses all PR #35 review findings (Greptile 3/5 blocker + Gemini + Sourcery).

Version: 2026.7.19 -> 2026.7.19-1 (same-day re-release; this feature commit
lands after the 06:10 UTC daily release of 2026.7.19. Precedent: pm-github
2026.7.18-1).

CORE CORRECTNESS (resume/compensation per-row-precise):
Resume/compensation matched created rows by title (applied.byTitle) and update
rows by key (applied.byKey). This is wrong for CSVs with duplicate titles or
duplicate keys: a resumed 2nd same-titled row found the 1st row's item via
byTitle and was WRONGLY skipped (only one item instead of two); two rows
sharing a new key both planned as create (keyIndex not updated during
planning) -> duplicate items.

Fix: stamp every item this transaction writes with a per-row ownership marker
`csv-txrow:<transactionId>#<rowIndex>` (source of truth) in addition to the
batch-level `csv-tx:<transactionId>` marker (kept for scanning). loadAppliedBy
Transaction now returns byRowIndex: Map<number,string> parsed from the per-row
marker; byTitle/byKey dropped. inspect() for EVERY row (create AND update)
resolves prior application via byRowIndex.get(row.rowIndex); compensate()
targets appliedId ?? byRowIndex.get(row.rowIndex); the final tally uses
byRowIndex.has(row.rowIndex). Duplicate titles/keys are now irrelevant to
resume/compensation. The marker round-trips through pm tag storage (pm lower-
cases tags; transactionId is csv-import-<hex> and rowIndex is numeric).

In-batch duplicate-KEY guard:
During planning, when --key is set, track keys claimed by an earlier planned
CREATE in THIS run (claimedKeys). A later row repeating a not-yet-existing
key is SKIPPED with a clear per-row warning + result.skipped++ (simplest
correct behavior; documented in README + code). Pre-existing-in-tracker keys
still update normally.

Sourcery perf:
rowFields(headers,row) was computed twice per row in the planning loop (once
for `parsed`, once for `keyValue`). Now computed once and reused.

Sourcery bug_risk:
resolveCommitWorkspaceTransaction dynamic `import("@unbrained/pm-cli/sdk")`
wrapped in try/catch; throws an actionable CommandError (USAGE) if the import
fails or commitWorkspaceTransaction is not a function (requires
@unbrained/pm-cli>=2026.7.19 installed). The resolved function is cached in a
module-level var so repeated --atomic calls don't re-import.

Export:
Now strips csv-tx: and csv-txrow: provenance tags (previously only csv-key:/
csv-source: were stripped) so round-trip exports stay clean. The non-atomic
import path is byte-identical.

Tests (test/atomic.test.ts, 12 atomic tests total, 102 suite total):
- duplicate-title creates: two rows same title -> TWO items; resume skips both
  via per-row marker (not byTitle).
- duplicate-title partial resume: only row 0 tagged -> resume creates row 1
  (the bug case: old byTitle would wrongly skip row 1).
- --key upsert mid-import failure: created rows compensated (closed), pre-
  existing updated rows NOT rolled back (still open, retains updated priority).
- resume detection via per-row tag (not title/key): sparse partial run (rows 0
  and 2 applied) resumes creating exactly rows 1 and 3.
- in-batch duplicate-key guard: a repeated NEW key is skipped, not double-
  created.
- existing partial-application test updated to stamp the new per-row marker.

pm items: closed pm-csv-4bvs (atomic task) and filed+closed sub-item pm-csv-j3pi
documenting the fixes (author pi-glm). CHANGELOG regenerated for 2026.7.19-1.

release:check: typecheck + build + build:test + node --test (102 pass) +
audit:prod (0 vuln) + pack:dry-run + changelog:check all clean.

Co-Authored-By: pi-glm <pi-glm@users.noreply.github.com>
Comment thread index.ts Resolved
@unbraind

Copy link
Copy Markdown
Owner Author

Pushed 83125ab addressing all prior review findings:

  • Core (Greptile 3/5 blocker + Gemini + Sourcery): resume/compensation is now per-row-precise via a csv-txrow:<transactionId>#<rowIndex> ownership marker (batch csv-tx: kept only for scanning; byTitle/byKey matching dropped). Duplicate titles/keys can no longer trick inspect() into skipping the wrong row or compensate() into closing the wrong item.
  • In-batch duplicate---key guard: a repeated not-yet-existing key is skipped with a clear per-row warning instead of double-creating (documented in README + code).
  • Sourcery perf: rowFields() computed once per row.
  • Sourcery bug_risk: resolveCommitWorkspaceTransaction wraps the dynamic SDK import in try/catch → actionable CommandError, and caches the resolved function.
  • Version → 2026.7.19-1 (this feature lands after today's 06:10 UTC daily release of 2026.7.19), CHANGELOG regenerated, 102 tests pass (12 atomic, incl. duplicate-title/key + resume + upsert-compensation coverage), release:check clean.

@greptileai
@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

@unbraind: Starting a fresh full review of #35 at 83125ab, focusing on the revised per-row ownership/resume and compensation logic, duplicate-key handling, SDK loading, and non-atomic compatibility.

✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 56 minutes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@index.ts`:
- Around line 1304-1311: Initialize the dry-run keyIndex in the opts.dryRun
branch by loading loadKeyIndex(pmRoot) when opts.keyField is set, while
retaining an empty map when no key field is configured. Pass this seeded index
to processImportRow so previews match the actual upsert outcome.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cfd7c8a8-e73d-4814-92ec-3a3745310eff

📥 Commits

Reviewing files that changed from the base of the PR and between c0c37d4 and 83125ab.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • .agents/pm/history/pm-csv-4bvs.jsonl
  • .agents/pm/history/pm-csv-j3pi.jsonl
  • .agents/pm/tasks/pm-csv-4bvs.toon
  • .agents/pm/tasks/pm-csv-j3pi.toon
  • CHANGELOG.md
  • README.md
  • index.ts
  • manifest.json
  • package.json
  • test/atomic.test.ts

Comment thread index.ts Resolved
…tection

Greptile round-2 P1 "Compensated rows block retry": compensateCreate() rolls a
create back with `pm close` (not delete, to avoid history resurrection), which
leaves the `csv-txrow:<txId>#<rowIndex>` tag on the now-closed item. Because
loadAppliedByTransaction() recorded any tagged item regardless of status, those
compensated items counted as "applied" — so re-running the same file after a
full rollback skipped every rolled-back row and imported only the rows after the
original failure, instead of redoing the whole batch.

loadAppliedByTransaction() now skips closed items. A post-rollback retry
re-creates the rolled-back rows (closed tombstones are left in place);
genuinely interrupted runs (crash before compensation) leave their items OPEN,
so real resume detection is unaffected.

+1 regression test ("post-rollback retry re-imports the FULL batch") → 103 tests
pass; release:check clean. Documented in pm-csv-j3pi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@unbraind

Copy link
Copy Markdown
Owner Author

Round-3 push 98804fe: fixes the Greptile P1 (compensated rows block retry) — loadAppliedByTransaction() now excludes closed items so a post-rollback retry re-imports the full batch (+1 regression test, 103 tests pass, release:check clean). CodeRabbit's dry-run keyIndex note is addressed inline (declined for parity with the non-atomic/streaming dry-run paths; tracked as a separate fleet-wide follow-up).

@greptileai
@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

@unbraind: Starting a fresh full review of #35 at 98804fe, focusing on post-compensation retry behavior, closed-item filtering, atomic transaction safety, and regression coverage. I’ll treat the dry-run keyIndex behavior as an intentional, documented parity decision for this PR.


✏️ Learnings added
Learnt from: unbraind
URL: https://github.com/unbraind/pm-csv/pull/35

Timestamp: 2026-07-19T09:29:40.819Z
Learning: For the TypeScript atomic CSV import flow in PR `#35`, `loadAppliedByTransaction()` must exclude closed items so rows compensated during a failed transaction do not appear applied and block a later retry. The intentionally deferred dry-run `keyIndex` behavior matches existing non-atomic and streaming import paths and is tracked as a separate fleet-wide follow-up.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 24 minutes.

Comment thread index.ts Resolved
Comment thread index.ts Resolved
@unbraind

Copy link
Copy Markdown
Owner Author

All prior findings are now resolved and replied inline:

  • Greptile round-1 P1s (duplicate-key double-create, duplicate-title resume-skip) → per-row csv-txrow:<txId>#<rowIndex> marker + in-batch dup-key guard (83125ab).
  • Greptile round-2 P1 (compensated rows block retry) → loadAppliedByTransaction() excludes closed items; post-rollback retry re-imports the full batch, +regression test (98804fe).
  • Sourcery perf/bug_risk/testing → all implemented.
  • CodeRabbit dry-run keyIndex → declined for parity (agreed by CodeRabbit).

103 tests pass, release:check clean, no open actionable threads. Requesting a fresh look:

@greptileai

…" signal

Greptile round-3 P1 ("Closed rows re-import" / "Closed rows duplicate"): the
round-2 fix excluded *all* closed items from resume detection, but a CSV row
whose status is `closed`/`canceled` is legitimately imported as a closed item
(upsertCreate/upsertUpdate pass the row's status). Excluding closed items made
those rows re-import (duplicate) on a resumed run.

Fix: match on marker PRESENCE, not status. loadAppliedByTransaction() records
any item carrying this transaction's `csv-txrow:<txId>#<rowIndex>` marker
regardless of status; compensateCreate() now STRIPS the csv-tx/csv-txrow markers
before closing a rolled-back row (best-effort; runs whether the item is open or
already closed, so a closed-status create being rolled back also loses its
marker). Net: marker present => applied (resume idempotently, including
closed-status rows); marker absent => rolled back (re-import on retry).

+1 regression test ("closed-status rows resume idempotently") → 104 tests pass;
release:check clean. The earlier post-rollback-retry test still passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@unbraind

Copy link
Copy Markdown
Owner Author

Round-4 push 19a34b2: fixes the round-3 P1 (closed-status rows re-import/duplicate). Resume now keys on marker presence, not item status; compensation strips the markers so only rolled-back rows are re-imported. A legitimately closed-status row keeps its marker and resumes idempotently. +1 regression test (104 tests pass), release:check clean. All threads addressed & replied.

@greptileai
@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- changed files since parent ---'
git diff --name-status 19a34b2^ 19a34b2

echo '--- focused Round-4 diff ---'
git diff --unified=80 19a34b2^ 19a34b2 -- index.ts test/atomic.test.ts

echo '--- module/test outlines ---'
ast-grep outline index.ts --items all --type function,class --view compact || true
ast-grep outline test/atomic.test.ts --items all --type function,class --view compact || true

echo '--- marker, compensation, and transaction call sites ---'
rg -n -C 5 -e 'csv-txrow:|csv-tx:' -e '\bcompensate\b' -e '\binspect\b' \
  -e 'loadAppliedByTransaction' -e 'commitWorkspaceTransaction' \
  index.ts test/atomic.test.ts

Length of output: 49774


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- atomic helper and create/update implementations ---'
sed -n '1180,1585p' index.ts
sed -n '1620,1745p' index.ts

echo '--- atomic test setup, transaction injection, and rollback tests ---'
sed -n '1,240p' test/atomic.test.ts
sed -n '240,570p' test/atomic.test.ts

echo '--- workspace transaction type declarations / installed SDK references ---'
rg -n -C 4 \
  -e 'interface WorkspaceTransactionStep\b' \
  -e 'type WorkspaceTransactionStep\b' \
  -e 'commitWorkspaceTransaction\b' \
  -e 'WorkspaceTransactionCommitResult\b' \
  -e 'remove-tags' \
  -e 'function upsertCreate\b' \
  -e 'function upsertUpdate\b' \
  -g '*.ts' -g '*.d.ts' .

echo '--- package metadata relevant to SDK version and test scripts ---'
sed -n '1,220p' package.json

Length of output: 60995


@unbraind Full review found one remaining transaction-safety issue:

  • P1 — Do not ignore failed marker removal during compensation. compensateCreate() at index.ts Line 1269 performs pm update --remove-tags but discards its exit status. Since loadAppliedByTransaction() now treats marker presence as the sole applied signal, a failed cleanup followed by a successful close leaves a tagged tombstone that will be skipped on retry—the regression Round-4 is intended to prevent. For a row created initially with status=closed, the function returns immediately after the failed removal, so the bad resume state is guaranteed to persist. Check the update result and surface/propagate a compensation failure so the transaction remains recoverable rather than claiming a successful rollback; add coverage that forces tag removal to fail.
✅ Action performed

Full review finished.


Your plan includes PR reviews subject to rate limits. More reviews will be available in 58 minutes.

Comment thread index.ts Resolved
Comment thread index.ts Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
index.ts (1)

1253-1287: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Silent failure on tag-strip is not surfaced.

The --remove-tags spawnSync result at Lines 1271-1275 is never checked. If it fails (non-zero exit, or a transport/pm invocation error), the markers are not actually stripped, but the function proceeds as if they were. Since marker-presence is now the sole "applied" signal (per the doc comment at Lines 1257-1260), a silent failure here would make a rolled-back row look "still applied" on resume — reintroducing exactly the bug this round's fix (and the test/atomic.test.ts:469-504 regression test) targets, with no error logged for diagnosis. The close call right below (Lines 1278-1286) does check r.status and log on unexpected failure; the strip call should follow the same pattern.

🔧 Proposed fix
   if (markers.length > 0) {
     // Best-effort: remove the tx markers so this row is no longer "applied".
-    spawnSync(
+    const untag = spawnSync(
       "pm",
       ["--path", pmRoot, "update", id, "--remove-tags", markers.join(",")],
       { encoding: "utf-8" },
     );
+    if (untag.status !== 0) {
+      console.error(
+        `atomic import: failed to strip tx markers for ${id}: ${untag.stderr?.trim() || untag.stdout?.trim()}`,
+      );
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@index.ts` around lines 1253 - 1287, Update compensateCreate’s marker-removal
spawnSync call to inspect its result, treating non-zero status and invocation or
transport errors as failures. Log the marker-strip failure with the item ID and
available stderr/stdout details, matching the existing compensation close
failure handling, before proceeding to the close step.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.agents/pm/tasks/pm-csv-j3pi.toon:
- Around line 15-18: Synchronize the release metadata in
.agents/pm/tasks/pm-csv-j3pi.toon lines 15-18 by updating resolution,
expected_result, actual_result, and close_reason to reflect the final closed
state and 104 tests. Append a corrective update/closure record to
.agents/pm/history/pm-csv-j3pi.jsonl lines 5-6 documenting the round-2/round-3
fixes and final 104-test result.

---

Outside diff comments:
In `@index.ts`:
- Around line 1253-1287: Update compensateCreate’s marker-removal spawnSync call
to inspect its result, treating non-zero status and invocation or transport
errors as failures. Log the marker-strip failure with the item ID and available
stderr/stdout details, matching the existing compensation close failure
handling, before proceeding to the close step.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f9070387-b00b-4f7b-9548-c670f7357c05

📥 Commits

Reviewing files that changed from the base of the PR and between 83125ab and 19a34b2.

📒 Files selected for processing (4)
  • .agents/pm/history/pm-csv-j3pi.jsonl
  • .agents/pm/tasks/pm-csv-j3pi.toon
  • index.ts
  • test/atomic.test.ts

Comment thread .agents/pm/tasks/pm-csv-j3pi.toon Resolved
…stale skips)

Greptile round-4 P1 ("Path-only transaction id" / "Transaction id stales"):
deriveTransactionId hashed only resolve(filePath), and resume matches on
rowIndex. So editing a file in place and re-running `pm csv import --atomic`
reused the previous import's csv-txrow:#<n> markers — inspect() treated the
changed rows as already applied and skipped them, reporting a successful import
that changed nothing (silent stale data / data loss; Greptile reproduced it).

The transaction id now folds a SHA-1 fingerprint of the exact parsed content
(rawHeaders + dataRows) via fingerprintContent(). Same content re-run after a
crash keeps the same id (resumes from the journal, no duplicates); editing the
file yields a fresh id (a clean import of the new contents) — never a stale skip.

Also exports atomicTransactionId(filePath, opts) as the single source of truth
for the derivation; the three partial-resume tests now compute the real id via
it instead of a hardcoded path-only formula.

New regression test (edit-in-place re-imports new content) + a marker-stripped
assertion on the mid-import-failure test. 105 tests pass; release:check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@unbraind

Copy link
Copy Markdown
Owner Author

Round-5 push 82bfa4d: fixes the round-4 P1 (path-only transaction id → stale skip on edit-in-place). Transaction id now folds a SHA-1 content fingerprint of the parsed rows, so editing a file yields a fresh id (imports new content) while an unchanged re-run still resumes idempotently. New atomicTransactionId() export + regression test. 105 tests pass, release:check clean. All threads addressed & replied.

@greptileai
@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- PR change overview ---'
git diff --stat "$(git merge-base HEAD origin/main 2>/dev/null || git rev-parse HEAD^)" HEAD || true
git log --oneline -12

echo '--- changed source/test candidates ---'
fd -t f -e ts -e json -e md . | sort

echo '--- exported/module structure ---'
ast-grep outline index.ts --items all --type function,class --view compact || true
ast-grep outline test/atomic.test.ts --items all --type function,class --view compact || true

echo '--- atomic flow, transaction-ID, compensation, and marker references ---'
rg -n -C 8 \
  -e 'atomicTransactionId\b' \
  -e 'importCSVAtomic\b' \
  -e 'loadAppliedByTransaction\b' \
  -e 'compensateCreate\b' \
  -e 'remove-tags\b' \
  -e 'csv-txrow:|csv-tx:' \
  -e 'commitWorkspaceTransaction\b' \
  index.ts test/atomic.test.ts

echo '--- current atomic-related diff ---'
git diff --unified=60 "$(git merge-base HEAD origin/main 2>/dev/null || git rev-parse HEAD^)" HEAD -- index.ts test/atomic.test.ts package.json README.md CHANGELOG.md || true

echo '--- transaction coordinator declarations and mocked CLI behavior ---'
rg -n -C 5 \
  -e 'WorkspaceTransactionStep\b' \
  -e 'commitWorkspaceTransaction\b' \
  -e 'compensate\s*:' \
  -e 'remove-tags\b' \
  -g '*.ts' -g '*.d.ts' .

Length of output: 152157


@unbraind: Full review found two remaining issues.

  • P1 — Do not ignore transaction-marker removal failures during compensation. compensateCreate() in index.ts Lines 1310-1317 invokes pm update --remove-tags but ignores a non-zero result, then closes the item. Resume detection intentionally treats csv-txrow: marker presence as the only applied signal (loadAppliedByTransaction()), regardless of closed status. Therefore, if tag removal fails, the closed compensation tombstone still has its row marker and an unchanged-file retry skips that row as already applied. This is especially deterministic for a row initially created with status=closed, since the function returns immediately after the ignored removal failure. Check the removal result and propagate a compensation failure rather than claiming rollback completion; add regression coverage that forces --remove-tags to fail.

  • P2 — Update the atomic-import README description of transaction identity. The “Crash-recoverable / resumable” bullet still says the ID derives from the absolute file path (sha1(absPath)), while the implementation now derives it from the path and parsed-content fingerprint. Documenting path-only identity incorrectly implies that editing a file resumes the old transaction, whereas the intended Round-5 behavior starts a fresh transaction for changed content.

…nds)

CodeRabbit review nit: the tracking task recorded a stale 102-test count and
was closed before the round-2..5 follow-ups. actual_result now reflects the
final shipped state (105 tests, all Greptile/Sourcery/CodeRabbit findings
resolved). Metadata-only; no code change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@unbraind
unbraind merged commit b9ec916 into main Jul 19, 2026
6 of 8 checks passed
@unbraind
unbraind deleted the feat/atomic-csv-import branch July 19, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant