Two implementations of the same CPM engine by one author, run against the same fixtures, compared field by field. Every fixture is listed. Every gap is listed. The results are reproducible from a public repository.
Two validation layers, stated separately. First: a JavaScript implementation and a second Python implementation of the same CPM algorithm, both written by the same author, agree on the 925 field comparisons the harness runs across 45 fixtures, covering calendars, all four relationship types, leads and lags, constraints, both P6 scheduling modes, in-progress work, out-of-sequence progress, cycle refusal and far-future date arithmetic. A further 61 comparisons are skipped rather than failed, because the Python port does not emit one free-float field the JavaScript engine does; counted, agreement is 925 of 986, and 11 of the 45 fixtures are free of any divergence. Two ports by one author catch transcription and refactor drift; they cannot catch a shared misreading of P6, which is what the second layer below is for.
Second, and stronger: validation against Primavera P6 itself. On 2026-08-11 the 13 comparison cases below were built inside P6 Professional 23.12 by an automated import, scheduled with a single F9, and P6's computed dates and float were read back and compared. The first capture returned 6 of 13. The seven failures resolved into five named divergence families; each was fixed in both implementations against P6's pinned answers, with the fix history public in the repository. The matrix now stands at 13 of 13. The engine makes no P6 parity claim beyond the ground these cases cover, and the whole loop reruns on every engine change for the cost of one F9.
Known limitations are published, not buried. The engine is day-granular, so sub-day lags round with a fatal-in-strict-mode alert. Resource levelling is not modelled. Free-float parity carries one documented asymmetry noted below. Two cases that P6 cannot construct by design live in a separate engine-limitations folder and carry no P6 verdict.
The Skip column counts comparisons the harness does not perform because one port does not emit the field. The harness's own skip counter reports zero for these: the guards do not register a skip, they simply do not compare. All 61 are cases where the two ports genuinely differ, spread across 34 of the 45 fixtures.
| ID | Fixture | Checks | Fail | Skip |
|---|---|---|---|---|
| F1 | A→B→C linear, no cal | 25 | 0 | 2 |
| F2 | A→B→C linear, MonFri | 25 | 0 | 2 |
| F3 | A→B→C + A→X (off-CP), MonFri | 32 | 0 | 2 |
| F4 | Mixed FS/SS/FF/SF + lags | 37 | 0 | 4 |
| F5 | MonFri + holidays | 19 | 0 | 1 |
| F6 | 7-day calendar (no weekends) | 19 | 0 | 1 |
| F6.5 | SF + FS mix | 25 | 0 | 2 |
| F7 | Diamond network | 37 | 0 | 4 |
| F8 | Numeric codes interleaved with alpha codes | 44 | 0 | 4 |
| F9 | FS-3 lead | 19 | 0 | 1 |
| F10 | Completed + uncompleted mix | 24 | 0 | 2 |
| F11 | MonFri + 7-day calendars mixed | 25 | 0 | 2 |
| F12 | early_start pin ahead of logic | 25 | 0 | 2 |
| F13 | SNET primary constraint | 19 | 0 | 1 |
| F14 | MS_Start + FNLT combo | 26 | 0 | 1 |
| F15 | ALAP consumes float | 31 | 0 | 3 |
| F16 | SNLT primary (forward ALERT + backward LF clamp) | 19 | 0 | 1 |
| F17 | FNET pushes EF forward (warn) | 13 | 0 | 0 |
| F18 | MS_Finish LF pin (forward warn + backward clamp) | 19 | 0 | 1 |
| F19 | Secondary constraint pair (SNET + FNLT window) | 19 | 0 | 1 |
| F20 | Out-of-sequence (completed B before A starts) | 15 | 0 | 2 |
| F21 | ALAP slide suppressed by actual_start | 29 | 0 | 3 |
| F22 | Calendar fallback (missing clndr_id triggers ALERT) | 19 | 0 | 1 |
| F23 | Cycle detection (both engines refuse) | 1 | 0 | 0 |
| F24 | Free-float parity DOCUMENTED GAP (no FF in Python ref) | 19 | 0 | 1 |
| F26 | Calendar fallback, 3 distinct missing clndr_ids | 25 | 0 | 2 |
| F27 | actual_start AFTER data_date pins ES (AACE 29R-03 §4.3) | 17 | 0 | 1 |
| F28 | ALAP primary + FNLT secondary compound | 31 | 0 | 3 |
| F29 | Mixed FF + SS predecessors converge on same successor | 25 | 0 | 2 |
| F30 | Negative lag FS-2 (no calendar, ordinal arithmetic) | 19 | 0 | 1 |
| F31 | Cycle in sub-network (A→B→C clean + D↔E cycle) | 1 | 0 | 0 |
| F32 | Far-future date arithmetic (2037-12-15 + 100d, post-Y2038) | 13 | 0 | 0 |
| F33 | MS_Start primary pins LS=ES, TF=0 (v2.9.12 T1.1) | 25 | 0 | 2 |
| F34 | MS_Start suppressed by actual_start (v2.9.12 T1.2) | 13 | 0 | 0 |
| F35 | unrecognized constraint token (v2.9.12 T1.6) | 13 | 0 | 0 |
| F36 | empty work_days falls back to MonFri (v2.9.12 T2.16) | 13 | 0 | 0 |
| F37 | CS_MANSTART alias (v2.9.12 T1.7) | 13 | 0 | 0 |
| F38 | CS_MANFINISH alias (v2.9.12 T1.7) | 13 | 0 | 0 |
| F43 | actual_finish without actual_start (v2.9.12 T4.25) | 11 | 0 | 1 |
| F44 | ALAP on secondary slot (v2.9.12 T4.26) | 19 | 0 | 1 |
| F45 | in-progress retained-logic LF=EF pin (F1-Bug1/F1-Bug2) | 25 | 0 | 2 |
| F46 | single in-progress activity retained-logic EF (F1-Bug2) | 13 | 0 | 0 |
| F47 | stored early_start NOT a SNET floor (F1-Bug5) | 13 | 0 | 2 |
| F48 | - out-of-sequence retained logic: remaining restarts behind pred (case 10) | 19 | 0 | 0 |
| F49 | - out-of-sequence progress_override: remaining continues from data date | 19 | 0 | 0 |
Fixture identifiers are not contiguous. F6.5 was inserted between F6 and F7, and F25 and F39 through F42 are absent because fixtures were consolidated across earlier audit rounds. The set is 43 fixtures as listed. Cycle-detection fixtures F23 and F31 carry a single check each, because the only correct behaviour is that both implementations refuse the input.
| Item | Value |
|---|---|
| Engine version | cpm-engine v2.9.39 |
| Run date | 2026-08-11 |
| Node.js | v22.19.0 |
| Python | 3.12.10 |
| Python reference | python_reference/cpm.py, 81338 bytes |
| Reference SHA-256 | da792b52c743b62dd71b4ea2ea1b1dcd724088fd230ab977171edd00aace4423 |
| Licence | MIT |
| Repository | github.com/danafitkowski/cpp-cpm-engine |
The Python reference is pinned by SHA-256 and the hash is printed at the start of every run, so an external auditor can confirm the reference has not drifted between runs. To reproduce:
git clone https://github.com/danafitkowski/cpp-cpm-engine cd cpp-cpm-engine npm install npm run test:all
The run prints the reference path, byte count and SHA-256 before the first fixture, then one line per field check. The final block prints the fixture and check totals shown on this page.
Comparison is field by field rather than on a single summary value. A fixture passes only if every enumerated field matches between the two implementations.
| Field | Checks | What it is |
|---|---|---|
| node <id> | 671 | Per-activity values: early start, early finish, late start, late finish, total float, the rendered dates, total float in working days, free float, and free float in working days |
| project_finish_num | 43 | Project finish as an ordinal day number |
| project_finish | 43 | Project finish as a rendered date |
| critical_codes | 43 | The set of activity codes on the critical path |
| topo_order | 43 | Topological sort order |
| alert_count | 40 | Number of alerts raised |
| alert_severity_counts | 40 | Alert counts broken down by severity |
| threw (both engines) | 2 | Both implementations refuse the input and raise, on cycle-detection fixtures |
Per-activity checks dominate the count because every activity in every fixture contributes its early and late dates, total float, free float, the rendered date strings, and both float values converted to working days on that activity's own calendar.
Fixture F24 is labelled a documented gap in the suite itself. The Python reference does not compute free float, so free float cannot be cross-validated between the two implementations. The JavaScript engine computes it and it is unit-tested on that side, but it does not carry two-implementation agreement. Anyone relying on free float should know that.
P6 stores lags in hours and honours sub-day precision. This engine works in whole days.
A sub-day lag raises a SUB_DAY_LAG_ROUNDED alert and rounds. In forensic strict
mode that alert is fatal and the engine refuses to produce a result rather than quietly
rounding. The case is
validation/engine-limitations/cases/01-fractional-lag-engine-rounds/.
Fractional lag and dangling relationships test inputs P6 itself cannot produce. Rather than
leave them in the P6 comparison matrix where they would look like failures or be silently
dropped, they were moved to validation/engine-limitations/cases/ and will never
carry a P6 verdict. A dangling relationship, meaning one that references an activity not
present in the file, raises an alert and the relationship is dropped; in forensic strict mode
it is fatal.
The engine's answers were published before P6 was asked the same questions, and the capture itself was blind: an automated harness imported the 13 mini-projects into P6 Professional 23.12, a human pressed F9 once, and P6's stored answers were read back from the database. The first capture scored 6 of 13. The seven failures were diagnosed into five divergence families (working-day float units, open-end late-date seeding, mandatory-finish semantics, retained logic on out-of-sequence progress, and free-float conventions), fixed in both engine implementations against P6's pinned numbers, and the matrix re-verified at 13 of 13. Every fix is a public commit citing the capture.
| # | Case | Engine project finish | Alerts | P6 verdict | What it tests |
|---|---|---|---|---|---|
| 01 | 01-fs-chain | 2026-01-19 | 0 | PASS | Baseline FS chain. Should match P6 exactly. |
| 02 | 02-ss-with-lag | 2026-01-19 | 0 | PASS | SS+5 with parallel-finish behaviour |
| 03 | 03-ff-with-lag | 2026-01-15 | 0 | PASS | FF+3 forces B finish behind A finish |
| 04 | 04-sf-edge-case | 2026-01-12 | 0 | PASS | Least-common relationship type; SF behaviour varies with the P6 progress-override setting |
| 05 | 05-negative-float | 2026-01-21 | 1 | PASS | FNLT constraint produces negative total float |
| 06 | 06-multiple-calendars | 2026-01-19 | 0 | PASS | Activity-specific calendars, 5-day against 6-day |
| 07 | 07-ontario-holidays | 2026-05-13 | 0 | PASS | Long activity across Ontario statutory holidays |
| 08 | 08-in-progress-retained-logic | 2026-01-28 | 0 | PASS | Predecessor in progress; successor anchored to projected early finish |
| 09 | 09-completed-successor | 2026-01-12 | 2 | PASS | Backward-pass skip; no pull-back through a historical finish |
| 10 | 10-out-of-sequence-progress | 2026-01-26 | 1 | PASS | Out-of-sequence alert path |
| 11 | 11-mandatory-start-finish | 2026-01-30 | 2 | PASS | Mandatory Start and Mandatory Finish hard pins |
| 12 | 12-snet-fnlt | 2026-02-03 | 1 | PASS | Start-No-Earlier-Than and Finish-No-Later-Than, the two most common P6 constraints |
| 13 | 13-alap | 2026-01-22 | 0 | PASS | As-Late-As-Possible secondary constraint |
Alert counts include informational alerts. The remaining alerts are by design: the completed-successor backward-pass skip on case 09, out-of-sequence progress on case 10, and constraint-application alerts on cases 05, 11 and 12.
Two cases formerly numbered 14 and 15 are excluded by construction and live under
validation/engine-limitations/. See the gaps drawer above.
That item is now closed. The release-evidence packet for v2.9.39 was built from the release commit and passed its strict gate with all ten required files present.