Skip to content

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. A ResourceKey is a LocalKey(path) or a NodeKey(node_id, root_id, relative), both with path arithmetic (name, stem, parent, with_name, joinpath, is_at_or_beneath); a LocalKey folds case as the filesystem does, a NodeKey compares as spelled, and str() of either is a stable name for it. KeyRelocation is Relocation over keys (KeyRelocation.from_local, and to_local for the local pairs). ChangeFeed announces Moved, Changed, FolderChanged and Invalidated without Qt. AccessRouter.key_for_path turns a path into its key, and AccessRouter.for_key hands a local key to LocalAccess and raises NodeUnreachable for 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 the RecordVersion the document was read at -- a hash of its metadata (every block's users map left out), of the file, and of each per-user field. A metadata edit made on disk since the read refuses the save as a RecordConflict carrying 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 and public only. record_differences lists what an edited document and the file disagree on -- the metadata and the caller's own per-user fields, each with its original value -- and put_paths puts the values kept. A legacy .tc is never saved over.
  • The Root Catalog through the access seam: LocalAccess.catalog (LocalCatalog) answers roots(), query(CatalogQuery), row(key) and count(root_id) over a .rehudb, each call opening the cache on its own thread, and raises CatalogUnreadable for a cache it cannot read. LocalCatalog also keeps the cache current: prepare and attach for the open .rehuco, reconcile and hide for an edit of its roots, follow for a rename, refresh for records the app wrote, and plan_verify / apply_verify for verify-on-access. RecordOps.signature(key) answers a record's modification time and size under the rename hold, and LocalAccess takes the RenameCoordinator that stat and the catalog's record re-reads go through.
  • CatalogSnapshot holds a catalog's rows in memory and filters them there: build keys every row once, select(query, ids) picks what a CatalogQuery matches -- exactly the rows CatalogCache.rows reads, in its order -- and patched brings it up to date after a change. CatalogMatcher is the query compiled, and CatalogSearchKey one row's folded text, holding its row. CatalogCache.rows shares one UUID per root among a read's rows.
  • app_folders() answers where the agent and the node keep their files without Qt: one borco/rehuco folder on Windows (%LOCALAPPDATA%) and macOS, the XDG config / data / cache split on Linux. default_username() is the OS login name, or admin when 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's current_username, replaces only that user's entry for that file, and leaves updated alone. With WatchRules' 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_totals sums 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_videos and set_videos read and write the two maps, ProgressEntry and WatchTotals describe them.
  • merge_write re-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.save takes touch_updated=False for a write that must not stamp updated or created.
  • DurationProbe.probe_video reads a video's frame size beside its length, as VideoFacts; content_videos probes each of a resource's videos into the videos map, and content_video_files lists them without probing. Both are keyed by content_key: the path under an info.rehu's folder, or a standalone <stem>.rehu's file name without the stem. content_path resolves a key back, refusing one that could reach outside the resource.
  • RehuDocument.user_field, set_user_field and remove_user_field take the username, for a caller serving several users.
  • A watched file that is gone keeps its progress entry: missing_progress_keys names the entries whose file is not on disk, and forget_progress removes one by hand — nothing removes one on its own.
  • A renamed video keeps its progress. reattach_renamed_progress moves 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 .checksum hash, or, with no hash recorded and the size rung allowed, when exactly one candidate has the same size and length. plan_progress_renames is the decision on its own. Progress entries record the file's size for it.

Changed

  • Writing None to a per-user field (set_active_user_field, set_user_field) removes the key instead of storing a JSON null.
  • The ffprobe probe asks for JSON output, to read the frame size along with the duration.
  • updated moves 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 through LocalRecords, the change is judged against the file the save replaces, so a save whose details are the file's keeps the file's updated. RehuDocument.save(edited=...) takes that decision from its caller, and payload_to_write() is what a save writes.
  • RehuDocument.load decodes the file as UTF-8 text and hands it back with load_with_text, so a record's version describes the very text it was read from.

[0.3.0] - 2026-10-12

Added

  • CatalogRecordUpdater.verify is now three steps, so a caller can keep the disk off the thread that owns the cache: plan reads the rows' signatures, check_records (any thread, never the cache) compares them with the files and reads what changed, and apply writes the findings back, dropping one whose row a save or a scan rewrote, or whose record a rename moved, in the meantime. What check_records returns, 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.locate takes the roots when the caller has them, so planning many records asks the database once.
  • staged_name names an image taken out of a resource rehu-<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_name reads the origin and the path back as StagedImageName, whose uuid tells an id from a location. staging_origin picks the origin: the record's id, else its location folder or stem. stage_image writes the image's bytes unchanged into a staging folder (reusing an identical copy), and prune_staged deletes the copies unused for an age. CatalogCache.resource_uuid is an indexed lookup of a record's id by root and relative path.
  • read_archive_facts describes a zip from its central directory alone — files and images, size unpacked and packed, each compression method with its member count, encrypted members — as ArchiveFacts, which says whether the methods are slow (bzip2, LZMA) or beyond zipfile (deflate64, encryption). list_archive_images lists one archive's content images in pack order, the listing the content-image scanner now shares. CatalogCache.resource_type is an indexed lookup of a record's type by root and relative path.
  • RootFolderLister also reads the checksum record that covers a listed file -- a same-stem foo.checksum, the folder's info.checksum, or the one a caller names above it (covering=) -- and returns, as DirectoryListing.covered, what each covered file's entry says (CoveredFile), under the same hold as the listing. DirectoryClassifier.reclassify names an already-read listing for another record without a second scandir.
  • A task-queue job that satisfies ReportingTaskJob says what it found in one line: JobOutcome(summary, clean), kept on the row as JobStatus.summary and saved with a finished job. Checksum runs report their summary this way, and checksum_report_is_clean is the rule for a clean run.
  • RootFolderLister lists one folder of one .rehuco root, asked by root id and path under the root, never by an absolute path: a read is one scandir under the rename coordinator's hold, and a folder that is gone or an unknown root comes back not reachable rather than raising. CatalogCache.resource_count says how many rows a root holds.
  • RehucoFile.move puts a root at any row, which a drag and drop of one asks for.
  • RootStorage — what a root's folder lives on: local, network, removable or compact_disk. A .rehuco stores it per root as storage, read as local when absent, and RehucoRoot.removable is true for the last two. RehucoFile.add_root takes a storage, and set_storage replaces set_removable.
  • The .rehudb catalog 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_fields names the ones a type contributes, and a scan fills only those. Each row also records its file's format version: a .rehu's own format_version (0 when unstamped), none for a legacy .tc or a record that could not be read. The schema is versioned through PRAGMA user_version and upgraded forward only. A cache newer than the build, or a file that is not a database, is discarded and rebuilt. Reconciling against a .rehuco keeps 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 and folder, authors, tags, publishers and type tokens, each token driven from an index rather than a scan: a value token through the value's name index, a folder token through the root label (ASCII case ignored) and a path_key range beneath it, folded as the filesystem folds paths.
  • A full scan of one root (CatalogRootScan, queued as ScanCatalogRootJob), and root removal as RemoveCatalogRootJob. The scan reads every .rehu, and every .tc no same-stem .rehu sits 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.upsert re-reads one record after a save or a conversion, and a converted .rehu takes over its .tc's row. CatalogRecordUpdater.verify re-reads a record whose file no longer matches its row. CatalogCache.remove drops a deleted record's row. CatalogCache.apply_relocation applies 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_ids names the rows at or beneath some paths, and CatalogCache.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 .tc records.
  • The .rehuco file (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.text is now terms, 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 and authors / tags / publishers values 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 which fold wrote 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 added rather than unexpected. The summary reads 210 matched, 2 mismatched, 3 added (checked verdicts first), added counts 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-less unexpected entry 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 .checksum into its own, as a verify of the enclosing record would. Called right after the record is first written, it returns TakenClaims, or None when nothing moved. enclosing_directory_record finds that record.
  • A .rehuco is version 2: a root's removable flag became a storage (removable for a flagged root, otherwise local), 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>NN image beside its record is a screenshot sidecar. A .checksum written before this lacks such files, and the next verify adopts them as added.
  • Converting a .tc measures current_size after renaming its screenshots, so the size never counts the images the conversion itself claims.
  • A rename's executed plan is a Relocation, available as RehuRenamer.relocation. Its relocate(path) gives a path's new location, and touches(folder) says whether a folder's listing changed. RenameCoordinator passes it to every rename listener, which used to be called with no arguments.

Removed

  • The screenshot_name_patterns parameter 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 .checksum record beside the .rehu with 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 .tc migration: a dry-run plan over a folder tree, conversion with .orig backups, and discarding them.
  • Screenshot naming patterns as a configurable regex list, and on-disk move/delete/convert of screenshots.
  • A Deleter protocol 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 .tc keeps each screenshot's own number instead of inferring its slot.

Fixed

  • A malformed core.type, or a required string field holding JSON null, 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.
  • created is stamped when a .rehu is first written.
  • .rehu/.tc parsing enforces size, depth and entry-count limits.
  • An unparseable legacy duration no longer imports as 0.
  • info.tc is treated as directory-scoped, like info.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

  • .rehu documents — 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 .rehu core block, with the steps shared between them factored out.
  • A plugin registry — the plugins a build ships, and an immutable index over them.
  • Legacy .tc support — 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.