ea16e07c4626af5c193914cb2d01459f8950cad1
22
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
31ada14111 |
docs: delete four documents that contradict the code
They do not merely lag it, they contradict it, which is worse than absent: the only way to find out one is wrong is to trust it. docs/dev/roadmap.md, 1648 lines. Its "Current State" describes an app with basic playback, a track list and an album grid. Phases 1, 2, 3 and 5 have shipped entire -- playlists, shuffle and repeat, shortcuts, virtualised lists, search, custom columns, smart playlists, the MusicBrainz client, autotag. Its Decision Log records "Testing: Deferred" against 836 Vitest tests, a Playwright suite on two engines and race-detector passes in three build configurations. .pi/journal.md, "what happened and what's next", frozen at plan 005 and still saying everything from phase 1 onward is uncommitted. That is the tracker's job now, and a work log that is wrong about what is committed is a hazard rather than a stale file. docs/dev/overview.md, a shallower and partly incorrect CLAUDE.md architecture section. README pointed at it and points at CLAUDE.md now. docs/dev/config-suggestions.md, eight suggestions of which one survived -- the rest were overtaken by rewrites: applyDefaults exists, scan concurrency is configurable with SSD/HDD detection, and httphandler.go and the WriteHeader-after-render bug it described are gone entirely. Nothing is lost: the genuinely unbuilt work was filed first, as #94 device sync, #95 layout customisation, #96 AcoustID and #97 the config setters that take no lock. Refs #98 |
||
|
|
0bfa2136be |
feat(dev): ask the phone instead of looking at it
The device tier could only take a screenshot and read what Go chose to log, and a screenshot cannot tell a dropped CSS declaration from a missing asset. This adds the third thing: the page's own answer, from the engine that is really rendering it. `make android-screenshot` grabs the screen, `make android-inspect` forwards the WebView's devtools socket, and `make android-eval EXPR=...` evaluates in the real page. Four details are load-bearing. Only a `debuggable` build opens that socket, so the debug build type takes `applicationIdSuffix ".dev"` and installs *beside* the release app -- the two carry different signing certificates, and Android's only remedy for a changed certificate is an uninstall, which takes the user's library with it. Playwright cannot drive a WebView (`connectOverCDP` calls `Browser.setDownloadBehavior`, which it answers "Browser context management is not supported"), so the eval is raw CDP over Node's built-in WebSocket. The socket name carries the pid, so it is resolved per launch rather than written down. And `exec-out`, not `shell`, for the screenshot: a pty translates LF and corrupts the PNG. What it immediately established is why it was worth having. The phone renders in Chrome 113 at 424x439 CSS px -- two years behind every browser the other tiers use, with no Popover API and no relaxed CSS nesting -- so a spec passing at that viewport says nothing about the device, and two conclusions drawn from version numbers alone were wrong. Both are corrected in NOTES.md and the plan. |
||
|
|
b1cdef8769 |
docs: record what a phone said that no tier could
The first device run of the published APK, and the first runtime evidence any of the Android work has ever had -- A4 shipped entirely reasoned from source. It confirms A4 whole: playback survives the screen locking, and the transport notification appears with cover art, which settles four open questions at once (the service starts, the permission was granted and the notification is visible, the lock screen picks up the session, and art decoded from a MANAGE_EXTERNAL_STORAGE path by a service is readable -- the one nobody could argue from documentation). It also found the two faults fixed in the preceding commits, and the lesson worth keeping is why *those two*: both are things the platform adds rather than things the app draws. So the skill's Android tier now says to ask a device about system bars, the back gesture, focus and audio interruptions, permissions and the keyboard -- and not about layout, which the other five tiers already cover. |
||
|
|
29299d17da |
fix(dev): run the local e2e tier against the app CI runs
Two specs failed locally and passed in CI, which is the least useful direction for a disagreement to point. **`dev-headless.sh` was the only launcher not stubbing out the catalog.** `seed-sandbox.sh` and `ci.yml` both send `YJ_CORE_INDEX_URL` to a dead address; the dev launcher did not, so the app downloaded and built the real ~1M-row Explore catalog into the run's YJ_HOME and every local `make e2e` after that ran against a world CI never sees. Found by reading the failure screenshot: the spec had searched Explore for its fixture album and the page was full of real ones. It defaults to the dead address now and takes an explicit one for exploring by hand. **And the shared backend carries spec state between runs.** `explore-shelves` staged its catalog only `IfEmpty`, so one album row left behind by `requested-badge` satisfied that gate: the shelves were drawn from a single foreign row and the artist card the spec clicks did not exist. It failed on the *second* local run and passed on the first, and never in CI, where every run gets a fresh home. "Is the catalog empty" was the wrong question and "are my rows there" is the right one, so staging is unconditional (INSERT OR IGNORE keyed on the MBID) and the assertion moved from *this insert wrote a row* to *every fixture row is present*. That is both idempotent and stronger: an MBID that fails CHECK(length(mbid) = 16) is silently dropped by OR IGNORE, which the old per-insert count caught only on a cold catalog and the new one catches always. Verified by running the whole suite twice against one app: 97/3 before, 100 passed both times after. |
||
|
|
df2e9ea777 |
docs: record what the Android work established and disproved
Section A of plan 016 is closed and B1 is decided, so the three tenses move together: CLAUDE.md for what mediacontrols now is, the skill for what to run, NOTES.md for what was measured and when. The entry worth reading is the one that disproves a claim written here earlier in the same session. Dropping x86_64 was expected to make make android-install fail with INSTALL_FAILED_NO_MATCHING_ABIS. Measured, it installs and launches: Google's google_apis x86_64 images carry arm64 translation (abilist = x86_64,arm64-v8a), so the loader maps lib/arm64/libwails.so and runs it. It dies before any of our code with SIGILL, and the disassembly names the reason exactly -- `mrs x0, ID_AA64ISAR0_EL1`, Go's internal/cpu reading the arm64 feature register at runtime init, which the translator does not implement. So no Go binary starts under it, and that is not a property of this app. Which closes the last plausible shortcut. There are now three distinct ways this app fails on an x86_64 Android -- seccomp on the x86_64 build, an unimplemented system register on the translated arm64 one, and a real device still unverified -- and none of them is a bug in it. A phone remains the only verification path. Plan 016 also carries the B2 scope, now decided rather than recommended: option 1's data model with option 2's surface. The phone gets home, library browse, now-playing-as-a-view, the queue, search and playlists; it does not get autotag, downloads, Explore or the 93-control Settings page, and each of those has a reason written beside it. One rule for the work: no view forks, because a phone template that copies a view's is two templates to fix every bug in. |
||
|
|
f7dc76c955 |
docs(android): an arm64 image will not run on an x86_64 host
Build & publish Arch package / arch-package (push) Successful in 2m32s
Search index maintenance / maintain-index (push) Successful in 7s
CI / e2e (push) Successful in 5m42s
CI / check (push) Successful in 2m22s
Sync Homebrew formula / sync-formula (push) Successful in 6s
Build & publish the Android APK / apk (push) Failing after 50s
Emulator 37 refuses cross-architecture emulation outright -- "Avd's CPU Architecture 'arm64' is not supported by the QEMU2 emulator on x86_64 host" -- and there is no flag for it. Google dropped it. That matters because the previous commit's finding points at arm64 as the ABI that works, so the obvious next move is to boot an arm64 AVD, and the obvious next move costs a 3.8 GB download before it fails. Written down so the next session does not spend it. The consequence is stated rather than hidden: the claim that arm64 avoids the seccomp trap rests on reading modernc's two code paths, not on having run it. Verifying it needs an arm64 host, a physical device or adb connect. |
||
|
|
ed975019dc |
fix(dev): the smoke target died silently on a genuinely dead app
Two harness bugs and the finding that exposed them. **`pidof` exits 1 when it finds nothing**, and under `set -e` a failing command substitution killed the script before it could print anything -- rc=1, no output. That was invisible for as long as the app crash-*looped*, because there is always some pid in that state. It appeared the moment the app died for good and ActivityManager stopped respawning it, which is precisely the run you most want output from. **And an install failure said nothing useful.** Both ways it fails are about identity rather than the build: INSTALL_FAILED_VERSION_DOWNGRADE when a bare `make android` (versionCode 1) meets something a versioned build left behind, and a signature mismatch when a debug-signed local build meets a release-signed one. Both were hit in one session, and both are fixed by uninstalling. The target says so now instead of leaving someone to read the constant name. The finding: with the startup bug fixed the app reaches the database and takes SIGSYS on the x86_64 emulator, because modernc.org/libc's Xlstat64 issues a raw lstat syscall on linux/amd64 and Android's seccomp filter forbids it -- bionic never issues it. arm64 has no lstat syscall at all, so ccgo_linux_arm64.go routes Xlstat through fstatat and is structurally unaffected; Go's own syscall package already used fstatat on both. So the default emulator cannot verify this app, and the skill says so rather than letting the next session read a tombstone as a regression. |
||
|
|
68468e5378 |
feat(dev): an Android failure looks exactly like a success
The APK installs and launches. It also dies six milliseconds later, and finding that out cost a cycle for three reasons that have nothing to do with the bug itself: **Go's stdout does not reach logcat.** An Android app's fd 1 and 2 go to /dev/null, so every slog line -- including the one naming the error the app is about to exit on -- is discarded. `setprop log.redirect-stdio true` does not help: that redirects the Java runtime's System.out, and our code is a c-shared native library. **os.Exit leaves no evidence.** No panic, no AndroidRuntime stack, nothing in /data/tombstones, nothing in `logcat -b crash` or dropbox. All three places anyone would look are empty, and the one signal that is present -- "Zygote: exited due to signal 9" -- reads as "the system killed it" and sends you after the low-memory killer. **ActivityManager restarts it faster than you can observe.** pidof always answers and `am start` always reports Status: ok, so a crash-looping app looks alive. "Did it start" is the wrong question; `make android-smoke` asks whether it is the *same pid* N seconds later, and prints the filtered logcat plus how to read it when it is not. The tell, once known: "I/WailsBridge: Wails bridge initialized" followed immediately by a new pid doing the same thing. scripts/android-emulator.sh follows dev-headless.sh's shape -- background start, saved-PID stop, filtered log tail, never pkill -f. Two scaffold tasks are deliberately not wrapped: `android:logs` greps logcat for (Wails|yellowjacket), which catches the WailsBridge tag but misses the app's own process tag (app.yellowjacket is lowercase) and misses ActivityManager's "has died" line, which is the one that says it crashed; and `ensure-emulator` boots whatever `-list-avds | tail -1` returns, with no pidfile and no boot wait, so it cannot be sequenced. One environment note that is not obvious on Arch: Gradle needs a platform and /opt/android-sdk has none, so ANDROID_SDK defaults to ~/Android/Sdk while ANDROID_NDK points at /opt/android-ndk. Two SDKs, one for each half of the build. |
||
|
|
e7748f1fd5 |
feat(database): shape the library like files, and shrink the catalog
Plans 013 and 014, the album page that prompted them, and the smaller fixes they turned up. Changelog, largest first. ## The local library is shaped like files, not like MusicBrainz `audio_files` carries its own tags and points at `albums` and `artists`; `file_genres` is the one real many-to-many. `recordings`, `release_group_recordings`, `artist_credit`, `artist_credit_artist`, `recording_genres`, `release_groups` and `release_to_rg` are gone from the local side, and with them a six-way join in every read, a `MIN(release_group_id)` subquery in eleven queries and a first-credited-artist subquery in nine. Measured on a real 25,966-file library, every many-to-many that model expressed was 1:1 in the data. - Ownership is a file. `GetFilePathsByRecordingMBIDs`, `LibraryMBIDIndex.CheckMBIDs`, `collectLibraryEntities` and `pruneStaleLocalCrossReferences` all join `audio_files`, so the 812 orphaned recordings, 216 release groups and 260 artists that library carried are now structurally impossible. - One projection: every track query selects from the `track_metadata` view, one row type, one mapper. Nine hand-rolled copies had drifted far enough to report different years on different screens. - `library_id = 0` means every library, so each list query exists once instead of scoped and unscoped with a branch at every call site. - No migration chain. `sql/schemas/` is the one description of the shape; `sql/migrations/`, `applyMigrations` and `schema_migrations` are squashed away, along with the drift between them that had sqlc generating against a stale schema. - `database.InsertTestTrack` is the one test seeder; twenty test files had been assembling the old FK chain each in its own order. ## The catalog stores its ids as bytes `explore_index`'s three 36-char MBID columns and its entity-type text are 16 raw bytes and a small integer. The table and its six indexes go 780 MB to 405 MB on a real 2,052,200-row catalog, which is why a fresh install is ~0.6 GB rather than ~1.0 GB. - `backend/explore/mbid.go` is the only place the encoding is known; everything above it speaks dashed strings. - `CHECK(length(mbid) = 16)` makes a stringly write fail at the insert rather than silently returning no rows, since SQLite does not coerce between TEXT and BLOB. - The importer asks the artifact what encoding it carries and converts on the way in, so the artifact already published keeps working and no format bump is needed. - `indexRowColumns`/`scanIndexRow` replace four copies of a 22-column list, and `TestStoredEncodingRoundTrips` sweeps every read path. ## An album page that says how much of the album is yours - One question, asked once: is there a file. `filePaths` is filled by a single batched lookup when the tracklist settles, and the badge, the Play count, the dimmed rows and every menu item read it — replacing four claims of decreasing confidence that could show a green tick on an album whose every action did nothing. - Play, Play 7 of 12, or no play button at all. - `total_tracks` on `explore_index` (~2 bytes over 400,677 release groups) and on `audio_files` from tags that have always carried it: a complete MBID-matched album now makes no catalog call at all, where it used to spend the most expensive request the app makes. - A merged cluster shows the running order the most releases agree on, and the version list marks the release you own rather than standing a synthetic entry in for it. - `AlbumReleasesFailed`: a slow fetch is no longer reported as a failed one by a 12-second timer. - Rows not in the library are dimmed in place (with `aria-disabled`) instead of the owned ones wearing a green tick and a legend. ## Caches and cover art get ceilings - Only the three tiers of a cover are stored; the full-resolution copy nothing rendered was 1,134 MB of a 1.4 GB covers directory. - One artist portrait is downloaded and the rest are remembered as URLs — 4.1 GB of a 5.3 GB cache was candidates no code path reads. - `browsedArtBudget` and `httpCacheBudget` bound what an age cannot: the same install held art for 5,770 artists in a 1,301-artist library. - `OrphanedArtistImagesJob` joined a bare MBID onto a sharded directory, so it deleted the rows that were the only record of the files it left behind. `explore.ArtistImageDir` is that layout's one definition now. ## The autotag queue asks whether there is work `tagging_items` was a row per album folder, not a queue, and no query read the `tag_status` column that held the answer. The four queue queries ask the files, which matters most where it is least visible: `startPrefetch` was scoring every album in a tagged library against MusicBrainz. ## Phantom playlist tracks resolve in place An M3U8 imported before its files leaves phantom rows; they now match by path and fall back to position, keep their place in the playlist when resolved, and pair best-first so two phantoms cannot claim the same file. ## Playing a track plays the list it is in Double-click, and Play on a single row's menu, queue the list as displayed with `startIndex` on that row — the album page and the track list used to queue one track and discard the album around it. A multi-row selection still plays exactly itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh |
||
|
|
deb3f3da7e |
feat(wails): move the e2e harness and headless launch onto v3
make e2e is green on chromium: 92 passed. The harness is rebuilt on
what v3 actually offers, and three of the four things it replaced turn
out to be better than what they replaced.
The headless launch is v3's own server mode. scripts/dev-headless.sh
ran a `-tags dev` binary whose app_dev.go parsed -devserver/-assetdir
out of os.Args; that file went with v2, so the harness had no server at
all. `-tags dev,server` is a first-class mode and needs no display, so
Xvfb is gone from the script and from CI.
The bridge hooks two places, neither of them EventsOn. Inbound is
window._wails.dispatchWailsEvent, wrapped by pre-creating the object
the runtime keeps and putting an accessor on the one property.
Outbound is fetch: v3 routes every runtime call through one POST, so
the bridge sees binding calls and event emits from any module, needs no
walk of an object graph, and cannot miss a call made before it looked.
__yjEvents.call posts to that endpoint by method name, so it depends on
nothing in the app's bundle and works on a page with no init script.
That is what lets seed-sandbox.sh drop playwright-cli entirely — it
drove AddLibrary through a browser only because window.go was v2's one
way in — and with it a global npm install and a second Chromium in CI.
measure.mjs and one spec lose their window.go walks and read the
bridge's log instead; e2e/support/method-ids.mjs derives id -> name
from frontend/bindings/ (phase 6b option 1, so it cannot go stale
silently). Plain .mjs because measure.mjs runs under bare node and one
derivation beats two that can disagree.
Four bugs surfaced, and the migration is how.
The cross-service wiring never ran headless. It hung off
Common.ApplicationStarted, which server mode never emits —
setupCommonEvents is an explicit no-op there — so the queue had no
TrackLoader and playing a track changed the queue and then silently did
nothing. It is a service registered last now (backend/startup.go):
services start in registration order, which is the ordering the wiring
needs, in every mode.
Six specs called SetQueue with 3 of its 4 arguments. v2 accepted that
and filled the gap; v3 answers "expects 4 arguments, got 3".
requested-badge's cleanup read window.go and returned early on
`if (!svc)` — the silent cleanup its own comment was written to
prevent, one migration later. It posts to the runtime endpoint now,
which any page can do.
SearchIndex.Search trusted a startup latch, so rows a spec staged
afterwards were unsearchable and three specs passed only when an
earlier one happened to flip it. shelves.go fixed exactly this and left
hasCatalogRows behind; the search path now uses it as the fallback,
with the latch still the fast path.
Two spec edits are deletions of assertions about v2. harness.spec
checked Object.keys(window.go) and that a bad call *hung*; it now
checks the real runtime is loaded and that the backend rejects with a
TypeError naming the argument. album-actions asserted a tracklist
legend that
|
||
|
|
a4ada725a2 |
feat(wails): rebuild the Vitest fake on v3's transport seam
v2 installed two globals and the fake replaced both. v3 has neither — the runtime is an npm module and the generated bindings call into it. What it has instead is better: setTransport() is a public seam for replacing the IPC transport, and *every* runtime call goes through it, so the fake is smaller than v2's and covers strictly more. The event dispatcher is no longer mirrored at all. v2's fake reimplemented desktop/events.js — the listener list, maxCallbacks expiry, the reverse iteration — because there was no way to reach the real one; emit() now goes through window._wails.dispatchWailsEvent, which is the entry point the backend's own push uses. What is mirrored instead is one line of Go: how EventManager.Emit packs variadic data into an event's single data field. Registration and unregistration are the public Events API. The one non-public thing left is the listener registry, aliased in vitest.config.mts and used only by listenerNames() — a test asks whether importing a store subscribed it, which nothing public can answer. A binding carries a method ID, not a name, so the fake derives the ID -> path map from the generated tree: FNV-1a over the FQN, with the Go type's casing recovered from each package's index.ts, which is the only place it survives (library/library.ts cannot tell you it is FrontendUtil). The map has to be complete rather than lazy because 21 assertions read calls() with no argument and compare the whole list. Two things had to move that are not the fake. fixture() drains microtasks between two renders: a v3 binding settles several hops later than v2's, and tests were already written as though fixture() meant "mounted and loaded". Microtasks and not a timer, which would hang under the suites that install fake ones. tracklist-store keeps its defaults on an empty answer instead of emptying the column list. GetTrackListColumns substitutes DefaultColumns only when the whole config section is missing; a section that exists with no columns returns nothing. Until now this was accidental — the binding was typed Column[], an absent answer arrived as undefined, and .map threw into the catch. 757 tests pass across all 63 files. They are run in batches: a single browser session dies partway through the 58 it queues, which reproduces unchanged at the pre-migration commit and is a resource limit on this machine rather than anything here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm |
||
|
|
c9905fbcff |
docs: drop the webkit2_41 tag from the commands agents run
The commit before this removed the tag from the Makefile, lefthook, both packaging recipes and CI, but left it in CLAUDE.md's "Running tests" section and the yellowjacket-dev skill — which are the copies a coding agent actually runs, so a stale tag there is worse than one in prose. skill-check does not catch this: it verifies that documented make targets exist, not that documented go commands do. The historical mentions in .planning/ and .pi/journal.md are left alone; they are records of what was true then. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm |
||
|
|
dc890d1fcc |
feat(library): remove a track from the library without deleting the file
RemoveFromLibrary deletes the audio_files rows the way the scan's own orphan cleanup does and records each path in excluded_paths. The exclusion is not an enhancement: without it the next scan finds the file, sees no row and imports it again, so the button undoes itself. The soft scan compares files on disk against rows in the database, so surveyAudioFiles and countAudioFiles both take the exclusion set — otherwise an excluded path makes the two disagree forever and queues a full scan on every launch. Deleting a row cascades to queue_tracks, so the removal calls the same CompactQueue hook RemoveLibrary does. Also lands the requested badge: library-status-indicator is a button again where it can act, utils/library-status.ts states once what owning and wanting mean, and the long-declared queued state finally has a producer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm |
||
|
|
0b7ffd5679 |
build: check that css template literals were not ended by a comment
A backtick inside a comment in a css`` literal ends the literal. It has cost four sessions across three plans, it is written down in CLAUDE.md, the skill and NOTES.md, and it was read twice in the session it then cost a cycle in. Knowledge that has been ignored three times is not a knowledge problem. The expense is the report, not the mistake: the literal ends early, the rest of the CSS parses as JavaScript, and tsc says 'Class static side incorrectly extends base class static side' pointing at a line of prose -- or, in a shared module, every test in the suite fails to import and the output reads like a broken test runner. make dev-headless mean- while keeps serving the last good bundle. Detection is exact rather than heuristic: if a backtick in a comment closed the literal early, the text the parser took as the literal contains an unterminated /*. Nothing else produces that. Verified both ways -- clean on the tree, and red on a deliberately broken comment. |
||
|
|
287b6445fa |
fix(a11y): give every wa-dialog an accessible name
Eleven dialogs passed a `label` that never reached the accessibility
tree: Web Awesome renders it into an <h2 id="title"> in the same shadow
root as the native <dialog> and never points aria-labelledby at it, so
getByRole('dialog', {name}) matched nothing and a screen reader
announced an unnamed dialog. a11y.md lists all of them under "what is
already correct".
utils/name-dialog.ts sets the IDREF, with aria-label as the fallback for
without-header (first-run-wizard), called from each host's updated().
aria-labelledby rather than aria-label because three call sites compute
their label at render time, and the heading re-renders anyway. It waits
for the dialog's own first update: wa-dialog populates its shadow root
in its own update, so a query at the host's firstUpdated names nothing.
Reaching into another library's open shadow root is deliberate and the
failure is bounded — if the structure moves, the query misses and the
dialog is as unnamed as it was.
|
||
|
|
d681a7223e |
docs: record the audio clock, and that CI is green
The e2e job passes on both engines for the first time, so the three files that describe it as red are wrong. Also records the two things that made it findable: the CI container is reproducible under Docker, and the app's own audio stack had to be the thing measured. |
||
|
|
425dd7c158 |
docs: record the third Phase 5 pass, and read the CI log
The e2e failure two sessions could not diagnose is the container's audio clock, on both engines — 48 specs pass under Chromium and 48 under WebKit, failing the same three. Nothing in last pass's dialog, focus or role work is WebKit-specific. Also records what got in the way of knowing that: gitea_ci's job-log endpoint 404s on this build while the REST API answers fine, and the WebKit step had been skipped on every red run. |
||
|
|
9f03b3ff94 |
ci: enforce the commit format CLAUDE.md said was enforced
CLAUDE.md has claimed since the file was written that commitlint gates the commit format in CI and that semantic-release runs off it. There was no commitlint config, no workflow running one, and nothing invoking .releaserc.yml — so the first thing every contributor and every agent reads about this repo was false in two places. scripts/commit-check.sh is the smaller honest answer: the grammar is one regex, and commitlint would mean a Node dependency tree at the root of a Go repo to run it. It is a commit-msg hook locally and a CI step over every commit in a push, and its type list is .releaserc.yml's so the check and the release rules cannot drift. The semantic-release half is recorded as configured-but-not-wired rather than implied to run. |
||
|
|
3269da3e92 |
docs: record the first Phase 5 pass and what it corrected
Two reproductions in this pass were read before Lit had rendered, so both reported the same answer on the broken build and the fixed one - the third costume of this plan's most-repeated trap, and the first time it has appeared in a reproduction rather than a measurement. Also records that the audit's symptom for H-11 outlives its mechanism, that fixing H-7's arithmetic does not remove every clipped Duration, and that two e2e specs spend backend state they never give back. |
||
|
|
9e92721bb7 |
docs: record plan 007, its four audits, and what measuring corrected
`.planning/audits/2026-08-11-ui/` is the pass this work came from: the app driven by hand headless plus three static reviews, ~118 findings that are really five problems, each spread by being copied rather than fixed. `.planning/plans/active/007-ui-reconciliation.md` sequences them by blast radius and records what each of the six passes actually shipped — including twenty-five entries under "where the plan was wrong", which is the point of writing it down. The discipline those entries add up to, now in NOTES.md: a finding is three hypotheses — how big it is, why it is that big, and what to do about it — and they can be independently right and wrong. Three of the audit's recommended fixes would have shipped a bug (`m1` stops the card grids repainting, `m6`'s index-ordered selection goes stale on any re-sort, `m5`'s guard leaves the marquee short), all three because they reasoned from the shape of the code and not from what the rest of the file already knew about it. Five findings evaporated or inverted on contact. CLAUDE.md gains the invariants that came out of it, and the skill gains the fourteen measurement traps, each of which produced a wrong number first — the newest being that a longtask entry arrives after the task that produced it, so two numbers that must agree are worth more than one you have to be sceptical about. |
||
|
|
213640c9a8 | docs(journal): record the CI push and the Gitea log-retrieval route | ||
|
|
5ca6cad45a |
feat(harness): agent-drivable dev harness and CI that gates
A coding agent could develop this repo's Go packages and could not develop the application: every path to running YellowJacket ended in a blocking GTK window, so 265 bound methods, 46 events, 33 component directories and 13 stores had exactly one form of verification available — `tsc --noEmit`. The unlock is that `wails dev`'s dev server on :34115 serves the real frontend with the real generated bindings against the same Go backend a desktop window attaches to, so a plain Chromium under Xvfb gets a fully functional app. Four test tiers now exist, cheapest first: - `make ui-test` — 313 Vitest tests in a real browser in ~2 s, no app, no backend, no display. Works because `frontend/wailsjs/` is a pure passthrough to `window.go`/`window.runtime`, so faking just those two globals runs the real bindings and the real store code. - `make test` — services in-process, asserting on the payload the frontend would receive, via a new `events.Emit` wrapper. - `make dev-headless` + `playwright-cli` — the real app, driven interactively, with an event bridge on `window.__yjEvents` and a dev-only control surface at `/__test/`. - `make e2e` — 19 of those flows frozen as Playwright specs. `events.Emit(ctx, …)` replaces all 35 direct `runtime.EventsEmit` call sites: wails' `getEvents` `log.Fatalf`s on any context without its runtime, so those paths could not run under test and a background worker could take the app down. Four packages had each hand-rolled the same guard; nine more guarded on `ctx != nil`, which does not help. `TestNoDirectRuntimeEmits` fails the build on a new one. Fixtures are generated, not committed (`make testdata`), and seeds are built by *running the app* — never by hand-writing config and DB rows, which would be a second description of a valid YJ_HOME. `.gitea/workflows/ci.yml` is the first workflow here that tests anything; the other three only package, so `gitea_ci` reported only packaging jobs and misled anyone asking whether a push was healthy. Both jobs were prototyped to green in a bare ubuntu:24.04 container before the YAML was written, which immediately caught `make lint` linting three configurations that nothing builds: all three passes omitted `webkit2_41`, so wails resolved webkit2gtk-4.0 — which Arch still ships and Ubuntu 24.04 dropped. Operational instructions live in `.pi/skills/yellowjacket-dev/`, measured discoveries in `.planning/NOTES.md`, and architecture in `CLAUDE.md` — split by tense, not by topic, because a topical split gives every new fact two plausible homes. `make skill-check` fails a commit if the skill cites a make target that does not exist. |