Skip to content

Add Phase 10 File Organizer - #82

Merged
RamonRDR merged 117 commits into
mainfrom
phase-10-file-organizer
Sep 2, 2026
Merged

Add Phase 10 File Organizer#82
RamonRDR merged 117 commits into
mainfrom
phase-10-file-organizer

Conversation

@RamonRDR

@RamonRDR RamonRDR commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Summary

Project 06: File Organizer adds a deterministic, collision-aware filesystem workflow for direct child files, with planning separated from mutation and explicit Linux/Windows concurrency boundaries.

Implemented

  • deterministic shallow discovery and suffix classification
  • immutable OrganizationPlan as pathname/category intent, not a live filesystem-object snapshot
  • explicit ERROR / SKIP collision policies with exact + casefold checks
  • source/category symlink boundaries and NTFS junction rejection
  • reserved .fo-stage-* / .fo-recovery-* internal namespaces
  • secure Linux execution with pinned root/category descriptors
  • execution-time source binding from fstat() on already-open source FDs
  • all accepted Linux source FDs retained through plan completion
  • O_NOFOLLOW | O_NONBLOCK source pinning and explicit readability prerequisite for recovery
  • bounded fixed-length staging names and no-replace source -> staging claims
  • atomic staging -> destination renameat2(RENAME_NOREPLACE) on Linux
  • mutation-time logical casefold recheck plus exact-name atomic no-replace commit
  • root/category anchor validation and FD cleanup on failures
  • generalized post-claim recovery with stage/source identity proof and exclusive .fo-recovery-* fallback
  • recovery durability uses fsync(recovery_fd) followed by fsync(root_fd) so the new recovery directory entry is crash-durable
  • the recovery FD is closed before the final anchored pathname identity proof, preventing an open descriptor from masking a removed recovery pathname
  • recovery pathname retention is a final point-in-time namespace proof; no indefinite guarantee is claimed against later external directory mutation
  • guarded Windows portable path with best-effort concurrency semantics and real NTFS junction coverage
  • deterministic demo, focused tests, manifest registration, and aligned EN / PT-BR / ES documentation

Identity boundary

OrganizationPlan deliberately records pathname/category intent. It does not keep live source descriptors or promise that a pathname still names the identical filesystem object observed during planning.

If a regular file is replaced at the same planned pathname before execute_plan() begins source binding, the current regular file at that pathname is selected. Strong object identity begins at execution-time pinning. On Linux, changes after that binding are rejected or conservatively recovered under the descriptor-based race contract.

Deliberate scope

Descriptor pinning stabilizes Linux filesystem-object identity, not file-content snapshots. Concurrent writes to the same inode, recursive organization, MIME inspection, duplicate auto-renaming, hashing/deduplication, cross-filesystem moves, whole-plan transactions, watchers, deletion, GUI, and cloud storage remain out of scope.

Validation status

Repository quality checks #694 passed on clean current HEAD ba72ac54e220e63b0ec6c88ac187f9fe26361109.

Ubuntu / repository gate

  • quality-script regression suite: 11 passed
  • approved executable examples: 182 passed, including File Organizer demo
  • repository practical-project suite: 327 passed, 1 platform-specific skip
  • internal Markdown links: 2,448 across 255 Markdown files, all resolved
  • repository structure validation passed

Windows / File Organizer gate

  • runner: Microsoft Windows Server 2025
  • focused File Organizer suite: 63 passed, 17 Linux-specific skips

The PR contains only the 22 intended project/documentation/CI files. The Review 16 correction is documentation-only: compared with reviewed HEAD 2230379bcc797300b24df2e30cfa05201b04a0a3, the final net change is exactly 1 line replaced in each of the EN / PT-BR / ES Project 06 READMEs. No implementation code, tests, workflow files, or unrelated documentation changed in that patch.

Codex review history

Reviews 1–10

Addressed findings included missing learning chapters/navigation, late destination overwrite, category/source symlink races, unsafe rollback/unlink windows, stale test-count docs, root/category anchor races, NAME_MAX staging, FIFO blocking, late casefold collisions, staging replacement/data recovery, readability contract, FD cleanup, Windows junctions, and immediate inode reuse.

Full Review 11

  • P2: clarified the plan/execution identity boundary by defining OrganizationPlan as pathname/category intent and binding strong source identity at execution-time descriptor pinning; regression coverage added.
  • P1: reverted unrelated Spanish/Portuguese roadmap rewrites so the PR remains focused.

Full Review 12

  • P2: generalized post-claim recovery so a missing/replaced/unproven stage causes pinned bytes to be preserved through .fo-recovery-*; added a regression for stage replacement + late exact destination + failed final rename.

Full Review 13

  • P2: closed the remaining check-to-link recovery window by accepting stage restoration only after the recreated source pathname itself is verified against the pinned identity; otherwise recovery falls back to the still-open source FD.

Full Review 14

  • P2: recovery retention is no longer inferred from successful descriptor writes and fsync() alone.
  • _recover_pinned_source_at() captures the created recovery inode identity from the open recovery FD, then re-reads the .fo-recovery-* pathname through the anchored root and requires the same regular-file (st_dev, st_ino) before returning its name.
  • a missing, renamed, replaced, or mismatched recovery pathname raises instead of falsely claiming that data was retained there; uncertain entries are not deleted or overwritten.
  • added test_recovery_path_removed_during_fsync_is_not_reported_as_retained.

Full Review 15

  • P2: closed the post-verification/pre-close recovery window by moving the final recovery pathname proof until after recovery_fd is closed. A regression removes the recovery pathname immediately after descriptor close and verifies that retention is not falsely reported.
  • P2: made the newly created recovery pathname crash-durable by syncing the recovery file first and then the anchored root directory with fsync(root_fd). A regression requires the exact recovery -> root sync order.
  • EN / PT-BR / ES chapters define recovery pathname retention as a point-in-time namespace proof and explicitly avoid claiming indefinite protection from later external namespace mutation.

Full Review 16

  • P2: qualified the failure-path source-change summary by the execution-time binding boundary in EN / PT-BR / ES.
  • a regular-file replacement before binding is documented as the current object selected by pathname intent; changes after binding are rejected under the strong object-identity contract.
  • the correction is documentation-only and the final net patch is one line replaced per localized chapter.

All submitted inline findings through Full Review 16 are addressed and resolved. Review 16 is green in CI #694.

Final gate

  • request another full Codex review of the entire PR on current HEAD ba72ac54e220e63b0ec6c88ac187f9fe26361109
  • explicitly re-check pathname-intent / execution-binding semantics, recovery FD handoff, recovery directory durability, final pathname verification, post-claim recovery, Linux descriptor lifetime/anchor handling, exact-name and casefold collision guarantees, Windows NTFS behavior, CI coverage, focused PR scope, and EN/PT-BR/ES consistency
  • if clean, retain quality checks #694 as the merge gate
  • squash merge only after explicit authorization

Branch

phase-10-file-organizer

@RamonRDR
RamonRDR marked this pull request as ready for review August 31, 2026 17:22
@RamonRDR

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0d840fd34

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread practical-projects/06-file-organizer/demo.py
Comment thread practical-projects/06-file-organizer/file_organizer.py Outdated
Comment thread practical-projects/06-file-organizer/demo.py

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 076ed6f250

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread practical-projects/06-file-organizer/file_organizer.py

RamonRDR commented Sep 1, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fcb7688d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread practical-projects/06-file-organizer/file_organizer.py
Comment thread practical-projects/06-file-organizer/file_organizer.py Outdated
Comment thread practical-projects/06-file-organizer/README.md Outdated

RamonRDR commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

@codex review

Full review of the entire PR, not only the latest diff.

Please review the full 22-file PR on current HEAD a3c7c07af0fe0bafe6595f9efb38f6d5a170b6c0 against main, with particular attention to the Full Review 14 recovery-pathname fix: post-fsync() recovery pathname identity proof, pinned source/recovery FD lifetime, post-claim failure recovery, stage/source/recovery replacement races, pathname-intent semantics, Linux root/category anchoring, exact-name and casefold collision guarantees, Windows NTFS behavior and portable-contract boundaries, CI coverage, focused PR scope, and EN/PT-BR/ES consistency.

Please inspect the entire PR for any remaining correctness, safety, resource-lifetime, portability, test, documentation, or scope issue. If there are no suggestions, please leave the clean-review 👍 reaction as usual. Do not merge.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3c7c07af0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread practical-projects/06-file-organizer/file_organizer.py Outdated
Comment thread practical-projects/06-file-organizer/file_organizer.py

RamonRDR commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

@codex address the two unresolved Review 15 P2 findings on the current HEAD. Please keep the change tightly scoped to recovery correctness: (1) eliminate the post-verification/pre-close recovery-path race without claiming stronger protection against later external namespace changes than can actually be guaranteed, and (2) fsync the anchored root directory so the newly created recovery directory entry is crash-durable before reporting recovery. Add focused regressions for both findings, keep EN/PT-BR/ES documentation consistent if the recovery contract wording changes, run the focused File Organizer tests, and do not merge or delete the branch.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

Testing

  • pytest -q practical-projects/06-file-organizer/tests — 79 passed, 1 skipped.
  • python scripts/check_internal_links.py — all 2,448 internal links across 255 Markdown files resolved.
  • python scripts/validate_repository_structure.py — repository structure is valid.
  • python -m py_compile practical-projects/06-file-organizer/file_organizer.py practical-projects/06-file-organizer/tests/test_atomic_move.py
  • git diff --check
  • git status --short --branch — clean working tree after commit.

View task →

RamonRDR commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

@codex please provide the exact unified diff for your completed local commit c497a74 (Make recovery handoff crash-durable) against PR HEAD a3c7c07af0fe0bafe6595f9efb38f6d5a170b6c0. Do not make any additional code changes. Return the complete patch for all files changed by that commit so it can be applied verbatim to the existing phase-10-file-organizer branch. Do not merge or delete anything.

@chatgpt-codex-connector

Copy link
Copy Markdown

I can’t provide the requested exact patch because commit c497a74 is not present in this repository’s object database, and the environment cannot access GitHub to retrieve it. Reconstructing a patch from the PR comments would not be guaranteed byte-for-byte identical and therefore could not safely be advertised as applicable verbatim.

Checks

  • git rev-parse a3c7c07af0fe0bafe6595f9efb38f6d5a170b6c0 — confirmed PR HEAD is available locally.
  • git cat-file -e c497a74^{commit}c497a74 is not a valid local Git object.
  • git reflog --all | rg 'c497a74|Make recovery handoff' — found no matching reflog entry.
  • git fsck --full --no-reflogs --unreachable — found no recoverable dangling commit.
  • ⚠️ git fetch --no-tags https://github.com/RamonRDR/python-study-guide.git c497a74 — network proxy rejected access with HTTP 403.
  • git status --short --branch — working tree remains clean; no code changes, commits, merges, branch deletions, or pull requests were made.

Once c497a74 is available locally, the complete patch can be generated exactly with:

git diff --no-ext-diff --no-color --full-index --binary \
  a3c7c07af0fe0bafe6595f9efb38f6d5a170b6c0 \
  c497a74

View task →

RamonRDR commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

@codex re-apply the Review 15 recovery fix from scratch on a fresh workspace based on current PR HEAD a3c7c07af0fe0bafe6595f9efb38f6d5a170b6c0, using the same validated design: close the recovery FD before the final anchored pathname identity proof; fsync the recovery file and then the anchored root directory for directory-entry durability; explicitly bound the pathname guarantee to the final point-in-time verification rather than claiming protection from later external namespace changes; add focused regressions for removal after recovery-FD close and root-directory fsync ordering; align EN/PT-BR/ES recovery documentation. Run the focused tests and validations. IMPORTANT: do not commit. At the end, paste the COMPLETE git diff -- practical-projects/06-file-organizer/file_organizer.py practical-projects/06-file-organizer/tests/test_atomic_move.py practical-projects/06-file-organizer/README.md practical-projects/06-file-organizer/README.pt-BR.md practical-projects/06-file-organizer/README.es.md into your GitHub response so the patch can be applied verbatim. Do not merge or delete anything.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

Testing

  • pytest -q practical-projects/06-file-organizer/tests/test_atomic_move.py — 19 passed.
  • pytest -q practical-projects/06-file-organizer/tests — 79 passed, 1 skipped.
  • python scripts/check_internal_links.py — all 2,448 links across 255 Markdown files resolved.
  • python scripts/validate_repository_structure.py — repository structure is valid.
  • python -m py_compile practical-projects/06-file-organizer/file_organizer.py practical-projects/06-file-organizer/tests/test_atomic_move.py
  • git diff --check
  • git status --short --branch — clean working tree.

Complete unified diff

diff --git a/practical-projects/06-file-organizer/README.es.md b/practical-projects/06-file-organizer/README.es.md
index 9085474..5a7fb4a 100644
--- a/practical-projects/06-file-organizer/README.es.md
+++ b/practical-projects/06-file-organizer/README.es.md
@@ -273,7 +273,7 @@ Los errores concurrentes pueden dejar estado incierto. La recuperación prioriza
 
 Si la ejecución ya movió el origen al staging y después detecta una condición insegura, puede crear un hard link no-replace de vuelta al nombre de origen cuando sea posible. No elimina a ciegas el staging.
 
-Un pathname de staging no funciona como lock de inode. Después de reclamar el origen, cada ruta de fallo vuelve a comprobar si la entrada de staging todavía coincide con la identidad fijada del origen. Si coincide, la ejecución puede intentar recrear el nombre original mediante un hard link no-replace desde ese staging comprobado, pero la restauración solo se acepta después de volver a leer el propio pathname recreado del origen y verificar que conserva la identidad fijada. Si el link falla, sufre una carrera hacia otro objeto, deja ausente el nombre de origen o la identidad posterior al link no coincide, la ejecución deja intactas las entradas inciertas y, antes de cerrar el descriptor todavía fijado del origen, copia los bytes planificados a un archivo regular exclusivo `.fo-recovery-*`. Ese recovery no se informa como conservado solo porque su descriptor se haya escrito y `fsync()` haya terminado: mientras el descriptor de recovery sigue abierto, la ejecución vuelve a leer el pathname de recuperación a través de la raíz anclada y exige que nombre el mismo archivo regular `(st_dev, st_ino)`. Si el pathname desaparece, se renombra o se reemplaza, la ejecución falla en lugar de afirmar falsamente que los datos quedaron retenidos, sin borrar ni sobrescribir entradas inciertas de terceros. Esto también cubre un fallo final de `RENAME_NOREPLACE` causado por un destino que aparece después de una carrera sobre el staging. Si un staging de reemplazo se renombra con éxito y la verificación de identidad del destino detecta la divergencia, el destino ajeno también queda intacto mientras se recuperan los bytes fijados. La recuperación solo afirma conservación cuando el propio pathname usado para informarla queda demostrado.
+Un pathname de staging no funciona como lock de inode. Después de reclamar el origen, cada ruta de fallo vuelve a comprobar si la entrada de staging todavía coincide con la identidad fijada del origen. Si coincide, la ejecución puede intentar recrear el nombre original mediante un hard link no-replace desde ese staging comprobado, pero la restauración solo se acepta después de volver a leer el propio pathname recreado del origen y verificar que conserva la identidad fijada. Si el link falla, sufre una carrera hacia otro objeto, deja ausente el nombre de origen o la identidad posterior al link no coincide, la ejecución deja intactas las entradas inciertas y, antes de cerrar el descriptor todavía fijado del origen, copia los bytes planificados a un archivo regular exclusivo `.fo-recovery-*`. Ese recovery no se informa como conservado solo porque su descriptor se haya escrito y sincronizado. La ejecución primero llama a `fsync()` sobre el archivo de recuperación y luego sobre el directorio raíz anclado, haciendo duraderos frente a fallos tanto el contenido como la nueva entrada de directorio. Cierra el descriptor de recovery antes de la comprobación final en un punto temporal, vuelve a leer entonces el pathname de recuperación a través de la raíz anclada y exige que nombre el mismo archivo regular `(st_dev, st_ino)`. Si el pathname desaparece, se renombra o se reemplaza en esa comprobación final, la ejecución falla en lugar de afirmar falsamente que los datos quedaron retenidos, sin borrar ni sobrescribir entradas inciertas de terceros. Esta prueba no puede impedir que un agente externo cambie el namespace después de la comprobación. Esto también cubre un fallo final de `RENAME_NOREPLACE` causado por un destino que aparece después de una carrera sobre el staging. Si un staging de reemplazo se renombra con éxito y la verificación de identidad del destino detecta la divergencia, el destino ajeno también queda intacto mientras se recuperan los bytes fijados. La recuperación solo afirma conservación cuando el propio pathname usado para informarla queda demostrado.
 
 Por ello, la ejecución segura en Linux exige deliberadamente permiso de lectura para cada archivo regular planificado. La legibilidad se valida antes de crear los directorios de categoría y de nuevo al fijar el inode del origen para la mutación; los fallos de permisos se informan como `PermissionError`, no como un falso cambio de identidad del origen.
 
diff --git a/practical-projects/06-file-organizer/README.md b/practical-projects/06-file-organizer/README.md
index 8eb1fd5..eaabaab 100644
--- a/practical-projects/06-file-organizer/README.md
+++ b/practical-projects/06-file-organizer/README.md
@@ -273,7 +273,7 @@ Concurrency errors can leave uncertain state. Recovery therefore favors preserva
 
 If execution has already claimed the source into a staging entry and later detects an unsafe condition, it may create a no-replace hard link back to the original source name when possible. It does not blindly delete the staging entry.
 
-A staging pathname is not an inode lock. After the source has been claimed, every failure path rechecks whether the staging entry still matches the pinned source identity. If it does, execution may attempt a no-replace hard link from that proven stage back to the original source name, but restoration is accepted only after the recreated source pathname itself is re-read and verified to have the pinned identity. If the link fails, races to a different object, leaves the source name missing, or the post-link source identity does not match, execution leaves uncertain entries untouched and, before closing the still-pinned source file descriptor, copies the planned source bytes into an exclusive `.fo-recovery-*` regular file. That recovery file is not reported as retained merely because its descriptor was written and `fsync()` completed: while the recovery descriptor is still open, execution re-reads the recovery pathname through the anchored root and requires it to name the same regular-file `(st_dev, st_ino)`. A missing, renamed, or replaced recovery pathname raises instead of falsely claiming durable retention, and uncertain third-party entries are not deleted or overwritten. This also covers a final `RENAME_NOREPLACE` failure caused by a destination that appears after the stage was raced. If a replacement stage is successfully renamed and destination identity verification detects the mismatch, the unrelated destination is likewise left intact while the pinned bytes are recovered. Recovery preserves data only when the pathname used to report that preservation is itself proven.
+A staging pathname is not an inode lock. After the source has been claimed, every failure path rechecks whether the staging entry still matches the pinned source identity. If it does, execution may attempt a no-replace hard link from that proven stage back to the original source name, but restoration is accepted only after the recreated source pathname itself is re-read and verified to have the pinned identity. If the link fails, races to a different object, leaves the source name missing, or the post-link source identity does not match, execution leaves uncertain entries untouched and, before closing the still-pinned source file descriptor, copies the planned source bytes into an exclusive `.fo-recovery-*` regular file. That recovery file is not reported as retained merely because its descriptor was written and synchronized. Execution first calls `fsync()` on the recovery file and then on the anchored root directory so both the contents and new directory entry are crash-durable. It closes the recovery descriptor before the final point-in-time check, then re-reads the recovery pathname through the anchored root and requires it to name the same regular-file `(st_dev, st_ino)`. A missing, renamed, or replaced recovery pathname at that final check raises instead of falsely claiming retention, and uncertain third-party entries are not deleted or overwritten. This proof cannot prevent an external actor from changing the namespace after the check. This also covers a final `RENAME_NOREPLACE` failure caused by a destination that appears after the stage was raced. If a replacement stage is successfully renamed and destination identity verification detects the mismatch, the unrelated destination is likewise left intact while the pinned bytes are recovered. Recovery preserves data only when the pathname used to report that preservation is itself proven.
 
 Safe Linux execution therefore deliberately requires read access to each planned regular file. Readability is validated before category directories are created and again when the source inode is pinned for mutation; permission failures are reported as `PermissionError`, not as a false source-identity change.
 
diff --git a/practical-projects/06-file-organizer/README.pt-BR.md b/practical-projects/06-file-organizer/README.pt-BR.md
index 4771273..5153b87 100644
--- a/practical-projects/06-file-organizer/README.pt-BR.md
+++ b/practical-projects/06-file-organizer/README.pt-BR.md
@@ -273,7 +273,7 @@ Erros concorrentes podem deixar estado incerto. A recuperação prioriza preserv
 
 Se a execução já moveu a origem para staging e depois detecta condição insegura, ela pode criar um hard link no-replace de volta para o nome de origem quando possível. Ela não apaga cegamente o staging.
 
-Um pathname de staging não funciona como lock de inode. Depois que a origem foi claimada, todo caminho de falha revalida se a entrada de staging ainda corresponde à identidade pinada da origem. Se corresponder, a execução pode tentar recriar o nome original por hard link no-replace a partir desse staging comprovado, mas a restauração só é aceita depois que o próprio pathname recriado da origem é relido e verificado com a identidade pinada. Se o link falhar, sofrer corrida para outro objeto, deixar o nome de origem ausente ou a identidade pós-link não corresponder, a execução deixa entradas incertas intactas e, antes de fechar o descritor ainda pinado da origem, copia os bytes planejados para um arquivo regular exclusivo `.fo-recovery-*`. Esse recovery não é reportado como preservado apenas porque seu descritor foi gravado e o `fsync()` terminou: enquanto o descritor do recovery ainda está aberto, a execução relê o pathname de recuperação pelo root ancorado e exige que ele aponte para o mesmo arquivo regular `(st_dev, st_ino)`. Se o pathname sumir, for renomeado ou substituído, a execução falha em vez de afirmar falsamente que os dados foram retidos, sem excluir nem sobrescrever entradas incertas de terceiros. Isso também cobre uma falha final de `RENAME_NOREPLACE` causada por um destino que aparece depois de uma corrida sobre o staging. Se um staging substituto for renomeado com sucesso e a verificação de identidade do destino detectar a divergência, o destino alheio também permanece intacto enquanto os bytes pinados são recuperados. A recuperação só afirma preservação quando o próprio pathname usado para reportá-la é comprovado.
+Um pathname de staging não funciona como lock de inode. Depois que a origem foi claimada, todo caminho de falha revalida se a entrada de staging ainda corresponde à identidade pinada da origem. Se corresponder, a execução pode tentar recriar o nome original por hard link no-replace a partir desse staging comprovado, mas a restauração só é aceita depois que o próprio pathname recriado da origem é relido e verificado com a identidade pinada. Se o link falhar, sofrer corrida para outro objeto, deixar o nome de origem ausente ou a identidade pós-link não corresponder, a execução deixa entradas incertas intactas e, antes de fechar o descritor ainda pinado da origem, copia os bytes planejados para um arquivo regular exclusivo `.fo-recovery-*`. Esse recovery não é reportado como preservado apenas porque seu descritor foi gravado e sincronizado. A execução primeiro chama `fsync()` no arquivo de recuperação e depois no diretório root ancorado, tornando duráveis contra falha tanto o conteúdo quanto a nova entrada de diretório. Ela fecha o descritor do recovery antes da verificação final em um ponto no tempo, relê então o pathname de recuperação pelo root ancorado e exige que ele aponte para o mesmo arquivo regular `(st_dev, st_ino)`. Se o pathname sumir, for renomeado ou substituído nessa verificação final, a execução falha em vez de afirmar falsamente que os dados foram retidos, sem excluir nem sobrescrever entradas incertas de terceiros. Essa comprovação não impede que um agente externo altere o namespace depois da verificação. Isso também cobre uma falha final de `RENAME_NOREPLACE` causada por um destino que aparece depois de uma corrida sobre o staging. Se um staging substituto for renomeado com sucesso e a verificação de identidade do destino detectar a divergência, o destino alheio também permanece intacto enquanto os bytes pinados são recuperados. A recuperação só afirma preservação quando o próprio pathname usado para reportá-la é comprovado.
 
 Por isso, a execução segura no Linux exige deliberadamente permissão de leitura para cada arquivo regular planejado. A legibilidade é validada antes da criação das pastas de categoria e novamente ao pinar o inode da origem para a mutação; falhas de permissão são reportadas como `PermissionError`, e não como uma falsa mudança de identidade da origem.
 
diff --git a/practical-projects/06-file-organizer/file_organizer.py b/practical-projects/06-file-organizer/file_organizer.py
index 7cbce40..0065d8a 100644
--- a/practical-projects/06-file-organizer/file_organizer.py
+++ b/practical-projects/06-file-organizer/file_organizer.py
@@ -732,24 +732,28 @@ def _recover_pinned_source_at(
                 view = view[written:]
         os.fchmod(recovery_fd, mode)
         os.fsync(recovery_fd)
-
-        try:
-            recovery_path_identity = _regular_identity_at(
-                recovery_name,
-                directory_fd=root_fd,
-            )
-        except OSError as exc:
-            raise RuntimeError(
-                f"recovery pathname changed during execution: {recovery_name}"
-            ) from exc
-        if recovery_path_identity != recovery_identity:
-            raise RuntimeError(
-                f"recovery pathname changed during execution: {recovery_name}"
-            )
+        os.fsync(root_fd)
     finally:
         os.lseek(source_fd, original_offset, os.SEEK_SET)
         os.close(recovery_fd)
 
+    # Closing the recovery FD before this final proof removes a check-to-close
+    # window. This proves only what the pathname names at this instant; an
+    # external actor can still change the namespace after verification.
+    try:
+        recovery_path_identity = _regular_identity_at(
+            recovery_name,
+            directory_fd=root_fd,
+        )
+    except OSError as exc:
+        raise RuntimeError(
+            f"recovery pathname changed during execution: {recovery_name}"
+        ) from exc
+    if recovery_path_identity != recovery_identity:
+        raise RuntimeError(
+            f"recovery pathname changed during execution: {recovery_name}"
+        )
+
     return recovery_name
 
 
diff --git a/practical-projects/06-file-organizer/tests/test_atomic_move.py b/practical-projects/06-file-organizer/tests/test_atomic_move.py
index f7e3873..5aa0b4f 100644
--- a/practical-projects/06-file-organizer/tests/test_atomic_move.py
+++ b/practical-projects/06-file-organizer/tests/test_atomic_move.py
@@ -39,7 +39,10 @@ def test_recovery_path_removed_during_fsync_is_not_reported_as_retained(
     monkeypatch.setattr(file_organizer.os, "fsync", unlink_recovery_during_fsync)
 
     try:
-        with pytest.raises(RuntimeError, match="recovery pathname changed during execution"):
+        with pytest.raises(
+            RuntimeError,
+            match="recovery pathname changed during execution",
+        ):
             file_organizer._recover_pinned_source_at(
                 source_fd,
                 source.name,
@@ -57,6 +60,91 @@ def test_recovery_path_removed_during_fsync_is_not_reported_as_retained(
         os.close(root_fd)
 
 
+def test_recovery_path_removed_after_fd_close_is_not_reported_as_retained(
+    monkeypatch: pytest.MonkeyPatch,
+    tmp_path: Path,
+) -> None:
+    if not file_organizer._supports_secure_directory_fds():
+        pytest.skip("secure directory descriptors are unavailable on this platform")
+
+    source = tmp_path / "notes.txt"
+    source.write_text("planned source", encoding="utf-8")
+    root_fd = file_organizer._open_source_directory_fd(tmp_path)
+    source_fd = os.open(source, os.O_RDONLY)
+    original_close = os.close
+    recovery_unlinked = False
+
+    def unlink_recovery_after_close(fd: int) -> None:
+        nonlocal recovery_unlinked
+        original_close(fd)
+        if fd in {source_fd, root_fd} or recovery_unlinked:
+            return
+        recovery_files = list(tmp_path.glob(".fo-recovery-*"))
+        if recovery_files:
+            assert len(recovery_files) == 1
+            recovery_files[0].unlink()
+            recovery_unlinked = True
+
+    monkeypatch.setattr(file_organizer.os, "close", unlink_recovery_after_close)
+
+    try:
+        with pytest.raises(
+            RuntimeError,
+            match="recovery pathname changed during execution",
+        ):
+            file_organizer._recover_pinned_source_at(
+                source_fd,
+                source.name,
+                root_fd=root_fd,
+            )
+
+        assert recovery_unlinked
+        assert not list(tmp_path.glob(".fo-recovery-*"))
+        os.lseek(source_fd, 0, os.SEEK_SET)
+        assert os.read(source_fd, 1024) == b"planned source"
+    finally:
+        original_close(source_fd)
+        original_close(root_fd)
+
+
+def test_recovery_fsyncs_file_before_anchored_root_directory(
+    monkeypatch: pytest.MonkeyPatch,
+    tmp_path: Path,
+) -> None:
+    if not file_organizer._supports_secure_directory_fds():
+        pytest.skip("secure directory descriptors are unavailable on this platform")
+
+    source = tmp_path / "notes.txt"
+    source.write_text("planned source", encoding="utf-8")
+    root_fd = file_organizer._open_source_directory_fd(tmp_path)
+    source_fd = os.open(source, os.O_RDONLY)
+    original_fsync = os.fsync
+    synced_fds: list[int] = []
+
+    def record_fsync(fd: int) -> None:
+        synced_fds.append(fd)
+        original_fsync(fd)
+
+    monkeypatch.setattr(file_organizer.os, "fsync", record_fsync)
+
+    try:
+        recovery_name = file_organizer._recover_pinned_source_at(
+            source_fd,
+            source.name,
+            root_fd=root_fd,
+        )
+
+        assert len(synced_fds) == 2
+        assert synced_fds[0] != root_fd
+        assert synced_fds[1] == root_fd
+        assert (tmp_path / recovery_name).read_text(
+            encoding="utf-8"
+        ) == "planned source"
+    finally:
+        os.close(source_fd)
+        os.close(root_fd)
+
+
 def test_execute_plan_never_replaces_destination_created_after_preflight(
     monkeypatch: pytest.MonkeyPatch,
     tmp_path: Path,

View task →

RamonRDR commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2230379bcc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread practical-projects/06-file-organizer/README.md Outdated

RamonRDR commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

@codex address that feedback

Please address only the currently open Full Review 16 P2, Qualify source-change failures by the binding boundary, and keep the patch documentation-only. Update the Planned source changes / Origem planejada muda / Origen planificado cambia failure summaries in README.md, README.pt-BR.md, and README.es.md so they match the existing pathname-intent contract: a regular-file replacement before execution-time binding is accepted as the current object selected by the plan, while changes after binding are rejected. Do not change implementation code, tests, unrelated wording, or PR scope.

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

Testing

  • python scripts/check_internal_links.py — all 2,448 internal links across 254 Markdown files resolved.
  • git diff --check
  • git status --short --branch — working tree clean after commit.

View task →

RamonRDR commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: ba72ac54e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

RamonRDR commented Sep 2, 2026

Copy link
Copy Markdown
Owner Author

🏁 Historical Record: Phase 10 File Organizer

This comment records the review history of PR #82 — Add Phase 10 File Organizer, which became the longest and most intensively reviewed pull request we have worked through in this repository so far.

Timeline

  • PR opened: 2026-08-31 17:09:47 UTC
  • Final clean Codex review: 2026-09-02 01:08:15 UTC
  • Elapsed cycle time: 31h 58m 28s
  • Estimated active engineering/review time: approximately 20 hours
  • Final reviewed HEAD: ba72ac54e220e63b0ec6c88ac187f9fe26361109

Final PR numbers

  • 17 Codex review cycles
    • Reviews 1 through 16 produced findings or follow-up corrections
    • Review 17 finished clean with: “Didn't find any major issues. 🎉”
  • 27 review findings addressed and resolved across the review history
  • 117 commits
  • 22 changed files
  • 4,186 additions
  • 36 deletions
  • 4,222 lines changed in the final PR diff
  • +4,150 net lines
  • 34 PR conversation comments
  • 55 inline review comments
  • 131 GitHub Actions workflow runs on the branch
    • 114 triggered by the pull_request event
    • 84 successful runs
    • 13 failed runs
    • 25 cancelled runs
    • 9 action_required runs

The final diff numbers do not include all historical churn. Many lines were written, removed, rewritten, and hardened again across the 117 commits, so the actual amount of engineering change during the review loop was substantially larger than the final +4,186 / -36 diff.

Final validation gate

Repository quality checks #694: GREEN

Ubuntu

  • 11 quality regression tests passed
  • 182 approved executable examples passed
  • 327 practical-project tests passed
  • 1 platform-specific skip
  • 2,448 internal Markdown links across 255 files validated
  • repository structure validation passed

Windows Server 2025

  • 63 focused File Organizer tests passed
  • 17 Linux-specific skips

What this project became

What began as a deterministic File Organizer evolved into a much deeper filesystem-engineering exercise involving:

  • atomic no-replace filesystem mutation
  • renameat2(RENAME_NOREPLACE)
  • pinned root, category, and source file descriptors
  • (st_dev, st_ino) filesystem-object identity
  • source and category symlink boundaries
  • NTFS junction handling
  • exact-name and casefold collision races
  • fixed-length staging namespaces
  • conservative post-claim recovery
  • pinned-FD recovery fallback
  • crash durability with fsync(recovery_fd) followed by fsync(root_fd)
  • final recovery-path identity verification
  • explicit Linux versus Windows concurrency contracts
  • EN / PT-BR / ES documentation kept aligned with implementation semantics

Several review findings exposed race windows that only became visible after earlier fixes had strengthened the surrounding state machine. Each finding was analyzed, corrected, covered by a focused regression where appropriate, validated by CI, and then subjected to another full review.

Final milestone

After 117 commits, 17 review cycles, 27 addressed findings, 131 workflow runs, and approximately 20 hours of active engineering/review work, the final Codex verdict on ba72ac54e2 was:

Codex Review: Didn't find any major issues. 🎉

Phase 10, Step 5 — File Organizer: REVIEW COMPLETE. 🐍🏁

This PR is officially recorded as our longest review marathon in the Python Study Guide to date.

@RamonRDR
RamonRDR merged commit 3aa08f1 into main Sep 2, 2026
2 checks passed
@RamonRDR
RamonRDR deleted the phase-10-file-organizer branch September 2, 2026 01:26
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