From e0794b7f94ccc0fbbeb83ef55fa11e15e7c7e22b Mon Sep 17 00:00:00 2001 From: agis Date: Thu, 10 Sep 2026 11:05:15 +0700 Subject: [PATCH] docs(migration): restructure roadmap to framework-first / strict-mode-L13 Change the L42x->L13 approach from "converge app + CI ratchet, then one flip" to framework-first: converge the FORK toward L13 as a "strict-mode Laravel 13" (tighten signatures so old 4.2 idioms become TypeError/method-not-found, enforced by PHP + Psalm rather than a grep ratchet), then swap each component to the real illuminate/* v13 package per dependency cluster until the fork reaches 0. Endgame unchanged: stock Laravel 13. - ROADMAP: framework-first strategy, 3-bucket model, strict-mode loop, replace-collision (1a), split-monolith prerequisite (5), phased plan. - TASKS: reordered waves (W1 split+Contracts -> W2 strict-mode tighten -> W4 swap bottom-up -> W5 fork=0); cache task 2.1 folded into 2.2 (framework-driven: fork tighten TTL param drives app conformance; no standalone app sweep). - ENFORCEMENT-MATRIX (new): per-component table (bucket / type-expressible / enforcement mechanism / fork action / app impact / swap order). Adversarial review corrections (verified vs code): cache-TTL is tighten-then-lint (stock 13 accepts untyped int -> fork strictness evaporates at swap -> needs a permanent Psalm/grep guard, not type); composer replace-collision (fork+stock are both laravel/framework) forces per-cluster swaps + editing the fork replace block. Structural facts (SCC-1/2, dependency graph, gotchas, auth dual-read) preserved. Co-Authored-By: Claude Opus 4.8 (1M context) --- MIGRATION-ENFORCEMENT-MATRIX.md | 90 ++++ MIGRATION-ROADMAP.md | 784 ++++++++++++++++++++------------ MIGRATION-TASKS.md | 473 +++++++++++++------ 3 files changed, 934 insertions(+), 413 deletions(-) create mode 100644 MIGRATION-ENFORCEMENT-MATRIX.md diff --git a/MIGRATION-ENFORCEMENT-MATRIX.md b/MIGRATION-ENFORCEMENT-MATRIX.md new file mode 100644 index 00000000..833e0602 --- /dev/null +++ b/MIGRATION-ENFORCEMENT-MATRIX.md @@ -0,0 +1,90 @@ +# L42x → Laravel 13 — Enforcement Matrix (per-komponen) + +> Satu baris per komponen. Turunan dari `.migration-verified-records.json` + re-klasifikasi +> 3-bucket (2026-09-10) + **koreksi review verified-vs-kode**. **Bucket:** 1=fork-only (nol app +> work) · 2=app-unavoidable removal · 3=value-semantics. **Type-expr?** = apakah break bisa jadi +> `TypeError`/method-not-found (PHP+Psalm) vs harus ratchet-grep / boot-guard / runbook. +> **Swap cluster/order** = urutan swap ke `illuminate/*` v13 (namespace `Illuminate\*` collide + +> fork/stock sama-sama `laravel/framework` `replace` illuminate/* → **tak bisa co-install** → +> per-cluster + **edit blok `replace` fork tiap langkah**, §1a roadmap). +> +> **Dua break yang strictness fork-nya MENGUAP saat swap (bukan self-liquidating):** +> **Cache-TTL** (stock `Repository.php:367`/`Contracts/Cache/Repository.php:29` `$ttl=null`, +> `getSeconds():903` terima bare int) → tighten-then-lint, guard permanen Psalm-rule/ratchet. +> **Console `handle()`** (stock `Command.php:289` `method_exists($this,'handle')?'handle':'__invoke'` +> → fire-only jatuh ke `__invoke` = runtime `BadMethodCall`) → fork-only temp-abstract, guard grep/boot. + +| Komponen | Bucket | Type-expr? | Enforcement mechanism | Fork strict-mode action | App impact | Swap cluster / order | Effort | +|---|:---:|:---:|---|---|---|---|---| +| **Contracts** | 1 | ✅ (by presence) | introduce verbatim; begitu komponen type-hint `Contracts\X`, consumer non-conform tertangkap PHP+Psalm | add `src/Illuminate/Contracts/*` (33 subdomain) dari stock 13; composer **PSR-only** (JANGAN `illuminate/*`). Fork tak `replace` contracts → aman | ~0 (1-file interface rename ikut Support: `PrakerjaApiResponse` `ArrayableInterface`→`Contracts\Support\Arrayable`) | **SCC-1 breaker, PERTAMA**; leaf (`realDeps=[]`); swap bareng Support/Container di SCC-1 window | low | +| **Support** | 2 | ⚠️ sebagian | rename/delete = type; **helper global `str_*`/`array_*` + `package()` = ratchet grep** | rename `MacroableTrait`→`Macroable`; delete `Support\Contracts\*Interface`; `ServiceProvider::register()` concrete, drop `package()`; trim helpers.php 59→23 | kecil: 1 file interface; MacroableTrait app=0; helper audit = unknown (ratchet) | bottom SCC-1; swap bareng Container/Http di window inti; hapus `support` dari `replace` fork | high | +| **Collections** | 1 | ❌ (copy source, superset) | **bukan tighten** → **copy SOURCE** ke tree Support fork + behavior-parity test (`random/first/groupBy/sortBy`). **JANGAN `composer require illuminate/collections`** (fork tak replace, collide `Support/Collection.php` on-disk) | copy source superset (Enumerable/LazyCollection/Arr/HigherOrderProxy) ke `Illuminate\Support\*` tree fork; namespace tetap | nol (namespace-stable; `->lists()` = concern Database) | foundation leaf; **copy source EARLY, real `illuminate/collections` swap DI SCC-1 window** (Support di-copot → tak collide) | high | +| **Macroable** | 2 | ✅ (rename) | rename fork trait → nama lama not-found (PHP autoload + Psalm UndefinedTrait) | move `MacroableTrait.php`→`Macroable.php` (`trait Macroable`, copy di tree Support); add `mixin/flushMacros`; widen `Closure`→`object\|callable`; add bindTo; rename ~4 site internal. JANGAN composer require illuminate/macroable early (fork tak replace, collide) | nol (0 app `MacroableTrait`, 0 app `::macro()`) | pure leaf; copy source FIRST bareng Conditionable; real swap dengan SCC-1 base | trivial | +| **Conditionable** | 1 | ❌ (copy source) | **nothing to tighten** → copy 2 file di namespace benar | 2 file verbatim: `Support/Traits/Conditionable.php` + `Support/HigherOrderWhenProxy.php` (JANGAN bikin dir baru; copy source, jangan composer require) | nol (app `when()/unless()` fluent = 0) | zero-dep leaf; copy source FIRST/EARLY; real swap transitif di illuminate/support (SCC-1 window) | trivial | +| **Pipeline** | 2 | ❌ (removal ada di Routing) | Pipeline sendiri copy-source; filter-removal (yang memaksa middleware) di **Routing** | copy source verbatim (Pipeline/Hub/Provider + 2 contract) ke tree fork; deps compile-time Contracts + Conditionable/Macroable. JANGAN composer require illuminate/pipeline early | indirect: tiap filter → middleware sebelum Routing swap (dihitung di Routing) | copy source EARLY (setelah Contracts+Support-traits); inert sampai Routing/Kernel/Bus/Queue reshape; real swap di SCC-1/L2 window | low | +| **Reflection** | 1 | ✅ (trivial, sudah aman) | nothing to catch — `getParameterClassName` backward-compatible fork↔v13 | replace stub 39-line dengan superset stock 13, **tetap fisik di tree Support** (`Illuminate\Support\Reflector`); bring `ReflectsClosures`. Copy source (fork tak replace reflection) | nol (`Reflector` di app = 0) | flip lockstep dengan Support/Container di SCC-1 core cutover | trivial | +| **Container** | 1 | ✅ | delete legacy method → method-not-found + Psalm UndefinedMethod; relokasi exception (alias jaga) | delete `share/isShared/bindShared/resolvingAny` (line 384/409/1234/1347); move `BindingResolutionException/CircularDependencyException` ke Contracts (subclass/alias lokal) | ~0 (0 call legacy; 11 exception ref, 10 test, alias resolve unchanged) | **SCC-1 core cutover** (DI kernel; drag Contracts+Reflection+Support satu window). Gated PHP 8.3; hapus `container` dari `replace` fork | medium | +| **Http** | 1 | ✅ | delete `FrameGuard` → class-not-found; internal contract realign gratis dari adopsi komponen | remove `FrameGuard.php`; re-point Response/JsonResponse/RedirectResponse `Support\Contracts\*`→`Contracts\Support\*` | nol (0 FrameGuard ref; app pakai surface Request/Response stabil) | swap dalam SCC-1 core (blockers session/support); butuh helper-shim survive + Guzzle/CORS/mime deps di swap; hapus `http` dari `replace` fork | medium | +| **Cache** | **3** | ⚠️ **tighten-then-lint** (BUKAN self-liquidating) | **fork tighten TTL → `DateTimeInterface\|DateInterval`** = bare int TypeError (PHP)+Psalm **untuk memaksa migrasi app**. **Stock 13 `put($k,$v,$ttl=null)` (verified `Repository.php:367`, `Contracts/Cache/Repository.php:29`, `getSeconds():903`) re-admit bare int → strictness fork MENGUAP saat swap → guard PERMANEN = Psalm-rule/ratchet-grep bare-int**. `StoreInterface`→`Contracts\Cache\Store` rename | `Repository.php` put/add/remember (88/106/124) + TaggedCache: `$minutes`→`DateTimeInterface\|DateInterval $ttl`, drop getMinutes; relokasi StoreInterface (alias); drop Encrypter dari DatabaseStore. **WAJIB commit sama: konversi fork-internal bare-int caller (verified `Session/CacheBasedSessionHandler.php:60`, `Cache/ArrayStore.php:74`, `Cache/ApcStore.php:94`) atau fork tak boot** | **task 1.1 DONE** (bare-int→Carbon); 5 custom Store → contract sebelum swap; **Psalm-rule bare-int aktif & permanen** | swap ENGINE di akhir dengan SCC-1 core; blockers support/contracts/database/filesystem/redis; encryption **bukan** blocker; hapus `cache` dari `replace` fork | high | +| **Events** | 2 | ✅ | add `dispatch()` canonical, deprecate lalu **delete** `fire()/queue()/firing()/forgetQueued()`; drop `$priority` → method-not-found/ArgumentCountError | `Dispatcher.php`: dispatch() canonical, fire() deprecated alias→delete; queue→push, forgetQueued→forgetPushed; drop `$priority` (line 62); implement L13 DispatcherContract | ~96 `fire()`→`dispatch()`; review 118 `listen()` (wildcard payload trap); string-name idiom tolerated | reshape NOW; ENGINE-swap di SCC-1 core. **`illuminate/events:^13` HARD-REQUIRE `illuminate/bus`** → Bus wajib ikut SCC-1 window (bukan skip); hapus `events` dari `replace` fork | medium | +| **Encryption** | 2 | ✅ | delete `setKey()` → method-not-found; implement Contracts; relokasi DecryptException (alias). **cipher default = config assertion / boot-guard, bukan type** | delete `setKey()`; implement `Contracts\Encryption\{Encrypter,StringEncrypter}`; relokasi Decrypt/EncryptException + `class_alias`; drop dead Symfony import; keep ctor cipher AES-256-CBC | ~0 code (0 setKey, 0 DecryptException catch; 16 file import Encrypter concrete FQCN tetap compile); config cipher = deploy prereq | **SCC-1 member**; swap early-in-cluster tepat setelah Contracts, sebelum Cookie (EncryptCookies butuh contract); hapus `encryption` dari `replace` fork | low | +| **Cookie** | 2 | ⚠️ sebagian | jar widen = additive (Psalm-safe); **Guard/Queue decorator→middleware = delete-and-rewrite** (class-shape, bukan signature); EncryptCookies contract-hint = type | widen `queued/hasQueued/unqueue/setDefaultPathAndDomain`; add `CookieValuePrefix`; rewrite `Guard.php/Queue.php` → `Middleware/EncryptCookies`+`AddQueuedCookiesToResponse` (flip-time) | ~0 code (15 site data-surface stabil; queued/getQueuedCookies app=0); runtime: logout sekali (old cookie ditolak) | **SCC-1 member**; flip-coupled ke L13 kernel pipeline; land dengan kernel/foundation bagian cluster, setelah Encryption; hapus `cookie` dari `replace` fork | medium | +| **Session** | 2 | ⚠️ sebagian | Store `implements`-swap + delete bag = type; **Middleware HttpKernel→pipeline = class-shape** (delete+rewrite) | Store `implements Contracts\Session\Session`, ekstrak bag → `SymfonySessionDecorator` (delete registerBag/getBag/...); Middleware → StartSession/AuthenticateSession; widen flash/put/get `UnitEnum\|string`; add additive method | ~0 code (402 site + helper 6 di data-API stabil; 0 Symfony-Session hint, 0 bag call) | **SCC-1 member**; middleware flip coupled ke kernel/foundation wave (sama Cookie); butuh console SessionTableCommand reshape; hapus `session` dari `replace` fork | medium | +| **Redis** | 1 | ❌ (wholesale replace + config) | **bukan type** (class diganti utuh, 0 method call app); driver resolve = **config assertion / boot-guard** | drop-in RedisManager (implements `Contracts\Redis\Factory`) + Connections/Connectors; provider bindShared→singleton + `redis.connection`; config `client` key | nol (3 `Redis::class`, 0 method call); deploy prereq: ext-redis/config | **SCC-1 member**; flip bareng Cache/Queue/Session (surface connection()/command() shared); hapus `redis` dari `replace` fork | low | +| **Filesystem** | 2 | ✅ (controlled deprecation) | relokasi `FileNotFoundException`→Contracts + `class_alias` (`@deprecated`→Psalm DeprecatedClass); widening additive = no enforcement | relokasi ke `Contracts\Filesystem\FileNotFoundException` + alias dari FQCN lama; import dari Contracts; add Macroable+Conditionable; provider bindShared→singleton. **JANGAN** introduce Storage/flysystem di sini | 11 import produksi repoint (aliasable/deferrable); `File::` 16 site superset-compatible; app-own `DicodingUtils\...FileNotFoundException` tak tersentuh | **SCC-1 member** (foundational leaf: View/Config/Cache/Migrator konsumen); flip WITH core; alias decouple app edit timing; hapus `filesystem` dari `replace` fork | low | +| **Database** | 2 (+3) | ⚠️ campuran | `lists()` delete + rename SoftDeletes/Arrayable/Scope + `pluck`→`value` = type; **`pluck`↔`value` makna-swap = NOT type-expressible** → grep/Psalm-rule + human review | Query+Eloquent Builder+Collection: remove `lists()`, rename `pluck`→`value`, add L13-semantics `pluck($col,$key)`; rename SoftDeletingTrait→SoftDeletes, ScopeInterface→Scope (no alias); Model implement Arrayable/Jsonable | **sangat tinggi**: ~173 `->lists()`→`->pluck()`; ~193 `->pluck()`(single)→`->value()`; ~19 SoftDelet*; ~2 Arrayable; ~44 model. (remember() query-cache app=0) | **SCC-1 core atomic, LAST**. **Namespace/replace-collision** yang paksa SCC-1 window (BUKAN hard-require: `illuminate/database:^13` hard-require **hanya** container/support/contracts/collections/conditionable/macroable; events/filesystem/console = SUGGEST). Klaim lama "drag pagination/http/queue/broadcasting" DIKOREKSI — bukan hard require. Hapus `database` dari `replace` fork | very-high | +| **Queue** | 1 | ✅ (removal kecil) | **string-push + `fire()` handler KEEP di 13** (bucket 1); remove closure-push branch → TypeError; drop Iron.io = config; **wire-format = runbook drain, bukan type** | remove Closure branch di `createPayload`/`createClosurePayload` (drop IlluminateQueueClosure); delete Iron.io trio; keep Sync/Sqs/Worker/Listener + fire()-dispatch string path AS-IS | ~2 closure push (`DicodingEventHandler` :44 + BoringTest); 121 push + 308 `fire()` handler KEEP; ops: **drain queue** cutover | **L2, setelah SCC-1**. **`illuminate/queue:^13` HARD-REQUIRE `illuminate/database`(SCC-1)+`illuminate/console`** → butuh SCC-1 + console dulu. newDeps bus/contracts/foundation/log/pipeline; hapus `queue` dari `replace` fork | high | +| **Console** | 2 | ⚠️ **fork-only temp-abstract** (BUKAN self-liquidating) | fork **sementara** `execute()`→`abstract handle()` = fire-only unimplemented-abstract fatal **HANYA di fork** (ratchet ~308 rewrite). **Stock 13 `Command.php:289` resolve `method_exists('handle')?'handle':'__invoke'` → fire-only jatuh ke `__invoke` missing = runtime `BadMethodCall`, BUKAN abstract/Psalm** → guard pasca-swap = **grep `function fire(` + boot-smoke**. delete Application static methods → method-not-found | `Command.php`: `execute()` panggil `abstract public function handle()` (temp; menguap saat swap); remove getArguments/getOptions reader; remove `Application::make/start/setExceptionHandler/renderException/setAutoExit`; ctor `(Container,Dispatcher,version)`; update command.stub | **LARGE**: ~308 `fire()`→`handle()` (Rector); ~87 getArguments/Options→`$signature`; 147 `Artisan::add(App::make)` → Kernel reg; 135 file app/commands; **+grep guard pasca-swap** | **L2, BUKAN L0** (koreksi review). **`illuminate/console:^13` HARD-REQUIRE `illuminate/view`** (menyeret container/events/filesystem/session/support) → **butuh SCC-1 selesai**. Namespace collide → atomik; butuh app fire→handle + Foundation Kernel; hapus `console` dari `replace` fork | medium | +| **Config** | 2 | ✅ | delete `getEnvironment()` + loader/package-cascade → method-not-found; ctor `(array)` → pass loader TypeError | `Repository.php`: delete `getEnvironment()` (346) + getLoader/setLoader/package/afterLoading/addNamespace/hasGroup; ctor `(array $items=[])`; implement `Contracts\Config\Repository`+Macroable; add typed getters | **MEDIUM**: ~48 `getEnvironment()`→`App::environment()` (45 php + 3 blade pixel/sentry/payment); read surface (config/get/array) tak tersentuh | **L2** (setelah SCC-1); namespace collide → atomik; koordinasi dengan Foundation flip (LoadConfiguration+LoadEnvironmentVariables+phpdotenv owns ctor); hapus `config` dari `replace` fork | medium | +| **Exception** | 2 | ❌ (closure/string-binding) | delete `App::error()`/bindings = method-not-found/unbound (**catches discovery**), tapi **rewrite ke reportable/renderable = grep/manual**, bukan type | destructive staging: remove `Handler::error()/pushError()` + reflection stack; remove ExceptionServiceProvider bindings (`exception`/`.plain`/`.debug`/`whoops`); introduce `Contracts\Debug\ExceptionHandler`; merge ke Foundation Handler | **LARGE** (contra record): 11 `App::error(Closure)` + `App::missing/fatal` di `app/start/error.php`; custom `ExceptionServiceProvider` bind `app['exception']` → rewrite ke reportable/renderable/render | **L2, tidak swap ke peer (none)** — **MERGE ke Foundation flip**; ride terminal cutover; hapus `exception` dari `replace` fork saat merge | medium | +| **Translation** | 1 | ✅ | delete `trans()/transChoice()` + switch interface → method-not-found/interface-not-implemented (app: 0 → self-liquidating gratis) | `Translator.php`: drop Symfony `TranslatorInterface`, implement `Contracts\Translation\Translator`; delete trans/transChoice; add Macroable+ReflectsClosures + string/array/hasForLocale; fold Interval/PluralizationRules; drop symfony/translation | nol (app pakai `Lang::get`/`__`/`choice`/`@lang` stabil; 0 transChoice/trans(domain)/Symfony-hint) | **L2** (setelah SCC-1); namespace collide → atomik; preconditions: Contracts + Collections/Macroable/Reflection/Support/Filesystem L13-shaped; hapus `translation` dari `replace` fork | medium | +| **Log** | 1 (+B2 tail) | ✅ | rename `getMonolog()`→`getLogger()`, `Writer`→`Logger`, delete `useFiles/...` → not-found; `write()` typed. **Monolog 1→3 API di caller = NOT type** → manual | rename class Writer→Logger, `getMonolog`→`getLogger`; `write($level,$message,array $context=[]):void`; remove useFiles/useDailyFiles/useErrorLog; bind `'log'` LogManager singleton; ctor `(Psr LoggerInterface, Contracts\Events\Dispatcher)` | **LOW-MED**: 335 `Log::` PSR-3 UTUH; 3 `getMonolog()` caller (`CoreLogServiceProvider` :30/:41 + test) rewrite + Monolog 1→3 | **L2** (order AFTER Config — LogManager butuh config/logging.php channels); collide → atomik; hapus `log` dari `replace` fork | medium | +| **View** | 1 (+B2 tail) | ❌ (core subset, sudah benar) | delete `Factory::of/name/alias` = method-not-found; **`Blade::extend` callback = NOT type** → grep/manual | delete `Factory::of/name/alias` (153/165/177); rename flushSections→flushState; **keep** `BladeCompiler::extend()/createMatcher()` (jangan strip pre-swap) | core: nol (60 View::make/view() stabil); tail: 2 `Blade::extend` (`DicodingServiceProvider` :180/:186) → `Blade::directive()`; ops: flush compiled views | **L2** (parallel-wide setelah SCC-1); butuh SCC-1 (support/container/filesystem/events); ViewErrorBag butuh Session; **Console swap depend on View** (console hard-require view); hapus `view` dari `replace` fork | high | +| **Workbench** | 1 | ❌ (delete) | **nothing to tighten** → DELETION (bukan type, bukan grep) | delete `src/Illuminate/Workbench` + hapus `illuminate/workbench` dari `replace` fork; drop /workbench paths di AutoloadCommand/AssetPublishCommand saat Foundation reshape | nol (0 app call) | **no swap — evaporate**; no cluster constraint; hapus opportunistik saat Foundation reshape | trivial | +| **Routing** | 2 | ✅ (method removal) | delete `filter/before/after/when/whenRegex/callFilter/controller` + drop HttpKernelInterface → method-not-found. **route-array `'before'=>` = NOT type** → grep/Psalm-rule | staged: (1) introduce `Http\Kernel`+middleware dispatch DULU; (2) delete filter engine + RouteFiltererInterface + controller/controllers, stop implement HttpKernelInterface; keep UrlGenerator/Redirector | **LARGE**: ~52 `Route::filter` (141-line filters.php) → middleware; 131 route-array `'before'/'after'=>` → `->middleware()`; app 0 controller/model/bind | **L2 very-high**. **`illuminate/routing:^13` HARD-REQUIRE `illuminate/session`(SCC-1)** → butuh SCC-1; compose-deps contracts/collections/conditionable/macroable/pipeline/reflection; swap LATE dalam L2; hapus `routing` dari `replace` fork | very-high | +| **Validation** | 1 | ✅ (contract identity) | Validator `implements Contracts\Validation\Validator` (drop old `Support\Contracts\MessageProviderInterface`) → old-hint gagal match (app: 0 → self-liquidating) | migrate implemented interface ke `Contracts\Validation\Validator`; provider bindShared→singleton; add additive validate/safe/validated/stopOnFirstFailure. JANGAN split 44 validateXxx incremental | nol (18 `Validator::make` stabil; 0 custom rule; 0 old-contract hint; ~200 `->validate()` sudah align) | **L2** (parallel-wide setelah SCC-1); butuh SCC-1 + database + deps contracts/translation/http + egulias/brick/mime; Translation & Http L13-shaped dulu; hapus `validation` dari `replace` fork | high | +| **Hashing** | 2 | ✅ | `HasherInterface` extend `Contracts\Hashing\Hasher`, migrate hint, lalu **delete** → hint/`make(HasherInterface)` unresolvable (PHP+Psalm) | rebind `'hash'` ke HashManager (Support\Manager, DeferrableProvider); `HasherInterface` extend `Contracts\Hashing\Hasher` (bridge); at swap delete BcryptHasher/HasherInterface; drop ircmaxell/password-compat | **SMALL**: 4 site (`ApplicationGateway` :18/:26, `UserPasswordUpdateSpecification` :65, 2 test) → `Contracts\Hashing\Hasher`; 3 HashServiceProvider reg survive; `Hash::`/`bcrypt()` 32 site utuh | **L2 low** (critical path SCC-1→console→hashing→foundation); butuh support+contracts L13-shaped; unit swap; hapus `hashing` dari `replace` fork | low | +| **Foundation** | 2 | ❌ (positive bootstrap) | delete legacy method (`error/missing/fatal/down/middleware`, drop HttpKernel/Terminable) = method-not-found. **reshape ke bootstrap/app.php builder = grep/boot-smoke**, bukan type | strip Application ke shape L13 (remove error/missing/fatal/pushError/down/before/after/finish/middleware; stop implement Symfony HttpKernel/Terminable/ResponsePreparer); remove Router::filter/before/after. Do LAST (container root) | **LARGE**: App::error 11, missing 1, fatal 1, down 1, Route::filter 52, before/after 306; bootstrap files (start/global/error/local/artisan) → bootstrap/app.php + Http/Console Kernel + withExceptions/withMiddleware; Testing base → semua feature test | **LAST**; SCC-2 member; butuh SEMUA blocker real-v13; putus SCC-2 dulu (Mail via contract, Auth via Contracts); terminal cutover = the flip; hapus `foundation` dari `replace` fork | very-high | +| **Mail** | 2 | ✅ | `send():?SentMessage` (void-caller break Psalm); delete `failures()`/closure-queue → method-not-found; `queue()` terima Mailable → closure = TypeError. **transport config = boot/grep** | introduce Mailable/PendingMail/SendQueuedMailable; retype `send():?SentMessage`, `queue()/later()` first param Mailable; delete failures()/SerializableClosure `mailer@handleQueuedMessage`; split ke MailManager | **SMALL**: Mail::send 13, Mail::queue 4 → Mailable class (~17); 3 isPretending/failures drop; mail.php→mailers.* | **SCC-2, least entangled**; putus cycle (MailServiceProvider Foundation-hint→contract); butuh view/queue/events/container/support v13 + bus/config/contracts/http/testing; swap slightly ahead Foundation; hapus `mail` dari `replace` fork | high | +| **Auth** | 1 | ✅ (self-liquidating gratis) | drop `attempt()` 3rd arg / relokasi UserInterface→Contracts / AuthManager implements Factory → semua break tapi **app trigger 0** (self-liquidating). **recaller format = runtime data, bukan type** → dual-read (§8) | split Guard→SessionGuard/TokenGuard behind Contracts; AuthManager implements `Contracts\Auth\Factory`; move UserInterface/UserProviderInterface ke Contracts; rename Reminders→Passwords; drop attempt 3rd arg | ~0 (853 file di facade stabil; 0 call-site setiap removed API); runtime: remember-me invalidated (dual-read §8); password_resets table jika dipakai | **SCC-2 member**; putus cycle (Auth/Reminders→Mail concrete → Mailer contract); **`illuminate/auth:^13` HARD-REQUIRE `illuminate/queue`(L2)** + session/cookie/events/hashing/database/support v13; moves at/just before Foundation flip; hapus `auth` dari `replace` fork | high | +| **Bus** | 1 | ❌ (introduce, TRANSITIF non-opsional) | **nothing to tighten** (component absent, 0 app ref) → **BUKAN YAGNI-skip** (koreksi review): `illuminate/events:^13` **HARD-REQUIRE `illuminate/bus`** → wajib terpasang saat Events swap | **copy L13 Bus dir verbatim** (sudah strict) — **WAJIB** hadir di SCC-1 window (bukan opsional) | nol (app pakai `DomainTransactionCommandBus` sendiri; 0 `Illuminate\Bus` call-site) | **transitif SCC-1** (via Events hard-require); introduce bareng Events swap; l13Deps contracts/support/queue/container/database/foundation/pipeline | medium | +| **Broadcasting** | 1 | ❌ (introduce / YAGNI) | **nothing to tighten** (absent, 0 app ref) | **NONE — leave out**. Add verbatim hanya saat event ShouldBroadcast pertama muncul | nol (0 call-site) | not SCC; LAST/never; l13Deps bus/container/contracts/queue/redis/routing/support/http/foundation; gated Queue/SQS; pure sink | low; 0 jika skip | +| **Concurrency** | 1 | ❌ (introduce / YAGNI) | **nothing to tighten** (absent, 0 app ref) | **NONE — do not introduce**. Butuh Process+Support/defer+Console+Contracts dulu (belum ada) | nol | absolute LAST/never; leaf consumer Process; swap hanya jika kebutuhan concurrent nyata | low; 0 jika skip | +| **Image** | 1 | ❌ (introduce / YAGNI) | **nothing to tighten** (absent, 0 app ref) | **NONE — do not add**. `composer require illuminate/image intervention/image` + DeferrableProvider jika perlu | nol (0 Intervention/ImageManager) | not SCC; opt-in leaf; blockers container/contracts/filesystem/http/support/macroable/conditionable v13; introduce at-will | low; 0 jika skip | +| **JsonSchema** | 1 | ❌ (introduce / YAGNI) | **nothing to tighten** (absent, 0 app ref) | **NONE**. Lands transitif sebagai dep illuminate/foundation | nol | zero blocker (only contracts); rides transitif dengan Foundation; sink, order-independent | trivial | +| **Notifications** | 1 | ❌ (introduce / YAGNI) | **nothing to tighten** (absent, 0 Illuminate\Notifications ref) | **NONE — do not port** (app pakai NotificationV2 sendiri). Jika perlu: `illuminate/notifications:^13` + provider | nol | not SCC; LATE-stage sink, heaviest fan-out; l13Deps broadcasting/bus/config/console/container/contracts/database/mail/queue/support all v13 dulu; skip default | medium on paper, LOW real; 0 jika skip | +| **Process** | 1 | ❌ (introduce / YAGNI) | **nothing to tighten** (absent; 3 app site pakai native exec/Symfony Process, tak lewat fork) | **NONE — native exec keeps working**. Add illuminate/process hanya jika fakeable/pool/pipe dibutuhkan | nol (3 native exec site opsional migrate; tak break di swap) | not SCC; self-contained leaf; blockers support/collections/conditionable/macroable/contracts + symfony/process v13; must precede Concurrency | low; 0 jika skip | +| **Testing** | 2 | ✅ (removal di Foundation) | **enforcement di FOUNDATION**: delete legacy `assertResponseOk/$this->call/crawler` dari `Foundation\Testing` → method-not-found (PHP+Psalm), ratcheting ~500 file | 2-phase: (1) introduce `Illuminate\Testing\TestResponse` (75 assert) + `Fluent\AssertableJson` + L13 Foundation TestCase; (2) delete legacy assertion surface dari Foundation\Testing. JANGAN tighten package baru (sudah strict) | **LARGE**: ~500 file test (assertResponseOk/assertResponseStatus/assertRedirectedTo/$this->call/crawler) → fluent `$response->assert*` | not SCC; test-only sink; l13Deps console/contracts/cookie/database/foundation/http/support/view v13 dulu; swap near END alongside Foundation; PHPUnit 11/12 | medium | + +--- + +## Legenda swap-cluster + +- **SCC-1 core cutover** (satu window, replace+namespace collide): `contracts, reflection, support, + container` (jangkar) + `http, session, cache, cookie, encryption, events, filesystem, redis, + database` + **`bus`** (transitif hard-require `events`). Contracts/collections/macroable/ + conditionable/reflection/pipeline (fork tak `replace` → copy-source dulu) swap ke real package + **di sini** (Support di-copot → tak collide). +- **SCC-2 terminal (the flip):** `foundation, mail, auth` — putus cycle dulu (Mailer contract + Auth + Contracts). `auth` hard-require `queue` (L2). +- **L2 per-unit (setelah SCC-1; namespace collide → atomik):** `console` (hard-require view!), + `config, translation, log, view, routing` (hard-require session!), `validation, hashing, queue` + (hard-require database+console!). +- **L3 terminal (NOT SCC):** `cachedrouting, html, pagination`. +- **Introduce/YAGNI (skip default):** `broadcasting, concurrency, image, jsonschema, notifications, + process`. **`bus` BUKAN di sini** — transitif non-opsional Events. + +## Legenda enforcement + +- **✅ type-expr:** break jadi `TypeError`/method-not-found/UndefinedMethod → tertangkap PHP runtime + Psalm statis (di CI app). **Self-liquidating** (removal yang tak balik di stock 13). +- **⚠️ sebagian / tighten-then-lint / temp-abstract:** sebagian item type-expressible; **Cache-TTL** & **Console handle()** = fork-tighten memaksa migrasi app tapi **strictness MENGUAP saat swap** (stock 13 re-admit bare int / resolve `__invoke`) → butuh **guard non-type PERMANEN** (Psalm-rule/ratchet untuk Cache, grep/boot-smoke untuk Console). +- **❌ bukan type:** introduce (nothing to tighten), atau break = closure/string-binding/config/wire-format/API-preserved → **ratchet grep / boot-guard / runbook / parity-test** (§7D roadmap). + +## Legenda composer replace-collision (§1a roadmap) + +- Fork & stock **sama-sama `laravel/framework`** + keduanya `replace` `illuminate/*` (fork 27, + stock 37) → **tak bisa co-install**. Swap per-cluster = **hapus entry `replace` fork tiap + langkah** lalu `composer require illuminate/:^13`. Interim = fork + real `illuminate/`. +- 6 package fork **tak** `replace` (contracts/collections/macroable/conditionable/reflection/ + pipeline) tapi collide via on-disk Support source → **copy source dulu, real swap di SCC-1 window**. +- **Fase-5 gate:** blok `replace` fork **= 0 entry** sebelum `composer require laravel/framework:^13`. \ No newline at end of file diff --git a/MIGRATION-ROADMAP.md b/MIGRATION-ROADMAP.md index 1e699030..656b1f93 100644 --- a/MIGRATION-ROADMAP.md +++ b/MIGRATION-ROADMAP.md @@ -1,57 +1,103 @@ # L42x → Laravel 13 — Migration Roadmap & Dependency Graph -> Status: living document. Tujuan akhir = **Laravel 13 stock (unforked)**, bukan fork baru. +> Status: living document. **Endgame TIDAK berubah** = aplikasi jalan di **Laravel 13 stock +> (unforked)**, fork dihapus. **Approach BERUBAH (2026-09-10, Agis)** dari "gradual convergence + +> CI ratchet" menjadi **FRAMEWORK-FIRST + STRICT-MODE-L13 + ENFORCEMENT-VIA-TYPE**. > Basis data: graph dependency **nyata** hasil ekstraksi `use`-statement dari -> `src/Illuminate/*` (L42x = `laravel/framework` 4.2.72) dan -> `../framework/src/Illuminate/*` (Laravel 13.30.1), Sept 2026. +> `src/Illuminate/*` (fork = `laravel/framework` 4.2.90) dan +> `../framework/src/Illuminate/*` (Laravel 13.30.1), plus 42 record verified di +> `.migration-verified-records.json` + re-klasifikasi 3-bucket (Sept 2026). +> +> **Koreksi review (2026-09-10, verified vs kode):** (1) Cache-TTL **BUKAN** type-expressible di +> stock 13 (`put($key,$value,$ttl=null)` untyped di `Cache/Repository.php:367` **dan** +> `Contracts/Cache/Repository.php:29`; `getSeconds()` `:903` terima bare int) → di-reklasifikasi +> **tighten-then-lint**, bukan self-liquidating type. (2) Console `fire()→handle()` **BUKAN** +> abstract/Psalm-catch di stock 13 (`Command.php:289` `method_exists($this,'handle')?'handle':'__invoke'` +> → fire()-only jatuh ke `__invoke` missing = runtime `BadMethodCall`) → di-reklasifikasi +> **fork-only temporary-abstract ratchet + post-swap grep/boot-smoke**. (3) **Composer replace +> collision**: fork **dan** stock keduanya `laravel/framework`; fork `replace` 27 `illuminate/*`, +> stock `replace` 37 → dua `laravel/framework` **tak bisa co-install**; swap-per-cluster butuh +> **edit blok `replace` fork** tiap langkah. (4) **Swap-order** di-re-derive dari **hard-require** +> v13 (bukan suggest). (5) **Collections early** hanya **copy SOURCE ke tree Support fork**, bukan +> `composer require illuminate/collections` (fork masih punya `Support/Collection.php` on-disk → collide). --- ## 0. Ringkasan eksekutif -**Konteks.** L42x adalah fork Laravel **4.2.72** yang dijaga hidup di PHP 8.3/8.5. -Upstream 4.2 sudah EOL bertahun-tahun → **tidak ada patch security/fitur dari vendor**. -Tim kecil → tidak sanggup jadi security-maintainer core Illuminate selamanya, tidak -sanggup big-bang migrasi, tidak bisa jalankan dua app paralel. App code jauh lebih besar -dari framework-nya. - -**Strategi.** Bukan "migrasi bertahap" (mustahil in-process: core 4.2 & 13 tak bisa -seboot). Yang bertahap = **membuat *flip* akhir jadi murah**: - -1. Setiap perubahan kode digeser mendekat ke idiom L13 (konvergensi oportunistik, - di-*enforce* ratchet CI — nol izin management). -2. Komponen "leaf" & yang native-di-13 dibereskan/diuapkan lebih dulu. -3. Logika bisnis ditarik keluar dari framework (app-space) → selamat dari migrasi. -4. Suatu hari: **satu flip** menukar mesin core 4.2 → Laravel 13 stock. Karena app - sudah bicara idiom L13, flip = beberapa minggu, bukan 18 bulan. +**Konteks.** Fork `dicoding-dev/L42x` = `laravel/framework` **4.2.90** monolitik yang dijaga +hidup di PHP 8.3/8.5. Upstream 4.2 sudah EOL bertahun-tahun → **tidak ada patch security/fitur +dari vendor**. Tim kecil → tidak sanggup jadi security-maintainer core Illuminate selamanya. +Endgame tetap: **hapus fork, jalan di stock Laravel 13** (`composer require laravel/framework:^13`). + +**Approach baru — FRAMEWORK-FIRST.** Alih-alih menggeser app dulu lalu "the flip", kita +**konvergenkan FORK-nya** menuju L13, lalu **swap tiap komponen ke package `illuminate/*` v13 +asli, satu dependency-cluster demi cluster, sampai fork = 0**. Fork diubah jadi +**"STRICT-MODE Laravel 13"**: signature publik-nya diperketat agar **sama-atau-lebih-ketat** +dari stock 13, sehingga idiom 4.2 lama menjadi `TypeError` / method-not-found — tertangkap PHP +saat runtime DAN Psalm secara statis (Psalm sudah jalan di CI app). + +**Enforcement VIA TYPE — tapi tidak semua break bisa jadi type.** Aturan hidup di **kontrak +framework** (single source of truth) bila break-nya type-expressible. **Verified koreksi:** ada +dua kelas break yang **tidak** self-liquidating lewat type: +- **Tighten-then-lint** (Cache-TTL): stock 13 lebih **longgar** dari yang kita butuhkan + (`$ttl=null`, terima bare int). Fork yang diperketat ke `DateTimeInterface|DateInterval` + **lebih ketat dari stock** → saat swap, package v13 **kembali menerima bare int** → bug + 60× senyap **kembali tak-terjaga**. Maka: fork-tighten hanya **memaksa migrasi app**, tapi + **guard permanen pasca-swap = Psalm-rule / ratchet-grep**, bukan type. +- **Fork-only temporary-abstract** (Console `handle()`): stock 13 me-resolve `handle`-or-`__invoke` + di runtime (`Command.php:289`), jadi `fire()`-only bukan error abstract/Psalm melainkan runtime + miss. Fork bikin `handle()` abstract **sementara** untuk me-ratchet ~308 rewrite; post-swap + deteksi = **grep + boot-smoke**. + +Ratchet CI **didemote tapi TIDAK dihapus**: ia adalah tempat berlabuh permanen untuk (a) pola yang +tak bisa jadi type sama sekali (SQL string-concat, missing global helper `str_*`/`array_*`, +route-array `'before'=>`, config assertion, wire-format), **dan** (b) **guard pasca-swap** untuk +tighten-then-lint (Cache-TTL) dan temporary-abstract (Console) yang strictness fork-nya menguap +saat swap. + +**Tiga bucket per perubahan** (klasifikasi utama tiap komponen — lihat §6): + +| Bucket | Definisi | Enforcement | Kerja app | +|---|---|---|---| +| **1 — FORK-ONLY** | idiom yang **juga ada** di stock 13 (Contracts, `dispatch()`, pagination `currentPage()`, Macroable) | fix di dalam fork; app tak tersentuh | **nol** | +| **2 — APP-UNAVOIDABLE** | API **dihapus** di stock 13 (`fire()`, `->lists()`, `getEnvironment()`, route filters, `illuminate/html`) | perketat/hapus signature di fork → app WAJIB adopsi idiom baru **sebelum** swap (shim mati saat swap). *Enforcement type bila removal; grep bila hanya reshape.* | ya, sebelum swap | +| **3 — VALUE-SEMANTICS** | signature **sama** tapi arti beda; framework tak bisa membedakan (Cache TTL menit→detik) | fork perketat param **sementara** (interval-only) → paksa migrasi app; **guard permanen = Psalm-rule/ratchet-grep** (stock 13 re-admit bare int → tidak self-liquidating) | ya, sudah/harus pass interval | -**Anti-goal.** Endgame **BUKAN** "L130x" (Laravel 13 yang di-fork). Itu cuma me-reset -jam kiamat satu versi ke atas. Semua kustomisasi diarahkan ke **extension point** -(`Auth::extend`, custom driver, macro, service provider), **core Laravel haram -di-patch** — kalau tidak, jebakan fork terulang. +**Anti-goal.** Endgame **BUKAN** "L130x" (Laravel 13 yang di-fork). "Strict-mode L13" hanya +**perancah** untuk memindahkan app ke idiom L13 sebelum swap — begitu semua komponen di-swap ke +`illuminate/*` v13 asli, fork = 0 dan tidak ada lagi yang di-maintain. Semua kustomisasi → +**extension point** (`Auth::extend`, custom driver, macro, service provider). **Core Laravel +haram di-patch** — kalau tidak, jebakan fork terulang. **Angka kepala:** | Metrik | Nilai | |---|---| -| Komponen L42x (existing) | 28 | -| Komponen L13 | 38 | -| Komponen fondasi baru L13 yang wajib diperkenalkan dulu | 7 (Contracts, Collections, Macroable, Conditionable, Reflection, Pipeline, Bus) | -| **Cyclic clusters (SCC) di core 4.2** | **2** — satu 11-node, satu 3-node | -| Layer topologis | 4 (0–3) | -| Panjang critical path | 4 | -| Sudah "done" (di primitif terawat) | 2 (Encryption→OpenSSL, Hashing→`password_*`) | -| "Evaporate" (native di L13, hapus saat flip) | ≥4 (CachedRouting, Workbench, Exception, sebagian Queue/SQS) | +| Komponen di-verify (record) | 42 | +| Bucket 1 (fork-only, nol app work) | Contracts, Collections, Conditionable, Reflection, Http, Redis, Log, View, Translation, Queue, Validation, Auth, Bus, Broadcasting, Concurrency, Image, JsonSchema, Notifications, Process, Container, Workbench | +| Bucket 2 (app-unavoidable removal) | Support, Macroable, Pipeline, Events, Encryption, Cookie, Session, Filesystem, Database, Console, Config, Exception, Foundation, Mail, CachedRouting, Html, Pagination, Routing, Hashing, Testing | +| Bucket 3 (value-semantics) | Cache (TTL menit→detik) — **tighten-then-lint, bukan self-liquidating** | +| **Cyclic clusters (SCC) di core fork** | **2** — satu 11-node (SCC-1), satu 3-node (SCC-2) | +| Sudah "done"/kerja app selesai | Cache task 1.1 (bare-int TTL → Carbon interval) — DONE, tetap valid | +| Fork `replace` illuminate/* (verified) | **27** (auth, cache, config, console, container, cookie, database, encryption, events, exception, filesystem, foundation, hashing, http, html, log, mail, pagination, queue, redis, routing, session, support, translation, validation, view, workbench) | +| Stock `replace` illuminate/* (verified) | **37** (superset; incl. contracts, collections, macroable, conditionable, reflection, pipeline, bus, testing, broadcasting, concurrency, image, json-schema, notifications, process) | + +> Catatan: banyak komponen "campuran" — bucket ditetapkan berdasarkan **item load-bearing**-nya +> (mis. Database mostly flip-only tapi diklasifikasikan bucket 2 karena `->lists()` removal + +> pluck-swap yang tak-terhindarkan). Detail per-komponen & rasional bucket ada di +> `MIGRATION-ENFORCEMENT-MATRIX.md`. --- -## 1. Temuan struktural utama — DUA CYCLE +## 1. Temuan struktural utama — DUA CYCLE (tervalidasi, JANGAN dilemahkan) -Graph `use`-statement mengungkap bahwa core 4.2 **bukan hierarki bersih** melainkan -mengandung dua **strongly-connected component (SCC)** — kumpulan komponen yang saling -bergantung melingkar. **Anggota satu SCC tidak bisa diurutkan/dimigrasi satu per satu -secara terisolasi** — mereka harus bergerak sebagai klaster, ATAU cycle-nya diputus dulu -dengan memperkenalkan interface (itulah fungsi komponen `Contracts` di L13). +Graph `use`-statement mengungkap core fork **bukan hierarki bersih** melainkan mengandung dua +**strongly-connected component (SCC)**. **Anggota satu SCC tidak bisa di-swap satu per satu**; +mereka bergerak sebagai klaster ATAU cycle-nya diputus dulu via interface (`Contracts`). +Di frame framework-first ini SCC memaksa **swap-per-cluster** — DAN diperkuat oleh **composer +replace-collision** (§1a): namespace `Illuminate\*` bertabrakan + kedua package bernama +`laravel/framework` → **tak bisa co-exist di classpath** → satu SCC harus di-swap dalam satu window. ### SCC-1 — "the core ball of mud" (11 komponen) ``` @@ -65,10 +111,14 @@ cookie, encryption, events, filesystem, redis Facade menyebut kelas **konkret** lintas framework → `support → http → session → cache → database → container → support`. Seluruh stack request/data terjerat jadi satu simpul. -**Cara L13 memutusnya:** komponen `Contracts` (interface) + Facade yang resolve lewat -container/kontrak, bukan import konkret. **Maka langkah pertama konvergensi = perkenalkan -`Contracts` dan alihkan Facade/import konkret ke interface** — ini yang memecah SCC-1 -sehingga komponen-komponennya bisa dikerjakan terpisah. +**Cara memutusnya = PREREQUISITE #1:** perkenalkan package `Contracts` (interface) + alihkan +Facade/binding dari import konkret ke interface. **Ini langkah PERTAMA** (Prasyarat, §5) yang +memecah SCC-1. Setelah putus, "SCC-1 core cutover" adalah **satu swap cluster low-risk** — +low-risk justru karena saat itu fork sudah berperilaku seperti 13. + +> ⚠️ **Gotcha graph (verified):** naive dep-graph melaporkan **false cycle** dari 5 cross-import +> type-hint-only di `Contracts` (database/http/image/support/validation). **JANGAN** promosikan +> jadi `composer require` — kalau dilakukan, setiap foundation package deadlock. ### SCC-2 — "application bootstrap knot" (3 komponen) ``` @@ -79,293 +129,368 @@ foundation, mail, auth - `Mail/MailServiceProvider.php` → `use Illuminate\Foundation\Application` (konkret) - `Foundation` → bootstrap `Auth` -`foundation → auth → mail → foundation`. Diputus di L13 via kontrak Mailer + service -provider yang decoupled. Praktisnya klaster ini bagian dari **the flip** (core aplikasi). +`foundation → auth → mail → foundation`. Diputus dengan **re-point Mail lewat Mailer contract** +dan **Auth lewat Contracts** (agar Foundation tak lagi import Mail/Auth konkret). Praktisnya +klaster ini = **terminal cutover** (the flip aplikasi). + +### 1a. Composer replace-collision — lebih kuat dari namespace-collision (VERIFIED, koreksi review) + +Bukan hanya namespace `Illuminate\*` yang bertabrakan. **Fork dan stock keduanya bernama +`laravel/framework`** (verified `composer.json`), dan keduanya punya blok `replace`: + +- **Fork** `replace` **27** `illuminate/*` (incl. `illuminate/html`, `illuminate/workbench`; + **TIDAK** ada `contracts`, `collections`, `macroable`, `conditionable`, `reflection`, `pipeline`, + `bus`, `testing`). +- **Stock** `replace` **37** `illuminate/*` (superset — semua di atas ada). + +`replace` berarti fork **mengklaim menyediakan** `illuminate/cache` dst. Konsekuensi Composer: + +1. **Fork + real `illuminate/cache:^13` tak bisa co-install** — resolver menolak karena fork + sudah `replace` `illuminate/cache`. Ini konflik **lebih kuat** dari sekadar class-collision. +2. **Dua `laravel/framework` tak bisa co-install** sama sekali (nama package identik). + +**Mekanik swap-per-cluster (konsekuensi):** +- Interim state = **fork (`laravel/framework` fork) + `illuminate/:^13`** yang di-`require` + **individual**. Untuk tiap cluster yang di-swap, **hapus entry-nya dari blok `replace` fork** + lalu `composer require illuminate/:^13`. Selama entry masih di `replace`, real package ditolak. +- Untuk paket yang fork **tak** `replace` tapi stock `replace` (contracts/collections/macroable/ + conditionable/reflection/pipeline): **tak bisa** `composer require illuminate/:^13` selama + fork masih on-disk menyediakan kelas namespace itu (mis. `Support/Collection.php`) → introduce = + **copy SOURCE ke tree fork**, real swap menyusul di window Support (§4/§5). +- **Fase 5 gate:** `composer require laravel/framework:^13` **hanya** setelah blok `replace` fork + **kosong** (0 entry tersisa) — kalau tidak, resolver deadlock antara dua `laravel/framework`. --- -## 2. Dependency graph (bottom-up, per layer) +## 2. Dependency graph (bottom-up, per layer) — swap **bottom-up per cluster** + +Panah = "butuh ini dulu (L13-shaped/swapped)". Kerja **bottom-up**. `[…]` = SCC (swap satu +window; replace-collision + namespace-collision memaksa cluster-swap). -Panah = "butuh ini dulu". Kerja **bottom-up**. `[…]` = cyclic cluster (bergerak bareng -atau putus cycle dulu). `*` = sudah `done`. +> **Koreksi review — layer di-re-derive dari HARD-REQUIRE v13 (bukan suggest):** +> - **Console BUKAN L0.** `illuminate/console:^13` **hard-require** `illuminate/view` (menyeret +> container/events/filesystem/session/support) → **tak bisa mendahului SCC-1**. Console pindah ke L2. +> - **Bus BUKAN opsional.** `illuminate/events:^13` (anggota SCC-1) **hard-require** `illuminate/bus` +> → Bus adalah **transitif non-opsional** dari Events → **diperkenalkan bersama SCC-1**, bukan SKIP. +> - **Semua L2 gated ke SELURUH cluster SCC-1.** `queue`→require `database`(SCC-1)+`console`; +> `auth`→require `queue`; `routing`→require `session`(SCC-1). "L2-parallel" hanya berlaku **setelah +> SCC-1 selesai**; tak ada L2 yang boleh mendahului SCC-1. ``` -L0 console ← tak bergantung apa pun (paling dasar) - │ -L1 ┌─────────────────── CYCLE / SCC-1 ───────────────────┐ - │ support ⇄ container ⇄ http ⇄ session ⇄ cache ⇄ │ ← 11-node ball of mud; - │ database ⇄ cookie ⇄ encryption* ⇄ events ⇄ │ putus dulu via Contracts - │ filesystem ⇄ redis │ - └──────────────────────────────────────────────────────┘ +PREREQ split monolith fork → potongan illuminate/*-shaped + + edit-ready blok `replace` (§1a) untuk dibuka per-cluster + + INTRODUCE Contracts (leaf, realDeps=[]) ← MEMUTUS SCC-1, membuka semua + + leaf traits (COPY SOURCE ke tree Support fork, BUKAN composer require): + Macroable, Conditionable, Reflection, Collections, Pipeline │ -L2 hashing* config exception translation log view ← 10 komponen INDEPENDEN - workbench routing validation queue (paralel lebar) +L1 ┌─────────────────── CYCLE / SCC-1 core cutover ───────────────────┐ + │ support ⇄ container ⇄ http ⇄ session ⇄ cache ⇄ │ ← 11-node ball of mud; + │ database ⇄ cookie ⇄ encryption ⇄ events ⇄ │ swap SATU cluster + │ filesystem ⇄ redis (Contracts+Reflection+BUS ikut jangkar) │ setelah Contracts putus + └───────────────────────────────────────────────────────────────────┘ (Bus = hard-require Events) │ -L3 ┌── SCC-2 ──┐ - │ foundation │ cachedrouting html pagination ← terminal - │ ⇄ mail │ - │ ⇄ auth │ - └────────────┘ +L2 console hashing config exception translation log view ← SEMUA butuh SCC-1 selesai; + workbench routing validation queue tiap swap per-unit + │ (console→view; queue→database+console; routing→session; (replace+namespace collide) + │ auth→queue — semua hard-require, dipenuhi oleh SCC-1) +L3 ┌── SCC-2 (terminal / the flip) ──┐ + │ foundation ⇄ mail ⇄ auth │ cachedrouting html pagination + └──────────────────────────────────┘ (auth juga hard-require queue → L2 dulu) ``` **Critical path (batas bawah durasi):** ``` -console → [SCC-1 core cluster] → hashing → [foundation+mail+auth] +[SCC-1 core cluster (+Bus)] → console → hashing → [foundation+mail+auth] ``` -Panjang 4. Semua kerja lain bisa disembunyikan paralel di balik rantai ini. +Panjang 4. Kerja lain paralel di balik rantai ini. **Catatan:** console pindah dari kepala rantai +(dulu keliru di L0) ke setelah SCC-1 karena hard-require `view`. + +### Klaster yang WAJIB bergerak bareng (karena replace/namespace collision / SCC / hard-require) +- **SCC-1 core:** `contracts + reflection + support + container` jadi jangkar; `http, session, + cache, cookie, encryption, events, filesystem, redis, database` swap dalam window yang sama. + **`bus` ikut** (hard-require `events`). Tiap entry di-`require` individual setelah dihapus dari + blok `replace` fork (§1a). +- **SCC-2 (terminal):** `foundation + mail + auth` — putus cycle dulu (Mailer contract + Auth + Contracts), lalu swap sebagai the flip. `auth` juga hard-require `queue` (L2). +- **Standalone-but-atomic (L2, setelah SCC-1):** `console`, `config`, `translation`, `log`, `view`, + `routing`, `validation`, `queue`, `hashing`, `pagination` — bukan di SCC, tapi replace+namespace + collide → swap per-unit; masing-masing punya hard-require yang dipenuhi SCC-1. --- -## 3. Rencana paralelisasi (apa boleh dikerjakan bersamaan) +## 3. Rencana paralelisasi -Komponen **satu layer tanpa edge antar-mereka** = boleh dikerjakan concurrent. +Komponen **satu layer tanpa edge antar-mereka** = boleh dikerjakan concurrent — **tapi SEMUA L2 +gated ke SCC-1 selesai** (hard-require), jadi paralelisme L2 baru terbuka pasca-SCC-1. | Layer | Batch paralel | Lebar | Catatan | |---|---|---|---| -| **0** | `console` | 1 | fondasi, tak ada blocker | -| **1** | **SCC-1** `[support, container, http, session, cache, database, cookie, encryption*, events, filesystem, redis]` | 1 klaster | **tidak bisa dipecah** sebelum cycle diputus via `Contracts`. Setelah diputus → 11 sub-target paralel | -| **2** | `hashing*`, `config`, `exception`, `translation`, `log`, `view`, `workbench`, `routing`, `validation`, `queue` | **10** | **titik paralel terlebar** — sebar tim / ratchet oportunistik di sini | -| **3** | **SCC-2** `[foundation, mail, auth]`, `cachedrouting`, `html`, `pagination` | 4 | terminal; SCC-2 = bagian the flip | +| **Prereq** | split monolith + `replace`-map + `Contracts` + leaf traits (copy source) | 1 | **membuka semua**; harus lebih dulu | +| **1** | **SCC-1 core cutover** (11 komponen **+ Bus** transitif) | 1 klaster | **tidak bisa dipecah**; swap satu window setelah Contracts memutus cycle; hapus 11+ entry dari `replace` fork | +| **2** | `console`, `hashing`, `config`, `exception`, `translation`, `log`, `view`, `workbench`, `routing`, `validation`, `queue` | **11** | **titik paralel terlebar — tapi baru setelah SCC-1**; tiap swap per-unit; hard-require (console→view, queue→database, routing→session) sudah dipenuhi SCC-1 | +| **3** | **SCC-2** `[foundation, mail, auth]`, `cachedrouting`, `html`, `pagination` | 4 | terminal; SCC-2 = the flip; auth juga butuh queue (L2) | -**Choke point (fan-in tertinggi, kerjakan/putus lebih dulu):** `support` dan `container` -— hampir semua melewatinya. Memutus cycle di `support` (Facade → kontrak) **membuka L2–L3**. +**Choke point:** `support` dan `container`. Memutus cycle di `support` (Facade → kontrak) +**membuka SCC-1**, dan SCC-1 selesai **membuka seluruh L2**. Karena itu Prasyarat (Contracts + +Support-shape) diprioritaskan. --- -## 4. Fondasi baru L13 — WAJIB diperkenalkan lebih dulu +## 4. Fondasi baru L13 — INTRODUCE lebih dulu (memutus cycle) -Komponen ini **tidak ada di 4.2** tapi di L13 hampir semua bergantung padanya. Mereka duduk -**di bawah `Support`** dan **memutus cycle** SCC-1. Perkenalkan sebagai shim/polyfill dulu. +Komponen ini **tidak ada di fork 4.2** tapi di L13 hampir semua bergantung padanya. Mereka +duduk **di bawah `Support`** dan **memutus cycle** SCC-1. Introduce = **ship verbatim dari stock +13** (bukan hand-port), jaga divergence dari upstream. -| Komponen | Effort | Peran | Status di 4.2 / kenapa prasyarat | +> **Koreksi review — "introduce EARLY" ≠ "composer require EARLY".** Fork `replace` `illuminate/support` +> tapi **TIDAK** `replace` `collections/contracts/macroable/conditionable/reflection/pipeline`. Real +> `illuminate/collections:^13` mengirim `Illuminate\Support\{Collection,Arr,Enumerable}` yang +> **collide** dengan `src/Illuminate/Support/Collection.php` fork yang masih on-disk. Maka introduce +> EARLY = **copy SUPERSET SOURCE ke dalam tree Support fork** (namespace fork), **BUKAN** +> `composer require illuminate/collections`. Real package baru masuk **di window swap SCC-1** saat +> Support fork di-copot. + +| Komponen | Bucket | Effort | Peran / cara introduce | |---|---|---|---| -| **Contracts** | low | interface untuk semua service | **tidak ada** package Contracts di 4.2 → **memutus SCC-1** (import konkret → interface) | -| **Collections** | **high** | `Collection` dipisah dari Support | 4.2: satu class `Collection` ~20KB di Support; pemisahan + API L13 jauh lebih besar | -| **Macroable** | trivial | `::macro()` | 4.2: `MacroableTrait` di dalam support → tinggal dipromosikan jadi package | -| **Conditionable** | trivial | `->when()/->unless()` | idiom fluent L13; **tak ada** di 4.2 | -| **Reflection** | trivial | util refleksi | 4.2: cuma `Reflector` stub 947B → bukan introduce, "isi ulang" saja | -| **Pipeline** | low | middleware pipeline | **tak ada** di 4.2 (request lewat filter, bukan pipeline) | -| **Bus** | medium | command/job bus | **tak ada** abstraksi command-bus di 4.2 sama sekali | - -> Fitur L13-baru lain (opsional, adopsi saat perlu): `Broadcasting`, `Concurrency`, `Image`, -> `JsonSchema`, `Notifications`, `Process`, `Testing` — semua `introduce`, tak ada padanan 4.2. -> `Access\Gate` (otorisasi) ikut gratis di package `Auth` L13. Bukan prasyarat migrasi. +| **Contracts** | 1 | low | 155 interface, PSR-only composer require (**JANGAN** tambah `illuminate/*` → false cycle). **PREREQ #1, memutus SCC-1.** Fork tak `replace` contracts → aman di-require verbatim atau di-copy. | +| **Collections** | 1 | high | **copy SOURCE** superset ke `Illuminate\Support\*` tree fork (Collection/`Enumerable`/`LazyCollection`/`Arr`). Bukan call-site churn — **behavior-parity testing** (59→111 method). Real `illuminate/collections` swap **di window SCC-1**, bukan sekarang. | +| **Macroable** | 2 | trivial | rename `MacroableTrait`→`Macroable` (nama pendek hilang di 13), copy di tree Support. Bucket 2 karena rename = removal. | +| **Conditionable** | 1 | trivial | 2 file verbatim di `Support/Traits/Conditionable.php` + `Support/HigherOrderWhenProxy.php`. Additive murni. Copy source. | +| **Reflection** | 1 | trivial | `Reflector` **sudah ada** (stub 39-line); ganti dengan superset stock 13, **tetap fisik di tree Support** (jangan bikin dir baru). | +| **Pipeline** | 2 | low | copy source verbatim; **inert** sampai Routing/Kernel/Bus/Queue reshape (filter→middleware). Bucket 2 karena removal-nya ada di Routing (filter dihapus). | +| **Bus** | 1 | medium | **BUKAN SKIP** (koreksi review). `illuminate/events:^13` **hard-require** `illuminate/bus` → Bus transitif **wajib** ikut window SCC-1. App tetap 0 `Illuminate\Bus` call-site (pakai `DomainTransactionCommandBus` sendiri), jadi **nol app work**, tapi package-nya **harus terpasang** saat Events di-swap. | + +> Fitur L13-baru lain (**semua bucket 1, SKIP by default / YAGNI**): `Broadcasting`, +> `Concurrency`, `Image`, `JsonSchema`, `Notifications`, `Process`, `Testing`. App tak pakai +> subsistem Illuminate-nya (grep = 0). `Access\Gate` ikut gratis di package `Auth` L13. +> `Testing` adalah pengecualian **bucket 2**: package-nya baru, tapi idiom LAMA yang digantinya +> (`assertResponseOk`/`$this->call`/crawler) **ada** di `Foundation\Testing` fork dan dipakai +> ~500 file test → rewrite wajib sebelum swap (lihat §6/matrix). +> **Catatan Bus:** beda dari YAGNI lain — Bus **tak boleh** di-skip karena hard-require Events. --- -## 5. Tabel migrasi per-komponen (28 existing) - -Kolom **class** (enum verified): `done` · `evaporate` (native di 13, hapus saat flip) · -`shim-standalone` (swap internal ke lib/native, API tetap) · `shim-symfony` (bungkus -Symfony) · `reshape-callsites` (bentuk ulang call-site sekarang, mesin flip di akhir) · -`flip-only` (core, mesin baru pindah saat flip) · `app-space-extract` (pindah ke package). - -**Breakdown verified (28 existing):** flip-only 13 · reshape-callsites 7 · evaporate 3 · -shim-symfony 2 · done 1 (hashing) · shim-standalone 1 (encryption) · app-space-extract 1 (html). -Effort: trivial 1 · low 6 · medium 10 · high 7 · very-high 4. - -> ✅ **Basis verifikasi:** kolom **deps/cycle** = deterministik (grep `use`-statement + -> Tarjan). Kolom **class/effort** = **28/28 komponen existing ter-verifikasi adversarial** -> oleh agent (baca kode L42x + L13, grep count nyata, koreksi klasifikasi) + cross-check -> backing library (§5b). Gotcha behavior-change konkret yang ditemukan → §5c. -> Catatan lensa: `flip-only` = *mesin* baru pindah saat flip; tapi call-site tetap -> disiapkan bertahap sekarang (Fase 5) — checklist reshape-nya = §5c. - -| Komponen | Layer | Class (verified) | Effort | Gotcha / catatan kunci | -|---|---|---|---|---| -| **hashing** | 2 | **done** | low | default cost 10→12 (hash lama tetap verify, no lockout) | -| **encryption** | 1 | **shim-standalone** | low | algoritma **identik** (OpenSSL AES-256-CBC, HMAC-SHA256, wire-format kompatibel); bukan `done` krn tak implement Contracts + pakai `bindShared()` lawas | -| **cookie** | 1 | **shim-symfony** | medium | L13 `EncryptCookies` HMAC-prefix `v2` + validasi; cookie terenkripsi lama L42x **tak valid** saat flip | -| **session** | 1 | **shim-symfony** | medium | `implements` pindah Symfony `SessionInterface`→`Contracts\Session\Session`; Middleware HttpKernel→pipeline (non-inkremental) | -| **cache** | 1 | **reshape-callsites** | high | ⚠️ **TTL menit→detik (60x lebih pendek, senyap)**; `StoreInterface`→`Contracts\Cache\Store` | -| **config** | 2 | **reshape-callsites** | medium | ctor `(loader,env)`→`(array)`; ⚠️ `getEnvironment()` **dihapus** tapi **dipakai app** (pixel/sentry blade) | -| **events** | 2 | **reshape-callsites** | medium | `fire()`→`dispatch()` (21 callsite internal); `firing()` dihapus | -| **log** | 2 | **reshape-callsites** | medium | Monolog **1→3** (level jadi enum); wiring imperatif→config channels | -| **routing** | 2 | **reshape-callsites** | very-high | ⚠️ **filters dihapus total → wajib jadi middleware sebelum flip**; Router bukan lagi HttpKernel | -| **pagination** | 3 | **reshape-callsites** | high | getter rename massal (`getCurrentPage`→`currentPage`…); Presenter dihapus | -| **mail** | 3 | **reshape-callsites** | high | engine sudah symfony/mailer (menipu!); API/provider beda; queued-mail path beda (Mailable) | -| **console** | 0 | **flip-only** | medium | `fire()`→`handle()` (48 def); array args→`$signature` | -| **container** | 1 | **flip-only** | medium | exception pindah ke `Contracts\Container` | -| **http** | 1 | **flip-only** | medium | `Support\Contracts\*`→`Contracts\*`; `FrameGuard` (X-Frame-Options) hilang | -| **filesystem** | 1 | **flip-only** | low | L13 = **superset** kompatibel; `FileNotFoundException` pindah ke Contracts | -| **redis** | 1 | **flip-only** | low | default Predis→**phpredis**; predis 2.x construct cluster beda | -| **translation** | 2 | **flip-only** | medium | dep Symfony **dilepas** → native; `transChoice`/domain diganti | -| **view** | 2 | **flip-only** | high | ⚠️ **flush Blade compiled cache saat flip**; direktif L4-era bisa beda render | -| **validation** | 2 | **flip-only** | high | surface lama kompatibel; +`validate()`/`safe()`, +egulias/brick deps | -| **support** | 1 | **flip-only** | high | split ke Collections/Macroable/Contracts; `MacroableTrait`→`Macroable`; Contracts pindah namespace | -| **database** | 1 | **flip-only** | very-high | ⚠️ **blast radius app**: `->lists()`, `ArrayableInterface`, `SoftDeletingTrait`, casting timestamp/null | -| **queue** | 2 | **flip-only** | very-high | ⚠️ **wire-format payload beda → drain antrian saat cutover**; dispatch API inkompatibel | -| **foundation** | 3 | **flip-only** | very-high | Application = container **+** HTTP kernel; `App::error/down/middleware` hilang | -| **auth** | 3 | **flip-only** | high | ⚠️ `Reminders`→`Passwords` (tabel `password_reminders`→`password_resets`); Guard split 3; recaller format beda (§8) | -| **cachedrouting** | 3 | **evaporate** | low | **custom dicoding** → `route:cache` bawaan L13; app pemanggil `->cache()` harus di-desugar | -| **exception** | 2 | **evaporate** | low | package hilang; `App::error()` closure → `reportable()/renderable()`; Whoops hilang | -| **workbench** | 2 | **evaporate** | trivial | dev-tool, dibuang; tak dipakai app | -| **html** | 3 | **app-space-extract** | medium | **~497 file view** pakai `Form::`/`HTML::` → ekstrak ke package, API dipertahankan verbatim | - -### Catatan per-komponen non-trivial -- **support** — choke point + biang cycle. Konvergensi = pisahkan `Collection` (→ - Collections), sediakan `Arr`/`Str` tanda tangan L13 (sudah dimulai: `Arr::first`), - dan **alihkan Facade dari import konkret ke kontrak**. Ini yang memutus SCC-1. -- **database** — surface app terbesar. Eloquent/query builder banyak beda (casts, relasi, - `$fillable` default). Fokus: pakai **binding** (bukan string-concat) → tutup SQLi - sekalian; pusatkan akses ke repository/service (app-extract) agar flip murah. -- **routing** — beda paling tajam: 4.2 "filters" vs 13 "middleware", `['uses'=>'C@m']` vs - `[C::class,'m']`, route model binding. Semua call-site route bisa digeser sekarang. -- **session** — kandidat **shim-symfony** terbaik: kontrak = storage, cocok dengan - Symfony HttpFoundation Session (sudah jadi dependency). Hati-hati semantik flash & CSRF. -- **auth** — ~200 baris glue di atas primitif yang **sudah** terawat (password_verify, - OpenSSL recaller). Audit sekali, pin; flip pakai dual-read (§8). -- **cachedrouting / exception / workbench / html** — tidak diport. Native/absorbed/dibuang - di L13. Bekukan sekarang (bugfix only), hapus saat flip. +## 5. Prasyarat #1 — SPLIT MONOLITH + INTRODUCE CONTRACTS + REPLACE-MAP (buka semua) + +Fork sekarang = `laravel/framework` **monolitik** (satu package, `replace` 27 `illuminate/*`). +Real `illuminate/*` v13 = puluhan package terpisah dengan namespace `Illuminate\*` yang **sama** +DAN keduanya bernama `laravel/framework`. Konsekuensi: + +1. **Replace + namespace collision** → fork dan v13 tak bisa co-exist di classpath → swap **wajib + per-cluster**, bukan per-file bebas. Tiap swap butuh **edit blok `replace` fork** (§1a). +2. **SCC-1 harus diputus** sebelum core-nya bisa di-swap → introduce `Contracts` agar Facade/ + binding/import pindah dari kelas konkret ke interface. + +**Langkah Prasyarat (urut):** +1. **Introduce `Contracts`** verbatim dari stock 13 (semua 33 subdomain) — pure leaf + (`realDeps=[]`, `blockers=[]`). Jangan tambah `illuminate/*` ke composer-nya. Fork tak + `replace` contracts → tak ada konflik. +2. **Introduce leaf traits/util** dengan **copy SOURCE** ke bawah Support, **tetap fisik di tree + Support** (namespace, bukan package boundary, yang penting di monolith): + `Macroable` (rename dari `MacroableTrait`), `Conditionable` + `HigherOrderWhenProxy`, + `Reflector` superset + `ReflectsClosures`, `Collections` shape superset. + **JANGAN** `composer require illuminate/collections|macroable|conditionable|pipeline` sekarang — + collide dengan on-disk fork Support (§4). Real package menyusul di window SCC-1. +3. **Alihkan Facade/binding** dari import konkret ke interface `Contracts\*` → **SCC-1 putus**. +4. **Siapkan `replace`-map (§1a):** dokumentasikan tiap entry `replace` fork → package v13 target + + urutan pembukaannya (SCC-1 cluster dulu, lalu L2 per-unit, lalu SCC-2). Belum diedit sekarang; + entry dihapus **saat** cluster-nya di-swap (Fase 4). + +Setelah ini, pola berulang per-komponen (§6) bisa jalan. --- -### 5b. Pergeseran backing library (penentu class/effort) - -`require` non-illuminate di composer.json = mesin terawat yang menopang komponen. Delta -backing = penentu utama `done`/`shim`/`reshape`. - -| Komponen | Backing 4.2 | Backing 13 | Implikasi | -|---|---|---|---| -| encryption | OpenSSL | OpenSSL | sama → **done** | -| hashing | `password_*` | `password_*` | sama → **done** | -| cookie/http/session/routing/console | `symfony/*` | `symfony/*` (sama) | mesin sama; delta = API Illuminate, bukan engine → effort lebih rendah | -| filesystem | `symfony/finder` (FS native) | **`league/flysystem`** | re-platform → shim-std | -| support | (mungil, pure) | **doctrine/inflector, vlucas/phpdotenv, ramsey/uuid, league/uri, commonmark, symfony/uid, carbon** | membengkak → choke point **very-high** | -| translation | `symfony/translation` | native (ditulis ulang) | backing dilepas; API mirip | -| pagination | `symfony/http-foundation`+`translation` | native (ditulis ulang) | reshape ringan | -| cache | native/carbon | **+PSR-16, `symfony/cache`** | reshape | -| validation | `symfony/translation`+`http-foundation` | **+`egulias/email-validator`, `brick/math`, `ramsey/uuid`** | rule bertambah | -| database | carbon | **+`brick/math`** (desimal presisi) | reshape, surface app terbesar | -| log | `monolog` v2 | `monolog` v3 | major bump | -| mail | `symfony/mailer` | **+`league/commonmark`, css-inline, transport bridges** | Mailable/markdown baru | -| container | pure | **`psr/container`** (PSR-11) | flip; adopsi kontrak PSR-11 | - -**Pola:** komponen ber-backing Symfony sama di kedua sisi (cookie/http/session/routing/ -console) → murah, delta cuma di lapisan Illuminate. Yang mahal = yang **ganti/menambah -backing** (filesystem→Flysystem, support membengkak, validation/cache/database nambah lib). +## 6. Pola berulang per-komponen (STRICT-MODE-L13 loop) + +Untuk **tiap** komponen, tiga langkah: + +**(a) Tighten fork type → strict-mode-L13.** Perketat signature publik fork agar sama/lebih +ketat dari stock 13. Idiom 4.2 lama jadi `TypeError`/method-not-found. Enforcement hidup di +kontrak framework (single source of truth) — **kecuali** dua kelas di bawah. + +**(b) App conforms — DIPAKSA PHP+Psalm (bucket 2/3 saja).** Karena signature diperketat, call-site +lama gagal compile/resolve. App migrasi ke idiom baru. **Bucket 1 = SKIP langkah ini** (app tak +tersentuh — idiom juga ada di stock 13). + +**(c) Swap komponen ke `illuminate/*` v13 asli.** Hormati replace+namespace-collision → hapus entry +dari blok `replace` fork (§1a), lalu `composer require illuminate/:^13`, swap per-cluster +bottom-up. Setelah swap, shim/alias di fork mati. + +**Dua pengecualian di mana strictness fork TIDAK bertahan pasca-swap (butuh guard non-type):** +- **Tighten-then-lint (Cache-TTL, bucket 3).** Fork perketat TTL → interval-only **sementara**, + memaksa app pass interval. Tapi stock 13 `put($k,$v,$ttl=null)` (verified `Repository.php:367`, + `Contracts/Cache/Repository.php:29`) **re-admit bare int** saat swap → bug 60× senyap kembali + **tak-terjaga**. **Guard permanen = Psalm-rule/ratchet-grep** yang melarang bare-int TTL, hidup + **melewati** swap. Bukan self-liquidating. +- **Fork-only temporary-abstract (Console `handle()`).** Fork bikin `handle()` abstract + **sementara** untuk me-ratchet ~308 rewrite `fire()`. Tapi stock 13 me-resolve + `method_exists($this,'handle')?'handle':'__invoke'` (verified `Command.php:289`) → `fire()`-only + jatuh ke `__invoke` missing = **runtime `BadMethodCall`**, bukan abstract/Psalm error. Post-swap + deteksi = **grep + boot-smoke**, bukan type. + +Untuk pola yang **TIDAK type-expressible** (missing global helper `str_*`/`array_*`, SQL +string-concat, config assertion seperti default cipher / redis client, wire-format payload, +Blade-cache flush, route-array `'before'=>`): **pakai ratchet CI grep / boot-guard / runbook**. + +### 6a. Ringkasan bucket + enforcement per-komponen + +Detail lengkap (rasional bucket, fork strict action, app impact, swap order) ada di +**`MIGRATION-ENFORCEMENT-MATRIX.md`**. Ringkas di sini: + +| Komponen | Bucket | Type-expr? | Enforcement inti | Kerja app | +|---|---|:---:|---|---| +| **Contracts** | 1 | ✅ (by presence) | introduce verbatim; binding type-hint interface | ~0 (1-file rename ikut Support) | +| **Support** | 2 | ⚠️ sebagian | rename `MacroableTrait`→`Macroable`, hapus `Support\Contracts\*Interface`; helper `str_*`/`array_*` → **ratchet (bukan type)** | kecil: 1 file interface + audit helper | +| **Collections** | 1 | ❌ (copy source, superset) | copy source ke tree Support; parity test; **real swap di SCC-1 window** (collide fork on-disk) | nol | +| **Macroable** | 2 | ✅ (rename) | `use ...MacroableTrait` → trait-not-found | nol app (framework-internal) | +| **Conditionable** | 1 | ❌ (copy source) | copy 2 file | nol | +| **Pipeline** | 2 | ❌ (removal ada di Routing) | copy source; filter-removal di Routing | indirect (filter→middleware) | +| **Reflection** | 1 | ✅ (trivial, sudah aman) | keep `Support\Reflector` superset | nol | +| **Container** | 1 | ✅ | hapus `share/isShared/bindShared/resolvingAny`; relokasi exception ke Contracts (alias) | ~0 (11 ref, 10 test, alias jaga) | +| **Http** | 1 | ✅ | hapus `FrameGuard`; internal `Support\Contracts\*`→`Contracts\Support\*` | nol | +| **Cache** | **3** | ⚠️ **tighten-then-lint** | **fork tighten TTL param → `DateTimeInterface\|DateInterval`** (bare int TypeError) untuk **memaksa migrasi app**; **guard permanen pasca-swap = Psalm-rule/ratchet-grep** (stock 13 `$ttl=null` re-admit bare int → strictness fork menguap). `StoreInterface`→`Contracts\Cache\Store`. **Fork-internal bare-int caller (verified: `CacheBasedSessionHandler.php:60`, `ArrayStore.php:74`, `ApcStore.php:94`) harus dikonversi di commit yang sama** atau fork tak boot. | task 1.1 DONE; 5 custom Store pindah contract | +| **Events** | 2 | ✅ | add `dispatch()` canonical, deprecate lalu hapus `fire()/queue()/firing()/forgetQueued()`; drop `$priority` | ~96 `fire()`→`dispatch()` + review 118 `listen()` | +| **Encryption** | 2 | ✅ | hapus `setKey()`; implement `Contracts\Encryption\*`; relokasi `DecryptException` (alias) | ~0 code; config `cipher=AES-256-CBC` (deploy) | +| **Cookie** | 2 | ⚠️ sebagian | widen jar param (additive); **rewrite Guard/Queue decorator → middleware** (flip-time) | ~0 code; runtime logout sekali di flip | +| **Session** | 2 | ⚠️ sebagian | Store `implements Contracts\Session\Session`, hapus bag API; Middleware → StartSession/AuthenticateSession | ~0 code (402 site di data-API stabil) | +| **Redis** | 1 | ❌ (wholesale replace + config) | drop-in RedisManager; config `client` = **config-guard, bukan type** | nol | +| **Filesystem** | 2 | ✅ (alias) | relokasi `FileNotFoundException` ke Contracts + `class_alias` (`@deprecated`) | 11 import (aliasable, deferrable) | +| **Database** | 2 | ⚠️ campuran | hapus `lists()`; rename `pluck`→`value` + `pluck` semantik-baru; `SoftDeletingTrait`→`SoftDeletes`; `ArrayableInterface`→`Arrayable`. **pluck-swap NOT type-expressible** → grep/Psalm-rule + review | **sangat besar**: ~173 lists + ~193 pluck + ~21 rename | +| **Queue** | 1 | ✅ (removal kecil) | hapus closure-push branch + Iron.io driver; **string-push + `fire()` handler tetap valid di 13** | 2 closure push; ops: **drain queue** di cutover | +| **Console** | 2 | ⚠️ **fork-only temp-abstract** | fork **sementara** `execute()` panggil `abstract handle()` untuk me-ratchet ~308 rewrite; **stock 13 resolve handle-or-`__invoke` runtime → post-swap deteksi = grep/boot-smoke, bukan abstract/Psalm**; hapus `Application::make/start/...` | **besar**: ~308 `fire()`→`handle()`, ~87 getArguments/Options→`$signature`, Kernel wiring | +| **Config** | 2 | ✅ | hapus `getEnvironment()` + loader/package-cascade; ctor `(array)` | ~48 site `getEnvironment()`→`App::environment()` (incl. blade) | +| **Exception** | 2 | ❌ (closure/string-binding) | hapus `App::error()`/bindings → merge ke Foundation Handler | **besar**: 11 `App::error` + custom ExceptionServiceProvider rewrite | +| **Translation** | 1 | ✅ | hapus `trans()/transChoice()`, implement `Contracts\Translation\Translator` | nol (app pakai `Lang::get`/`__`) | +| **Log** | 1 | ✅ | rename `getMonolog()`→`getLogger()`, `Writer`→`Logger`; hapus `useFiles/useDailyFiles` | 3 `getMonolog()` (CoreLogServiceProvider + Monolog 1→3) | +| **View** | 1 | ❌ (subset, sudah benar) | hapus `Factory::of/name/alias`; Blade `extend` **bukan** type → grep/manual | 2 `Blade::extend`→`directive` | +| **Workbench** | 1 | ❌ (delete) | hapus dir + composer require | nol | +| **Routing** | 2 | ✅ (method removal) | hapus `filter/before/after/when/callFilter/controller`; drop HttpKernelInterface. Route-array `'before'=>` **bukan** type → grep | **besar**: ~52 filter + 141-line filters.php + 131 route-array → middleware | +| **Validation** | 1 | ✅ (contract identity) | Validator `implements Contracts\Validation\Validator` (drop old) | nol (18 `make`, 0 custom rule) | +| **Hashing** | 2 | ✅ | `HasherInterface` extend/alias `Contracts\Hashing\Hasher`, lalu delete | 4 site type-hint → contract | +| **Foundation** | 2 | ❌ (positive bootstrap) | hapus `App::error/missing/fatal/down/middleware`, drop HttpKernel/Terminable; **reshape bootstrap = grep/boot-smoke** | **sangat besar**: bootstrap rewrite + testing base | +| **Mail** | 2 | ✅ | `send():?SentMessage`, hapus `failures()`/closure-queue, `queue()` terima Mailable | ~17 closure/callback → Mailable | +| **Auth** | 1 | ✅ (self-liquidating, app kosong) | drop `attempt()` 3rd arg, relokasi UserInterface→Contracts, AuthManager `implements Factory` | ~0 (853 file di facade stabil; 0 breaking site) | +| **Bus** | 1 | ❌ (introduce, transitif Events) | **BUKAN skip** — hard-require `illuminate/events` → wajib terpasang di SCC-1 window; copy verbatim (sudah strict) | nol (0 `Illuminate\Bus` call) | +| **CachedRouting** | 2 | ✅ (removal) | hapus `Router::cache()/clearCache()` + subtree | 28 `Route::cache(__FILE__,fn)` de-sugar | +| **Html** | 2 | ❌ (preserve API) | **app-space-extract** verbatim; enforcement = boot-smoke, bukan type | 0 view edit; pindah 3 file provider | +| **Pagination** | 2 | ✅ (mostly) | rename getter (`getCurrentPage`→`currentPage`…); hapus Factory/Presenter | ~3 site (Presenter/Factory) + `items()` rename | +| **Testing** | 2 | ✅ (removal di Foundation) | hapus `assertResponseOk/$this->call/crawler` dari `Foundation\Testing` | **besar**: ~500 file test → fluent `TestResponse` | --- -### 5c. ⚠️ Gotcha kritis migrasi (temuan verified per-komponen) +## 7. ⚠️ Gotcha kritis migrasi (verified — JANGAN dilemahkan) -Ini jebakan konkret yang ditemukan agent saat baca kode L42x vs L13. **Prioritaskan yang -"silent" — tak ada error, cuma hasil salah.** +Jebakan konkret yang ditemukan agent saat baca kode fork vs L13. **Prioritaskan yang "silent".** **A. Silent behavior change (paling bahaya — tak ada error, hasil beda diam-diam)** -- 🔴 **Cache TTL menit → detik.** Setiap `Cache::put/add/remember($k,$v, N)` dengan TTL - numerik: L42x = N **menit**, L13 = N **detik** → cache **60× lebih pendek**, senyap. - Regresi paling berdampak. Sweep semua call-site TTL numerik. -- 🟠 **Hashing cost 10 → 12.** Hash lama tetap `verify` (aman), cuma CPU/login naik. -- 🟠 **Database casting.** Timestamp default, `$dateFormat`, null vs empty-string, - strict-mode — bisa ubah nilai tanpa error. -- 🟠 **Redis default Predis → phpredis.** L13 default `ext-redis`; kalau tak terpasang & app - mengandalkan Predis, wiring harus dipin ke `predis`. - -**B. Persisted-state pecah saat flip (data ditulis L42x tak terbaca L13)** -- 🔴 **Auth recaller cookie & session.** Format "remember me" beda (L42x `getRecallerName` - vs L13 `Recaller`) → sesi aktif invalid, logout massal. **Wajib dual-read** (§8). -- 🔴 **Cookie `EncryptCookies` v2.** L13 mem-prefix HMAC (`CookieValuePrefix 'v2'`) & - memvalidasi; cookie terenkripsi lama L42x ditolak saat flip. -- 🔴 **Queue wire-format.** Payload ter-serialisasi L42x di SQS/Redis **tak bisa** dikonsumsi - worker L13. **Drain antrian sampai kosong saat cutover.** -- 🟠 **Auth `Reminders` → `Passwords`.** Tabel `password_reminders` → `password_resets`; - migration/kode yang menyebut `Illuminate\Auth\Reminders\*` pecah. -- 🟠 **View Blade compiled cache.** `.php` hasil kompilasi di `storage/framework/views` - **wajib di-flush** saat flip. - -**C. Rename mekanis (sweep sebelum/saat flip)** -- `Console` `fire()` → `handle()` (**48 definisi**); array args → `$signature`. -- `Events` `fire()` → `dispatch()` (**21 callsite**); `firing()` dihapus. -- `Support` `MacroableTrait` → `Macroable`; `Illuminate\Support\Contracts\*` → - `Illuminate\Contracts\Support\*` (Arrayable/Jsonable/Renderable/MessageProvider). -- `Pagination` getter: `getCurrentPage`→`currentPage`, `getLastPage`→`lastPage`, - `getFrom`→`firstItem`, `getTo`→`lastItem`, `getTotal`→`total`, `getPerPage`→`perPage`. -- `Routing` filters (`Route::filter`, `->before/->after`, `filters.php`) → **middleware**. -- Eloquent `->lists()` → `pluck()`; `ArrayableInterface` → `Arrayable`; `SoftDeletingTrait`. - -**D. Breakage di app-space (ditemukan agent grep app konsumen — verifikasi ulang di app)** -- `Html` `Form::`/`HTML::` di **~497 file view** → ekstrak ke package, API dipertahankan. -- `Config::getEnvironment()` dipakai di `pixel.blade.php`, `sentry.blade.php` dll — method - ini **dihapus** di L13. -- `CachedRouting` `->cache(...)` pada route facade → de-sugar ke registrasi route biasa. - -> Struktural: **komponen `Contracts` tidak ada sama sekali di pohon L42x** (`src/Illuminate/ -> Contracts` absen). Karena banyak rename di atas bermuara ke relokasi ke namespace -> `Illuminate\Contracts\*`, **memperkenalkan `Contracts` = prasyarat** (Fase 4) yang membuka -> mayoritas sweep rename ini. - ---- - -## 6. Roadmap berfase (cross-ref graph) - -### Fase 0 — Enabler (sekali kerja, buka semua) -- **WAF di edge** (Cloudflare/AWS) — tutup surface security core 4.2 yang tak terpatch **sekarang**. -- **Ratchet CI + divergence ledger** — rem satu arah agar konvergensi tak mundur. -- **`../framework` L13 sebagai north-star** tanda tangan API. - -### Fase 1 — Evaporate (susutkan fork tanpa port) -`cachedrouting`, `workbench`, `exception`, bagian `queue`/SQS → bekukan, tandai "hapus saat flip". - -### Fase 2 — Leaf/done (audit + pin) -`encryption`, `hashing` → sudah di primitif terawat. Audit sekali, dokumentasikan "sengaja begini". - -### Fase 3 — Decoupling app-space *(bulk bertahun-tahun, nol izin management)* -Tarik logika bisnis dari `database`/controller ke PHP polos; kustomisasi core → service -provider/package. Digerakkan ratchet, bukan proyek. **Metric % file lepas-framework = -bukti progres ke management.** - -### Fase 4 — Putus cycle + shim (bottom-up per graph) -1. Perkenalkan `Contracts`/`Collections` → **putus SCC-1**. -2. `support` (Arr/Str/Collection L13-sig, Facade→kontrak) — buka L2–L3. -3. `session` (shim-symfony), `config`, `log`, `filesystem` (flysystem), `translation`. - -### Fase 5 — Reshape call-site (framework-locked) -`routing`, `validation`, `http`, `view`, `pagination`, `mail`, `cache`, `events`, `queue`, -`auth` → bentuk call-site ke idiom L13; mesin tetap 4.2 sampai flip. - -### Fase 6 — The Flip *(minggu, bukan bulan)* -Skeleton L13 stock → pindah app code (sudah L13-sig) + package app-space → copot semua shim, -buang core 4.2 + komponen evaporate → **dual-read Auth** (§8) → **Laravel 13 stock, tanpa fork**. - ---- - -## 7. Risk register - -| Risiko | Dampak | Mitigasi | -|---|---|---| -| Framework EOL, tanpa patch vendor (core Illuminate) | security surface tak terjaga | WAF (Fase 0); leaf sudah di primitif terawat; audit sekali komponen glue | -| Cycle SCC-1 menghalangi kerja terpisah | migrasi macet | perkenalkan `Contracts` lebih dulu (Fase 4) | -| "Gradual" membusuk jadi limbo dua-idiom | onboarding makin kacau | **ratchet CI** (blok pola 4.2 baru, grandfather lama) | -| Endgame jadi fork baru "L130x" | jebakan terulang | anti-goal (§9): kustomisasi ke extension point, core haram | -| Flip Auth me-logout semua user | insiden produksi | **dual-read transition** (§8) | -| SQL injection di query string-concat | data breach | audit `whereRaw`/concat → binding (Fase 3) | +- 🔴 **Cache TTL menit → detik** (bucket 3, **tighten-then-lint — BUKAN self-liquidating**). + `Cache::put/add/remember($k,$v,N)` TTL numerik: fork = N **menit**, L13 = N **detik** → 60× + lebih pendek, senyap. **Enforcement dua tahap:** (1) fork **sementara** tighten param ke + `DateTimeInterface|DateInterval` → bare int = `TypeError` → paksa migrasi app; (2) **guard + permanen pasca-swap = Psalm-rule/ratchet-grep** melarang bare-int TTL — karena stock 13 + `put($k,$v,$ttl=null)` (verified `Repository.php:367`, `Contracts/Cache/Repository.php:29`, + `getSeconds():903`) **kembali menerima bare int**, strictness fork **menguap saat swap**. Task + 1.1 (app) **DONE** (sudah pass Carbon interval). Follow-on: fork tighten (memaksa) + Psalm-rule + (menjaga). **Fork-internal bare-int caller (verified: `CacheBasedSessionHandler.php:60` `put($id,$data,$this->minutes)`, + `ArrayStore.php:74` & `ApcStore.php:94` `put($key,$value,0)`) harus dikonversi bareng tighten** + atau fork tak boot. +- 🔴 **Database `pluck`↔`value`/`lists` name-swap** (bucket 2, **NOT type-expressible**). + fork `pluck`=single value → L13 `value()`; fork `lists`=column array → L13 `pluck()`. + 193 `->pluck()` site berubah arti diam-diam. Rename fork memaksa review; **grep/Psalm-rule** + cegah dev "memperbaiki" balik ke `pluck()` dengan makna salah. +- 🟠 **Database casting.** Timestamp default, `$dateFormat`, null vs empty-string, strict-mode. +- 🟠 **Redis default Predis → phpredis** (config assertion, bukan type): pin `client=>predis` + bila `ext-redis` absen. +- 🟠 **Encryption default cipher** (fork AES-256-CBC vs L13 aes-128-cbc): boot-guard config, + bukan signature — pin `cipher=AES-256-CBC` sebelum swap atau cookie/cache lama undecryptable. + +**B. Persisted-state pecah saat flip (data ditulis fork tak terbaca L13)** +- 🔴 **Auth recaller cookie & session** — format "remember me" beda → logout massal. + **Wajib dual-read** (§8). +- 🔴 **Cookie `EncryptCookies` v2.** L13 mem-prefix HMAC (`CookieValuePrefix 'v2'`) & memvalidasi; + cookie terenkripsi lama fork ditolak saat flip (logout sekali, expected). +- 🔴 **Queue wire-format.** Payload ter-serialisasi fork **tak bisa** dikonsumsi worker L13. + **Drain antrian sampai kosong saat cutover** (runbook, bukan type). +- 🟠 **Auth `Reminders` → `Passwords`.** Tabel `password_reminders` → `password_resets`. +- 🟠 **View Blade compiled cache** wajib di-flush di `storage/framework/views` saat swap. +- 🟠 **Cache DatabaseStore drop Encrypter** — row cache-table terenkripsi lama tak terbaca; + invalidate/flush DB cache di cutover. + +**C. Rename mekanis yang type-caught (idiom lama jadi TypeError/method-not-found)** +- `Events` `fire()` → `dispatch()` (**~96 callsite**); `firing()`/`$priority` dihapus. +- `Support` `MacroableTrait` → `Macroable`; `Support\Contracts\*Interface` → `Contracts\Support\*`. +- `Pagination` getter: `getCurrentPage`→`currentPage`, dll.; `getItems()`→`items()`. +- `Database` `->lists()` → `->pluck()`; `SoftDeletingTrait`→`SoftDeletes`; `ArrayableInterface`→`Arrayable`. +- `Config` `getEnvironment()` → `App::environment()`. +- `Cache` `StoreInterface` → `Contracts\Cache\Store`. +- `Hashing` `HasherInterface` → `Contracts\Hashing\Hasher`. +- `Filesystem` `Illuminate\Filesystem\FileNotFoundException` → `Contracts\Filesystem\...` (alias). +- **`Console` `fire()` → `handle()` (~308 def)** — type-caught **HANYA di fork** (temporary + abstract). **Menguap saat swap** (stock resolve `handle`-or-`__invoke` runtime) → post-swap = + grep/boot-smoke, lihat §7D. + +**D. Enforcement grep/boot/runbook (NOT type-expressible / strictness fork menguap saat swap)** +- 🔴 **Cache bare-int TTL — guard PERMANEN** (strictness fork menguap; stock re-admit bare int) + → **Psalm-rule / ratchet-grep** yang hidup melewati swap. +- 🔴 **Console `fire()`-only command pasca-swap** (stock jatuh ke `__invoke` missing = + runtime `BadMethodCall`, bukan Psalm) → **grep `function fire(` + boot-smoke tiap command**. +- Global helper `str_*`/`array_*` (helpers.php 59→23) — missing function fatal saat call, tak + terlihat Psalm → **ratchet grep**. +- `Routing` route-array `'before'=>'auth'` (array key, bukan method call) → **grep/Psalm-rule**. +- `Foundation` bootstrap `bootstrap/app.php` builder / Kernel middleware arrays / `withExceptions` + → **grep + boot-smoke test** (file yang tak panggil builder baru bukan type error). +- `Exception` `App::error(Closure)` + `$app['exception']` string-binding → **grep**. +- SQL string-concat / `whereRaw` → **ratchet grep** (SQLi surface). +- Redis client / Encryption cipher / config channels logging.php → **boot-guard / config assertion**. + +> Struktural: **package `Contracts` tidak ada sama sekali di tree fork**. Banyak rename di atas +> bermuara ke relokasi namespace `Illuminate\Contracts\*` → **introduce `Contracts` = Prasyarat** +> (§5) yang membuka mayoritas sweep rename ini. --- -## 8. Auth — transition dual-read (agar flip tanpa logout massal) +## 8. Auth — transition dual-read (agar flip tanpa logout massal) — TIDAK berubah `Guard` menyimpan state di format Illuminate yang **sudah beredar** di user: - recaller cookie: `{user_id}|{remember_token}` (dienkripsi app key) - session key user login: `login_{sha1(class)}` -Saat flip, **jangan** langsung ganti format (semua sesi/cookie invalid). Pola zero-downtime: -1. **Baca dua format**: recaller & session key lama (Illuminate) *dan* format baru L13. +Saat swap Auth, **jangan** langsung ganti format. Pola zero-downtime: +1. **Baca dua format**: recaller & session key lama (fork) *dan* format baru L13. 2. **Tulis** hanya format baru. -3. Format lama kedaluwarsa alami seiring TTL remember-me (mis. beberapa minggu). +3. Format lama kedaluwarsa alami seiring TTL remember-me. 4. Hapus kode dual-read setelah TTL lewat. +> Catatan Auth (verified): app pakai facade stabil di **853 file**, dan **0 call-site** untuk +> setiap API yang dihapus (attempt 3rd arg, Reminders, custom provider, manager driver). Jadi +> Auth = **bucket 1** (fork-only) untuk app ini; strictness self-liquidating gratis. Yang tersisa +> = dual-read state format (runtime), bukan code edit. Catatan swap: `illuminate/auth:^13` +> **hard-require** `illuminate/queue` → Auth swap butuh Queue (L2) sudah L13-shaped. + --- -## 9. ⛔ Anti-goal (kotak peringatan) +## 9. ⛔ Anti-goal (kotak peringatan — DIPERKUAT) ``` ╔══════════════════════════════════════════════════════════════════════╗ -║ ENDGAME = LARAVEL 13 STOCK (UNFORKED). ║ +║ ENDGAME = LARAVEL 13 STOCK (UNFORKED). FORK DIHAPUS, FORK = 0. ║ +║ ║ +║ "STRICT-MODE L13" cuma PERANCAH: fork yang signature-nya diperketat ║ +║ agar app pindah ke idiom L13 SEBELUM tiap komponen di-swap ke ║ +║ illuminate/* v13 asli. Ia SELF-LIQUIDATING untuk removal (fire/ ║ +║ lists/getEnvironment) — hilang begitu swap selesai. TAPI DUA KELAS ║ +║ TIDAK self-liquidating & butuh guard non-type PERMANEN: ║ +║ • Cache-TTL (stock re-admit bare int) → Psalm-rule/ratchet. ║ +║ • Console handle() (stock resolve handle|__invoke) → grep/boot. ║ ║ ║ -║ "L13 custom" hanya MILESTONE/perancah (4.2 memakai baju L13 via ║ -║ shim), BUKAN tujuan. Jika berhenti di L13 yang di-fork, jebakan ║ -║ 4.2 terulang satu versi ke atas. ║ +║ Enforcement lewat TYPE (PHP+Psalm) bila removal; ratchet CI ║ +║ DIDEMOTE tapi TETAP untuk: pola non-type (helper, SQL-concat, ║ +║ config) DAN guard pasca-swap tighten-then-lint di atas. ║ +║ ║ +║ Fork & stock SAMA-SAMA laravel/framework & sama-sama replace ║ +║ illuminate/* → TAK BISA co-install. Swap per-cluster = hapus entry ║ +║ replace fork tiap langkah; Fase-5 gate: replace fork = 0 baru ║ +║ composer require laravel/framework:^13. ║ ║ ║ ║ Semua kustomisasi → EXTENSION POINT L13: ║ ║ Auth::extend, custom cache/queue/route driver, macro, ║ @@ -376,19 +501,110 @@ Saat flip, **jangan** langsung ganti format (semua sesi/cookie invalid). Pola ze --- -## 10. Metodologi & keterbatasan +## 10. Roadmap berfase (framework-first) + +### Fase 0 — Enabler (sekali kerja) +- **WAF di edge** (Cloudflare/AWS) — tutup surface security core 4.2 tak terpatch **sekarang**. +- **`../framework` L13 sebagai north-star** signature (single source of truth kontrak). +- **Ratchet CI didemote** — untuk pola non-type (helper global, SQL-concat, config) **DAN** guard + pasca-swap tighten-then-lint (Cache-TTL bare-int) + fork-only temp-abstract (Console fire-only). + +### Fase 1 — PRASYARAT: split monolith + introduce Contracts + replace-map (§5) +Split fork monolitik → potongan `illuminate/*`-shaped; introduce `Contracts` + leaf traits +(**copy source**, bukan composer require); alihkan Facade/binding ke interface → **putus SCC-1**. +Siapkan `replace`-map (§1a) untuk dibuka per-cluster. **Ini membuka semua.** + +### Fase 2 — Strict-mode tighten (bucket 2/3, per-komponen, tanpa swap) +Perketat signature fork komponen-per-komponen (§6 langkah a). App conforms, dipaksa PHP+Psalm +(§6 langkah b). Cache TTL tighten (+ konversi fork-internal caller + tambah Psalm-rule guard), +Events `dispatch()`, Database `lists`/`pluck`/`SoftDeletes`, Console `handle()` (temp abstract), +Config `getEnvironment()`, Routing filters→middleware, dst. Mesin masih fork. + +### Fase 3 — App-space extract (yang tak punya padanan v13) +`Html` extract verbatim ke package app-space (0 view edit). `CachedRouting` de-sugar 28 site. +Logika bisnis ditarik ke PHP polos (selamat dari swap). + +### Fase 4 — Swap bottom-up per cluster (§6 langkah c; tiap swap = edit `replace` fork §1a) +1. **SCC-1 core cutover** (contracts+reflection+support+container jangkar; http/session/cache/ + cookie/encryption/events/filesystem/redis/database + **bus** transitif satu window). Hapus + ~12 entry dari `replace` fork; `composer require illuminate/:^13` masing-masing. +2. **L2** per-unit (baru terbuka setelah SCC-1; hard-require dipenuhi): console (butuh view), + config, translation, log, view, routing (butuh session), validation, hashing, queue (butuh + database+console). Tiap: hapus entry `replace` → require real. +3. **SCC-2 terminal (the flip):** putus cycle (Mailer contract + Auth Contracts) → swap + foundation+mail+auth (auth butuh queue L2) → dual-read Auth (§8) → drain queue, flush Blade + cache, migrasi tabel. + +### Fase 5 — Fork = 0 +Copot semua shim/alias; hapus komponen evaporate (CachedRouting/Workbench/Exception/Html-in-core); +buang core fork; **verifikasi blok `replace` fork = 0 entry** (prasyarat resolver) → `composer +require laravel/framework:^13` stock, tanpa fork; verifikasi tak ada `Illuminate\*` fork tersisa. +Hapus dual-read setelah TTL remember-me lewat. **Pertahankan** Psalm-rule Cache-TTL bare-int + +grep Console fire-only (guard pasca-swap yang tak self-liquidate). + +--- + +## 11. Risk register + +| Risiko | Dampak | Mitigasi | +|---|---|---| +| Framework EOL, tanpa patch vendor (core Illuminate) | security surface tak terjaga | WAF (Fase 0); swap ke v13 asli mengakhiri maintainer-burden | +| Cycle SCC-1 menghalangi swap terpisah | migrasi macet | **Prasyarat #1**: introduce `Contracts` → putus SCC-1 (§5) | +| **Fork & stock sama-sama `laravel/framework` & replace illuminate/*** | **tak bisa co-install (resolver conflict)** | swap **per-cluster** + **edit blok `replace` fork tiap langkah** (§1a); Fase-5 gate replace=0 | +| Namespace `Illuminate\*` collide fork↔v13 | tak bisa co-exist | swap **per-cluster** (SCC-1 core sebagai satu window) | +| Idiom 4.2 lolos ke prod (bucket 2 removal) | regresi senyap | **enforcement via type** (PHP+Psalm), self-liquidating | +| **Cache-TTL bare-int kembali pasca-swap** (stock re-admit) | **bug 60× senyap balik** | **Psalm-rule/ratchet-grep PERMANEN** (tighten fork hanya memaksa migrasi, tak menjaga) | +| **Console `fire()`-only lolos pasca-swap** (stock jatuh ke `__invoke`) | command mati runtime | **grep + boot-smoke tiap command** (temp-abstract fork menguap saat swap) | +| Pola non-type lolos (helper/SQL-concat/config) | regresi | ratchet grep + boot-guard + runbook (§7D) | +| Endgame jadi fork baru "L130x" | jebakan terulang | anti-goal (§9): strict-mode = perancah, fork=0 | +| Flip Auth me-logout semua user | insiden produksi | **dual-read transition** (§8) | +| Queue wire-format / Cookie v2 / DB-cache | data tak terbaca | drain queue + invalidate cache + logout-sekali di cutover (runbook) | +| SQL injection di query string-concat | data breach | audit `whereRaw`/concat → binding (Fase 2, ratchet) | +| Fork tak boot setelah Cache tighten (internal bare-int caller) | build merah | konversi `CacheBasedSessionHandler:60`+`ArrayStore:74`+`ApcStore:94` di commit yang sama | + +--- + +## 12. Metodologi & keterbatasan - **Edges** = `grep -rhoE 'use Illuminate\\[A-Za-z]+'` per direktori komponen di kedua repo, dedupe lowercase, exclude self. Deterministik & tervalidasi manual. -- **SCC/layer/critical-path** = Tarjan + topological layering (kode deterministik), - divalidasi manual (akar cycle di-grep langsung: Facade→Http, CapsuleTrait→Container, - PasswordBroker→Mail, MailServiceProvider→Foundation). -- **Verifikasi per-komponen** = workflow multi-agent (baca kode L42x + L13, grep count - nyata, koreksi adversarial). Dijalankan bertahap dengan resume tiap org spend limit reset. - Status: **28/28 komponen existing ter-verifikasi**; 7 komponen **L13-baru** (Bus, - JsonSchema, Process, Broadcasting, Notifications, Image, Testing) belum ter-verify — - tidak kritikal (bukan tabel utama, lihat §4). Data mentah verified disimpan di - `.migration-verified-records.json` (apiDelta + risks lengkap per komponen). -- **Keterbatasan**: auto-generate doc + audit oleh agent gagal di spend limit; dokumen ini - **di-merge manual** dari record verified (bukan tulisan agent). Klaim app-space di §5c(D) - ditemukan agent via grep app konsumen — **verifikasi ulang di repo `dicoding`** saat digarap. +- **SCC/layer/critical-path** = Tarjan + topological layering (deterministik), divalidasi manual + (akar cycle di-grep langsung: Facade→Http, CapsuleTrait→Container, PasswordBroker→Mail, + MailServiceProvider→Foundation). **Layer L2 di-re-derive dari HARD-REQUIRE v13** (console→view, + queue→database+console, auth→queue, routing→session), bukan suggest — console pindah dari L0 ke L2. +- **Verifikasi & re-klasifikasi 3-bucket per-komponen** = workflow multi-agent adversarial (baca + kode fork + L13, grep count nyata di app `dicoding`, koreksi klasifikasi). Data mentah di + `.migration-verified-records.json` (42 record) + prosa `MIGRATION-DETAIL.md`; matriks + enforcement `MIGRATION-ENFORCEMENT-MATRIX.md`. +- **Koreksi enforcement dari review (verified vs kode, 2026-09-10):** + - **Cache-TTL BUKAN type-expressible di stock 13.** `Cache/Repository.php:367` + `put($key,$value,$ttl=null)` + `Contracts/Cache/Repository.php:29` (untyped) + `getSeconds():903` + (terima bare int). Fork interval-only lebih ketat dari stock → **tidak self-liquidating** → + reklasifikasi tighten-then-lint (guard permanen = Psalm/ratchet). + - **Console `handle()` BUKAN abstract/Psalm-catch.** `Command.php:289` + `method_exists($this,'handle')?'handle':'__invoke'` → `fire()`-only jatuh ke `__invoke` missing = + runtime `BadMethodCall`. Reklasifikasi fork-only temporary-abstract + post-swap grep/boot-smoke. + - **Composer replace-collision.** Fork & stock sama-sama `laravel/framework`; fork replace 27, + stock replace 37 `illuminate/*` → tak bisa co-install → swap-per-cluster butuh edit blok + `replace` fork (§1a). + - **Collections early = copy source, bukan composer require.** Fork replace `support` tapi tak + replace `collections`; real `illuminate/collections` collide `Support/Collection.php` fork + on-disk → introduce = copy source ke tree Support, real swap di window SCC-1. + - **Database hard-require** (dari review): `illuminate/database:^13` hard-require **hanya** + container/support/contracts/collections/conditionable/macroable; events/filesystem/console = + suggest. Klaim lama "drag pagination/http/queue/broadcasting" **dikoreksi** — itu bukan hard + require; swap-window tetap benar via namespace/replace. + - **Bus non-opsional.** `illuminate/events:^13` hard-require `illuminate/bus` → Bus wajib ikut + SCC-1 window (bukan SKIP/YAGNI seperti record lama). +- **Koreksi bucket dari re-klasifikasi (tetap berlaku):** + - Record lama menyebut Exception "nothing to migrate, effort low" — **SALAH untuk app**: 11 + `App::error` + custom ExceptionServiceProvider harus di-rewrite → medium. + - Record lama menyiratkan Queue "every job must become ShouldQueue" — **SALAH**: L13 KEEP + string-push + `fire()` default → bucket 1, ~2 closure site saja. + - Record lama Hashing "done, no imports" — **SALAH**: 4 site type-hint `HasherInterface` + (dihapus di 13) → bucket 2 kecil. +- **Keterbatasan**: klaim app-space (grep count) ditemukan agent di repo `dicoding` — verifikasi + ulang saat digarap. `.env`-specific config dir (`app/config/local/…`) harus direkonsiliasi + dengan model single-dir + `env()` L13 saat Foundation flip. Hard-require graph L2 (console→view + dst) berbasis metadata package v13 yang dikutip review; verifikasi ulang `composer.json` tiap + split-package saat swap. \ No newline at end of file diff --git a/MIGRATION-TASKS.md b/MIGRATION-TASKS.md index a2d0243c..66806af0 100644 --- a/MIGRATION-TASKS.md +++ b/MIGRATION-TASKS.md @@ -1,167 +1,382 @@ -# L42x → Laravel 13 — Task Backlog (kerjakan 1-per-1) +# L42x → Laravel 13 — Task Backlog (framework-first, kerjakan 1-per-1) -Turunan actionable dari `MIGRATION-ROADMAP.md`. **Cara pakai:** ambil task ber-nomor -terendah yang blocker-nya sudah ✅, kerjakan sampai "Done when" terpenuhi, centang, lanjut. -Tiap task **independen & shippable sendiri**. +Turunan actionable dari `MIGRATION-ROADMAP.md` (approach **framework-first / strict-mode-L13 / +enforcement-via-type**). **Cara pakai:** ambil task ber-nomor terendah yang blocker-nya sudah ✅, +kerjakan sampai "Done when" terpenuhi, centang, lanjut. Tiap task **independen & shippable sendiri**. **Kolom tempat:** `[fork]` = di repo L42x ini · `[app]` = di repo konsumen `dicoding` · `[ops]` = infra. **Effort:** T=trivial S=small M=medium L=large XL=x-large. - -> Prinsip: mayoritas task = **konvergensi sambil tetap di 4.2** (bikin app bicara idiom L13 -> lebih dulu). Pola berulang: **(a)** tambah shim/alias di `[fork]` yang menerima idiom -> lama & baru → **(b)** migrasi call-site di `[app]` ke idiom baru → **(c)** ratchet cegah -> yang lama balik. Mesin core baru benar-benar ditukar di **Wave 4 (flip)**. +**Bucket:** `B1`=fork-only · `B2`=app-unavoidable removal · `B3`=value-semantics. + +> **Pola berulang (STRICT-MODE-L13):** untuk tiap komponen — +> **(a)** `[fork]` perketat signature ke strict-mode-L13 (idiom lama → `TypeError`/method-not-found) +> → **(b)** `[app]` conform, **dipaksa PHP+Psalm** (hanya B2/B3; B1 nol app work) +> → **(c)** `[fork/ops]` swap komponen ke `illuminate/*` v13 asli (per-cluster; **edit blok `replace` +> fork tiap langkah** — fork & stock sama-sama `laravel/framework`, tak bisa co-install). +> Enforcement lewat **type** bila removal. **DUA pengecualian butuh guard non-type permanen:** +> **Cache-TTL** (stock 13 `$ttl=null` re-admit bare int → Psalm-rule/ratchet) dan **Console +> `handle()`** (stock resolve `handle|__invoke` → grep/boot-smoke). Ratchet CI **didemote** ke pola +> non-type + dua guard pasca-swap itu. --- -## Wave 0 — Enabler (sekali kerja, buka semua; kerjakan berurutan) +## Wave 0 — Enabler (sekali kerja) - [ ] **0.1 — WAF di edge** `[ops · S]` - - Why: tutup surface security core 4.2 yang tak terpatch **sekarang**; beli waktu buat sisa migrasi. - - Steps: pasang Cloudflare/AWS WAF di depan app; aktifkan ruleset OWASP + rate-limit; mode block. + - Why: tutup surface security core 4.2 tak terpatch **sekarang**; beli waktu. - Done when: trafik produksi lewat WAF; dashboard menampilkan hit rule. -- [x] **0.2 — Baseline inventory anti-pattern** `[fork ✅ / app ⬜ · S]` - - ✅ Baseline **fork** (`src/`) dibuat: `ci/convergence-baseline.txt` (real counts, 11 pola). - - ⬜ Sisa: generate baseline **app** `dicoding` → `ci/convergence-ratchet.sh --init ` di repo itu. - -- [x] **0.3 — Divergence ledger** `[fork · S]` ✅ - - Done: `docs/DIVERGENCE-LEDGER.md` — 11 pola (kiri→kanan + `key` ratchet) + gotcha manual (Cache TTL/Queue/Auth/Cookie/View/Redis) + aturan induk (extension point, monotonik). - -- [x] **0.4 — CI ratchet** `[fork · M]` ✅ - - Done & **teruji**: `ci/convergence-ratchet.sh` (mode check + `--init`); job `convergence-ratchet` di `.github/workflows/pull-request-check.yml`. - - Terverifikasi: lolos di baseline (exit 0); ❌ exit 1 saat pola 4.2 baru; ✅ lapor saat count turun. - - Sisa (app): copy skrip + `--init` baseline app; wire ke CI app. +- [ ] **0.2 — Demote ratchet CI ke pola non-type + guard pasca-swap** `[fork · S]` + - Why: enforcement pindah ke type (PHP+Psalm) **bila removal**. Ratchet tetap untuk yang **tak** + bisa jadi type, **plus** dua guard yang strictness fork-nya menguap saat swap. + - Steps: pangkas `ci/convergence-ratchet.sh` → sisakan pola: helper global `str_*`/`array_*`, + SQL string-concat/`whereRaw`, route-array `'before'=>`, `App::error(`/`['exception']` binding, + **`Cache::put/add/remember` dengan arg TTL bare-int** (guard permanen §7D), **`function fire(` + di command app** (Console fire-only pasca-swap §7D). + - Done when: ratchet hanya cek pola non-type + 2 guard pasca-swap; sisanya dihapus dari daftar. + +- [ ] **0.3 — Pastikan Psalm jalan di CI app + siapkan 2 custom rule** `[app · S]` + - Why: enforcement statis butuh Psalm hijau di `dicoding` (sudah ada) sebagai baseline; 2 pola + non-self-liquidating perlu Psalm-rule permanen. + - Steps: catat baseline Psalm; siapkan (draft) custom Psalm-rule/plugin: (1) bare-int arg pada + `Cache::put/add/remember` = error; (2) command class tanpa `handle()` = error. Rule aktif penuh + saat komponen di-tighten (2.2 / 2.6). + - Done when: Psalm baseline app tercatat; 2 rule ter-draft; siap menangkap `UndefinedMethod`/`TooManyArguments`. --- -## Wave 1 — Sweep app aman (jalan di 4.2, ROI tertinggi, urutan bebas) - -> Tiap task menghapus satu **gotcha senyap / rename** sebelum flip. Bisa dicicil paralel. - -- [ ] **1.1 — 🔴 Cache TTL unit-safe** `[app(+fork) · M]` - - Why: L13 TTL numerik = **detik** (4.2 = menit) → `Cache::put(...,N)` jadi **60× lebih pendek, senyap**. - - Steps: grep `Cache::(put|add|remember|remember)\(` + `->put/add/remember` dengan arg numerik → - ganti ke **interval `Carbon`/`DateTime`** (unit-safe di kedua versi), mis. `now()->addMinutes(N)`. - - Done when: nol TTL numerik telanjang; ratchet pola `->(put|add|remember)\([^,]+,[^,]+,\s*\d+` = 0. - -- [ ] **1.2 — Eloquent idiom L13-safe** `[app · L]` - - Why: `->lists()`, `ArrayableInterface`, `SoftDeletingTrait`, magic `->lists` hilang/pindah di L13. - - Steps: `->lists('c')`→`->lists('c')` tetap? tidak → pakai `->pluck('c')` bila tersedia, atau helper app; - `SoftDeletingTrait`→`SoftDeletes` (cek ketersediaan di fork); `ArrayableInterface`→`Arrayable` alias. - - Done when: grep `->lists(|SoftDeletingTrait|ArrayableInterface` = 0 di app. - -- [ ] **1.3 — Query pakai binding (bukan concat)** `[app · M]` - - Why: tutup SQL injection **+** konvergensi (surface core yang tak keurus). - - Steps: grep `whereRaw|DB::raw|"\$` dalam query builder → ubah ke binding `where('c',$v)` / `?` params. - - Done when: audit selesai; sisa `whereRaw` terdaftar & dijustifikasi. - -- [ ] **1.4 — Events `fire()` → `dispatch()`** `[fork+app · S]` - - Why: L13 rename `fire()`→`dispatch()`; `firing()` dihapus (21 callsite internal + app). - - Steps: `[fork]` tambah alias `dispatch()` di `Events\Dispatcher` (delegasi ke `fire`) → `[app]` migrasi callsite → deprecate `fire()`. - - Done when: app pakai `dispatch()`; ratchet blok `->fire(` baru. - -- [ ] **1.5 — `Config::getEnvironment()` callsites** `[app · S]` - - Why: method **dihapus** di L13 tapi dipakai app (`pixel.blade.php`, `sentry.blade.php`, dll). - - Steps: buat helper app-space `app_env()` (baca `config('app.env')`/`App::environment()`) → ganti semua `Config::getEnvironment()`. - - Done when: grep `getEnvironment(` di app = 0. - -- [ ] **1.6 — Route definition style** `[app · S]` - - Why: `['uses'=>'C@m']` → `[C::class,'m']` (valid di 4.2 **dan** 13); siapkan model binding. - - Steps: sweep route files ke callable-array; hindari string `C@m`. - - Done when: ratchet blok `'uses'\s*=>` baru. - -- [ ] **1.7 — Pagination getter alias** `[fork+app · S]` - - Why: getter di-rename massal (`getCurrentPage`→`currentPage`, `getLastPage`→`lastPage`, `getFrom`→`firstItem`, `getTo`→`lastItem`, `getTotal`→`total`, `getPerPage`→`perPage`). - - Steps: `[fork]` tambah alias L13-named di Paginator → `[app]` migrasi template/controller ke nama baru. - - Done when: template pakai nama L13; ratchet blok `get(CurrentPage|LastPage|From|To|Total|PerPage)\(`. - -- [ ] **1.8 — `Arr::`/`Str::` L13 signature** `[app · S]` (sudah dimulai) - - Why: lanjutkan `array_first/last`→`Arr::first/last`; helper global → `Arr::`/`Str::`. - - Done when: ratchet `array_(first|last)\(` = 0; helper 4.2-only ter-daftar. +## Wave 1 — PRASYARAT: split monolith + Contracts + replace-map (buka semua; urut wajib) + +> Ini yang **memutus SCC-1** dan memungkinkan pola berulang jalan. Kerjakan berurutan. + +- [ ] **1.0 — Split fork monolitik → struktur `illuminate/*`-shaped + replace-map** `[fork · L]` + - Why: real `illuminate/*` v13 = banyak package; namespace `Illuminate\*` collide **DAN** fork & + stock sama-sama `laravel/framework` yang `replace` illuminate/* → tak bisa co-install → swap + **per-cluster** + edit `replace` tiap langkah. + - Steps: (1) petakan `src/Illuminate/*` fork ke boundary package v13 (support/collections/macroable/ + conditionable/reflection dst) — **namespace tetap**, siapkan composer split-map. Jangan bikin dir + baru untuk leaf yang di stock 13 tetap di tree Support (Conditionable/Reflector). (2) **Dokumentasikan + `replace`-map (§1a):** 27 entry `replace` fork → package v13 target + urutan pembukaan (SCC-1 + cluster dulu, lalu L2 per-unit, lalu SCC-2). Catat: entry dihapus **saat** cluster-nya di-swap + (Wave 4), bukan sekarang. Catat 6 package yang fork **tak** `replace` tapi stock `replace` + (contracts/collections/macroable/conditionable/reflection/pipeline) → introduce = copy source. + - Done when: peta split + `replace`-map + urutan pembukaan terdokumentasi; tiap komponen fork dipetakan ke package v13 target. + +- [ ] **1.1 — Introduce `Contracts`** `[fork · M]` `B1` · blocked-by: 1.0 + - Why: `Contracts` **tak ada di tree fork**; **memutus SCC-1**. Prasyarat semua rename namespace. + - Steps: copy `src/Illuminate/Contracts/*` verbatim dari `../framework` (semua 33 subdomain); + composer require **PSR-only** (⚠️ **JANGAN** tambah `illuminate/*` → false cycle dari 5 type-hint cross-import). + Fork tak `replace` contracts → aman. + - Enforcement: **by presence** — begitu komponen type-hint `Contracts\X`, PHP+Psalm tangkap consumer non-conform. + - Done when: `Illuminate\Contracts\*` tersedia; komponen bisa refer interface. + +- [ ] **1.2 — Introduce leaf traits/util (COPY SOURCE, fisik di tree Support)** `[fork · S]` `B1/B2` · blocked-by: 1.1 + - Steps: `Macroable` (rename `MacroableTrait`→`Macroable`, `B2`); `Conditionable` + + `HigherOrderWhenProxy` verbatim (`B1`); `Reflector` superset + `ReflectsClosures` (`B1`). + **Copy SOURCE ke tree Support fork** — **JANGAN** `composer require illuminate/macroable|conditionable` + (fork tak replace-nya, collide dengan on-disk Support). **Jangan** bikin `src/Illuminate/{Conditionable,Reflection}` dir. + - Enforcement: `use ...\MacroableTrait` → trait-not-found (PHP+Psalm). + - Done when: idiom `->when()/::macro()`/`Reflector` tersedia dengan shape L13; ~4 site internal `use MacroableTrait` di-rename. + +- [ ] **1.3 — Introduce `Collections` shape (COPY SOURCE ke tree Support fork)** `[fork · L]` `B1` · blocked-by: 1.1, 1.2 + - Why: `Collection` dipisah dari Support di v13 (namespace `Illuminate\Support\Collection` tetap). + **Fork tak `replace` collections**, dan real `illuminate/collections:^13` mengirim + `Illuminate\Support\{Collection,Arr,Enumerable}` yang **collide** dengan `src/Illuminate/Support/Collection.php` + fork on-disk → **TIDAK BISA `composer require illuminate/collections` sekarang**. + - Steps: **copy SUPERSET SOURCE** ke tree Support fork: tambah `EnumeratesValues`/`Enumerable`/ + `LazyCollection`/`Arr` standalone/`HigherOrderCollectionProxy`. Real package swap **nanti di + window SCC-1** (saat Support fork di-copot, §4.2). + - Enforcement: **bukan type** (superset) → **behavior-parity test** (`random/first/groupBy/sortBy` edge). + - Done when: surface Collection = L13 (source di tree fork); parity test hijau. (Real `illuminate/collections` swap ditunda ke 4.2.) + +- [ ] **1.4 — Alihkan Facade/binding konkret → interface Contracts** `[fork · L]` `B1` · blocked-by: 1.1 + - Why: memutus SCC-1 (`Support\Facades\Response`→Http konkret, `CapsuleManagerTrait`→Container konkret). + - Steps: Facade resolve via container/kontrak; binding type-hint `Contracts\*`. + - Done when: grep SCC-1 cycle (Support→Http/Container konkret) hilang. --- -## Wave 2 — Extract & decouple (per-modul, oportunistik) - -- [ ] **2.1 — Ekstrak `Html` (Form/HTML) ke package app-space** `[fork→app · L]` - - Why: `illuminate/html` dihapus di L13; **~497 file view** pakai `Form::`/`HTML::`. - - Steps: pindah `Illuminate\Html` ke package internal (mis. `dicoding/html`) yang mempertahankan facade `Form`/`HTML` **verbatim**; bind ke Session/Request app; daftarkan provider. - - Done when: view render identik lewat package; `illuminate/html` tak lagi dari core. - -- [ ] **2.2 — Tarik logika bisnis keluar framework** `[app · XL, continuous]` - - Why: kode framework-agnostic **selamat dari flip** → flip jadi murah. - - Steps: tiap sentuh controller/model gemuk, ekstrak logika ke **service/class PHP polos** (tanpa Eloquent/facade/Request di domain). Ratchet ukur % file domain lepas-framework. - - Done when (per-modul): logika modul X ada di service teruji unit; controller tipis. - -- [ ] **2.3 — Pusatkan rule validasi ke FormRequest-like** `[app · M]` - - Why: 1 tempat per form → migrasi ke `FormRequest` L13 murah. - - Steps: pindah rule inline controller ke class request app-space. - - Done when: controller tak lagi punya `Validator::make` inline untuk form utama. - -- [ ] **2.4 — Route filters → middleware** `[fork+app · L]` - - Why: filter **dihapus total** di L13; wajib jadi middleware **sebelum** flip. - - Steps: tiap `Route::filter`/`->before/->after`/`filters.php` → class middleware-shaped; `[fork]` sediakan jalur middleware kompatibel bila perlu. - - Done when: grep `Route::filter|->before\(|->after\(` = 0; semua jadi middleware. +## Wave 2 — Strict-mode tighten (bucket 2/3; app conform via PHP+Psalm; urutan bebas) + +> Tiap task: `[fork]` perketat → `[app]` migrasi (dipaksa type). Bisa dicicil paralel setelah Wave 1. + +> **Catatan:** bekas **2.1 (app-sweep cache) DIGABUNG ke 2.2** — di framework-first, konversi call-site +> app adalah *konsekuensi* dari fork-tighten (dipaksa type), bukan task berdiri sendiri. Draft app +> sempat dibuat lalu **di-drop** (2026-09-10); recoverable: `git cherry-pick 9edeba6f52b`. + +- [ ] **2.2 — 🔴 Cache: tighten TTL param di fork → app dipaksa konform + guard permanen** `[fork+app · M]` `B3` · blocked-by: 1.1, 0.3 + - Why: L13 baca int TTL = **detik**, fork 4.2 = **menit** → bare int senyap **60× lebih pendek** + pasca-swap. Interval absolut (`Carbon`) aman di kedua versi. Fork-tighten = *driver* yang **memaksa** + migrasi app; strictness fork **menguap saat swap** (stock `Repository.php:367`/`Contracts/Cache/Repository.php:29` + `$ttl=null`, `getSeconds():903` re-admit bare int) → guard permanen = **Psalm-rule/ratchet** (0.3), bukan type. + - Steps: `[fork]` di `src/Illuminate/Cache/Repository.php` `put()/add()/remember()` (line 88/106/124) + + mirror di `TaggedCache`, ganti `$minutes` untyped → `DateTimeInterface|DateInterval $ttl`; drop + `getMinutes()/getDefaultCacheTime()`. Relokasi `StoreInterface`→`Contracts\Cache\Store` (alias sementara). + **WAJIB di commit yang sama — konversi fork-internal bare-int caller** atau fork tak boot (verified): + `Session/CacheBasedSessionHandler.php:60` `put($id,$data,$this->minutes)` → interval; + `Cache/ArrayStore.php:74` & `Cache/ApcStore.php:94` `put($key,$value,0)` (dari `forever()`) → + `DateInterval`/null forever-path. + - Steps `[app]` (DIPAKSA oleh type di atas): bare-int TTL → `Carbon::now()->addMinutes(N)`. Cakupan + verified: **NullableCache fix pusat** (menutup 54 caller via param `int $cacheDurationInMinutes`) + + **6 inject raw `Repository`** + **6 call-site facade**. 2 FAQ `3600*24*3` (=180 hari) = latent bug lama + → **pertahankan + flag** (bukan diubah di sini). + - Steps `[ci/psalm]`: aktifkan Psalm-rule bare-int TTL (0.3) — **guard PERMANEN** (strictness fork menguap saat swap). + - Enforcement: fork = bare int `TypeError`+Psalm (memaksa app). **Pasca-swap = Psalm-rule/ratchet + (permanen)** — stock 13 re-admit bare int. + - Done when: bare-int TTL tak type-check di fork; fork boot hijau (3 internal caller dikonversi); + app dikonversi & Psalm app hijau; Psalm-rule bare-int aktif & hijau. + +- [ ] **2.3 — Cache: 5 custom Store → contract baru** `[app · S]` `B2` · blocked-by: 2.2 + - Steps: 5 kelas app-space yang implement `StoreInterface` → `Illuminate\Contracts\Cache\Store` + (tambah `many()/putMany()` bila perlu). Alias mati saat swap. + - Done when: grep `StoreInterface` di app = 0; custom store implement contract. + +- [ ] **2.4 — Events `fire()` → `dispatch()`** `[fork+app · M]` `B2` · blocked-by: 1.1 + - Steps: `[fork]` `Events\Dispatcher`: add `dispatch()` canonical + `fire()` deprecated alias; + rename `queue()`→`push()`, `forgetQueued()`→`forgetPushed()`; drop `$priority` (line 62); + implement L13 `DispatcherContract`. Lalu hapus `fire()/queue()/firing()/forgetQueued()`. + `[app]` ~96 `Event::fire()`/`->fire()` → `dispatch()`; review 118 `listen()` (wildcard payload). + - Enforcement: post-hapus, `fire()` = method-not-found; 3-arg `listen()` = ArgumentCountError. + - Note swap: `illuminate/events:^13` **hard-require** `illuminate/bus` → Bus wajib ikut window SCC-1 (bukan skip). + - Done when: grep `->fire(` app = 0; 118 `listen()` di-review; Psalm hijau. + +- [ ] **2.5 — Database: `lists()`/`pluck`-swap/`SoftDeletes`/`Arrayable`** `[fork+app · XL]` `B2(+B3)` · blocked-by: 1.1 + - Steps (urut aman): `[fork]` (1) hapus `lists()` dari Query+Eloquent Builder+Collection; rename + single-value `pluck($column)`→`value($column)`; tambah L13-semantics `pluck($column,$key=null)` + (= body `lists()` lama). (2) rename `SoftDeletingTrait`→`SoftDeletes`, `ScopeInterface`→`Scope` + (hapus nama lama, **no alias** — mau fatal). (3) Model implement `Contracts\Support\{Arrayable,Jsonable}`. + `[app]` ~173 `->lists()`→`->pluck()`; ~193 `->pluck()` (single) → `->value()`; ~21 SoftDelete/Arrayable rename. + - Enforcement: `lists()`/`pluck(single)` = method-not-found (type-caught). **pluck-swap makna = + NOT type-expressible** → grep/Psalm-rule + human review cegah "fix" balik ke `pluck()` salah makna. + - Done when: grep `->lists(|SoftDeletingTrait|ArrayableInterface` app = 0; pluck/value review selesai; Psalm hijau. + +- [ ] **2.6 — Console `fire()` → `handle()` + `$signature` (fork temp-abstract)** `[fork+app · L]` `B2` · blocked-by: 1.1, 0.3 + - Why koreksi: **stock 13 BUKAN abstract** — `Command.php:289` `method_exists($this,'handle')?'handle':'__invoke'` + → `fire()`-only jatuh ke `__invoke` missing = runtime `BadMethodCall`, **bukan** unimplemented-abstract/Psalm. + Maka fork bikin `handle()` **abstract sementara** hanya untuk **me-ratchet ~308 rewrite**; guard + pasca-swap = **grep `function fire(` + boot-smoke** (0.2), bukan type. + - Steps: `[fork]` `Console/Command.php` `execute()` → panggil `abstract public function handle()` + (drop `fire()` indirection, **catatan: strictness ini menguap saat swap ke stock**); hapus reader + `getArguments()/getOptions()` (keep `$signature` parsing); hapus `Application::make/start/ + setExceptionHandler/renderException/setAutoExit`; ctor `(Container,Dispatcher,version)`; update + `command.stub`. `[app]` ~308 `fire()`→`handle()` (Rector rule Laravel fire→handle), ~87 + getArguments/getOptions→`$signature`, pindah `artisan.php` manual reg → Kernel command registration. + `[ci]` aktifkan grep `function fire(` di command app (0.2). + - Enforcement: **fork sementara** = subclass hanya-`fire()` unimplemented-abstract fatal (ratchet). + **Pasca-swap = grep + boot-smoke** (stock jatuh ke `__invoke`). + - Done when: grep `function fire(` app command = 0; artisan.php → Kernel; boot-smoke tiap command hijau; Psalm hijau. + +- [ ] **2.7 — Config `getEnvironment()` → `App::environment()`** `[fork+app · M]` `B2` · blocked-by: 1.1 + - Steps: `[fork]` `Config/Repository.php` hapus `getEnvironment()` (line 346) + loader/package-cascade + (`getLoader/setLoader/package/afterLoading/addNamespace/hasGroup`); ctor `(LoaderInterface,$env)`→`(array)`; + implement `Contracts\Config\Repository`; add typed getter `string()/integer()/…`. `[app]` ~48 site + `getEnvironment()`→`App::environment()` (incl. blade `pixel/sentry/payment`). + - Enforcement: `getEnvironment()` = method-not-found; ctor pass loader = TypeError. + - Done when: grep `getEnvironment(` app = 0; Psalm hijau. + +- [ ] **2.8 — Routing filters → middleware** `[fork+app · XL]` `B2` · blocked-by: 1.1, 1.2 (Pipeline) + - Steps: `[fork]` (1) **dulu** introduce `Http\Kernel` + middleware dispatch (Pipeline) agar middleware + jalan saat filter masih ada. (2) hapus `Router::filter/before/after/when/whenRegex/callFilter` + + `RouteFiltererInterface`, `controller()/controllers()`, drop `HttpKernelInterface`. Keep + `UrlGenerator`/`Redirector` (identik L13). `[app]` ~52 `Route::filter` (141-line filters.php) → + middleware; 131 route-array `'before'/'after'=>` → `->middleware()`. + - Enforcement: `Route::filter()/->before()/->after()` = method-not-found. Route-array `'before'=>` + (array key) = **NOT type** → **grep/Psalm-rule** (§7D). + - Note swap: `illuminate/routing:^13` **hard-require** `illuminate/session` (SCC-1) → routing swap butuh SCC-1 selesai. + - Done when: grep `Route::filter|->before\(|->after\(` app = 0; route-array grep bersih; boot hijau. + +- [ ] **2.9 — Encryption: strict-mode contract** `[fork+app · S]` `B2` · blocked-by: 1.1 + - Steps: `[fork]` hapus `setKey()`; implement `Contracts\Encryption\{Encrypter,StringEncrypter}`; + relokasi `DecryptException/EncryptException` ke `Contracts\Encryption` (`class_alias` bridge); + throw `EncryptException` (bukan `\RuntimeException`); drop dead Symfony import + `symfony/security-core`. + `[app]` optional re-hint ke Contract (0 forced). `[ops]` pin `config['cipher']=AES-256-CBC` + `previous_keys`. + - Enforcement: `setKey()` = method-not-found. **cipher default = config assertion / boot-guard, bukan type**. + - Done when: `setKey` di app = 0 (sudah); cipher config pinned; Psalm hijau. + +- [ ] **2.10 — Filesystem `FileNotFoundException` → Contracts** `[fork+app · S]` `B2` · blocked-by: 1.1 + - Steps: `[fork]` relokasi ke `Contracts\Filesystem\FileNotFoundException` + `class_alias` dari FQCN lama + (`@deprecated` → Psalm `DeprecatedClass`); provider `bindShared('files')`→`singleton`. `[app]` repoint + 11 import produksi (aliasable/deferrable). **Jangan** introduce Storage/flysystem di sini. + - Enforcement: alias jaga catch lama; `@deprecated` surface 11 import untuk dimigrasi. + - Done when: 11 import repoint (atau alias jalan); Psalm hijau. (Catatan: `DicodingUtils\...FileNotFoundException` app **tak** tersentuh.) + +- [ ] **2.11 — Hashing `HasherInterface` → contract** `[fork+app · S]` `B2` · blocked-by: 1.1 + - Steps: `[fork]` `HasherInterface` extend `Contracts\Hashing\Hasher` (bridge); rebind `'hash'` ke + HashManager; register contract. `[app]` 4 site (`ApplicationGateway`, `UserPasswordUpdateSpecification` + + 2 test) → `Illuminate\Contracts\Hashing\Hasher`. `[ops]` verify `hashing.driver`=bcrypt fallback. + - Enforcement: setelah delete `HasherInterface`, hint/`make(HasherInterface::class)` = unresolvable. + - Done when: 4 site migrasi ke contract; Psalm hijau. + +- [ ] **2.12 — Cookie/Session: widen jar + siapkan middleware** `[fork · M]` `B2` · blocked-by: 1.1, 2.9 + - Steps: `[fork]` **Cookie**: widen `queued/hasQueued/unqueue/setDefaultPathAndDomain` (additive optional + param); add `CookieValuePrefix`; siapkan `Middleware/EncryptCookies`+`AddQueuedCookiesToResponse` + (constructor-typed `Contracts\Encryption\Encrypter`) — **rewrite Guard/Queue decorator → middleware + flip-time-coupled**. **Session**: Store `implements Contracts\Session\Session`, ekstrak bag ke + `SymfonySessionDecorator` (hapus `registerBag/getBag/...`); siapkan `Middleware/StartSession`+ + `AuthenticateSession`; widen `flash/put/get` key `UnitEnum|string`. + - Enforcement: `implements` Symfony `SessionInterface` / bag call = TypeError/method-not-found (app: 0). + - Done when: jar/store shape = L13; middleware disiapkan (aktif saat kernel flip); app data-API tak tersentuh. + +- [ ] **2.13 — Log: `Writer`→`Logger`, `getMonolog`→`getLogger`** `[fork+app · M]` `B1(+B2 tail)` · blocked-by: 1.1, 2.7(Config) + - Steps: `[fork]` rename class `Writer`→`Logger`, `getMonolog()`→`getLogger()`; `write($level,$message,array $context=[]):void`; + hapus `useFiles/useDailyFiles/useErrorLog` (pindah ke LogManager); bind `'log'` ke LogManager singleton; + ctor `(Psr\Log\LoggerInterface, Contracts\Events\Dispatcher)`. `[app]` 3 `getMonolog()` caller + (`CoreLogServiceProvider` :30/:41 + test) → `getLogger()` **+ rewrite Monolog 1→3** (Level enum, handler API). + `[ops]` author `config/logging.php` channels. + - Enforcement: `getMonolog()`/`use Writer` = not-found. **Monolog API di caller = NOT type** → manual. + - Done when: 335 `Log::` PSR-3 utuh; 3 getMonolog caller di-rewrite; Psalm hijau. + +- [ ] **2.14 — View: hapus named-view sugar + rename internal** `[fork+app · S]` `B1` · blocked-by: SCC-1 (nanti) + - Steps: `[fork]` hapus `Factory::of()/name()/alias()` (line 153/165/177); rename `flushSections`→`flushState`. + **Keep** `BladeCompiler::extend()/createMatcher()` (jangan strip pre-swap). `[app]` 2 `Blade::extend` + (`DicodingServiceProvider` :180/:186) → `Blade::directive()`. `[ops]` flush `storage/framework/views` di swap. + - Enforcement: `of/name/alias` = method-not-found. **`Blade::extend` callback = NOT type** → manual/grep. + - Done when: 2 Blade::extend → directive; Psalm hijau. + +- [ ] **2.15 — Pagination: rename getter + reshape Factory/Presenter** `[fork+app · M]` `B2` · blocked-by: 1.1 + - Steps: `[fork]` rename `getCurrentPage`→`currentPage` (+ lastPage/firstItem/lastItem/total/perPage), + `getItems()`→`items()`; hapus Factory object + Presenter/BootstrapPresenter; ctor Paginator drop `$factory`. + `[app]` `PaginationPresenter.php` → Blade view; `Factory::class->setCurrentPage` (`ParentComments...` :83) + → `currentPageResolver`; 2 `new Paginator($factory,...)` test; sweep `->getItems()`→`items()`. + `[ops]` `useBootstrapFour/Five()` untuk approx markup (slider view tak ada di L13). + - Enforcement: getter lama = method-not-found; `extends Presenter`/`Factory::class` = class-not-found; `new Paginator($factory)` = TypeError. + - Done when: 3 site reshape + items() sweep; `->links()` (164) utuh; Psalm hijau. + +- [ ] **2.16 — Mail: retype send/queue + drop closure-queue** `[fork+app · L]` `B2` · blocked-by: 1.1, Queue L13-shaped + - Steps: `[fork]` introduce `Mailable/PendingMail/SendQueuedMailable`; `send():?SentMessage`; `queue()/later()` + first param `Mailable`; hapus `failures()` + `SerializableClosure` `mailer@handleQueuedMessage`; split ke MailManager. + `[app]` ~17 closure/callback (Mail::send 13 + Mail::queue 4) → Mailable class; drop 3 `isPretending/failures`. + `[ops]` `mail.php` → `mailers.*`. + - Enforcement: closure-queue = method-not-found; `queue(Closure)` = TypeError; `send()` void→?SentMessage (Psalm). + - Done when: ~17 site → Mailable; Psalm hijau. (Depends: Queue L13-shaped untuk SendQueuedMailable.) + +- [ ] **2.17 — Testing: rewrite ~500 file test ke fluent TestResponse** `[fork+app · L]` `B2` · blocked-by: SCC-1, Foundation-shape + - Steps: `[fork]` (1) introduce `Illuminate\Testing\TestResponse` (75 assert) + `Fluent\AssertableJson` + + L13 `Foundation TestCase` (return `Illuminate\Http\Response`). (2) hapus legacy `assertResponseOk/ + assertResponseStatus/assertRedirectedTo/see()/$this->call()` (BrowserKit) dari `Foundation\Testing`. + `[app]` ~500 file test → fluent `$response->assert*`. + - Enforcement: `assertResponseOk` dll = method-not-found (PHP+Psalm), ratcheting ~500 file. + - Done when: grep `assertResponseOk|assertResponseStatus|->call(` di test = 0; suite hijau di PHPUnit 11/12. --- -## Wave 3 — Shim fondasi L13 (bottom-up STRICT — urutan wajib) - -> Ini yang **memutus cycle** & membuka core. Kerjakan berurutan; tiap task blocker task berikut. - -- [ ] **3.1 — Introduce `Contracts`** `[fork · M]` - - Why: `Contracts` **tak ada di pohon 4.2**; memutus SCC-1 (import konkret → interface). Prasyarat semua rename namespace. - - Steps: tambah `Illuminate\Contracts\*` (interface) sesuai L13; alias interface lama (`Support\Contracts\ArrayableInterface`→`Contracts\Support\Arrayable`). - - Done when: kontrak inti tersedia; Facade/komponen bisa refer interface. - -- [ ] **3.2 — Promote `Macroable` + add `Conditionable`/`Pipeline` shims** `[fork · S]` · blocked-by: 3.1 - - Steps: `MacroableTrait`→`Macroable` (alias lama); tambah `Conditionable` (`when/unless`), `Pipeline` shim signature L13. - - Done when: idiom `->when()/::macro()` tersedia dengan tanda tangan L13. +## Wave 3 — App-space extract & de-sugar (yang tak punya padanan v13; urutan bebas) + +- [ ] **3.1 — Ekstrak `Html` (Form/HTML) ke package app-space** `[fork→app · M]` `B2` · blocked-by: Routing/Session/Support shape + - Why: `illuminate/html` **tak ada padanan** di L13; ~484 blade pakai `Form::`/`HTML::`. Preserve API verbatim. + (Catatan: fork `replace` `illuminate/html` → saat Fase-5 drop dari `replace`; tak ada real package pengganti.) + - Steps: lift `FormBuilder/HtmlBuilder/HtmlServiceProvider` verbatim ke package app-space (namespace app), + keep binding `'html'/'form'` + facade alias; fix 3 mekanis: `bindShared()`→`singleton()`, + `MacroableTrait`→`Macroable`, rebind `session.store` ke Session L13. + - Enforcement: **bukan type** (API preserved) → **boot-smoke / 1 integration test** render `Form::open`. + - Done when: 484 blade render identik; `illuminate/html` tak dari core. + +- [ ] **3.2 — CachedRouting: de-sugar 28 `Route::cache()`** `[fork+app · S]` `B2` · blocked-by: Routing L13-shaped + - Steps: `[fork]` hapus `Router::cache()/clearCache()` + subtree CachedRouting + rebinding provider. + `[app]` 28 `Route::cache(__FILE__, fn)` (app/routes.php + 24 file app/routes/) → de-sugar ke registrasi biasa; + pastikan **tak ada** Closure-based route (native `route:cache` menolak). `[ops]` `php artisan route:cache` di pipeline deploy. + - Enforcement: `Route::cache()` = method-not-found (28 site menyala). + - Done when: grep `Route::cache(` app = 0; `route:cache` masuk pipeline. + +- [ ] **3.3 — Tarik logika bisnis keluar framework** `[app · XL, continuous]` + - Why: kode framework-agnostic **selamat dari swap** → swap murah. + - Steps: tiap sentuh controller/model gemuk, ekstrak logika ke service/class PHP polos (tanpa Eloquent/facade di domain). + - Done when (per-modul): logika modul X di service teruji unit; controller tipis. -- [ ] **3.3 — `Support`: Arr/Str/Collection ke signature L13 + pisah Collection** `[fork · L]` · blocked-by: 3.1 - - Why: choke point + biang SCC-1; **alihkan Facade dari import konkret ke kontrak**. - - Steps: `Collection` API L13; Facade resolve via container/kontrak (bukan `use Http`/`use Container` konkret) → **SCC-1 putus**. - - Done when: grep SCC-1 cycle (Support→Http/Container konkret) hilang; Collection sig = L13. - -- [ ] **3.4 — `Session` shim atas Symfony HttpFoundation** `[fork · M]` · blocked-by: 3.1 - - Why: kandidat shim terbaik (kontrak=storage). `implements` pindah Symfony→`Contracts\Session\Session`. - - Steps: bungkus HttpFoundation Session di balik API `Store` Illuminate; hati-hati semantik flash & CSRF token. - - Done when: `Session::get/put/flash/token` jalan via shim; test sesi hijau. +--- -- [ ] **3.5 — Config/Log/Filesystem konvergensi ringan** `[fork · M]` · blocked-by: 3.1 - - Steps: Config ctor `(loader,env)`→terima `array`; Log wiring imperatif→channel-ready; Filesystem siap Flysystem-shaped. - - Done when: ketiganya expose API L13-shaped tanpa ganti mesin. +## Wave 4 — Swap bottom-up per cluster (§6 langkah c; urut wajib; tiap swap = edit `replace` fork §1a) + +> Namespace `Illuminate\*` collide + fork/stock sama-sama `laravel/framework` yang `replace` +> illuminate/* → **tak bisa co-install**. Swap **per-cluster**: tiap langkah **hapus entry dari +> blok `replace` fork** lalu `composer require illuminate/:^13`. Interim = fork + +> `illuminate/:^13` individual. Prasyarat tiap swap: Wave 1–2 komponen terkait ✅. + +- [ ] **4.1 — SCC-1 core cutover (satu window)** `[fork · XL]` · blocked-by: 1.1–1.4, 2.2, 2.4, 2.5, 2.9, 2.10, 2.12, 2.13 + - Cluster: `contracts+reflection+support+container` (jangkar) + `http, session, cache, cookie, + encryption, events, filesystem, redis, database` + **`bus`** (transitif hard-require `events`). + Swap ke `illuminate/*` v13 **bersamaan**. + - Composer (§1a): hapus ~12 entry dari blok `replace` fork (support/container/http/session/cache/ + cookie/encryption/events/filesystem/redis/database + html **tetap** sampai 3.1); `composer require` + `illuminate/{contracts,collections,macroable,conditionable,reflection,pipeline,support,container, + http,session,cache,cookie,encryption,events,bus,filesystem,redis,database}:^13`. Contracts/ + collections/macroable/conditionable/reflection/pipeline (yang fork tak replace, dulu copy-source) + swap ke real package **di sini** (Support fork di-copot → tak lagi collide). + - Prasyarat: PHP 8.3 runtime; helper-shim `str_*`/`array_*` survive di Support sampai swap; Redis + client config; Encryption cipher pinned; Cache Psalm-rule bare-int aktif (guard pasca-swap). + - Done when: SCC-1 komponen di classpath v13; entry `replace` cluster terhapus; boot hijau; app test hijau. + +- [ ] **4.2 — Swap `console` ke illuminate/console v13** `[fork · M]` · blocked-by: 2.6, 4.1, Foundation Kernel wiring + - Note koreksi: **console BUKAN L0** — `illuminate/console:^13` **hard-require** `illuminate/view` + (menyeret container/events/filesystem/session/support) → **butuh SCC-1 (4.1) selesai**. Namespace + collide → atomik; butuh app fire()→handle() (2.6) + Kernel; hapus `console` dari `replace` fork. + - Done when: console v13 di classpath; boot-smoke tiap command hijau; app test hijau. + +- [ ] **4.3 — Swap L2 per-unit** `[fork · L]` · blocked-by: 4.1, 4.2 + - Urut: `config` (2.7) → `log` (2.13) → `translation` → `hashing` (2.11) → `view` (2.14) → + `validation` → `routing` (2.8; hard-require session/SCC-1 ✅) → `queue` (hard-require + database+console ✅). Tiap swap per-unit: hapus entry `replace` fork → `composer require illuminate/:^13`. + - Done when: tiap komponen L2 di classpath v13; entry `replace` terhapus; app test hijau. + +- [ ] **4.4 — Delete evaporate components** `[fork · S]` · blocked-by: sesuai + - `Workbench` (hapus dir + entry `replace`), `Exception` (merge ke Foundation Handler; fork tak + replace exception? — verified fork **replace** `illuminate/exception` → hapus entry saat merge), + `CachedRouting` (3.2 selesai), `Html`-in-core (3.1 selesai; hapus `illuminate/html` dari `replace`). + **Jangan port.** + +- [ ] **4.5 — SCC-2 terminal (the flip): foundation+mail+auth** `[fork+app · XL]` · blocked-by: 4.1, 4.3, 2.16, 2.17 + - Steps: putus cycle (Mail via Mailer contract, Auth via Contracts) → swap `foundation+mail+auth` ke v13. + `auth` **hard-require** `queue` (L2 ✅ di 4.3). `[app]` bootstrap rewrite (`start.php`→`bootstrap/app.php` + builder, `App::error`→`withExceptions`, Kernel middleware arrays), testing base swap. `[ops]` + **dual-read Auth** (§8). Composer: hapus `foundation/mail/auth` dari `replace` fork → require real. + - Enforcement: `App::error/missing/fatal/down` = method-not-found; **bootstrap baru = grep/boot-smoke** (§7D). + - Done when: foundation+mail+auth di classpath v13; entry `replace` terhapus; app boot & test hijau. --- -## Wave 4 — The Flip (cutover terjadwal — sekali, berurutan) - -> Prasyarat: Wave 1–3 mayoritas ✅. Ini jam-jaman/hari, bukan bulan. +## Wave 5 — Data cutover + fork = 0 -- [ ] **4.1 — Skeleton L13 stock** `[app · M]`: buat app L13 stock; port config → `.env`/`config()`. -- [ ] **4.2 — Port app code + package app-space** `[app · L]`: pindah kode (sudah idiom L13) + package Html/domain. -- [ ] **4.3 — 🔴 Data cutover** `[ops · M]`: - - **Drain antrian** SQS/Redis sampai kosong (wire-format L42x tak terbaca worker L13). +- [ ] **5.1 — 🔴 Data cutover** `[ops · M]` · blocked-by: 4.5 + - **Drain antrian** SQS/Redis sampai kosong (wire-format fork tak terbaca worker L13). - **Flush** Blade compiled cache (`storage/framework/views`). + - **Invalidate** DB cache (Cache DatabaseStore drop Encrypter → row lama undecryptable). - Migrasi tabel `password_reminders` → `password_resets`. - Pasang **dual-read** Auth recaller cookie + session key + Cookie v2 HMAC. -- [ ] **4.4 — Cutover** `[ops · M]`: arahkan trafik ke L13; **hapus semua shim** + komponen evaporate (CachedRouting/Workbench/Exception); buang core 4.2. -- [ ] **4.5 — Post-cutover** `[app · S]`: setelah TTL remember-me lewat, **hapus dual-read**; verifikasi tak ada `Illuminate\*` 4.2 tersisa. + +- [ ] **5.2 — Cutover trafik** `[ops · M]` · blocked-by: 5.1 + - Arahkan trafik ke stok L13; copot semua shim/alias; buang core fork. + +- [ ] **5.3 — Verifikasi fork = 0 (gate: `replace` fork kosong)** `[fork+app · S]` · blocked-by: 5.2 + - **Prasyarat resolver:** blok `replace` fork **= 0 entry** (kalau tidak, dua `laravel/framework` + deadlock). `composer require laravel/framework:^13` stock; verifikasi tak ada `Illuminate\*` fork tersisa. + - Done when: fork dihapus; app jalan di stock L13; `composer why laravel/framework` = hanya stock. + +- [ ] **5.4 — Post-cutover: hapus dual-read; PERTAHANKAN guard pasca-swap** `[app · S]` · blocked-by: 5.2 + TTL remember-me lewat + - Setelah TTL remember-me lewat, hapus kode dual-read Auth. + - **JANGAN hapus** Psalm-rule Cache-TTL bare-int + grep Console `function fire(` — dua guard ini + **tidak self-liquidating** (stock 13 re-admit bare int / resolve `__invoke`), jadi permanen. --- ## Papan ringkas (dependency) ``` -0.1 0.2 0.3 → 0.4 ─┐ - ├─► Wave 1 (1.1–1.8, paralel) ─┐ - └─► Wave 2 (2.1–2.4, per-modul) ─┤ -3.1 → 3.2/3.3/3.4/3.5 (bottom-up) ─────────────────┤ - └─► Wave 4 (flip 4.1→4.5) +0.1 0.2 0.3 + │ +1.0 (split+replace-map) → 1.1 (Contracts) → 1.2 → 1.3 (copy-source) ─┐ + └→ 1.4 (putus SCC-1) ─────────────┤ + │ │ + ├─► Wave 2 (2.2–2.17, paralel setelah 1.1) ─┐ + ├─► Wave 3 (3.1–3.3) ─────────────────────────────────┤ + │ │ + └─► Wave 4 (4.1 SCC-1+Bus → 4.2 console[hard-req view] → 4.3 L2 → 4.4 → 4.5 SCC-2) ─► Wave 5 (5.1→5.4) + (tiap swap: hapus entry `replace` fork §1a; Fase-5 gate replace=0) ``` -**Mulai dari:** 0.1 → 0.2 → 0.3 → 0.4, lalu ambil Wave 1 mana saja (rekomendasi **1.1 Cache TTL** -duluan — gotcha paling berbahaya). Wave 3 butuh 3.1 (Contracts) lebih dulu. +**Mulai dari:** 0.1–0.3 → **1.0 split+replace-map + 1.1 Contracts** (buka semua) → Wave 2 +(rekomendasi **2.2 Cache: fork-tighten TTL → app konform + guard** duluan; bekas 2.1 digabung ke 2.2). +Wave 4 swap butuh Wave 1–2 komponen terkait ✅ dan **SCC-1 (4.1) selesai sebelum semua L2** (hard-require). -> Referensi detail tiap komponen (apiDelta/risks) → `MIGRATION-DETAIL.md`. Gotcha → `MIGRATION-ROADMAP.md` §5c. +> Referensi: detail apiDelta/risks per komponen → `MIGRATION-DETAIL.md`. Matriks enforcement per +> komponen → `MIGRATION-ENFORCEMENT-MATRIX.md`. Gotcha → `MIGRATION-ROADMAP.md` §7. Replace-collision +> mekanik → `MIGRATION-ROADMAP.md` §1a. \ No newline at end of file