v0.0.40
2 July 2026
Permission system, Page::new() for programmatic page creation, Field::validate(), weighted and fuzzy FTS5 search, and fieldtype search metadata.
Added
Permissionclass :Permission::forRole(string $role, App $app)loads the role'sindex.json(site overrides phlat).Permission::check(string $key)resolves dot-notation keys with*wildcard support across the permissions tree.User::can(string $key): delegates toPermission::forRole($this->role, $this->app)->check($key).Session::can(string $key): checks the logged-in user's permissions ($this->user()?->can($key) ?? false).Session::requirePermission(string $key): redirects to login if not logged in, throwsForbiddenExceptionif the user lacks the given permission.Session::popIntended(): pops the_intendedflash key written byrequireLogin(), so login handlers can redirect back to the original destination. Proxied onNamespacedSession.permission(string $key)global helper : shorthand forapp()->session->can($key).Page::new(string $name, array $data = []): creates a child page: slugifies$name, mints a UUID, writesindex.json, then callssave($data)via the normal structural/content split. ThrowsValidationExceptionfor an empty slug,ConflictExceptionif the slug is already taken.Page::has(string $slug): returns true if a child page at that slug already exists on disk.Field::validate(mixed $rawValue): runs the inner fieldtype'svalidate()then enforcesrequired,min_length, andmax_lengthfrom the field's ownoptions, returning a flat error array.App::fieldtypeMeta(string $type): loads and memoises a fieldtype'sindex.json(site overrides phlat), exposingtitle,description, andsearchdefaults for use byFieldIndex.- Registry schema 7:
search_indexFTS5 columns split intocontent_high,content_normal,content_low; BM25 weights are 3.0 / 1.0 / 0.5. A companionsearch_index_vocabFTS5 vocab table enables fuzzy expansion.fuzzy=true(orfuzzy=1) alongsidesearch=widens each token to nearby indexed terms by edit distance (max 1 for tokens under 6 chars, 2 for longer; capped at 5 candidates per token). Each fieldtype'sindex.jsonmay declaresearch.weightandsearch.enableddefaults; individual field options can override these. Str::distance(string $a, string $b): Levenshtein edit distance.
Changed
Page::save(),Field::save(),Record::save():$dataparameter defaults to[]; callers that just want to trigger the save lifecycle (e.g. bust caches) no longer need to pass an empty array explicitly.Record::identity(): throws\RuntimeExceptionon a missing uuid instead of auto-generating one. Creation call sites (Page::new(), user creation) are now responsible for minting the uuid and writing it toindex.jsonbefore the record is constructed.App::page(''): throws\InvalidArgumentExceptionimmediately; use'/'for the root page.FieldIndex::index()routes each field's text into the appropriatecontent_high/normal/lowtier and skips fields withsearch.enabled = falsefromsearch_indexentirely.phlat/Caddyfile.classicandphlat/Caddyfile.workermerged into a singlephlat/Caddyfile.