From 08c8237778567cf73486523e46841b5f834a618e Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:42:22 +0330 Subject: [PATCH 01/38] Expose weighted least squares through PHP Matrix API --- src/numerus_matrix_php.c | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/numerus_matrix_php.c b/src/numerus_matrix_php.c index df0d188..dd3acbd 100644 --- a/src/numerus_matrix_php.c +++ b/src/numerus_matrix_php.c @@ -176,6 +176,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_matrix_least_squares, 0, 1, Numer ZEND_ARG_OBJ_INFO(0, rightHandSide, Numerus\\Matrix, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_matrix_weighted_least_squares, 0, 2, Numerus\\Matrix, 0) + ZEND_ARG_OBJ_INFO(0, rightHandSide, Numerus\\Matrix, 0) + ZEND_ARG_TYPE_INFO(0, weights, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + PHP_METHOD(NumerusMatrix, __construct) { ZEND_PARSE_PARAMETERS_NONE(); @@ -440,6 +445,61 @@ PHP_METHOD(NumerusMatrix, leastSquares) numerus_matrix_return_owned(return_value, result); } +PHP_METHOD(NumerusMatrix, weightedLeastSquares) +{ + zval *rhs_zv; + zval *weights_zv; + zval *weight_zv; + HashTable *weights_ht; + uint32_t weights_count; + size_t index = 0; + double *weights; + numerus_matrix *source; + numerus_matrix *rhs; + numerus_matrix *result = NULL; + numerus_matrix_status status; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(rhs_zv, numerus_matrix_ce) + Z_PARAM_ARRAY(weights_zv) + ZEND_PARSE_PARAMETERS_END(); + + source = numerus_matrix_require(ZEND_THIS); + rhs = numerus_matrix_require(rhs_zv); + if (source == NULL || rhs == NULL) { + RETURN_THROWS(); + } + + weights_ht = Z_ARRVAL_P(weights_zv); + weights_count = zend_hash_num_elements(weights_ht); + if (weights_count == 0) { + zend_value_error("Weights must not be empty"); + RETURN_THROWS(); + } + + weights = safe_emalloc((size_t) weights_count, sizeof(*weights), 0); + ZEND_HASH_FOREACH_VAL(weights_ht, weight_zv) { + if (Z_TYPE_P(weight_zv) != IS_LONG && Z_TYPE_P(weight_zv) != IS_DOUBLE) { + efree(weights); + zend_type_error("Weights must contain only integers or floats"); + RETURN_THROWS(); + } + weights[index++] = zval_get_double(weight_zv); + } ZEND_HASH_FOREACH_END(); + + status = numerus_matrix_weighted_least_squares( + source, rhs, weights, (size_t) weights_count, &result + ); + efree(weights); + + if (status != NUMERUS_MATRIX_SUCCESS) { + numerus_matrix_throw_status(status); + RETURN_THROWS(); + } + + numerus_matrix_return_owned(return_value, result); +} + static const zend_function_entry numerus_matrix_methods[] = { PHP_ME(NumerusMatrix, __construct, arginfo_matrix_private_construct, ZEND_ACC_PRIVATE) PHP_ME(NumerusMatrix, fromRows, arginfo_matrix_from_rows, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) @@ -451,6 +511,7 @@ static const zend_function_entry numerus_matrix_methods[] = { PHP_ME(NumerusMatrix, inverse, arginfo_matrix_unary, ZEND_ACC_PUBLIC) PHP_ME(NumerusMatrix, multiply, arginfo_matrix_binary, ZEND_ACC_PUBLIC) PHP_ME(NumerusMatrix, leastSquares, arginfo_matrix_least_squares, ZEND_ACC_PUBLIC) + PHP_ME(NumerusMatrix, weightedLeastSquares, arginfo_matrix_weighted_least_squares, ZEND_ACC_PUBLIC) PHP_FE_END }; From edff313595861d21bda9e99fb2b95743b6d100b0 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:42:28 +0330 Subject: [PATCH 02/38] Test PHP weighted least squares contracts --- tests/011-weighted-least-squares.phpt | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/011-weighted-least-squares.phpt diff --git a/tests/011-weighted-least-squares.phpt b/tests/011-weighted-least-squares.phpt new file mode 100644 index 0000000..4ea68c4 --- /dev/null +++ b/tests/011-weighted-least-squares.phpt @@ -0,0 +1,49 @@ +--TEST-- +Numerus weighted least-squares PHP API +--FILE-- +leastSquares($y); +$wls = $x->weightedLeastSquares($y, [1.0, 1.0, 1.0]); +var_dump(abs($ols->get(0, 0) - $wls->get(0, 0)) < 1e-10); +var_dump(abs($ols->get(1, 0) - $wls->get(1, 0)) < 1e-10); + +$x2 = Matrix::fromRows([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]); +$y2 = Matrix::fromRows([[1.0], [2.0], [100.0]]); +$zeroWeight = $x2->weightedLeastSquares($y2, [1.0, 1.0, 0.0]); +var_dump(abs($zeroWeight->get(0, 0) - 1.0) < 1e-10); +var_dump(abs($zeroWeight->get(1, 0) - 2.0) < 1e-10); + +try { + $x->weightedLeastSquares($y, [1.0, -1.0, 1.0]); +} catch (ValueError $e) { + echo "negative weight rejected\n"; +} +try { + $x->weightedLeastSquares($y, [1.0, NAN, 1.0]); +} catch (Exception $e) { + echo "non-finite weight rejected\n"; +} +try { + $x->weightedLeastSquares($y, [1.0, 1.0]); +} catch (ValueError $e) { + echo "weight count rejected\n"; +} +try { + $x->weightedLeastSquares($y, [1.0, "2", 1.0]); +} catch (TypeError $e) { + echo "non-numeric weight rejected\n"; +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +negative weight rejected +non-finite weight rejected +weight count rejected +non-numeric weight rejected From fee4b805cf68e39c80ad365628a681ca1861f997 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:42:30 +0330 Subject: [PATCH 03/38] Add executable PHP weighted least squares example --- examples/wls.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 examples/wls.php diff --git a/examples/wls.php b/examples/wls.php new file mode 100644 index 0000000..c9e299f --- /dev/null +++ b/examples/wls.php @@ -0,0 +1,24 @@ +weightedLeastSquares($response, $weights); + +printf("intercept: %.6f\n", $coefficients->get(0, 0)); +printf("slope: %.6f\n", $coefficients->get(1, 0)); From b46edff5d1ea4a56196694fe15050a0761b5c122 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:42:37 +0330 Subject: [PATCH 04/38] Document PHP weighted least squares API --- docs/php-api.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/php-api.md b/docs/php-api.md index 65e4605..9965873 100644 --- a/docs/php-api.md +++ b/docs/php-api.md @@ -47,6 +47,19 @@ $betaFromNormalEquations = $xt The explicit formula is useful for teaching and for users who deliberately need that composition. It is not the numerical default: forming normal equations squares the condition number, and explicitly forming an inverse can amplify rounding error. The QR-based least-squares operation accepts one or more right-hand-side columns and requires the design matrix to have full column rank. It rejects rank-deficient and underdetermined designs with a generic `Exception`; a right-hand-side row-count mismatch raises `ValueError`. Non-finite values encountered by the QR/least-squares computation raise a generic `Exception`. For minimum-norm solutions to rank-deficient or underdetermined systems, the C API provides a pseudoinverse primitive, but this initial PHP façade does not expose it. +## Composing weighted least squares (WLS) + +Use the design matrix as the receiver and pass the response matrix plus one weight per observation: + +```php +$beta = $design->weightedLeastSquares($response, [1.0, 0.5, 2.0]); +``` + +Weights must be finite, non-negative numeric values, and their count must equal the number of rows in both the design and response matrices. A zero weight excludes that observation from the objective. The weighted design (after square-root row scaling) must have full column rank; otherwise the operation raises an exception. The method returns an independently owned Matrix and supports multiple response columns. + +The current native implementation applies square-root row scaling and delegates to the existing pivoted-QR least-squares solver. It materializes the scaled design and response matrices, so WLS currently uses additional memory proportional to the input matrices. No model-specific `fit_wls()` function is provided. See the runnable [WLS example](../examples/wls.php). + + ## Public methods - `Matrix::fromRows(array $rows): Matrix` @@ -58,6 +71,7 @@ The explicit formula is useful for teaching and for users who deliberately need - `$matrix->multiply(Matrix $other): Matrix` - `$matrix->inverse(): Matrix` - `$matrix->leastSquares(Matrix $rightHandSide): Matrix` +- `$matrix->weightedLeastSquares(Matrix $rightHandSide, array $weights): Matrix` All returned matrices own independent native storage. In particular, `transpose()` materializes its result, so returned objects do not depend on the lifetime of their source object. Cloning is deliberately unsupported because the native object owns its storage. From 1988e4c62635c160b7e825e24ee110d9bd4f9e00 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:42:40 +0330 Subject: [PATCH 05/38] Document PHP weighted least squares API --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2fc9c68..92ffc95 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,11 @@ The PHP-facing API currently includes: - `rows()`, `columns()`, and zero-based `get($row, $column)` - `transpose()`, `multiply(Matrix $other)`, and `inverse()` - `leastSquares(Matrix $rightHandSide)`, using pivoted QR +- `weightedLeastSquares(Matrix $rightHandSide, array $weights)`, using square-root row scaling and pivoted QR The least-squares operation supports one or more right-hand-side columns and requires a full-column-rank design matrix. Rank-deficient and underdetermined inputs are rejected explicitly; the initial PHP API does not expose a minimum-norm solution. Prefer `leastSquares()` to explicitly forming normal equations: forming (X^T X) squares the condition number, and explicitly forming an inverse can amplify rounding error. -See the [PHP API reference](docs/php-api.md) for exact contracts and exceptions, the [user guide](docs/user-guide.md) for examples and limitations, and [examples/ols.php](examples/ols.php) for the executable workflow. +See the [PHP API reference](docs/php-api.md) for exact contracts and exceptions, the [user guide](docs/user-guide.md) for examples and limitations, and [examples/ols.php](examples/ols.php) / [examples/wls.php](examples/wls.php) for executable OLS and WLS workflows. ## Architecture From 745fc61b051043b5f26adf56f5bb85bc6b56dff4 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:42:47 +0330 Subject: [PATCH 06/38] Document weighted least squares PHP workflow --- docs/user-guide.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/user-guide.md b/docs/user-guide.md index 80360b9..8a9ee26 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -61,6 +61,7 @@ Dimensions are positive counts. Element coordinates are **zero-based**, so get(0 | $matrix->multiply(Matrix $other) | Matrix multiplication | | $matrix->inverse() | Invert an invertible square matrix | | $matrix->leastSquares(Matrix $rightHandSide) | Solve least squares using pivoted QR | +| $matrix->weightedLeastSquares(Matrix $rightHandSide, array $weights) | Solve weighted least squares using square-root row scaling and pivoted QR | See the [PHP API reference](php-api.md) for exact signatures and error behavior. @@ -113,6 +114,21 @@ The explicit normal-equation expression (XᵀX)⁻¹Xᵀy can be composed from t The least-squares API accepts one or more right-hand-side columns and requires the design matrix to have full column rank. Rank-deficient and underdetermined designs are rejected. A minimum-norm solution for such systems is not available through this initial PHP façade. +## Example: weighted least squares + +WLS minimizes the sum of squared residuals weighted by the supplied observation weights. Pass one finite, non-negative numeric weight per row of the design and response matrices: + +~~~php +$weights = [1.0, 2.0, 2.0, 1.0]; +$beta = $X->weightedLeastSquares($y, $weights); +~~~ + +A zero weight excludes an observation. The effective weighted design must have full column rank; otherwise the operation raises an exception. The current native implementation materializes square-root-scaled design and response matrices before calling pivoted QR. Run the executable example with: + +~~~sh +php -d extension="$(pwd)/modules/numerus.so" examples/wls.php +~~~ + ## Errors and input constraints - fromRows() requires non-empty, equally sized row arrays. Elements must be PHP integers or floats. @@ -120,6 +136,7 @@ The least-squares API accepts one or more right-hand-side columns and requires t - Invalid dimensions, shapes, or coordinates raise ValueError. - Invalid PHP argument types raise TypeError. - Least-squares row-count mismatch raises ValueError; rank-deficient or underdetermined inputs and non-finite values encountered by the computation raise an exception. +- WLS requires one finite, non-negative numeric weight per observation. Weight-count mismatch and negative weights raise ValueError; non-finite weights raise an exception. - The current API does not promise a specialised exception class for numerical failures. ## Current scope: what is and is not exposed @@ -130,11 +147,12 @@ The least-squares API accepts one or more right-hand-side columns and requires t - Dimension and element access - Transpose, multiplication, inverse - Pivoted-QR least-squares solving -- Composition of an OLS coefficient workflow +- Weighted least-squares via square-root row scaling and pivoted QR +- Composition of OLS and WLS coefficient workflows ### Not currently exposed as PHP-level APIs -The repository contains additional internal C capabilities, but they are not part of the current PHP-facing API. Do not assume PHP callers can use ready-made OLS, WLS, GLS, or MLE estimators; a PHP-facing pseudoinverse; a complete statistical inference API; or all internal matrix views, decompositions, probability, RNG, and optimizer primitives. +The repository contains additional internal C capabilities, but they are not part of the current PHP-facing API. Do not assume PHP callers can use ready-made OLS, GLS, or MLE estimators; a PHP-facing pseudoinverse; a complete statistical inference API; or all internal matrix views, decompositions, probability, RNG, and optimizer primitives. Consult the [release roadmap](release-roadmap.md) for intended milestones, not as a list of shipped PHP features. From ce3c62873d89a70e7ed5f3f6aa85a26e0c2fd711 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:42:56 +0330 Subject: [PATCH 07/38] Align WLS milestone with PHP primitive API --- docs/release-roadmap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-roadmap.md b/docs/release-roadmap.md index c4d0e1f..43f307e 100644 --- a/docs/release-roadmap.md +++ b/docs/release-roadmap.md @@ -13,7 +13,7 @@ A release milestone is defined by a reliable, documented end-to-end workflow a P | Release | User-facing goal | Scope boundary | |---|---|---| | **1.0 — OLS-enabling foundation** | A PHP user can construct design/response matrices, compose operations, estimate OLS coefficients, and validate the result. | Ownership-safe PHP matrix API; multiplication/transpose and suitable stable solve/least-squares paths; explicit rank-deficiency semantics; PHPT/native tests, runnable PHP example, documentation, supported CI. No `fit_ols()`. | -| **1.1 — WLS-enabling foundation** | A PHP user can implement weighted least squares from primitives. | Weight application and composition using existing primitives; zero/unequal/invalid weights, scaling, and reference-equivalence tests. No `fit_wls()`. | +| **1.1 — WLS-enabling foundation** | A PHP user can implement weighted least squares from primitives. | Public `Matrix::weightedLeastSquares()` over the existing native weighted least-squares primitive; finite non-negative weights, zero/unequal/invalid weights, rank and dimension failures, reference-equivalence tests, executable PHP example, and API documentation. No `fit_wls()`. | | **1.2 — GLS-enabling foundation** | A PHP user can implement GLS with non-diagonal covariance. | Compose Cholesky, triangular solves/whitening, log-determinant where needed, and covariance validation. Avoid explicit covariance inversion. No `fit_gls()`. | | **1.3 — Linear-model inference primitives** | Users can compose common uncertainty and inference calculations for linear models. | Residuals, degrees of freedom, coefficient-covariance building blocks, and only the distribution functions required by a clearly scoped workflow. No ready-made model fitting. | | **1.4 — MLE-enabling foundation** | A PHP user can implement a likelihood and optimize it from PHP. | Generic objective/gradient callback surface, optimizer results and termination diagnostics, numerical-gradient behavior, failure semantics, and examples. No model-specific likelihood/estimator API. | From 08a8832c6d4519018d02fc9294bd90550fdebbe1 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:43:08 +0330 Subject: [PATCH 08/38] Fix PHP namespace syntax in WLS files --- tests/011-weighted-least-squares.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/011-weighted-least-squares.phpt b/tests/011-weighted-least-squares.phpt index 4ea68c4..fd79a01 100644 --- a/tests/011-weighted-least-squares.phpt +++ b/tests/011-weighted-least-squares.phpt @@ -2,7 +2,7 @@ Numerus weighted least-squares PHP API --FILE-- Date: Fri, 23 Oct 2026 01:43:11 +0330 Subject: [PATCH 09/38] Fix PHP namespace syntax in WLS files --- examples/wls.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/wls.php b/examples/wls.php index c9e299f..c48f412 100644 --- a/examples/wls.php +++ b/examples/wls.php @@ -1,6 +1,6 @@ Date: Fri, 23 Oct 2026 01:45:22 +0330 Subject: [PATCH 10/38] Strengthen WLS tests for unequal weights and multiple RHS --- tests/matrix_weighted_least_squares_test.c | 74 ++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/matrix_weighted_least_squares_test.c b/tests/matrix_weighted_least_squares_test.c index e3e2d98..e81a9e3 100644 --- a/tests/matrix_weighted_least_squares_test.c +++ b/tests/matrix_weighted_least_squares_test.c @@ -106,6 +106,78 @@ static void test_zero_weight_excludes_observation(void) numerus_matrix_destroy(matrix); } +static void test_unequal_weights_and_multiple_rhs(void) +{ + const double matrix_values[] = { + 1, 0, + 0, 1, + 1, 1 + }; + const double rhs_values[] = { + 1, 2, + 2, 4, + 4, 8 + }; + const double weights[] = {1, 2, 3}; + numerus_matrix *matrix = NULL; + numerus_matrix *rhs = NULL; + numerus_matrix *solution = NULL; + double value; + + assert(numerus_matrix_create_dense(3, 2, matrix_values, &matrix) == + NUMERUS_MATRIX_SUCCESS); + assert(numerus_matrix_create_dense(3, 2, rhs_values, &rhs) == + NUMERUS_MATRIX_SUCCESS); + assert(numerus_matrix_weighted_least_squares( + matrix, rhs, weights, 3, &solution + ) == NUMERUS_MATRIX_SUCCESS); + + /* Exact weighted normal-equation solution is [17/11, 25/11]^T. + * The second RHS is twice the first, so its coefficient column doubles. + * This catches implementations that accidentally ignore unequal weights. + */ + assert(numerus_matrix_rows(solution) == 2); + assert(numerus_matrix_columns(solution) == 2); + assert(numerus_matrix_get(solution, 0, 0, &value) == + NUMERUS_MATRIX_SUCCESS); + assert(fabs(value - 17.0 / 11.0) < 1e-9); + assert(numerus_matrix_get(solution, 1, 0, &value) == + NUMERUS_MATRIX_SUCCESS); + assert(fabs(value - 25.0 / 11.0) < 1e-9); + assert(numerus_matrix_get(solution, 0, 1, &value) == + NUMERUS_MATRIX_SUCCESS); + assert(fabs(value - 34.0 / 11.0) < 1e-9); + assert(numerus_matrix_get(solution, 1, 1, &value) == + NUMERUS_MATRIX_SUCCESS); + assert(fabs(value - 50.0 / 11.0) < 1e-9); + + numerus_matrix_destroy(solution); + numerus_matrix_destroy(rhs); + numerus_matrix_destroy(matrix); +} + +static void test_infinite_weight_is_rejected(void) +{ + const double matrix_values[] = {1, 0, 0, 1, 1, 1}; + const double rhs_values[] = {1, 2, 4}; + const double weights[] = {1, INFINITY, 1}; + numerus_matrix *matrix = NULL; + numerus_matrix *rhs = NULL; + numerus_matrix *solution = (void *) 1; + + assert(numerus_matrix_create_dense(3, 2, matrix_values, &matrix) == + NUMERUS_MATRIX_SUCCESS); + assert(numerus_matrix_create_dense(3, 1, rhs_values, &rhs) == + NUMERUS_MATRIX_SUCCESS); + assert(numerus_matrix_weighted_least_squares( + matrix, rhs, weights, 3, &solution + ) == NUMERUS_MATRIX_NON_FINITE); + assert(solution == NULL); + + numerus_matrix_destroy(rhs); + numerus_matrix_destroy(matrix); +} + static void test_validation_and_rank_failure(void) { const double matrix_values[] = {1, 0, 1, 0, 1, 1}; @@ -177,6 +249,8 @@ int main(void) { test_equal_weights_match_ordinary_least_squares(); test_zero_weight_excludes_observation(); + test_unequal_weights_and_multiple_rhs(); + test_infinite_weight_is_rejected(); test_validation_and_rank_failure(); test_nonfinite_and_read_failures(); puts("Matrix weighted least-squares tests passed."); From a9451e55b896be4feb1776d022f56781a59880d8 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:45:30 +0330 Subject: [PATCH 11/38] Run weighted least-squares tests under sanitizers --- .github/workflows/tests.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6b2b1e8..b35a529 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -762,6 +762,14 @@ jobs: -o tests/matrix_cholesky_sanitized tests/matrix_cholesky_test.c src/matrix/numerus_matrix_cholesky.c src/matrix/numerus_matrix.c src/matrix/numerus_matrix_view_nodes.c src/matrix/numerus_matrix_constructors.c src/storage/numerus_storage.c src/matrix/numerus_matrix_lu.c -lm ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 UBSAN_OPTIONS=halt_on_error=1 ./tests/matrix_cholesky_sanitized + - name: Run weighted least-squares tests with AddressSanitizer and UBSan + run: | + cc -Isrc/core -Isrc/storage -Isrc/matrix -Isrc/statistics -std=c11 -g -O1 -Wall -Wextra -Wpedantic -Werror \ + -fsanitize=address,undefined -fno-omit-frame-pointer \ + -DNUMERUS_MATRIX_USE_LIBC_ALLOC -DNUMERUS_STORAGE_USE_LIBC_ALLOC \ + -o tests/matrix_weighted_least_squares_sanitized tests/matrix_weighted_least_squares_test.c src/matrix/numerus_matrix_weighted_least_squares.c src/matrix/numerus_matrix_least_squares.c src/matrix/numerus_matrix_qr.c src/matrix/numerus_matrix.c src/matrix/numerus_matrix_view_nodes.c src/matrix/numerus_matrix_constructors.c src/storage/numerus_storage.c src/matrix/numerus_matrix_lu.c -lm + ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 UBSAN_OPTIONS=halt_on_error=1 ./tests/matrix_weighted_least_squares_sanitized + - name: Run Matrix QR tests with AddressSanitizer and UBSan run: | cc -Isrc/core -Isrc/storage -Isrc/matrix -Isrc/statistics -std=c11 -g -O1 -Wall -Wextra -Wpedantic -Werror \ From 98cfd34141a0b0b730a17c9525eaafe54d03f813 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:45:50 +0330 Subject: [PATCH 12/38] Cover WLS RHS shape and non-finite matrix data --- tests/matrix_weighted_least_squares_test.c | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/matrix_weighted_least_squares_test.c b/tests/matrix_weighted_least_squares_test.c index e81a9e3..7b1ce0d 100644 --- a/tests/matrix_weighted_least_squares_test.c +++ b/tests/matrix_weighted_least_squares_test.c @@ -218,6 +218,55 @@ static void test_validation_and_rank_failure(void) numerus_matrix_destroy(matrix); } +static void test_rhs_dimension_and_nonfinite_data(void) +{ + const double matrix_values[] = {1, 0, 0, 1, 1, 1}; + const double rhs_values[] = {1, 2, 4}; + const double short_rhs_values[] = {1, 2}; + const double weights[] = {1, 2, 3}; + const double nonfinite_matrix_values[] = {1, 0, 0, 1, NAN, 1}; + const double nonfinite_rhs_values[] = {1, INFINITY, 4}; + numerus_matrix *matrix = NULL; + numerus_matrix *rhs = NULL; + numerus_matrix *short_rhs = NULL; + numerus_matrix *bad_matrix = NULL; + numerus_matrix *bad_rhs = NULL; + numerus_matrix *solution = (void *) 1; + + assert(numerus_matrix_create_dense(3, 2, matrix_values, &matrix) == + NUMERUS_MATRIX_SUCCESS); + assert(numerus_matrix_create_dense(3, 1, rhs_values, &rhs) == + NUMERUS_MATRIX_SUCCESS); + assert(numerus_matrix_create_dense(2, 1, short_rhs_values, &short_rhs) == + NUMERUS_MATRIX_SUCCESS); + assert(numerus_matrix_weighted_least_squares( + matrix, short_rhs, weights, 3, &solution + ) == NUMERUS_MATRIX_DIMENSION_MISMATCH); + assert(solution == NULL); + + assert(numerus_matrix_create_dense( + 3, 2, nonfinite_matrix_values, &bad_matrix + ) == NUMERUS_MATRIX_SUCCESS); + assert(numerus_matrix_weighted_least_squares( + bad_matrix, rhs, weights, 3, &solution + ) == NUMERUS_MATRIX_NON_FINITE); + assert(solution == NULL); + + assert(numerus_matrix_create_dense( + 3, 1, nonfinite_rhs_values, &bad_rhs + ) == NUMERUS_MATRIX_SUCCESS); + assert(numerus_matrix_weighted_least_squares( + matrix, bad_rhs, weights, 3, &solution + ) == NUMERUS_MATRIX_NON_FINITE); + assert(solution == NULL); + + numerus_matrix_destroy(bad_rhs); + numerus_matrix_destroy(bad_matrix); + numerus_matrix_destroy(short_rhs); + numerus_matrix_destroy(rhs); + numerus_matrix_destroy(matrix); +} + static void test_nonfinite_and_read_failures(void) { const double matrix_values[] = {1, 0, 0, 1, 1, 1}; @@ -252,6 +301,7 @@ int main(void) test_unequal_weights_and_multiple_rhs(); test_infinite_weight_is_rejected(); test_validation_and_rank_failure(); + test_rhs_dimension_and_nonfinite_data(); test_nonfinite_and_read_failures(); puts("Matrix weighted least-squares tests passed."); return 0; From 0f558e7e152277dbe3833a4a75394116ae7261dc Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:46:06 +0330 Subject: [PATCH 13/38] Test unequal weights through PHP API --- tests/011-weighted-least-squares.phpt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/011-weighted-least-squares.phpt b/tests/011-weighted-least-squares.phpt index fd79a01..1045f29 100644 --- a/tests/011-weighted-least-squares.phpt +++ b/tests/011-weighted-least-squares.phpt @@ -17,6 +17,15 @@ $zeroWeight = $x2->weightedLeastSquares($y2, [1.0, 1.0, 0.0]); var_dump(abs($zeroWeight->get(0, 0) - 1.0) < 1e-10); var_dump(abs($zeroWeight->get(1, 0) - 2.0) < 1e-10); +$unequal = $x->weightedLeastSquares($y, [1.0, 2.0, 3.0]); +var_dump(abs($unequal->get(0, 0) - 17.0 / 11.0) < 1e-9); +var_dump(abs($unequal->get(1, 0) - 25.0 / 11.0) < 1e-9); + +$multiResponse = Matrix::fromRows([[1.0, 2.0], [2.0, 4.0], [4.0, 8.0]]); +$multi = $x->weightedLeastSquares($multiResponse, [1.0, 2.0, 3.0]); +var_dump($multi->rows() === 2 && $multi->columns() === 2); +var_dump(abs($multi->get(0, 1) - 34.0 / 11.0) < 1e-9); + try { $x->weightedLeastSquares($y, [1.0, -1.0, 1.0]); } catch (ValueError $e) { @@ -43,6 +52,9 @@ bool(true) bool(true) bool(true) bool(true) +bool(true) +bool(true) +bool(true) negative weight rejected non-finite weight rejected weight count rejected From 0249aa782a11525a6b1b8dba1cdf49886e8f389c Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:49:05 +0330 Subject: [PATCH 14/38] Fix WLS PHPT expected output count --- tests/011-weighted-least-squares.phpt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/011-weighted-least-squares.phpt b/tests/011-weighted-least-squares.phpt index 1045f29..cdb3a13 100644 --- a/tests/011-weighted-least-squares.phpt +++ b/tests/011-weighted-least-squares.phpt @@ -55,6 +55,7 @@ bool(true) bool(true) bool(true) bool(true) +bool(true) negative weight rejected non-finite weight rejected weight count rejected From 5c1a2ba2be21dbd7137105de969a2b4e7ecaa966 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:53:06 +0330 Subject: [PATCH 15/38] Make WLS example an executable API smoke test --- examples/wls.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/wls.php b/examples/wls.php index c48f412..a79bc0f 100644 --- a/examples/wls.php +++ b/examples/wls.php @@ -1,6 +1,8 @@ weightedLeastSquares($response, $weights); -printf("intercept: %.6f\n", $coefficients->get(0, 0)); -printf("slope: %.6f\n", $coefficients->get(1, 0)); +$intercept = $coefficients->get(0, 0); +$slope = $coefficients->get(1, 0); + +if (abs($intercept - 1.0) > 1e-10 || abs($slope - 1.0) > 1e-10) { + throw new RuntimeException('WLS coefficients differ from the expected fixture.'); +} + +printf("intercept: %.6f\\n", $intercept); +printf("slope: %.6f\\n", $slope); From 99f92131026bb8f4f9a8c081b95261eb4f24302f Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:53:17 +0330 Subject: [PATCH 16/38] Document WLS example alongside OLS smoke test --- docs/php-api.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/php-api.md b/docs/php-api.md index 9965873..e6d0a00 100644 --- a/docs/php-api.md +++ b/docs/php-api.md @@ -5,7 +5,7 @@ The initial PHP-facing API is intentionally small. It exposes general matrix ope ## Constructing matrices ```php -use Numerus\Matrix; +use Numerus\\Matrix; $design = Matrix::fromRows([ [1.0, 1.0], @@ -29,6 +29,16 @@ php -d extension="$(pwd)/modules/numerus.so" examples/ols.php The example estimates the intercept and slope for a small deterministic data set and checks the expected coefficients before printing them. It is intended as an executable API smoke test and a starting point for userland composition. +## End-to-end WLS example + +Run the weighted least-squares example the same way: + +```sh +php -d extension="$(pwd)/modules/numerus.so" examples/wls.php +``` + +Like the OLS example, this is an executable API smoke test: it uses a deterministic data set, applies one non-negative weight per observation, checks the expected intercept and slope, and prints the coefficients. It fails with a `RuntimeException` if the result differs from the fixture beyond the stated tolerance. + ## Composing OLS Numerus supports both a direct least-squares solve and the explicit normal-equation composition. @@ -59,7 +69,6 @@ Weights must be finite, non-negative numeric values, and their count must equal The current native implementation applies square-root row scaling and delegates to the existing pivoted-QR least-squares solver. It materializes the scaled design and response matrices, so WLS currently uses additional memory proportional to the input matrices. No model-specific `fit_wls()` function is provided. See the runnable [WLS example](../examples/wls.php). - ## Public methods - `Matrix::fromRows(array $rows): Matrix` From 3553e64b1c6f8021a9bf992556937bf53da18f86 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:56:50 +0330 Subject: [PATCH 17/38] Prepare documentation for Numerus 1.1.0 --- examples/wls.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/wls.php b/examples/wls.php index a79bc0f..0cc3a1f 100644 --- a/examples/wls.php +++ b/examples/wls.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Numerus\\Matrix; +use Numerus\Matrix; $design = Matrix::fromRows([ [1.0, 1.0], @@ -29,5 +29,5 @@ throw new RuntimeException('WLS coefficients differ from the expected fixture.'); } -printf("intercept: %.6f\\n", $intercept); -printf("slope: %.6f\\n", $slope); +printf("intercept: %.6f\n", $intercept); +printf("slope: %.6f\n", $slope); From 9ca2ab2549e89547a0e43f300142447eaa45264e Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:56:52 +0330 Subject: [PATCH 18/38] Prepare documentation for Numerus 1.1.0 --- docs/php-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/php-api.md b/docs/php-api.md index e6d0a00..cfa48f6 100644 --- a/docs/php-api.md +++ b/docs/php-api.md @@ -5,7 +5,7 @@ The initial PHP-facing API is intentionally small. It exposes general matrix ope ## Constructing matrices ```php -use Numerus\\Matrix; +use Numerus\Matrix; $design = Matrix::fromRows([ [1.0, 1.0], From 6a8f25f451515414b6dae76c78e08cac82b25cc3 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:56:55 +0330 Subject: [PATCH 19/38] Prepare extension version 1.1.0 --- php_numerus.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php_numerus.h b/php_numerus.h index 9a6315e..3ab8bff 100644 --- a/php_numerus.h +++ b/php_numerus.h @@ -14,6 +14,6 @@ extern zend_module_entry numerus_module_entry; #define phpext_numerus_ptr &numerus_module_entry -#define PHP_NUMERUS_VERSION "1.0.0" +#define PHP_NUMERUS_VERSION "1.1.0" #endif From a87286103f3e004951818403d1caeb1f54c953e3 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:56:57 +0330 Subject: [PATCH 20/38] Add 1.1.0 release notes --- CHANGELOG.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bff8da1..cbf9b72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ Notable user-facing changes are recorded here. This project follows Semantic Versioning for its public PHP API. +## 1.1.0 — Unreleased + +### Added +- Public `Matrix::weightedLeastSquares(Matrix $rightHandSide, array $weights): Matrix` API for weighted least-squares solving. +- WLS uses square-root row scaling and the existing pivoted-QR least-squares solver; it does not form weighted normal equations. +- Support for multiple right-hand-side columns and zero-weight observations. +- PHP and native regression tests for equal/unequal/zero weights, invalid weights, dimension mismatches, non-finite values, and solver failure cases. +- A runnable, checked WLS example and updated user/API documentation. + +### Numerical and compatibility notes +- Weights must be numeric, finite, non-negative, and supplied one per observation. A zero weight removes that observation's contribution to the objective, but input matrix values are still validated by the solver. +- The square-root-scaled design matrix must have full column rank. Rank-deficient and underdetermined weighted designs are rejected; minimum-norm solutions are not exposed through the PHP API. +- WLS materializes scaled design and response matrices, using additional memory proportional to their sizes. +- OLS remains available through `leastSquares()`; neither `fit_wls()` nor other model-specific estimator APIs are introduced. +- Linux CI targets PHP 8.2–8.5. Windows build configuration exists, but Windows support remains unverified because Windows build/test CI is not yet present. + ## 1.0.0 ### Added @@ -17,4 +33,3 @@ Notable user-facing changes are recorded here. This project follows Semantic Ver - Linux CI covers PHP 8.2–8.5, including debug builds, PHPTs, native tests, and native coverage. - Windows build configuration exists, but Windows support remains unverified because Windows build/test CI is not yet present. - Weighted least squares (WLS), generalized least squares (GLS), model inference, and MLE-enabling PHP workflows are outside the 1.0 scope and remain roadmap work. - From 11758113d6369cdc36ee0b4643328069b25e008a Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:00 +0330 Subject: [PATCH 21/38] Prepare documentation for Numerus 1.1.0 --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 92ffc95..0add8e3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The project intentionally exposes general-purpose primitives rather than model-s ## Current status -The initial PHP API and executable OLS workflow are implemented. The 1.0 release gate is tracked in the [release roadmap](docs/release-roadmap.md) and [PHP OLS release-gate audit](docs/php-api-release-audit.md). Linux CI currently covers PHP 8.2–8.5, a debug build, PHPTs, native tests, and native line/branch coverage. Windows build configuration exists, but Windows support remains unverified because Windows build/test CI is not yet present. +The 1.0 OLS foundation is established, and the 1.1 release candidate adds a tested PHP weighted least-squares (WLS) primitive. The 1.1 acceptance criteria are tracked in the [release roadmap](docs/release-roadmap.md) and [WLS release-gate audit](docs/php-api-wls-release-audit.md). Linux CI currently covers PHP 8.2–8.5, a debug build, PHPTs, native tests, and native line/branch coverage. Windows build configuration exists, but Windows support remains unverified because Windows build/test CI is not yet present. ## Quick start @@ -79,7 +79,7 @@ The PHP-facing API currently includes: The least-squares operation supports one or more right-hand-side columns and requires a full-column-rank design matrix. Rank-deficient and underdetermined inputs are rejected explicitly; the initial PHP API does not expose a minimum-norm solution. Prefer `leastSquares()` to explicitly forming normal equations: forming (X^T X) squares the condition number, and explicitly forming an inverse can amplify rounding error. -See the [PHP API reference](docs/php-api.md) for exact contracts and exceptions, the [user guide](docs/user-guide.md) for examples and limitations, and [examples/ols.php](examples/ols.php) / [examples/wls.php](examples/wls.php) for executable OLS and WLS workflows. +Run the checked WLS example with `php -d extension="$(pwd)/modules/numerus.so" examples/wls.php`. See the [PHP API reference](docs/php-api.md) for exact contracts and exceptions, the [user guide](docs/user-guide.md) for examples and limitations, and [examples/ols.php](examples/ols.php) / [examples/wls.php](examples/wls.php) for executable OLS and WLS workflows. ## Architecture @@ -171,7 +171,8 @@ Start at the [documentation index](docs/README.md): - [Developer guide](docs/developers/README.md) — architecture, testing, contribution workflow, and release engineering. - [PHP API reference](docs/php-api.md) — exact public method contracts. - [Release roadmap](docs/release-roadmap.md) — user-facing milestones and acceptance criteria. -- [Release-gate audit](docs/php-api-release-audit.md) — evidence and remaining limitations for 1.0. +- [1.0 OLS release-gate audit](docs/php-api-release-audit.md) — historical evidence for the 1.0 foundation. +- [1.1 WLS release-gate audit](docs/php-api-wls-release-audit.md) — acceptance evidence and remaining release checks. The user guide documents only capabilities exposed through PHP. Internal C primitives and planned roadmap items are labelled separately and should not be mistaken for shipped PHP APIs. From 24343428027e63f702bcc5180edb27407095f7fc Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:02 +0330 Subject: [PATCH 22/38] Prepare documentation for Numerus 1.1.0 --- docs/user-guide.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user-guide.md b/docs/user-guide.md index 8a9ee26..507c56c 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -1,6 +1,6 @@ # Numerus User Guide -Numerus is a native PHP extension for numerical-computing infrastructure. Its public PHP API currently focuses on matrix construction and operations that users can compose into workflows such as ordinary least squares (OLS). +Numerus is a native PHP extension for numerical-computing infrastructure. Its public PHP API focuses on matrix construction and operations that users can compose into ordinary least squares (OLS) and weighted least squares (WLS) workflows. **Important:** Numerus does not currently expose ready-made statistical model methods such as fit_ols(), fit_wls(), fit_gls(), or fit_mle(). Users compose general-purpose operations themselves. @@ -123,7 +123,7 @@ $weights = [1.0, 2.0, 2.0, 1.0]; $beta = $X->weightedLeastSquares($y, $weights); ~~~ -A zero weight excludes an observation. The effective weighted design must have full column rank; otherwise the operation raises an exception. The current native implementation materializes square-root-scaled design and response matrices before calling pivoted QR. Run the executable example with: +A zero weight removes that observation's contribution to the weighted objective. The implementation still validates the design and response data, including values in zero-weight rows. The effective weighted design must have full column rank; otherwise the operation raises an exception. WLS materializes square-root-scaled design and response matrices before calling pivoted QR, so it needs additional memory proportional to the inputs. Run the executable example with: ~~~sh php -d extension="$(pwd)/modules/numerus.so" examples/wls.php @@ -136,7 +136,7 @@ php -d extension="$(pwd)/modules/numerus.so" examples/wls.php - Invalid dimensions, shapes, or coordinates raise ValueError. - Invalid PHP argument types raise TypeError. - Least-squares row-count mismatch raises ValueError; rank-deficient or underdetermined inputs and non-finite values encountered by the computation raise an exception. -- WLS requires one finite, non-negative numeric weight per observation. Weight-count mismatch and negative weights raise ValueError; non-finite weights raise an exception. +- WLS requires one finite, non-negative numeric weight per observation. Weight-count mismatch and negative weights raise ValueError; non-finite weights raise an exception. String numerals are not accepted as numeric weights. - The current API does not promise a specialised exception class for numerical failures. ## Current scope: what is and is not exposed From 4c24728faa402714ae52078d053423649df3c7f6 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:04 +0330 Subject: [PATCH 23/38] Prepare documentation for Numerus 1.1.0 --- docs/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 01c2da7..77ca8d0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,9 +4,10 @@ Numerus is a native PHP extension providing composable numerical-computing primi ## User guide -- [User guide](user-guide.md) — requirements, build instructions, Matrix API, OLS example, errors, and current limitations. +- [User guide](user-guide.md) — requirements, build instructions, Matrix API, OLS/WLS examples, errors, and current limitations. - [PHP API reference](php-api.md) — currently exposed PHP methods. - [Runnable OLS example](../examples/ols.php) — executable example source. +- [Runnable WLS example](../examples/wls.php) — executable example source. ## Developer and contributor guide From ff2b541d91a138d8f6a9d7060e8b1568c4662bb5 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:07 +0330 Subject: [PATCH 24/38] Prepare documentation for Numerus 1.1.0 --- docs/developers/releases.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/developers/releases.md b/docs/developers/releases.md index 31a4971..ed52a32 100644 --- a/docs/developers/releases.md +++ b/docs/developers/releases.md @@ -19,7 +19,7 @@ Keep the extension version macro in `php_numerus.h`, Git tag, release notes, and - [ ] Ensure public API documentation matches registered methods and PHPT contracts. - [ ] Run PHPT and native C tests. - [ ] Verify required CI jobs on the exact release commit. -- [ ] Verify the **Source archive smoke test** passes on the exact release commit. This workflow creates an archive from tracked Git files, extracts it without a working `.git` directory, builds the extension, runs PHPTs, verifies the extension version, and executes the OLS example. +- [ ] Verify the **Source archive smoke test** passes on the exact release commit. This workflow creates an archive from tracked Git files, extracts it without a working `.git` directory, builds the extension, runs PHPTs, verifies the extension version, and executes both OLS and WLS examples. - [ ] Review supported PHP versions and state unverified platforms honestly (Windows is currently unverified). - [ ] Review ownership, error behavior, numerical limitations, and known issues. - [ ] Verify extension version, Git tag, release notes, and package metadata agree. @@ -30,9 +30,9 @@ Keep the extension version macro in `php_numerus.h`, Git tag, release notes, and 1. Merge release-preparation changes only after the regular test, native coverage, and source-archive workflows pass. 2. Confirm the merged commit on `main` has all three workflows green. Do not tag a PR head or a commit whose checks are still running. -3. Review the final diff and verify `PHP_NUMERUS_VERSION` and the `CHANGELOG.md` release entry both describe `1.0.0`. -4. Create an annotated Git tag named `1.0.0` on that exact green `main` commit. -5. Create a GitHub Release from tag `1.0.0`, using the matching changelog entry and including the source-build prerequisites and the Windows support caveat. +3. Review the final diff and verify `PHP_NUMERUS_VERSION` and the `CHANGELOG.md` release entry both describe `1.1.0`. +4. Create an annotated Git tag named `1.1.0` on that exact green `main` commit. +5. Create a GitHub Release from tag `1.1.0`, using the matching changelog entry and including source-build prerequisites, WLS numerical limitations, and the Windows support caveat. 6. Verify that the release page and auto-generated source archives are available and correspond to the tagged commit. Do not create a tag or publish a release while any required workflow is pending or failing. This repository currently has no package manifest for a registry-specific distribution; do not imply PIE, Packagist, or PECL installation support unless that distribution is separately implemented and verified. @@ -45,4 +45,4 @@ The packaging choice should not change release milestone scope or force a major- ## No premature stable release -Do not label Numerus 1.0.0 stable until the documented 1.0 gate has passed. A green build is necessary but not sufficient: the PHP API must be coherent, tested, documented, and usable in a clean installation workflow. +Do not publish a release until its documented milestone gate has passed. A green build is necessary but not sufficient: the PHP API must be coherent, tested, documented, and usable in a clean installation workflow. For 1.1.0, also verify the WLS example and WLS PHPT from a clean source archive. From ad27ddb3b50f6f9f81c6df788b4a0a9698fc0a9d Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:10 +0330 Subject: [PATCH 25/38] Verify WLS in release source archive smoke test --- .github/workflows/source-archive.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/source-archive.yml b/.github/workflows/source-archive.yml index 7b4dcbe..378fa69 100644 --- a/.github/workflows/source-archive.yml +++ b/.github/workflows/source-archive.yml @@ -68,8 +68,8 @@ jobs: run: | set -euo pipefail php -d extension="$(pwd)/modules/numerus.so" -r ' - if (phpversion("numerus") !== "1.0.0") { - fwrite(STDERR, "Expected Numerus 1.0.0, got " . var_export(phpversion("numerus"), true) . PHP_EOL); + if (phpversion("numerus") !== "1.1.0") { + fwrite(STDERR, "Expected Numerus 1.1.0, got " . var_export(phpversion("numerus"), true) . PHP_EOL); exit(1); } ' @@ -77,3 +77,7 @@ jobs: printf '%s\n' "$output" grep -Fxq 'intercept: 1.000000' <<< "$output" grep -Fxq 'slope: 2.000000' <<< "$output" + wls_output="$(php -d extension="$(pwd)/modules/numerus.so" examples/wls.php)" + printf '%s\\n' "$wls_output" + grep -Fxq 'intercept: 1.000000' <<< "$wls_output" + grep -Fxq 'slope: 1.000000' <<< "$wls_output" From 5af36f84c84acaece8fb81899b77f401496ab921 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:12 +0330 Subject: [PATCH 26/38] Prepare documentation for Numerus 1.1.0 --- docs/release-roadmap.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/release-roadmap.md b/docs/release-roadmap.md index 43f307e..7802318 100644 --- a/docs/release-roadmap.md +++ b/docs/release-roadmap.md @@ -31,6 +31,16 @@ Milestones after 1.0 are provisional planning targets. Combine or split minor re 6. Supported PHP 8.2–8.5 CI and required debug/sanitizer checks pass. Unverified platforms, including Windows until its build/test job passes, are clearly identified. 7. API contracts, numerical limitations, and the supported workflow are documented. README does not claim PHP-userland readiness before these gates pass. +## 1.1 definition of done + +1. The public `Matrix::weightedLeastSquares(Matrix $rightHandSide, array $weights): Matrix` signature and PHP named-argument metadata are documented and tested. +2. Weights are finite, non-negative integers/floats, one per observation; zero weights contribute no residual term. Dimension, type, non-finite, rank-deficiency, and multiple-RHS behavior are covered. +3. The native solver uses square-root row scaling plus the existing pivoted-QR path and does not form `XᵀWX`. +4. The executable WLS example validates a deterministic expected result, and the PHPT suite executes it end to end. +5. README, user guide, API reference, changelog, version metadata, source-archive workflow, and this release audit agree on the 1.1 scope. +6. Regular PHP 8.2–8.5 CI, native coverage, and clean source-archive smoke test pass on the exact release commit. +7. Windows remains explicitly unverified until a Windows build/test job passes. WLS is a matrix primitive, not a `fit_wls()` model API. + ## Execution order The original implementation sequence is complete: [#147 — Minimal PHP-facing numerical API](https://github.com/ephpicman/Numerus/issues/147) was delivered in PRs #194–#197, and [#148 — rank-deficient/underdetermined least-squares semantics](https://github.com/ephpicman/Numerus/issues/148) was resolved and its PHP-visible behavior is covered by PHPTs. @@ -41,13 +51,13 @@ The current release sequence is: 2. Verify post-merge main CI for the intended release-candidate commit: PHP 8.2–8.5, debug build, native coverage, and all PHPTs. 3. Confirm the executable OLS example and published PHP API documentation match the current implementation. 4. Keep Windows explicitly unverified until a Windows build and test job passes. -5. Only then decide whether the 1.0 release gate is satisfied; do not equate merged feature PRs with release approval. +5. Only then decide whether the relevant release gate is satisfied; do not equate merged feature PRs with release approval. ## Relationship to the foundation roadmap The [v1 statistical foundation roadmap](v1-statistical-foundation-roadmap.md) tracks a broader set of C-level capabilities, including optimization, RNG, and probability primitives. Its P0/P1 labels prioritize work within that foundation inventory; they do **not** mean that every P0 capability is mandatory for product release 1.0. The 1.0 release gate is the OLS-enabling PHP workflow above. Later milestones select the next coherent user-facing workflow from the broader inventory. -The [issue execution order](v1-issue-execution-order.md) records the historical foundation queue and its completion evidence. The [PHP 1.0 OLS release-gate audit](php-api-release-audit.md) tracks concrete userland evidence and remaining limitations. For new release work, follow the release milestone and execution order in this document, while preserving unresolved foundation requirements where they are dependencies or necessary correctness fixes. +The [issue execution order](v1-issue-execution-order.md) records the historical foundation queue and its completion evidence. The [PHP 1.0 OLS release-gate audit](php-api-release-audit.md) tracks the historical OLS gate; the [PHP 1.1 WLS release-gate audit](php-api-wls-release-audit.md) tracks WLS release evidence and remaining limitations. For new release work, follow the release milestone and execution order in this document, while preserving unresolved foundation requirements where they are dependencies or necessary correctness fixes. ## Non-goals From afb33af692cda83d877075e1b496a050f6f57a43 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:31 +0330 Subject: [PATCH 27/38] Test WLS example end to end --- tests/012-wls-example.phpt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/012-wls-example.phpt diff --git a/tests/012-wls-example.phpt b/tests/012-wls-example.phpt new file mode 100644 index 0000000..bc89c12 --- /dev/null +++ b/tests/012-wls-example.phpt @@ -0,0 +1,15 @@ +--TEST-- +Numerus PHP WLS example runs end to end +--SKIPIF-- + +--FILE-- + +--EXPECT-- +intercept: 1.000000 +slope: 1.000000 From f42c618a8f2d473ff56b740917f9262da41d954b Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:33 +0330 Subject: [PATCH 28/38] Test WLS named argument metadata --- ...eighted-least-squares-named-arguments.phpt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/013-weighted-least-squares-named-arguments.phpt diff --git a/tests/013-weighted-least-squares-named-arguments.phpt b/tests/013-weighted-least-squares-named-arguments.phpt new file mode 100644 index 0000000..42f5e51 --- /dev/null +++ b/tests/013-weighted-least-squares-named-arguments.phpt @@ -0,0 +1,36 @@ +--TEST-- +Numerus Matrix weightedLeastSquares exposes documented named arguments +--SKIPIF-- + +--FILE-- +weightedLeastSquares( + rightHandSide: $response, + weights: [1.0, 1.0, 1.0], +); +var_dump(abs($beta->get(0, 0) - 1.0) < 1e-10); +var_dump(abs($beta->get(1, 0) - 2.0) < 1e-10); + +$parameters = (new ReflectionMethod(Matrix::class, 'weightedLeastSquares')) + ->getParameters(); +var_dump($parameters[0]->getName()); +var_dump($parameters[1]->getName()); +?> +--EXPECT-- +bool(true) +bool(true) +string(13) "rightHandSide" +string(7) "weights" From 5fe33841b5c4ef85e4b64220e7210eec013bcf21 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:35 +0330 Subject: [PATCH 29/38] Add WLS 1.1 release gate audit --- docs/php-api-wls-release-audit.md | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/php-api-wls-release-audit.md diff --git a/docs/php-api-wls-release-audit.md b/docs/php-api-wls-release-audit.md new file mode 100644 index 0000000..650846e --- /dev/null +++ b/docs/php-api-wls-release-audit.md @@ -0,0 +1,32 @@ +# PHP 1.1 WLS Release-Gate Audit + +This audit tracks the user-facing 1.1 milestone in the [release roadmap](release-roadmap.md). The prior [1.0 OLS audit](php-api-release-audit.md) remains historical evidence for the initial API. + +## Acceptance evidence + +| Gate | Evidence | Assessment | +|---|---|---| +| Public PHP API | `Matrix::weightedLeastSquares(Matrix $rightHandSide, array $weights): Matrix` is registered with named-argument metadata | Implemented; named arguments are covered by `tests/013-weighted-least-squares-named-arguments.phpt` | +| Numerical algorithm | Native implementation scales design and RHS rows by square-root weights and delegates to pivoted QR; it does not form `XᵀWX` | Implemented | +| OLS equivalence | Equal weights reproduce `leastSquares()` on a deterministic fixture | Covered by `tests/011-weighted-least-squares.phpt` | +| Weight semantics | Unequal weights, zero weight, negative weight, non-finite weight, wrong count, and non-numeric type | Covered by PHP and native WLS tests | +| Shape and data validation | Multiple RHS, RHS row mismatch, non-finite matrix values, infinite weights, and rank/solver failures | Covered by native tests and PHP-facing validation paths | +| Runnable user workflow | `examples/wls.php` checks expected coefficients; `tests/012-wls-example.phpt` executes it end to end | Implemented | +| User-facing documentation | README, user guide, API reference, documentation index, changelog, and roadmap describe WLS and its limits | Updated for 1.1 | +| Version and clean archive | `php_numerus.h` identifies version 1.1.0; source-archive workflow verifies that version and runs OLS and WLS examples | Workflow updated; final result must be green on the exact release commit | +| Supported Linux CI | PHP 8.2–8.5 matrix, debug build, PHPT suite, native tests, sanitizer-oriented jobs, native coverage | Must be verified on the exact release-preparation commit and again after merge to `main` | +| Windows | `config.w32` exists, but Windows build/test CI is not present | Unverified; do not claim Windows support is validated | + +## Public contract and known limitations + +- The design matrix has shape `n × p`; the RHS has `n` rows and may contain one or more columns. The result has shape `p × k`. +- Supply exactly one PHP integer or float weight per observation. Weights must be finite and non-negative. Numeric strings are rejected. +- A zero weight removes that observation's contribution to the weighted objective. The current implementation still validates matrix entries in zero-weight rows; zero weight does not promise to ignore non-finite input data. +- The square-root-scaled design must have full column rank. Rank-deficient and underdetermined problems are rejected; the PHP API does not return a minimum-norm solution. +- The native implementation materializes scaled design and RHS matrices, requiring additional memory proportional to their sizes. +- Numerical solver failures are reported as exceptions; invalid dimensions and negative weights are reported as `ValueError`, non-numeric PHP weight values as `TypeError`, and non-finite weights as numerical exceptions. +- This release adds a reusable matrix primitive, not a model-specific `fit_wls()` API. GLS, inference workflows, and MLE workflows remain outside the 1.1 scope. + +## Release decision + +**Status: release preparation; not yet published.** Do not create tag `1.1.0` or publish a GitHub Release until the source-archive smoke test, the PHP 8.2–8.5 test matrix, native coverage, and post-merge `main` checks are green on the exact intended release commit. Review the final diff and changelog before tagging. From d7ad1efdadebfb4b9a015c0fe75614eb6ff15a61 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:46 +0330 Subject: [PATCH 30/38] Finalize 1.1 release documentation and smoke tests --- .github/workflows/source-archive.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/source-archive.yml b/.github/workflows/source-archive.yml index 378fa69..e5bc21d 100644 --- a/.github/workflows/source-archive.yml +++ b/.github/workflows/source-archive.yml @@ -62,7 +62,7 @@ jobs: working-directory: ${{ env.archive_path }} run: make test TESTS='tests/*.phpt' - - name: Verify extension version and OLS example + - name: Verify extension version, OLS, and WLS examples working-directory: ${{ env.archive_path }} shell: bash run: | @@ -78,6 +78,6 @@ jobs: grep -Fxq 'intercept: 1.000000' <<< "$output" grep -Fxq 'slope: 2.000000' <<< "$output" wls_output="$(php -d extension="$(pwd)/modules/numerus.so" examples/wls.php)" - printf '%s\\n' "$wls_output" + printf '%s\n' "$wls_output" grep -Fxq 'intercept: 1.000000' <<< "$wls_output" grep -Fxq 'slope: 1.000000' <<< "$wls_output" From f432f87afc730155929ae29971233fbf5a6c6a96 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:49 +0330 Subject: [PATCH 31/38] Finalize 1.1 release documentation and smoke tests --- docs/release-roadmap.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/release-roadmap.md b/docs/release-roadmap.md index 7802318..79676be 100644 --- a/docs/release-roadmap.md +++ b/docs/release-roadmap.md @@ -48,10 +48,10 @@ The original implementation sequence is complete: [#147 — Minimal PHP-facing n The current release sequence is: 1. Keep the release-gate audit synchronized with the shipped PHP API and tests. -2. Verify post-merge main CI for the intended release-candidate commit: PHP 8.2–8.5, debug build, native coverage, and all PHPTs. -3. Confirm the executable OLS example and published PHP API documentation match the current implementation. +2. Verify the PHP 8.2–8.5 test matrix, debug build, native coverage, and source-archive smoke test on the intended 1.1.0 release-candidate commit. +3. Confirm the executable OLS and WLS examples, public API documentation, changelog, and version metadata match the implementation. 4. Keep Windows explicitly unverified until a Windows build and test job passes. -5. Only then decide whether the relevant release gate is satisfied; do not equate merged feature PRs with release approval. +5. Review the [PHP 1.1 WLS release-gate audit](php-api-wls-release-audit.md), then decide whether the 1.1 gate is satisfied; do not equate merged feature PRs with release approval. ## Relationship to the foundation roadmap From 44fefbfe1cd756af45609469e5b65e29a944c003 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:51 +0330 Subject: [PATCH 32/38] Finalize 1.1 release documentation and smoke tests --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0add8e3..8cf380b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Native coverage](https://github.com/ephpicman/Numerus/actions/workflows/coverage.yml/badge.svg?branch=main)](https://github.com/ephpicman/Numerus/actions/workflows/coverage.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -Numerus is a native PHP extension that provides reusable numerical-computing primitives. Its current PHP-facing API focuses on matrix operations that can be composed into workflows such as ordinary least squares (OLS). +Numerus is a native PHP extension that provides reusable numerical-computing primitives. Its current PHP-facing API focuses on matrix operations that can be composed into ordinary least squares (OLS) and weighted least squares (WLS) workflows. The project intentionally exposes general-purpose primitives rather than model-specific functions such as `fit_ols()`, `fit_wls()`, `fit_gls()`, or `fit_mle()`. @@ -150,6 +150,7 @@ The allocator defines are for standalone test executables only; the extension bu - `src/statistics/` — RNG, sampling, probability, and optimizer primitives. - Root-level C headers are forwarding includes retained for existing native test include paths; declarations and implementations live under `src/`. - `examples/ols.php` — runnable PHP OLS coefficient workflow. +- `examples/wls.php` — runnable PHP WLS coefficient workflow. - `tests/` — PHPT and native C tests. - `.github/workflows/` — build/test matrix and native coverage workflows. @@ -167,7 +168,7 @@ The allocator defines are for standalone test executables only; the extension bu Start at the [documentation index](docs/README.md): -- [User guide](docs/user-guide.md) — installation, current PHP API, OLS example, and limitations. +- [User guide](docs/user-guide.md) — installation, current PHP API, OLS/WLS examples, and limitations. - [Developer guide](docs/developers/README.md) — architecture, testing, contribution workflow, and release engineering. - [PHP API reference](docs/php-api.md) — exact public method contracts. - [Release roadmap](docs/release-roadmap.md) — user-facing milestones and acceptance criteria. From 81851eea16f7682df2afdfb0e0de82606eba65ee Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:57:53 +0330 Subject: [PATCH 33/38] Finalize 1.1 release documentation and smoke tests --- docs/php-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/php-api.md b/docs/php-api.md index cfa48f6..7f441d3 100644 --- a/docs/php-api.md +++ b/docs/php-api.md @@ -1,6 +1,6 @@ # PHP Matrix API -The initial PHP-facing API is intentionally small. It exposes general matrix operations rather than statistical estimators. +The PHP-facing API is intentionally small. It exposes general matrix operations rather than model-specific statistical estimators. ## Constructing matrices @@ -65,7 +65,7 @@ Use the design matrix as the receiver and pass the response matrix plus one weig $beta = $design->weightedLeastSquares($response, [1.0, 0.5, 2.0]); ``` -Weights must be finite, non-negative numeric values, and their count must equal the number of rows in both the design and response matrices. A zero weight excludes that observation from the objective. The weighted design (after square-root row scaling) must have full column rank; otherwise the operation raises an exception. The method returns an independently owned Matrix and supports multiple response columns. +Weights must be finite, non-negative PHP integers or floats, and their count must equal the number of rows in both the design and response matrices. Numeric strings are not accepted. A zero weight excludes that observation from the objective, but values in that row are still validated. The weighted design (after square-root row scaling) must have full column rank; otherwise the operation raises an exception. The method returns an independently owned Matrix and supports multiple response columns. PHP named arguments are `rightHandSide` and `weights`; both names are part of the public API contract. The current native implementation applies square-root row scaling and delegates to the existing pivoted-QR least-squares solver. It materializes the scaled design and response matrices, so WLS currently uses additional memory proportional to the input matrices. No model-specific `fit_wls()` function is provided. See the runnable [WLS example](../examples/wls.php). From 2ebf91348e3b0ae94bebbec194a6ad12bed65820 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:58:28 +0330 Subject: [PATCH 34/38] Make WLS guide example self-contained --- docs/user-guide.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/user-guide.md b/docs/user-guide.md index 507c56c..cdce6b5 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -119,7 +119,15 @@ The least-squares API accepts one or more right-hand-side columns and requires t WLS minimizes the sum of squared residuals weighted by the supplied observation weights. Pass one finite, non-negative numeric weight per row of the design and response matrices: ~~~php +$X = Matrix::fromRows([ + [1.0, 1.0], + [1.0, 2.0], + [1.0, 3.0], + [1.0, 4.0], +]); +$y = Matrix::fromRows([[2.0], [2.9], [4.2], [4.8]]); $weights = [1.0, 2.0, 2.0, 1.0]; + $beta = $X->weightedLeastSquares($y, $weights); ~~~ From 316c08cc45348e4a238288eb7124830ea1b623cf Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:58:57 +0330 Subject: [PATCH 35/38] Update module version assertion for 1.1.0 --- tests/002-version.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/002-version.phpt b/tests/002-version.phpt index 310d535..5ab2443 100644 --- a/tests/002-version.phpt +++ b/tests/002-version.phpt @@ -11,4 +11,4 @@ if (!extension_loaded('numerus')) { echo phpversion('numerus'), PHP_EOL; ?> --EXPECT-- -1.0.0 +1.1.0 From 54262c8159037a0e3f380dcc31a5427cc3655c61 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:59:09 +0330 Subject: [PATCH 36/38] Align release policy with the 1.1 minor release --- docs/developers/releases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developers/releases.md b/docs/developers/releases.md index ed52a32..5f3f6a3 100644 --- a/docs/developers/releases.md +++ b/docs/developers/releases.md @@ -9,7 +9,7 @@ Use Semantic Versioning for the public PHP API: - **Minor**: backward-compatible new public capabilities. - **Major**: intentional incompatible changes to the public API or documented behavior. -Before 1.0, public API contracts may still evolve; nevertheless, avoid gratuitous breaking changes and document intentional ones. A 0.x version should not be represented as a stable 1.0 contract. +After 1.0, the public API is a compatibility commitment. Avoid gratuitous breaking changes; document intentional behavior changes and treat parameter names, error behavior, and numerical semantics as part of the contract. Keep the extension version macro in `php_numerus.h`, Git tag, release notes, and any package metadata synchronized. Do not bump versions merely because internal files were reorganized unless a release is being cut. From 218543990ca435d0b28c74cc66f4a1983a07c267 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:59:12 +0330 Subject: [PATCH 37/38] Align release policy with the 1.1 minor release --- docs/release-roadmap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-roadmap.md b/docs/release-roadmap.md index 79676be..fae24ed 100644 --- a/docs/release-roadmap.md +++ b/docs/release-roadmap.md @@ -1,6 +1,6 @@ # Numerus Release Roadmap -This document defines the **user-facing release milestones** for Numerus. It is the source of truth for what a version means to a PHP user. The [v1 statistical foundation roadmap](v1-statistical-foundation-roadmap.md) remains the detailed inventory of reusable numerical capabilities and engineering gaps; its priority labels are not, by themselves, a promise that every listed capability must ship in 1.0. +This document defines the **user-facing release milestones** for Numerus. It is the source of truth for what a version means to a PHP user. The [v1 statistical foundation roadmap](v1-statistical-foundation-roadmap.md) remains the detailed inventory of reusable numerical capabilities and engineering gaps; its priority labels are not, by themselves, a promise that every listed capability must ship in any particular release. ## Product principle From d54a1e551dcd293fe55aa6ce1ad1dc031736f875 Mon Sep 17 00:00:00 2001 From: Sina Kuhestani Date: Fri, 23 Oct 2026 01:59:23 +0330 Subject: [PATCH 38/38] Index the 1.1 release audit --- docs/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/README.md b/docs/README.md index 77ca8d0..b48a787 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,6 +26,8 @@ These documents describe implementation details and internal C APIs. They are no - [Native test coverage](native-test-coverage.md) - [Statistical API boundary](statistical-api-boundary.md) - [Release roadmap](release-roadmap.md) +- [PHP 1.1 WLS release-gate audit](php-api-wls-release-audit.md) +- [Historical PHP 1.0 OLS release-gate audit](php-api-release-audit.md) - [Statistical foundation roadmap](v1-statistical-foundation-roadmap.md) ## Documentation rule