Skip to content

fix(export): place generated branchforge_*.rpy under game/ and dedupe define/default symbols (#244)#245

Merged
mikkisguy merged 5 commits into
mainfrom
things-and-stuff-and-some-other-things-and-stuff
Jun 20, 2026
Merged

fix(export): place generated branchforge_*.rpy under game/ and dedupe define/default symbols (#244)#245
mikkisguy merged 5 commits into
mainfrom
things-and-stuff-and-some-other-things-and-stuff

Conversation

@mikkisguy

@mikkisguy mikkisguy commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #244.

Two bugs in the Ren'Py project export/import flow:

Bug 1 — zip export places branchforge_*.rpy at archive root
branchforge_variables.rpy, branchforge_stats.rpy and
branchforge_definitions.rpy were written with bare keys, so the zip
contained them at the archive root instead of inside game/. Ren'Py
silently ignored them and the resulting project had no variables, stats
or character definitions.

Bug 2 — exported files duplicate define/default symbols
Both zip and GitLab exports could produce files containing
define <tag> = Character(...) and default <key> = ... lines that
already existed in the source project's files. Ren'Py then failed to
launch with NameError: name 'X' is already defined.

Fix

  • New utility apps/backend/src/services/rpy-statements.service.ts:

    • computeCommonDirectoryPrefix (moved from gitlab-sync)
    • extractAndStripRpySymbols recognises single- and multi-line
      define|default <tag> = Character(...) plus
      default <key> = True|False|<number> statements. Unknown RHS
      values are preserved rather than dropped.
  • export.service.ts (zip): places the three generated files under
    the common top-level directory prefix computed from project file
    paths, mirroring the GitLab export. Defensively strips any
    define/default lines that may still be present in legacy
    project_files.content rows so existing projects are not broken.

  • zip-import.service.ts and gitlab-sync.service.ts: strip symbols
    at ingestion, store the pre-strip text in originalContent, and
    promote the extracted characters / variables / stats into their
    tables with onConflictDoNothing for idempotent re-imports. The
    zip symbol-promotion now runs inside the same transaction as the
    per-file savepoint loop for atomicity.

  • characters.service.detectCharacters: reads from
    originalContent ?? content so the import wizard still surfaces
    stripped characters.

Verification

  • pnpm typecheck — passes
  • pnpm lint — passes
  • pnpm test:unit — 745 backend + 815 frontend tests pass
    (30 new test cases across rpy-statements, export, and zip-import)
  • pnpm format — clean
  • Oracle security / architectural review verdict: PASS
    (after addressing all must-fix and should-fix findings)

Summary by CodeRabbit

  • New Features
    • RPY symbol extraction now separates character, variable, and stat definitions into dedicated database tables during imports and exports.
    • Exports now place generated supporting branchforge_* files within the correct archive subdirectory (not always at the root), and GitLab sync follows the same cleaned-content flow.
  • Bug Fixes
    • More robust sanitization of legacy and mixed-directory project content.
    • Improved handling of character parsing and idempotent zip imports (no symbol duplication).
  • Documentation
    • Added a PR review checklist and pull request workflow guidelines.
  • Tests
    • Added/expanded regression and parsing unit tests for export/import/symbol extraction.
  • Chores
    • Updated Vite to v8.

renovate Bot and others added 4 commits June 18, 2026 19:35
… define/default symbols (#244)

The zip export wrote branchforge_variables.rpy, branchforge_stats.rpy and
branchforge_definitions.rpy at the archive root, so Ren'Py silently
ignored them. Both zip and GitLab exports also produced duplicate
`define <tag> = Character(...)` and `default <key> = ...` lines
when the source project already declared those symbols, which crashed
Ren'Py on launch with `NameError: name 'X' is already defined`.

Fix:

- New utility `rpy-statements.service.ts` exposes
  `computeCommonDirectoryPrefix` (moved from gitlab-sync) and a
  new `extractAndStripRpySymbols` that recognises single- and
  multi-line `define|default <tag> = Character(...)` plus
  `default <key> = True/False|<number>` statements. Unknown RHS
  values (quoted strings, identifier references) are preserved
  rather than silently dropped, so we never lose user-authored
  state BranchForge has no place to store.

- `export.service.ts` (zip) now places the three generated files
  under the common top-level directory prefix computed from the
  project file paths, mirroring the existing GitLab export
  behaviour. It also defensively strips any `define`/`default`
  lines that may still be present in `project_files.content`
  (legacy projects imported before this fix shipped).

- `zip-import.service.ts` and `gitlab-sync.service.ts` strip
  symbols at ingestion, store the pre-strip text in
  `originalContent`, and promote the extracted characters,
  variables and stats into their respective tables with
  `onConflictDoNothing` for idempotent re-imports. The zip
  symbol-promotion now runs inside the same transaction as the
  per-file savepoint loop, so file inserts and symbol promotion
  are atomic.

- `characters.service.detectCharacters` reads from
  `originalContent` (with a fallback to `content`) so the
  import wizard still surfaces characters whose `define` lines
  were stripped from the stored file content.

Tests:

- New unit suite `rpy-statements.service.unit.test.ts` covers
  prefix computation, single- and multi-line character
  definitions, `default` keyword parity with `define`, stat vs
  variable classification, and preservation of unknown defaults.

- Extended `export.service.unit.test.ts` asserts that the
  generated files land under the common prefix and that the
  defensive export-time strip cleans legacy content.

- Extended `zip-import.service.unit.test.ts` asserts the stored
  file content has the symbols stripped, the DB receives the
  expected character / variable / stat rows, and re-imports are
  idempotent.

Oracle review verdict: PASS.
@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
branchforge-docs Ready Ready Preview, Comment Jun 20, 2026 11:52am

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4e8b2a58-31d1-4a26-9c85-ccb7903b602f

📥 Commits

Reviewing files that changed from the base of the PR and between 32d99b7 and 1b33d54.

📒 Files selected for processing (6)
  • apps/backend/src/services/__tests__/export.service.unit.test.ts
  • apps/backend/src/services/__tests__/rpy-statements.service.unit.test.ts
  • apps/backend/src/services/export.service.ts
  • apps/backend/src/services/gitlab-sync.service.ts
  • apps/backend/src/services/rpy-statements.service.ts
  • apps/backend/src/services/zip-import.service.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/backend/src/services/export.service.ts
  • apps/backend/src/services/rpy-statements.service.ts
  • apps/backend/src/services/zip-import.service.ts
  • apps/backend/src/services/gitlab-sync.service.ts

📝 Walkthrough

Walkthrough

This PR fixes issue #244 by introducing a new rpy-statements.service.ts that parses and strips define/default symbols from .rpy file content. Import pipelines (ZIP and GitLab) now store cleaned content plus originalContent, promoting extracted symbols into dedicated DB tables idempotently. Export paths compute a shared directory prefix so branchforge_*.rpy files land under game/ instead of the archive root. AGENTS.md gains a Review Checklist and PR Workflow section, and the frontend vite dependency is bumped to ^8.0.0.

Changes

RPY Symbol Stripping and Directory Prefix Fix

Layer / File(s) Summary
rpy-statements.service: types and extraction logic
apps/backend/src/services/rpy-statements.service.ts
New service exports DetectedCharacterStatement, DetectedDefaultStatement, RpySymbolExtraction interfaces, computeCommonDirectoryPrefix, and extractAndStripRpySymbols; implements internal helpers for parenthesis-depth tracking, character body parsing, color extraction, and default RHS classification to strip BranchForge-managed symbols while preserving surrounding content.
rpy-statements.service: comprehensive unit tests
apps/backend/src/services/__tests__/rpy-statements.service.unit.test.ts
Full test suite covering directory prefix extraction (shared/mismatched/empty paths), empty/unchanged content, single/multi-line character definitions with color/who_color precedence, multiple characters, whitespace tolerance, boolean/numeric/unknown default assignments, character defaults, trailing comments, comment preservation, tag deduplication, Character(None), default colors, and label-internal assignment preservation.
ZIP import Step 3: symbol preprocessing and cross-file deduplication
apps/backend/src/services/zip-import.service.ts (lines 11–31, 78–112, 267–294)
Reorganizes imports to include characters/variables/stats schemas; adds accumulateSymbols helper for "first occurrence wins" deduplication; Step 3 preprocesses each .rpy file via extractAndStripRpySymbols, preserves originalContent, computes contentHash from cleanedContent, and accumulates deduplicated symbol maps across all files.
ZIP import Steps 4–5: per-file transaction and symbol promotion
apps/backend/src/services/zip-import.service.ts (lines 305–322, 337–431, 454, 463–525)
Introduces cross-file aggregation maps; Step 4 checks existing project_files by contentHash (skip/update/insert with cleanedContent/originalContent), accumulates symbols per-file success, parses STORY labels inline from originalContent; Step 5 promotes deduplicated symbols into characters/variables/stats with onConflictDoNothing, computing stats.minValue via Math.round(parseFloat(...)) || 0.
ZIP import: validation tests
apps/backend/src/services/__tests__/zip-import.service.unit.test.ts
Configures mocked transaction with onConflictDoNothing method; adds tests asserting define/default lines are stripped from stored content while originalContent is preserved, extracted symbols are inserted with correct shapes, and repeated imports exercise conflict-handling without duplication.
Export service: symbol stripping and directory-prefix fix
apps/backend/src/services/export.service.ts, apps/backend/src/services/characters.service.ts
generateExport imports and applies extractAndStripRpySymbols to derive strippedContent before patching STORY files; tracks safe paths to exclude unsafe entries from prefix calculation; computes fileDirPrefix and places branchforge_*.rpy files under that prefix. detectCharacters reads from originalContent (fallback to content) for parsing.
Export service: regression tests
apps/backend/src/services/__tests__/export.service.unit.test.ts
Adds four tests: (1) branchforge_*.rpy placement under common directory prefix (e.g., game/); (2) stripping of legacy define/default lines while preserving other content; (3) exclusion of unsafe traversal paths from both prefix and archive; (4) fallback to empty prefix when top-level directories disagree.
GitLab sync: symbol stripping, utilities reuse, and phase 1.5 promotion
apps/backend/src/services/gitlab-sync.service.ts
Replaces local computeCommonDirectoryPrefix with re-export from rpy-statements.service; exportToGitlab strips symbols before variable patching; importFromGitlab cleans content via extractAndStripRpySymbols, stores cleanedContent with originalContent preserved, computes contentHash from cleaned output, and adds phase 1.5 transaction promoting symbols into DB with onConflictDoNothing and numeric stat minima parsing.

Development Process Documentation

Layer / File(s) Summary
AGENTS.md: Review Checklist and PR Workflow
AGENTS.md
Adds Review Checklist covering scope/correctness/docs requirements; adds PR Workflow documenting branching, oracle delegation for sensitive diffs, oracle iteration requirements, and operational steps (stage/commit, gh pr create, CI watching) plus CodeRabbit exception and force-push constraints.

Frontend Dependency

Layer / File(s) Summary
Vite: ^8.0.0 dependency bump
apps/frontend/package.json
Updates vite devDependency from ^7.3.5 to ^8.0.0.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Step3 as importZipFile<br/>Step 3
  participant extractStrip as extractAndStripRpySymbols
  participant Step4 as Step 4<br/>Transaction
  participant DB

  Client->>Step3: importZipFile(zipBuffer, projectId)
  Note over Step3: Preprocess each .rpy entry
  loop each .rpy file
    Step3->>extractStrip: file.content
    extractStrip-->>Step3: cleanedContent, symbols[]
    Step3->>Step3: accumulate deduplicated symbols
  end
  Step3->>Step4: enriched preprocessing results
  Note over Step4: Per-file transaction + savepoint
  loop each entry
    Step4->>DB: upsert project_files(cleanedContent, originalContent)
    Step4->>Step4: on success: accumulate symbol dedupes
    Step4->>DB: parse and sync STORY labels from originalContent
  end
  Note over Step4: Step 5 — symbol promotion
  Step4->>DB: insert characters onConflictDoNothing
  Step4->>DB: insert variables onConflictDoNothing
  Step4->>DB: insert stats onConflictDoNothing (minValue=parsed)
  DB-->>Client: success
Loading
sequenceDiagram
  participant Client
  participant generateExport
  participant extractStrip as extractAndStripRpySymbols
  participant computePrefix as computeCommonDirectoryPrefix
  participant patchRPY as patchRPYWithVariables

  Client->>generateExport: generateExport(projectId)
  Note over generateExport: Iterate project files
  loop each file
    generateExport->>extractStrip: file.content
    extractStrip-->>generateExport: strippedContent
    generateExport->>generateExport: track safe paths
    alt STORY + labels
      generateExport->>patchRPY: strippedContent + conditions
      patchRPY-->>generateExport: patchedContent
    else
      generateExport->>generateExport: use strippedContent as-is
    end
  end
  generateExport->>computePrefix: safe project file paths
  computePrefix-->>generateExport: fileDirPrefix (e.g. "game/")
  generateExport->>generateExport: insert branchforge_*.rpy under fileDirPrefix
  generateExport-->>Client: patchedFiles ZIP map
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • mikkisguy/branchforge#149: Modifies gitlab-sync.service.ts's export flow around computing common directory prefix and placing branchforge_*.rpy files, which is the GitLab-side equivalent of the directory-prefix fix in this PR.
  • mikkisguy/branchforge#202: Touches the ZIP export feature end-to-end including export.service.ts and the export-generation flow that this PR's symbol-stripping and prefix changes build directly upon.

Poem

🐰 Hop hop, no more duplicate define e!
The symbols got stripped, now the exports are clean,
game/branchforge_*.rpy lands right in the fold,
originalContent keeps the story of old.
One source of truth — the DB holds the key,
And this little rabbit merged it with glee!

🚥 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 clearly summarizes the main changes: fixing file placement in exports (under game/) and deduplicating define/default symbols.
Linked Issues check ✅ Passed The PR fully addresses all coding objectives from issue #244: fixes file placement, implements symbol stripping, deduplication, and provides comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #244 objectives; the vite dependency update is a minimal, unrelated change that does not impact the scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 things-and-stuff-and-some-other-things-and-stuff

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 and usage tips.

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

Caution

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

⚠️ Outside diff range comments (1)
apps/backend/src/services/export.service.ts (1)

201-203: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Compute the generated-file prefix from exported safe paths, not raw DB paths.

At Line 202 invalid paths are filtered out, but Line 275 still computes the prefix from all raw filePaths. A skipped malformed path can alter fileDirPrefix and misplace branchforge_*.rpy.

Use the sanitized paths that actually made it into patchedFiles (or a collected safePaths array) when calling computeCommonDirectoryPrefix.

Also applies to: 270-277

🤖 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 `@apps/backend/src/services/export.service.ts` around lines 201 - 203, The
computeCommonDirectoryPrefix function is being called with raw filePath values
from the database, but invalid paths that fail the sanitizeZipEntryPath check
are being filtered out during processing. This causes skipped malformed paths to
incorrectly affect the fileDirPrefix calculation and misplace branchforge_*.rpy
files. Collect only the sanitized paths that pass the sanitizeZipEntryPath
validation check (the safePath values where safePath is truthy) into an array as
you iterate through files, then pass this array of valid sanitized paths to
computeCommonDirectoryPrefix instead of using the raw file.filePath values.
🤖 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 `@apps/backend/src/services/__tests__/rpy-statements.service.unit.test.ts`:
- Around line 120-161: Add a new test case within the test suite for the
extractAndStripRpySymbols function to verify that character definitions with a
space before the opening parenthesis are handled correctly. Create a test
similar to the existing single-line and multi-line character definition tests,
but use the format "define e = Character (" with a space before the parenthesis,
to ensure the extraction logic properly captures the character tag, name, and
color attributes and removes the definition from the cleaned content regardless
of whitespace placement.

In `@apps/backend/src/services/gitlab-sync.service.ts`:
- Around line 504-524: The file upsert operation using
db.insert(projectFiles).values() is storing stripped content to the database
without being part of a transaction that includes the subsequent symbol
promotion phase. If symbol promotion fails later, the stripped files remain
committed without matching symbol records, creating data inconsistency. Wrap the
db.insert(projectFiles) call and all symbol promotion operations (which occur
around lines 557-638 as noted in the comment) within a single database
transaction boundary so that if symbol promotion fails, the entire transaction
rolls back and neither the stripped files nor incomplete symbols persist to the
database.

In `@apps/backend/src/services/rpy-statements.service.ts`:
- Around line 159-170: The indexOf call searching for "Character(" on line 168
does not account for optional whitespace between "Character" and the opening
parenthesis, even though the regex pattern on line 160 already permits forms
like "Character (". When this spaced syntax is used, indexOf returns -1, causing
an empty body to be passed to parseCharacterBody and losing the character
definition. Update the search logic to handle the optional whitespace that the
regex pattern allows, ensuring consistency between the regex match and the body
extraction in the characterStart block.

In `@apps/backend/src/services/zip-import.service.ts`:
- Around line 263-284: The symbol deduplication logic aggregates characters,
variables, and stats from all preProcessedFiles without verifying that those
files were successfully imported, causing symbols from failed imports to be
promoted to the database. Before populating the charactersByTag, variablesByKey,
and statsByKey maps in the deduplication loop, filter preProcessedFiles to
include only files that completed successfully in Step 4, or alternatively
maintain a set of failed filePaths during the catch phase and exclude them from
the aggregation loop. This ensures only symbols from successfully imported files
are promoted in Step 5.

---

Outside diff comments:
In `@apps/backend/src/services/export.service.ts`:
- Around line 201-203: The computeCommonDirectoryPrefix function is being called
with raw filePath values from the database, but invalid paths that fail the
sanitizeZipEntryPath check are being filtered out during processing. This causes
skipped malformed paths to incorrectly affect the fileDirPrefix calculation and
misplace branchforge_*.rpy files. Collect only the sanitized paths that pass the
sanitizeZipEntryPath validation check (the safePath values where safePath is
truthy) into an array as you iterate through files, then pass this array of
valid sanitized paths to computeCommonDirectoryPrefix instead of using the raw
file.filePath values.
🪄 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: CHILL

Plan: Pro

Run ID: 5d05833a-c162-4f9f-8a96-7963840fa5c7

📥 Commits

Reviewing files that changed from the base of the PR and between b1b0dd0 and 32d99b7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • AGENTS.md
  • apps/backend/src/services/__tests__/export.service.unit.test.ts
  • apps/backend/src/services/__tests__/rpy-statements.service.unit.test.ts
  • apps/backend/src/services/__tests__/zip-import.service.unit.test.ts
  • apps/backend/src/services/characters.service.ts
  • apps/backend/src/services/export.service.ts
  • apps/backend/src/services/gitlab-sync.service.ts
  • apps/backend/src/services/rpy-statements.service.ts
  • apps/backend/src/services/zip-import.service.ts
  • apps/frontend/package.json

Comment thread apps/backend/src/services/gitlab-sync.service.ts Outdated
Comment thread apps/backend/src/services/rpy-statements.service.ts
Comment thread apps/backend/src/services/zip-import.service.ts Outdated
Follow-up fixes for issue #244 from code review:

- `rpy-statements.service.ts`: the body slice in the character
  extractor was using `line.indexOf("Character(")` which silently
  dropped definitions like `define e = Character ("Eileen", ...)`
  (with a space before the open paren). The regex already accepted
  the spaced form; the body search now uses `line.search(/Character\s*\(/)`
  to match. Added a regression test that covers both single- and
  multi-line spaced forms.

- `export.service.ts`: the directory-prefix calculation now uses
  only the sanitized paths we actually emit. Before, a stray path
  like `evil/../escape.rpy` (which `sanitizeZipEntryPath` rejects)
  would have dragged the prefix off `game/` to "" and put
  `branchforge_*.rpy` at the archive root. New regression test
  verifies the unsafe path is dropped and the prefix is correct.

- `zip-import.service.ts`: cross-file symbol aggregation is now
  driven by the per-file savepoint loop, not by the raw
  `preProcessedFiles` list. Files whose savepoint rolls back no
  longer contribute characters / variables / stats to the
  database, so a partial-failure import leaves no orphan symbols.
  Added a small `accumulateSymbols` helper to keep the call sites
  readable.

- `gitlab-sync.service.ts`: the file upsert (Phase 1) and the
  symbol promotion (Phase 1.5) are now wrapped in a single
  `db.transaction` so they commit or roll back together. The
  cross-file aggregation maps are populated inside the
  transaction, so a mid-import failure also discards any partial
  symbol accumulation.

Tests: `pnpm test:unit` — 747 backend + 815 frontend pass.
Oracle verdict (round 1): PASS.
@mikkisguy

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 20, 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.

@mikkisguy mikkisguy merged commit b61bc3c into main Jun 20, 2026
12 checks passed
@mikkisguy mikkisguy deleted the things-and-stuff-and-some-other-things-and-stuff branch June 20, 2026 18:44
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.

Export: branchforge_definitions.rpy placed outside game/ folder, and generated content duplicates symbols already defined in project files

1 participant