Skip to content

Export safe replacement: manifest-owned prune + --force gate (#108)#110

Merged
mobileskyfi merged 3 commits into
mainfrom
export-safe-replace
Jul 17, 2026
Merged

Export safe replacement: manifest-owned prune + --force gate (#108)#110
mobileskyfi merged 3 commits into
mainfrom
export-safe-replace

Conversation

@mobileskyfi

@mobileskyfi mobileskyfi commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Implements the safe-replacement scheme #104 flagged as blocking E4, using the strawman ratified on #108: manifest-owned prune-then-publish, with the manifest as the ownership record. Closes #108; part of #95 / B-0022.

Behavior

manifest.toml is now both the index and the ownership record ([[files]].name is a directory-relative path, so it already describes directory-nested files — no manifest schema change).

  • Adopt a directory that holds a manifest rosetta wrote: rewrite the current file set, then prune every file the old manifest listed that this run no longer produces, and clean the directories that empties. This is the stale-slug cleanup E3/E4's variable products/** and pages/<slug>/** sets need. It only ever deletes files a prior rosetta manifest named — files you added yourself are untouched.
  • New / empty directory → just write.
  • Non-empty directory with no rosetta manifest → refused, so the command can never clobber a directory it didn't create. --force overrides; on a TTY the CLI prompts instead.
  • Manifest written last → a crash can't leave a manifest naming half-written data. A crashed run (no manifest) needs --force once to resume; the next run heals it.

runExport gains an optional { force, confirmForeign } (default: refuse foreign dirs); the CLI parses --force and supplies a TTY confirm. confirmForeign is a callback so export.ts stays free of stdin/prompt coupling and fully testable.

DB-only boundary

Preserved. The one new file read is a read-back of our own prior manifest — not a data-source read, which is what the boundary forbids. The primitive-scan test is narrowed accordingly: it still bans fetch/new Database/child_process/Bun.file/require/async readFile, and now asserts the only readFileSync reads manifestPath. The behavioral "importing export.ts opens no database" proof is unchanged.

Verification

bun test 880 pass / 0 fail; typecheck, biome, markdownlint, cspell clean. New tests cover stale-file pruning (flat + directory-nested, incl. emptied-dir cleanup), the foreign-dir refusal (default + declining confirm + --force override + subsequent adopt), and a brand-new directory. Confirmed live on the schema-v10 corpus: a hand-injected stale products/ghost/specs.tsv is pruned and its products/ dir removed, a foreign README.md is kept, and a foreign non-empty dir is refused.

Notes

Export is [Unreleased]. This unblocks the #104 chunks: E4 (per-fragment doc tables) and E3 (products) can now build on this scheme. MANUAL.md documents the replacement semantics and the --force flag; CHANGELOG.md updated.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added safer rosetta export <dir> replacement behavior using manifest tracking.
    • Stale files from previous exports are removed while unrelated files remain untouched.
    • Added --force support and interactive confirmation for non-empty directories without a Rosetta manifest.
    • New exports can target directories that do not yet exist.
  • Documentation

    • Updated CLI help, manual guidance, and changelog details for safe export replacement.

Decides and implements the safe-replacement scheme #104 flagged as blocking
E4. The manifest is both index and ownership record:

- Adopt a directory that holds a manifest WE wrote; prune every file that
  manifest listed but this run no longer produces, then clean the dirs that
  empties — the stale-slug hazard E3/E4's variable products/** and
  pages/<slug>/** sets introduce. Only ever deletes files a prior rosetta
  manifest named, so user-added files are untouched.
- Write freely into a new/empty directory.
- Refuse a non-empty directory with no rosetta manifest unless force (or, on
  a TTY, an interactive confirm) — never clobber a dir we didn't create.
- Manifest last (crash can't leave a manifest naming half-written data);
  a crashed run needs --force once, then heals.

manifest [[files]].name is a directory-relative path, so the manifest shape
already describes directory-nested files — no manifest change needed.

runExport gains an optional { force, confirmForeign } (default: refuse); the
CLI parses --force and supplies a TTY prompt. DB-only boundary preserved: the
one new file read is a read-back of our own prior manifest, not a data
source — the boundary test now asserts readFileSync reads only manifestPath.

Verified live on the schema-v10 corpus: stale nested file pruned + its dir
cleaned, foreign file kept, foreign dir refused.

Part of #95. Closes #108.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 17, 2026 03:45
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4f1164fd-db7f-4f70-ac10-d9230b2c931c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

rosetta export now tracks owned files in manifest.toml, prunes stale owned files, protects foreign directories, supports --force and confirmation, and writes the manifest last. Tests, CLI help, manual documentation, and changelog entries cover the new behavior.

Changes

Safe export replacement

Layer / File(s) Summary
Manifest ownership and overwrite gate
src/export.ts
Adds export options, prior-manifest ownership detection, foreign-directory checks, and refusal unless forced or confirmed.
Prune-then-publish and safety validation
src/export.ts, src/export.test.ts
Writes datasets before pruning stale owned paths, removes empty ancestors, enforces root containment, and writes manifest.toml last.
CLI wiring and export documentation
src/mcp.ts, MANUAL.md, CHANGELOG.md
Adds --force, TTY confirmation, export error handling, help text, and safe-replacement documentation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI as rosetta export CLI
  participant Exporter as runExport
  participant FS as export directory
  participant Manifest as manifest.toml
  User->>CLI: invoke export with optional --force
  CLI->>Exporter: pass force and confirmForeign
  Exporter->>Manifest: read prior ownership record
  Exporter->>FS: inspect and validate output directory
  Exporter->>FS: write dataset files
  Exporter->>FS: prune stale owned files
  Exporter->>Manifest: write updated manifest last
  Exporter-->>CLI: return export summary
  CLI-->>User: print written datasets and manifest
Loading

Possibly related PRs

  • tikoci/rosetta#103: Modifies the same exporter, tests, and CLI wiring that this safe-replacement flow extends.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the manifest-owned safe-replacement export change.
Linked Issues check ✅ Passed The changes satisfy #108: manifest-owned prune-then-publish, foreign-dir refusal, manifest-last writes, docs, and tests are all implemented.
Out of Scope Changes check ✅ Passed No out-of-scope changes are evident; the edits stay within export behavior, CLI/docs, and test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch export-safe-replace

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Implements the “safe replacement” export scheme (issue #108) so rosetta export can safely adopt prior exports, prune stale manifest-owned files, and refuse clobbering foreign non-empty directories unless explicitly forced/confirmed—unblocking future variable file-set exports.

Changes:

  • Added --force handling and TTY confirmation plumbing in the CLI for exporting into non-empty foreign directories.
  • Extended runExport with ExportOptions and manifest-based ownership/prune logic, plus empty-directory cleanup.
  • Added tests covering stale-file pruning (including nested paths), foreign-dir refusal/override, and new-directory exports; updated docs/changelog accordingly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/mcp.ts Parses --force and provides optional TTY confirmation callback when exporting.
src/export.ts Adds manifest-ownership detection, stale-file pruning, and export options to gate foreign directories.
src/export.test.ts Adds coverage for prune/adopt behavior and foreign-dir refusal/force paths; adjusts boundary assertions.
MANUAL.md Documents --force and the safe-replacement semantics for export directories.
CHANGELOG.md Adds an [Unreleased] entry describing the safe-replacement behavior change for rosetta export.

Comment thread src/export.ts Outdated
@mobileskyfi

Copy link
Copy Markdown
Contributor Author

Test is failing gate check:

1 tests failed:
(fail) CLI help ↔ MANUAL flag parity > MANUAL CLI Flags table matches src/mcp.ts --help [35.00ms]

- CLI-help ↔ MANUAL parity test: --help now prints `export <dir> [--force]`
  to match the MANUAL CLI Flags row (the failing check).
- Prune before writing the new manifest (Copilot): the manifest still lands
  last so it never names half-written data, but pruning first means a crash
  mid-publish leaves the OLD manifest on disk with the full owned set, so the
  next run re-prunes instead of orphaning stale files.
- Validate each pruned path stays under the export root (Copilot): a tampered
  or hand-edited manifest with a `../` traversal name can no longer delete
  outside the directory. New test proves an outside file survives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mobileskyfi

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 3

🤖 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 `@src/export.ts`:
- Around line 577-585: The export path handling around dataset writes and stale
cleanup must reject symlink components, not just lexically contained paths. Add
component-by-component lstatSync validation for resolved targets, rejecting any
existing symlink before Bun.write, rmSync, or removeEmptyAncestors executes;
apply this to both generated dataset paths and stale manifest paths, and add a
regression test covering a symlinked directory escaping the export root.
- Around line 507-509: Replace the manifest-last export flow in the export
implementation with a staged-generation approach: write all datasets and the
manifest to an isolated staging location, then atomically commit the generation
as a unit. Add recovery handling for interrupted commits so startup/export
resumption never exposes or accepts a mixed-generation manifest and data set.
- Around line 569-585: Reorder the publish operations so stale entries in
ownedBefore are pruned before the dataset writes in the loop over datasets. Keep
the produced-name exclusion, manifest protection, path-containment defense, and
removeEmptyAncestors behavior unchanged; write the manifest last as currently
intended.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4c0f022e-8cfa-46eb-ba4e-94a1aac6b132

📥 Commits

Reviewing files that changed from the base of the PR and between a152958 and 6b1ae29.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • MANUAL.md
  • src/export.test.ts
  • src/export.ts
  • src/mcp.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use Bun and TypeScript, including Bun-native runtime APIs such as bun:sqlite, Bun.serve, and bunx.

Rosetta uses Bun and TypeScript; prefer bun and bun test rather than Node/npm-oriented substitutes.

Files:

  • src/mcp.ts
  • src/export.test.ts
  • src/export.ts
src/{query,mcp,browse}.ts

📄 CodeRabbit inference engine (AGENTS.md)

Route MCP, query, classifier, TUI, and canonicalizer behavior through shared core code, usually src/query.ts; keep src/mcp.ts and src/browse.ts thin adapters.

Files:

  • src/mcp.ts
*

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Keep this Copilot instructions file short; put substantive rules in narrow instruction files under .github/instructions/*.instructions.md.

Files:

  • CHANGELOG.md
  • MANUAL.md
CHANGELOG.md

📄 CodeRabbit inference engine (CLAUDE.md)

Record user-visible shipped changes in CHANGELOG.md under [Unreleased] or release sections.

Files:

  • CHANGELOG.md
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer each document's canonical home and do not duplicate operational detail, schema blocks, or long rule lists in the routing index.

Files:

  • CHANGELOG.md
  • MANUAL.md
{README.md,src/setup.ts,MANUAL.md}

📄 CodeRabbit inference engine (AGENTS.md)

Keep public client setup snippets aligned across README.md, src/setup.ts, and MANUAL.md, including the Codex command codex mcp add rosetta -- bunx @tikoci/rosetta``.

Files:

  • MANUAL.md
MANUAL.md

📄 CodeRabbit inference engine (CLAUDE.md)

Put installation, operations, release/re-extraction procedures, and schema reference in MANUAL.md.

Files:

  • MANUAL.md
🪛 ast-grep (0.44.1)
src/export.ts

[warning] 437-437: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^format = "${EXPORT_FORMAT}"$, "m")
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)

🔇 Additional comments (10)
src/mcp.ts (2)

223-248: LGTM!


184-184: LGTM!

MANUAL.md (2)

30-30: LGTM!


314-316: LGTM!

CHANGELOG.md (1)

48-48: LGTM!

src/export.ts (2)

21-21: LGTM!

Also applies to: 407-460


511-530: LGTM!

src/export.test.ts (3)

340-388: LGTM!


405-410: LGTM!


419-429: LGTM!

Comment thread src/export.ts
Comment thread src/export.ts Outdated
Comment thread src/export.ts Outdated
…eRabbit)

- Prune before writing datasets (CodeRabbit): a name reused across runs now
  transitions cleanly between a flat file and a directory (old `pages` file is
  removed before a new `pages/<slug>/x.tsv` is written, and the inverse). Still
  manifest-last, so the crash-safety from the prior fix is unchanged.
- Contain writes AND prunes to the export root through symlinks, not just
  lexically (CodeRabbit): `containedTarget` compares the realpath of the deepest
  existing ancestor to the realpath of the root, so a symlinked component
  (root/pages -> /outside) can't make a write or delete act outside the dir. A
  prune escape is skipped; a write escape throws. New regression test.
- Docs: state the ordering + symlink containment, and note honestly that the
  directory is a regenerable audit surface, not a transactional store — a crash
  can leave a mix of generations until the next run (full staged-atomic-commit
  was deferred by design in #108).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mobileskyfi
mobileskyfi merged commit a153888 into main Jul 17, 2026
10 of 14 checks passed
@mobileskyfi
mobileskyfi deleted the export-safe-replace branch July 17, 2026 05:33
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.

Safe-replacement scheme for the export's variable file sets (blocks E4)

2 participants