gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit54796ff254796ff2tracker mission 028 (code order) 4/5 thin faces + last copies: the show page's check/follow faces call lib/watches.hl toggleWatched / toggleSeasonWatched (seasonAllWatched moved there) and lib/follows.hl toggleFollowed; both logins (header selector face, /login/callback) share lib/users.hl userOfCode; todayStr/listOf copies in components and the export readers copied into tools/migrate.hl + tools/old-short-ids.hl now once (lib/util.hl, lib/export.hl); gates 342/0, 32/0, 52/0; old-short-ids output byte-identical, migrate output identicalmre54796ff2/tools/migrate.hl

16.6 KB

  1. // tools/migrate.hl — ONE-OFF MIGRATION (tracker.worldapi.org#2): the old MongoDB export
  2. // (/media/STORAGE/projects/old-tracker/mongo-export/*.jsonl, EJSON, one document per line)
  3. // into this app's own mpackdb tables (storage/mpackdb/). EVERY record gets a NEW mpackdb id
  4. // (self-assigned below — see "why self-assigned ids" — not mpackdb's auto-generated one); the
  5. // old Mongo hex id is kept as `oldId` (a unique index: the idempotency key — a second run
  6. // finds the same id again via `oldId` and skips the row, printing "already there").
  7. //
  8. // genres.db { name, tmdbId, urlSegment, oldId }
  9. // persons.db { name, tmdbId, gender, urlSegment, birthDate, birthPlace, deathDate, image,
  10. // imdbId, shows [{ show, character }], oldId }
  11. // shows.db { title, summary, tmdbSummary, tmdbId, language, type, release, year, image,
  12. // urlSegment, episodesCount, homepage, imdbId, seasonsCount, status, tagline,
  13. // tvdbId, tvmzId, genres [genre id], cast [{ person, character, actor }],
  14. // seasons [season id], oldId }
  15. // seasons.db { seasonNumber, release, title, summary, show, episodes [episode id], oldId }
  16. // episodes.db { tmdbId, episodeNumber, seasonNumber, title, summary, release, show, season,
  17. // oldId } -- season is null where the OLD record's own season link was
  18. // already null or dangling (pre-existing in the export, not introduced here)
  19. // follows.db { show, user, at, oldId }
  20. // watches.db { targetType ('episode'|'season'), target, user, at, oldId } -- target is
  21. // null for the handful of watches whose old target no longer exists in the
  22. // export (pre-existing dangling refs; kept so counts still match)
  23. //
  24. // The single old user (User.jsonl, [email protected], password NOT taken over) becomes
  25. // this app's own user for ident short id az5b2 (users.hl `ensureUser`, storage/mpackdb/users.db
  26. // — the SAME table step 1's login already uses).
  27. //
  28. // WHY SELF-ASSIGNED IDS, NO update(): Show.seasons/Season.show and Show.cast/Person.shows are
  29. // each a genuine two-way reference (a show needs its seasons' ids; a season needs its show's id
  30. // — same for shows and persons via cast). The first version of this tool broke that circle the
  31. // obvious way — put() a bare record, come back later with fetch()+update() once the other side's
  32. // ids exist. That LOSES ROWS: reopening a table in a fresh process after a run that had done
  33. // update() on it read back fewer rows than were put (seen directly: count() dropped from 15157 to
  34. // 15152 after persons.db round-tripped through update() once — a genuine hl:mpackdb bug, filed as
  35. // a Hybriel gap, tracker.worldapi.org#2 — not something a vendored-plugin patch is allowed to fix
  36. // here). Tables that only ever got put() never lost a row in the same testing.
  37. // The fix: assign every new id ourselves (`resolveId`, 8 random bytes as hex, hl:crypto) in one
  38. // pass over each file BEFORE building any record, so by the time a record is actually built every
  39. // side of every reference is already a known id — one put() per row, with its id and every
  40. // reference already resolved. mpackdb's own auto id is never used; hybriel#23 already establishes
  41. // that put() keeps an id the record carries, which this leans on deliberately.
  42. //
  43. // Run (absolute paths; relative ones resolve against tools/, not the project root):
  44. // TRACKER_OLD_DATA=/media/STORAGE/projects/old-tracker/mongo-export \
  45. // TRACKER_STORAGE=$PWD/storage/mpackdb \
  46. // TRACKER_MIGRATE_TMP=$PWD/.scratch/migrate-tmp \
  47. // ./bin/hybriel tools/migrate.hl
  48. import { MPackDB } from 'hl:mpackdb'
  49. import { env } from 'hl:proc'
  50. import { newId, listOr } from '../lib/util.hl'
  51. import { linesOf, oidOf, bigLinesOf } from '../lib/export.hl'
  52. import { ensureUser } from '../lib/users.hl'
  53. oldDir = env('TRACKER_OLD_DATA')
  54. storageDir = env('TRACKER_STORAGE')
  55. tmpDir = env('TRACKER_MIGRATE_TMP')
  56. if (oldDir == null || !oldDir.startsWith('/') || storageDir == null || !storageDir.startsWith('/') || tmpDir == null || !tmpDir.startsWith('/')) {
  57. hlError('set TRACKER_OLD_DATA (the mongo-export directory), TRACKER_STORAGE (the mpackdb directory) and TRACKER_MIGRATE_TMP (a scratch directory for the chunk workaround below), all ABSOLUTE')
  58. }
  59. // ---- reading the export (lib/export.hl: linesOf, oidOf, bigLinesOf) ------------------------------
  60. static numOf = (&v) => {
  61. if (v == null) { return null }
  62. if (hlTypeName(v) == 'Number') { return v }
  63. let a = v['$numberInt']
  64. if (a == null) { a = v['$numberLong'] }
  65. return a == null ? null : toNumber(a)
  66. }
  67. static dateOf = (&v) => {
  68. if (v == null) { return null }
  69. let d = v['$date']
  70. return d == null ? null : numOf(d)
  71. }
  72. // a JSON list field that may be absent or explicitly null (most Show.cast/.seasons and nearly all Person.shows are) —
  73. // iterating `null` with for-of aborts the whole run: util.hl listOr
  74. // ---- tables ---------------------------------------------------------------------------
  75. genresTable = new MPackDB(file = storageDir + '/genres.db', primaryKey = '@id', indexes = ['!oldId'])
  76. personsTable = new MPackDB(file = storageDir + '/persons.db', primaryKey = '@id', indexes = ['!oldId'])
  77. showsTable = new MPackDB(file = storageDir + '/shows.db', primaryKey = '@id', indexes = ['!oldId'])
  78. seasonsTable = new MPackDB(file = storageDir + '/seasons.db', primaryKey = '@id', indexes = ['!oldId'])
  79. episodesTable = new MPackDB(file = storageDir + '/episodes.db', primaryKey = '@id', indexes = ['!oldId'])
  80. followsTable = new MPackDB(file = storageDir + '/follows.db', primaryKey = '@id', indexes = ['!oldId'])
  81. watchesTable = new MPackDB(file = storageDir + '/watches.db', primaryKey = '@id', indexes = ['!oldId'])
  82. // the new id for an old row: reuse what a previous run already gave it (a point lookup on the
  83. // unique `oldId` index — proven reliable; it's a FULL table scan, find(null, null), that lost
  84. // rows together with update() in testing, so this never calls that), or a fresh random id
  85. static existingId = (&table, &oldId) => {
  86. let found = table.find('oldId', oldId)
  87. if (found != null && found.length > 0) { return found[0].id }
  88. return null
  89. }
  90. static resolveId = (&table, &oldId) => {
  91. let ex = existingId(table, oldId)
  92. return ex == null ? newId() : ex
  93. }
  94. report = []
  95. failed = 0
  96. // ---- read every file once (chunked for the big three) --------------------------------
  97. genreLines = linesOf(oldDir + '/Genre.jsonl')
  98. personLines = linesOf(oldDir + '/Person.jsonl')
  99. showLines = bigLinesOf(oldDir + '/Show.jsonl', tmpDir + '/show', 5000)
  100. seasonLines = bigLinesOf(oldDir + '/Season.jsonl', tmpDir + '/season', 3000)
  101. episodeLines = bigLinesOf(oldDir + '/Episode.jsonl', tmpDir + '/episode', 5000)
  102. followLines = linesOf(oldDir + '/Follow.jsonl')
  103. watchLines = linesOf(oldDir + '/Watch.jsonl')
  104. // ---- phase 1: id maps only (every old id -> its new id, self-assigned) ---------------
  105. // every reference below (Show.genres/cast/seasons, Season.show/episodes, Episode.show/season,
  106. // Person.shows, Follow.oid, Watch.oid) is resolved against one of these five maps, ALL of
  107. // which are complete before phase 2 builds a single record — that is what removes the need
  108. // for update() (see the file header).
  109. genreMap = {}
  110. for (l of genreLines) { let d = JSON.parse(l) let oldId = oidOf(d._id) genreMap[oldId] = resolveId(genresTable, oldId) }
  111. personMap = {}
  112. for (l of personLines) { let d = JSON.parse(l) let oldId = oidOf(d._id) personMap[oldId] = resolveId(personsTable, oldId) }
  113. showMap = {}
  114. for (l of showLines) { let d = JSON.parse(l) let oldId = oidOf(d._id) showMap[oldId] = resolveId(showsTable, oldId) }
  115. seasonMap = {}
  116. for (l of seasonLines) { let d = JSON.parse(l) let oldId = oidOf(d._id) seasonMap[oldId] = resolveId(seasonsTable, oldId) }
  117. episodeMap = {}
  118. for (l of episodeLines) { let d = JSON.parse(l) let oldId = oidOf(d._id) episodeMap[oldId] = resolveId(episodesTable, oldId) }
  119. // ---- phase 2: build and put the real records (one put() each, every reference resolved) ----
  120. // ---- Genre ----------------------------------------------------------------------------
  121. genreFresh = 0
  122. genreKnown = 0
  123. for (l of genreLines) {
  124. let d = JSON.parse(l)
  125. let oldId = oidOf(d._id)
  126. let id = genreMap[oldId]
  127. if (genresTable.fetch(id) != null) {
  128. genreKnown = genreKnown + 1
  129. } else {
  130. let putId = genresTable.put({ id = id name = d.name tmdbId = numOf(d.tmdbId) urlSegment = d.urlSegment oldId = oldId })
  131. if (putId == null) { console.log('FAIL genre ' + oldId + ': ' + genresTable.lastError()) failed = failed + 1 } else { genreFresh = genreFresh + 1 }
  132. }
  133. }
  134. report.push('genres ' + genreFresh + ' new, ' + genreKnown + ' already there (old ' + genreLines.length + ' -> new ' + genresTable.count() + ')')
  135. // ---- Person (shows resolved against showMap, already complete) -----------------------
  136. personFresh = 0
  137. personKnown = 0
  138. personShowsMissing = 0
  139. for (l of personLines) {
  140. let d = JSON.parse(l)
  141. let oldId = oidOf(d._id)
  142. let id = personMap[oldId]
  143. if (personsTable.fetch(id) != null) {
  144. personKnown = personKnown + 1
  145. } else {
  146. let shows = []
  147. for (s of listOr(d.shows)) {
  148. let shid = showMap[oidOf(s._id)]
  149. if (shid == null) { personShowsMissing = personShowsMissing + 1 } else { shows.push({ show = shid character = s.character }) }
  150. }
  151. let putId = personsTable.put({
  152. id = id name = d.name tmdbId = numOf(d.tmdbId) gender = numOf(d.gender) urlSegment = d.urlSegment
  153. birthDate = d.birthDate birthPlace = d.birthPlace deathDate = d.deathDate image = d.image
  154. imdbId = d.imdbId shows = shows oldId = oldId
  155. })
  156. if (putId == null) { console.log('FAIL person ' + oldId + ': ' + personsTable.lastError()) failed = failed + 1 } else { personFresh = personFresh + 1 }
  157. }
  158. }
  159. report.push('persons ' + personFresh + ' new, ' + personKnown + ' already there (old ' + personLines.length + ' -> new ' + personsTable.count() + '), show refs skipped ' + personShowsMissing)
  160. // ---- Show (genres/cast/seasons resolved against genreMap/personMap/seasonMap) --------
  161. showFresh = 0
  162. showKnown = 0
  163. castMissing = 0
  164. showGenresMissing = 0
  165. showSeasonsMissing = 0
  166. for (l of showLines) {
  167. let d = JSON.parse(l)
  168. let oldId = oidOf(d._id)
  169. let id = showMap[oldId]
  170. if (showsTable.fetch(id) != null) {
  171. showKnown = showKnown + 1
  172. } else {
  173. let genres = []
  174. for (g of listOr(d.genres)) {
  175. let gid = genreMap[oidOf(g._id)]
  176. if (gid == null) { showGenresMissing = showGenresMissing + 1 } else { genres.push(gid) }
  177. }
  178. let cast = []
  179. for (c of listOr(d.cast)) {
  180. let pid = personMap[oidOf(c._id)]
  181. if (pid == null) { castMissing = castMissing + 1 } else { cast.push({ person = pid character = c.character actor = c.actor }) }
  182. }
  183. let seasons = []
  184. for (s of listOr(d.seasons)) {
  185. let sid = seasonMap[oidOf(s)]
  186. if (sid == null) { showSeasonsMissing = showSeasonsMissing + 1 } else { seasons.push(sid) }
  187. }
  188. let putId = showsTable.put({
  189. id = id title = d.title summary = d.summary tmdbSummary = d.tmdbSummary tmdbId = numOf(d.tmdbId)
  190. language = d.language type = d.type release = d.release year = numOf(d.year) image = d.image
  191. urlSegment = d.urlSegment episodesCount = numOf(d.episodesCount) homepage = d.homepage
  192. imdbId = d.imdbId seasonsCount = numOf(d.seasonsCount) status = d.status tagline = d.tagline
  193. tvdbId = numOf(d.tvdbId) tvmzId = numOf(d.tvmzId) genres = genres cast = cast seasons = seasons
  194. oldId = oldId
  195. })
  196. if (putId == null) { console.log('FAIL show ' + oldId + ': ' + showsTable.lastError()) failed = failed + 1 } else { showFresh = showFresh + 1 }
  197. }
  198. }
  199. report.push('shows ' + showFresh + ' new, ' + showKnown + ' already there (old ' + showLines.length + ' -> new ' + showsTable.count() + '), cast refs skipped ' + castMissing + ', genre refs skipped ' + showGenresMissing + ', season refs skipped ' + showSeasonsMissing)
  200. // ---- Season (show resolved against showMap, episodes against episodeMap) -------------
  201. seasonFresh = 0
  202. seasonKnown = 0
  203. seasonShowMissing = 0
  204. seasonEpisodesMissing = 0
  205. for (l of seasonLines) {
  206. let d = JSON.parse(l)
  207. let oldId = oidOf(d._id)
  208. let id = seasonMap[oldId]
  209. if (seasonsTable.fetch(id) != null) {
  210. seasonKnown = seasonKnown + 1
  211. } else {
  212. let show = showMap[oidOf(d.show._id)]
  213. if (show == null) { seasonShowMissing = seasonShowMissing + 1 }
  214. let episodes = []
  215. for (e of listOr(d.episodes)) {
  216. let eid = episodeMap[oidOf(e)]
  217. if (eid == null) { seasonEpisodesMissing = seasonEpisodesMissing + 1 } else { episodes.push(eid) }
  218. }
  219. let putId = seasonsTable.put({ id = id seasonNumber = numOf(d.seasonNumber) release = d.release title = d.title summary = d.summary show = show episodes = episodes oldId = oldId })
  220. if (putId == null) { console.log('FAIL season ' + oldId + ': ' + seasonsTable.lastError()) failed = failed + 1 } else { seasonFresh = seasonFresh + 1 }
  221. }
  222. }
  223. report.push('seasons ' + seasonFresh + ' new, ' + seasonKnown + ' already there (old ' + seasonLines.length + ' -> new ' + seasonsTable.count() + '), show refs skipped ' + seasonShowMissing + ', episode refs skipped ' + seasonEpisodesMissing)
  224. // ---- Episode (show/season resolved; season null where the OLD link was already null or
  225. // dangling — pre-existing in the export, not introduced here) -------------------------
  226. episodeFresh = 0
  227. episodeKnown = 0
  228. episodeShowMissing = 0
  229. episodeSeasonMissing = 0
  230. for (l of episodeLines) {
  231. let d = JSON.parse(l)
  232. let oldId = oidOf(d._id)
  233. let id = episodeMap[oldId]
  234. if (episodesTable.fetch(id) != null) {
  235. episodeKnown = episodeKnown + 1
  236. } else {
  237. let show = showMap[oidOf(d.show._id)]
  238. if (show == null) { episodeShowMissing = episodeShowMissing + 1 }
  239. let oldSeasonId = oidOf(d.season._id)
  240. let season = oldSeasonId == null ? null : seasonMap[oldSeasonId]
  241. if (oldSeasonId != null && season == null) { episodeSeasonMissing = episodeSeasonMissing + 1 }
  242. let putId = episodesTable.put({
  243. id = id tmdbId = numOf(d.tmdbId) episodeNumber = numOf(d.episodeNumber) seasonNumber = numOf(d.seasonNumber)
  244. title = d.title summary = d.summary release = d.release show = show season = season oldId = oldId
  245. })
  246. if (putId == null) { console.log('FAIL episode ' + oldId + ': ' + episodesTable.lastError()) failed = failed + 1 } else { episodeFresh = episodeFresh + 1 }
  247. }
  248. }
  249. report.push('episodes ' + episodeFresh + ' new, ' + episodeKnown + ' already there (old ' + episodeLines.length + ' -> new ' + episodesTable.count() + '), show refs skipped ' + episodeShowMissing + ', season refs left null (pre-existing) ' + episodeSeasonMissing)
  250. // ---- the single old user -> this app's user for ident short id az5b2 ---------------
  251. creator = ensureUser('az5b2')
  252. if (creator == null) { hlError('could not create/find the user for az5b2') }
  253. userId = creator.id
  254. // ---- Follow ---------------------------------------------------------------------
  255. followFresh = 0
  256. followKnown = 0
  257. followMissing = 0
  258. for (l of followLines) {
  259. let d = JSON.parse(l)
  260. let oldId = oidOf(d._id)
  261. let id = resolveId(followsTable, oldId)
  262. if (followsTable.fetch(id) != null) {
  263. followKnown = followKnown + 1
  264. } else {
  265. let show = showMap[oidOf(d.oid)]
  266. if (show == null) { followMissing = followMissing + 1 }
  267. let putId = followsTable.put({ id = id show = show user = userId at = dateOf(d._createdAt) oldId = oldId })
  268. if (putId == null) { console.log('FAIL follow ' + oldId + ': ' + followsTable.lastError()) failed = failed + 1 } else { followFresh = followFresh + 1 }
  269. }
  270. }
  271. report.push('follows ' + followFresh + ' new, ' + followKnown + ' already there (old ' + followLines.length + ' -> new ' + followsTable.count() + '), show refs skipped ' + followMissing)
  272. // ---- Watch (target null where the OLD target no longer exists — pre-existing) -----
  273. watchFresh = 0
  274. watchKnown = 0
  275. watchMissing = 0
  276. for (l of watchLines) {
  277. let d = JSON.parse(l)
  278. let oldId = oidOf(d._id)
  279. let id = resolveId(watchesTable, oldId)
  280. if (watchesTable.fetch(id) != null) {
  281. watchKnown = watchKnown + 1
  282. } else {
  283. let kind = d.type
  284. let oldTarget = oidOf(d.oid)
  285. let target = kind == 'episode' ? episodeMap[oldTarget] : seasonMap[oldTarget]
  286. if (target == null) { watchMissing = watchMissing + 1 }
  287. let putId = watchesTable.put({ id = id targetType = kind target = target user = userId at = dateOf(d._createdAt) oldId = oldId })
  288. if (putId == null) { console.log('FAIL watch ' + oldId + ': ' + watchesTable.lastError()) failed = failed + 1 } else { watchFresh = watchFresh + 1 }
  289. }
  290. }
  291. report.push('watches ' + watchFresh + ' new, ' + watchKnown + ' already there (old ' + watchLines.length + ' -> new ' + watchesTable.count() + '), target refs left null (pre-existing) ' + watchMissing)
  292. genresTable.persist()
  293. personsTable.persist()
  294. showsTable.persist()
  295. seasonsTable.persist()
  296. episodesTable.persist()
  297. followsTable.persist()
  298. watchesTable.persist()
  299. for (line of report) { console.log('migrate: ' + line) }
  300. console.log('migrate: ' + failed + ' failed (' + oldDir + ' -> ' + storageDir + ')')

Branches

Latest commits

  • 54796ff2tracker mission 028 (code order) 4/5 thin faces + last copies: the show page's check/follow faces call lib/watches.hl toggleWatched / toggleSeasonWatched (seasonAllWatched moved there) and lib/follows.hl toggleFollowed; both logins (header selector face, /login/callback) share lib/users.hl userOfCode; todayStr/listOf copies in components and the export readers copied into tools/migrate.hl + tools/old-short-ids.hl now once (lib/util.hl, lib/export.hl); gates 342/0, 32/0, 52/0; old-short-ids output byte-identical, migrate output identicalmre
  • 06b078e3tracker mission 028 (code order) 3/5 project.hl is the map: config, routes, wiring and a feature → file index (914 → 258 lines); the background jobs (daily sync run, backfills, details repair, credits job, merge, short ids, collection seed) moved unchanged into lib/jobs.hl (a class: their state is reassigned every step, a static cannot be; one instance made after the server), the login callback into lib/users.hl, poster/photo serving into lib/images.hl, the /shows/<slug> rule into lib/shows.hl showsMovedPath; route handlers are thin wrappers; gates 342/0, 32/0, 52/0, real-copy pages identicalmre
  • 94716fd2tracker mission 028 (code order) 2/5 util + topics: lib/util.hl holds envOr, storageDir, postersDir, profilesDir, newId, hexDigits, todayStr, dateOr, textOr, hasId, listOr, firstOf, sortDesc once (were copied into up to 5 files); tmdbsync.hl split into tmdb.hl (TMDB/TVmaze requests), sync.hl (one title's sync), sync-helpers.hl, backfill.hl; details.hl split into details.hl, credits.hl, credits-helpers.hl (isIncomplete to shows.hl); search-helpers.hl (words, query, ranking, slugs); collections.hl (the TMDB collection seed, out of franchises.hl); deltasync.hl renamed dailysync.hl; no behaviour change: gates 342/0, 32/0, 52/0, real-copy pages identicalmre
  • 186079b0tracker mission 028 (code order) 1/5 move: every root .hl except project.hl into lib/ (styles.hl into components/), import paths only; gates 342/0, 32/0, 52/0; real-copy pages identicalmre
  • 4f47f181tracker: report 027mre
  • dc1d4be4tracker mission 027: Hybriel master 06617221 vendored (plugin allocator fixes 3a781359 + 413f60e4); real copy RSS through first-start jobs + 400 loads flat ~2.55 GB (190aa11d 2.3 -> 5.6 GB), page times <= 1.1x; gates 342/0, 32/0, 52/0mre
  • 84e1b3e1tracker: reports 025 + 026mre
  • dc40d859tracker#31 (mission 026): duplicate titles merged — the 68 type+tmdbId pairs held by 157 records were the old tracker's (all migrated); merge.hl repair job (own clock, before the TMDB jobs) keeps one keeper per title (follows/watches > old short id > oldest), moves follows, watches, seasons, cast, credits, timelines, tombstones the rest (mergedInto, never deleted), slugs + short ids 301 to the keeper; stray seasons merged into their listed twin (Reacher S3 watches) or linked when watched; search import re-checks before its put; deploy.sh waits up to 90 s for 200; real copy 68 -> 0 dup ids, az5b2 follows/watches equal; gates 342/0, 32/0, 52/0mre
  • 2667da05tracker#30 (mission 025): /my/ pages from slim cached title cards, episode rows and watch sets (after the jobs /my/series 1.8 s -> 0.06 s, /my/unwatched 4.1 -> 0.18 s); timeline page shows its name once; franchise widget under the poster/title; movies with TV leftovers (First Contact) go through the details repair; tools/count-tmdb-ids.hl; gates 327/0, 52/0, 32/0mre
  • 3909810dantcolony#40: mission references in README/STATUS/docs point to the moved missionsmre
  • e63b1d28antcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 1cda451dtracker: Hybriel master 190aa11d (#127 both shapes, GC correctness fc838894) — conductor adopts despite /my/series 2.4x after jobs (memory 8.0 → 1.7 GB boot); see reports/071mre
  • c1fa2f2etracker (mission 071): Hybriel master 190aa11d measured on the real copy vs the live binary 8590df63 — NOT adopted (after the first-start jobs /my/series 2.4x slower, /series 1.6x, RSS swings 7.4-12.2 GB; fresh it is flat at 1.7-2.2 GB and /my/unwatched faster), vendor stays 8efba065, candidate kept in .scratch/w071/vendor-190aa11d; tests/kinds.mjs: collection seed off (its TMDB request broke check 1 in 1 of 4 runs); tests/realdata-071.sh + realdata-071-bench.mjs + tools/realdata-071-table.py; README + STATUS (numbers, how to repeat); gates 325/0, 32/0, 50/0mre
  • 8081350atracker docs (mission 070): README (summary, Config HL_GC_BYTES — kept at Hybriel's default, the 256 MiB setting is taken out of docker-compose.yml again: the jobs grew to 12+ GB with it too, see STATUS), Test (three gates), Deploy (first start ~50 min: kinds then seed, restart once after collections done, memory numbers), Vendored Hybriel 8efba065 + #48 audit, Files; STATUS mission 070 entry (merges, migrated counts, lambda audit, gates, RSS old vs new, how to repeat, open points); docs/kinds.md + docs/franchises.md job order; tests/realdata-070-*.sh, tools/count-migrated.hl, tools/ref-params.py, tools/lambda-audit.pymre
  • 1ad19c8ctracker: re-vendor Hybriel master 8efba065 (#126 GC by bytes, #48 lambda parameters copy) (mission 070): bin/hybriel sha256 50361e95…, plugins core crypto data fetch fs http http1 mpackdb proc smtp time web; lambda audit: 13 lambdas change a passed record/list (11 through a local alias), no caller relies on it — unchanged; 319 read-only lambda parameters get & (no copy per call: /my/schedule 2.35 → 0.46 s, /my/unwatched 13.7 → 5.7 s on the real copy); /my/unwatched one merge sort instead of n² inserts; docker-compose HL_GC_BYTES=268435456; deploy.sh runs kinds.mjs + franchises.mjs too (default ports 8700–8710); tests/realdata-070.mjs; gates browser 325/0, kinds 32/0, franchises 50/0mre
  • 46b21389tracker (mission 070, conductor): the sync never destroys migrated data — the one-time summary step MOVES a copied summary to migratedSummary (marker summaries-moved.txt) instead of clearing it; a migrated record's first title/genres/homepage/tagline TMDB replaces → migratedTitle/migratedGenres/migratedHomepage/migratedTagline, a migrated season's/episode's title/summary → migratedTitle/migratedSummary (set once); tests/peek-shows.hl prints them; gate 325/0mre
  • 749019b0Merge t19 (tracker#19 franchises + timelines) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs/faketmdb.mjs, both sides kept; franchise/timeline pages get #20's typed heading (Franchise | …, Timeline | …), their title links via titlePath (/movies|/series|/shows); the collection seed waits for repair, kinds and credits too; franchises.mjs URLs + 2 new checks; gates browser 323/0, kinds 32/0, franchises 50/0mre
  • daf49feaMerge t20 (tracker#20 typed headings + #21 series/shows split) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/search.hl/components/search.hl/browser.mjs/faketmdb.mjs, both sides kept; clock order backfill → repair → kinds → credits; withDetailsFields stores tmdbType + kind; gates: browser.mjs URLs → /series|/movies, typed h1 selectors; kinds.mjs repair/credits off, fixture name = seed name; browser 323/0, kinds 32/0mre
  • 20e09d89Merge t18 (tracker#18 delta sync) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs, both sides kept; pageShowOf summary = summaryOfmre
  • f83571c3tracker#18 (mission 067): daily sync by change lists — TMDB /tv|movie/changes (since the stored day, paged) + TVmaze /updates/shows → only our changed titles (followed: full step, unfollowed: light step — changed seasons, no TVmaze), full walk on first run / gap > 14 days / failed list; show record refreshed (title, tmdbSummary, tagline, status, genres …; renamed titles re-indexed); summary = the creator's own text (page: summary > tmdbSummary > tvmazeSummary), one-time clear of copied summaries (9,647 on the live copy); gate 261, tests/realdata-018*.mjs, README + STATUSmre