Skip to content

feat(reconcile): per-file snapshot diff of upstream drift since the migration cut - #477

Merged
aka-sacci-ccr merged 3 commits into
mainfrom
snapshot-diff-migration-drift
Aug 18, 2026
Merged

feat(reconcile): per-file snapshot diff of upstream drift since the migration cut#477
aka-sacci-ccr merged 3 commits into
mainfrom
snapshot-diff-migration-drift

Conversation

@aka-sacci-ccr

@aka-sacci-ccr aka-sacci-ccr commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Problema

Migração Deno/Fresh → TanStack leva semanas. Durante a janela, o repo de origem continua recebendo commits que nunca chegam no repo migrado — e não existia nada que detectasse isso. migrate.ts é in-place, one-shot, e não grava proveniência (nenhum SHA de origem, nenhum mapeamento source→target persistido).

O que entra

deco-reconcile (packages/blocks-cli/scripts/reconcile.ts), bin novo:

npx -p @decocms/blocks-cli deco-reconcile \
  --source <clone Fresh> --base <SHA do corte> \
  --target <clone TanStack> --target-base <SHA do commit de migração>

Diffa BASE_SHA..SOURCE_HEAD na origem e emite um patch por arquivo — segmentar é o ponto, deixa a LLM processar um de cada vez em vez de engolir um patch inteiro. Por arquivo anexa:

  • targetCandidates — basename match sobre o git ls-files do alvo, ranqueado pelo palpite convencional (src/<rel>, islands/src/components/, static/public/). Candidato para o agente confirmar, não veredito.
  • collisiongit log <target-base>..HEAD -- <candidato> no alvo, com os subjects. Não-vazio = alguém corrigiu à mão ali; reconciliar hunk a hunk em vez de aplicar a mudança upstream inteira.

Saída em <target>/.reconcile/<sourceHead:7>/: manifest.json (que também é o estado de retomada, campo done por arquivo), INDEX.md humano, patches/NNN-<slug>.patch. Zero escrita no alvo, zero julgamento.

Ruído filtrado: .deco/ (conteúdo de CMS sincroniza por canal próprio), .github/, lockfiles, binários.

.agents/skills/deco-reconcile-snapshot/SKILL.md — o protocolo de aplicação: é rebase, não re-migração. Aprender as regras de tradução do par corte↔migração em vez de lista decorada; resolver colisão perguntando se a correção local existe por causa da stack nova ou de um defeito que o upstream já resolveu; varrer o CSS aplicado atrás do dano de token de seletor que um codemod já causou antes; paridade como critério de aceite (bug upstream é portado como está); parar e reportar quando as colisões passarem do que cabe numa sentada de revisão humana.

O aplicador é o loop da skill — não há --apply. Escrever no alvo é julgamento.

Decisões

  • bin, não exports — política declarada no "//exports" do package.json.
  • Não reusa decideAction (migrate/phase-analyze.ts:212), que sabe o mapeamento real: é module-private e exige um FileRecord completo (classificação de island, detecção de re-export → leitura e parse de conteúdo). Basename match + convenção já entregam a shortlist e o agente confirma. Se o palpite errar na prática, exportar decideAction é o upgrade natural.

Verificação

packages/blocks-cli/scripts/reconcile.test.ts — 5 testes, verde. O e2e monta dois repos git temporários (origem com 2 commits, alvo com commit de migração + uma hand-fix depois) e confere que o lockfile é filtrado, que o patch carrega o hunk certo, e que a hand-fix pós-migração aparece como colisão.

Os 6 erros de typecheck no pacote são pré-existentes (confirmado com stash: 6 antes, 6 depois) e ficam todos em migrate/*.

🤖 Generated with Claude Code


Summary by cubic

Generates a per-file snapshot diff of upstream changes since the migration cut so migrated TanStack repos can safely rebase Fresh/Deno drift. Previously there was no drift detection; now the deco-reconcile CLI emits one patch per changed file, suggests target candidates, dedupes collision commits, and writes nothing to the target.

  • Adds deco-reconcile bin in @decocms/blocks-cli. Usage: --source <fresh> --target <tanstack> --snapshot <cut-sha> with optional --target-snapshot; output at <target>/.reconcile/<sourceHead:7>/ (manifest.json with done, INDEX.md, patches/*).
  • CLI change: replace --base with --snapshot; replace --target-base with optional --target-snapshot (auto-detected via the oldest add of MIGRATION_REPORT.md, else HEAD; the chosen commit is printed).
  • Filters .deco/, .github/, _fresh/, lockfiles, binaries, submodule gitlinks, and now any path the migration deletes (single source of truth in scripts/migrate/delete-sets.ts used by both analyze and reconcile).
  • Computes targetCandidates via basename match plus conventional path guesses; collision commits are deduped by SHA across candidates to avoid inflated counts.
  • Adds .agents/skills/deco-reconcile-snapshot/SKILL.md (apply protocol: rebase, not re-migration) and reconcile.test.ts covering patch emission, filtering (including delete-sets), collision dedupe, and target snapshot auto-detection.
  • No changes to existing generators or exports; functionality is opt-in via the bin.

Written for commit c541bf9. Summary will update on new commits.

Review in cubic

…igration cut

Migrations run for weeks. The Fresh/Deno source repo keeps shipping while the
migration team hand-fixes what the codemod got wrong, so upstream changes silently
never reach the migrated TanStack repo.

New `deco-reconcile` bin: diffs BASE_SHA..SOURCE_HEAD on the source and emits ONE
patch per file, plus the context needed to port each one — candidate target paths
(basename match over the target tree, ranked by the conventional guess) and, per
candidate, the target commits since the migration commit. A non-empty collision
list means someone hand-fixed that file: reconcile hunk by hunk instead of applying
the upstream change whole.

The script makes no judgement and writes nothing to the target. Applying is the
agent's job under the new `deco-reconcile-snapshot` skill, which carries the
rebase-not-re-migration protocol: learn the translation rules from the
cut↔migration commit pair, resolve collisions by asking whether the local fix
exists because of the new stack or because of a defect upstream already fixed,
sweep applied CSS for the selector-token damage a previous codemod caused, and
stop-and-report when collisions exceed one human review sitting.

`manifest.json` doubles as resume state (`done` per file). CMS content (`.deco/`)
is filtered out — it syncs through its own channel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aka-sacci-ccr
aka-sacci-ccr requested a review from a team August 18, 2026 17:05
decobot and others added 2 commits August 18, 2026 15:31
…apshot auto-detected

Two mandatory SHAs from two different repos was the wrong ergonomics. Now:

  deco-reconcile --source <fresh> --target <tanstack> --snapshot <cut sha>

`--base` → `--snapshot`, `--target-base` → `--target-snapshot`, the latter now
optional. Default is NOT target HEAD: `targetSnapshot..HEAD` would then be an
empty range and every collision list would come back empty — reading as "nobody
hand-fixed anything", which is the single worst thing to get silently wrong here.
Instead it detects the commit that added MIGRATION_REPORT.md (the only provenance
`deco-migrate` leaves), falls back to HEAD, and prints which commit it picked.

detectTargetSnapshot takes the OLDEST add, since git log is newest-first and a
re-added report must not shadow the real migration commit.

The e2e now drops the flag so auto-detection is on the tested path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y SHA

First real run (zeedog, 0098f4e..79b54eb) surfaced two defects.

19 files reported, 4 of them noise: deno.json, fresh.gen.ts, manifest.gen.ts and a
submodule gitlink. All are paths the migration deletes outright, so they showed up
as "no target match — new file?" — an invitation to migrate a file that must not
exist on the target.

The migrator already owns that list, so this hoists the six delete sets out of
phase-analyze.ts into migrate/delete-sets.ts and has both sides read from it. Kept
deliberately conservative: routes/ and apps/ are rescaffolded but a NEW upstream
route is a real change, and root-level docs are context-dependent, so those rules
stay in decideAction. Dropping a genuine upstream change is a worse failure than
showing one file too many. Submodules are filtered separately via ls-files mode
160000 — a gitlink bump has no source to port.

Second: a file with two target candidates (the island/section re-export pair)
counted one hand-fix twice. NewTitleBox read as 5 collisions, actually 3. The
inflated number misreads the "does this fit one human review sitting" gate, which
is the one call this tool exists to inform. Now deduped by SHA.

zeedog re-run: 19 → 15 files, zero false positives, same 10 real collisions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aka-sacci-ccr
aka-sacci-ccr merged commit 3f8303f into main Aug 18, 2026
1 check passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 7.45.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants