Roadmap & Open Items¶
A running list of planned work and known gaps in the framework. Shipped features live in the Changelog; this page tracks what is not yet done.
Items here are deliberately not started or intentionally deferred. Each notes why and what "done" requires. Anything touching live authentication is verified in a real application before it is merged.
Authentication & Auth Server¶
Retire the deprecated auth / session addons¶
The built-in login lifecycle, session tracking and activity logging now cover
what the legacy auth and session addons provided — except cookie-based
"remember me" re-authentication, which still relies on the UserDatabase
addon's onAuthCheck().
Planned:
- A built-in remember-me
authCheck(validate theauth/usernamecookies and re-establish the session) to replaceUserDatabase::onAuthCheck(). - Point
SessionTrackingMiddlewareat the built-in check. - Drop the deprecated addons from new scaffolds, keeping backward compatibility for applications that still register them.
Done when: remember-me login persistence is verified across new requests / browser restarts in a real application.
Email¶
Where a sent message's body is stored¶
Under design, written up separately in Mail body storage — design.
The short version: mails.path is where applications on this framework have kept gzipped bodies
for years, and this framework has never written to it or read from it. BodyStore was added
without looking at that, so there are now two conventions, BodyStore::bodyOf() returns nothing
for a row that uses the older one, and mails.hash means one thing in the schema comment and
another in Email::send().
A first attempt to reconcile them was reverted whole (03e944cc) because it was written while the
design was still being argued and carried decisions nobody had agreed to.
Done requires: finishing the survey of what installations actually have; deciding whether the archived body may stop being byte-identical to what was sent; and settling how deduplication and orphan collection work at ten million rows. The design page has the measurements, the traps found by prototyping, and four real bugs that are still open — including one where the garbage collection deletes the body of a message that was just sent.
hreflang, and a sitemap¶
Under design, written up separately in hreflang and Sitemaps — design.
Neither exists. The framework has a full language system and no way for a page to say which URL is
its equivalent in another language; there is no sitemap generator, route or <link rel="sitemap">.
robots has no home either — it is done in four places with three mechanisms and no Document
property.
They are one piece of work rather than two, because the same declaration has to serve both: if a sitemap and a page head disagree about which pages are translations of each other, Google discards the declaration. And a controller cannot be the place it is declared, because no controller runs when a sitemap is generated.
Done requires: deciding whether hreflang is delivered in the head, in the sitemap, or both
under the all-or-nothing rule for language groups; localized route groups, which make reciprocity
structural and are available because Routing\Route already compiles through Symfony; and a
sitemap generator that shards, since the protocol caps a file at 50,000 URLs and the sites this is
for have more pages than that.
The design page carries the requirements taken from an application that already does this in production across five locales, what its implementation gets wrong, and the conflict between a lean sitemap and sitemap-delivered hreflang.
Debug toolbar¶
Everything the toolbar was planned to do, it does: one renderer
(src/Pramnos/Debug/assets/debugbar.js, owned by Pramnos\Debug\DebugBarAsset)
serving both deliveries, and the same answers on a server-rendered page and in a
SPA — requests, time, SQL, route, auth, session, logs, views, the domain layer,
migrations, server exceptions, browser errors, the client's own state, and a
playground that calls the documented API for real.
Two things are deliberately not planned:
- Component state is the job of Svelte DevTools and the
$inspectrune. Correlation is ours, state is theirs. - A container-resolved timing proxy for services, which would make
Service::measure()unnecessary, waits until services are actually resolved through the container. Until then the explicit call is the honest option: a proxy that only wraps some services would report a domain layer that is partly missing, which is worse than one that is visibly opt-in.
Testing¶
The suite takes 15 minutes, and that paces every change¶
Measured, and it contradicted the standing hypothesis. Full analysis with the
numbers: Test suite performance. From a
JUnit-logged run on 2026-08-13 (1471dc9a):
./dockertest17:02 with coverage, 14:58 without — instrumentation is ~12%, real but not the lever;tests/bootstrap.phptouches no database at all, so there is no fixed setup cost to remove. The suspicion that database setup dominates does not survive contact;- 203 tests (2.2%) account for 46% of the run. The other 7646 cost eleven seconds more than those 203 do, together — which is what makes this tractable;
tests/Unitis 49% of the time at 60ms/test;tests/Integrationis 43% at 303ms/test;tests/Featureis declared and empty.
Planned, in order of return:
- Connect timeouts. Seven tests wait exactly 8.00 s each for a hostname that is
supposed not to resolve —
BaseTestCaseTestandTestEnvironmentTestassert which DSN was built, proven by the failure naming the host, then wait for TCP to give up. One line in four places, ≈49 s. InitCommandUnitTestscaffolds a whole project per test — 61 tests × 1877 ms. Scaffold once per class for the read-only assertions, keep per-test only where the test changes the project. 80–130 s.- Integration tests create their schema per test. Schema per class in
setUpBeforeClass(), data per test inside a transaction rolled back afterwards — the split that fits a database, since DDL is not transactional in MySQL. Needs one shared base class rather than fifty copies. Up to 150 s. - Two specific classes:
MediaObjectTestbuilds real JPEGs 86 times;TwoFactorAuthService*hashes backup codes at default cost. 40–80 s.
Together ≈5–6 minutes, without removing a test, a database or the coverage report.
Not to be done: dropping a database from the matrix (the query-builder bugs this
framework has shipped were dialect-specific — a ? only MySQL tolerated, a backtick only
MySQL accepts) or making coverage opt-in (12%, and --no-coverage already exists).
Parallelism is the next step and becomes cheaper once item 3 has moved schema creation
into one place, since each worker then needs its own schema.
Done when: the "≥ 1000 ms" row of the distribution has moved. A change that does not move it has not moved the suite.
Deduplicate the two Getting Started pages¶
docs/index.md and docs/Getting_Started.md are the same document — identical
headings and prose, differing only in tab-versus-space indentation inside the
code fences — yet the nav presents them as "Quickstart" and "Full Setup Guide".
This matters more now that docs/ is a retrieval corpus (rule 13): two pages
with the same content compete for the same question and neither is more
authoritative than the other.
- Decide which one survives, or split them for real — a short quickstart that stops at "it runs", and a full guide that continues into Docker, structure and scaffolding.
- Update the
nav:labels so they describe what each page actually is. - Keep whichever URL is already linked from the README and the scaffolded projects, or add a redirect.
Done when: the two pages answer different questions, and the use_cases: of
each say which.
A verification tool over the documented rules — done¶
The retrieval half of this is done: framework-docs indexes, searches and
reads docs/*.md using each page's use_cases, keeps the changelog posts as a
separate corpus so "when did this change" cannot outrank "how does this work",
and demotes pages carrying no use cases because they are not guidance. See
MCP server. Runtime scan, no build artifact to go stale.
The half that says no is now built too: pramnos-check, a seventh MCP tool. See
MCP server. It covers the rules this entry asked for:
- raw SQL where the query builder belongs (rule 12);
- unqualified
authserver.*tables, which fail silently on PostgreSQL; ?message=/?error=query params instead of flash messages, which re-show the message on reload;- view variables named
sections,path,modelor_layout, which collide with the View engine and vanish without a word; - migrations prefixed
2020_01_01_*, which installations with a cutoff skip; - a hand-rolled debug panel beside the framework's own.
Each is mechanically detectable, and prose in a guide demonstrably does not prevent them — every item on that list is something that happened after the guide describing it was written. Being able to find the rule and being told when you have broken it are different mechanisms, and the second one is the one with evidence behind it.
Each is matched as a construction rather than a name, with a negative test per rule and
a suppression mechanism whose reason is mandatory. The precision mattered more than the
coverage: the first run against the framework's own src/ produced 29 raw-SQL findings of
which sixteen were noise, and a check that cries wolf is a check that gets muted.
What this surfaced, and what is now open: the framework does not pass its own check — 9
raw-SQL findings and 67 flash-query-parameter findings under src/. Every one was reviewed
and they are real. That is the entry's own argument turned on its author: the rules were
written down, and the framework drifted from them in seventy-six places. Rewriting them is a
priority decision, not a tidy-up, so it is recorded here rather than done quietly.
Done: an assistant now gets told "no" when it breaks a documented rule, rather than only being able to look the rule up.
Have a request or found a gap? Open an issue on GitHub.
Test suite performance — closed¶
Both open items are answered in Test suite performance, with measurements:
- The remaining
DatabaseTestCaseconversions are declined. The seven convertible classes are 10.26 s of 211.9 s of measured test time, and a perfect conversion of all seven saves about 8.5 s — at or below the suite's own run-to-run spread. Three of the seven are PostgreSQL, where the conversion has been measured making things slower. There is no experiment that could show the work succeeded. paratestis now the only remaining lever, and the recommendation is to decide it on CI rather than on local wall clock. Prize: 3:45 → roughly 50 s at 8 workers. Cost: per-worker databases on three engines, permanently. For a local loop that is a poor trade; for CI it is minutes × runs × people and plausibly worth it. Step 3 — routing every connection through one helper — is worth doing first either way.
Open only if CI wall clock becomes the binding constraint.