What this proves, and what it does not
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 1009 field comparisons the harness runs across 46 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 6 comparisons are skipped rather than failed, on the two signed free-float fields, and none of them is a port gap: all 6 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 1009 of 1015, and 43 of the 46 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. 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
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: 3 fall on ff_signed_working_days and 3 on ff_signed, giving 6 in total across 3 of the 46 fixtures. All 6 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.
| ID | Fixture | Checks | Fail | Skip |
|---|---|---|---|---|
| F1 | A→B→C linear, no cal | 27 | 0 | 0 |
| F2 | A→B→C linear, MonFri | 27 | 0 | 0 |
| F3 | A→B→C + A→X (off-CP), MonFri | 34 | 0 | 0 |
| F4 | Mixed FS/SS/FF/SF + lags | 41 | 0 | 0 |
| F5 | MonFri + holidays | 20 | 0 | 0 |
| F6 | 7-day calendar (no weekends) | 20 | 0 | 0 |
| F6.5 | SF + FS mix | 27 | 0 | 0 |
| F7 | Diamond network | 41 | 0 | 0 |
| F8 | Numeric codes interleaved with alpha codes | 48 | 0 | 0 |
| F9 | FS-3 lead | 20 | 0 | 0 |
| F10 | Completed + uncompleted mix | 25 | 0 | 2 |
| F11 | MonFri + 7-day calendars mixed | 27 | 0 | 0 |
| F12 | early_start pin ahead of logic | 27 | 0 | 0 |
| F13 | SNET primary constraint | 20 | 0 | 0 |
| F14 | MS_Start + FNLT combo | 27 | 0 | 0 |
| F15 | ALAP consumes float | 34 | 0 | 0 |
| F16 | SNLT primary (forward ALERT + backward LF clamp) | 20 | 0 | 0 |
| F17 | FNET pushes EF forward (warn) | 13 | 0 | 0 |
| F18 | MS_Finish LF pin (forward warn + backward clamp) | 20 | 0 | 0 |
| F19 | Secondary constraint pair (SNET + FNLT window) | 20 | 0 | 0 |
| F20 | Out-of-sequence (completed B before A starts) | 18 | 0 | 2 |
| F21 | ALAP slide suppressed by actual_start | 34 | 0 | 0 |
| F22 | Calendar fallback (missing clndr_id triggers ALERT) | 20 | 0 | 0 |
| F23 | Cycle detection (both engines refuse) | 1 | 0 | 0 |
| F24 | Free-float parity DOCUMENTED GAP (no FF in Python ref) | 20 | 0 | 0 |
| F26 | Calendar fallback, 3 distinct missing clndr_ids | 27 | 0 | 0 |
| F27 | actual_start AFTER data_date pins ES (P6 forward-pass semantics) | 20 | 0 | 0 |
| F28 | ALAP primary + FNLT secondary compound | 34 | 0 | 0 |
| F29 | Mixed FF + SS predecessors converge on same successor | 27 | 0 | 0 |
| F30 | Negative lag FS-2 (no calendar, ordinal arithmetic) | 20 | 0 | 0 |
| 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) | 27 | 0 | 0 |
| 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 | 2 |
| F44 | ALAP on secondary slot (v2.9.12 T4.26) | 20 | 0 | 0 |
| F45 | in-progress retained-logic LF=EF pin (F1-Bug1/F1-Bug2) | 27 | 0 | 0 |
| 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 | 0 |
| F48 | - out-of-sequence retained logic: remaining restarts behind pred (case 10) | 20 | 0 | 0 |
| F49 | - out-of-sequence progress_override: remaining continues from data date | 20 | 0 | 0 |
| F50 | special_workdays: a forced-ON Saturday and a forced-OFF Monday | 20 | 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 46 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
| Item | Value |
|---|---|
| Engine version | cpm-engine v2.9.43 |
| Run date | 2026-09-08 |
| Node.js | v22.19.0 |
| Python | 3.12.10 |
| Python reference | python_reference/cpm.py, 136967 bytes |
| Reference SHA-256 | 83c6db6f61b36d8c8c22fee59d37bfeb36d3d8bc131a3716793e930327301ed5 |
| 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 git checkout v2.9.43 npm install npm run test:all
The tag is pinned deliberately. This page records a run performed on 8 September 2026 at engine v2.9.43, and the hashes and dates below are that run's. Checking out the tag is what makes them reproducible; v2.9.43 is the current release, and the 1,273 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. 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.
| Field | Checks | What it is |
|---|---|---|
| node <id> | 743 | 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 | 44 | Project finish as an ordinal day number |
| project_finish | 44 | Project finish as a rendered date |
| critical_codes | 44 | The set of activity codes on the critical path |
| topo_order | 44 | Topological sort order |
| alert_count | 44 | Number of alerts raised |
| alert_severity_counts | 44 | 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.
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.
| # | 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-29 | 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.
The other gate suitesEight checks that run alongside the cross-validation, including a client-name guard
- Unit suite. 1216 assertions across calendar arithmetic, the forward and backward pass, topological sort, cycle detection and the public API. 0 failures.
- No fabricated citations. Scans markdown, JavaScript, HTML and Python for standards citations that do not resolve.
- No client names. Scans tracked source for client and project identifiers, so no real engagement can reach a public commit.
- No truncation. Confirms engine surfaces return complete data rather than a capped subset.
- No stale version references. Scanned 16 files, 3595 lines and 151 version references against the current engine version.
- Procedure validator. Four fixtures exercising the documented analysis procedure.
- Signing and attestation. Seven sub-suites over the cryptographic sign-off path.
- 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 does the same (CI-signed witness, Sigstore Rekor logIndex 2685194682). An independent evaluation record from 16 August 2026 documents what an external audit found and what was done about each item.