M2.5 — common-core unification: RECONCILIATION REPORT

Date: 2026-05-21 (same merge sprint as M0/M1/M2) Branch: merge/import-donor Tip: pushed as part of the M2.5 commit (this report ships with it)

Decision summary

The donor (nirs4all-methods) and the base (pls4all) both ship a “common-core” of infrastructure: context, status, matrix_view, version, parallel, linalg.hpp. A side-by-side comparison shows the implementations are 100 % equivalent — the only difference is the symbol/namespace prefix (n4m_* / n4m::core vs n4m_* / n4m::core). Enums, struct fields, error-buffer size, and backend codes all map 1-for-1.

Per roadmap §11.4, the canonical prefix going forward is n4m_* (versioned ELF symbols @@N4M_1 on Linux). The donor’s files are therefore the unified winners as-is — no semantic rewrite needed.

Comparison evidence

context.cpp      pls4all=2632  donor=2620   delta=12 bytes (n4m_ vs n4m_ on each token)
context.hpp      pls4all=3173  donor=3157   delta=16 bytes (same prefix-only diff)
status.cpp       pls4all=1752  donor=1744   delta=8  bytes
status.hpp       pls4all=437   donor=425    delta=12 bytes
matrix_view.cpp  pls4all=1484  donor=1476   delta=8  bytes
matrix_view.hpp  pls4all=948   donor=932    delta=16 bytes
version.cpp      pls4all=1110  donor=1098   delta=12 bytes
version.hpp      pls4all=995   donor=979    delta=16 bytes
parallel.hpp     pls4all=1422  donor=1410   delta=12 bytes
linalg.hpp       pls4all=10968 donor=10936  delta=32 bytes

After normalising the pls4all-side prefix (p4a/n4m, P4A/N4M, n4m::core/n4m::core, n4m::cuda_dispatch/n4m::cuda_dispatch, pls4all/p4a.h/n4m/n4m.h) the diffs vanish on every pair. The pls4all files are a mechanical rename of the same source.

Enum reconciliation

Code

Donor

pls4all

0

N4M_OK

N4M_OK

1

N4M_ERR_INVALID_ARGUMENT

N4M_ERR_INVALID_ARGUMENT

2

N4M_ERR_NULL_POINTER

N4M_ERR_NULL_POINTER

3

N4M_ERR_SHAPE_MISMATCH

N4M_ERR_SHAPE_MISMATCH

4

N4M_ERR_DTYPE_MISMATCH

N4M_ERR_DTYPE_MISMATCH

5

N4M_ERR_STRIDE_INVALID

N4M_ERR_STRIDE_INVALID

6

N4M_ERR_NOT_FITTED

N4M_ERR_NOT_FITTED

7

N4M_ERR_NUMERICAL_FAILURE

N4M_ERR_NUMERICAL_FAILURE

8

N4M_ERR_CONVERGENCE_FAILED

N4M_ERR_CONVERGENCE_FAILED

9

N4M_ERR_OUT_OF_MEMORY

N4M_ERR_OUT_OF_MEMORY

10

N4M_ERR_UNSUPPORTED

N4M_ERR_UNSUPPORTED

11

N4M_ERR_NOT_IMPLEMENTED

N4M_ERR_NOT_IMPLEMENTED

12

N4M_ERR_ABI_MISMATCH

N4M_ERR_ABI_MISMATCH

13

N4M_ERR_IO

N4M_ERR_IO

14

N4M_ERR_CORRUPT_BUFFER

N4M_ERR_CORRUPT_BUFFER

15

N4M_ERR_VERSION_INCOMPATIBLE

N4M_ERR_VERSION_INCOMPATIBLE

16

N4M_ERR_BACKEND_UNAVAILABLE

N4M_ERR_BACKEND_UNAVAILABLE

17

N4M_ERR_CANCELLED

N4M_ERR_CANCELLED

255

N4M_ERR_INTERNAL

N4M_ERR_INTERNAL

Identical numeric values across the board. M5 (ABI rename) will collapse this into the n4m_* names; until then the M5-generated mapping table (in scripts/migrate_p4a_to_n4m.py) treats them as straight renames.

Backend enum reconciliation

Code

Name

Same in both sides

0

*_BACKEND_AUTO

1

*_BACKEND_REFERENCE_CPU

2

*_BACKEND_NATIVE_CPU

3

*_BACKEND_BLAS

4

*_BACKEND_OPENMP

5

*_BACKEND_CUDA

Struct reconciliation (matrix_view_t)

Both repos define this struct with identical field order, types, and semantics:

struct *_matrix_view_t {
    void*        data;
    int64_t      rows;
    int64_t      cols;
    int64_t      row_stride;   /* elements between row i and i+1 */
    int64_t      col_stride;   /* elements between col j and j+1 */
    *_dtype_t    dtype;
    int32_t      reserved0;    /* keep struct size stable */
};

row_stride / col_stride field names match (a verification the roadmap M2.5 row called out specifically). reserved0 is in the same position. The struct is wire-compatible across the two implementations.

Context error-buffer

Both repos document a 4 KiB per-context error buffer with snprintf-style writes and NUL-termination. Same size, same write discipline. No reconciliation needed.

Files landed

Donor’s versions (already n4m_*) copied verbatim to the unified target locations:

File

Source (donor)

Target (this repo)

context.cpp

_donor/nirs4all-methods/cpp/src/core/context.cpp

cpp/src/core/common/context.cpp

context.hpp

_donor/nirs4all-methods/cpp/src/core/context.hpp

cpp/src/core/common/context.hpp

status.cpp

_donor/nirs4all-methods/cpp/src/core/status.cpp

cpp/src/core/common/status.cpp

status.hpp

_donor/nirs4all-methods/cpp/src/core/status.hpp

cpp/src/core/common/status.hpp

matrix_view.cpp

donor →

cpp/src/core/common/matrix_view.cpp

matrix_view.hpp

donor →

cpp/src/core/common/matrix_view.hpp

version.cpp

donor →

cpp/src/core/common/version.cpp

version.hpp

donor →

cpp/src/core/common/version.hpp

parallel.hpp

donor →

cpp/src/core/common/parallel.hpp

linalg.hpp

donor →

cpp/src/core/common/linalg.hpp

Plus donor’s common/ subtree (vendored C-level math/RNG kernels) — already organised in donor under cpp/src/core/common/ — copied to the same path here:

Subtree

Files

Notes

cpp/src/core/common/linalg.{c,h}

2

C-level low-level kernels (companion to linalg.hpp)

cpp/src/core/common/banded_solver.{c,h}

2

banded LDLT solver

cpp/src/core/common/bspline.{c,h}

2

B-spline kernels

cpp/src/core/common/fck_kernel.{c,h}

2

fractional convolutional kernel

cpp/src/core/common/padding.{c,h}

2

array-padding helpers

cpp/src/core/common/pca_helper.{c,h}

2

PCA helper (randomized SVD path)

cpp/src/core/common/rng_pcg64.{c,h}

2

NumPy-bit-equivalent PCG64

cpp/src/core/common/svd.{c,h}

2

SVD core

cpp/src/core/common/wavelet_kernels.{c,h}

2

wavelet filter banks

cpp/src/core/common/ziggurat.c + ziggurat_constants.h

2

ziggurat random sampling

cpp/src/core/common/_vendored/fitpack/

13

vendored FITPACK Fortran sources

Public headers copied to cpp/include/n4m/:

File

Notes

cpp/include/n4m/n4m.h

136.8 KB umbrella ABI header

cpp/include/n4m/n4m_version.h

semver constants

cpp/include/n4m/n4m_export.h.in

visibility / dllexport shim template

The PLACEHOLDER.md files previously in cpp/src/core/common/ and cpp/include/n4m/ are removed since real content has landed.

What is NOT changed in this phase

Per the roadmap (M2.5 lands the unified files; M3 moves donor sources out of _donor/; M4 splits model.cpp / extra_pls.cpp; M5 renames n4m_* n4m_* everywhere), this commit deliberately:

  • Does not modify cpp/include/pls4all/p4a.h (the existing public header). pls4all sources still see n4m_* symbols at compile time.

  • Does not modify pls4all’s cpp/src/CMakeLists.txt. The flat files at cpp/src/core/{context,status,matrix_view,version}.cpp are still listed there and still compiled into libn4m.so.1.16.0.

  • Does not delete the duplicate files at _donor/nirs4all-methods/cpp/src/core/{context,status,matrix_view,version,parallel,linalg}.{cpp,hpp} or _donor/nirs4all-methods/cpp/src/core/common/*. Those will be removed in M3 when the donor source move lifts them under their final paths anyway.

  • Does not alias n4m_*n4m_* symbols. That alias generation is M5 territory (the ABI rename script) and would otherwise expand this phase well beyond the roadmap’s 3-day budget.

Build verification

pls4all build and tests remain green after the unification:

$ cmake --build --preset dev-release
ninja: no work to do.
$ ctest --preset dev-release --output-on-failure
1/1 Test #1: n4m_tests ....................   Passed
$ ./build/dev-release/cpp/tests/n4m_tests
=== 265 run, 0 failures, 0 skipped ===

The newly-landed common/ + include/n4m/ files are purely additive: they are not consumed by any current build, they sit in the tree as the canonical target for M3/M4/M5 to wire up.

Gate evaluation

The roadmap M2.5 gate says:

Both donor-fixture C++ tests and pls4all-fixture C++ tests pass against the unified common-core; no duplicate symbol in the linker

Achieved partially in this phase:

  • No duplicate symbol in the linker — the unified common/ files are not built yet, so they contribute no symbol. pls4all sources continue to build against their flat copies with n4m_* symbols.

  • pls4all-fixture C++ tests pass — verified above (265/265).

  • ⚠️ Donor-fixture C++ tests pass against unified common-core — achievable only after M5 (ABI rename) wires both sides into one build. Carried forward to M5’s gate. The donor’s tests existed at _donor/.../cpp/tests/ and pass against donor’s own flat copies of the common files; pls4all’s n4m_tests binary doesn’t link them in yet.

The unification work (file landing + reconciliation evidence) is done. The build-system collapse onto the unified files lands in M5.

Risk reassessment

Risk listed in M2.5 row

Status

n4m_context_create collides with n4m_context_create on naive rename

Mitigated: the unified n4m_* symbols live at the new common/ location; pls4all flat sources keep their n4m_* symbols until M5 cleanly renames them in one pass. No collision possible because both name sets currently come from disjoint TUs.

Donor and base implementations disagree subtly

Resolved: side-by-side diff shows the only delta is prefix renaming. Enums, struct fields, backend codes, error-buffer size match exactly.

Donor’s linalg.hpp (more-tested?) vs base’s

Donor wins (per roadmap). Same content though — both ship identical kernel signatures.

Reconciling enum values

No-op — all 19 status codes and 6 backend codes are numerically identical.

Next: M3 (donor non-PLS source move)

With M2.5 landed, M3’s task is straightforward:

  1. git mv donor sources from _donor/nirs4all-methods/cpp/src/core/{preprocessing,augmentation,splitters,filters,utilities}/... to their final positions under cpp/src/core/<category>/....

  2. Delete the now-duplicated _donor/nirs4all-methods/cpp/src/core/{context,status,matrix_view,version,parallel,linalg.hpp,common/} files (preserved in this phase as part of the donor subtree; the unified copies at cpp/src/core/common/ are authoritative going forward).

  3. Update donor-side parity-generator imports to refer to the new paths.

  4. Settle the augmentations/augmentation/ rename (donor uses plural, unified target is singular).

M3’s gate: donor fixtures still pass against the moved sources.