Changelog
All notable changes to rehuco-core are recorded here. The format follows
Keep a Changelog, and the package follows
Semantic Versioning.
Changelogs are per package in this monorepo, matching the per-package release tags: this file covers
rehuco-core only, and its releases are tagged rehuco-core-X.Y.Z.
[Unreleased]
Added
rehuco_core.access, the first types of the resource access seam, which the agent now names its open documents and file announcements by. AResourceKeyis aLocalKey(path)or aNodeKey(node_id, root_id, relative), both with path arithmetic (name,stem,parent,with_name,joinpath,is_at_or_beneath); aLocalKeyfolds case as the filesystem does, aNodeKeycompares as spelled, andstr()of either is a stable name for it.KeyRelocationisRelocationover keys (KeyRelocation.from_local, andto_localfor the local pairs).ChangeFeedannouncesMoved,Changed,FolderChangedandInvalidatedwithout Qt.AccessRouter.key_for_pathturns a path into its key, andAccessRouter.for_keyhands a local key toLocalAccessand raisesNodeUnreachablefor a node key.- Records through the access seam:
LocalAccess.records(LocalRecords) loads a record, saves it, reads its text as stored and converts a legacy.tc. A file that cannot be read loads as a locked stub rather than raising. A save is a read-merge-write against theRecordVersionthe document was read at -- a hash of its metadata (every block'susersmap left out), of the file, and of each per-user field. A metadata edit made on disk since the read refuses the save as aRecordConflictcarrying the file as it is now; the per-user fields merge field by field (merge_users), a field the caller changed being the caller's and every other one the file's, in the caller's own scope andpubliconly.record_differenceslists what an edited document and the file disagree on -- the metadata and the caller's own per-user fields, each with its original value -- andput_pathsputs the values kept. A legacy.tcis never saved over. - The Root Catalog through the access seam:
LocalAccess.catalog(LocalCatalog) answersroots(),query(CatalogQuery),row(key)andcount(root_id)over a.rehudb, each call opening the cache on its own thread, and raisesCatalogUnreadablefor a cache it cannot read.LocalCatalogalso keeps the cache current:prepareandattachfor the open.rehuco,reconcileandhidefor an edit of its roots,followfor a rename,refreshfor records the app wrote, andplan_verify/apply_verifyfor verify-on-access.RecordOps.signature(key)answers a record's modification time and size under the rename hold, andLocalAccesstakes theRenameCoordinatorthat stat and the catalog's record re-reads go through. CatalogSnapshotholds a catalog's rows in memory and filters them there:buildkeys every row once,select(query, ids)picks what aCatalogQuerymatches -- exactly the rowsCatalogCache.rowsreads, in its order -- andpatchedbrings it up to date after a change.CatalogMatcheris the query compiled, andCatalogSearchKeyone row's folded text, holding its row.CatalogCache.rowsshares oneUUIDper root among a read's rows.app_folders()answers where the agent and the node keep their files without Qt: oneborco/rehucofolder on Windows (%LOCALAPPDATA%) and macOS, the XDG config / data / cache split on Linux.default_username()is the OS login name, oradminwhen the platform has none.- Per-user watch progress on a tutorial's videos.
record_progress(path, username, relative, position, duration, *, viewed)is the one write path for every player: it re-reads the record as the host'scurrent_username, replaces only that user's entry for that file, and leavesupdatedalone. WithWatchRules' two settings, reaching within a second of a file's end marks it viewed, and the tutorial is marked viewed at the moment its last file is — every video on disk and every missing file with an entry.watch_totalssums the watched seconds per folder and for the whole tutorial, a viewed file counting its full length and a missing file still counting.read_progress,set_progress_entry,read_videosandset_videosread and write the two maps,ProgressEntryandWatchTotalsdescribe them. merge_writere-reads a record, applies one change and writes it back atomically, one writer at a time per record in a process, so a progress write and a metadata write both survive.RehuDocument.savetakestouch_updated=Falsefor a write that must not stampupdatedorcreated.DurationProbe.probe_videoreads a video's frame size beside its length, asVideoFacts;content_videosprobes each of a resource's videos into thevideosmap, andcontent_video_fileslists them without probing. Both are keyed bycontent_key: the path under aninfo.rehu's folder, or a standalone<stem>.rehu's file name without the stem.content_pathresolves a key back, refusing one that could reach outside the resource.RehuDocument.user_field,set_user_fieldandremove_user_fieldtake the username, for a caller serving several users.- A watched file that is gone keeps its progress entry:
missing_progress_keysnames the entries whose file is not on disk, andforget_progressremoves one by hand — nothing removes one on its own. - A renamed video keeps its progress.
reattach_renamed_progressmoves a missing file's entry to a file under a new name anywhere in the resource, a renamed folder included, when exactly one same-size candidate hashes equal to the file's recorded.checksumhash, or, with no hash recorded and the size rung allowed, when exactly one candidate has the same size and length.plan_progress_renamesis the decision on its own. Progress entries record the file's size for it.
Changed
- Writing
Noneto a per-user field (set_active_user_field,set_user_field) removes the key instead of storing a JSONnull. - The ffprobe probe asks for JSON output, to read the frame size along with the duration.
updatedmoves only when a save changes the record's metadata: a per-user change -- a rating, a progress entry, another user's fields merged in -- no longer stamps it. Saved throughLocalRecords, the change is judged against the file the save replaces, so a save whose details are the file's keeps the file'supdated.RehuDocument.save(edited=...)takes that decision from its caller, andpayload_to_write()is what a save writes.RehuDocument.loaddecodes the file as UTF-8 text and hands it back withload_with_text, so a record's version describes the very text it was read from.
[0.3.0] - 2026-10-12
Added
CatalogRecordUpdater.verifyis now three steps, so a caller can keep the disk off the thread that owns the cache:planreads the rows' signatures,check_records(any thread, never the cache) compares them with the files and reads what changed, andapplywrites the findings back, dropping one whose row a save or a scan rewrote, or whose record a rename moved, in the meantime. Whatcheck_recordsreturns,RecordsChecked, also gives every record's modification time and size as it saw them, changed or not, for whatever shows a record to compare with what it read.CatalogCache.locatetakes the roots when the caller has them, so planning many records asks the database once.staged_namenames an image taken out of a resourcerehu-<origin>__<path segments joined by __>, safe on Windows and macOS and capped at 200 UTF-8 bytes without shortening an id or the basename's extension;parse_staged_namereads the origin and the path back asStagedImageName, whoseuuidtells an id from a location.staging_originpicks the origin: the record's id, else its location folder or stem.stage_imagewrites the image's bytes unchanged into a staging folder (reusing an identical copy), andprune_stageddeletes the copies unused for an age.CatalogCache.resource_uuidis an indexed lookup of a record's id by root and relative path.read_archive_factsdescribes a zip from its central directory alone — files and images, size unpacked and packed, each compression method with its member count, encrypted members — asArchiveFacts, which says whether the methods are slow (bzip2, LZMA) or beyondzipfile(deflate64, encryption).list_archive_imageslists one archive's content images in pack order, the listing the content-image scanner now shares.CatalogCache.resource_typeis an indexed lookup of a record's type by root and relative path.RootFolderListeralso reads the checksum record that covers a listed file -- a same-stemfoo.checksum, the folder'sinfo.checksum, or the one a caller names above it (covering=) -- and returns, asDirectoryListing.covered, what each covered file's entry says (CoveredFile), under the same hold as the listing.DirectoryClassifier.reclassifynames an already-read listing for another record without a secondscandir.- A task-queue job that satisfies
ReportingTaskJobsays what it found in one line:JobOutcome(summary, clean), kept on the row asJobStatus.summaryand saved with a finished job. Checksum runs report their summary this way, andchecksum_report_is_cleanis the rule for a clean run. RootFolderListerlists one folder of one.rehucoroot, asked by root id and path under the root, never by an absolute path: a read is onescandirunder the rename coordinator's hold, and a folder that is gone or an unknown root comes back not reachable rather than raising.CatalogCache.resource_countsays how many rows a root holds.RehucoFile.moveputs a root at any row, which a drag and drop of one asks for.RootStorage— what a root's folder lives on:local,network,removableorcompact_disk. A.rehucostores it per root asstorage, read aslocalwhen absent, andRehucoRoot.removableis true for the last two.RehucoFile.add_roottakes astorage, andset_storagereplacesset_removable.- The
.rehudbcatalog cache (CatalogCache): a SQLite file per.rehuco, named by its rehuco id inside a cache folder the caller names. It holds the roots, keyed by their ids, and one row per record found under them: the common core a browser shows, the authors, tags and publishers, and the record's size, modification time and content hash. It also holds the type-specific fields: a tutorial's three durations and its levels, and a reference pack's claimed and measured image counts.catalog_type_fieldsnames the ones a type contributes, and a scan fills only those. Each row also records its file's format version: a.rehu's ownformat_version(0when unstamped), none for a legacy.tcor a record that could not be read. The schema is versioned throughPRAGMA user_versionand upgraded forward only. A cache newer than the build, or a file that is not a database, is discarded and rebuilt. Reconciling against a.rehucokeeps a relabeled or re-pointed root's rows. Removing a root deletes its rows and frees their space. Rows can be read whole or narrowed by free text andfolder,authors,tags,publishersandtypetokens, each token driven from an index rather than a scan: a value token through the value's name index, afoldertoken through the root label (ASCII case ignored) and apath_keyrange beneath it, folded as the filesystem folds paths. - A full scan of one root (
CatalogRootScan, queued asScanCatalogRootJob), and root removal asRemoveCatalogRootJob. The scan reads every.rehu, and every.tcno same-stem.rehusits beside. A record that will not read keeps a row naming why. A root that does not list keeps its rows; a scan whose root went away before it finished is not applied. Each listing and each record read is held under the rename coordinator, so a rename waits for one read rather than the scan. - The root scan is incremental: given the root's cached rows (
CatalogCache.signatures), it opens only the records whose modification time or size changed, plus any row that could not be read before. Unchanged records keep their rows, and what was not found is removed. The scan job reports progress against the previous scan's row count. It still lists every folder, so a record nested inside a tutorial is found like any other. - Single-record cache updates between scans.
CatalogRecordUpdater.upsertre-reads one record after a save or a conversion, and a converted.rehutakes over its.tc's row.CatalogRecordUpdater.verifyre-reads a record whose file no longer matches its row.CatalogCache.removedrops a deleted record's row.CatalogCache.apply_relocationapplies a rename's executed plan (RehuRenamer.executed) without reading anything: a renamed folder rebases every row beneath it, a file-scoped rename only its own. A record missing under an offline root keeps its row.CatalogCache.resource_idsnames the rows at or beneath some paths, andCatalogCache.rows(query, ids=...)reads just those rows through a query, so a view can follow a change without reading everything again. CatalogScanner.walk()yields one listing at a time. Given a coordinator, it lists each directory under its hold and follows a folder renamed mid-walk. It can also collect uncovered.tcrecords.- The
.rehucofile (RehucoFile): a rehuco id and an ordered list of folder roots, each with a stable id, a label and a removable flag. Roots are added (a duplicate folder is refused, compared case-insensitively where paths are), removed, relabeled and moved to the top, up, down or bottom. A label defaults to the folder's name, numbered on a clash (foo (2)). Saves are atomic, unknown keys are kept, and a file newer than the build opens read-only. - A reference pack's content images include its loose images as well as its archive members: whatever images are among the files the content walk gives the record. A loose image is keyed by size and modification time. Archives and folders of loose images sort together, case-insensitively, with the pack's own folder first. The enumeration takes the excluded-files globs the checksums take, so the two agree on every file.
Changed
CatalogQuery.textis nowterms, a tuple: each term must be found in a title or a path, either one, so free text matches every word rather than one phrase. Terms andauthors/tags/publishersvalues ignore case and diacritics both ways (borco_core.fold). The cache is schema v5: the folded title and path of each resource and an indexed folded name per author, tag and publisher, filled from the stored rows on upgrade (about 1 s per 100k resources), with no rescan. The cache remembers whichfoldwrote them and writes them again, the same way, the next time it opens under a different one.- A verify reports a file it gives its first checksum as
addedrather thanunexpected. The summary reads210 matched, 2 mismatched, 3 added(checked verdicts first),addedcounts as clean, and each added file is logged by name: Checksum added for new file foo.zip. A new file the run cannot read gets no entry and is counted unreadable; an older record's hash-lessunexpectedentry is read as no entry, so no record needs migrating. take_enclosing_claims(rehu_path)moves the checksum entries a new file-scoped record covers out of the enclosing directory-scoped record's.checksuminto its own, as a verify of the enclosing record would. Called right after the record is first written, it returnsTakenClaims, orNonewhen nothing moved.enclosing_directory_recordfinds that record.- A
.rehucois version 2: a root'sremovableflag became astorage(removablefor a flagged root, otherwiselocal), migrated when a file is read and written back on its next save. A build that predates this opens the file read-only. - An image named by a legacy screenshot pattern (
01.jpg,cover.jpg,sample-01.jpg, ...) is content in every folder: enumerated, checksummed and measured like any other file. Only a<record>NNimage beside its record is a screenshot sidecar. A.checksumwritten before this lacks such files, and the next verify adopts them as added. - Converting a
.tcmeasurescurrent_sizeafter renaming its screenshots, so the size never counts the images the conversion itself claims. - A rename's executed plan is a
Relocation, available asRehuRenamer.relocation. Itsrelocate(path)gives a path's new location, andtouches(folder)says whether a folder's listing changed.RenameCoordinatorpasses it to every rename listener, which used to be called with no arguments.
Removed
- The
screenshot_name_patternsparameter of the content walk, the checksum runs, jobs and seeding, the directory classifier, size on disk and video duration: the patterns no longer decide what is content. Checksum, sweep and manifest jobs no longer save them in their state; an older saved state still loads.
[0.2.0] - 2026-09-27
Added
- Checksums — a
.checksumrecord beside the.rehuwith five algorithms, generate/verify that skip recently checked files, a resumable folder sweep, adoption of legacy.sfv/.md5/.sha*manifests, and per-location trust. - Content enumeration — which files a resource covers (exclusive between nested records), the images inside a reference pack's archives, and measured size on disk and video duration.
- Per-type field schemas declared by each plugin.
- A task-queue engine: one job at a time, pause/cancel/reorder, optional persistence, per-job log scope.
- A rename barrier (
RenameCoordinator) that makes running jobs and open archives stand aside for a rename. - Bulk
.tcmigration: a dry-run plan over a folder tree, conversion with.origbackups, and discarding them. - Screenshot naming patterns as a configurable regex list, and on-disk move/delete/convert of screenshots.
- A
Deleterprotocol for choosing how files are removed (e.g. to the Recycle Bin). - Record-list editing helpers for authors, collections and learning paths.
Changed
- Converting a
.tckeeps each screenshot's own number instead of inferring its slot.
Fixed
- A malformed
core.type, or a required string field holding JSONnull, locks the document instead of reading as the text"None". - A typeless document's empty block key is no longer treated as an active block.
- Switching a document's type normalizes the block being activated.
createdis stamped when a.rehuis first written..rehu/.tcparsing enforces size, depth and entry-count limits.- An unparseable legacy duration no longer imports as
0. info.tcis treated as directory-scoped, likeinfo.rehu.
[0.1.0] - 2026-07-29
The first version carrying the library itself: 0.0.0 and 0.0.1 were name-reservation stubs built
outside this repository, so everything below is new rather than changed.
Added
.rehudocuments — a model whose JSON read/write preserves unknown fields across a round-trip, plus the format definition, serialization, screenshots, and rename support.- Lock derivation — why a parsed document is read-only, and the field-value coercion that goes with it.
- A migration runner and the migration chains for tutorials, reference images, and the
.rehucore block, with the steps shared between them factored out. - A plugin registry — the plugins a build ships, and an immutable index over them.
- Legacy
.tcsupport — document and screenshot reading, description handling, and conversion to.rehu. - Collection and learning-path entries, a titled index, and the shared constants.
[0.0.1] - 2026-06-30
Second name-reservation stub on PyPI, published a day after 0.0.0 and still carrying no library code.
[0.0.0] - 2026-06-29
Name-reservation stub, published to PyPI so the name could not be taken by anyone else. Neither this
release nor 0.0.1 was built from this repository — when both were uploaded the repository contained no
Python packages at all, so there is no source in the history that corresponds to them.