Skip to content

feat: add Folder Autopilot module slice - #51

Open
BeforeLights wants to merge 62 commits into
mainfrom
feat/folder-autopilot-20260804
Open

feat: add Folder Autopilot module slice#51
BeforeLights wants to merge 62 commits into
mainfrom
feat/folder-autopilot-20260804

Conversation

@BeforeLights

@BeforeLights BeforeLights commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • adds the content-free Folder Autopilot domain, v1 contracts, tenant-scoped API persistence boundary, dashboard projection, and OpenAPI routes
  • adds deterministic engine observation and bounded plan evaluation with typed action registration
  • adds guarded Windows local stabilization, path containment, collision allocation, staged actions, journaling, and undo state
  • adds Web profile/dashboard/review surfaces and Android offline review intents with bilingual resources
  • adds cross-runtime contract fixtures and Python parity adapters for all 13 canonical v1 schemas
  • bounds Web Vitest workers for reliable repository-wide CI execution

Scope and safety

This PR is a testable module slice, not a claim that the full FA P0/P1 specification is complete. Remaining preview, reconciliation, full execution, approval, recovery, export, and monitoring projections remain fail-closed and are documented in the release evidence. Local paths, bytes, previews, and source values do not cross the Web/API boundary.

Verification

  • corepack pnpm repo:check
  • corepack pnpm repo:build
  • API: 497 tests
  • Web: 33 tests
  • Engine: 140 Python tests
  • Contracts: 48 tests plus 34 TypeScript/Python/Kotlin parity fixtures
  • Android compile, unit, and instrumentation compile gates

Review notes

  • 48 independently reversible commits; below the agreed PR maximum
  • direct PR to main per the current delivery flow
  • CodeRabbit should be invoked once after required checks are ready

Summary by CodeRabbit

  • New Features
    • Added Folder Autopilot workspace for profiles, assignments, previews, approvals, exceptions, outcomes, and undo requests.
    • Added Android support for pausing, approving, and undoing actions with offline handling.
    • Added localized English and Vietnamese interface text.
    • Added content-free previews, tenant-scoped access, collision handling, path safety, and fail-closed authorization.
    • Added public contracts and API support for Autopilot profiles, bindings, and assignments.
  • Documentation
    • Added API and release evidence documentation.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 496c5137-1e46-4206-b377-39a6b9a51f80

📥 Commits

Reviewing files that changed from the base of the PR and between 365c080 and 430c9ff.

📒 Files selected for processing (10)
  • apps/desktop/src/features/folder-autopilot/local-actions.ts
  • apps/desktop/test/folder-autopilot-local-actions.test.ts
  • docs/operations/coderabbit-pr-51-disposition.md
  • services/api/openapi/v1.json
  • services/api/src/features/fa/api/folder-autopilot.controller.ts
  • services/api/test/features/fa/folder-autopilot.controller.test.ts
  • services/api/test/features/fa/folder-autopilot.service.test.ts
  • services/engine/src/databreeze_engine/processors/folder_autopilot.py
  • services/engine/src/databreeze_engine/registry.py
  • services/engine/tests/test_folder_autopilot_observation.py
🚧 Files skipped from review as they are similar to previous changes (10)
  • services/engine/tests/test_folder_autopilot_observation.py
  • apps/desktop/test/folder-autopilot-local-actions.test.ts
  • services/api/openapi/v1.json
  • docs/operations/coderabbit-pr-51-disposition.md
  • services/engine/src/databreeze_engine/processors/folder_autopilot.py
  • services/api/test/features/fa/folder-autopilot.service.test.ts
  • services/api/src/features/fa/api/folder-autopilot.controller.ts
  • services/api/test/features/fa/folder-autopilot.controller.test.ts
  • services/engine/src/databreeze_engine/registry.py
  • apps/desktop/src/features/folder-autopilot/local-actions.ts

📝 Walkthrough

Walkthrough

Folder Autopilot adds versioned contracts, deterministic engine planning, guarded Desktop execution, tenant-scoped API persistence, Web and Android workflows, and cross-runtime validation.

Changes

Folder Autopilot

Layer / File(s) Summary
Contracts and domain model
packages/contracts/..., packages/domain/..., packages/test-fixtures/...
Adds strict v1 schemas, immutable domain constructors, package exports, compatibility records, fixtures, and validation checks.
Engine planning contracts and registry
services/engine/...
Adds content-free observations, bounded plan evaluation, collision policies, deterministic hashes, and a read-only registered action.
API persistence and service boundaries
services/api/...
Adds Prisma storage, repository adapters, tenant-scoped service operations, DTO validation, dashboard projection, REST routes, module wiring, and fail-closed approval and undo delegation.
Desktop observation, authorization, and execution
apps/desktop/src/features/folder-autopilot/..., apps/desktop/test/...
Adds stable file observation, Windows path containment, execution authorization, guarded local actions, journaling, recovery, compensation, and undo lifecycle handling.
Web API client and workspace
apps/web/src/..., apps/web/test/...
Adds validated API calls, localized navigation, lazy routing, profile authoring, assignment controls, approval decisions, exceptions, outcomes, and undo actions.
Android offline workflow
apps/android/...
Adds validated mobile state, content-free offline action queueing, Compose navigation and controls, localized strings, and UI tests.
Release evidence and validation tooling
docs/..., tools/..., apps/web/vitest.config.ts
Adds release evidence, review disposition records, fixture validation adapters, parity-count updates, and a two-worker Vitest limit.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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 change: adding the Folder Autopilot module slice.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/folder-autopilot-20260804

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

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

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (20)
apps/desktop/src/features/folder-autopilot/file-observation.ts-189-198 (1)

189-198: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Whole-file buffering conflicts with the 10 GiB size bound.

readBytes must return the complete file as one Uint8Array, and fingerprintBytes hashes it in a single update call. MAX_FILE_BYTES on Line 3 accepts files up to 10 GiB, so validateStat admits sizes that this read path cannot hold. A single Buffer/Uint8Array on 64-bit Node is capped near 4 GiB, and the desktop process exhausts memory well before that.

Select one of the following:

  • Hash through a stream and pass incremental chunks to createHash().update(), then compare the total byte count against first.sizeBytes.
  • Lower MAX_FILE_BYTES to a value the buffered read can support.
🤖 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/desktop/src/features/folder-autopilot/file-observation.ts` around lines
189 - 198, Resolve the mismatch between the 10 GiB MAX_FILE_BYTES limit and the
whole-file buffering in the observation flow. Prefer updating fingerprintBytes
and its callers to hash streamed chunks incrementally with
createHash().update(), track the total bytes read, and compare that count with
first.sizeBytes; otherwise lower MAX_FILE_BYTES to a safely supported
buffered-read limit.
apps/desktop/src/features/folder-autopilot/local-journal.ts-200-206 (1)

200-206: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

recoverJournal never derives the terminal COMMITTED state, and no test covers the fully recovered case. recordJournalStep derives the terminal state on Lines 162-164, but recoverJournal returns COMMITTING regardless of the checkpoints it applies. After a crash in which every step committed on disk, recovery promotes every step to COMMITTED while the journal stays COMMITTING. recordJournalStep then rejects each step with DUPLICATE_STEP, failJournal only moves to COMPENSATING, and buildUndoPlan rejects with UNDO_NOT_AVAILABLE. The execution has no terminal outcome and undo is permanently unavailable.

  • apps/desktop/src/features/folder-autopilot/local-journal.ts#L200-L206: derive nextState from nextSteps exactly as recordJournalStep does, and pass it to cloneJournal.
  • apps/desktop/test/folder-autopilot-journal.test.ts#L89-L104: add a case that checkpoints both rename-1 and move-1 as COMMITTED and asserts the recovered state is COMMITTED.
🤖 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/desktop/src/features/folder-autopilot/local-journal.ts` around lines 200
- 206, Update recoverJournal to derive nextState from nextSteps using the same
terminal-state logic as recordJournalStep, then pass that state to cloneJournal
so fully committed recovery returns COMMITTED. In
apps/desktop/test/folder-autopilot-journal.test.ts lines 89-104, add coverage
with both rename-1 and move-1 checkpoints marked COMMITTED and assert the
recovered journal state is COMMITTED.
apps/desktop/src/features/folder-autopilot/local-journal.ts-219-231 (1)

219-231: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Undo of a COPY step emits a RENAME that cannot succeed.

buildUndoPlan maps every step to action: 'RENAME' with sourcePath: step.destinationPath and destinationPath: step.sourcePath. That inverse is correct for RENAME and MOVE. It is wrong for COPY, because the original file at step.sourcePath still exists.

executeLocalPlan then finds the destination present and rejects with DESTINATION_COLLISION. No data is lost, but undo is unavailable for any journal that contains an undoable COPY step, and the reported reason does not explain why. The UndoOperation type only permits 'RENAME', so a copy cannot be undone by deletion either.

Select one of the following:

  • Reject undoable: true for action: 'COPY' in validateStep.
  • Add a delete action to UndoOperation and emit it for COPY steps.
🤖 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/desktop/src/features/folder-autopilot/local-journal.ts` around lines 219
- 231, Update the undo flow so COPY steps cannot produce an invalid RENAME
inverse: either have validateStep reject COPY actions marked undoable, or extend
UndoOperation with a delete action and have buildUndoPlan emit deletion for COPY
steps. Preserve the existing RENAME inverse for RENAME and MOVE steps, and
ensure the chosen behavior reports the appropriate rejection or removes only the
copied destination.
apps/desktop/src/features/folder-autopilot/local-actions.ts-133-145 (1)

133-145: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Both runtime validators accept a missing identifier, because RegExp.prototype.test coerces its argument. Each validator applies SAFE_ID.test(...) to a value it never type-checks. undefined becomes the string "undefined", which matches ^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$, so an operation or step with no identifier passes. A number such as 5 also passes. The shared fix is an explicit typeof check before each regex test.

  • apps/desktop/src/features/folder-autopilot/local-actions.ts#L133-L145: add typeof operation.operationId !== 'string' and typeof operation.sourceFingerprint !== 'string' to the guard, so a plan reaching executeLocalPlan as unknown cannot produce a receipt with operationId: undefined.
  • apps/desktop/src/features/folder-autopilot/local-journal.ts#L84-L99: add typeof step.operationId !== 'string' to validateStep, so createJournal builds its duplicate check on Line 124 from validated identifiers and recordJournalStep can address every step.
🤖 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/desktop/src/features/folder-autopilot/local-actions.ts` around lines 133
- 145, Update validateOperation in
apps/desktop/src/features/folder-autopilot/local-actions.ts at lines 133-145 to
require operation.operationId and operation.sourceFingerprint to be strings
before applying SAFE_ID and SHA256; update validateStep in
apps/desktop/src/features/folder-autopilot/local-journal.ts at lines 84-99 to
require step.operationId to be a string before regex validation.
apps/desktop/src/features/folder-autopilot/local-actions.ts-262-275 (1)

262-275: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

A mid-plan failure discards the receipts for the writes that already landed.

The loop applies operations in sequence. If copyExclusive or renameExclusive fails at operation n, Line 266 throws LOCAL_IO_FAILED and every receipt collected for operations 1..n-1 is lost. Those writes are already on disk.

The caller cannot then determine which operations completed. local-journal.ts needs exactly that information to drive recordJournalStep and compensateJournal, so compensation cannot be reconstructed after a partial failure.

Report the applied receipts together with the failure. Two options:

  • Attach the receipts to the thrown error.
  • Accept a per-operation callback and invoke it after each successful write.
♻️ Proposed direction: carry the applied receipts on the error
+export class LocalActionFailure extends LocalActionError {
+  readonly appliedReceipts: readonly LocalActionReceipt[];
+
+  constructor(code: LocalActionCode, appliedReceipts: readonly LocalActionReceipt[]) {
+    super(code);
+    this.name = 'LocalActionFailure';
+    this.appliedReceipts = Object.freeze([...appliedReceipts]);
+  }
+}
     try {
       if (operation.action === 'COPY') await fileSystem.copyExclusive(source, destination);
       else await fileSystem.renameExclusive!(source, destination);
     } catch {
-      return reject('LOCAL_IO_FAILED');
+      throw new LocalActionFailure('LOCAL_IO_FAILED', receipts);
     }
🤖 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/desktop/src/features/folder-autopilot/local-actions.ts` around lines 262
- 275, Update the operation loop containing copyExclusive and renameExclusive so
a mid-plan I/O failure preserves the receipts already added for successful
operations. Attach the accumulated receipts to the LOCAL_IO_FAILED error (or
equivalent failure result) while retaining the existing failure classification,
allowing local-journal.ts to recover them for recordJournalStep and
compensateJournal.
apps/desktop/src/features/folder-autopilot/file-observation.ts-26-27 (1)

26-27: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Use a representation that preserves nanosecond timestamps.

If modifiedAtNs stores epoch nanoseconds, Number.isSafeInteger rejects current timestamps because they exceed Number.MAX_SAFE_INTEGER. Converting mtimeNs to number also loses nanosecond precision. Use a JSON-safe decimal string, or rename the field and use milliseconds. Add a test with a current-scale timestamp.

🤖 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/desktop/src/features/folder-autopilot/file-observation.ts` around lines
26 - 27, Update the file-observation model and its mtimeNs conversion to
preserve epoch nanoseconds using a JSON-safe decimal string, including renaming
modifiedAtNs if needed for the chosen representation. Remove
Number.isSafeInteger validation and numeric coercion that lose precision, update
consumers accordingly, and add coverage using a current-scale timestamp.
services/engine/src/databreeze_engine/processors/folder_autopilot_plan.py-29-38 (1)

29-38: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep generated destination names within the contract limit.

destinationName can contain 255 characters. _unique_name then appends " (1)" and can return a longer name.

The evaluator emits this value as a valid operation because PlanOperation.destinationName has no corresponding length constraint. The plan can report READY for a destination that violates the input contract.

Reserve space for the suffix and extension before constructing each candidate. Apply the same destination-name validator to generated operations.

🤖 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 `@services/engine/src/databreeze_engine/processors/folder_autopilot_plan.py`
around lines 29 - 38, Update _unique_name to reserve space for the numeric
suffix and extension so every generated candidate stays within the
destination-name contract limit, including when the original name is already at
the limit. Apply the existing destination-name validator when creating generated
PlanOperation values, ensuring invalid generated names cannot produce a READY
plan.
services/engine/src/databreeze_engine/folder_autopilot_contracts.py-54-76 (1)

54-76: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Bind stableExecutionKey to the observation fields.

The model accepts any 64-character digest as stableExecutionKey. A raw execution request can therefore supply a key that does not match observationId, displayName, sizeBytes, modifiedAtNs, and contentSha256.

_plan_hash trusts this key. Different observations can consequently receive the same plan identity.

Move the canonical key derivation to a shared contract helper. Recompute and compare the key during FileObservation validation, or remove the field from caller input.

🤖 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 `@services/engine/src/databreeze_engine/folder_autopilot_contracts.py` around
lines 54 - 76, Add a shared contract helper that derives the canonical stable
execution key from FileObservation’s observationId, displayName, sizeBytes,
modifiedAtNs, and contentSha256 fields, then update FileObservation validation
to recompute and compare stableExecutionKey against that helper result. Keep the
existing digest-format validation, but reject caller-supplied keys that do not
match the observation fields and ensure _plan_hash continues using the validated
canonical key.
services/engine/src/databreeze_engine/processors/folder_autopilot_plan.py-66-70 (1)

66-70: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use Windows-equivalent destination keys for collision checks.

The occupied set compares destination names with exact string equality. On Windows, an occupied Invoice.csv can therefore fail to collide with a requested invoice.csv.

The evaluator then reports READY, although both names select the same destination. Use the same canonical destination-key function as the Desktop safety layer for existing, requested, generated, and newly occupied names.

Add a test for names that differ only by case.

Also applies to: 97-109

🤖 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 `@services/engine/src/databreeze_engine/processors/folder_autopilot_plan.py`
around lines 66 - 70, Update the collision-key handling in the folder autopilot
plan logic around the occupied, requested, generated, and newly occupied
destination sets to use the canonical destination-key function shared with the
Desktop safety layer, rather than exact destination-name strings. Ensure
existing and requested names differing only by case collide on Windows and
cannot produce a READY result, and add coverage for that case.
services/api/src/features/fa/adapter/prisma-folder-autopilot-repository.adapter.ts-213-235 (1)

213-235: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

All three save methods read the existing row and then insert it in a separate statement. Under concurrency both callers observe no existing row, both call create, and the primary key rejects the second insert. No duplicate row persists, so the data stays correct, but the caller receives a raw Prisma unique-constraint error instead of the typed FA_IMMUTABLE_* code. Wrap each create in a try/catch that re-reads the row, compares it, and throws the matching typed code.

  • services/api/src/features/fa/adapter/prisma-folder-autopilot-repository.adapter.ts#L213-L235: catch the insert failure in saveProfile, re-read by (profileId, version), and throw FA_IMMUTABLE_PROFILE when the stored row differs.
  • services/api/src/features/fa/adapter/prisma-folder-autopilot-repository.adapter.ts#L266-L284: apply the same handling in saveBinding and throw FA_IMMUTABLE_BINDING.
  • services/api/src/features/fa/adapter/prisma-folder-autopilot-repository.adapter.ts#L313-L340: apply the same handling in saveAssignment and throw FA_IMMUTABLE_ASSIGNMENT. This method also races on the recipe_assignments_scope_idempotency_key index, so the catch must distinguish the primary-key violation from the idempotency-key violation.
🤖 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
`@services/api/src/features/fa/adapter/prisma-folder-autopilot-repository.adapter.ts`
around lines 213 - 235, Update
services/api/src/features/fa/adapter/prisma-folder-autopilot-repository.adapter.ts
lines 213-235 in saveProfile to catch create failures, re-read by profileId and
version, compare the stored row, and throw FA_IMMUTABLE_PROFILE when it differs.
Apply the same create-failure handling at lines 266-284 in saveBinding with
FA_IMMUTABLE_BINDING, and at lines 313-340 in saveAssignment with
FA_IMMUTABLE_ASSIGNMENT; there, distinguish primary-key conflicts from
recipe_assignments_scope_idempotency_key conflicts and preserve the latter’s
existing behavior.
services/api/src/features/fa/fa.module.ts-43-47 (1)

43-47: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject missing FA persistence in production. main.ts calls createApiApplication() without FA persistence options, so production selects InMemoryFolderAutopilotRepositoryAdapter. Throw when NODE_ENV is production and neither persistence option is supplied.

🤖 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 `@services/api/src/features/fa/fa.module.ts` around lines 43 - 47, Update the
repository selection in the FA module’s application factory to throw when
NODE_ENV is production and both folderAutopilotRepository and
folderAutopilotDatabase are missing; retain the existing adapter selection for
non-production environments or when either persistence option is supplied.
services/api/prisma/migrations/20260804050000_fa_folder_autopilot/migration.sql-68-69 (1)

68-69: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use a non-null scope key for idempotency uniqueness. The current composite index allows duplicate idempotency_key values when workspace_id or project_id is NULL. Prisma 7.9.1 cannot represent PostgreSQL NULLS NOT DISTINCT in @@unique, so adding that clause only to the migration would create drift. Replace the nullable composite key in services/api/prisma/schema/fa.prisma and its migration with a non-null canonical scope key, or use scope-specific indexes with a matching Prisma-compatible design.

🤖 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
`@services/api/prisma/migrations/20260804050000_fa_folder_autopilot/migration.sql`
around lines 68 - 69, Replace the nullable composite idempotency uniqueness
design in services/api/prisma/schema/fa.prisma:65 and
services/api/prisma/migrations/20260804050000_fa_folder_autopilot/migration.sql:68-69
with a Prisma-compatible non-null canonical scope key, and make the unique
constraint use that key with idempotency_key. Keep the schema and migration
definitions aligned without relying on PostgreSQL NULLS NOT DISTINCT.
services/api/src/features/fa/api/folder-autopilot-dashboard.ts-48-84 (1)

48-84: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Do not build user-facing display names in the API projection.

Lines 51 and 74 generate English-only labels: Folder profile ${profile.version} and Assignment ${assignment.assignmentId.slice(0, 8)}. The projection is rendered by the Web and Android surfaces, so these strings reach the user without translation. Vietnamese is the default product locale, and English must stay complete, so the API cannot hard-code one language.

Emit the raw fields and let each client localize. For example, return version on the profile and assignmentId on the assignment, and drop displayName from the projection contract. If the field must stay, return a message key plus parameters instead of a formatted sentence.

Note that services/api/test/features/fa/folder-autopilot-dashboard.test.ts line 61 asserts the literal 'Assignment 44444444', so update that assertion with the contract.

As per coding guidelines: "Keep Vietnamese the default product locale and English complete."

🤖 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 `@services/api/src/features/fa/api/folder-autopilot-dashboard.ts` around lines
48 - 84, The API projections must not generate hard-coded English display names.
Update the FolderAutopilotDashboardProfileV1 and
FolderAutopilotDashboardAssignmentV1 contracts and their profileProjection and
assignmentProjection functions to remove displayName and expose the raw profile
version and assignmentId fields instead; update the corresponding dashboard test
assertion to match the revised contract.

Source: Coding guidelines

services/api/src/features/fa/api/folder-autopilot.controller.ts-56-57 (1)

56-57: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A Folder Autopilot list failure is reported to the client as an empty dashboard. The service does not map repository errors for the list methods, and the dashboard route discards a non-accepted result. During a persistence outage the response is accepted: true with empty profiles and assignments, so the user cannot distinguish an empty workspace from a failed read.

  • services/api/src/features/fa/api/folder-autopilot.controller.ts#L56-L57: stop substituting [] for a rejected result. Return the service code to the caller, or map it to a status code, so the Web surface can render an error state instead of an empty dashboard.
  • services/api/src/features/fa/application/folder-autopilot.service.ts#L331-L347: wrap each this.repository.list* call in try/catch and return rejected(mapPersistenceError(error)), matching createProfile and updateAssignmentState. Without this the promise rejects and the route never receives a code to forward.
🤖 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 `@services/api/src/features/fa/api/folder-autopilot.controller.ts` around lines
56 - 57, Propagate Folder Autopilot list failures instead of converting them to
empty results: in
services/api/src/features/fa/api/folder-autopilot.controller.ts lines 56-57,
forward the rejected service code or map it to an HTTP status; in
services/api/src/features/fa/application/folder-autopilot.service.ts lines
331-347, wrap each repository list call in try/catch and return
rejected(mapPersistenceError(error)), matching createProfile and
updateAssignmentState.
services/api/src/features/fa/api/folder-autopilot.controller.ts-64-73 (1)

64-73: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not return 201 for a rejected result.

createProfile returns the service result unchanged. A rejection such as FA_IMMUTABLE_PROFILE, INVALID_IDENTIFIER, or FA_PERSISTENCE_UNAVAILABLE is therefore sent with HTTP 201 Created and a body of { accepted: false, code }. createBinding at line 98 and createAssignment at line 126 behave the same way, and updateAssignment and pauseAssignment return rejections with 200.

The same OpenAPI document maps rejected results to real status codes elsewhere. IamMembershipController declares 400, 403, 404, 409, 410, and 503 with MembershipRejectedResponseDto. A client that follows status codes will treat a failed Folder Autopilot create as a success. Map each FolderAutopilotServiceErrorV1 to a status code, and declare those responses in the OpenAPI paths at services/api/openapi/v1.json lines 10410-11368.

🤖 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 `@services/api/src/features/fa/api/folder-autopilot.controller.ts` around lines
64 - 73, Update createProfile, createBinding, createAssignment,
updateAssignment, and pauseAssignment to translate each
FolderAutopilotServiceErrorV1 rejection into its corresponding HTTP status
instead of returning rejected results as successful responses. Preserve accepted
responses while using the established error-to-status mapping and rejection DTO.
Declare the same error responses for all affected Folder Autopilot operations in
the v1 OpenAPI document, including the relevant 400, 403, 404, 409, 410, and 503
statuses.
apps/web/src/features/folder-autopilot/folder-autopilot-api.ts-510-513 (1)

510-513: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not fall back to a constant idempotency key.

If crypto.randomUUID is unavailable, every mutation in the session sends Idempotency-Key: autopilot-client-generated. The server then treats a pause, an approval, and an undo as replays of the first request and can silently drop them. Fail closed instead, which matches createFolderAutopilotProfile at Line 557 and sha256Hex at Line 517.

🐛 Proposed fix
 function idempotencyKey(prefix: string): string {
-  const random = globalThis.crypto?.randomUUID?.();
-  return `${prefix}-${random ?? 'client-generated'}`;
+  const random = globalThis.crypto?.randomUUID?.();
+  if (random === undefined) throw new Error('AUTOPILOT_CRYPTO_UNAVAILABLE');
+  return `${prefix}-${random}`;
 }
🤖 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/web/src/features/folder-autopilot/folder-autopilot-api.ts` around lines
510 - 513, Update idempotencyKey to fail closed when
globalThis.crypto?.randomUUID?.() is unavailable: do not use the constant
'client-generated' fallback, and instead propagate the same failure behavior
established by createFolderAutopilotProfile and sha256Hex.
apps/web/src/features/folder-autopilot/folder-autopilot-page.tsx-76-79 (1)

76-79: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Refresh the dashboard after a profile is created.

submit shows autopilot.profile.saved and calls onSaved(). FolderAutopilotPage passes onSaved={() => undefined} at Line 589, so the folder-autopilot query is never invalidated. The profile list above the form keeps showing stale data after a successful create, and the user must reload the page. Pass the query refetch into the prop that already exists for this purpose.

🐛 Proposed fix at the call site (Line 589)
-        <ProfileAuthoring onSaved={() => undefined} profiles={dashboard.profiles} />
+        <ProfileAuthoring onSaved={() => void query.refetch()} profiles={dashboard.profiles} />
🤖 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/web/src/features/folder-autopilot/folder-autopilot-page.tsx` around
lines 76 - 79, Update the FolderAutopilotPage call site to pass the existing
folder-autopilot query refetch function through the onSaved prop instead of the
current no-op callback, so the profile list refreshes after submit successfully
creates a profile.
apps/web/src/features/folder-autopilot/folder-autopilot-api.ts-293-293 (1)

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

Reject an invalid revision instead of clamping it.

Math.max(1, count(...)) rewrites a server-sent revision of 0 to 1. The clamped value is then sent back as expectedRevision by pauseFolderAutopilotAssignment and requestFolderAutopilotUndo, so the optimistic concurrency check compares against a revision the record never had. Every other field in this module fails closed. Do the same here. The same clamp exists at Line 408 in parseExecution.

🐛 Proposed fix
+function revision(input: unknown): number {
+  const value = count(input);
+  if (value < 1) throw new Error(UUID_ERROR);
+  return value;
+}
-    revision: Math.max(1, count(value['revision'])),
+    revision: revision(value['revision']),
🤖 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/web/src/features/folder-autopilot/folder-autopilot-api.ts` at line 293,
Update the revision parsing in the surrounding parser and parseExecution to
reject invalid or non-positive server revisions instead of clamping them with
Math.max. Preserve valid revisions unchanged and fail closed consistently with
the module’s other fields, so pauseFolderAutopilotAssignment and
requestFolderAutopilotUndo never send a fabricated expectedRevision.
apps/web/src/features/folder-autopilot/folder-autopilot-api.ts-552-572 (1)

552-572: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Remove unsupported profile fields from the form.

FolderAutopilotProfileInput contains displayName, confidenceThreshold, approvalRequired, and dataModeConstraint, but the canonical profile contract excludes them. The form reports success while discarding these values. Remove them from the profile form and input type. If the product needs them, submit them through their owning JRA or assignment contracts. Do not add dataModeConstraint to the profile contract.

The API limits undoWindowHours to 168, but parseProfile permits 8_760. Align the parser with the API limit.

🤖 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/web/src/features/folder-autopilot/folder-autopilot-api.ts` around lines
552 - 572, Remove displayName, confidenceThreshold, approvalRequired, and
dataModeConstraint from the folder autopilot profile form and
FolderAutopilotProfileInput, leaving those concerns to their owning JRA or
assignment contracts; do not add dataModeConstraint to the profile payload.
Update parseProfile to cap undoWindowHours at 168, matching
createFolderAutopilotProfile’s boundedSeconds limit.
apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotModels.kt-45-71 (1)

45-71: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reject approval decisions after expiresAt.

A PENDING approval remains actionable after its deadline. At 2026-08-05T00:00:00Z, the bundled sample expires, but the app can still enqueue a decision and mark it locally as approved or rejected. Parse and validate the deadline, use an injected clock before queueing or transitioning state, disable expired controls, and add an expired-approval test.

  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotModels.kt#L45-L71: enforce expiresAt in the approval transition.
  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotOfflineActionQueue.kt#L29-L40: reject an expired approval before writing an offline intent.
  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotScreen.kt#L93-L104: disable approval controls when the approval has expired.
  • apps/android/app/src/main/java/com/databreeze/android/MainActivity.kt#L119-L145: do not apply an optimistic decision after an expired action.
  • apps/android/app/src/main/java/com/databreeze/android/MainActivity.kt#L189-L197: replace the release-date-bound sample deadline with time-safe fixture data.
🤖 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/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotModels.kt`
around lines 45 - 71, Reject expired pending approvals across the complete flow:
in FolderAutopilotApprovalSummary.decide, parse and validate expiresAt and
require the injected clock to be before the deadline; in
FolderAutopilotOfflineActionQueue reject expired approvals before persisting
offline intents; in FolderAutopilotScreen disable approval controls once
expired; in MainActivity prevent optimistic transitions for expired actions; and
replace the release-date-bound sample deadline with time-safe fixture data.
Update or add coverage for an expired approval.
🟡 Minor comments (15)
apps/desktop/src/features/folder-autopilot/local-journal.ts-155-159 (1)

155-159: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

An unknown operationId reports DUPLICATE_STEP.

Line 156 rejects with DUPLICATE_STEP when findIndex returns -1. An identifier that is absent from the journal is not a duplicate. The caller cannot distinguish "already recorded" from "not part of this journal", which makes a recovery bug hard to diagnose. compensateJournal repeats the same mapping on Line 178.

Use INVALID_JOURNAL for the not-found case and keep DUPLICATE_STEP for the wrong-state case.

♻️ Proposed fix
   const index = journal.steps.findIndex((step) => step.operationId === operationId);
-  if (index < 0) return reject('DUPLICATE_STEP');
+  if (index < 0) return reject('INVALID_JOURNAL');
   const step = journal.steps[index];
-  if (step === undefined) return reject('DUPLICATE_STEP');
+  if (step === undefined) return reject('INVALID_JOURNAL');
   if (step.state !== 'PENDING') return reject('DUPLICATE_STEP');
🤖 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/desktop/src/features/folder-autopilot/local-journal.ts` around lines 155
- 159, Update the not-found handling in the journal step lookup used by the
relevant function and compensateJournal: when findIndex returns -1, reject with
INVALID_JOURNAL instead of DUPLICATE_STEP. Preserve DUPLICATE_STEP for steps
found in the journal whose state is not PENDING.
apps/desktop/src/features/folder-autopilot/file-observation.ts-121-134 (1)

121-134: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Strengthen isByteArray so fingerprintBytes stays fail-closed.

isByteArray only inspects byteLength. A plain object such as { byteLength: 4 } satisfies the predicate and is asserted as Uint8Array. createHash().update() then throws a raw TypeError from node:crypto. Line 211 calls fingerprintBytes outside a try block, so that TypeError escapes instead of the stable INVALID_OBSERVATION code, and the error message is not guaranteed to be content-free.

Check the value is an ArrayBuffer view.

🛡️ Proposed fix
 function isByteArray(value: unknown): value is Uint8Array {
-  return (
-    typeof value === 'object' &&
-    value !== null &&
-    typeof (value as { readonly byteLength?: unknown }).byteLength === 'number' &&
-    Number.isSafeInteger((value as { readonly byteLength: number }).byteLength) &&
-    (value as { readonly byteLength: number }).byteLength >= 0
-  );
+  return ArrayBuffer.isView(value) && value instanceof Uint8Array;
 }
🤖 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/desktop/src/features/folder-autopilot/file-observation.ts` around lines
121 - 134, Strengthen isByteArray to require the value to be an ArrayBuffer view
before validating byteLength, while preserving the existing non-null, numeric,
safe-integer, and non-negative checks. This must reject plain objects such as {
byteLength: 4 } so fingerprintBytes returns INVALID_OBSERVATION instead of
allowing createHash().update() to throw.
services/api/src/features/fa/api/folder-autopilot.dto.ts-192-195 (1)

192-195: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject an empty decisionReason.

decisionReason is required and bounded at 500 characters, but no minimum length is set. @IsString() accepts '', so a caller can approve or reject with no reason. The field records the approval justification, so an empty value defeats its purpose.

🛡️ Proposed fix to require a non-empty reason
-  `@ApiProperty`({ maxLength: 500 })
+  `@ApiProperty`({ minLength: 1, maxLength: 500 })
   `@IsString`()
+  `@MinLength`(1)
   `@MaxLength`(500)
   decisionReason!: string;

Add MinLength to the class-validator import at lines 2-18.

🤖 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 `@services/api/src/features/fa/api/folder-autopilot.dto.ts` around lines 192 -
195, Update the DTO field decisionReason by importing and applying
class-validator’s MinLength validator with a minimum of one character, while
preserving its existing string and 500-character maximum validation.
services/api/openapi/v1.json-10557-10562 (1)

10557-10562: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Bound the version query parameter in the published contract.

The version query parameter declares minimum: 1 and no maximum. FolderAutopilotService.findProfile rejects any version above 10_000 with INVALID_VERSION, and CreateFolderAutopilotProfileDto.version declares maximum: 10000 at line 12949. The contract therefore advertises versions the API rejects.

This file is generated, so fix the source decorator in services/api/src/features/fa/api/folder-autopilot.controller.ts line 84:

♻️ Proposed fix at the decorator source
-  `@ApiQuery`({ name: 'version', required: false, type: 'integer', minimum: 1 })
+  `@ApiQuery`({ name: 'version', required: false, type: 'integer', minimum: 1, maximum: 10_000 })

One related note on the new schemas at lines 12945-13080: none declare additionalProperties: false, but the validation pipe rejects unknown fields with 400, as services/api/test/features/fa/folder-autopilot.controller.test.ts lines 75-80 confirm. Declaring the closed shape would make the contract match runtime behavior.

🤖 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 `@services/api/openapi/v1.json` around lines 10557 - 10562, Update the version
query-parameter decorator in FolderAutopilotController to declare a maximum of
10000, matching FolderAutopilotService.findProfile and
CreateFolderAutopilotProfileDto.version. Regenerate services/api/openapi/v1.json
from the source decorator; do not modify the generated contract directly.
services/api/src/features/fa/adapter/in-memory-folder-autopilot-repository.adapter.ts-189-238 (1)

189-238: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Public reads bypass the transaction queue and can observe uncommitted state.

findProfile, listProfiles, findBinding, listBindings, findAssignment, and listAssignments call the *Unlocked methods directly. withTransaction serializes only against other withTransaction calls. A transaction callback awaits between writes, so a concurrent read can observe records that a later rollback discards at lines 269-271.

FolderAutopilotService.listProfiles and listAssignments run concurrently through Promise.all in the dashboard route, so interleaving with an in-flight create transaction is reachable.

Route the public reads through withTransaction so they queue behind pending work:

♻️ Proposed fix to serialize reads
-  public findProfile(context: IamTenantContextV1, profileId: StableIdentifierV1, version?: number) {
-    return this.findProfileUnlocked(context, profileId, version);
-  }
+  public findProfile(context: IamTenantContextV1, profileId: StableIdentifierV1, version?: number) {
+    return this.withTransaction(context, (transaction) =>
+      transaction.findProfile(context, profileId, version),
+    );
+  }
 
-  public listProfiles(context: IamTenantContextV1) {
-    return this.listProfilesUnlocked(context);
-  }
+  public listProfiles(context: IamTenantContextV1) {
+    return this.withTransaction(context, (transaction) => transaction.listProfiles(context));
+  }

Note one related hazard: the public methods that already wrap withTransaction deadlock if a caller invokes them from inside another withTransaction callback, because the inner call awaits a tail that only the outer call releases. The service uses transaction.* inside callbacks today, so the deadlock is not reachable. Add a comment on withTransaction to record that the lock is not re-entrant.

🤖 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
`@services/api/src/features/fa/adapter/in-memory-folder-autopilot-repository.adapter.ts`
around lines 189 - 238, Route the public read methods findProfile, listProfiles,
findBinding, listBindings, findAssignment, and listAssignments through
withTransaction, invoking their corresponding *Unlocked methods inside the
transaction callback so reads wait for pending writes. Add a concise comment on
withTransaction documenting that its lock is not re-entrant and callers inside a
transaction must use the transaction methods directly.
services/api/src/features/fa/application/folder-autopilot.service.ts-331-347 (1)

331-347: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Map persistence failures in the list methods.

createProfile, createBinding, createAssignment, and updateAssignmentState convert repository errors into a rejected result. The three list methods do not. If the repository throws, the promise rejects and the caller receives an unmapped error instead of FA_PERSISTENCE_UNAVAILABLE. FolderAutopilotController.dashboard already handles a non-accepted result, so mapping keeps the dashboard route available during a persistence outage.

♻️ Proposed fix for consistent error mapping
   public async listProfiles(
     context: IamTenantContextV1,
   ): Promise<FolderAutopilotServiceResultV1<readonly FolderAutopilotProfileV1[]>> {
-    return Object.freeze({ accepted: true, value: await this.repository.listProfiles(context) });
+    try {
+      return Object.freeze({ accepted: true, value: await this.repository.listProfiles(context) });
+    } catch (error) {
+      return rejected<readonly FolderAutopilotProfileV1[]>(mapPersistenceError(error));
+    }
   }
 
   public async listBindings(
     context: IamTenantContextV1,
   ): Promise<FolderAutopilotServiceResultV1<readonly AutopilotFolderBindingV1[]>> {
-    return Object.freeze({ accepted: true, value: await this.repository.listBindings(context) });
+    try {
+      return Object.freeze({ accepted: true, value: await this.repository.listBindings(context) });
+    } catch (error) {
+      return rejected<readonly AutopilotFolderBindingV1[]>(mapPersistenceError(error));
+    }
   }
 
   public async listAssignments(
     context: IamTenantContextV1,
   ): Promise<FolderAutopilotServiceResultV1<readonly RecipeAssignmentV1[]>> {
-    return Object.freeze({ accepted: true, value: await this.repository.listAssignments(context) });
+    try {
+      return Object.freeze({
+        accepted: true,
+        value: await this.repository.listAssignments(context),
+      });
+    } catch (error) {
+      return rejected<readonly RecipeAssignmentV1[]>(mapPersistenceError(error));
+    }
   }

The same gap exists in findProfile, findBinding, and findAssignment at lines 295-329.

🤖 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 `@services/api/src/features/fa/application/folder-autopilot.service.ts` around
lines 331 - 347, Update listProfiles, listBindings, and listAssignments to catch
repository failures and return the same rejected result mapped to
FA_PERSISTENCE_UNAVAILABLE used by createProfile, createBinding,
createAssignment, and updateAssignmentState. Apply the identical error mapping
to findProfile, findBinding, and findAssignment, while preserving successful
accepted results and existing controller handling.
services/api/test/features/fa/folder-autopilot.service.test.ts-127-144 (1)

127-144: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the assignment isolation that the test name claims.

The test is named sibling tenant cannot read a profile or assignment, but the body only calls service.findProfile. Assignment isolation is untested, so a regression in findAssignment visibility would pass.

💚 Proposed fix to cover assignment isolation
   const tenant = context();
   await service.createProfile(tenant, profileInput);
+  await service.createBinding(tenant, bindingInput(ids.inputBindingId, 'INPUT'));
+  await service.createBinding(tenant, bindingInput(ids.outputBindingId, 'OUTPUT'));
+  await service.createAssignment(tenant, assignmentInput);
   const sibling = context(
     {
       scopeType: 'workspace',
       organizationId: ids.organizationId,
       workspaceId: 'ffffffff-ffff-4fff-8fff-ffffffffffff',
     },
     'fa-sibling',
   );
   assert.deepEqual(await service.findProfile(sibling, ids.profileId), {
     accepted: false,
     code: 'FA_PROFILE_NOT_FOUND',
   });
+  assert.deepEqual(await service.findAssignment(sibling, ids.recipeId), {
+    accepted: false,
+    code: 'FA_ASSIGNMENT_NOT_FOUND',
+  });
🤖 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 `@services/api/test/features/fa/folder-autopilot.service.test.ts` around lines
127 - 144, Extend the test case around `service.findProfile` to also call
`service.findAssignment` using the sibling context and the relevant assignment
identifier, then assert it returns the same rejected `FA_ASSIGNMENT_NOT_FOUND`
result. Ensure the test creates or references an assignment associated with the
created profile so it verifies sibling-tenant assignment isolation.
services/api/src/features/fa/api/folder-autopilot-dashboard.ts-29-29 (1)

29-29: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Align the dashboard projection with the assignment state.

  • Use 'LOCAL' | 'HYBRID' | 'CLOUD' for dataModeConstraint. Source it from RecipeAssignmentV1.dataModeConstraint; the current profile projection always reports 'Hybrid' and cannot represent LOCAL, CLOUD, or no constraint.
  • Do not map assignment updatedAt from createdAt. State transitions increment revision without changing createdAt, so the dashboard timestamp becomes stale. Persist and update updatedAt during state transitions.
🤖 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 `@services/api/src/features/fa/api/folder-autopilot-dashboard.ts` at line 29,
Update the dashboard projection’s dataModeConstraint to use the `'LOCAL' |
'HYBRID' | 'CLOUD'` values from RecipeAssignmentV1.dataModeConstraint,
preserving an absent constraint instead of defaulting to `'Hybrid'`. In the
assignment state transition flow, persist and update updatedAt independently
from createdAt so dashboard timestamps reflect revision changes.
apps/web/src/features/folder-autopilot/folder-autopilot-page.tsx-36-40 (1)

36-40: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Localize the remaining engine reason codes.

reasonLabel localizes only DESTINATION_COLLISION. The engine also emits DESTINATION_COLLISION_SKIPPED and MOVE_REQUIRES_APPROVAL (see services/engine/src/databreeze_engine/processors/folder_autopilot_plan.py:64-137). Those codes reach the preview reasonCodes list and render as raw uppercase tokens in the Vietnamese default locale. Add message keys for the codes the engine can produce, and keep the raw token only as a last-resort fallback.

As per coding guidelines: "Keep Vietnamese the default product locale and English complete."

🤖 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/web/src/features/folder-autopilot/folder-autopilot-page.tsx` around
lines 36 - 40, Update reasonLabel to localize DESTINATION_COLLISION_SKIPPED and
MOVE_REQUIRES_APPROVAL in addition to DESTINATION_COLLISION, using appMessage
with new message keys. Add Vietnamese default translations and complete English
translations for all engine reason codes, while preserving the raw value only as
the fallback for unknown codes.

Source: Coding guidelines

apps/web/src/features/folder-autopilot/folder-autopilot-page.tsx-90-90 (1)

90-90: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Localize the hardcoded English UI strings.

Line 90 renders JRA profile facade, Line 586 renders Hybrid, and lines 271 and 478 render the screen-reader label Actions. All four bypass appMessage, so they stay English in the Vietnamese default locale. The screen-reader labels matter most, because assistive-technology users on the default locale hear an English column name. Add message keys for these strings.

As per coding guidelines: "Keep Vietnamese the default product locale and English complete."

🤖 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/web/src/features/folder-autopilot/folder-autopilot-page.tsx` at line 90,
Replace the hardcoded “JRA profile facade”, “Hybrid”, and both “Actions” labels
in the folder autopilot page with appMessage lookups, adding corresponding
entries to the message definitions with Vietnamese as the default locale and
complete English translations.

Source: Coding guidelines

apps/web/src/features/folder-autopilot/folder-autopilot-page.tsx-340-345 (1)

340-345: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Show the empty state when every approval is filtered out.

Line 344 returns null for an approval with no matching preview. The empty-state check at Line 340 tests approvals.length, not the rendered count. If the dashboard returns approvals whose previews expired and were dropped from the projection, the panel renders an empty card list with no message. Derive the rendered list first, then branch on its length.

♻️ Proposed refactor
+  const pairs = approvals.flatMap((approval) => {
+    const preview = previews.find((candidate) => candidate.previewId === approval.previewId);
+    return preview === undefined ? [] : [{ approval, preview }];
+  });

Then render from pairs and test pairs.length === 0 for the empty state.

🤖 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/web/src/features/folder-autopilot/folder-autopilot-page.tsx` around
lines 340 - 345, Update the approval rendering flow in the folder autopilot page
to derive the preview-matched approval pairs before rendering. Exclude approvals
without a matching preview, render the card list from that derived collection,
and use its length for the empty-state check so the message appears when all
approvals are filtered out.
apps/web/src/app/messages.ts-110-114 (1)

110-114: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Translate the remaining Vietnamese values.

Line 114 keeps the English word Preview. Line 110 mixes Vietnamese and English in 'Tạm dừng assignment'. Vietnamese is the default locale, so these strings appear untranslated to default users. Use the same Vietnamese term you already use elsewhere (Phân công for assignment, and a Vietnamese term for preview such as Bản xem trước).

🌐 Proposed wording fix
-  'autopilot.assignment.pause': 'Tạm dừng assignment',
+  'autopilot.assignment.pause': 'Tạm dừng phân công',
@@
-  'autopilot.approval.preview': 'Preview',
+  'autopilot.approval.preview': 'Bản xem trước',

As per coding guidelines: "Keep Vietnamese the default product locale and English complete."

🤖 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/web/src/app/messages.ts` around lines 110 - 114, Update the Vietnamese
locale values in the autopilot assignment and approval translations: replace the
English “assignment” in autopilot.assignment.pause with the existing Vietnamese
term “Phân công,” and translate autopilot.approval.preview using “Bản xem trước”
or the established equivalent. Keep the remaining translations unchanged.

Source: Coding guidelines

apps/web/src/features/folder-autopilot/folder-autopilot-page.tsx-605-607 (1)

605-607: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not render an epoch date when no profile exists.

With an empty dashboard.profiles, the fallback new Date(0).toISOString() renders a formatted 1 January 1970 timestamp. The paragraph also carries no label, so the date appears with no explanation of what it represents. Omit the paragraph when there is no profile, and add a message key that names the value.

🐛 Proposed fix
-      <p className="authority-note">
-        {dateLabel(locale, dashboard.profiles[0]?.updatedAt ?? new Date(0).toISOString())}
-      </p>
+      {dashboard.profiles[0] === undefined ? null : (
+        <p className="authority-note">
+          {dateLabel(locale, dashboard.profiles[0].updatedAt)}
+        </p>
+      )}
🤖 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/web/src/features/folder-autopilot/folder-autopilot-page.tsx` around
lines 605 - 607, Update the profile date rendering in the folder autopilot page
to omit the authority-note paragraph when dashboard.profiles has no profile
instead of using an epoch-date fallback. When a profile exists, continue
formatting its updatedAt value and add/use a localization message key that
clearly labels the displayed date.
packages/domain/src/folder-autopilot/v1.ts-129-135 (1)

129-135: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject sparse binding-ID arrays.

Array.prototype.map() skips sparse indexes, so identifiers() accepts a sparse array and returns an array containing undefined. Use Array.from(input, stable) and add tests for sparse inputBindingIds and outputBindingIds.

🤖 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 `@packages/domain/src/folder-autopilot/v1.ts` around lines 129 - 135, The
identifiers function currently allows sparse arrays because map skips missing
indexes; replace the values construction with Array.from(input, stable) so every
index is validated and sparse input is rejected. Add coverage for sparse
inputBindingIds and outputBindingIds, ensuring both are rejected.
apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotScreen.kt-49-57 (1)

49-57: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Complete the Vietnamese default product UI.

The screen inserts English enum names into default resource strings. The default resource also retains Watcher and Preview, and omits the file-content privacy statement from autopilot_body.

  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotScreen.kt#L49-L57: map assignment and watcher states to localized resources.
  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotScreen.kt#L89-L92: map approval decisions to localized resources.
  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotScreen.kt#L117-L123: map outcome and undo states to localized resources.
  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotScreen.kt#L137-L141: map exception severity to localized resources while retaining technical reason codes.
  • apps/android/app/src/main/res/values/strings.xml#L13-L20: translate the remaining labels and preserve the source-path and file-content privacy meaning.

As per coding guidelines, keep Vietnamese the default product locale and English complete.

🤖 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/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotScreen.kt`
around lines 49 - 57, Complete Vietnamese localization across all listed sites:
in FolderAutopilotScreen.kt lines 49-57 map assignment and watcher states to
localized resources; lines 89-92 map approval decisions; lines 117-123 map
outcome and undo states; and lines 137-141 map exception severity while
retaining technical reason codes. In
apps/android/app/src/main/res/values/strings.xml lines 13-20 translate remaining
labels, including Watcher and Preview, and restore both source-path and
file-content privacy meaning. Keep Vietnamese as the default product locale and
provide complete English translations.

Source: Coding guidelines

Comment thread apps/web/src/features/folder-autopilot/folder-autopilot-page.tsx

@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 (3)
services/engine/src/databreeze_engine/processors/folder_autopilot.py (1)

49-66: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize stable-key validation failures.

At Line 49, an invalid modified_at_ns fails in stable_execution_key before the try block. This bypasses the INVALID_OBSERVATION error normalization used for invalid model fields.

Move stable-key derivation into the try block. Add a regression test for an invalid timestamp.

🤖 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 `@services/engine/src/databreeze_engine/processors/folder_autopilot.py` around
lines 49 - 66, Move the _stable_execution_key() call and stable_key assignment
from outside the try block into the beginning of the try block so that
validation failures in the stable-key derivation (such as invalid
modified_at_ns) are caught by the same exception handler and normalized to
ValueError("INVALID_OBSERVATION") like FileObservation constructor failures.
This ensures consistent error normalization for all observation-validation
failures.
apps/desktop/src/features/folder-autopilot/local-actions.ts (1)

274-279: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve receipts for every later execution failure.

The catch only wraps write failures. If a later operation fails stale-source validation at Line 230, prior operations can already be applied but the caller receives no appliedReceipts for journaling or compensation.

Wrap later LocalActionError failures after the first receipt in LocalActionFailure. Preserve the original error code. Add coverage for a successful first operation followed by a stale second operation.

🤖 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/desktop/src/features/folder-autopilot/local-actions.ts` around lines 274
- 279, Expand the try-catch block to encompass all operations beyond the initial
copyExclusive or renameExclusive calls, including stale-source validation and
other logic that may throw LocalActionError. When catching a LocalActionError
that occurs after a receipt has been recorded, wrap it in LocalActionFailure
while preserving the original error code from the LocalActionError and including
the accumulated receipts. This ensures that callers receive appliedReceipts even
when later operations fail after earlier operations succeed.
services/api/src/features/fa/api/folder-autopilot.controller.ts (1)

304-327: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve rejected approval and undo HTTP statuses.

Both facade methods can return accepted: false, but these handlers force HTTP 200. Add @Res({ passthrough: true }), @applyFolderAutopilotOutcomeResponses(), and preserveFolderAutopilotStatus. Map FA_JRA_APPROVAL_FACADE_UNAVAILABLE and FA_JRA_UNDO_FACADE_UNAVAILABLE to 503; the current default maps them to 400.

🤖 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 `@services/api/src/features/fa/api/folder-autopilot.controller.ts` around lines
304 - 327, Update decideApproval and requestUndo to preserve rejected-outcome
HTTP statuses instead of always returning 200. Add passthrough response
handling, applyFolderAutopilotOutcomeResponses, and
preserveFolderAutopilotStatus to both handlers, mapping
FA_JRA_APPROVAL_FACADE_UNAVAILABLE and FA_JRA_UNDO_FACADE_UNAVAILABLE to 503
while retaining the existing outcome mappings.
🤖 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 `@services/api/test/features/fa/folder-autopilot.service.test.ts`:
- Around line 132-134: Update the setup in the affected autopilot service test
to assert successful completion of both createBinding calls and createAssignment
instead of discarding their results. Before the sibling lookup assertion, use
the owner context to read the assignment by the same ID and assert that it
succeeds, ensuring the sibling rejection is validated against established data.

---

Outside diff comments:
In `@apps/desktop/src/features/folder-autopilot/local-actions.ts`:
- Around line 274-279: Expand the try-catch block to encompass all operations
beyond the initial copyExclusive or renameExclusive calls, including
stale-source validation and other logic that may throw LocalActionError. When
catching a LocalActionError that occurs after a receipt has been recorded, wrap
it in LocalActionFailure while preserving the original error code from the
LocalActionError and including the accumulated receipts. This ensures that
callers receive appliedReceipts even when later operations fail after earlier
operations succeed.

In `@services/api/src/features/fa/api/folder-autopilot.controller.ts`:
- Around line 304-327: Update decideApproval and requestUndo to preserve
rejected-outcome HTTP statuses instead of always returning 200. Add passthrough
response handling, applyFolderAutopilotOutcomeResponses, and
preserveFolderAutopilotStatus to both handlers, mapping
FA_JRA_APPROVAL_FACADE_UNAVAILABLE and FA_JRA_UNDO_FACADE_UNAVAILABLE to 503
while retaining the existing outcome mappings.

In `@services/engine/src/databreeze_engine/processors/folder_autopilot.py`:
- Around line 49-66: Move the _stable_execution_key() call and stable_key
assignment from outside the try block into the beginning of the try block so
that validation failures in the stable-key derivation (such as invalid
modified_at_ns) are caught by the same exception handler and normalized to
ValueError("INVALID_OBSERVATION") like FileObservation constructor failures.
This ensures consistent error normalization for all observation-validation
failures.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 57535106-acc4-4eed-a227-99fd2c53e2fe

📥 Commits

Reviewing files that changed from the base of the PR and between 575d4ae and 365c080.

📒 Files selected for processing (42)
  • apps/android/app/src/main/java/com/databreeze/android/MainActivity.kt
  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotModels.kt
  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotOfflineActionQueue.kt
  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotScreen.kt
  • apps/android/app/src/main/res/values-en/strings.xml
  • apps/android/app/src/main/res/values/strings.xml
  • apps/android/app/src/test/java/com/databreeze/android/folderautopilot/FolderAutopilotOfflineQueueTest.kt
  • apps/desktop/src/features/folder-autopilot/file-observation.ts
  • apps/desktop/src/features/folder-autopilot/local-actions.ts
  • apps/desktop/src/features/folder-autopilot/local-journal.ts
  • apps/desktop/test/folder-autopilot-journal.test.ts
  • apps/desktop/test/folder-autopilot-local-actions.test.ts
  • apps/desktop/test/folder-autopilot-observation.test.ts
  • apps/web/src/app/messages.ts
  • apps/web/src/features/folder-autopilot/folder-autopilot-api.ts
  • apps/web/src/features/folder-autopilot/folder-autopilot-page.tsx
  • apps/web/test/folder-autopilot-api.test.ts
  • apps/web/test/folder-autopilot-page.test.tsx
  • docs/operations/coderabbit-pr-51-disposition.md
  • packages/domain/src/folder-autopilot/v1.ts
  • packages/domain/test/folder-autopilot-v1.test.mjs
  • services/api/openapi/v1.json
  • services/api/prisma/migrations/20260804120000_fa_assignment_scope_key/migration.sql
  • services/api/prisma/schema/fa.prisma
  • services/api/src/features/fa/adapter/in-memory-folder-autopilot-repository.adapter.ts
  • services/api/src/features/fa/adapter/prisma-folder-autopilot-repository.adapter.ts
  • services/api/src/features/fa/api/folder-autopilot-dashboard.ts
  • services/api/src/features/fa/api/folder-autopilot.controller.ts
  • services/api/src/features/fa/api/folder-autopilot.dto.ts
  • services/api/src/features/fa/application/folder-autopilot.service.ts
  • services/api/src/features/fa/fa.module.ts
  • services/api/test/features/fa/folder-autopilot-dashboard.test.ts
  • services/api/test/features/fa/folder-autopilot.controller.test.ts
  • services/api/test/features/fa/folder-autopilot.service.test.ts
  • services/api/test/prisma-foundation.test.mjs
  • services/engine/src/databreeze_engine/folder_autopilot_contracts.py
  • services/engine/src/databreeze_engine/processors/folder_autopilot.py
  • services/engine/src/databreeze_engine/processors/folder_autopilot_plan.py
  • services/engine/src/databreeze_engine/registry.py
  • services/engine/tests/test_folder_autopilot_action.py
  • services/engine/tests/test_folder_autopilot_observation.py
  • services/engine/tests/test_folder_autopilot_plan.py
🚧 Files skipped from review as they are similar to previous changes (27)
  • apps/web/src/app/messages.ts
  • apps/android/app/src/main/res/values-en/strings.xml
  • packages/domain/test/folder-autopilot-v1.test.mjs
  • apps/desktop/test/folder-autopilot-observation.test.ts
  • services/api/test/prisma-foundation.test.mjs
  • services/api/prisma/schema/fa.prisma
  • services/api/src/features/fa/api/folder-autopilot-dashboard.ts
  • apps/android/app/src/main/res/values/strings.xml
  • apps/web/test/folder-autopilot-api.test.ts
  • services/engine/tests/test_folder_autopilot_observation.py
  • services/api/test/features/fa/folder-autopilot.controller.test.ts
  • services/engine/tests/test_folder_autopilot_action.py
  • apps/web/src/features/folder-autopilot/folder-autopilot-page.tsx
  • services/api/src/features/fa/adapter/in-memory-folder-autopilot-repository.adapter.ts
  • services/api/test/features/fa/folder-autopilot-dashboard.test.ts
  • services/engine/tests/test_folder_autopilot_plan.py
  • apps/desktop/src/features/folder-autopilot/file-observation.ts
  • apps/android/app/src/main/java/com/databreeze/android/MainActivity.kt
  • services/engine/src/databreeze_engine/processors/folder_autopilot_plan.py
  • packages/domain/src/folder-autopilot/v1.ts
  • apps/android/app/src/main/java/com/databreeze/android/folderautopilot/FolderAutopilotOfflineActionQueue.kt
  • services/engine/src/databreeze_engine/registry.py
  • services/api/src/features/fa/application/folder-autopilot.service.ts
  • apps/desktop/src/features/folder-autopilot/local-journal.ts
  • services/api/openapi/v1.json
  • services/api/src/features/fa/adapter/prisma-folder-autopilot-repository.adapter.ts
  • apps/web/src/features/folder-autopilot/folder-autopilot-api.ts

Comment thread services/api/test/features/fa/folder-autopilot.service.test.ts Outdated
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