Main's #332 PR 2 (#406) admits Images by each file's effective category
(own, else inherited) and passes an own-category map through the listing;
this branch replaced the favorites set with collection or favorite members.
The admission now returns both (members, own), and a new test covers a
collection filter together with an image's own category.
* feat(images): store images' own categories in the library database (#332)
One new table, image_categories, and its category index, created empty
by a forward-only migration that reads and writes only its own objects.
Startup checks the structure; restore also checks the values. The store
sets, clears and reads own categories by file key (the deepest output
source holding the file and the path below it), and copies a removed
duplicate's category to a kept copy that has none.
* feat(images): duplicate cleanup keeps images' own categories (#332)
The kept copy's own category wins; a kept copy without one receives the
removed copy's, keyed by the deepest output source holding each file.
The removed copy's row is never deleted, so a give-back or a failed
delete loses nothing. The job log reports how many were kept.
* feat(images): set, remove and list images' own categories (#332)
PUT /prompt_manager/images/category sets the own category of up to 500
listed files, DELETE removes it (listed or not), and
GET /prompt_manager/images/categories lists the values whose file is
listed. Targets are the Images listing identity of an output source,
checked by media_target (shared with Collections). The routes never
touch prompts, recipes or links; POST /prompt_manager/bulk/category is
unchanged.
* feat(vocabulary): count and rename image categories (#332)
Tags & categories lists every category value with image_count, the
stored image_categories rows holding it. Values only images hold follow
every prompt-backed value (first_pid at SQLite's INTEGER maximum), so
keyset cursors cross the boundary. /library/options keeps its own query
and never lists image-only values.
Manage category counts image rows (impact.images), treats an image-held
target as existing, renames image rows in the same transaction, allows
renaming an image-only value, and hashes the sorted image rowids into
the impact token so a change between preview and apply is refused with
vocabulary-conflict. Every image-category statement reads
idx_image_categories_category.
* feat(images): the category filter matches each image's effective category (#332)
An image's own category wins; without one it inherits its link's: the
recipe's category when set, else the positive prompt's (MC4, one rule
with the inspector chip). Ambiguous and unlinked images inherit nothing,
so an unlinked image with its own category is now listed. The link read
no longer filters by category in SQL; the page filter, select all N
matching and saved views compare the effective value. A failed
own-category read answers the existing listing-failed 500.
* fix(images): key symlinked output folders and keep key work off the event loop (#332)
Review fixes: a source matches by its configured spelling too, so a
detected output folder that is a symlink keys its files (routes and
duplicate cleanup). File keys are computed in the executor. A path no
source holds is a plain 403, a lone surrogate a 400. The install repairs
a table that lacks its category index instead of refusing to open.
* feat(inspector): show an image's own category in the inspector and History (#332)
The inspector's library facts gain category_scope "image": an image's
own category overrides the inherited one for any link status (linked,
ambiguous, none). Images reads it for output sources only; History reads
it once per inspection inside its snapshot, keyed by the same file key
from the symlink-resolved output folder. A failed read hides the chip
rather than show a possibly wrong value. The chip's hidden prefix names
the origin ("Image category: "); the visible origin text comes in PR 3.
* perf(images): keep the category filter's link read narrow; record D141, D142, D145 (#332)
Under a category the link read keeps only links whose raw recipe or
positive category is the value (Python still decides the inherited
one), the links of images whose own category is the value are read by
path, and plain reads skip the inherited-category work. The own-category
map is keyed by normalized path. TESTING.md records D141, D142 and D145
and amends D13, D36 and D39; the filter budget seeds 2,000 own
categories.
* feat(images): set or remove a category on all N matching images (#332)
The category routes also accept the Images listing filter of "Select
all N matching" (#392) instead of explicit items, resolved now and
capped at 500 images like an explicit selection.
* fix(images): Windows path keys for own categories; plan check for the category read (#332)
Review fixes: on Windows the own-category map keys each file with
normalize_image_path (abspath drops trailing dots and spaces), and the
plan test also covers the category's narrowing clause.
* test(vendor): count the #332 host case in the execution matrix
image-attribution.spec.mjs gains the image category route case, so the
execution config lists 92 tests (3 attribution cases per project).
* test(legacy): record the image category routes in the legacy contract snapshot (#332)
#332 adds PUT/DELETE /prompt_manager/images/category and
GET /prompt_manager/images/categories; the legacy pages' routes are
unchanged.
* fix(usage): count cached re-runs of runtime-linked prompt text (#347)
A PromptManager-family node whose text comes from a runtime link
(PreviewAny, an LLM, a list node) counted only the job that executed it;
later jobs served from ComfyUI's output cache counted nothing.
Each execution now records a receipt (digests of its STRING outputs and
hashes of the text it stores) per run key and queue item. On
execution_cached the receipt whose digests equal the cache entry's text is
replayed once for the job, with the node's role and what the queue hook
already saw recomputed from the job's own graph. The executing node and the
replay count through one shared own_counts. Unmatched cache entries count
nothing, as before; a node that executes after its replay in the same job
skips its own count. No schema change, no backfill.
* fix(usage): never replay a node twice in one job; dedupe receipts once complete (#347)
A repeated execution_cached id or message for the same job now counts the
node once. Receipts replace equal older ones only when their job finishes
(or the next job starts), so a list still recording [A] of [A, B] no longer
drops an older [A] receipt. Node-level tests check the receipt each
execution records.
* test(comfyui): count #347's cached-run cases in the execution suite pin (#347)
The pin listed 19 execution-text-link cases per project; #347 adds five node
kinds and eight one-renderer cases, so the suite lists 32 per project and 118
in total with #332's image-attribution case from main.
* perf(images): read prompt links in one SQLite step (#354)
get_image_link_matches streamed one sqlite3_step per linked row, and
Python's sqlite3 releases and retakes the GIL around every step. While
ComfyUI generates, each retake waits on the busy thread, so the
first-page link read of the workspace Images page stretched from tens of
milliseconds to seconds.
SQLite now returns the unchanged filter SELECT's rows as one JSON value
(json_group_array), decoded with json.loads: a constant two or three GIL
hand-backs instead of one per row. Nothing is cached. Any
OperationalError from the wrapped statement, or a BLOB file_path or
rating, makes that read stream as before with one warning per process; a
missing JSON function stops the attempt for the rest of the process.
tests/test_database_link_matches.py proves the result equals the
streamed read on every filter combination and on NULL ratings, quoted,
backslashed, non-ASCII and multi-line paths, ambiguous and pair links,
an empty database and both fallbacks; the structural budget test also
checks the wrapped query plan.
* fix(images): keep the maintenance link reader, refine the one-step fallback (#354)
The one-step reader was named _link_rows, which shadowed
MaintenanceOperations._link_rows on PromptDatabase and broke the Missing
links preview and apply; it is now _one_step_link_rows, and a test pins
that the maintenance reader is not shadowed.
An interrupted read (Move library retiring the connection) is raised as
before instead of falling back and spending the one warning; a JSON text
over SQLite's length limit (DataError) streams like the other value
failures. The equivalence test compares value types per path rather than
by row order, and a test checks that the read leaves no WAL snapshot
pinned.
* docs(testing): name every one-step link read fallback (#354)
* feat(images): store images' own categories in the library database (#332)
One new table, image_categories, and its category index, created empty
by a forward-only migration that reads and writes only its own objects.
Startup checks the structure; restore also checks the values. The store
sets, clears and reads own categories by file key (the deepest output
source holding the file and the path below it), and copies a removed
duplicate's category to a kept copy that has none.
* feat(images): duplicate cleanup keeps images' own categories (#332)
The kept copy's own category wins; a kept copy without one receives the
removed copy's, keyed by the deepest output source holding each file.
The removed copy's row is never deleted, so a give-back or a failed
delete loses nothing. The job log reports how many were kept.
* feat(images): set, remove and list images' own categories (#332)
PUT /prompt_manager/images/category sets the own category of up to 500
listed files, DELETE removes it (listed or not), and
GET /prompt_manager/images/categories lists the values whose file is
listed. Targets are the Images listing identity of an output source,
checked by media_target (shared with Collections). The routes never
touch prompts, recipes or links; POST /prompt_manager/bulk/category is
unchanged.
* fix(images): key symlinked output folders and keep key work off the event loop (#332)
Review fixes: a source matches by its configured spelling too, so a
detected output folder that is a symlink keys its files (routes and
duplicate cleanup). File keys are computed in the executor. A path no
source holds is a plain 403, a lone surrogate a 400. The install repairs
a table that lacks its category index instead of refusing to open.
* feat(images): set or remove a category on all N matching images (#332)
The category routes also accept the Images listing filter of "Select
all N matching" (#392) instead of explicit items, resolved now and
capped at 500 images like an explicit selection.
* test(vendor): count the #332 host case in the execution matrix
image-attribution.spec.mjs gains the image category route case, so the
execution config lists 92 tests (3 attribution cases per project).
* test(legacy): record the image category routes in the legacy contract snapshot (#332)
#332 adds PUT/DELETE /prompt_manager/images/category and
GET /prompt_manager/images/categories; the legacy pages' routes are
unchanged.
* test(legacy): snapshot the routes and the settings response
Pins every registered route and the key paths of GET /prompt_manager/settings
while System S13 (#330) moves the legacy pages' tools to native System. Recorded
on the unchanged main; cutover (#334) removes it.
* test(legacy): prove each removed legacy control has native coverage
TESTING.md gains the "Legacy System UI removed by S13" table (no rows yet).
legacy-coverage.test.mjs fails when a removal cites a capability that is not
covered, copies another native test, lists an id or method twice, or leaves
the id or method on its page. legacy-system-links.spec.mjs holds the System
link table and guards the kept admin flows.
* refactor(admin): open System for Diagnostics and logs
The admin page's Diagnostics and View Logs buttons become links that open
System › Diagnostics & logs in a new tab. The legacy Diagnostics and System Logs
dialogs and their methods go; TESTING.md lists them against their covered
capabilities (rows B1–B7).
* refactor(admin): open System for settings and keep page options
Settings opens System › Overview in a new tab. The settings dialog becomes Page
options (opened from the list header) with only the Infinite Scrolling default,
and its Save posts {"infinite_scroll": …} alone, so a stale legacy form can no
longer overwrite scan folders or other values edited in System (#330 S13-9).
Scan folders, monitoring status, auto-hide, display mode, worker threads,
LoraManager and the LoRA import leave the page (rows B8–B14).
* refactor(admin): open System for maintenance
Maintenance opens System › Maintenance in a new tab, where each operation runs
as a reviewed Activity job. The one-click legacy Database Maintenance dialog
and its methods go (rows B15–B18); formatFileSize stays for the image modal.
* refactor(admin): open System for backup and restore
Backup DB and Restore DB open System › Backup & export in a new tab, where a
restore is staged, reviewed and run as an Activity job. The one-click Restore
dialog and its methods go (rows B19–B20); the scan dialog's quick backup stays.
The removed browser tests' data checks live on at the route: a new round trip
test restores every prompt with its tags and image links.
* test(legacy): find a removed modal id passed to showModal and settle the links spec
The removal check also finds an id passed as a string to a ...Modal() call and no
longer reads data-id= as id=. The no-request test waits for the page's own
start-up reads to finish, the sort check compares as the server sorts, and the
kept-flow test checks the added prompt shows in the list.
* test(admin): open Page options after the saved default is read
Drops the leftover LoraManager section comment above the Page options save.
* refactor(gallery): open System for settings and maintenance
The gallery's Settings opens System › Appearance in a new tab. Its settings
dialog becomes Page options (a new header button) with only the page's own
settings: thumbnail startup check, lazy loading, items per page, grid columns,
infinite scrolling, debug mode and API timeout. Save and Reset keep every other
stored key, so the Appearance values System saves survive (S13-13). Generate
Thumbnails, Clear cache, Rescan and Find duplicates leave the page; Page options
links System › Maintenance and Sources & scans (rows B21–B28).
* refactor(autotag): manage tagging models in System
In both auto-tag dialogs a model that is not downloaded shows a Manage in
System link to System › Auto-tagging (a new tab) instead of a Download button,
and the gallery's Unload Model becomes the same link. The legacy download
dialog and the download, cancel and unload methods go (rows B29–B31); the batch
and review stay. A test hook marks a fake model loaded.
* docs(system): name System as the home for settings and maintenance
README and TESTING.md send settings, maintenance, backups, diagnostics, the
gallery's tools and auto-tag model downloads to their System section and name
each legacy page's Page options.
* fix(gallery): keep the default view in Page options and save viewer choices alone
System › Appearance's Default layout is the workspace's own key, so the legacy
gallery's Default View Mode stays a page option instead of losing its only
control. The viewer's mute and loop buttons remember just that choice instead of
saving the Page options dialog's unfilled inputs over the stored page options.
The pending-thumbnails toast names System › Sources & scans.
* test(legacy): check each System link sends no request in its own test
The no-request check runs inside each link's test, so a slow page no longer
times out the whole table, and it ignores lazy thumbnail loads the layout
starts on its own.
* test(legacy): a lazy thumbnail load is not a System link's request
* fix(gallery): let the header buttons wrap on a narrow screen
With Page options beside Settings the header no longer fits a phone's width;
it wrapped off the page and covered the Page options dialog in the touch runs.
* feat(gallery): remove Default View Mode from Page options (#330)
System > Appearance's Default layout is the one default-layout control, as
the S13 design's G5 says. The legacy gallery's Page options no longer show
or save a default view and Reset no longer resets it. A stored legacy
defaultViewMode still picks the view the gallery opens in (grid otherwise),
and Save and Reset keep that key for the workspace-preferences migration.
* feat(inspector): use recorded run history when the graph alone can't name the prompt (#386)
When a file's own graph leaves the prompt open, both inspect routes ask
recorded run history about the file (one run reported it and it was written
during that run). The save node that run reported it from anchors the walk
like a file-name match, so dataset branches that share one saver prefix
show their own prompt, labelled "Recorded run". Still open at one sampler
whose PromptManager text was recorded at execution (a language model's
output), the recorded text shows as "Execution recorded". Results the file
resolves alone never change; a failed lookup judges the file alone.
* fix(inspector): recorded texts follow history's saver; conflicts anchor nothing (#386)
Review follow-ups: the texts PromptManager recorded are looked up at the
sampler history's saver leads to, never at a sampler the unanchored walk
named; a file name naming a different saver than history anchors nothing;
outputs and texts are read in one transaction; "Recorded run" ranks below
every workflow-inferred source; unexpected lookup errors log at error level.
* fix(history): look up recorded runs by history's own spelling of the file (#386)
The history route inspects a case-normalized target on Windows, while
history matches file names exactly, so the occurrence's reference names
the file.
* ci: run the full matrix once per PR, in the merge queue (#420)
Pull requests now run a fast subset: Python 3.12 on ubuntu-latest, the
coverage gate, black/flake8, frontend modules, bandit and the Chromium/touch
frontend coverage shards plus merge. The merge queue, pushes to main and
manual runs keep the full matrix: Linux/Windows/macOS x Python 3.12-3.14,
real ComfyUI compatibility and the Firefox/WebKit page workflows.
A dropped matrix leg never reports a check, so the new required "all tests"
gate requires every test.yml job to succeed outside pull requests and real
ComfyUI to be skipped only on pull requests; viewerjs does the same for the
browser shards. branch-protection.json drops the eight legs a pull request
no longer runs and adds "all tests".
* docs(ci): ruleset edit keeps every rule and integration id; find merge-queue runs
* ci: move Windows and macOS out of the blocking path (#420)
The maintainer deferred Windows and macOS to the end of the redesign.
The merge queue now runs Linux on Python 3.12-3.14, real ComfyUI, the
frontend coverage shards and the Firefox/WebKit page workflows; pull
requests keep the Linux 3.12 subset. The six Windows/macOS legs move to
the new portability.yml, which runs on push to main, nightly and on
demand, keeps the 27-minute limit and is never a required check. Red
runs there are fixed at their cause before the cutover (#334).
* docs(ci): required checks live in the ruleset and the classic protection (#420)
Keep both features: a duplicate removal moves collection memberships and
copies the image category; both migrations and schema checks run; the
legacy route snapshot now records the six collection routes beside the
image category routes.
* test(legacy): snapshot the routes and the settings response
Pins every registered route and the key paths of GET /prompt_manager/settings
while System S13 (#330) moves the legacy pages' tools to native System. Recorded
on the unchanged main; cutover (#334) removes it.
* test(legacy): prove each removed legacy control has native coverage
TESTING.md gains the "Legacy System UI removed by S13" table (no rows yet).
legacy-coverage.test.mjs fails when a removal cites a capability that is not
covered, copies another native test, lists an id or method twice, or leaves
the id or method on its page. legacy-system-links.spec.mjs holds the System
link table and guards the kept admin flows.
* refactor(admin): open System for Diagnostics and logs
The admin page's Diagnostics and View Logs buttons become links that open
System › Diagnostics & logs in a new tab. The legacy Diagnostics and System Logs
dialogs and their methods go; TESTING.md lists them against their covered
capabilities (rows B1–B7).
* refactor(admin): open System for settings and keep page options
Settings opens System › Overview in a new tab. The settings dialog becomes Page
options (opened from the list header) with only the Infinite Scrolling default,
and its Save posts {"infinite_scroll": …} alone, so a stale legacy form can no
longer overwrite scan folders or other values edited in System (#330 S13-9).
Scan folders, monitoring status, auto-hide, display mode, worker threads,
LoraManager and the LoRA import leave the page (rows B8–B14).
* refactor(admin): open System for maintenance
Maintenance opens System › Maintenance in a new tab, where each operation runs
as a reviewed Activity job. The one-click legacy Database Maintenance dialog
and its methods go (rows B15–B18); formatFileSize stays for the image modal.
* refactor(admin): open System for backup and restore
Backup DB and Restore DB open System › Backup & export in a new tab, where a
restore is staged, reviewed and run as an Activity job. The one-click Restore
dialog and its methods go (rows B19–B20); the scan dialog's quick backup stays.
The removed browser tests' data checks live on at the route: a new round trip
test restores every prompt with its tags and image links.
* test(legacy): find a removed modal id passed to showModal and settle the links spec
The removal check also finds an id passed as a string to a ...Modal() call and no
longer reads data-id= as id=. The no-request test waits for the page's own
start-up reads to finish, the sort check compares as the server sorts, and the
kept-flow test checks the added prompt shows in the list.
* test(admin): open Page options after the saved default is read
Drops the leftover LoraManager section comment above the Page options save.
* test(legacy): a lazy thumbnail load is not a System link's request
* fix(exit): stop running jobs before Python joins the executor threads (#376)
ComfyUI returns from main.py after Ctrl+C without closing its event loop,
and Python joins every executor thread before any atexit callback runs, so
a scan batch, thumbnail batch or model download in flight kept ComfyUI
alive until it ended (on Windows each ffmpeg up to its 30 s timeout).
The package now registers a threading._register_atexit hook (after
concurrent.futures', so it runs before that join) that sets an exit signal
and kills the running encoder processes. Job.cancelled follows the signal,
so every check a job already makes between files stops it at exit;
map_parallel starts no further item, the scan walk stops between folders and
a tagging model download stops before its next chunk. Without the internal
hook exit behaves as before.
* fix(exit): abort Xet downloads and stop LoRA staging at exit (#376)
Review follow-ups: Xet transfers report progress from native code and ignore
the chunk checkpoint, so the exit hook calls huggingface_hub's own
abort_xet_session when it is loaded; an aborted download logs as stopped.
LoRA import staging reads the exit signal itself, so the legacy LoRA scan
stops too. The test's fake encoder outlives the ceiling by less, and
TESTING.md states the download limits of older huggingface_hub.
* fix(logs): Clear all logs deletes and empties only what its confirmation showed (#389)
- /logs/files rows carry an identity string (size:mtime_ns:ino); the
workspace sends back the active and rotated rows its dialog was built
from. Under the files and handler locks every listed copy is re-checked
with _matches_review and the listed active log's inode; any difference
refuses with nothing changed (409 logs-changed). A copy that appeared
after the confirmation is kept.
- A second writer of the active log (our flushed stream is behind the
file's size, or our file is now a rotated copy) refuses first (409
logs-foreign-writer); each unlink is preceded by a fresh identity check
and the active log is emptied last.
- An active log with a hard link (st_nlink > 1) is never truncated (409
logs-hard-link), also on the file-logging-off path.
- The legacy admin page's POST without a body keeps clearing what is
there, with the foreign-writer and hard-link refusals.
- The browser fixture grows the active log through the handler's stream.
* fix(logs): accept pre-1970 identities, refuse duplicate rows, say the foreign-writer check is best effort (#389)
Review follow-ups: a negative mtime_ns no longer makes every workspace
clear 400; a name listed twice is a malformed list; the confirmed-row cap
is 10000; the hard-link refusal says how to recover; the hard-link and
foreign-append tests assert the kept bytes before the refusal code, so on
the old code they fail on lost data.
* test(browser): API restores stay unseen by the open page's Activity poll (#413)
Specs that restore through the API while a workspace page is open check
how the page handles a restore it did not see. The page's Activity store
reads the job list every 15 s; on a slow runner (CI run 40646112668, a
16 s restore) that read landed during the restore, the page followed the
job and, as designed (S9), showed the open page again when it finished,
closing the Create tag dialog the vocabulary spec was about to use.
unseenRestore holds the page's job-list reads until the restore has
answered, so they read it as already ended. Used by the vocabulary,
library editor and history specs.
* test(browser): arm the restore gate before the page loads and drain reads in flight (#413)
A job-list read the page sent before the restore could still be answered
while it ran. The gate now routes every job-list read from before the
first navigation and, before a restore, waits for the reads in flight to
be answered. The history and library editor conversions are pre-emptive:
same race, never seen failing.
* fix(review): anchor files on their saver in the scan and Needs review (#390)
The scan and Apply now resolve with the file's name, as the workspace
inspector does since #383, so the one saver whose filename_prefix names
the file anchors its branch. A file whose branch that anchor resolves is
never queued (from the inspection profile it offers no choice; a
default-ambiguous file asks saver_anchored) and its pending, undecided
items leave on the next scan. REVIEW_RULE_VERSION is folded into
inspection item versions so old items are re-evaluated. No match, several
matches or an unfinished walk anchor nothing. Apply refuses a choice the
anchor contradicts.
* fix(review): refuse a default choice the saver anchor contradicts (#390)
Apply re-resolves a default item with the file's name: when its saver
anchors the branch, no candidate is offered and the choice is refused.
The scan's anchor check skips bytes carrying a decision and remembers
unanchored file states per process, so a rescan does not inspect an
unchanged file again.
* fix(review): queue an anchored file with its saver's pair as the choice (#390)
Maintainer ruling: a file never falls through both the link and the
queue. When only the saver anchor resolves a file the default profile and
the parser floor leave unlinked, the scan queues it with the anchor's pair
as its one candidate (evidence anchor: saver), never linking it. Files
already linked or resolved by the default path are unchanged. Apply
accepts the anchored pair when the bytes now resolve to it, and an
anchored item is a choice the untraced settle keeps.
* fix(review): upgrade old default items of anchored files, tag anchored results (#390)
A pending default item recorded before the saver anchor no longer
shortcuts the rescan when only the anchor resolves the file: the
anchored item supersedes it. Apply accepts a resolved re-resolution only
when it is tagged as the saver anchor's, the anchored pair must carry a
positive text to choose, and limited anchored evidence still counts as a
choice. TESTING.md describes the ruling.
* fix(images): read a loading collection as loading, never as 0 of 0 images (#391)
A reset published {loading, loaded:0, total:0} and the caption painted it
as "0 of 0 images" until the first page answered, so a fast reader (the
inspector concept spec on WebKit) read an empty result the server never
returned. The count line now says "Loading images…" while a first page is
in flight; the spec waits for the answered count, and a held-listing spec
pins the ordering.
* test(thumbnails): make the no-JPEG-XL-encoder fixture authoritative (#371)
{available:false} stood in only the cached detection, and every settings
read probes a cached None again, finding the host's real encoder. The
fixture now stands in for the detector itself and reset restores it; a
unit test proves the route against a detector that finds an encoder.
* test(system): wait for the restore job's own end before its toast (#361)
The reviewed-restore specs waited for the "Database restore finished"
toast with the 5 s expect, in effect a deadline on the disk-bound publish.
They now wait for the job's progress stream to close, assert the job
finished, and only then check the toast with the default timeout.
* test(images): wait for the search's answered count in the concept comparison (#391)
searchOf took the first changed caption as the search's count, the same
read-before-the-response race as the inspector's tag chip.
* ci: pin black and flake8, let the bandit hook fail, cache npm in the JS job (#352)
The lint job installs black==26.10.1 and flake8==7.4.1 from
requirements-dev.txt (the versions CI resolved on main today), so a new
release cannot turn CI red with no code change.
The local bandit hook ran 'bash -c ... "$@"; true': it could never
fail, and bash bound the first staged file to $0 so it was never
scanned. It now runs 'bandit -lll -q' directly, failing on high
severity exactly like code-quality.yml.
The frontend modules job's setup-node caches npm keyed on
tools/vendor/package-lock.json.
* test(history): wait on the writer's state, not short wall-clock pauses (#352)
Four 0.08 s pauses before asserting that the worker had not probed yet
now wait until the idle worker has run a whole loop pass at the current
manual clock value (three clock reads), so a slow runner cannot end the
pause before the worker decides. A mutation that probes 20 ms early now
fails two tests deterministically.
The real-clock bounded close no longer asserts close(0.02) returned in
under 0.2 s wall time; it asserts the in-flight write is still blocked
and the worker alive, like the output writer test. The manual-clock
bound stays exact. The last 0.1 s non-blocking submit check uses the
1 s NON_BLOCKING_CEILING from #351.
* test(api): give each API fixture database its own temp folder (#346)
Every PromptManagerAPI sweeps its database's folder at startup for the
stages, uploads and safety-backup copies an interrupted restore left.
The API test fixtures created their database directly in the system
temp folder, so any other test process starting an API there (another
worktree's suite, a pre-push hook, an agent's targeted run) deleted
this process's in-flight restore files. The restore, epoch and
retirement tests then failed with 'staged restore changed', 'unable to
open database file' or 503 instead of 409. Busy machines fail because
they are running other suites, not because they are slow: the same
CPU load with a private TMPDIR passes.
private_temp_db() puts the fixture database in a new folder removed at
cleanup (with any safety backups a restore left beside it). A
regression test sweeps a stand-in shared folder in the middle of a
multipart restore, as another suite's startup would.
* test(history): count worker parks, hold saturation blocks until released (#352)
wait_for_worker_pass now counts the idle worker's parks on its wake
event (two parks from now = one whole pass begun after the call)
instead of assuming a fixed number of clock reads per pass, which a
future clock read would have made vacuous. The real-clock bounded
close keeps a loose upper bound (0.02 s + NON_BLOCKING_CEILING).
The two saturation tests (history and output writer) blocked the
worker with release.wait(3) while they queued 1023 more items. On a
loaded runner the fill took longer than 3 s, the worker unblocked on
its own and drained part of the queue (pending 868..1017 != 1024),
and cleanup then reported a leaked worker. The block now holds until
the test releases it; 60 s is only a hang guard, and cleanup always
sets the release. Under one core with 8 busy loops: 8/8 runs failed
before, 6/6 pass after.
* test(maintenance): wait on duplicate jobs with a disk-bound hang guard (#409)
On Windows py3.13 CI (PR #405) a one-file duplicate cleanup missed its
10 s WAIT. The test already waits on the job's own task; the 10 s bound
acted as a deadline on disk-bound work. In the same 50 s window the
following tests' setups took up to 9 s against 0.1-0.6 s around it
while the test bodies stayed fast, which is consistent with a
process-wide disk I/O stall on the runner rather than a lease, handle,
executor or job stall (the log has no thread dump to show more).
tests/disk_waits.py holds the one disk-bound hang guard (90 s per wait,
under the 120 s per-test watchdog), shared with RESTORE_SECONDS (#382).
Waits on find and cleanup job tasks in the duplicate-files tests use
it; gates and server-state polls keep WAIT. The keeper-validation test
turns the safety backup off, since it does not test backups.
- M1: after a failed Save the Collections dialog shows the membership
loading and keeps Save disabled until the re-read lands, so a stale
plan cannot be sent.
- M2: the changes a Save made reach the page only when the dialog
closes, so a collection page's reload cannot remove the dialog
before its failure is read.
- L4: a New collection name equal to a listed collection ignoring case
is that collection's checkbox, checked; never an add then a remove.
- L5: a name that cannot be a collection name is refused on the field;
nothing is sent, the checkbox edits stay, no re-read.
- L6: focus starts on the field and moves to the first checkbox once
listed; closing after a re-render replaced the trigger focuses the
inspector's current Collections… button or its heading
(openContentDialog takes an optional focusTarget).
- L3, L7: TESTING.md D138/D139 describe the stale-source and deleted
collection outcomes and the above; the viewer spec asserts no listing
request while it is open, and a browser test covers M2.
images-concept compares Add to collection in place in the selection bar
(only Remove links… stays omitted) and the collection page's heading,
toolbar and filter row against the concept's #collection-landscapes,
with D137's Edit drawn into the concept. collections-concept compares
the Add to collection dialog with the concept's simpleInput (D138:
"Add", an empty field, the datalist of names), in both themes at 1600
and 390 px, with a mutation check.
TESTING.md gains the Collections contract (database, keys, missing
members, duplicate move, restore, routes, rules, limits, the PR 1
rulings and the all-matching ruling), D136-D140, and amends D28 and D8;
docs/application-redesign.md gets the delivery-status line. The coverage
baseline adds the two new modules; optional collection callbacks are
null rather than no-op defaults so they do not count as uncovered
functions.
* test(legacy): snapshot the routes and the settings response
Pins every registered route and the key paths of GET /prompt_manager/settings
while System S13 (#330) moves the legacy pages' tools to native System. Recorded
on the unchanged main; cutover (#334) removes it.
* test(legacy): prove each removed legacy control has native coverage
TESTING.md gains the "Legacy System UI removed by S13" table (no rows yet).
legacy-coverage.test.mjs fails when a removal cites a capability that is not
covered, copies another native test, lists an id or method twice, or leaves
the id or method on its page. legacy-system-links.spec.mjs holds the System
link table and guards the kept admin flows.
* refactor(admin): open System for Diagnostics and logs
The admin page's Diagnostics and View Logs buttons become links that open
System › Diagnostics & logs in a new tab. The legacy Diagnostics and System Logs
dialogs and their methods go; TESTING.md lists them against their covered
capabilities (rows B1–B7).
* test(legacy): find a removed modal id passed to showModal and settle the links spec
The removal check also finds an id passed as a string to a ...Modal() call and no
longer reads data-id= as id=. The no-request test waits for the page's own
start-up reads to finish, the sort check compares as the server sorts, and the
kept-flow test checks the added prompt shows in the list.
* fix(restore): a restore cut off by shutdown waits for its worker thread (#360)
A cancelled restore handler or job no longer removes the stage or the
upload, or ends its exclusive registry admission, while the executor
thread still stages or publishes. The cancel waits for the worker; a
publish that finished is adopted before the cancel goes on, and a stage
finished after the cancel is removed.
* fix(threads): a thread or pool refused at interpreter exit is quiet and leaves nothing pending (#375)
During interpreter exit, threading._shutdown shuts every thread pool
before the package teardown runs while our daemon threads keep working.
map_parallel now runs the items a refused pool did not start on the
calling thread, schedule() drops the path and returns False when its
worker cannot start, and the preview queue skips the file. Each
refusal is logged once at DEBUG without a traceback; other start
failures behave as before.
* fix(restore): keep the cancel and the adoption when a stopped restore's publish ends (#360)
A publish that fails after the cancel now ends in CancelledError (after
its prune) instead of a 500, and the restored library is adopted to its
end even when a second cancel arrives. The blocking calls go through
_run_in_executor again, as the executor contract test expects.
Exit-window refusals: the comments state what was measured (a pool is
refused during threading._shutdown; Thread.start only once the
interpreter is finalizing), and the tests assert the one DEBUG line.
The shared inspector's footer ends with Collections… (folder icon) after
Prompt history for a listed file, in the Images side panel and the
viewer. Its dialog reads the file's membership, lists every collection
as the concept's checkbox rows, takes an optional new name, and Save
sends one request per change (new name, added, removed). A failure keeps
the dialog with the membership read again. On a collection page a change
to the open collection reloads the listing at once from the side panel
and only when the viewer closes from the viewer. The inspector concept
comparison draws the button into the concept after Prompt history (D139,
#331).
- A list read superseded by a newer one (the 300 ms count read, a restore)
no longer shows "could not be loaded": the collection and saved-view
pages wait for the list to settle.
- A listing that finds the open collection gone shows the deleted page once
the list no longer holds it; when the list read fails or still holds it,
the page keeps the listing's error and Retry instead of staying blank, and
each failed read is reported again.
- The missing-members note follows every list read and goes away at zero.
- A rename that settles after its Edit dialog closed leaves any dialog
opened since alone.
- The deleted-collection, list-failed and saved-view failure pages focus
their title after a route change, as every page does.
Part of #331
Add to collection follows Set category in the Images selection bar and
opens the concept's simpleInput ("Add selected images to collection",
"Collection name", Add) with the existing names as a datalist. Remove
from collection follows it while a collection filter is active and
reloads the listing in place, keeping the checked files still listed.
Every outcome uses the design's strings (section 7.4). The routes take
at most 500 explicit files, so an "all N matching" selection is sent
only when every match is loaded, checked and at most 500; otherwise the
action toasts "Select at most 500 images." (D138, #331).
The slideshow fixture wrapped window.setTimeout before page.clock.install(),
so the viewer's timer ran on real time and clock.runFor() did not drive it;
a late WebKit timer failed 'slideshow exposes exact state labels' on CI.
The concept hides COLLECTIONS at 950 px and below, which would leave a phone
no way to open a collection; production shows the rows there in the wrapped
layout of every other sidebar nav (D136). The shell comparison now compares
the group instead of filtering it out: the concept fixture seeds Landscapes
(12) and Experiments (8), the rows compare by name order, and at phone width
the nav compares against the concept's own sidebar-nav layout. The
collection rows are checked at 1600x1100 and 390x844 in both themes.
Part of #331
The sidebar gains the concept's COLLECTIONS group (D136): one row per
collection with its folder icon, name and listed-member count, without the
PROPOSED badge; the label shows once a collection exists. A row opens
#/collection/<id>, the Images page filtered to that collection (D137): the
collection's name as h1, Collection as breadcrumb and title, Edit to rename
or delete it through the saved views' Edit dialog with collection nouns, and
a note counting members the sources do not list now. An unknown or deleted
collection shows a page that says so with a link to Images. A saved view on
a deleted collection lists nothing and says so (MC6, D140). The list is read
again after a restore and the counts 300 ms after a change.
Part of #331
* fix(system): Tagging selects show their whole chosen option at 390 px (#398)
The concept's phone cap (120px) cut Existing tags ("Replace after review").
#tag-existing now never shrinks for its row text and keeps its natural width
at phone width, beside the text. The comparison expects selectsFit for every
select, and a new test walks every option of both selects at 390 and 320 px
in both themes. TESTING.md D132 records the decision.
* fix(system): bound Existing tags by its row at phone width (#398 review)
The category routes also accept the Images listing filter of "Select
all N matching" (#392) instead of explicit items, resolved now and
capped at 500 images like an explicit selection.
Review fixes: a source matches by its configured spelling too, so a
detected output folder that is a symlink keys its files (routes and
duplicate cleanup). File keys are computed in the executor. A path no
source holds is a plain 403, a lone surrogate a 400. The install repairs
a table that lacks its category index instead of refusing to open.
PUT /prompt_manager/images/category sets the own category of up to 500
listed files, DELETE removes it (listed or not), and
GET /prompt_manager/images/categories lists the values whose file is
listed. Targets are the Images listing identity of an output source,
checked by media_target (shared with Collections). The routes never
touch prompts, recipes or links; POST /prompt_manager/bulk/category is
unchanged.
The kept copy's own category wins; a kept copy without one receives the
removed copy's, keyed by the deepest output source holding each file.
The removed copy's row is never deleted, so a give-back or a failed
delete loses nothing. The job log reports how many were kept.
One new table, image_categories, and its category index, created empty
by a forward-only migration that reads and writes only its own objects.
Startup checks the structure; restore also checks the values. The store
sets, clears and reads own categories by file key (the deepest output
source holding the file and the path below it), and copies a removed
duplicate's category to a kept copy that has none.