gaps
Nothing degrades quietly
The design rule is that an unrepresentable construct stops the build instead of producing a handler that is subtly wrong at runtime. That is why the fail-loud count is the largest of the three buckets, and it is the intended shape, not a backlog.
the published table
What refuses, and the way through
Quoted from the escapes table in README.md: the construct, why it refuses, and the documented alternative.
- Blade <x-…> components
compile to $__env->…, only the layouts compile
escape hatch: hand-written ports/*.mjs
- Controller code outside the subset
transpiler fails the build (LW001 to LW004)
escape hatch: a ports/ handler
- Queue::push(SomeJob::class)
the job would never run at the edge, no queue handler, the binding carries JSON not a PHP job
escape hatch: CF Queues binding + a consumer you write; or a D1 table a cron reads
- named Storage::disk('x')
needs its own bucket; R2 asks for a credit card
escape hatch: bind R2 explicitly, or use the default disk
- Cache::tags()
the D1 database store has no tags
escape hatch: drop tags
- CORS config
the Access-Control-* headers would be missing
escape hatch: declare the routes in registry.json ignore
- the compiled code reads an app config group (config('needs.categories'))
the runtime config dump carries the framework keys plus the groups the app declares; without a declaration the edge would answer null where PHP answers the value, and the build refuses the config('<key>') it cannot answer
escape hatch: registry.json → {"config": ["needs"]}, the group tree lands in dist/src/config.generated.mjs; declare only public data (an app's config/ may hold credentials, a guard port reads those as bindings)
- a non-session auth.guards.* driver the app registers itself (Auth::extend)
the registering closure is PHP the rung cannot run, and answering "no user" for every request would let a guarded route through
escape hatch: registry.json → {"guards": {"<driver>": "<slug>"}} + ports/<slug>.mjs exporting default { async user(ctx) { ... } }, where ctx is { request, guard, driver, config, app: { config, db, models, env, session, cookieJar } }, the app's bindings are ctx.app.env / ctx.app.db / ctx.app.models, never ctx.env; the working recipe is on the auth row below
batch by batch
Where the refusals cluster
Each construct was proven in a numbered batch; the failures spread the way the audit went, not the way a release-note summary would.
| batch | refusals | share of all refusals |
|---|---|---|
| M49 | 44 | 69% |
| M39 | 7 | 11% |
| M40 | 3 | 5% |
| M43 | 3 | 5% |
| M45 | 2 | 3% |
| M39c | 1 | 2% |
| M60 | 1 | 2% |
| M41 | 1 | 2% |
| M42 | 1 | 2% |
| GAP | 1 | 2% |
platform walls
5 documentation pages with no compiled path
These are the pages the coverage index marks as a wall rather than a trade: the capability cannot be carried, only replaced.
- broadcastingplanned: none
SSE/polling padanan; WS via DO/external
- imagesplanned: none
no GD/Imagick → CF Images or build-time
- processesplanned: none
no process spawning → off-edge padanan
- redisplanned: none
KV/D1 drivers; Upstash REST optional
- reverbplanned: none
WS server → DO/external/SSE padanan
trades
59 pages that fit, with a trade attached
The row's own note is the trade. All 59 are listed in the coverage page; these are the first twelve.
| page | planned | the trade |
|---|---|---|
| ai-sdk | ph3 | HTTP + streaming via fetch fits |
| authentication | n/a | kernel SessionGuard oracle-verified (M14); wired M15 to M20 (auth.test 4/4, login-flow E2E) |
| authorization | ph2 | the Gate facade compiles: Gate::allows()/denies() answer from the compiled policy (the guest rules included); authorize()/any()/define()/before() fail loud |
| billing | ph3 | Cashier via fetch; webhooks = HMAC routes (padanan) |
| blade | n/a | views compile (echo/if/loops/expr); the layouts compile too (M40: the sections travel to the layout, @extends/@section/@yield); the Blade components (<x-...>) stay a port |
| cache | n/a | the Cache facade + the cache() helper compile to the D1 database store; the tags need a taggable store (LW002, the D1 store has none, Laravel throws there too); a non-database driver fails the build (it would read and write a different store) |
| cashier-paddle | ph3 | same as billing (padanan) |
| collections | n/a | kernel shim subset; fail loud per method |
| concurrency | n/a | Promise.all; 6-conn limit → batch |
| container | n/a | compile-time DI subset |
| context | n/a | request-scoped shim |
| controllers | n/a | M10/M17/M22/M23/M28: GET/POST/PUT/PATCH/DELETE actions compiled (one handler per verb); \->validate() + flash (M19); edirect()->intended() + session regen/invalidate (M20); FormRequests, Cache/Hash/Storage facades, class constants and Eloquent writes compile (M23/M28); the route-model binding compiles (M40: show(Post $post) fetches the model from the segment); pagination + Eloquent relations still a port; fail-loud LW002 (unsupported construct) / LW003; REQUEST_CALL synced to kernel (#2: date removed, str added) |