From ef62b38c23f022b36df058b9731bac5e726951f1 Mon Sep 17 00:00:00 2001 From: Wessel Verheij Date: Sat, 22 Aug 2026 16:48:04 +0200 Subject: [PATCH 1/3] Add docs version labels and Jump availability labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requested by Simon in #website: a pill in the docs indicating which minor version a feature is available from, plus an "Available in Jump" / "Not in Jump yet" label since some features don't reach Jump immediately. Two independent labels sharing one visual primitive (): - — since/changed/deprecated/removed, usable at page level (front matter, beside the

), section level (under a ## heading), or inline in a bullet or table cell. x.0 never renders, since everything in a major's tree was there at x.0 unless stated otherwise. `since` renders bare; the other three carry a one-word prefix so a bare number on a removal doesn't read like the version that introduced it. - — "Jump 2.2+" when a feature needs a newer Jump than currently ships, or "Not in Jump yet" when no build has it. `config('docs.jump.current_version')` records what Jump ships, so bumping one value retires every label it now satisfies. A page whose Jump requirement isn't met loses its "Preview in Jump" QR card. Labels are validated in tests against config('docs.released_versions') so one can't point at an unreleased version or one belonging to a different major after a page is copied forward. Section labels sit on the line after a heading rather than inside it: HeadingRenderer slugs the heading's rendered contents to build the anchor id, so injected markup there would change the id and break existing deep links. Seeds three real annotations traced against NativePHP/mobile-air tags: per-side/per-corner rounded utilities (4.2), the ScreenMounted/ ScreenResumed/ScreenUnmounted lifecycle events (4.1), and a docs correctness fix — System::flashlight() was documented as merely deprecated but was actually removed in 4.1, not just deprecated. docs.jump.current_version is currently a guess ('2.0', going by "Jump v2 or later" on the v4 Jump page) — needs confirming with Simon before merge. released_versions for the older majors is likewise a best-effort reconstruction from the mobile-air tag list, worth a maintainer's eye; it's only consumed by the guard test, so being wrong there just means false failures, not a live bug. Bulk annotation of the rest of the v4 tree is intentionally left for a follow-up PR, to keep this one to mechanism + a few worked examples. Co-Authored-By: Claude Sonnet 5 --- .../ShowDocumentationController.php | 8 +- app/Support/DocsLabels.php | 56 +++++++ app/Support/JumpApp.php | 22 +++ config/docs.php | 46 ++++++ .../views/components/docs/badge.blade.php | 43 ++++++ .../components/docs/jump-badge.blade.php | 19 +++ .../components/docs/version-badge.blade.php | 34 +++++ resources/views/docs/index.blade.php | 30 +++- .../4/digging-deeper/lifecycle-hooks.md | 2 + .../docs/mobile/4/edge-components/layout.md | 4 +- .../mobile/4/getting-started/versioning.md | 28 ++++ .../views/docs/mobile/4/the-basics/system.md | 3 +- tests/Feature/Docs/DocsCachingTest.php | 6 +- tests/Feature/Docs/JumpBadgeTest.php | 121 +++++++++++++++ tests/Feature/Docs/VersionBadgeTest.php | 138 ++++++++++++++++++ 15 files changed, 550 insertions(+), 10 deletions(-) create mode 100644 app/Support/DocsLabels.php create mode 100644 resources/views/components/docs/badge.blade.php create mode 100644 resources/views/components/docs/jump-badge.blade.php create mode 100644 resources/views/components/docs/version-badge.blade.php create mode 100644 tests/Feature/Docs/JumpBadgeTest.php create mode 100644 tests/Feature/Docs/VersionBadgeTest.php diff --git a/app/Http/Controllers/ShowDocumentationController.php b/app/Http/Controllers/ShowDocumentationController.php index 42d4f4b8..5936f4a8 100644 --- a/app/Http/Controllers/ShowDocumentationController.php +++ b/app/Http/Controllers/ShowDocumentationController.php @@ -83,6 +83,8 @@ public function __invoke(Request $request, string $platform, string $version, ?s /** * Cache the callback's result for a day, or compute it fresh in local so * docs edits show up immediately without clearing (or racing on) the cache. + * The key folds in `config('docs')` so a Jump version bump invalidates + * rendered pages instead of trailing by up to a day. */ private function cacheOrCompute(string $key, Closure $callback): mixed { @@ -90,7 +92,11 @@ private function cacheOrCompute(string $key, Closure $callback): mixed return $callback(); } - return Cache::remember($key, now()->addDay(), $callback); + return Cache::remember( + $key.'_'.substr(md5(serialize(config('docs'))), 0, 8), + now()->addDay(), + $callback + ); } public function serveRawMarkdown(Request $request, string $platform, string $version, string $page) diff --git a/app/Support/DocsLabels.php b/app/Support/DocsLabels.php new file mode 100644 index 00000000..97adc099 --- /dev/null +++ b/app/Support/DocsLabels.php @@ -0,0 +1,56 @@ +route('platform')) { + 'mobile' => 'NativePHP for Mobile', + 'desktop' => 'NativePHP for Desktop', + default => 'NativePHP', + }; + } + + /** + * Null when that version's tree has no versioning page — an unlinked + * label beats one that 404s. + */ + public static function versioningPolicyUrl(): ?string + { + return self::pageUrl('getting-started/versioning', 'version-labels'); + } + + public static function jumpUrl(): ?string + { + return self::pageUrl('the-basics/jump'); + } + + private static function pageUrl(string $page, ?string $fragment = null): ?string + { + $platform = request()->route('platform'); + $version = request()->route('version'); + + if (blank($platform) || blank($version)) { + return null; + } + + if (! file_exists(resource_path("views/docs/{$platform}/{$version}/{$page}.md"))) { + return null; + } + + $url = route('docs.show', [ + 'platform' => $platform, + 'version' => $version, + 'page' => $page, + ]); + + return $fragment ? "{$url}#{$fragment}" : $url; + } +} diff --git a/app/Support/JumpApp.php b/app/Support/JumpApp.php index 5dd3a535..4b0f3ed5 100644 --- a/app/Support/JumpApp.php +++ b/app/Support/JumpApp.php @@ -51,4 +51,26 @@ public static function docsDeepLink(string $path): string { return self::CANONICAL_DOMAIN.'/'.ltrim($path, '/').'?'.self::QR_PARAM; } + + public static function currentVersion(): string + { + return (string) config('docs.jump.current_version'); + } + + /** + * `null`/`true` = no requirement, `false` = no Jump build has it, a + * version string = the minimum Jump version needed. + */ + public static function supports(string|bool|null $requirement): bool + { + if ($requirement === null || $requirement === true) { + return true; + } + + if ($requirement === false) { + return false; + } + + return version_compare(self::currentVersion(), $requirement, '>='); + } } diff --git a/config/docs.php b/config/docs.php index 9c2ada3a..a77e6482 100644 --- a/config/docs.php +++ b/config/docs.php @@ -35,6 +35,52 @@ 'mobile' => [], ], + /* + |-------------------------------------------------------------------------- + | Released Minor Versions + |-------------------------------------------------------------------------- + | + | Every minor release that exists, keyed by platform and then by major. + | The version labels rendered by are checked against + | this list in the test suite, so a label can't quietly point at a version + | that was never released — or at one belonging to a different major after + | a page has been copied forward into a new version's tree. + | + | Add the new entry here as part of shipping a release. + | + */ + + 'released_versions' => [ + 'desktop' => [ + 1 => ['1.0'], + 2 => ['2.0'], + ], + 'mobile' => [ + 1 => ['1.0', '1.1'], + 2 => ['2.0'], + 3 => ['3.0', '3.1', '3.2', '3.3'], + 4 => ['4.0', '4.1', '4.2'], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Jump + |-------------------------------------------------------------------------- + | + | Jump ships on its own cadence, so a feature can be released in NativePHP + | and still not render when someone scans the QR code on a docs page. + | + | Pages and sections declare the Jump version they need; this value records + | what Jump currently ships. Bump it when Jump catches up and every label it + | now satisfies disappears on its own — no docs edits required. + | + */ + + 'jump' => [ + 'current_version' => '2.0', + ], + /* |-------------------------------------------------------------------------- | Renamed Documentation Pages diff --git a/resources/views/components/docs/badge.blade.php b/resources/views/components/docs/badge.blade.php new file mode 100644 index 00000000..9401cea2 --- /dev/null +++ b/resources/views/components/docs/badge.blade.php @@ -0,0 +1,43 @@ +@props([ + 'label', + 'tooltip' => null, + 'href' => null, + 'variant' => 'neutral', +]) + +@php + $palettes = [ + 'neutral' => 'bg-gray-100 text-gray-600 ring-gray-200 dark:bg-white/10 dark:text-gray-300 dark:ring-white/15', + 'info' => 'bg-sky-50 text-sky-700 ring-sky-200 dark:bg-sky-400/10 dark:text-sky-300 dark:ring-sky-400/25', + 'warning' => 'bg-amber-50 text-amber-700 ring-amber-200 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/25', + 'danger' => 'bg-rose-50 text-rose-700 ring-rose-200 dark:bg-rose-400/10 dark:text-rose-300 dark:ring-rose-400/25', + 'jump' => 'bg-indigo-50 text-indigo-700 ring-indigo-200 dark:bg-indigo-400/10 dark:text-indigo-300 dark:ring-indigo-400/25', + ]; + + // not-prose: keeps the typography plugin from restyling the pill in markdown. + $classes = implode(' ', [ + 'not-prose inline-flex select-none items-center whitespace-nowrap rounded-full', + 'px-1.5 py-0.5 align-middle text-[11px] font-medium leading-4 no-underline', + 'ring-1 ring-inset transition', + $palettes[$variant] ?? $palettes['neutral'], + ]); +@endphp + +@if (filled($href)) + {{ $label }} +@else + {{ $label }} +@endif diff --git a/resources/views/components/docs/jump-badge.blade.php b/resources/views/components/docs/jump-badge.blade.php new file mode 100644 index 00000000..5d879a2c --- /dev/null +++ b/resources/views/components/docs/jump-badge.blade.php @@ -0,0 +1,19 @@ +@props([ + 'since' => null, + 'unavailable' => false, +]) + +@php + $requirement = $unavailable ? false : $since; +@endphp + +@unless (\App\Support\JumpApp::supports($requirement)) + +@endunless diff --git a/resources/views/components/docs/version-badge.blade.php b/resources/views/components/docs/version-badge.blade.php new file mode 100644 index 00000000..fd67a3af --- /dev/null +++ b/resources/views/components/docs/version-badge.blade.php @@ -0,0 +1,34 @@ +{{-- Never place inside a heading — HeadingRenderer slugs the heading's + rendered contents into the anchor id, so injected markup would change + existing deep links. --}} + +@props([ + 'since' => null, + 'changed' => null, + 'deprecated' => null, + 'removed' => null, +]) + +@php + $states = [ + ['version' => $since, 'variant' => 'neutral', 'prefix' => '', 'verb' => 'Added in'], + ['version' => $changed, 'variant' => 'info', 'prefix' => 'Changed ', 'verb' => 'Changed in'], + ['version' => $deprecated, 'variant' => 'warning', 'prefix' => 'Deprecated ', 'verb' => 'Deprecated in'], + ['version' => $removed, 'variant' => 'danger', 'prefix' => 'Removed ', 'verb' => 'Removed in'], + ]; + + $state = collect($states)->firstWhere(fn (array $state) => filled($state['version'])); + + // x.0 never renders — everything in a major's tree was there at x.0 + // unless stated otherwise. + $minor = (int) (explode('.', (string) ($state['version'] ?? ''))[1] ?? 0); +@endphp + +@if ($state && $minor > 0) + +@endif diff --git a/resources/views/docs/index.blade.php b/resources/views/docs/index.blade.php index 2e8aeb82..dec21853 100644 --- a/resources/views/docs/index.blade.php +++ b/resources/views/docs/index.blade.php @@ -4,10 +4,16 @@ @endpush @php - // Jump previews EDGE components, which only exist in the Mobile v4 docs. + // Front matter `jump`: a version string, or false if no Jump build has it. + $jumpRequirement = $jump ?? null; + + // Jump previews EDGE components (Mobile v4 only), and only where the + // shipping Jump can render the page — a QR to a blank screen is worse + // than no QR. $showJumpPreview = $platform === 'mobile' && (string) $version === '4' - && str_starts_with((string) request()->route('page'), 'edge-components/'); + && str_starts_with((string) request()->route('page'), 'edge-components/') + && \App\Support\JumpApp::supports($jumpRequirement); @endphp @@ -58,9 +64,23 @@ :page="request()->route('page')" /> -

- {{ $title }} -

+
+

+ {{ $title }} +

+ + + + +
diff --git a/resources/views/docs/mobile/4/digging-deeper/lifecycle-hooks.md b/resources/views/docs/mobile/4/digging-deeper/lifecycle-hooks.md index dad099dd..82415a31 100644 --- a/resources/views/docs/mobile/4/digging-deeper/lifecycle-hooks.md +++ b/resources/views/docs/mobile/4/digging-deeper/lifecycle-hooks.md @@ -145,6 +145,8 @@ the screen's layout chrome. ## Observing the lifecycle from outside + + The hooks above are yours to override, which makes them the wrong place for anything cross-cutting. Put analytics, telemetry, or crash breadcrumbs in a base class's `mount()` and any screen that defines its own `mount()` silently replaces it — so the observer goes quiet on exactly the screens with the most logic in them. diff --git a/resources/views/docs/mobile/4/edge-components/layout.md b/resources/views/docs/mobile/4/edge-components/layout.md index 99c21f2b..7da8cb55 100644 --- a/resources/views/docs/mobile/4/edge-components/layout.md +++ b/resources/views/docs/mobile/4/edge-components/layout.md @@ -329,8 +329,8 @@ The parser recognizes the classes listed below. | Border color | `border-{palette}-{shade}`, `border-white`, `border-black`, `border-transparent`, `border-[#hex]`, `border-theme-{token}` | | Border width | `border` (1dp), `border-2`, `border-4`, `border-8` | | Rounded | `rounded` (4dp), `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl`, `rounded-2xl`, `rounded-3xl`, `rounded-full`, `rounded-[N]` | -| Rounded (per side) | `rounded-t-*`, `rounded-r-*`, `rounded-b-*`, `rounded-l-*` — each rounds that side's two corners. A bare side (`rounded-b`) uses the same 4dp default as `rounded` | -| Rounded (per corner) | `rounded-tl-*`, `rounded-tr-*`, `rounded-br-*`, `rounded-bl-*`, including arbitrary values (`rounded-br-[4]`) | +| Rounded (per side) | `rounded-t-*`, `rounded-r-*`, `rounded-b-*`, `rounded-l-*` — each rounds that side's two corners. A bare side (`rounded-b`) uses the same 4dp default as `rounded` | +| Rounded (per corner) | `rounded-tl-*`, `rounded-tr-*`, `rounded-br-*`, `rounded-bl-*`, including arbitrary values (`rounded-br-[4]`) | | Shadow | `shadow`, `shadow-sm`, `shadow-md`, `shadow-lg`, `shadow-xl`, `shadow-2xl`, `shadow-inner`, `shadow-none` | | Opacity | `opacity-{0..100}`, arbitrary `opacity-[0.5]` | | Text size | `text-xs`, `text-sm`, `text-base`, `text-lg`, `text-xl`, `text-2xl`, `text-3xl`, `text-4xl`, `text-5xl`, `text-6xl`, arbitrary `text-[N]` | diff --git a/resources/views/docs/mobile/4/getting-started/versioning.md b/resources/views/docs/mobile/4/getting-started/versioning.md index 07640b03..7c4cea91 100644 --- a/resources/views/docs/mobile/4/getting-started/versioning.md +++ b/resources/views/docs/mobile/4/getting-started/versioning.md @@ -57,6 +57,34 @@ with a full minimum patch release defined in your `composer.json`: This automatically receives patch updates while giving you control over minor releases. +## Version labels + +Anything documented in this version of the docs has been here since 4.0 unless it carries a label. Labels appear next +to a page title, under a section heading, or beside the individual prop or class they describe: + +| Label | Meaning | +|-------|---------| +| | Added in that minor release. Upgrade to at least that version to use it | +| | Behaviour or signature changed in that release — check it against what your app relies on before upgrading | +| | Still works, but slated for removal. Move off it when convenient | +| | Gone as of that release. Documented only so you know what replaced it | + +Remember that a minor release [may contain native code changes](#minor-releases), so picking up a labelled feature +means rebuilding with `php artisan native:install --force` rather than a `composer update` alone. + +### Jump labels + +[Jump](../the-basics/jump) ships on its own release cadence, so a feature can be released in NativePHP and still not +render on your phone when you scan a QR code. Where that's the case, you'll see: + +| Label | Meaning | +|-------|---------| +| | Needs a newer Jump than the one on the stores. Build to a simulator or device to try it today | +| | No Jump build supports it. It'll work in a packaged build of your app | + +These disappear on their own as Jump catches up. Pages carrying one don't offer the "Preview in Jump" QR code, since +scanning it wouldn't show you the component. + ## Your application versioning Just because we're using semantic versioning for the `nativephp/mobile` package, doesn't mean your app must follow that diff --git a/resources/views/docs/mobile/4/the-basics/system.md b/resources/views/docs/mobile/4/the-basics/system.md index 7a6011f9..550244bd 100644 --- a/resources/views/docs/mobile/4/the-basics/system.md +++ b/resources/views/docs/mobile/4/the-basics/system.md @@ -73,6 +73,7 @@ Reading the current appearance and reacting to theme changes lives with the rest diff --git a/tests/Feature/Docs/DocsCachingTest.php b/tests/Feature/Docs/DocsCachingTest.php index e284fd32..5ab784f2 100644 --- a/tests/Feature/Docs/DocsCachingTest.php +++ b/tests/Feature/Docs/DocsCachingTest.php @@ -54,6 +54,10 @@ public function test_non_local_docs_request_caches_page_properties(): void $this->get('/docs/mobile/4/edge-components/stack')->assertStatus(200); - $this->assertTrue(Cache::has('docs_mobile_4_edge-components/stack')); + // The key is suffixed with a hash of config('docs') so a Jump version + // bump invalidates rendered pages instead of trailing by up to a day. + $key = 'docs_mobile_4_edge-components/stack_'.substr(md5(serialize(config('docs'))), 0, 8); + + $this->assertTrue(Cache::has($key)); } } diff --git a/tests/Feature/Docs/JumpBadgeTest.php b/tests/Feature/Docs/JumpBadgeTest.php new file mode 100644 index 00000000..aa1aee7b --- /dev/null +++ b/tests/Feature/Docs/JumpBadgeTest.php @@ -0,0 +1,121 @@ + 'test-token']); + Http::fake([ + '*' => Http::response(['blocks' => []], 200), + ]); + + // testing runs with CACHE_DRIVER=array, but cacheOrCompute() still + // caches (only `local` bypasses it) — flush so a fixture written by + // one test can't be served stale to the next. + Cache::flush(); + + $this->fixturesDir = resource_path('views/docs/mobile/4/edge-components'); + } + + protected function tearDown(): void + { + foreach (['unavailable', 'unshipped', 'shipped'] as $name) { + @unlink("{$this->fixturesDir}/_test-jump-badge-{$name}.md"); + } + + Cache::flush(); + + parent::tearDown(); + } + + protected function writeFixture(string $name, string $frontMatterExtra): void + { + file_put_contents( + "{$this->fixturesDir}/_test-jump-badge-{$name}.md", + <<assertTrue(JumpApp::supports(null)); + } + + public function test_supports_when_no_build_has_it(): void + { + $this->assertFalse(JumpApp::supports(false)); + } + + public function test_supports_an_older_or_equal_version(): void + { + config(['docs.jump.current_version' => '2.2']); + + $this->assertTrue(JumpApp::supports('2.0')); + $this->assertTrue(JumpApp::supports('2.2')); + } + + public function test_does_not_support_a_newer_version(): void + { + config(['docs.jump.current_version' => '2.0']); + + $this->assertFalse(JumpApp::supports('2.2')); + } + + public function test_page_with_jump_false_shows_not_in_jump_yet_and_no_qr(): void + { + $this->writeFixture('unavailable', 'jump: false'); + + $this->get('/docs/mobile/4/edge-components/_test-jump-badge-unavailable') + ->assertStatus(200) + ->assertSee('Not in Jump yet') + ->assertDontSee('Preview in Jump'); + } + + public function test_page_with_unshipped_jump_version_shows_pill_and_no_qr(): void + { + config(['docs.jump.current_version' => '2.0']); + $this->writeFixture('unshipped', 'jump: "99.0"'); + + $this->get('/docs/mobile/4/edge-components/_test-jump-badge-unshipped') + ->assertStatus(200) + ->assertSee('Jump 99.0+') + ->assertDontSee('Preview in Jump'); + } + + public function test_page_with_shipped_jump_version_shows_no_pill_and_the_qr(): void + { + config(['docs.jump.current_version' => '2.0']); + $this->writeFixture('shipped', 'jump: "1.0"'); + + $response = $this->get('/docs/mobile/4/edge-components/_test-jump-badge-shipped') + ->assertStatus(200) + ->assertDontSee('Not in Jump yet') + ->assertDontSee('Jump 1.0+') + ->assertSee('Preview in Jump'); + + $response->assertOk(); + } +} diff --git a/tests/Feature/Docs/VersionBadgeTest.php b/tests/Feature/Docs/VersionBadgeTest.php new file mode 100644 index 00000000..aa698912 --- /dev/null +++ b/tests/Feature/Docs/VersionBadgeTest.php @@ -0,0 +1,138 @@ + 'test-token']); + Http::fake([ + '*' => Http::response(['blocks' => []], 200), + ]); + } + + public function test_since_renders_bare(): void + { + // The tooltip legitimately says "Added in ..." — it's the visible + // label, sandwiched between tags with no prefix, that must be bare. + $this->blade('') + ->assertSee('>4.2<', false); + } + + public function test_x_dot_zero_renders_nothing(): void + { + $this->blade('') + ->assertDontSee('4.0'); + } + + public function test_changed_renders_with_prefix(): void + { + $this->blade('') + ->assertSee('Changed 4.2'); + } + + public function test_deprecated_renders_with_prefix(): void + { + $this->blade('') + ->assertSee('Deprecated 4.1'); + } + + public function test_removed_renders_with_prefix(): void + { + $this->blade('') + ->assertSee('Removed 4.1'); + } + + public function test_layout_page_contains_the_4_2_pill(): void + { + $this->get('/docs/mobile/4/edge-components/layout') + ->assertStatus(200) + ->assertSee('4.2'); + } + + public function test_section_label_does_not_change_the_heading_anchor_id(): void + { + $html = CommonMark::convertToHtml( + "## Observing the lifecycle from outside\n\n\n\nBody text." + ); + + $this->assertStringContainsString('id="observing-the-lifecycle-from-outside"', $html); + } + + public function test_lifecycle_hooks_page_keeps_its_heading_anchor(): void + { + $this->get('/docs/mobile/4/digging-deeper/lifecycle-hooks') + ->assertStatus(200) + ->assertSee('id="observing-the-lifecycle-from-outside"', false); + } + + public function test_search_index_content_contains_no_badge_markup(): void + { + $page = app(DocsSearchService::class)->getPage('mobile', '4', 'edge-components', 'layout'); + + $this->assertNotNull($page); + $this->assertStringNotContainsString('assertStringNotContainsString('version-badge', $page['content']); + } + + public function test_every_version_label_points_at_a_released_version(): void + { + $releasedVersions = config('docs.released_versions'); + $finder = (new Finder)->files()->name('*.md')->in(resource_path('views/docs')); + + $violations = []; + + foreach ($finder as $file) { + $relative = $file->getRelativePathname(); + $parts = explode(DIRECTORY_SEPARATOR, $relative); + + if (count($parts) < 2 || ! is_numeric($parts[1])) { + continue; + } + + [$platform, $major] = [$parts[0], (int) $parts[1]]; + $allowed = $releasedVersions[$platform][$major] ?? []; + + $content = $file->getContents(); + $document = YamlFrontMatter::parse($content); + + foreach (['since', 'changed', 'deprecated', 'removed'] as $key) { + $value = $document->matter($key); + + if ($value !== null && ! in_array((string) $value, $allowed, true)) { + $violations[] = "{$relative} front matter `{$key}: {$value}`"; + } + } + + if (preg_match_all('/]*)\/>/s', $content, $tagMatches)) { + foreach ($tagMatches[1] as $attrs) { + foreach (['since', 'changed', 'deprecated', 'removed'] as $key) { + if (preg_match('/'.$key.'="([^"]+)"/', $attrs, $m)) { + if (! in_array($m[1], $allowed, true)) { + $violations[] = "{$relative} "; + } + break; + } + } + } + } + } + + $this->assertEmpty( + $violations, + "Version labels pointing at an unreleased or mismatched version:\n".implode("\n", $violations) + ); + } +} From 22ebbb5f3ae9ec2636c995ffd6220cd8f926327a Mon Sep 17 00:00:00 2001 From: Wessel Verheij Date: Sat, 22 Aug 2026 17:00:27 +0200 Subject: [PATCH 2/3] Update jump.current_version from a guess to the verified live version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checked the live App Store listing directly: Jump is on v3.0.0 as of Jul 30 ("Rebuilt from the ground up with SuperNative!"). Play Store doesn't expose a numeric version publicly, but was last updated Jul 25 with no indication of lagging behind — assuming parity at 3.0. Co-Authored-By: Claude Sonnet 5 --- config/docs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/docs.php b/config/docs.php index a77e6482..389b2718 100644 --- a/config/docs.php +++ b/config/docs.php @@ -78,7 +78,7 @@ */ 'jump' => [ - 'current_version' => '2.0', + 'current_version' => '3.0', ], /* From 2af590cb6420e9f764f9b3585989544d50591402 Mon Sep 17 00:00:00 2001 From: Wessel Verheij Date: Sat, 22 Aug 2026 17:02:49 +0200 Subject: [PATCH 3/3] Add missing 2.1 and 2.2 to desktop's released_versions Checked against real NativePHP/desktop git tags: 2.0.0/.1/.2, 2.1.0/.1, 2.2.0/.1 all exist, so only listing 2.0 was incomplete. Mobile's v4 entries (4.0, 4.1, 4.2) already matched the real mobile-air tags exactly, so no change needed there. Co-Authored-By: Claude Sonnet 5 --- config/docs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/docs.php b/config/docs.php index 389b2718..92c545a1 100644 --- a/config/docs.php +++ b/config/docs.php @@ -53,7 +53,7 @@ 'released_versions' => [ 'desktop' => [ 1 => ['1.0'], - 2 => ['2.0'], + 2 => ['2.0', '2.1', '2.2'], ], 'mobile' => [ 1 => ['1.0', '1.1'],