Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
570b238
:memo: docs(release): add 5.3.2 review and release plan
abmmhasan Oct 30, 2026
a017d1d
docs: extend ArrayKit 5.3.2 release review with median and dispatch r…
abmmhasan Oct 30, 2026
3a5a3eb
test: capture guarded object comparisons and failed move regression
abmmhasan Oct 30, 2026
cc9d5ef
fix: bound guarded sorts by refusing unbounded object comparisons
abmmhasan Oct 30, 2026
fec08a3
fix: stage dot path move and rename before committing source removal
abmmhasan Oct 30, 2026
7a934a4
style: align Batch A regression test closures with project format
abmmhasan Oct 30, 2026
c47a2bb
refactor: isolate guarded object-sort rejection to respect complexity…
abmmhasan Oct 30, 2026
6b26398
test: use rejecting setter argument to satisfy strict PHPCS
abmmhasan Oct 30, 2026
d22359b
style: order guarded sort helper methods as required by Pint
abmmhasan Oct 30, 2026
7a95306
fix: correct sampling, median, NaN, enum, and collection dispatch bou…
abmmhasan Oct 30, 2026
b3cf8dd
fix: satisfy enum contract analysis and move test enums into fixtures
abmmhasan Oct 30, 2026
9d22aaf
docs: define guarded sort and staged move behavior; sync 5.3.2 progress
abmmhasan Oct 30, 2026
1d0040b
fix: calculate overflow-safe averages and guard integer casts
abmmhasan Oct 30, 2026
f72f827
ci: qualify optional Runwire 2.1.1 and 3.0 across PHP 8.4/8.5
abmmhasan Oct 30, 2026
3fc54c1
refactor: keep overflow-safe averaging in existing numeric ops owner
abmmhasan Oct 30, 2026
1af66ea
chore: target only Runwire 3.0 and repair numeric helper docblock
abmmhasan Oct 30, 2026
3e32be7
fix: isolate snapshot references and refresh numeric lazy namespaces
abmmhasan Oct 30, 2026
a8efd41
bench: compare 5.3.2 to immediate 5.3.1 baseline with snapshot workload
abmmhasan Oct 30, 2026
906bb87
fix: type numeric namespace storage as PHP array keys
abmmhasan Oct 30, 2026
cfb7bce
docs: explain snapshot isolation, numeric namespaces, and 3.0 integra…
abmmhasan Oct 30, 2026
ca77557
ci: require clean Sphinx documentation build for release acceptance
abmmhasan Oct 30, 2026
5087975
test: cover snapshot isolation in lazy and layered config owners
abmmhasan Oct 30, 2026
6ad1114
docs: resolve Sphinx duplicate anchors and malformed examples
abmmhasan Oct 30, 2026
eb9cbcb
docs: synchronize all four implementation batches and acceptance evid…
abmmhasan Oct 30, 2026
91d7b3b
docs: close all implementation batches against green 5.3.2 acceptance…
abmmhasan Oct 30, 2026
6dfcaad
:art: refactor(config,array): enhance snapshot isolation, numeric cal…
abmmhasan Oct 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.github export-ignore
benchmarks export-ignore
build export-ignore
docs export-ignore
examples export-ignore
tests export-ignore
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/security-standards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,73 @@ jobs:
actions: read
contents: read

docs:
if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')
name: "Sphinx documentation"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install documentation dependencies
run: python -m pip install --requirement docs/requirements.txt
- name: Build documentation and reject warnings
run: sphinx-build -b html -W --keep-going docs build/sphinx

runwire-compat:
if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')
name: "Runwire 3.0 / PHP ${{ matrix.php }}"
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
php: ["8.4", "8.5"]
env:
XDEBUG_MODE: off
steps:
- uses: actions/checkout@v7
- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
extensions: opcache
ini-values: opcache.enable_cli=1
coverage: none
- name: Install exact Runwire 3.0
run: composer require --dev "infocyph/runwire:3.0.0" --no-interaction --prefer-dist --no-progress
- name: Exercise lazy pipeline and host-owned coroutine lifecycle
run: >-
php vendor/bin/pest
--configuration vendor/infocyph/phpforge/resources/pest.xml
--bootstrap vendor/autoload.php
tests/Feature/LazyCollectionTest.php
tests/Feature/Release530BatchETest.php
tests/Feature/Release530ReviewRegressionTest.php
tests/Feature/Release530BatchFTest.php
- name: Exercise persistent worker with Runwire 3.0
if: matrix.php == '8.5'
run: |
mkdir -p build
composer ic:soak:worker \
--duration=300 \
--warmup=10 \
--sample-interval=1 \
--max-growth-mb=16 \
--report=build/runwire-worker-soak.json \
-- php -d opcache.enable_cli=1 tests/Support/release530-persistent-worker.php
- name: Upload Runwire 3.0 worker-soak report
if: always() && matrix.php == '8.5' && hashFiles('build/runwire-worker-soak.json') != ''
uses: actions/upload-artifact@v7
with:
name: runwire-3.0-php-${{ matrix.php }}-worker-soak
path: build/runwire-worker-soak.json
if-no-files-found: error
retention-days: 61

release-acceptance:
if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')
name: "Release Acceptance"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ configuration or object models.

* **PHP 8.4** or higher

Runwire is optional. ArrayKit 5.3 tests its lazy-runtime integration against `infocyph/runwire` 2.1.1; ordinary ArrayKit installation has no Runwire runtime dependency.
Runwire is optional. ArrayKit 5.3.2 qualifies its passed-instance lazy-runtime integration against `infocyph/runwire` 3.0 on PHP 8.4 and 8.5; ordinary ArrayKit installation has no Runwire runtime dependency.


## Installation
Expand Down
3 changes: 2 additions & 1 deletion benchmarks/release-representative.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@

require dirname(__DIR__) . '/vendor/autoload.php';

const RELEASE_BASELINE_TAG = '5.2.0';
const RELEASE_BASELINE_TAG = '5.3.1';
const RELEASE_REPETITIONS = 3;
const RELEASE_STABILITY_SPREAD_PERCENT = 10.0;

/** @var list<array{name:string,operations:int,warmup:int}> $workloadSpecs */
$workloadSpecs = [
['name' => 'array-query', 'operations' => 100, 'warmup' => 20],
['name' => 'dot-config', 'operations' => 100, 'warmup' => 20],
['name' => 'config-snapshot', 'operations' => 100, 'warmup' => 20],
['name' => 'lazy-array', 'operations' => 100, 'warmup' => 20],
['name' => 'lazy-file-cold', 'operations' => 40, 'warmup' => 10],
['name' => 'lazy-file-generated', 'operations' => 60, 'warmup' => 10],
Expand Down
28 changes: 28 additions & 0 deletions benchmarks/release-workload.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
$operation = match ($workload) {
'array-query' => releaseArrayQueryWorkload(),
'dot-config' => releaseDotConfigWorkload(),
'config-snapshot' => releaseConfigSnapshotWorkload(),
'lazy-array' => releaseLazyArrayWorkload(),
'lazy-file-cold' => releaseLazyFileColdWorkload($temporaryDirectories),
'lazy-file-generated' => releaseLazyFileGeneratedWorkload($temporaryDirectories),
Expand Down Expand Up @@ -119,6 +120,33 @@ function releaseCpuSeconds(array $usage): float
return $user + $system;
}

/**
* @return \Closure(): void
*/
function releaseConfigSnapshotWorkload(): \Closure
{
$data = [
'app' => [
'service' => ['enabled' => true, 'limits' => range(1, 32)],
'version' => 1,
],
];
$config = new Config();
$config->loadArray($data);

return static function () use ($config): void {
$config->snapshot('benchmark');
$config->set('app.version', 2);
if (!$config->changed('benchmark') || !$config->restore('benchmark')) {
throw new RuntimeException('Snapshot release workload failed to restore configuration.');
}

if ($config->get('app.version') !== 1 || $config->changed('benchmark')) {
throw new RuntimeException('Snapshot release workload returned invalid configuration.');
}
};
}

/**
* @return \Closure(): void
*/
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
},
"require-dev": {
"infocyph/phpforge": "dev-main@dev",
"infocyph/runwire": "2.1.1"
"infocyph/runwire": "^3.0"
},
"suggest": {
"infocyph/runwire": "Optional LazyCollection cancellation and cooperative-yield integration; ArrayKit 5.3 is tested against Runwire 2.1.1."
"infocyph/runwire": "Optional LazyCollection cancellation and cooperative-yield integration; integration tested against Runwire 3.0."
},
"minimum-stability": "stable",
"prefer-stable": true,
Expand Down
3 changes: 2 additions & 1 deletion docs/array-helpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,8 @@ Behavior Notes
the two-argument shorthand form.
- ``ArrayMulti::flatten($array, 0)`` returns unchanged top-level values.
- Use ``depthGuarded()``, ``flattenGuarded()``, and ``sortRecursiveGuarded()`` when processing untrusted/deep inputs.
- A non-throwing recursive sort retains the order of a parent whose child traversal was cut short, instead of comparing unvisited nested values. Throwing mode rejects the limit breach.
- Guarded recursive sorting accepts scalar/array values only. It refuses object values, including objects nested inside arrays, because PHP sorting may compare arbitrarily deep public object graphs outside a traversal budget. Normalize object data before guarded sorting, or use ordinary ``sortRecursive()`` only for trusted inputs.
- A non-throwing recursive sort retains the affected array order when traversal cannot be completed or object values are rejected. Throwing mode rejects the limit breach or object comparison with ``RuntimeException``.
- ``ArrayMulti`` callback helpers such as ``sortBy()``, ``sum()``, ``maxBy()``, ``minBy()`` support ``($row, $key)``.
- In ``string|callable`` row APIs, strings always identify fields; use a closure or
another non-string callable for callback behavior.
Expand Down
5 changes: 3 additions & 2 deletions docs/collection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,9 @@ renewable sources when each traversal can create a fresh iterable.
Optional Runwire Binding
------------------------

Runwire is optional. Ordinary ``LazyCollection`` use does not require or load
Runwire. When a host already owns a Runwire runtime, pass those exact instances
Runwire is optional. ArrayKit 5.3.2 targets Runwire 3.0 and tests the
integration on PHP 8.4 and 8.5. Ordinary ``LazyCollection`` use does not
require or load Runwire. When a host already owns a Runwire runtime, pass those exact instances
to ``withRunwire()``:

.. code-block:: php
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def get_version() -> str:
"linkify",
]
myst_heading_anchors = 3
autosectionlabel_prefix_document = True

# Autodoc/Napoleon
autodoc_default_options = {
Expand Down
5 changes: 4 additions & 1 deletion docs/config-layering.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ values from lower-precedence layers.

``all()`` materializes the configured namespace set. ``warmNamespaceCache()``
and ``clearNamespaceCache()`` delegate generated source-cache lifecycle to the
underlying lazy source.
underlying lazy source. Canonical numeric namespace names such as ``0`` and
``-1`` are supported as filenames and paths. PHP converts those array keys
to integers in the result from ``all()``; ``loadedNamespaces()`` always
returns namespace names as strings.

Resilient Lazy Source Cache
---------------------------
Expand Down
26 changes: 25 additions & 1 deletion docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ This example shows a typical bootstrap flow:

Example ``.env`` file:

.. code-block:: dotenv
.. code-block:: text

APP_NAME=ArrayKit Demo
APP_ENV=local
Expand Down Expand Up @@ -338,6 +338,30 @@ Merging, Snapshots, and Read-Only Mode
$locked = $config->isReadonly(); // true
$config->readonly(false); // unlock

Snapshots retain a by-value copy of scalar and nested array data. PHP references
to external variables do not cause a later mutation to change an earlier
snapshot. ``restore()`` uses PHP's copy-on-write isolation of these reference-free
arrays, so repeated restores remain safe without expanding shared storage.
Repeated internal array references are copied independently; cyclic array
references raise ``UnexpectedValueException`` before a valid named snapshot
can be overwritten. Creation is bounded to depth 256 and 100,000 visited entries;
exceeding either limit raises ``UnexpectedValueException`` and leaves existing
snapshots intact. Completed shared references reuse detached array storage
without retaining writable aliases. Object values remain shallow: the snapshot preserves
their identity and cannot reverse changes to an object's internal state.

.. code-block:: php

<?php
$value = 1;
$config = new \Infocyph\ArrayKit\Config\Config();
$config->loadArray(['app' => ['value' => &$value]]);
$config->snapshot('start');

$value = 9;
$config->restore('start');
assert($config->get('app.value') === 1);

Array-Value Helpers
-------------------

Expand Down
4 changes: 4 additions & 0 deletions docs/dot-notation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@ Behavior Notes
- Object writes support ``stdClass`` additions, existing public writable
properties, and magic ``__set`` handlers. Missing ordinary properties and
inaccessible or readonly properties fail with ``InvalidArgumentException``.
- ``rename()`` and ``move()`` stage array changes before committing source
removal. A destination-write failure retains the original array source.
Magic setters and other application-owned object methods can have external
side effects and are not transactionally rolled back.
- Defaults may be plain values or ``Closure`` instances. Closures are evaluated
only when path resolution fails; other callable values are returned unchanged.
- Wildcard traversal in ``get`` returns arrays of matched results.
Expand Down
7 changes: 6 additions & 1 deletion docs/lazy-config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,14 @@ Cache behavior:
- Structural, wildcard, and namespace reads use the same pinned immutable generation as exact reads.
- ``Environment::ref()`` values and closures are resolved before publication.
- Warm-up rereads the authoritative source unless the caller explicitly supplied or mutated that namespace in memory; an older generated cache does not feed a new source-backed generation.
- Readers do not take the writer lock. The first cache lookup pins a generation for that instance. External publication does not change its view. Construct a new instance or call ``namespaceCache()`` explicitly to refresh; warm-up and flush on the instance also reset its generated state, retaining intentional runtime overrides.
- Readers do not take the writer lock. The first cache lookup pins a generation for that instance. External publication does not change its view. Construct a new instance or call ``namespaceCache()`` explicitly to refresh; refresh also drops source-derived values but retains deliberate runtime overrides. Warm-up and flush on the instance also reset its generated state, retaining intentional runtime overrides.
- Writers copy unmodified namespaces from the latest published generation even when that writer was previously a pinned reader. Partial merges retain source/cache origins for untouched namespaces.

Namespace filenames may include canonical numeric names such as ``0.php``
and ``-1.php``; dot paths still use strings (``'0.value'``) and
``loadedNamespaces()`` returns string names. PHP arrays naturally convert
canonical integer-string map keys into integer keys.

Environment Values in Namespace Files
-------------------------------------

Expand Down
20 changes: 20 additions & 0 deletions docs/migration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ generation selected by ``.arraykit-generation``. The caller namespace
``__flat`` is therefore no longer ambiguous with ArrayKit's internal flat
index.

5.3.2 Patch Notes
-----------------

The patch fixes failed dot-path moves (the source is preserved if a destination
write fails), guarded sort boundaries for arbitrary object values, snapshot
reference aliasing, and lazy config refresh against newly published source
generations. It also fixes numeric namespace discovery, numeric and enum edge
cases, median overflow and private pipeline method dispatch.

Optional Runwire integration is qualified for Runwire 3.0 only. Production
installation does not require Runwire; applications that explicitly bind a
Runwire runtime must use a compatible 3.0 host instance.

Normal array/snapshot APIs retain their signatures. Snapshotting now visits
nested arrays to detach external references; object values remain shallow.
A cyclic array reference is rejected at snapshot creation rather than saved.
Snapshot creation also rejects depth beyond 256 or more than 100,000 visited
entries before replacing an existing snapshot. Reused referenced subgraphs are
detached once per snapshot and shared through PHP copy-on-write storage.

Recent Additions
----------------

Expand Down
2 changes: 1 addition & 1 deletion docs/quick-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ keeps its data-focused features together: shape and access arrays, transform
collections, hydrate configuration, and map values into DTOs.

ArrayKit Facade Example
----------------------
-----------------------

.. code-block:: php

Expand Down
6 changes: 3 additions & 3 deletions docs/release-5.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Highlights
``hydrateNestedGuarded()`` and ``toArrayDeepGuarded()``.
- Lower replay overhead for array-backed ``LazyCollection`` sources while
retaining repeatability for one-shot iterators.
- Optional Runwire 2.1.1 integration through
- Optional Runwire integration through
``LazyCollection::withRunwire()``. The host passes its existing
``RuntimeContext`` and optional request/scope objects; ArrayKit does not own
Runwire lifecycle management.
Expand Down Expand Up @@ -91,8 +91,8 @@ property getters and callbacks remain trusted application code.
Optional Runwire Integration
----------------------------

Runwire is not a production dependency. ArrayKit 5.3 tests the optional
integration against ``infocyph/runwire`` 2.1.1.
Runwire is not a production dependency. The 5.3.2 maintenance line tests the
optional integration against ``infocyph/runwire`` 3.0.

``LazyCollection::withRunwire()`` accepts the host's exact ``RuntimeContext``
and optional ``RequestContext`` / ``CoroutineScope``. Cancellation is checked
Expand Down
37 changes: 12 additions & 25 deletions src/Array/ArraySingle.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,7 @@ class ArraySingle
*/
public static function avg(array $array): float|int
{
$total = 0;
$count = 0;

foreach ($array as $value) {
$numeric = self::toNumericOrNull($value);
if ($numeric === null) {
continue;
}

$total += $numeric;
$count++;
}

if ($count === 0) {
return 0.0;
}

$avg = $total / $count;

return fmod($avg, 1.0) === 0.0 ? (int) $avg : $avg;
return ArraySingleOps::avg($array);
}

/**
Expand Down Expand Up @@ -312,7 +293,7 @@ public static function isNegative(array $array): bool
}

$hasNumeric = true;
if ($numeric >= 0) {
if (!($numeric < 0)) {
return false;
}
}
Expand All @@ -337,7 +318,7 @@ public static function isPositive(array $array): bool
}

$hasNumeric = true;
if ($numeric <= 0) {
if (!($numeric > 0)) {
return false;
}
}
Expand Down Expand Up @@ -453,9 +434,15 @@ public static function median(array $array): float|int
$count = count($values);
$mid = intdiv($count, 2);

return ($count % 2)
? $values[$mid]
: ($values[$mid - 1] + $values[$mid]) / 2;
if (($count % 2) !== 0) {
return $values[$mid];
}

$left = $values[$mid - 1];
$right = $values[$mid];
$sum = $left + $right;

return is_finite((float) $sum) ? $sum / 2 : $left / 2 + $right / 2;
}

/**
Expand Down
Loading
Loading