§13 was 332 lines of phase-by-phase implementation history (Phase 0
spike → Phase 5 closure). All of that is now redundant with
CHANGELOG.md (per-version summary) and `git log` (per-commit detail);
keeping it in PLAN.md was duplication that would only rot.
Replaced with a Versions section organised around SemVer:
- v0.1.0 — shipped 2026-05-03 (links to CHANGELOG + release page).
- v0.1.1 — bugfix follow-ups from v0.1.0 shakedown (perfsmoke gap,
bin/php-qml-init parity with the AppImage fixes that landed in
examples/todo, bundled-mode integration test, Caddyfile fmt).
- v0.2.0 — minor features: macOS/Windows packaging (was Phase 4b/4c),
deferred makers (event/read-model), ReactiveObject pagination,
qmltestrunner + end-to-end UI test, pre-migration auto-backup,
bridge:export, periodic auto-update check, build-time cache warmup,
native-dialogs boundary doc.
- v0.3.0 — bigger pieces: i18n bridge, persistent log files +
rotation, multi-arch builds, Sentry + opt-in telemetry, Flathub /
Snap, Composer create-project package.
- v1.0.0 — when public API stabilises: auth model, Mercure storage,
AppImage relinkability, telemetry plumbing, security audit,
FrankenPHP-as-library evaluation.
Every deferred item from the original §11/§12/Phase 3-5 deferral
lists got a version target — no orphans.
Top-of-file status line and "Where else to look" pointer added so
readers know docs/ is for how-to-use, CHANGELOG for what-shipped, and
PLAN.md keeps why + what's next.
Net: 836 → 561 lines (33% smaller). §1-12 (architectural rationale)
unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the two release-prep items called out in the Phase 5 closure
paragraph (a3d35a7).
License: LGPL-3.0-or-later. Chosen to align with Qt 6's LGPLv3, which
keeps the AppImage's relinkability obligation (PLAN.md §12) satisfied
and avoids version-mixing friction with upstream Qt. Two files at the
repo root:
- LICENSE — LGPL-3.0 text (the project license).
- LICENSE.GPL — GPL-3.0 text the LGPL-3.0 explicitly incorporates
("This version of the GNU Lesser General Public
License incorporates the terms and conditions of
version 3 of the GNU General Public License…").
framework/php/composer.json: "license": "proprietary" → SPDX
"LGPL-3.0-or-later". CHANGELOG Notes section updated with the actual
license + LICENSE/LICENSE.GPL pointer.
Repo URL: every `gitea.example/<org|you>/php-qml` (and `<org>/<repo>`
in docs/packaging-linux.md) replaced with the real
`src.bundespruefstelle.ch/magdev/php-qml`. Touched README.md,
CHANGELOG.md (compare + tag links), docs/getting-started.md,
docs/packaging-linux.md (build-appimage --update-info example +
latest.json appcast example).
PLAN.md: status line bumped to "v0.1.0 ready to tag — LGPL-3.0-or-later
license shipped, repo URL fixed". Phase 5 closure paragraph rewritten
to record both items resolved (rather than pending).
Only remaining manual edit at tag time: CHANGELOG `[0.1.0] — TBD` →
`[0.1.0] — YYYY-MM-DD` (per Keep-a-Changelog), and the actual
`git tag v0.1.0 && git push --tags` itself, which triggers
.gitea/workflows/release.yml. Per the branching memory, releases land
on main — merge dev → main first.
Verified: `make quality` from framework/skeleton green (16 tests, 45
assertions; PHPStan + cs-fixer clean; maker snapshots match).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All four planned sub-commits (4c15ac2 → 26a2b37) landed; the unplanned
docs/ rewrite (da04843) followed, lifting long-form material out of
the README into ten topic guides under docs/.
Done-criteria checked:
- bin/php-qml-init scaffolds a runnable app (sub-commit 2).
- DevConsole.qml + childLogLine signal surface child stdout/stderr
with Ctrl+` toggle in skeleton + todo example (sub-commit 1).
- README is end-to-end (60-second tour + links into docs/).
- CHANGELOG v0.1.0 entry records every Phase 0–4a + Phase 5 deliverable.
- `make quality` from framework/skeleton passes (16 tests, 45
assertions; PHPStan clean; cs-fixer clean; qmllint warnings only;
maker snapshots match).
Status line at PLAN.md head bumped to "Phases 0–5 complete; v0.1.0
ready to tag pending LICENSE selection + Gitea host URL substitution".
Two release-prep items remain — both user-driven, neither a framework
regression — captured in the new Phase-5-closure paragraph: choose +
add a LICENSE (composer.json still says "proprietary"), and replace
the `gitea.example/<org>/` placeholder URLs in CHANGELOG / README /
docs once the repo's published location is fixed. Tagging itself
remains the user's call per the release-process memory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README.md rewritten to reflect actual onboarding (clone → php-qml-init
→ make dev / make appimage) instead of the planning-stage placeholder.
Phase status checklist now reflects 0–4a green and 5 in progress.
CHANGELOG.md created at repo root following Keep-a-Changelog
conventions, with a v0.1.0 entry that summarises Phases 0–4a plus the
Phase 5 polish work (DevConsole, php-qml-init, editor configs,
hot-reload docs). Date is TBD; tagging is the user's call.
PLAN.md gains a Status banner so it's obvious at a glance which phase
the implementation tracks against the design.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5 closes out the remaining DX seams from PLAN.md §8: child-output
capture with an optional DevConsole.qml, a single-file `bin/php-qml-init`
bash script for scaffolding fresh apps (chosen over a Composer template
since the project mixes PHP and CMake/Qt — no clean Composer fit), the
hot-reload story documented and IDE configs shipped with the skeleton,
then a release-readiness pass (README, CHANGELOG.md) culminating in a
plausible v0.1.0 milestone — tagging stays user-driven per the
release-process feedback rule.
4 sub-commits. macOS / Windows packaging stays deferred to 4b / 4c.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
examples/todo/tests/perfsmoke.sh asserts the PLAN.md §11 budgets
against the built AppImage:
- Bundle size ≤ 200 MB (hard cap; ≤ 120 MB target)
- Cold start ≤ 2000 ms from launch to first /healthz 200
- Idle RSS (host + descendants in the process group) ≤ 200 MB after
a 2 s settle.
Each budget is overridable via env (PERF_COLD_START_MS etc.) for slow
shared CI runners; defaults are the strict numbers from the plan. Runs
the AppImage under xvfb-run when DISPLAY is unset; falls back to
QT_QPA_PLATFORM=offscreen otherwise (the build script already bundles
libqoffscreen.so via EXTRA_PLATFORM_PLUGINS).
Wired into:
- examples/todo/Makefile → `make perf`
- .gitea/workflows/release.yml → runs after AppImage build, before
zsync + upload, with cold-start budget bumped to 4 s for CI.
CI now also installs zsync + xvfb in one step.
examples/todo/README.md gains an "AppImage packaging (Phase 4a)"
section walking through `make appimage`, bundled-mode behaviour, the
auto-update QML hooks (BackendConnection.checkForUpdates() / applyUpdate()),
and `make perf`.
PLAN.md §13 Phase 4 marked **4a closed**. 4b (macOS) and 4c (Windows)
stay stubs until their runners + certs exist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI was failing on the Install-bundle-dependencies step because
shivammathur/setup-php was installing 8.3 while Symfony 8.x dependencies
declare php >= 8.4. Local composer install worked because the dev box
runs PHP 8.5.5; CI doesn't.
Bumps:
- framework/php/composer.json
- framework/skeleton/symfony/composer.json
- examples/todo/symfony/composer.json
- .gitea/workflows/ci.yml php-version: '8.3' → '8.4'
- .gitea/workflows/release.yml same
- PLAN.md §13 Phase 1 *Detailed scope* PHP minimum row
PHPStan / cs-fixer / PHPUnit stay green locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Honest scoping: macOS and Windows packaging carry hard operational
prerequisites (self-hosted runners, Apple Developer + Authenticode certs)
that can't be solved from a Linux dev machine. Phase 4a delivers the full
Linux pipeline now; 4b and 4c wait until those prerequisites land.
4a sub-commits:
1. Bundled-mode startup (auto-detected: no BRIDGE_URL → spawn child,
per-session secret, first-launch migrations into XDG data dir)
2. AppImage recipe (packaging/linux/build-appimage.sh + make appimage)
3. Linux release.yml on v* tags (Gitea Release + SHA256SUMS + appcast)
4. AppImageUpdate + BackendConnection.checkForUpdates()
5. Performance-smoke harness + 4a phase closure
The framework code stays platform-agnostic — only the packaging layer
is per-OS. 4b / 4c entries get filled into PLAN.md when their runners
and certs become available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a 5-step sub-commit sequence and the done-criteria for Phase 3.
Defaults baked in (subject to user override before sub-commit 1):
- Ship `make:bridge:command` and `make:bridge:window` only; defer
`make:bridge:event` and `make:bridge:read-model` since the todo
app doesn't use them. Phase 3.x can pick them up.
- Multi-window + crash-recover validated via a bridge-integration test
that boots a real FrankenPHP child + offscreen Qt host. qmltestrunner
smoke covers RestClient.qml and AppShell.qml.
- Maker-output snapshot test in CI catches silent generator drift by
diffing fresh maker runs against the checked-in baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BackendConnection's ConnectionState enum is now Connecting / Online /
Reconnecting / Offline (PLAN.md §5). The probe loop tracks the first
failure since the last Online and transitions to Reconnecting on any
failed probe, then to Offline once the configurable threshold (30 s
default) is exceeded. The Error state is gone; Reconnecting + the
exposed `error` string subsume its UI role.
ReactiveListModel is the headline QML type:
- QAbstractListModel that GETs `baseUrl + source` for an initial JSON
array and then keeps in sync via an internal MercureClient subscribed
to `topic`.
- Role names are derived dynamically from the first row's keys plus an
internal `pending` boolean role used by optimistic mutations.
- Diff application: upsert (insert-or-update), delete, replace; gap
detection via the envelope `version` field with auto re-fetch.
- `invoke(method, path, body, optimistic)` is the optimistic command
primitive. Generates an Idempotency-Key, applies the local diff,
POST/PATCH/DELETEs with that key, and resolves on the matching
Mercure echo (correlation-key matched in ModelPublisher's envelope).
Rolls back and emits commandFailed on 4xx/5xx, commandTimedOut after
10 s without an echo. Phase 4 packaging will surface configuration
for the timeout.
AppShell.qml is the optional convenience root:
- Reads BackendConnection.connectionState.
- Reconnecting → top banner.
- Offline → modal overlay with the error string and a Retry button
(calls BackendConnection.restart()).
- Wraps user content via `default property alias content`.
Apps that want full chrome control can skip AppShell entirely; the
skeleton's Main.qml keeps its own status display for demonstration
and is unaffected.
ReactiveObject (single-entity twin of ReactiveListModel) is intentionally
deferred — same envelope handling, smaller surface; will land in Phase 2
follow-up or Phase 3 alongside the todo example. Cursor pagination is
similarly deferred (the Phase 2 done criterion uses small lists).
Smoke tested: /healthz + /api/ping round-trip cleanly, zero Mercure 401s,
clean shutdown. composer quality stays green (16 tests, 45 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Makes the time-ordered, distributed-friendly UUIDv7 (via symfony/uid)
the framework's default. Auto-increment integers remain available via
an explicit --int-id flag on the maker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a stack-additions table (ORM, dev DB, default ID type, pagination
strategy, Doctrine→Mercure trigger), a 5-step sub-commit sequence, and
the done-criteria for Phase 2. Defaults baked in (subject to user
override before sub-commit 1 starts):
- Doctrine ORM 3.x with DoctrineBundle + DoctrineMigrationsBundle
- SQLite at var/data.sqlite for dev
- Auto-incrementing int IDs by default; the maker takes a UUIDv7 flag
- Cursor-based pagination, default page size 50
- Synchronous postPersist/postUpdate/postRemove subscribers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundle code for php-qml/bridge: BridgeBundle (AbstractBundle, autoloads
config/services.yaml), Publisher (thin wrapper over Mercure HubInterface
that enforces envelope-as-JSON), SessionAuthenticator (bearer-token
custom Symfony authenticator with problem+json failures), and
HealthController (GET /healthz readiness probe).
Composer constraints bumped to Symfony ^8.0 across the board (per user
request); mercure component to ^0.7. PHPUnit 11 suite covers Publisher
publish + private flag and SessionAuthenticator support/auth/failure
paths — 8 tests, 22 assertions, all green.
PLAN.md §13 updated to record the Symfony 8 minimum.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds naming/identifier table, directory layout, eight sub-commits, and
done criteria for Phase 1. Also swaps the planned Taskfile for a plain
Makefile across §8 and §13 — Make is universal and skips a dependency
that openSUSE Tumbleweed doesn't package.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the spike's layout, flow, hardcoded values, and done criteria so
implementation has a clear target. No code yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architectural design (PLAN.md) and project intro committed before any
implementation begins.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>