Skip to content

feat(commons): package layout sources/ masters/ derivatives/ under commons/assets/song/<id>/ - #152

Closed
jzongker wants to merge 7 commits into
mainfrom
feat/commons-package-layout
Closed

feat(commons): package layout sources/ masters/ derivatives/ under commons/assets/song/<id>/#152
jzongker wants to merge 7 commits into
mainfrom
feat/commons-package-layout

Conversation

@jzongker

@jzongker jzongker commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Live song files now sit in the three package folders under the existing id-keyed prefix: commons/assets/song/<id>/sources/<file>, masters/song.json, masters/lyrics.chordpro, masters/cover.webp (+ masters/score.musicxml, masters/art.<ext> when present), derivatives/<file>; manifest.json stays at the package root. assetFiles.name stores the package-relative name; roles are decided by basename (new helpers/PackageLayout.ts), so fileUrls keys are unchanged for the site and B1Admin — only the URL path carries the folder. Non-song assets keep their flat layout.
  • Approve: pending uploads stay flat; packagePath() places them on promotion (uploads → sources/, art → masters/art.<ext>, browser art-thumb.webp renamed onto derivatives/cover-thumb.webp, generated files → derivatives/). A same-basename live file in another folder (a seeded derivatives/score.musicxml under an uploaded sources/score.musicxml) is superseded. The song hook writes masters/song.json and masters/lyrics.chordpro; filesChanged and the manifest's files[].name / sources[].file are package-relative.
  • Seed (tools/commons-seed/catalog.ts): the song row is built from masters/song.json + masters/lyrics.chordpro (header stripped) + sources/hymnary.json + sources/video.json + derivatives/duration.json; catalog.json only fills ids, package paths, confidence, parentSongId and writer bio/portrait — the reverse of before. Every package file is copied into the id-keyed folder keeping its folder; files inherited from a work land under the song's own package. commons-sync-catalog.ts uses the same reader (its S3 keys gain the subfolders).
  • Flat mirror: reset-commons no longer mirrors writers/ (referenced portraits travel in copies). New tools/manual/commons-prune-flat-mirror.ts lists (dry run) and with --apply deletes commons/songs/, commons/works/, commons/writers/ except the portraits authors.portraitUrl still points at; selection is the pure tools/commons-seed/prune.ts. Not run against S3 here.
  • Every reader of the file-name column now goes through basename/packagePath: ContentLibraryHelper.role/fileUrls/songJson/removePrefix (disk listing now walks subfolders), PublishHelper.approve/writeFile/readFile/fileSummary, publishHooks/song.ts (packageFields), publishHooks/index.ts (manifest), SubmissionHelper.recordFile/submit, SubmitValidation.resultingFileNames, CommonsAssetController (download by name accepts the package-relative name or the basename; files[].role), CommonsAdminController (declined-file live check, readText, score-missing roles), CommonsSongController.readText.
  • README: layout table, seed source of truth, prune tool and how portraits move later.

Stacks on #151, #150 and #149 (branched from the local integration/site-tasks = main + those three); merge after them.

Test plan

  • yarn jest src/modules/commons — 23 suites / 289 tests green (SeedCatalog: copies land in subfolders, song row from song.json/lyrics.chordpro/hymnary.json/video.json, inherited work files under the song; ContentLibraryHelper: role by basename with folder prefix, packagePath, findByBase, fileUrls paths, recursive prefix removal; PublishHelper: destination folders, thumb rename, supersede across folders, manifest at the root, non-song assets stay flat; SongPublishHook: masters paths, basename reads; PruneFlatMirror: selection logic; SubmissionHelper: replace/remove against a foldered live file)
  • yarn tsc --noEmit clean; yarn eslint --fix src/modules/commons — 0 errors (3 pre-existing max-len warnings)
  • e2e runs in the orchestrator's integration pass (no server started here; grep of the site worktree src/ found no hard-coded assets/song/ path)

Cross-repo

Depends on / pairs with: #151, #150, #149 (stacked). Site/B1Admin: none (roles unchanged).
Migration: none (no schema change; reset-commons reseeds the new layout, commons-prune-flat-mirror.ts --apply on prod after cut-over)

…mmons review

Reviewers can now send a pending submission back to the writer as a draft
with a note (POST /commons/admin/submissions/:id/request-changes), and
approve a proposal while declining some of its files with a reason each
(approve body gains declineFiles). Declined files are recorded on the
submission as filesChanged { action: "declined", reason } and listed in
the approval mail.

COMMONS_MUSIC_EDITORS (user ids or emails) names reviewers who are not
server admins: they see the queue and detail, approve, request changes
and reject, but an approval whose proposal changes rights (license,
licenseVersion, recordingOwned, proAnswer, a new demo/stems recording, or
a first publish) answers 403, and reports/visibility/feature/score routes
answer 403 too. GET /admin/status gains musicEditor.

The four submitter mails (received, approved, changes requested,
rejected) share one template so wording and footer stay consistent.

Tests: yarn jest src/modules/commons (192), yarn tsc --noEmit, eslint on
touched files.
Adds the package-model columns to songs (confidence tier, first line, rights
and form maps, sing time, score source, listen-gate record), seeds them from
the content repo packages (masters/song.json, derivatives/*, work inheritance),
and recomputes them in the song publish hook. GET /commons/songs rows gain
confidence/sundayReady/featured/has* flags and the sundayReady/confidence/
language/q filters; the detail adds rights, rightsMatrix, ccliReport,
attribution, form, publishedKeys, contributors and the new fileUrls roles.
New GET /commons/songs/:id/page bundles song + rating + history + family +
similar, and POST /commons/admin/songs/:id/listen records the listen gate.

Tests: RightsHelper table, seed reader against a fixture package repo,
package shaping, /page shape, listen-gate transitions, publish hook fields.
Adds submissions.type (new | translation | arrangement | correction | additionalFile | removal) with per-type rights triage at submit, the score / scoreImage / lyrics upload roles, a ChordPro bracket lint that blocks submit, a sources inventory inside manifest.json, songs.contributors credits on approve, removal-request approval that unpublishes, triage-ordered queue and type-aware emails.

Covered by the commons jest suite (177 tests); e2e runs in the batch integration pass.
# Conflicts:
#	src/modules/commons/controllers/CommonsAdminController.ts
Reconciled PublishHelper.approve so A3 declineFiles, A2 removal-type approval and A1 filesChanged-to-hooks all run; song hook writes both A2 contributors and A1 packageFields, manifest keeps A2 sources rows with A1 ContentLibraryHelper.role().
Reviewer checks stay on ReviewerHelper.canReview; controllers keep A1 query params and /page, A2 type in queue/detail/mine, A3 request-changes; mail merges A3 submitterMail footer with A2 type-aware wording.
songs.contributors is now a real column, so SongPackageHelper.detail parses it from the loaded row and SongRepo.loadContributors (A1 fallback) is gone; models, SongRepo and test mocks are the union of all three branches.
…mmons/assets/song/<id>/

A song's live files now keep their package folder: assetFiles.name stores the
package-relative name (sources/tune.mid, masters/lyrics.chordpro,
derivatives/slides.json) and roles are decided by basename
(helpers/PackageLayout.ts), so fileUrls keys are unchanged and only the URL path
gains the folder. manifest.json stays at the package root. Pending uploads stay
flat; packagePath() places them on approve (uploads to sources/, art to masters/,
art-thumb.webp renamed onto derivatives/cover-thumb.webp) and a same-basename
file in another folder is superseded. The song hook writes masters/song.json and
masters/lyrics.chordpro. Non-song assets keep the flat layout.

The seed builds the song row from masters/song.json + lyrics.chordpro (+
sources/hymnary.json, sources/video.json, derivatives/duration.json), with
catalog.json only filling ids, paths, confidence and parentSongId; every package
file is copied into the id-keyed folder keeping its folder, inherited work files
land under the song. reset-commons no longer mirrors writers/ (portraits travel
in copies); tools/manual/commons-prune-flat-mirror.ts lists and with --apply
deletes commons/songs|works|writers/ except portraits authors.portraitUrl
references.

Tests: yarn jest src/modules/commons — 23 suites / 289 tests.
@jzongker

jzongker commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #153 (squash-merged integration of #149 #150 #151 #152, tested together).

@jzongker jzongker closed this Sep 8, 2026
@jzongker
jzongker deleted the feat/commons-package-layout branch September 8, 2026 05:11
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