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

Results

45
Fixtures
925
Field checks
0
Failures
61
Skipped, all divergent
1134
Unit tests
13 / 13
P6-native cases

Every fixture, and its check count

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.

IDFixtureChecksFailSkip
F1A→B→C linear, no cal2502
F2A→B→C linear, MonFri2502
F3A→B→C + A→X (off-CP), MonFri3202
F4Mixed FS/SS/FF/SF + lags3704
F5MonFri + holidays1901
F67-day calendar (no weekends)1901
F6.5SF + FS mix2502
F7Diamond network3704
F8Numeric codes interleaved with alpha codes4404
F9FS-3 lead1901
F10Completed + uncompleted mix2402
F11MonFri + 7-day calendars mixed2502
F12early_start pin ahead of logic2502
F13SNET primary constraint1901
F14MS_Start + FNLT combo2601
F15ALAP consumes float3103
F16SNLT primary (forward ALERT + backward LF clamp)1901
F17FNET pushes EF forward (warn)1300
F18MS_Finish LF pin (forward warn + backward clamp)1901
F19Secondary constraint pair (SNET + FNLT window)1901
F20Out-of-sequence (completed B before A starts)1502
F21ALAP slide suppressed by actual_start2903
F22Calendar fallback (missing clndr_id triggers ALERT)1901
F23Cycle detection (both engines refuse)100
F24Free-float parity DOCUMENTED GAP (no FF in Python ref)1901
F26Calendar fallback, 3 distinct missing clndr_ids2502
F27actual_start AFTER data_date pins ES (AACE 29R-03 §4.3)1701
F28ALAP primary + FNLT secondary compound3103
F29Mixed FF + SS predecessors converge on same successor2502
F30Negative lag FS-2 (no calendar, ordinal arithmetic)1901
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)2502
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)1101
F44ALAP on secondary slot (v2.9.12 T4.26)1901
F45in-progress retained-logic LF=EF pin (F1-Bug1/F1-Bug2)2502
F46single in-progress activity retained-logic EF (F1-Bug2)1300
F47stored early_start NOT a SNET floor (F1-Bug5)1302
F48- out-of-sequence retained logic: remaining restarts behind pred (case 10)1900
F49- out-of-sequence progress_override: remaining continues from data date1900

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.

Detail

Run manifestVersions, the pinned reference hash, and how to reproduce this page
ItemValue
Engine versioncpm-engine v2.9.39
Run date2026-08-11
Node.jsv22.19.0
Python3.12.10
Python referencepython_reference/cpm.py, 81338 bytes
Reference SHA-256da792b52c743b62dd71b4ea2ea1b1dcd724088fd230ab977171edd00aace4423
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
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.

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>671Per-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_num43Project finish as an ordinal day number
project_finish43Project finish as a rendered date
critical_codes43The set of activity codes on the critical path
topo_order43Topological sort order
alert_count40Number of alerts raised
alert_severity_counts40Alert 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 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.

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-261PASSOut-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. 1134 assertions 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, 3112 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.

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.