settag 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. settag-0.1.0/.gitignore +17 -0
  2. settag-0.1.0/DESIGN.md +512 -0
  3. settag-0.1.0/LICENSE +661 -0
  4. settag-0.1.0/Makefile +63 -0
  5. settag-0.1.0/PKG-INFO +688 -0
  6. settag-0.1.0/PRODUCT.md +124 -0
  7. settag-0.1.0/README.md +654 -0
  8. settag-0.1.0/THIRD_PARTY_NOTICES.md +115 -0
  9. settag-0.1.0/pnpm-lock.yaml +9 -0
  10. settag-0.1.0/pyproject.toml +138 -0
  11. settag-0.1.0/scripts/render_site_app_image.py +193 -0
  12. settag-0.1.0/src/settag/__init__.py +3 -0
  13. settag-0.1.0/src/settag/__main__.py +4 -0
  14. settag-0.1.0/src/settag/analysis_worker.py +328 -0
  15. settag-0.1.0/src/settag/analyzer.py +356 -0
  16. settag-0.1.0/src/settag/catalog.py +167 -0
  17. settag-0.1.0/src/settag/cli/__init__.py +12 -0
  18. settag-0.1.0/src/settag/cli/args.py +300 -0
  19. settag-0.1.0/src/settag/cli/commands.py +616 -0
  20. settag-0.1.0/src/settag/cli/render.py +468 -0
  21. settag-0.1.0/src/settag/config.py +78 -0
  22. settag-0.1.0/src/settag/hashing.py +219 -0
  23. settag-0.1.0/src/settag/hygiene.py +419 -0
  24. settag-0.1.0/src/settag/journal.py +501 -0
  25. settag-0.1.0/src/settag/model_store.py +192 -0
  26. settag-0.1.0/src/settag/plans.py +562 -0
  27. settag-0.1.0/src/settag/policy.py +128 -0
  28. settag-0.1.0/src/settag/records.py +229 -0
  29. settag-0.1.0/src/settag/scanner.py +45 -0
  30. settag-0.1.0/src/settag/state.py +422 -0
  31. settag-0.1.0/src/settag/tags.py +972 -0
  32. settag-0.1.0/src/settag/tasks.py +45 -0
  33. settag-0.1.0/src/settag/taxonomy.py +52 -0
  34. settag-0.1.0/src/settag/tui/__init__.py +30 -0
  35. settag-0.1.0/src/settag/tui/app.py +1729 -0
  36. settag-0.1.0/src/settag/tui/entries.py +121 -0
  37. settag-0.1.0/src/settag/tui/hygiene.py +462 -0
  38. settag-0.1.0/src/settag/tui/screens.py +334 -0
  39. settag-0.1.0/src/settag/tui/style.py +323 -0
  40. settag-0.1.0/src/settag/tui/table.py +239 -0
  41. settag-0.1.0/src/settag/workflow.py +891 -0
  42. settag-0.1.0/tests/conftest.py +17 -0
  43. settag-0.1.0/tests/fixtures/README.md +24 -0
  44. settag-0.1.0/tests/fixtures/tagged.flac +0 -0
  45. settag-0.1.0/tests/fixtures/tagged.m4a +0 -0
  46. settag-0.1.0/tests/test_analysis_worker.py +197 -0
  47. settag-0.1.0/tests/test_analyzer.py +181 -0
  48. settag-0.1.0/tests/test_architecture.py +115 -0
  49. settag-0.1.0/tests/test_cli.py +1346 -0
  50. settag-0.1.0/tests/test_config.py +62 -0
  51. settag-0.1.0/tests/test_hashing.py +139 -0
  52. settag-0.1.0/tests/test_hygiene.py +207 -0
  53. settag-0.1.0/tests/test_hygiene_tui.py +209 -0
  54. settag-0.1.0/tests/test_journal.py +212 -0
  55. settag-0.1.0/tests/test_model_store.py +113 -0
  56. settag-0.1.0/tests/test_policy.py +122 -0
  57. settag-0.1.0/tests/test_records.py +191 -0
  58. settag-0.1.0/tests/test_scanner.py +32 -0
  59. settag-0.1.0/tests/test_state.py +450 -0
  60. settag-0.1.0/tests/test_tags.py +547 -0
  61. settag-0.1.0/tests/test_tui.py +1486 -0
  62. settag-0.1.0/tests/test_tui_table.py +147 -0
  63. settag-0.1.0/tests/test_workflow.py +567 -0
  64. settag-0.1.0/uv.lock +668 -0
@@ -0,0 +1,17 @@
1
+ .DS_Store
2
+ .python-version
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ __pycache__/
7
+ *.egg-info/
8
+ *.py[cod]
9
+ analysis.jsonl
10
+ models/
11
+ .context/model-eval/
12
+
13
+ # site/ carries its own ignore file for build output; these are belt-and-braces
14
+ # so a stray node_modules can never reach the sdist.
15
+ node_modules/
16
+ site/dist/
17
+ .impeccable
settag-0.1.0/DESIGN.md ADDED
@@ -0,0 +1,512 @@
1
+ # settag design
2
+
3
+ ## Product boundary
4
+
5
+ ```text
6
+ audio files → metadata scan → choose → analysis → staged plan → verified write
7
+ │ │ ↑
8
+ │ └→ hygiene review → cleanup plan ──────┘
9
+ └────────────── Textual UI ─────────────────────────┤
10
+ │ ↕
11
+ │ local SQLite workbench
12
+ plain CLI / JSONL ─────────────┘
13
+ ```
14
+
15
+ SetTag is independent from SetPath. SetPath may consume the resulting metadata
16
+ but should not install, invoke, bundle, or write through SetTag.
17
+
18
+ ## One workflow, two presentations
19
+
20
+ `src/settag/workflow.py` owns the reusable application operations:
21
+
22
+ - inspect existing standard and SetTag metadata without loading the model
23
+ - classify metadata as never analyzed, up to date, needing reanalysis, or incomplete
24
+ - prepare a track and its evidence
25
+ - build a batch while isolating analysis errors
26
+ - preflight saved or in-memory plans
27
+ - apply and verify prepared writes, journaling each completed one
28
+ - preflight and apply an undo of a previous write
29
+ - persist compact plans
30
+
31
+ `src/settag/hygiene.py` owns the independent, model-free cleanup operations:
32
+
33
+ - inspect supported comment-like and generated text fields
34
+ - turn deterministic rules into individually reviewable findings
35
+ - preflight selected field-level cleanup plans
36
+ - apply, verify, and journal cleanup writes
37
+
38
+ The Textual app and plain CLI are presentation and input adapters over those
39
+ operations. Metadata policy does not live in either UI.
40
+
41
+ Undo is deliberately built the same way: `preflight_undo` and `apply_undo`
42
+ mirror `preflight_plan` and `apply_prepared`, and both the `U` key in the app
43
+ and `settag undo` are adapters over them. Neither UI decides what an undo
44
+ restores, and the human-readable change lines come from one place
45
+ (`WriteRecord.readable_changes`, reusing `plans.friendly_change`) so the two
46
+ never drift.
47
+
48
+ ### Where the presentations live
49
+
50
+ ```text
51
+ settag/cli/ args the accepted command grammar
52
+ commands dispatch and the work each command performs
53
+ render everything printed, prompted, or logged
54
+
55
+ settag/tui/ app phases, selection, background work
56
+ hygiene independent metadata-hygiene review
57
+ screens modal dialogs
58
+ table column layout and row rendering
59
+ entries the per-track state a row displays
60
+ style the stylesheet
61
+ ```
62
+
63
+ Both are packages named for the modules they replaced, so `settag.cli:main`
64
+ and existing imports resolve unchanged.
65
+
66
+ The rule that keeps the two honest: **no count or human-readable phrase
67
+ derived from `PlannedWrite`, `PreparedWrite`, `WriteRecord`, or
68
+ `MetadataTrack` belongs in `cli/` or `tui/`.** UI modules choose layout; the
69
+ domain layer decides what the numbers and words are. `WriteSummary` exists
70
+ because that rule was broken twice — the CLI counted genre evidence while the
71
+ app counted every task, so one batch reported two different totals in the
72
+ confirm dialog, `settag apply`, and `settag preview`.
73
+
74
+ `tests/test_architecture.py` enforces the count half of the rule: a `sum()` in
75
+ `cli/` or `tui/` fails the suite unless it carries a `# ui-count: <reason>`
76
+ marker declaring that it aggregates the UI's own state (rows in the current
77
+ view, the current selection, terminal widths) rather than domain objects.
78
+ Aggregates over a batch or a track belong in `summarize_writes`,
79
+ `summarize_planned`, or a property on the domain object —
80
+ `PlannedWrite.evidence_score_count` is there for exactly that reason.
81
+
82
+ The phrasing half stays a convention: `plans.friendly_change` and
83
+ `WriteRecord.readable_changes` are the only places change text is produced,
84
+ but nothing mechanical stops a UI from writing its own wording.
85
+
86
+ When the first argument is a file or directory, it is normalized to
87
+ `run PATH`.
88
+
89
+ - If stdin and stdout are TTYs, `run` opens `SetTagApp`.
90
+ - If either is not a TTY, `run` is a plain dry run.
91
+ - `run --no-tui` explicitly selects the plain dry run.
92
+ - Named `analyze`, `inspect`, `preview`, and `apply` commands are always plain.
93
+ - `hygiene PATH` opens its independent Textual review in a TTY; `--no-tui`
94
+ prints the same findings without writing.
95
+
96
+ There is no second interactive menu or per-track prompt workflow. Textual is
97
+ the single interactive product.
98
+
99
+ ## Metadata hygiene workflow
100
+
101
+ Hygiene is a sibling workflow, not an analysis phase. It never constructs or
102
+ loads a model:
103
+
104
+ ```text
105
+ scan tags → detect → select field-level findings → preflight → confirm → clean
106
+ ```
107
+
108
+ Findings are deterministic suggestions: a web address in a comment-like field,
109
+ a generated encoder marker, an empty text value, or an exact duplicate. The
110
+ review table shows the native field's user-facing label, current value, and the
111
+ reason it was flagged. Every finding is independently checked. A normal comment
112
+ with no matching rule is absent from review and cannot be changed by the plan.
113
+
114
+ `H` switches from the analysis app into hygiene after background analysis has
115
+ stopped. `settag hygiene PATH` opens hygiene directly without starting the
116
+ analyzer. Hygiene preflights the file size, mtime, native adapter, and exact
117
+ current values, then repeats preflight after confirmation. Its changes join the
118
+ same temporary-copy transaction, candidate verification, atomic replacement,
119
+ and write journal used by other metadata writes.
120
+
121
+ ## Interactive state model
122
+
123
+ The app has two explicit phases:
124
+
125
+ 1. **Choose:** read existing metadata, filter the library, and choose which
126
+ tracks to analyze. No analyzer is constructed in this phase.
127
+ 2. **Review:** inspect new ranked evidence, choose which staged changes to
128
+ write, and optionally stage a conventional genre.
129
+
130
+ The choose phase classifies each track as `Never analyzed`, `Up to date`,
131
+ `Reanalyze (model/config changed)`, or `Incomplete metadata`. Tracks needing
132
+ analysis are preselected. Up-to-date tracks stay visible and unselected. The
133
+ library can be filtered to all tracks, tracks needing analysis, tracks missing
134
+ a conventional genre, or up-to-date tracks. Analysis consumes the intersection
135
+ of the current filtered view and its selected tracks; hidden selections are
136
+ never silently included.
137
+
138
+ Analysis is a serial background job layered over both phases. Its input batch
139
+ is frozen at start. Each completed track is staged, persisted to the workbench,
140
+ and exposed to review before the next track begins. The user can navigate the
141
+ library, inspect details, enter review with `V`, edit or save completed plans,
142
+ and write the completed snapshot while later tracks continue analyzing. An
143
+ in-flight track has no plan and cannot enter a write. Track-level concurrency
144
+ is deliberately avoided so background operation does not multiply model load.
145
+
146
+ The dense table combines analysis validity and date into one `Analysis`
147
+ column: `Never`, `Up to date · date`, `Reanalyze · date`,
148
+ `Incomplete · date`, or `New · date`. The details panel retains the full
149
+ status wording and timestamp.
150
+
151
+ The review phase has three distinct layers of state:
152
+
153
+ 1. immutable ranked model evidence;
154
+ 2. track inclusion in the pending write batch;
155
+ 3. an optional staged standard-genre target for each track.
156
+
157
+ Changed tracks are checked for writing by default; `Space` checks or unchecks
158
+ the highlighted track. After a new analysis, an empty conventional genre
159
+ defaults to the conservative standard-genre suggestion; a non-empty genre is
160
+ never replaced automatically. No candidate above the review cutoff means no
161
+ default. `E` opens one genre screen where the user can enter or clear a value,
162
+ or explicitly use the model suggestion. House-family children use an explicit
163
+ allowlist to roll up to `House`; all other children retain their direct name.
164
+ The inspector shows any roll-up, and every edit is shown as `before → after`.
165
+
166
+ Completed interactive analysis is persisted before review in a local SQLite
167
+ workbench. On startup, the library remains the default view: a matching plan
168
+ skips inference and appears as `Ready · date`, with `V` available to open saved
169
+ results in review. A mismatched plan appears as `Reanalyze · date`. Returning
170
+ to the library does not preselect ready plans, though the user may select one
171
+ for deliberate reanalysis.
172
+
173
+ The app keeps the track table primary at full terminal width. The inspector is
174
+ secondary, hidden by default, and toggled with `I` without changing the cursor
175
+ or selection. In review, the table's `Write plan` column names a user-level
176
+ state such as `Refresh`, `Evidence`, or `Genre edit`, never a count of internal
177
+ fields. Raw model scores are omitted from the primary table because they are
178
+ ranking evidence, not calibrated confidence; the optional inspector retains
179
+ them for users diagnosing candidate order or review cutoffs. The inspector
180
+ leads with one compact write-plan block, then shows the candidates admitted by
181
+ the current review policy on one line per task with the total stored score
182
+ count. It does not expose internal field counts. The footer changes with the
183
+ current phase:
184
+
185
+ ```text
186
+ Choose: Space toggle · I details · A all/none · F filter · V review (when ready) · Enter/R analyze · Q quit
187
+ Analyzing in Library: I details · F filter · V review completed · Esc stop after current
188
+ Analyzing in Review: Space toggle · A all/none · I details · E genre · S save · Enter/W write completed · Esc stop after current
189
+ Review: Space toggle · A all/none · I details · E genre · S save · Enter/W write
190
+ ```
191
+
192
+ SetTag follows SetPath's Booth Compass palette so both DJ tools read as one
193
+ family: green-tinted Booth Black and deck surfaces, cool equipment-like text,
194
+ and Ember Signal only for focus, action, and meaningful state. A selected
195
+ track is always marked with `✓`; color is supporting information, never the
196
+ only selection cue.
197
+
198
+ `W` performs preflight and opens one confirmation screen with `Write` focused
199
+ by default. The screen previews up to three filenames and states, for each,
200
+ whether SetTag evidence changes and whether the standard genre is unchanged or
201
+ staged as `before → after`; larger batches show the remaining track count and
202
+ one batch total. Narrow or short terminals preview one track so the reassurance
203
+ and actions remain visible. `Enter` confirms and `Esc` returns to review.
204
+ SetTag performs preflight again, then writes and verifies. Analysis errors
205
+ disable batch writing. When analysis is still running, preflight snapshots only
206
+ completed, checked plans; later results cannot enter an already-confirmed
207
+ write. A successful write does not exit the app: written tracks become current
208
+ library entries, while any unwritten tracks remain in review.
209
+
210
+ ## Local workbench
211
+
212
+ The workbench and embedded tags have different ownership:
213
+
214
+ - SQLite is SetTag's private, restartable working state.
215
+ - Audio tags are the portable published result consumed by other tools.
216
+ - JSONL plans are explicit export/apply artifacts.
217
+
218
+ The default SQLite path follows the platform application-data convention:
219
+
220
+ - macOS: `~/Library/Application Support/settag/state.sqlite3`
221
+ - Linux: `${XDG_DATA_HOME:-~/.local/share}/settag/state.sqlite3`
222
+ - Windows: `%LOCALAPPDATA%\settag\state.sqlite3`
223
+
224
+ `SETTAG_STATE_DB` changes the default and `run --state-db PATH` overrides one
225
+ invocation. Plain CLI commands are stateless and never open the workbench.
226
+
227
+ Records reuse the validated `settag.plan/v4` representation. An upsert is
228
+ committed after every successful track analysis and after each staged standard
229
+ genre edit. A verified write deletes its corresponding entry. Persistence
230
+ failure leaves the in-memory review intact; cleanup failure after a verified
231
+ audio write is reported without reclassifying the audio write as failed.
232
+
233
+ On metadata load, cached plans are validated against source size and mtime,
234
+ model identifier, evidence-configuration hash, and the currently observed
235
+ standard genre. Review-only changes to the score cutoff or displayed-result
236
+ limit reuse the existing evidence; changing how much audio the genre model
237
+ reads does not, because that changes the evidence itself. A true evidence
238
+ mismatch retains the old evidence for inspection but requires reanalysis. Current embedded SetTag
239
+ metadata is authoritative and causes an obsolete local entry to be removed.
240
+
241
+ A row the running build cannot decode at all — a superseded plan schema after
242
+ an upgrade, or a corrupt record — is deleted and treated as a cache miss, so
243
+ the track simply needs reanalysis. Only the audio tags are irreplaceable, and
244
+ refusing to read the workbench instead refused to start the app, leaving no
245
+ recovery but deleting the database by hand. Bumping the plan schema therefore
246
+ needs no workbench migration.
247
+
248
+ ## Write journal
249
+
250
+ The journal is durable history, not cache, so it is a separate database
251
+ (`journal.sqlite3`, `SETTAG_JOURNAL_DB`, `--journal-db PATH`) beside the
252
+ workbench. Clearing the workbench to recover from a problem must not destroy
253
+ the ability to undo a write.
254
+
255
+ One apply operation is one batch. Each entry stores the complete SetTag-owned
256
+ bundle, conventional genre, and any explicitly cleaned hygiene fields exactly
257
+ as they were before that file was written, plus the size and mtime immediately
258
+ after. The before-state is
259
+ captured during preflight and is trustworthy at write time because
260
+ `apply_prepared` rechecks the source SHA-256 and `apply_metadata_tags` rechecks
261
+ the plan before saving.
262
+
263
+ An entry is recorded only after a file is written and verified, so the journal
264
+ never claims a change that did not land. Recording is failure-absorbing: a
265
+ journal that cannot be written is surfaced as a warning beside an otherwise
266
+ successful write, never as a failed write.
267
+
268
+ Undo restores by rewriting the recorded before-state through the same verified
269
+ `apply_metadata_tags` path a normal write uses. Files whose size or mtime no
270
+ longer match what was recorded are skipped with a reason rather than restored,
271
+ using the same staleness signal the workbench cache uses; `--force` overrides.
272
+ This restores tag values, not bytes: mutagen rewrites the tag block on save, so
273
+ a reverted file does not regain its pre-write SHA-256.
274
+
275
+ ## Safety invariants
276
+
277
+ 1. Analysis and the default plain mode never write.
278
+ 2. Opening the interactive app reads metadata only and does not load the model.
279
+ 3. Only visible tracks explicitly selected in the current library filter are
280
+ sent to the analyzer.
281
+ 4. Interactive writes require `W` plus a batch confirmation.
282
+ 5. Plain writes require a reviewed plan through confirmed `apply` or
283
+ `apply --yes`; `analyze` never writes.
284
+ 6. `apply --yes` bypasses only confirmation, never validation.
285
+ 7. Only formats with an approved native metadata adapter are writable.
286
+ 8. Ranked evidence is immutable in review and always written to SetTag-owned
287
+ fields.
288
+ 9. A conventional genre changes only through a separate staged target.
289
+ 10. New TUI analysis may stage the conservative standard-genre suggestion only
290
+ when the conventional genre is empty; the user can edit or clear it before
291
+ writing.
292
+ 11. Predictions below the review cutoff are never suggested merely to force a
293
+ result, but may remain in the bounded evidence bundle for consumers.
294
+ 12. Artwork, titles, artists, and metadata owned by other tools are preserved.
295
+ Comments and other hygiene fields change only when their individual cleanup
296
+ suggestions are checked and confirmed in the separate hygiene workflow.
297
+ 13. Every completed write is reopened and verified against all planned SetTag,
298
+ conventional genre, and hygiene values.
299
+ 14. Every completed interactive analysis is persisted for restart recovery.
300
+ 15. Every input produces an analysis or error record when an output stream is
301
+ requested.
302
+
303
+ ## Pipeline
304
+
305
+ ```text
306
+ scan → read tags → choose tracks → load model → infer → select → plan
307
+
308
+ verify ← write ← confirm ← preflight
309
+ ```
310
+
311
+ The Textual workflow starts one lightweight spawned analyzer process before
312
+ entering the terminal UI, then constructs the model there only on the first
313
+ analysis action and reuses it for later batches. Starting before Textual takes
314
+ over the terminal avoids platform-specific file-descriptor and thread hazards.
315
+ The Textual thread worker waits only on IPC so native inference cannot block
316
+ terminal rendering or input. The plain workflow constructs the model
317
+ immediately in its own process.
318
+ When MAEST and EffNet tasks are selected together, one 16 kHz decode feeds both
319
+ stacks; mood/theme and instrument share a single EffNet embedding pass. Every
320
+ required model artifact is verified against its pinned SHA-256 before the
321
+ analyzer is constructed.
322
+
323
+ MAEST embeds one 30-second patch at a time and its graph is fixed at batch one,
324
+ so its cost is strictly linear in patch count and it is the run: 15.5 s against
325
+ EffNet's 1.2 s on a 482-second track. Nothing else moves that number — thread
326
+ counts are already saturated and concurrent workers return under 1.2x — so the
327
+ `genre_sample` setting chooses how many patches MAEST reads: `full`, `middle` (4 from
328
+ the centre, the default) or `spaced` (6 across the track). It narrows the audio
329
+ handed to MAEST only. EffNet always reads the whole track, because it is cheap
330
+ and its taxonomies want whole-track averaging for the same reason the evidence
331
+ limit covers them completely. Because sampling changes which audio produced the
332
+ evidence, it lives in the evidence configuration and its digest, not in the
333
+ review policy beside the score cutoff.
334
+ Interactive cancellation is cooperative between tracks: the in-flight native
335
+ inference finishes, completed results remain reviewable, and unprocessed tracks
336
+ remain selected. Failures are isolated during analysis. A run returns non-zero
337
+ if any input fails.
338
+
339
+ Preflight verifies for every included track:
340
+
341
+ - the source exists and its SHA-256 matches
342
+ - the observed conventional genre still matches
343
+ - the parsed metadata adapter still matches
344
+ - reconstructed SetTag-owned changes match the plan
345
+ - any staged conventional genre change matches the plan
346
+
347
+ Preflight is all-or-nothing. Native files cannot form one transaction across a
348
+ directory, so a failure after writes begin stops immediately and reports the
349
+ number already completed.
350
+
351
+ ## Metadata adapters
352
+
353
+ | Adapter | Files | SetTag evidence | Standard genre | Hygiene fields |
354
+ |---|---|---|---|---|
355
+ | `id3` | MP3, AIFF, WAV | `TXXX:SETTAG_*` | `TCON` | `COMM`, `WXXX`, selected `TXXX`, `TSSE` |
356
+ | `vorbis-comments` | FLAC | `SETTAG_*` comments | `GENRE` | named comment/source/URL/encoder fields |
357
+ | `mp4-freeform` | M4A, M4B, MP4 | `----:com.lsdcapital.settag:*` | `©gen` | `©cmt`, `©too`, matching freeform atoms |
358
+
359
+ A combined write loads one native container, validates every planned layer,
360
+ updates SetTag fields plus the optional conventional genre and hygiene edits,
361
+ saves once, and reopens the file for verification.
362
+
363
+ The scanner accepts `.mp3`, `.flac`, `.m4a`, `.m4b`, `.mp4`, `.aif`, `.aiff`,
364
+ `.wav`, and `.wave`. A recognized extension whose parsed metadata container is
365
+ unsupported remains unwritable.
366
+
367
+ ## SetTag evidence contract
368
+
369
+ The owned logical fields are:
370
+
371
+ - `SETTAG_GENRE`: the bounded Discogs519 evidence labels in ranked order
372
+ - `SETTAG_GENRE_SCORES`: compact JSON with the same labels, order, and scores
373
+ - `SETTAG_MOOD_THEME`: bounded EffNet mood/theme labels in ranked order
374
+ - `SETTAG_MOOD_THEME_SCORES`: compact JSON with the same labels, order, and scores
375
+ - `SETTAG_INSTRUMENT`: bounded EffNet instrument labels in ranked order
376
+ - `SETTAG_INSTRUMENT_SCORES`: compact JSON with the same labels, order, and scores
377
+ - `SETTAG_VERSION`: SetTag version
378
+ - `SETTAG_MODEL`: model-pair identifier
379
+ - `SETTAG_ANALYZED_AT`: UTC analysis time
380
+ - `SETTAG_CONFIG_SHA256`: evidence-configuration fingerprint
381
+ - `SETTAG_PROVENANCE`: `settag.provenance/v3`, keyed by task with full model
382
+ manifests, artifact digests, label taxonomy, configuration, thresholds, and
383
+ timestamps
384
+
385
+ Each task's model manifest carries `vocabulary`, the name of the taxonomy its
386
+ labels are drawn from (`discogs519`, `mtg-jamendo-moodtheme`,
387
+ `mtg-jamendo-instrument`). It is declared rather than inferred: only the
388
+ producer knows which taxonomy it ran, field names do not change when a head is
389
+ swapped, and two taxonomies sharing a label spelling are not the same label.
390
+ `ModelSpec` owns the value so it moves with the head it describes.
391
+
392
+ `PROVENANCE_SCHEMA` in `tags.py` is both the value written and the value
393
+ required when reading, so the pair cannot drift. Bumping it makes every earlier
394
+ record unreadable, which is the mechanism by which a record-shape change reaches
395
+ the user: those tracks report as stale and are offered for re-analysis. Bump it
396
+ for a change to the record's shape, not for a new model or evidence setting —
397
+ both of those are already compared field by field.
398
+
399
+ That mechanism reaches a track only through the tasks the run is configured for.
400
+ A task outside the configured list is regenerated by nothing, so a bump would
401
+ otherwise leave its labels on the file while discarding the record that explains
402
+ them — evidence still readable and still filterable, with no model or vocabulary
403
+ anyone can name. Two rules close this. `build_task_owned_values` drops the label
404
+ and score fields of any task with no provenance entry, so a write cannot produce
405
+ that state; the removal is an ordinary planned change and is shown before it is
406
+ applied. `records.orphaned_tasks` reports the state where it already exists, for
407
+ tasks the caller is not otherwise asking about — the metadata scan calls those
408
+ tracks incomplete, the workbench calls their cached plans stale, and either way
409
+ the next analysis clears it. A configured task with no record is left to
410
+ `read_task_provenance_status`, which calls it `MISSING` on better evidence.
411
+
412
+ Whether one task's provenance is out of date is decided in exactly one place,
413
+ `records.read_task_provenance_status`. The metadata scan and the workbench cache
414
+ both consume its `ProvenanceStatus` and phrase the result their own way; neither
415
+ re-derives it. A structural test enforces that the comparison stays there.
416
+
417
+ `genre` is the default task and loads only MAEST. Explicit `mood-theme` and
418
+ `instrument` tasks share one Discogs-EffNet embedding pass and use their own
419
+ heads. Genre evidence is exclusively MAEST-derived. EffNet output cannot write
420
+ the SetTag or conventional genre fields.
421
+
422
+ Task updates are independent. A partial run replaces only the requested task
423
+ records and preserves valid evidence and provenance for other tasks. Audio
424
+ metadata stores the top 60 ranked results for each task without applying the
425
+ review cutoff. `EVIDENCE_LIMIT` in `policy.py` is the single source of that
426
+ bound and records why it is 60 rather than a shortlist.
427
+
428
+ Scores are mean sigmoid activations across audio patches. They are suitable
429
+ for ranking and applying a score cutoff but are not demonstrated calibrated
430
+ confidence or probabilities.
431
+
432
+ ## Complete analysis record
433
+
434
+ `settag.analysis/v3` records the source fingerprint, analysis time, backend,
435
+ requested tasks, exact model files and SHA-256 values, evidence configuration,
436
+ review policy, task-keyed full predictions, bounded evidence, review-selected
437
+ subsets, and the native SetTag plan.
438
+
439
+ `analyze` never writes, so the record carries no write result. Every write goes
440
+ through a reviewed plan, `preflight_plan`, and `apply_prepared`.
441
+
442
+ Failed records use `settag.error/v1` and never claim analysis or writing
443
+ succeeded.
444
+
445
+ ## Compact plan record
446
+
447
+ Current compact plans use `settag.plan/v4`:
448
+
449
+ ```json
450
+ {
451
+ "schema": "settag.plan/v4",
452
+ "path": "/absolute/path/track.mp3",
453
+ "source": {
454
+ "sha256": "...",
455
+ "size": 12345678,
456
+ "mtime_ns": 1750000000000000000
457
+ },
458
+ "file_genre": [],
459
+ "target_file_genre": ["House"],
460
+ "evidence": [
461
+ {
462
+ "label": "Electronic---Progressive House",
463
+ "score": 0.664
464
+ }
465
+ ],
466
+ "selected": [
467
+ {
468
+ "label": "Electronic---Progressive House",
469
+ "score": 0.664
470
+ }
471
+ ],
472
+ "metadata_format": "id3",
473
+ "provenance": {
474
+ "settag_version": "0.1.0",
475
+ "model": "essentia/genre-discogs519-maest/v1",
476
+ "analyzed_at": "2026-07-24T12:34:56Z",
477
+ "config_sha256": "..."
478
+ },
479
+ "changes": {
480
+ "settag": [
481
+ "Genre labels: 0 → 1",
482
+ "Ranked score data: add"
483
+ ],
484
+ "file_genre": "File genre: None → House"
485
+ }
486
+ }
487
+ ```
488
+
489
+ `file_genre` is the observed safety precondition.
490
+ `target_file_genre` is either `null` (preserve it), an array of desired values,
491
+ or an empty array (explicitly clear it). SetTag and conventional changes are
492
+ serialized separately.
493
+
494
+ `analyze --plan` writes v4 with a null target. The Textual app may save an
495
+ explicit target. `settag.plan/v4` is the only accepted plan schema; earlier
496
+ drafts were never released. A *plan file* on an earlier schema is rejected with
497
+ an explicit error, because it is user-supplied input that must not be silently
498
+ ignored; the same record in the *workbench* is discarded instead, because that
499
+ is SetTag's own restartable cache.
500
+
501
+ Failed tracks use `settag.plan-error/v1`. A file containing any error record
502
+ cannot be applied.
503
+
504
+ ## Deferred work
505
+
506
+ - concurrent decoding with controlled model lifecycle
507
+ - Ogg Vorbis and Opus
508
+ - APEv2 formats such as WavPack, Monkey's Audio, and Musepack
509
+ - ASF/WMA
510
+ - analysis-only support for decodable but unwritable containers
511
+ - representative-library calibration of task-specific thresholds
512
+ - curated taxonomy search and aliases beyond direct user input