Skip to main content
Critical Path Partners · Engine Validation

CPM Engine Cross-Validation Results

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.

What this proves, and what it does not

Read this before quoting any number below

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 1957 field comparisons the harness runs across 82 fixtures, covering calendars, all four relationship types, leads and lags, constraints, both P6 scheduling modes, in-progress work, out-of-sequence progress, the retained-logic pass-through, the unexpired part of a lag off started and completed work, cycle refusal and far-future date arithmetic. A further 54 comparisons are skipped rather than failed, on the two signed free-float fields, and none of them is a port gap: all 54 fall on completed activities where NEITHER implementation emits the field. The 58 one-sided skips that were a real port gap closed when the has-successors branch was ported. Counted, agreement is 1957 of 2011, and 59 of the 82 fixtures have no skipped comparison. 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. Re-capture is not automatic. It costs one import and one F9 in P6 by a human operator, no held-out capture has been taken since, and the capture sheet the applier reads sits outside the public repository, so the matrix cannot be regenerated end to end from a clean clone. The committed import XER and the per-case comparison CSVs preserve P6's answers, so the capture stays inspectable. The honest reading is that these thirteen behaviours are now correct, not that the next thirteen would pass first time.

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.

Results

82
Fixtures
1957
Field checks
0
Failures
54
Skipped, not compared
1315
Unit tests
13 / 13
P6-native cases, fitted
first blind run 6 of 13

Every fixture, and its check count

The Skip column counts comparisons the harness does not perform because at least one port does not emit the field. The harness counts them: 27 fall on ff_signed_working_days and 27 on ff_signed, giving 54 in total across 23 of the 82 fixtures. All 54 are cases where NEITHER implementation emits the field, on completed activities, so there is nothing to verify and no port gap behind them. None is a case where this engine returns a computed value and the Python reference returns none: that class of skip numbered 58 until the has-successors branch was ported, and now numbers zero.

IDFixtureChecksFailSkip
F1A→B→C linear, no cal2700
F2A→B→C linear, MonFri2700
F3A→B→C + A→X (off-CP), MonFri3400
F4Mixed FS/SS/FF/SF + lags4100
F5MonFri + holidays2000
F67-day calendar (no weekends)2000
F6.5SF + FS mix2700
F7Diamond network4100
F8Numeric codes interleaved with alpha codes4800
F9FS-3 lead2000
F10Completed + uncompleted mix2502
F11MonFri + 7-day calendars mixed2700
F12early_start pin ahead of logic2700
F13SNET primary constraint2000
F14MS_Start + FNLT combo2700
F15ALAP consumes float3400
F16SNLT primary (forward ALERT + backward LF clamp)2000
F17FNET pushes EF forward (warn)1300
F18MS_Finish LF pin (forward warn + backward clamp)2000
F19Secondary constraint pair (SNET + FNLT window)2000
F20Out-of-sequence (completed B before A starts)1802
F21ALAP slide suppressed by actual_start3400
F22Calendar fallback (missing clndr_id triggers ALERT)2000
F23Cycle detection (both engines refuse)100
F24Free-float parity DOCUMENTED GAP (no FF in Python ref)2000
F26Calendar fallback, 3 distinct missing clndr_ids2700
F27actual_start AFTER data_date pins ES (P6 forward-pass semantics)2000
F28ALAP primary + FNLT secondary compound3400
F29Mixed FF + SS predecessors converge on same successor2700
F30Negative lag FS-2 (no calendar, ordinal arithmetic)2000
F31Cycle in sub-network (A→B→C clean + D↔E cycle)100
F32Far-future date arithmetic (2037-12-15 + 100d, post-Y2038)1300
F33MS_Start primary pins LS=ES, TF=0 (v2.9.12 T1.1)2700
F34MS_Start suppressed by actual_start (v2.9.12 T1.2)1300
F35unrecognized constraint token (v2.9.12 T1.6)1300
F36empty work_days falls back to MonFri (v2.9.12 T2.16)1300
F37CS_MANSTART alias (v2.9.12 T1.7)1300
F38CS_MANFINISH alias (v2.9.12 T1.7)1300
F43actual_finish without actual_start (v2.9.12 T4.25)1102
F44ALAP on secondary slot (v2.9.12 T4.26)2000
F45in-progress retained-logic LF=EF pin (F1-Bug1/F1-Bug2)2700
F46single in-progress activity retained-logic EF (F1-Bug2)1300
F47stored early_start NOT a SNET floor (F1-Bug5)1300
F48- out-of-sequence retained logic: remaining restarts behind pred (case 10)2000
F49- out-of-sequence progress_override: remaining continues from data date2000
F50special_workdays: a forced-ON Saturday and a forced-OFF Monday2000
F51pass-through: completed C carries unfinished P's date to X2502
F52progress_override ignores the pass-through (same topology as F51)2502
F53pass-through carries the lag into C; the lag out of C adds its unexpired part2502
F54pass-through survives a chain of two completed activities3004
F55SS_U: restart wins over actual_start+lag2000
F56SS_U: actual_start+lag wins when it exceeds the restart2000
F57PO_SNAP: progress_override restart snaps off a weekend data date1300
F58SSL: SS off a started predecessor whose restart is held past the data date2700
F59SSL: a used-up lag lays nothing; X starts on the held restart2700
F60SSL: SF off a started predecessor whose restart is held2700
F61SSL: an SS link off started work lays no lag into a completed activity3202
F62SSL: a future actual start uses up none of the lag2000
F63SSL: six-day predecessor, Mon-Fri successor, predecessor lag calendar2700
F64SSL: a started successor restarts at the held restart + unused lag2700
F65FA: FS+0 off an actual finish recorded weeks after the data date, beside an unfinished predecessor3002
F66FA: FS+2 off an actual finish after the data date is laid from the data date1802
F67FA: SS+3 off an actual start after the data date1602
F68FA: FF+2 off an actual finish after the data date1802
F69FA: the unexpired lag rides on a carried date2502
F70FA: the backward pass takes the unexpired lag off the carried bound3202
F71FA: a started successor restarts at the data date plus the unexpired lag1802
F72FA: progress override lays the lag from the data date too1802
F73FA: without a data date the actual dates still drive1802
F74FA: six-day predecessor, Mon-Fri successor, predecessor lag calendar1802
F75FA: a completed-to-completed link carries its unexpired lag2304
F76SSL measured: SS under lag from Actual Start is laid from the data date2700
F77SSL measured: under Actual Start the successor can start before the restart2700
F78SSL measured: a used-up lag under Actual Start starts on the data date (flag N)2700
F79SSL measured: Actual Start, six-day predecessor, Mon-Fri successor2700
F80SSL measured: under Actual Start a started successor restarts at the data date + unused lag2700
F81SSL measured: SF ignores lag from Actual Start2700
F82SSL measured: no lag from started work into a completed activity; float through it3902
F83SSL measured: into a completed activity under Actual Start the data date is carried3902
F84SSL measured: SF into a completed activity carries the restart under Actual Start3202
F85SSL measured: other links into completed work keep their lag7006
F86SSL measured: an unknown lag-from option alerts and keeps Early Start2700

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 82 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.

Detail

Run manifestVersions, the pinned reference hash, and how to reproduce this page
ItemValue
Engine versioncpm-engine v2.9.47
Run date2026-09-23
Node.jsv22.19.0
Python3.12.10
Python referencepython_reference/cpm.py, 190699 bytes
Reference SHA-2568148a7584c67c8b4945fdd048b4362bb53c9ffe9b630d7061a6659afb3e15a74
LicenceMIT
Repositorygithub.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
git checkout v2.9.47
npm install
npm run test:all

The tag is pinned deliberately. This page records a run performed on 23 September 2026 at engine v2.9.47, and the hashes and dates below are that run's. Checking out the tag is what makes them reproducible; v2.9.47 is the current release, and the 1,315 unit tests pass in the same run. The agreement figure moved from the 931 of 995 recorded at v2.9.41 when v2.9.42 corrected constraint pairing and restored SS/SF late-finish conversion; it has held at 1009 of 1015 through v2.9.43 and v2.9.44. v2.9.44 changed the arithmetic where a relationship crosses calendars: a successor now starts from its predecessor's finish instant, placed on the successor's own calendar, and the backward pass mirrors it. Every fixture except the mixed-calendar F11 computed the same values before and after that change, and on F11 the two implementations agree on the new reading. It grew to 1167 of 1183 when v2.9.46 added seven fixtures for the retained-logic pass-through, and to 1957 of 2011 when v2.9.47 added twenty-nine for the unexpired part of a lag off started and completed work; the fixtures that existed before each change agree in both implementations after it, F53 moving one working day to the measured value in both. 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.

What is compared on each fixtureThe enumerated field set, with the number of checks each contributes

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.

FieldChecksWhat it is
node <id>1479Per-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_num80Project finish as an ordinal day number
project_finish80Project finish as a rendered date
critical_codes80The set of activity codes on the critical path
topo_order80Topological sort order
alert_count78Number of alerts raised
alert_severity_counts78Alert counts broken down by severity
threw (both engines)2Both 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.

Documented gaps and disclosed limitationsThree items, stated plainly, each with the fixture or case that exercises it

1. Free-float parity is a documented gap

Fixture F24 was labelled a documented gap in the suite itself when the Python reference did not compute free float. That gap closed when the free-float port landed: ff and ff_working_days are cross-validated on every fixture, and the signed variants on every activity where either engine emits them. What remains uncompared is 6 comparisons on three fixtures, on completed activities where NEITHER implementation emits the signed free-float fields, so there is nothing to verify and no one-sided gap behind them.

2. The engine is day-granular

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/.

3. Two cases are known to diverge from P6 by construction

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.

P6-native comparison matrix: 13/13 validatedCaptured from Primavera P6 Professional 23.12 on 2026-08-11; first capture 6/13, five divergence families fixed against the pinned answers, now 13/13

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.

#CaseEngine project finishAlertsP6 verdictWhat it tests
0101-fs-chain2026-01-190PASSBaseline FS chain. Should match P6 exactly.
0202-ss-with-lag2026-01-190PASSSS+5 with parallel-finish behaviour
0303-ff-with-lag2026-01-150PASSFF+3 forces B finish behind A finish
0404-sf-edge-case2026-01-120PASSLeast-common relationship type; SF behaviour varies with the P6 progress-override setting
0505-negative-float2026-01-211PASSFNLT constraint produces negative total float
0606-multiple-calendars2026-01-190PASSActivity-specific calendars, 5-day against 6-day
0707-ontario-holidays2026-05-130PASSLong activity across Ontario statutory holidays
0808-in-progress-retained-logic2026-01-280PASSPredecessor in progress; successor anchored to projected early finish
0909-completed-successor2026-01-122PASSBackward-pass skip; no pull-back through a historical finish
1010-out-of-sequence-progress2026-01-291PASSOut-of-sequence alert path
1111-mandatory-start-finish2026-01-302PASSMandatory Start and Mandatory Finish hard pins
1212-snet-fnlt2026-02-031PASSStart-No-Earlier-Than and Finish-No-Later-Than, the two most common P6 constraints
1313-alap2026-01-220PASSAs-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.

The other gate suitesEight checks that run alongside the cross-validation, including a client-name guard
  1. Unit suite. 1315 unit tests across calendar arithmetic, the forward and backward pass, topological sort, cycle detection and the public API. 0 failures.
  2. No fabricated citations. Scans markdown, JavaScript, HTML and Python for standards citations that do not resolve.
  3. No client names. Scans tracked source for client and project identifiers, so no real engagement can reach a public commit.
  4. No truncation. Confirms engine surfaces return complete data rather than a capped subset.
  5. No stale version references. Scanned 16 files, 3595 lines and 151 version references against the current engine version.
  6. Procedure validator. Four fixtures exercising the documented analysis procedure.
  7. Signing and attestation. Seven sub-suites over the cryptographic sign-off path.
  8. P6 comparison validator and corpus topology. Seven scenarios, one real and six synthetic, plus a topology fixture confirming the diamond-cascade shape of case 13.

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; the v2.9.43 packet did the same (Sigstore Rekor logIndex 2685194682), and the v2.9.44 packet does (CI-signed witness, Sigstore Rekor logIndex 2854361810). An independent evaluation record from 16 August 2026 documents what an external audit found and what was done about each item.