revert: remove the placement reconciler — it manufactured the problem it solved
CI / lint (push) Successful in 2s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 26s
CI / backend-lint-and-test (push) Successful in 33s
Build images / build-web (push) Successful in 59s
Build images / smoke-web (push) Skipped
Build images / build-ml (push) Successful in 1m55s
Build images / promote (push) Skipped
CI / integration (push) Successful in 2m22s

Milestone #421 built a sweep that compared each image's `artist_id` to the
name of the directory holding its file, and called every mismatch a misplaced
image. It reported 33,789 of 63,605 as wrongly filed. That number described
the comparison, not the library.

What it actually was:

  32,475  (97.1%)  one artist's own folder, spelled differently
                   — Telepurte/ vs telepurte/. Same artist, same art.
     657  ( 2.0%)  loose at the images root
     328  ( 1.0%)  in a folder named after a different artist

And the 1% did not mean what the tool assumed either. `ImageProvenance`
records the post and source every file was downloaded from — the
authoritative answer, which the tool never consulted. Querying it for all 328:

    144  provenance agrees with the record  (move would be right)
     87  provenance agrees with the FOLDER  (the record is wrong; move wrong)
     53  provenance names SEVERAL artists   (no single correct folder)
     41  no provenance at all
      3  agrees with neither

So the sweep would have misfiled or arbitrarily picked for ~41% of the only
set it was really needed for. The system already knew where each file came
from; the tool inferred it from a column and a directory name instead.

Operator, 2026-09-21: *"the current system consistently records where items
are and where they came from this is just complicating something works and
doesn't need fixing."* Correct on both counts.

Removed: the service, the tasks, the model and migration 0099's table, the
/api/cleanup/layout and /placement/* endpoints, the Maintenance card and its
store actions, and the tests. 0100 drops the table (rule #22 — no legacy).

KEPT deliberately, per the operator:
- `utils.paths.canonical_subdir` — new filesystem imports derive their
  directory from the artist's slug, matching what the downloader always did.
  Not part of this tool; removing it would be churn that fixes nothing.
- The 327 files run 1 moved (InsoUwu/ -> insouwu/). Same artist either way,
  and the gallery renders them correctly.
- Everything from #4223 (three-gate dedup, 256-bit pHash) and #4234 (backup
  credential exclusion). Those fixed problems that were actually reported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-21 18:17:13 -04:00
co-authored by Claude Opus 5
parent 2dd9b956d5
commit 11a01a9686
14 changed files with 111 additions and 1850 deletions
-56
View File
@@ -71,66 +71,10 @@ export const useCleanupStore = defineStore('cleanup', () => {
return await api.post(`/api/cleanup/audit/${id}/cancel`)
}
// --- placement reconciler (milestone #421) --------------------------------
//
// Runs are server-side rows, so the DATABASE is the durable state here —
// no localStorage resurfacing (useMaintenanceTask) is needed. Reload the
// page, open it on another machine, and the run and its status are simply
// there. That also means a plan survives being walked away from for a day.
const placementRuns = ref([])
const layout = ref(null)
// The survey: which rows sit outside their artist's directory. check_disk
// additionally stats every destination (collisions, missing sources) and
// costs one stat per misplaced row over NFS, so it is opt-in.
async function loadLayout(checkDisk = false) {
layout.value = await api.get('/api/cleanup/layout', {
params: checkDisk ? { check_disk: 1 } : {},
})
return layout.value
}
// id -> name, so a run row can say "Conto" instead of "#47". The runs
// endpoint carries artist_id alone: the name belongs to the artist, and
// denormalising it into every run would go stale the moment one is renamed.
async function loadArtistNames() {
const rows = await api.get('/api/artists/names')
return Object.fromEntries((rows || []).map(a => [a.id, a.name]))
}
async function loadPlacementRuns(limit = 25) {
const body = await api.get('/api/cleanup/placement/runs', { params: { limit } })
placementRuns.value = body.runs || []
return placementRuns.value
}
// Detail carries `moves` — the plan the operator reads before agreeing.
async function getPlacementRun(id) {
return await api.get(`/api/cleanup/placement/runs/${id}`)
}
async function planPlacement(artistId = null) {
return await api.post('/api/cleanup/placement/plan', {
body: artistId === null ? {} : { artist_id: artistId },
})
}
async function applyPlacement(id) {
return await api.post(`/api/cleanup/placement/runs/${id}/apply`)
}
async function revertPlacement(id) {
return await api.post(`/api/cleanup/placement/runs/${id}/revert`)
}
return {
defaults, recentRuns,
loadDefaults,
previewMinDim, deleteMinDim,
startAudit, getAudit, loadHistory, latestAuditForRule, applyAudit, cancelAudit,
placementRuns, layout,
loadLayout, loadArtistNames, loadPlacementRuns, getPlacementRun,
planPlacement, applyPlacement, revertPlacement,
}
})