Skip to main content
A checklist you can point at anyone, including us

How to judge forensic scheduling software.

Ten criteria, each written so it applies to any product on the market. Then Critical Path Partners graded against every one of them by name, with the evidence, including the nine criteria that came back partial and the one that came back a straight fail.

Graded 16 August 2026. 0 full passes, 9 partial, 1 does not meet. The failures are on this page because a grade the vendor edits before publishing is not a grade.

Why this page exists. Every vendor in this field says its numbers are defensible. There is no shared test that lets a buyer check the claim, so the claim gets made and nobody has to back it. What follows is one attempt at that shared test. Each criterion is stated tool-neutrally first, with the questions to put to a vendor and what a real answer looks like, so it can be applied to a product that has nothing to do with this firm.

Then this firm is graded against it. Same ten criteria, same evidence standard, no softer wording because the subject is us. On 16 August 2026 the result was 0 full passes out of 10. Nine criteria came back partial and one came back a straight fail: what the product does with a schedule file, and what it tells you about that, do not match. Two grades moved since the previous review on 12 August. Change control climbed out of a fail into a partial. Data handling went the other way. The remaining eight did not move.

What to do with it. Take the questions in each criterion to whoever is selling you a schedule analysis, this firm included, and ask for the artefact rather than the answer. “Show me the file” beats “yes”. If a vendor cannot produce the artefact, that is the finding, and you can stop there.

The grade

Ten criteria. 0 full passes.

The honest count first, before any of the detail. Every criterion examined is listed below with its grade, whether it moved since the last time, and how many separate pieces of evidence sit behind it.

0
Meets in full
9
Partially meets
1
Does not meet
155
Evidence items published
MeetsThe criterion is satisfied and something automated keeps it satisfied. Not awarded on any criterion here.
Partially meetsReal work is in place and verifiable, and named gaps remain. Nine of the ten criteria sit here.
Does not meetWhat the product tells you is contradicted by what it does, and the gap is not disclosed anywhere the reader looks. Criterion 10.
All 10 criteria examined are listed. Nothing is withheld.
Grade summary, 10 rows, complete.
#CriterionGrade, 16 Aug 2026Since 12 AugEvidence items
1Change control and version identityPartially meetsImproved9
2Agreement with the reference scheduling applicationPartially meetsUnchanged14
3Independent implementation cross-checkPartially meetsUnchanged16
4Honest description of the validation boundaryPartially meetsUnchanged18
5Where the float number comes fromPartially meetsUnchanged18
6Day units and calendarsPartially meetsUnchanged16
7Reproducibility of the deliverablePartially meetsUnchanged24
8Method labelling honestyPartially meetsUnchanged22
9Completeness of what the report shows youPartially meetsUnchanged9
10What the product does with your schedule and who can read itDoes not meetDowngraded9

Jump to any criterion:

The criteria

Each one stated for any product, then applied to this one.

The top half of each block is tool-neutral and can be used to interrogate any vendor. The bottom half is this firm's grade against it. Detail sits in drawers so the page stays readable; the first evidence table is open as a worked exhibit. Nothing is deleted, only folded.

Criterion 1

Change control and version identity

The standard, stated for any product

You must be able to name the exact build of the software that produced any given number, and the vendor's published verification material must match the code it actually ships. A product that cannot tell you which build ran cannot tell you whether a later correction changed your answer, and cannot tell you which of your issued reports needs withdrawing when it does.

What to ask the vendor

  1. Which version produced this report, and where on the report does it say so?
  2. Do the published fingerprints match the code I can download from you today?
  3. Is that match enforced automatically at release, or checked by hand?
  4. What is your written procedure when a calculation changes after a report is issued?

What a pass looks like

Every output names its build. An automated gate recomputes the fingerprint at release and refuses to ship when any published document disagrees. A reissue and supersession procedure exists in writing.

What a fail looks like

Version numbers typed by hand in more than one place. Public pages one release behind the product. No procedure covering reports already in a client's hands.

Critical Path Partners, graded against it
Partially meets Improved since the 12 August grade

The release paperwork now proves itself. Every published number for the current release matches the code that shipped, the signed record verifies end to end, and an automatic check enforces six values that were wrong before. What still fails is downstream: the public website advertises the previous version, and most tools that produce client documents print no version at all.

This one is open by default as the worked exhibit. The other nine fold, and fold only. Nothing has been removed.

Evidence9 items, each checked on 16 August 2026
All 9 evidence items are listed. The table scrolls and the header stays put; nothing is truncated.
Evidence, 9 rows, complete.
#Evidence, as recorded on 16 August 2026
1Signed chain resolves. release-evidence/v2.9.40/witness-v2.9.40.json hashes to 635f505707d8ba96e1243f651fc9f401315052f1fcbad85873c657bd48b87042; decoding the DSSE payload inside sigstore-attestation-output.txt returns subject digest sha256 635f5057..., the same value. That witness records engine sha256 72c1081d..., which is the tagged engine.
2The attestation file is the full signed bundle, not a stub. wc -c returns 13756 bytes for v2.9.40, in line with 13383 to 27606 for the eight earlier releases and up from the 314-byte hand-written summary recorded on 12 August. Its certificate extensions carry refs/tags/v2.9.40, commit 69d96684..., and CI run 31948553931; rekor-entry.txt and the bundle both read logIndex 2488685021.
3Published hashes match the released code and reproduce from a clean clone. git show v2.9.40:cpm-engine.js | sha256sum returns 72c1081de1e6a1b4953f4bc11fc291df5978251c666b1f5c4f891fa0999f7ec7 at 454460 bytes, equal to the packet pin; python_reference/cpm.py returns 27829ddab0a6... Cloning to a fresh folder and checking out v2.9.40 on Windows gives the same engine hash.
4The gate checks six values, not one. tests/no-stale-version-refs.test.js compares the engine hash (line 296), the python-reference pin (line 333), the witness-to-bundle binding and bundle size (lines 351-368), the commit (line 374), the release date (line 382) and the Rekor index (line 396) against the tag and the signed bundle. CHECK_RELEASE_EVIDENCE=1 node tests/no-stale-version-refs.test.js prints PASS.
5Cross-validation is published honestly. release-evidence/v2.9.40/VERIFY_RELEASE.md reads '925 of 989 defined comparisons executed and bit-identical, 0 failures, across 45 fixtures; 64 skipped'. node cpm-engine.crossval.js returns 'Fixtures: 45 passed, 0 failed' and 'Checks: 925 / 925 comparisons executed' with the skip caveat printed inline. node cpm-engine.test.js returns 1134 passed, 0 failed.
6The live public site still names the previous version. Fetching the homepage returns 'Regression Tests v2.9.39' while _cpp_common/scripts/engine_version.py:26 declares ENGINE_VERSION = '2.9.40'. The same stale number sits at cpp-forensic-mcp/static/index.html:596, in two further stored copies of that page, and at try-engine.html:865. Grepping the test folder for any check on those files returns nothing.
7Most producers carry no version. Running the seller's own producer discovery (_cpp_common/tests/test_conventions_on_page_one.py::_discover) returns 54 producers; searching each for ENGINE_VERSION, ENGINE_LABEL or engine_version matches 18 and misses 36. Separately, _cpp_common/scripts/docx_shell.py:890-906 raises an error unless the engine field carries the version from the single source of truth.
8The pre-deploy scrub is currently red. timeout 240 python -m pytest tests/ -q in the website repo returns '1 failed, 309 passed'. The failure is tests/test_bundle_no_client_names.py, reporting a real organisation's name in a comment at _cpp_common/scripts/engine_version.py:35. git status shows that file modified and uncommitted, so nothing has shipped, but the gate does not pass as it stands.
9Two record-keeping gaps remain. attestations/latest.json still reports version 2.9.39, commit f90b0cb and a null run URL, and git ls-files attestations/ returns only attestations/README.md, so it is not distributed. engine_version.py cites a prior bump to v2.9.36, for which git tag returns nothing, CHANGELOG.md has zero occurrences, and no release-evidence folder exists.
What worksIncluding the parts that were wrong before and are right now

The 12 August verdict was a fail, and the reason it failed no longer holds. At that time the verification packet for the current release carried the previous release's fingerprints and told buyers no calculation behaviour had changed when the code said otherwise. That is fixed at the source, and the fix holds up under checking.

Every number in the current packet matches the tagged code. I recomputed the engine fingerprint and the companion Python fingerprint from the tag and both equal the published pins. The commit and release date match the tag. I cloned the code to a clean folder and checked out the release: the fingerprint reproduced exactly, so the instructions the packet gives a reader work on a plain Windows machine. The earlier note that they did not was a stale local copy, not a defect in the instructions.

The independent proof is real this time, and this is the largest change since the last review. The signed record is the full cryptographic bundle again, not a hand-written summary. The witness file in the packet is the byte-for-byte artifact that was signed: I hashed it and found the same digest inside the signed payload. The signing certificate names the release tag, the release commit and the build run. A buyer working offline can follow the chain from published number to signed evidence without trusting any prose.

The automatic check grew from one value to six. It recomputes the engine fingerprint, the Python fingerprint, the commit, the release date and the transparency-log entry from the tag and the signed bundle, and refuses to pass if the witness in the packet is not the signed one or if the signed record has been replaced by a summary. I ran it and it passed.

The claim of no behaviour change is true for this release. The difference between the last two releases is the version number and wording inside the product's own disclosure text.

Other gaps stay closed. There is a written procedure for reissuing and marking superseded a report already sent. Word reports refuse the "reproducible" stamp without a real version. Version fallbacks read "unknown" rather than a stale number. The web viewer's second hand-typed version is gone and a test prevents its return. Cross-validation is published as 925 of 989 comparisons executed with 64 skipped, not a clean 925 of 925, and the fitted nature of the Primavera P6 comparison is disclosed. Results reproduce: 1,134 unit checks pass, 45 fixtures pass, and all seven copies of the engine on this machine are byte-for-byte identical and correctly stamped.

What failsThe full list, not a selection

The public website still names the wrong version. The live page a prospective buyer reads advertises the previous version while the product is a release ahead. The same stale number sits in the stored copies of that page and on the interactive demo page. This is the identical defect the last grade recorded, one release later, which shows it is corrected by hand each time rather than prevented. No test reads those pages, so nothing would catch it again.

Most report producers still do not print a version. Using the seller's own list of the tools that produce client documents, 18 of 54 refer to the engine version anywhere in their code. The other 36 do not. Word documents are protected because the seal is now refused without a version, but that protection does not reach the majority of outputs, including the HTML dashboards a buyer is most likely to receive.

The pre-deploy scrub for the website software is currently failing. Its own test suite reports one failure out of 310, and the failure is the check that stops a real organisation's name reaching a shipped bundle. The offending line is a comment added today to the very file that holds the single version number. The edit is not committed, so nothing wrong has shipped, but the gate is red right now, and it is red because of a change made to the version source file. That is the shape of problem change control exists to catch. It caught it, which is the system working; leaving it red is the part that is not.

The new automatic check has real edges. It skips entirely if the release has not been tagged yet, so it is silent during the window where mistakes are made. It does not compare the build-run link in the packet to anything, though that link happens to agree with the signed certificate today.

Smaller record-keeping gaps remain. A stored summary file still describes the previous release and is not included when the code is downloaded, so anyone reading it locally gets last month's answer. The file holding the single version number still cites an earlier release that has no tag, no change-log entry and no evidence folder, so a report stamped with that version points at nothing. Two of the seven copies of the engine on disk sit outside every check by deliberate exclusion, on the reasoning that they are regenerated rather than edited.

Limits of this gradeWhat this row does not establish

This row covers version and change discipline only. It says nothing about whether the schedule calculations are correct. Knowing which code ran is not the same as the code being right.

I verified the signed record as files on disk and by internal consistency: the witness hash equals the digest inside the signed payload, and the certificate names the tag, commit and build run. I did not query the public transparency log, so I cannot independently confirm the signature was issued.

I ran no report-producing tool end to end today. The finding that 36 of 54 producers carry no version comes from reading and searching their code against the seller's own list; a tool receiving the version indirectly could be undercounted.

Everything was checked on one machine where all repositories are present. Several checks weaken or skip elsewhere. I changed nothing, committed nothing and deployed nothing.

Criterion 2

Agreement with the reference scheduling application

The standard, stated for any product

Most delay arguments are run on schedules built in one commercial scheduling application. Any product that recomputes dates or float is claiming to reproduce that application's arithmetic, so it has to show its answers against that application's answers on a stated set of cases, and state how the cases were chosen, how many there are, and how large they are.

What to ask the vendor

  1. How many comparison cases, and how many activities in the largest one?
  2. Was the product changed after seeing the reference answers, and is there a held-back case?
  3. Does the comparison include activities that are in progress or already finished?
  4. Can I re-run the whole comparison myself from what you publish?

What a pass looks like

A published case list, a blind first capture, a held-out set the product was never tuned against, and the raw capture published so a third party can repeat it.

What a fail looks like

One capture, taken once, that the product was corrected against, then published as an accuracy score with the correction unmentioned.

Critical Path Partners, graded against it
Partially meets Unchanged since the 12 August grade

The maker's own comparison against the industry-standard scheduling software is real and I re-checked it today, but it is thirteen tiny test schedules that the engine was tuned to match after it first failed seven of them, and none of them tests the in-progress work that delay claims actually turn on.

Evidence14 items, each checked on 16 August 2026
All 14 evidence items are listed. The table scrolls and the header stays put; nothing is truncated.
Evidence, 14 rows, complete.
#Evidence, as recorded on 16 August 2026
1Re-ran the engine today at version 2.9.40 against all 13 case inputs in C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/cases/*/input.json and diffed against the committed *_engine columns: "ENGINE 2.9.40 cells checked 162 drift 0"
2Read the scheduling software's database read-only today (C:/Users/danaf/OneDrive/Documents/PPMDBSQLite.db). All 13 test projects present, each with LAST_SCHEDULE_DATE '2026-08-11 13:07:38.267'. Compared every committed date cell against the database: "date-cell mismatches: 0" across 108 cells
3Cell census of the 13 files at C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/cases/*/comparison.csv: "rows 27 cells 162 computed-date 100 actual-suffixed 8 float 52 blank 2 / real comparisons = 152"
4C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/comparison-matrix.md:13 reads "| Field-level checks | 27 |"; C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/apply-p6-capture.py:130 sets that value with "checks = sum(per_case_checks.values())", which counts activity rows carrying a verdict
5Late-date substitution, verified against the live database today: case 08 activity A has LATE_START_DATE '2026-01-12 08:00:00' while comparison.csv carries '2026-01-06 08:00:00 A'; case 10 activity B has '2026-01-26 08:00:00' against '2026-01-08 08:00:00 A'; case 09 activity B has EARLY/LATE START and END all '2026-01-09 17:00:00' and null float, while the CSV carries the December actuals and two blanks. Substitution written at C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/db-read-capture.py:188-191
6Float conversion: the database stores float in hours (observed values 0.0, 8.0, 16.0, 24.0, 40.0, -56.0) and the committed cells are days (0, 1, 2, 3, 5, -7). Converted by duration_hours_to_days imported at C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/db-read-capture.py:44 via sys.path.insert to a skills folder outside the repository. 42 of the 52 float cells are zero on both sides
7C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/apply-p6-capture.py:145 emits the provenance line as a fixed string: lines.append('| Compared against | Primavera P6 23.12, scheduled by a human operator |'). No version, operator, date or database fingerprint is read
8C:/Users/danaf/Projects/cpp-cpm-engine/.gitignore:78 still excludes the capture sheet, and 'git log --all' on that path returns no commits, so it has never been tracked. C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/db-read-capture.py:40 hardcodes one machine's database path
9Uncompared axis: C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/db-read-capture.py:51 labels the driving-path columns "evidence-only columns; apply-p6-capture.py ignores unknowns". Today the database shows case 11 activity A with DRIVING_PATH_FLAG 'N', total float 0.0 hours and free float 40.0 hours, while the engine returns criticalCodes ["A","B"]
10Calibration and its paired second implementation: git 9b748cc is the first capture at 6 PASS / 7 FAIL; commits 23ffeca, 264de84, bf442d5 and 05dc8b4 follow. 'git show --numstat' on those three engine fixes shows python_reference/cpm.py changed 51/12, 24/0 and 156/14, i.e. +231/-26 alongside the engine. No capture commit exists after 05dc8b4
11Disclosure now published (the August finding on this point is out of date): C:/Users/danaf/Projects/cpp-cpm-engine/DAUBERT.md:30, :376 and :474; CHANGELOG.md:53 and :70; validation/p6-comparison/README.md:26 and :127; docs/p6-comparison-schema.md:18 all state the 6-of-13 first result, the fit to one capture, the absence of a held-out case, and the uncommitted capture sheet
12The loaded file carries inputs only: parsing C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/cases-import.xer gives "TASK rows 27 non-blank computed fields: {}" across early dates, late dates, remaining-late dates, total float, free float, float path and driving-path flag
13Excluded-case wording: comparison-matrix.md:34-38 and DAUBERT.md:30 both call the two dropped cases divergences the scheduling software "cannot represent", while validation/engine-limitations/README.md:11 says in its own row that the software "stores lags in hours and honors sub-day precision natively" and the engine is the one that rounds, under a column heading still reading "Why P6 cannot compare". Both limitation case READMEs are still headed "## Engine output (v2.9.31)"
14Maker's validator re-run today: 'node scripts/validate-p6-comparison.js --all' returns "PASS — 13 case(s) validated (engine columns + P6 discipline + verdict format)." It checks the spreadsheets against committed engine output rather than re-running the engine
What worksIncluding the parts that were wrong before and are right now

The comparison is genuine, not a self-check. Today I opened the scheduling software's own database read-only and confirmed all thirteen test projects are there, all scheduled in a single run stamped 2026-08-11 13:07:38.267. Every one of the 108 date values the maker published matches the database exactly. Zero mismatches. The file that was loaded into the scheduling software carries no computed dates and no float at all, so that software worked out its own answers with nothing to copy. I also re-ran the engine today at its current version against all thirteen inputs and got zero drift from the published engine columns. The maker's own validator passes thirteen of thirteen.

The honesty problem flagged in the previous grade has been largely fixed. In August the calibration story was buried in one file and the main court-facing document did not carry the result at all. Today the calibration is stated plainly in the court-facing disclosure, in the release notes, in the comparison folder's own front page, and in the schema document: the first and only capture scored six of thirteen, the engine was then changed to match, no held-out case exists, and the input sheet is not in the public repository. Two stale figures that contradicted each other in the same document have also been corrected, and the release history now records the versions that were previously missing. On disclosure, the earlier finding is now out of date and I am recording that as a fix.

What failsThe full list, not a selection

The underlying evidence has not moved. There is still one capture, taken once, on one day. The engine failed seven of thirteen cases against it, was changed to match on those same cases, and was never tested against a fresh capture. Nothing has been added since. The second implementation the maker uses as a corroborating witness was edited in the very same commits, 231 lines added across the three fixes, so it is not independent confirmation for the behaviours this comparison covers.

Four specific things a buyer should check and will not like. First, the summary page a buyer opens still reports "Field-level checks: 27". That is the number of activity rows, not checks. The real cell count is 162. Anyone who counts the spreadsheets will catch it. Second, of those 162 cells only 152 are actual comparisons. Eight carry a date both tools were handed as an input, and two are blank. That census is not published anywhere. Third, and most serious for forensic work, no cell in this comparison tests a late date on an activity that has started or finished. The capture writes the actual date over the scheduling software's own computed value on those rows. I confirmed the discarded values today: on one case the software's own late start is 12 January while the published sheet reads 6 January; on another it is 26 January against 8 January; on a third, all four computed dates are 9 January and the sheet carries December actuals instead. In fairness, the engine's separate remaining-late-start field does match the software's raw value on the two cells I could check, so this looks like a display convention rather than a wrong answer. But the population that delay analysis lives on contributes no late-date comparison at all, and none of this is disclosed. Fourth, fifty-two of the comparisons are float values that the maker's own code converted from hours into days before comparing. Forty-two of those are zero on both sides, so ten are doing real work. The converter is loaded from a folder outside the repository.

Smaller but still true: the line stating which software version was used and that a human scheduled it is a fixed piece of text in a script. Nothing reads the version, the operator, or the date. Feed that script a hand-typed sheet and it prints the same line. The operator was the engine's author, and no third party has ever reproduced this. The input sheet is still excluded from the repository and the database path is hardcoded to one machine, so a buyer cannot re-run the comparison end to end. Criticality and driving-path data were captured but deliberately not compared, and on the one activity I checked by hand the two tools disagree on the label. And the two cases dropped from the count are still described as things the scheduling software cannot represent, when one of them is a four-hour lag that the software handles natively and the engine rounds. The maker's own limitation folder says so in its row text while its column heading still says the opposite, and both of those case files are still stamped with an engine version from several releases ago.

Limits of this gradeWhat this row does not establish

This row is only about whether the engine's answers agree with the industry-standard scheduling software. It does not grade anything else about the product: not the release process, not the version pinning, not the internal test suite, not the report writing. It does not say the engine is wrong. On the 152 cells that are real comparisons, both tools agree, and I confirmed that against the software's own database today. What it does not establish is how far that agreement generalises. The thirteen cases are synthetic, the largest has three activities, and all were run with one set of scheduling options, so nothing here speaks to real project files, alternative float settings, or the progress-override mode. The engine was fed hand-built inputs rather than a schedule file, so the file-reading path is untested by this comparison. I could not verify who pressed the schedule button or whether they had already seen the engine's expected answers, and the software's build number is not readable from the database, so the version is supported only by the installation folder and the file timestamps. The raw evidence exists on one machine, and the scheduled results currently sit in a sidecar file beside the database rather than in the database file itself, which is worth knowing if anyone ever needs to go back to the source. Finally, the disagreement I found on the critical-path label may be a definition difference rather than an error. It is unresolved because the comparison never tests that axis.

Criterion 3

Independent implementation cross-check

The standard, stated for any product

Running the same maths through a second, separately written implementation catches transcription and refactor errors. It is worth something only if you know how much of the product it reaches, how large the test networks are, and whether the two implementations share an author or a common ancestor.

What to ask the vendor

  1. What share of the product's code does the cross-check actually exercise?
  2. Who wrote the second implementation, and was it derived from the first?
  3. How many test networks, and how large is the largest?
  4. Which comparisons are skipped rather than passed, and are they in the headline denominator?

What a pass looks like

A published coverage figure, an honest denominator that counts skipped comparisons, and a plain statement of any shared ancestry between the two implementations.

What a fail looks like

A headline reading N of N passed, where N counts only the comparisons that ran and the ones that could not run were quietly removed from the total.

Critical Path Partners, graded against it
Partially meets Unchanged since the 12 August grade

The core scheduling calculation is checked line for line against a second, separately written build of the same maths and agrees on every comparison that runs, but that check reaches well under half the product and is run only on 45 tiny made-up test networks, never on a real project file.

Evidence16 items, each checked on 16 August 2026
All 16 evidence items are listed. The table scrolls and the header stays put; nothing is truncated.
Evidence, 16 rows, complete.
#Evidence, as recorded on 16 August 2026
1Ran node cpm-engine.crossval.js in C:/Users/danaf/Projects/cpp-cpm-engine. Exit 0. Prints: 'Fixtures: 45 passed, 0 failed' and 'Checks: 925 / 925 comparisons executed (the denominator is checks run, not the full field surface: a guarded field is skipped and not counted when either engine does not emit it...)'.
2Published figure now honest and consistent: README.md:6 badge 'JS↔Python 925/989'; README.md:13; README.md:221; DAUBERT.md:35; DAUBERT.md:144; VERIFY_RELEASE.md:35; python_reference/README.md:6; CHANGELOG.md:49-51. All state 925 of 989 with 64 skipped (61 ff_signed_working_days, 3 ff_signed).
3Independently instrumented a copy of the harness to log every skipped comparison. Output: 'SKIPCOUNTS {"ff_signed_working_days":61,"ff_signed":3}' / 'TOTALSKIPS 64' / 'PY_NULL_JS_VALUE 58'. Matches the published breakdown exactly. Of the 58, 44 have JS value 0 and 14 have a substantive value (12, 1, 3, 5, 5, 6, 6, 6, 14, 8, 2, 2, 17, -10).
4Tarjan claim corrected. DAUBERT.md:35: 'Tarjan SCC is not part of this surface: _tarjan_scc was stripped from python_reference/cpm.py for OSS distribution'. DAUBERT.md:358 repeats it in the limitations section.
5SHA pin now correct. python_reference/README.md:40 pins 27829ddab0a6440cbb3ea890bc21a71cdfa35168e6bb9473531e24556aee138f; git cat-file -p HEAD:python_reference/cpm.py | sha256sum returns the same value. node tests/no-stale-version-refs.test.js exits 0 and prints 'python_reference SHA-256 pin check — pin and sample figures match the bundled bytes'.
6Release evidence pins now match the tagged bytes. release-evidence/v2.9.39/python_reference-cpm.py.sha256 = da792b52... and git cat-file -p v2.9.39:python_reference/cpm.py | sha256sum = da792b52...; engine pin 8dc37455... matches git cat-file -p v2.9.39:cpm-engine.js. Same check passes for v2.9.40 (27829dda...).
7Stale production copy fixed. Parsed every cpm.py on disk and hashed the compute_cpm function's syntax tree. C:/Users/danaf/.claude/skills/_cpp_common/scripts/cpm.py, C:/Users/danaf/.claude/skills/cpp-forensic-mcp/_deploy/_cpp_common/scripts/cpm.py, C:/Users/danaf/Projects/cpp-forensic-mcp/_cpp_common/scripts/cpm.py and C:/Users/danaf/Projects/cpp-cpm-engine/python_reference/cpm.py all hash to 918a6325db0219e2. A full top-level function-by-function syntax-tree diff between the skills copy and the MCP copy returns no differences.
8Coverage of the JS engine by the parity run alone, measured today: npx c8 --include=cpm-engine.js node cpm-engine.crossval.js reports 'Statements : 38.22% ( 3438/8993 )' and 'Functions : 47.12% ( 41/87 )'.
9Scale of the test material. 45 fixtures, 105 activity results compared, largest fixture 6 activities (counted from harness output). grep -ci '\.xer|parseXER' cpm-engine.crossval.js returns 0. Distinct lag values in the whole harness: -3, -2, 0, 1, 2, 3.
10Extra harness exists but is private. node cpm-engine.crossval-extended.js in C:/Users/danaf/.claude/skills/_cpp_common/cpm-engine-js prints 'EXTENDED SURFACE crossval (LPM / salvaging / strategies)' / 'Fixtures: 10 passed, 0 failed' / 'Checks: 35 / 35', exit 0. No file matching *crossval-extended* exists anywhere under C:/Users/danaf/Projects/cpp-cpm-engine.
11Two production routines still uncompared. grep -c 'float_burndown|tarjan' against cpm-engine.crossval-extended.js returns 0, and 'topology_hash' returns 0 in both harnesses, while compute_float_burndown and compute_topology_hash are public in C:/Users/danaf/.claude/skills/_cpp_common/scripts/cpm.py.
12Alert carve-out reason does not hold. Re-ran the harness with the three skip_alert_parity: true flags (cpm-engine.crossval.js:766, :797, :930) set to false. Result: 'Fixtures: 45 passed, 0 failed' / 'Checks: 931 / 931'. Six extra comparisons run and all pass.
13Sub-day rounding exclusion reason is stale. cpm-engine.crossval.js:1069-1073 says sub-day-lag fixtures are excluded because 'JS Math.round and Python round() disagree on half-up vs banker's', but cpm-engine.js:386 defines _roundHalfUp and python_reference/cpm.py:68 defines _round_half_up, both floor(x + 0.5).
14Version drift in one repo. C:/Users/danaf/Projects/cpp-forensic-mcp/_cpp_common/scripts/engine_version.py:26 reads ENGINE_VERSION = '2.9.39' while that repo's own cpp-forensic-mcp/static/cpm-engine.js:151 and _cpp_common/cpm-engine-js/cpm-engine.js:151 both read '2.9.40', and C:/Users/danaf/.claude/skills/_cpp_common/scripts/engine_version.py:26 reads '2.9.40'. The lockstep guard at .claude/skills/_cpp_common/tests/test_engine_mirrors_in_lockstep.py walks only the skills tree (_SKILLS_ROOT) and checks cpm-engine.js files only.
15Same-author limit disclosed. DAUBERT.md:240 states the two implementations share an ancestor, that cpm-engine.js 'was itself reconstructed on 2026-05-09 from that same Python file', and that the harness 'cannot detect an error both sides inherited from the shared source logic'.
16Test suites green today. timeout 240 python -m pytest tests/ -q in C:/Users/danaf/Projects/cpp-forensic-mcp returns '310 passed in 17.07s'. python -m pytest tests/test_engine_mirrors_in_lockstep.py -q in .claude/skills/_cpp_common returns '5 passed'; test_crossval_js_python_parity.py returns '2 passed'.
What worksIncluding the parts that were wrong before and are right now

Two builds of the same engine, one in JavaScript and one in Python, are run side by side on 45 test cases and their answers compared value by value. It runs in one command, exits clean, and prints the fingerprint of the exact Python file it used so a checker knows which bytes were tested.

The accuracy failures found on 2026-08-12 have since been fixed, and I confirmed each one today.

The pass number used to be printed and published as "925 out of 925", which looked like a perfect score but was really just the count of comparisons that ran. Comparisons the two builds could not both answer were quietly dropped from the total. That is now published as 925 of 989, with the 64 dropped ones named and explained, in the badge, the readme, the court-facing disclosure document, the release verification file and the harness's own printed output. I instrumented the harness myself and counted the drops independently: exactly 64, made up of 61 on one field and 3 on another, and 58 of those are cases where the Python side has nothing to say and the JavaScript side prints a number. That is precisely what the published breakdown states.

The disclosure used to list a graph routine (Tarjan) as cross-checked when it was not in the Python file at all. It now says plainly that the routine is not on the cross-checked surface.

The fingerprint pinned in the documentation used to name a different file than the one shipped, which would have failed the product's own integrity step. It now matches the committed bytes, and an automated gate checks it on every run. The release evidence folders for the last two versions also now name the correct fingerprints for the code at those tags.

One production copy of the Python engine used to be a version behind and calculated differently. All four real copies on this machine now compute the core routine from identical code.

More of the production engine is cross-checked than the earlier grade credited. A second harness in the working skills tree also compares salvage mode, one alternative critical-path method and the multi-strategy path. It runs 10 cases and 35 comparisons and passes.

The product is also now candid about the deepest limit. The disclosure states that the JavaScript build was originally reconstructed from the Python file, that the two share an ancestor and one author, and that the check therefore cannot catch a mistake both inherited.

What failsThe full list, not a selection

The test material is very small and entirely invented. All 45 cases are hand-built. There are 105 activity results compared in total. The largest single network is six activities. Delays between activities are only whole numbers from minus three to plus three. No real schedule file is cross-checked anywhere; the file format that real schedules arrive in is never mentioned in the harness. There is one claimed check against a real 282-activity file, but that file is not published and the run cannot be repeated by anyone else. The product says so, which is honest, but a claim nobody can re-run is not evidence.

The check reaches only part of the product. Measured today with coverage instrumentation on the parity run alone, it exercises 38.22 percent of the JavaScript engine's statements and 41 of its 87 functions. Reading P6 files, salvage mode, time impact analysis, schedule health and DCMA scoring, float burndown, statutory holiday calendars and the court-facing disclosure renderer all have no second build to compare against. The disclosure correctly says the parity claim applies only to the core maths, but nowhere states how small a share of the product that is.

One number the engine prints as forensic evidence is still largely unchecked. On 58 of 105 activity results there is no Python implementation of it to compare against. On 14 of those the JavaScript side prints a real figure, including one value of minus 10, which is exactly the kind of unusual reading a forensic report would rely on. This is now disclosed rather than hidden, but it is not fixed.

In the production Python engine, two of the ten published routines, float burndown and topology fingerprint, plus the internal cycle-detection routine, still have no second-implementation comparison at all.

The extra harness that does cover salvage mode and the alternative critical-path methods lives only in a private working tree. It is not in the published open-source package, so a buyer or an opposing expert cannot run it.

Warning comparison is still counts and severity only. No warning code and no warning wording is ever compared, so a renamed warning of the same severity would pass unnoticed.

Two stated reasons for excluding things from the check do not hold up. Three cases skip warning comparison on the stated ground that one warning exists only on the JavaScript side. I switched those skips off: six extra comparisons run and all six pass. And sub-day delay cases are excluded on the stated ground that the two languages round differently, when both were harmonised on the same rounding rule.

Small labelling drift remains. In one repository the Python version file says 2.9.39 while its own bundled JavaScript engine and the master version both say 2.9.40. Every function in that file is identical to the canonical one, so no number would come out wrong, but a document produced through that path would name the wrong engine version. The automated lockstep guard walks only the skills tree and does not reach that file.

Limits of this gradeWhat this row does not establish

This row is only about whether the maths is checked against a second build of itself. It does not say whether either build is right. Both builds have one author and one common ancestor, which the product now admits, so agreement between them proves the two ports match, not that they match how commercial scheduling software behaves or how a court would expect a schedule to be calculated. Anything about the separate comparison against actual P6 output, the unit test suite, the release signing chain, or the products built on top of the engine is graded elsewhere.

Three specific things I did not establish. First, the comparison is done by turning both answers into text and matching the text, so a whole number on one side and a decimal on the other would be treated as equal. That blind spot exists even where the comparison does run. Second, I did not test the live hosted service, so the version drift I found applies to the local working copy of that repository, not necessarily to what the server is serving. Third, the coverage figure of 38.22 percent is for the parity run only. That is the right measure for this question, but it is not the product's overall test coverage, which is reported separately and is much higher.

One local wrinkle worth naming so nobody is surprised. On this Windows machine the reference file on disk carries Windows line endings, so running the documented fingerprint command against it returns a value that does not match the pin. The committed bytes do match, and a fresh checkout would match, so the pin itself is correct. But the instructions tell a checker that a mismatch invalidates the result and say nothing about line endings, so a Windows-based checker could stop at step one for no real reason.

Criterion 4

Honest description of the validation boundary

The standard, stated for any product

Every validation claim has an edge. The product has to describe that edge in the document the client actually receives, in the same words used in the technical repository. Candour that lives only where the client never looks is not candour.

What to ask the vendor

  1. Where on the paid deliverable is the validation boundary stated?
  2. Does that wording match the wording in your technical documents?
  3. Are excluded cases described as the reference tool's limits or as your product's?
  4. Does each published count mean what its label says it means?

What a pass looks like

The same caveat, in the same words, on page one of the report the client pays for.

What a fail looks like

Full disclosure in the code repository, a clean unqualified claim on the report that carries the invoice.

Critical Path Partners, graded against it
Partially meets Unchanged since the 12 August grade

Most places now admit the engine was corrected to match the reference scheduling software before it scored full marks, but the report a paying client receives still tells them it passed on its own.

Evidence18 items, each checked on 16 August 2026
All 18 evidence items are listed. The table scrolls and the header stays put; nothing is truncated.
Evidence, 18 rows, complete.
#Evidence, as recorded on 16 August 2026
1C:/Users/danaf/.claude/skills/schedule-health-review/template/Schedule_Health_Dashboard_v7_Obsidian.html:7791 still reads: 'the CPP CPM engine agrees with Primavera P6 23.12 on 13 of 13 independently scheduled test projects'. Identical text at C:/Users/danaf/.claude/skills/cpp-forensic-mcp/_deploy/schedule-health-review/template/Schedule_Health_Dashboard_v7_Obsidian.html:7791. Neither mentions the correction, the 27-activity scale, or the excluded case.
2C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/comparison-matrix.md:34-38 reads 'Two further cases are by-construction divergences that P6 cannot represent'. It is emitted by C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/apply-p6-capture.py:158, hardcoded, not computed.
3Fetched https://criticalpathpartners.ca/validation.html today (HTTP 200): 'Two cases that P6 cannot construct by design live in a separate engine-limitations folder and carry no P6 verdict.' Two lines above, the same page says 'The engine is day-granular, so sub-day lags round with a fatal-in-strict-mode alert.'
4C:/Users/danaf/Projects/cpp-cpm-engine/validation/engine-limitations/README.md, case 01 row: engine behaviour 'Engine is day-granular; sub-day lags emit SUB_DAY_LAG_ROUNDED ALERT'; reason column 'P6 stores lags in hours and honors sub-day precision natively.' The excluded case is an engine limit.
5C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/comparison-matrix.md:12 reads '| Field-level checks | 27 |'. The count is built at apply-p6-capture.py:249-250, which sums comparison.csv ROWS carrying a verdict, not fields.
6Counted the 13 committed comparison.csv files today: 'cases 13 rows 27 populated p6 cells 160' (162 defined, 2 blank).
7C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/db-read-capture.py:190 sets the graded late-start cell to the actual start for any started activity. Listing the affected cells today returns 8, on cases 08, 09 and 10. Case 09 adds 2 blanks accepted as PASS (verdict text in its comparison.csv). Total 10 of 162.
8The capture sheet carries FLOAT_PATH and DRIVING_PATH_FLAG columns for all 27 activities. apply-p6-capture.py grades only ES, EF, LS, LF, TF and FF, so they are never read. On case 11 the sheet records DRIVING_PATH_FLAG N for activity A and Y for B; the engine's critical rule is float at or below zero (C:/Users/danaf/Projects/cpp-cpm-engine/python_reference/cpm.py:1622), giving both A and B.
9C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/apply-p6-capture.py:205: 'if not ok and iso == eng: ok = True'. Two different dates can both score PASS on the same field.
10Built a manifest for the health screen today: engine block stamps version 2.9.40, the crossval sentence, and 'release-evidence/v2.9.40/'; conventions float_basis is null. grep for computeCPM, cpm-engine or require over the 1.3 MB report template returns 1 match, a display label at line 8091. The screen recomputes nothing; total_float_hr_cnt is read straight off the file 21 times.
11FIXED and re-verified: C:/Users/danaf/Projects/cpp-cpm-engine/DAUBERT.md:376 and :474 now describe the comparison as shipped and fitted, naming the 6-of-13 first pass, replacing the earlier 'analyst populates the P6 columns' and 'P6 comparison evidence is on the Roadmap' wording. C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/README.md:22,26 now records the columns as populated 2026-08-11 and the result as fitted.
12FIXED and re-verified: C:/Users/danaf/Projects/cpp-lens-viewer/src/sections/engine-parity.js:53-54 declares 162 field cells and 152 real comparisons; :90-92 states the fitted-not-blind limit and the 6-of-13 first pass. 'npm test' in that repo today: '66 passed (66)', 'Tests 523 passed (523)'.
13C:/Users/danaf/Projects/cpp-lens-viewer/src/sections/dcma-lite.js:185-194 renders a heading, a one-line note and a marketing link. No basis note, no pointer to the parity section. Float is read straight off the file at :90, :100, :159.
14Only one capture exists. 'git log --oneline --all -- validation/p6-comparison/cases/*/comparison.csv' ends at 9b748cc, 'first P6-native capture - 13/13 scheduled, 6 PASS / 7 FAIL', dated 2026-08-11. Nothing since.
15No test compares the Python engine to the reference software: grep for 'python_reference' in the comparison harness returns nothing. I ran it myself today against the same captured values using the harness's own normalization: 'cases: 13 failing cases: [] / cells compared: 162 cell failures: 0'.
16Case sizes measured today from the 13 input.json files: 27 activities, 13 relationships, maximum in-degree 1 and maximum out-degree 1 in every case. Largest case is 3 activities.
17C:/Users/danaf/Projects/cpp-cpm-engine/validation/p6-comparison/generate-cases.js:23 requires '../../cpm-engine.js' and :632 calls E.computeCPM. The comparison covers that one function in the JavaScript engine.
18C:/Users/danaf/Projects/cpp-forensic-mcp: 'timeout 240 python -m pytest tests/ -q' returns '310 passed in 16.42s', exit 0.
What worksIncluding the parts that were wrong before and are right now

Four things improved since 12 August and I confirmed each today.

The headline agreement score is now openly labelled as a self-corrected result. The engine was checked against the industry-standard scheduling application once, scored 6 of 13, was then changed to match the answers that check had produced, and only then scored 13 of 13. That admission now appears in the change log, the court-facing disclosure document, the validation folder README, the cross-examination prep notes, the live public results page, and the free viewer.

Two specific faults named in the earlier review are genuinely fixed, so the earlier finding is wrong as written today. The court-facing document used to describe this evidence as still pending and file it under future work. It now describes it as delivered and fitted, and names the commits that did the fitting. The validation folder README used to say the reference columns were empty and awaiting an analyst. It now says they are filled, dated, and fitted rather than blind.

The free viewer now publishes the small print the earlier review had to work out by hand. It states 162 fields compared across 27 activities, of which only 152 are genuine comparisons, the rest being actual dates carried through or blanks. It states the first blind run scored 6 of 13. Its own test suite runs green.

A separate overstated figure is gone. Internal agreement between the two copies of the engine used to be published as 925 out of 925. It now reads 925 of 989 with 64 comparisons skipped, on the live site, in the shared provenance code, and in the viewer.

What failsThe full list, not a selection
  1. The paid deliverable still overstates it on page one. The schedule health report says the engine agrees with the reference software on 13 of 13 "independently scheduled test projects". It does not say the engine was changed to match those same 13, does not say they total 27 activities, and does not say a real disagreement was removed from the count. Unchanged since the last review, and it also ships in the hosted bundle.
  2. The exclusion is still described backwards on the live public page, in the generated comparison table, and in the operating procedure. All three say the two dropped cases are things the reference software "cannot represent" or "cannot construct by design". One of them is a lag of a few hours. The reference software handles that natively. It is the engine that cannot. The company's own limitations folder says so plainly, and the sentence is hardcoded in the script that writes the table, so it is not computed from the data.
  3. The comparison table's headline count is mislabelled. It reads "Field-level checks | 27". Twenty-seven is the number of activities. The actual number of fields filled in is 160, out of 162 defined. This understates rather than inflates, but it is the wrong label on the published number.
  4. Ten of the 162 fields are not comparisons. Eight are the schedule's own actual dates handed back and compared to themselves, on the two cases that matter most in delay work, work in progress and work done out of sequence. Two are blanks accepted as passes. The free viewer now discloses this. The comparison table and the paid report do not.
  5. Critical path membership is still collected from the reference software and never scored. The one case where the two can be compared disagrees: the reference software marks one activity as driving, the engine calls both critical. A critical path count is something these products publish, and it sits outside the agreement claim.
  6. The scoring script still accepts two different dates as a match on the same field, by falling back to the raw value if the adjusted one misses.
  7. The provenance file attached to every health report stamps an engine version, an agreement figure, and a link to the engine's release evidence, on a report that never runs the engine. The one field designed to record how float was obtained is left empty.
  8. The free viewer's own screening section still carries no note saying where its numbers come from and no link to the parity page that explains the limits.
  9. The Python copy of the engine, which every scripted product actually uses, is still never compared to the reference software by any test. I ran it myself today and it matched on all 162 fields. Nothing in the product asserts that, so it could drift without anyone noticing.
  10. There is still only one capture and no held-back case. The 13 test schedules total 27 activities and 13 links. No case anywhere has an activity with two predecessors or two successors, so nothing with merging or branching paths, and nothing with competing float, has ever been checked against the reference software.
Limits of this gradeWhat this row does not establish

This row is about how the checking is described, not about whether the engine's arithmetic is correct. I did not find a wrong number in the engine itself. I could not redo the reference-software capture. That side is pinned data from one session, run by the company's own principal, so "agrees with the reference software" rests on that record being honest. The word "independently" in the published wording means independent of the engine, not a third party. I checked the live public results page and the staged copy on the owner's machine, plus the site map, but not every page on the site. I did not render a finished health report, so the page-one wording is established from the template that produces it and from the copy in the hosted bundle, not from a rendered file. Whether the internal agreement figures, the release evidence, or the version records are accurate are separate questions graded elsewhere.

Criterion 5

Where the float number comes from

The standard, stated for any product

Float can be read from the schedule file as submitted, or worked out again by the product. Those are different numbers, and the choice decides which activities get called critical and who gets blamed. The product must declare which one it did, on the figure, and a recompute must honour the imposed dates already in the schedule.

What to ask the vendor

  1. Is this float read from my file, or recomputed by you?
  2. If it is recomputed, are the schedule's constraints fed into the calculation?
  3. Does the declared basis match what the code actually does?
  4. If the stored float is stale or overridden, does anything flag it?

What a pass looks like

A basis statement on every product that prints a float-derived number, and the schedule's own constraints fed into any recompute.

What a fail looks like

A declared basis that contradicts the code, or a critical path test that passes merely because the file asserts one activity has no spare time.

Critical Path Partners, graded against it
Partially meets Unchanged since the 12 August grade

Most of this suite reports the float number already sitting in your schedule file rather than working it out again, two products describe their own float backwards, and every product that does recompute ignores the imposed dates in the schedule, which on a test file moved the project finish by six working days and turned 14 activities from having spare time to having none.

Evidence18 items, each checked on 16 August 2026
All 18 evidence items are listed. The table scrolls and the header stays put; nothing is truncated.
Evidence, 18 rows, complete.
#Evidence, as recorded on 16 August 2026
1C:/Users/danaf/.claude/skills/path-explorer/scripts/path_explorer_builder.py:271 calls compute_cpm (a full forward and backward pass); C:/Users/danaf/.claude/skills/path-explorer/scripts/render_path_dashboard.py:539-544 publishes float_basis 'total float as computed by the source scheduler; this trace reads the network as presented and does not recompute it'. The same text is in C:/Users/danaf/Projects/cpp-forensic-mcp/path-explorer/scripts/render_path_dashboard.py:539.
2C:/Users/danaf/.claude/skills/schedule-health-review/template/Schedule_Health_Dashboard_v7_Obsidian.html:7784 verbatim: 'Taken from the file as submitted. No forward or backward pass is re-run, so a stale or overridden float value is reported as it stands rather than corrected.'
3C:/Users/danaf/.claude/skills/schedule-health-review/template/Schedule_Health_Dashboard_v7_Obsidian.html:8921 declines the formal DCMA C12 perturbation test and directs the reader to the analysis server's critical_path_validator. grep for 'perturbat' across C:/Users/danaf/.claude/skills/critical-path-validator returns no hits, so no such test exists there.
4Ran C:/Users/danaf/.claude/skills/critical-path-validator/scripts/cp_validator.py validate_critical_path on schedule-health-review/tests/fixtures/scenario_pairs/03-constraint-heavy/current.xer. Output: overall_rating GREEN / overall_score 87.0 / overall_confidence High Confidence; constraint_driven AMBER 70; lpm_confirmed_false_cp n=9 of cp_total 10; overall_rating_capped_by_red None.
5C:/Users/danaf/.claude/skills/critical-path-validator/scripts/cp_validator.py:756 caps the diverging sub-check with 'cstr_score = min(cstr_score, 70)' regardless of how many activities diverge; cp_validator.py:1423-1430 caps a GREEN headline only when a sub-check is RED, never AMBER.
6C:/Users/danaf/.claude/skills/critical-path-validator/scripts/cp_validator.py:1674-1679 runs compute_cpm for the driver-chain narrative, then overwrites the result: comment 'Override TF with P6's pre-computed total_float_hr_cnt' followed by _node['tf'] = _hrs_to_days(...).
7C:/Users/danaf/.claude/skills/critical-path-validator/scripts/dcma14.py:732 registers ('DCMA-12-ResourceCoverage', 'Resource Coverage', 'DCMA 14-Point #12 (same as #10)', False); dcma14.py:925-926 gives every such row 'passed_count += 1; continue'. Ran dcma_14_assess on the same fixture: dcma_score 13, with DCMA-12-ResourceCoverage reported INFO 'Resource coverage is assessed under DCMA #10 in this implementation'.
8Constraint test, run today on schedule-health-review/tests/fixtures/scenario_pairs/03-constraint-heavy/current.xer using the suite's own converter and engine. Constraint-blind (what the products actually do): project finish 2026-10-15, 11 unfinished activities at zero or negative float. Same file with the XER's own cstr_type/cstr_date fed in: finish 2026-10-21, 25 unfinished activities at zero or negative float, float changed on 26 activities, 14 flips from positive to zero or negative including CST-01014 from +7.0 to -4.0.
9grep across all of C:/Users/danaf/.claude/skills for any code setting the engine's 'constraint' key returns exactly two hits: C:/Users/danaf/.claude/skills/_cpp_common/scripts/cpm.py:972 (the engine reading it) and _cpp_common/tests/test_cpm_strategies.py:170 (a unit test). No product feeds it.
10C:/Users/danaf/.claude/skills/impacted-as-planned/scripts/iap_critique.py:94-95 verbatim: "compute_cpm's node.get('constraint') consumer is fed from a different, constraint-aware caller, not this one." No such caller exists.
11C:/Users/danaf/.claude/skills/path-explorer/scripts/path_explorer_builder.py:153-176 builds the activity dict handed to the engine with no 'constraint' key; the XER's cstr_type and cstr_type2 go into task_meta, which is attached to the nodes after the calculation (path_explorer_builder.py:203-212).
12C:/Users/danaf/.claude/skills/forensic-delay-analysis: criticality test is stored float at scripts/compute_window.py:123; per-activity float at compute_window.py:216, 439-440; windows_analysis.py:268. Its basis of measurement renderer, scripts/render_cinematic_narrative.py:1085-1120, emits only day unit, calendar and date fields. grep for 'float_basis' across the whole skill returns zero hits.
13Survey of float_basis declarations across all skills that read total_float_hr_cnt: declared by path-explorer, critical-path-validator, measured-mile-disruption, claim-provability and schedule-risk-analysis. Not declared by forensic-delay-analysis, time-impact-analysis, collapsed-as-built, claim-workbench, monthly-progress-report, bess-monthly-report, [client-named monthly report skill, name withheld] or 3-week-lookahead.
14C:/Users/danaf/Projects/cpp-lens-viewer/src/sections/dcma-lite.js:158-165: metric 12 'Critical Path Test (CP activities)' is criticalCount = tasks.filter(t => parseFloat(t.total_float_hr_cnt) <= 0).length, status PASS when criticalCount > 0. grep for computeCPM across cpp-lens-viewer/src returns no hits; total_float_hr_cnt is read by gantt.js:48, summary.js:24 and :67, dashboard.js:64, dashboard-creator.js:12, risk-register.js:74, distribution.js:89, lookahead.js:91 and constraints-float.js:79, none of which discloses the source.
15C:/Users/danaf/Projects/cpp-lens-viewer/src/sections/engine-parity.js (rewritten, header dated 2026-08-12) now publishes 'Fitted, not blind' and 'The port total skips comparisons' as named limits and closes with 'The comparison covers the CPM engine. It says nothing about the browser-side sections of this viewer, which read your file and present it.'
16C:/Users/danaf/.claude/skills/schedule-risk-analysis/scripts/risk_builder.py:61-63 declares float_basis 'total float from the deterministic CPM pass'; risk_builder.py:326-329 docstring for the helper that supplies it says 'Uses total_float_hr_cnt directly - does NOT rebuild a CPM pass'; risk_exporters.py:24-26 confirms the On CP column is driven by the XER's native total_float field.
17C:/Users/danaf/.claude/skills/forensic-delay-analysis/scripts/mip_3_5.py:16-22 (the module labelled MIP 3.7 in output): 'time_impact_days is taken directly from the event's claimed_calendar_days, and the period finish before/after is reconstructed by adding that delta to the contemporaneous PROJECT scheduled finish.' No CPM is run.
18C:/Users/danaf/.claude/skills/schedule-health-review/template/Schedule_Health_Dashboard_v7_Obsidian.html:7791 states '13 of 13 independently scheduled test projects' with no fitted caveat. grep for 'fitted' or '6 of 13' across schedule-health-review, critical-path-validator, path-explorer, forensic-delay-analysis and _cpp_common returns no hits.
What worksIncluding the parts that were wrong before and are right now

Two things are stated correctly and one has been fixed since the last grading.

The schedule health dashboard is the honest end of the range. On page one it tells the reader that float is "Taken from the file as submitted. No forward or backward pass is re-run, so a stale or overridden float value is reported as it stands rather than corrected." It also declines the formal DCMA critical path test by name and says its own version is a screening substitute, not something to cite in a claim.

The critical path validator declares its float basis accurately as "total float as computed by the source scheduler, with longest-path cross-check", and it does run a genuine independent longest-path calculation, so it can and does report activities the file calls critical that the arithmetic does not put on the longest path.

Now fixed: the last grading said the browser viewer published engine credentials against Primavera P6 with no limit naming the boundary. That is no longer true. The viewer's engine parity page was rewritten and now states, in its own words, that the 13 of 13 P6 result is fitted, that the first blind run scored 6 of 13 and the engine was then changed to match, that the port-agreement headline skips 64 comparisons rather than passing them, and that "The comparison covers the CPM engine. It says nothing about the browser-side sections of this viewer, which read your file and present it."

What failsThe full list, not a selection

Everything else in the original finding reproduces today, unchanged.

  1. The path explorer product does recompute the network from scratch. Its published basis of measurement says the opposite: "this trace reads the network as presented and does not recompute it". That statement is false about its own code, and the same false line is served by the live analysis server.
  2. The forensic delay analysis product, the one used to build a delay claim, reads the stored float everywhere and declares no float basis at all. Its basis of measurement panel lists day unit, calendar and date fields, and has no row for float. A wrong or overridden float in the source file flows straight into which activities are called critical, into who gets blamed, and into the float consumption figures, with nothing flagged.
  3. Every recompute in the suite ignores the imposed dates in the schedule. The engine supports the full Primavera constraint set, but no product feeds it. Reproduced today on the supplied constraint-heavy test file: with constraints ignored the project finishes 2026-10-15; with them honoured it finishes 2026-10-21. Float changes on 26 activities. The count of unfinished activities with no spare time goes from 11 to 25. Fourteen activities the constraint-blind pass shows with 7 to 8 days of spare time are actually at zero or minus 4. This matters most where it hurts: the time impact and but-for delay products exist to produce a delay figure from a recomputed finish date, and they produce it with the imposed dates switched off. The code even documents the gap and then names a constraint-aware caller that does not exist.
  4. The browser viewer's DCMA metric 12, labelled "Critical Path Test (CP activities)", is still a green PASS whenever the file asserts that at least one activity has no spare time. No test is performed. The viewer contains no scheduling engine, and none of its reading screens says where the float came from.
  5. The risk analysis product's declared basis says float comes "from the deterministic CPM pass". The column it drives comes from a helper whose own comment says it "Uses total_float_hr_cnt directly - does NOT rebuild a CPM pass". The declaration is wrong in the product's favour.
  6. The critical path validator's own finding cannot reach its headline. On the test file, 9 of 10 activities the file calls critical are not on the longest path, and the report still reads GREEN, 87.0, High Confidence. The divergence caps one sub-check at 70 flat, and one diverging activity produces the same 70 as nine. Its narrative section runs a full recompute and then overwrites the recomputed float with the file's stored value before the prose is written.
  7. The same tool reports a DCMA score out of 14 while awarding a free point in the slot where the Critical Path Test belongs. That slot holds a duplicate of the resources check and is scored as an automatic point. On the test file it reports 13 of 14 with the critical path test never run.
  8. The schedule health dashboard tells readers to run the analysis server's critical path validator for the formal test. That tool has no such test either.
  9. Reports that go out to clients read the stored float and declare no basis: the monthly progress report and its two project-specific siblings, the three-week field lookahead, and the claim workbench trust report.
  10. The health dashboard still states "the CPP CPM engine agrees with Primavera P6 23.12 on 13 of 13 independently scheduled test projects" with no mention that the engine was corrected to match after a first blind run of 6 of 13. The browser viewer now carries that caveat. The dashboard does not.
Limits of this gradeWhat this row does not establish

This row is only about where the float number comes from and whether the product says so. It does not establish whether the recomputed float is right. Nothing here was compared against a live Primavera P6 session, so no figure above measures how close this suite gets to P6.

The scale of the constraint effect was measured on one small synthetic test file supplied with the product, and that file's stored float values were hand-typed by the file generator, not produced by Primavera. That does not weaken the constraint finding, because that test compares the product against itself with only the imposed dates switched on, but it does mean the six-day and 11-to-25 figures are illustrative of one file, not a measured error rate on real schedules. No real project schedule was tested.

The test file also contains one constraint code that is not a genuine Primavera token, so only 8 of its 15 imposed dates were recognised. The effect above is what those 8 alone produce.

This row says nothing about the other criteria: engine version consistency, release evidence, the P6 comparison itself, data privacy, or the test suites.

Criterion 6

Day units and calendars

The standard, stated for any product

A delay figure means nothing without its unit and the calendar behind it. Working days on the project calendar and calendar days are different numbers, and an approximation such as five sevenths is neither. The unit and the calendar belong beside the figure, not in a policy document.

What to ask the vendor

  1. Working days or calendar days, and on whose calendar?
  2. What does the product do when no project calendar is available?
  3. Is that fallback disclosed on the affected figure, or only in your conventions?
  4. What happens to a calendar your parser cannot decode?

What a pass looks like

Unit and calendar named beside the figure, and the fallback disclosed at the point of use by a mechanism rather than by a hand-typed sentence.

What a fail looks like

A written convention promising per-figure disclosure with nothing in the code able to deliver it, and silent substitution of a five-day week.

Critical Path Partners, graded against it
Partially meets Unchanged since the 12 August grade

The tool counts working days correctly under the hood, and the worst reporting bug is now genuinely fixed, but several finished reports still print a promise about days and calendars that the numbers on the same page do not keep.

Evidence16 items, each checked on 16 August 2026
All 16 evidence items are listed. The table scrolls and the header stays put; nothing is truncated.
Evidence, 16 rows, complete.
#Evidence, as recorded on 16 August 2026
1FIXED, verified by execution: cd C:/Users/danaf/.claude/skills/monthly-progress-report && python -m pytest tests/test_variance_basis_is_work_day_delta.py -q -> "3 passed in 0.58s". The test builds a fixture where the old five-sevenths path and the correct method disagree, runs the real pipeline, and asserts the page prints the computed figure and contains no "5/7" footnote.
2FIXED: monthly-progress-report/scripts/render_dashboard.py:422 and monthly-progress-report/scripts/render_report.py:165 now both read variance_wd = meta.get('variance_to_baseline_wd') with no fallback. render_dashboard.py:421 comment: "No baseline shift available -> render an em dash, not an estimate."
3FIXED: monthly-progress-report/scripts/compute_metrics.py:1051-1056 publishes the computed working-day figure and the calendar name into project.metadata, which is what both renderers read.
4STILL FAILS: _cpp_common/scripts/skill_manifest.py:82-87 still states the fallback "is disclosed on the affected figure". Searched _cpp_common/scripts for any per-figure disclosure mechanism: none exists. xer-parser/scripts/xer_parser.py:835-892 returns a bare integer with no record of the calendar used.
5STILL FAILS: rendered the concurrency report from its own cached fixture. Output: "day_unit_note present: True" (the sentence "No calendar-day figure is displayed") and "cd occurrences on tiles: 10". Source: concurrency-apportionment/scripts/render_conc_dashboard.py:464-471 (four headline tiles carry a calendar-day sub-label) and :474 pct = (100.0 * cc_cd / span_cd) (headline gauge computed on calendar days).
6STILL FAILS: ran counter-claim-analysis end to end via its own e2e fixture. Output: "panel promise present : True" / "calendar-day figures : ['91 calendar days', '91 calendar days']" / "5/7 disclosure present: True" / "default cal row : True".
7STILL FAILS: _cpp_common/scripts/report_shell.py:540 renders _row("Calendar", cal_id or "The activity's own P6 calendar", ...); skill_manifest.py:88 defaults calendar_basis_id to None; counter-claim-analysis/scripts/render_rebuttal_dashboard.py:776 calls rs.conventions_block(build_conventions()) with no calendar named. Verified the rendered HTML contains "The activity&#x27;s own P6 calendar".
8STILL FAILS: counter-claim-analysis/scripts/defense_theories.py:270 return round(cal_days * 5 / 7, 1), cal_days, 'working', disclosed at :556 and :596. constructive-acceleration/scripts/accel_elements.py:183 uses the same helper; :217 and :229 print the resulting day count with no basis note; grep for "5/7" across constructive-acceleration/scripts returns only the docstring at :138.
9PARTLY CORRECTED FROM THE 2026-08-12 ROW: forensic-delay-analysis/scripts/compute_window.py:374 still contains actual_workdays = round(cal_days * 5 / 7, 1), but grep across the whole skills tree for "actual_overrun" outside tests returns only compute_window.py itself. No renderer prints it, so I cannot show it reaching a reader. The earlier row's claim that four shipped skills print such a figure no longer holds.
10STILL FAILS: delay-analysis/scripts/component_schedule.py:193 slip_days = work_day_delta(baseline_end, current_end) with no calendar argument, under a comment at :192 claiming the working-day rule is followed. delay-analysis/tests/test_audit_fixes_2026_06_05.py:45-49 asserts that literal string, so the suite pins the defect. python -m pytest tests/test_audit_fixes_2026_06_05.py -q -> "11 passed in 0.05s".
11STILL FAILS: xer-parser/scripts/xer_parser.py:580-581 sets result['parse_incomplete'] = True for an undecodable calendar. grep -rn "parse_incomplete" --include=*.py over the whole skills tree returns only the parser itself, test fixtures and tests. No production consumer.
12STILL FAILS, reproduced by execution: work_day_delta('2026-01-05 08:00','2026-01-05 17:00') -> 0; work_day_delta('2026-01-05 23:00','2026-01-06 01:00') -> 1. Cause: xer-parser/scripts/xer_parser.py:859 datetime.strptime(d[:10], '%Y-%m-%d').
13STILL FAILS, reproduced by execution: with calendar {'work_days':[99]}, work_day_delta('2026-01-05','2026-01-09') -> 0 (true answer 4), get_work_days_between -> 0, add_work_days -> OverflowError. The warning at _cpp_common/scripts/cpm.py:865-875 says such a calendar falls back to Monday to Friday; xer_parser.py:788 and :885 use calendar_info.get('work_days') or [1,2,3,4,5], so a non-empty invalid list is truthy and does not fall back.
14STILL TRUE, and disclosed: contract-vs-baseline-comparison/scripts/build_report.py:148-165 hardcodes ONTARIO_HOLIDAYS through 2028 only; :167-183 defines a local Monday-Friday workday_delta that never reads the P6 calendar. Disclosed at :973-980.
15CREDIT, verified: cpp-cpm-engine/cpm-engine.js:872 emits SUB_DAY_LAG_ROUNDED; cpp-cpm-engine/package.json line 3 reads "version": "2.9.40". _cpp_common/scripts/cpm.py:95-96 records that this disclosure "intentionally remain[s] JS-only", so it reaches no Python report.
16SUITE STATE: cd C:/Users/danaf/.claude/skills/_cpp_common && python -m pytest tests/ -q -> "312 passed, 1 skipped, 2 deselected, 4 subtests passed in 27.23s". The conventions tests in _cpp_common/tests/test_skill_manifest_conventions.py assert the wording of the convention, not the behaviour of the reports, which is why the contradictions above pass a green suite.
What worksIncluding the parts that were wrong before and are right now

The core date-counting routine is real. It counts working days on the project's own P6 calendar, honours days forced off such as statutory holidays and days forced on such as a worked Saturday, and refuses to compute a nonsense span instead of guessing. Ran it: a Friday to Monday move returns 1 working day on a five-day calendar, 2 on a six-day, 3 on a seven-day.

The biggest failure in the 2026-08-12 grade is fixed, and the fix is real rather than cosmetic. The monthly progress report used to print a headline "Variance to Baseline" figure derived by multiplying calendar days by five sevenths, then labelled as working days. On the sample data it printed "0 Working Days" on a page whose own headline said the job was 11 working days late. Both the web dashboard and the Word report now read the single figure that was actually computed on the finish driver's calendar, and print a dash rather than an estimate when there is no figure. A test builds a case where the old shortcut and the correct method deliberately disagree, runs the real pipeline, and fails if the old behaviour returns. It passes today.

Two reports state their counting basis honestly at the top of the page. The concurrency report says in plain words when it fell back to a Monday to Friday week with no calendar evidence, and raises a warning. The contract-versus-baseline report states outright that its bucket figures use a fixed five-day Ontario calendar while its milestone figures use the project's own calendar. Naming that is to CPP's credit.

One report discloses its shortcut right beside the affected number. The counter-claim report prints "5/7 Mon-Fri approximation; supply a P6 calendar for exact count" in the same sentence as the day figure. That is the standard the published convention asks for, and it is met there.

What failsThe full list, not a selection

The published promise is still not built. CPP's own written convention says that where no project calendar is available, the Monday to Friday fallback "is disclosed on the affected figure". No mechanism anywhere does this. The counting routine returns a plain number with no record of which calendar produced it, so nothing downstream can tell. The one skill that discloses beside the figure does so by hand-written sentence, not by any shared safeguard.

Two reports still print calendar-day figures on a page that promises none are shown. The convention text says "No calendar-day figure is displayed." Rendered the concurrency report from its own fixture: that exact sentence appears in the basis panel, while all four headline tiles carry a calendar-day sub-label and the headline percentage gauge is computed on calendar days. Rendered the counter-claim report end to end: the same promise appears, and so does "91 calendar days", twice.

A report can claim calendar awareness it never had. The shared basis panel prints, in bold, "The activity's own P6 calendar" whenever a report does not name one. The counter-claim report renders that panel with no calendar named, on a page where the day figure came from the five-sevenths shortcut. The bold claim and the figure disagree.

The five-sevenths shortcut is still live in two shipped skills, and one of them says nothing. The convention forbids it outright. Counter-claim discloses it. Constructive acceleration uses the same helper and prints the resulting day count with no note at all. A third skill still computes such a figure, but I could find no report that displays it.

One owner-facing figure is still calendar-blind, and the test suite pins it that way. In the claim package builder, the per-component slip is counted with no calendar passed, so every component silently uses Monday to Friday, while the comment above the line claims it follows the working-day rule. A test asserts the exact text of that calendar-blind call, so the green suite certifies the defect rather than catching it.

A calendar the parser cannot read still becomes Monday to Friday in silence. The parser sets an internal flag when a calendar decodes to no working days at all. Searched every skill: nothing in production reads that flag. Nothing warns and nothing stops.

Part-day precision is still dropped without a word. The routine cuts every timestamp back to the date. Ran it: a nine-hour slip inside one day reads as zero days, and a two-hour slip across midnight reads as a full working day. The separate calculation engine raises a loud alert about part-day loss, but that alert reaches no report a client receives, by design.

A warning message still describes a fallback the code does not perform. The engine warns that an invalid calendar will fall back to Monday to Friday. Ran it: an invalid but non-empty calendar does not fall back. It returns zero days across a span whose true answer is four, and a related helper crashes outright. Zero days reads as "no slip".

One report still counts on a hardcoded Ontario five-day week with a fixed holiday table that runs out after 2028, never consulting the project's calendar. It says so, but the numbers are wrong for any project not on that calendar.

Every remaining error runs in the same direction. Each one makes the delay look smaller than it was.

Limits of this gradeWhat this row does not establish

This row is only about how days are counted and how that basis is stated. It says nothing about whether the delay conclusions themselves are right, whether the method matches any published standard, or whether the numbers would survive cross-examination on any other ground.

I checked the shared counting routine, the shared basis panel, and the skills named in the earlier grade. I did not read all thirty-plus skills line by line, so the claim that no per-figure disclosure mechanism exists rests on the absence of any such mechanism in the shared code and on the plain-number return type, not on an exhaustive per-skill read. I did not check the schedule viewer product at all.

The undecodable-calendar path was proven with a synthetic file. I have not measured how often a genuine schedule export triggers it, only that the silent substitution happens once it does.

The invalid-calendar crash is not reachable from CPP's own file parser, which cleans up bad values first. It is reachable when calendar data is supplied by hand or by another tool. I report it as a warning message that contradicts the code, and a robustness gap, not an everyday failure.

All checks were read-only. No files were changed, committed or published.

Criterion 7

Reproducibility of the deliverable

The standard, stated for any product

A report the other side cannot reproduce is an assertion, not evidence. Reproducibility means the recipient can identify every input, the build, and the settings, and can re-run to the same answer. Anything with a random component must record its seed.

What to ask the vendor

  1. Can I identify every input from the report alone?
  2. Is there a command I can run, and can I obtain the code to run it?
  3. Does re-running the same inputs produce the same numbers?
  4. If a simulation is involved, where is the seed recorded?

What a pass looks like

Input fingerprints on the face of the report, a rerun command that works, stable outputs across runs, and a recorded seed for anything random.

What a fail looks like

The word Reproducible stamped on a page carrying no fingerprints, or a simulation that returns a different answer every time while claiming determinism.

Critical Path Partners, graded against it
Partially meets Unchanged since the 12 August grade

The calculation engine underneath the reports can now be rebuilt and checked by an outsider, and I did exactly that today and it matched, but most of the reports themselves still cannot be reproduced by the person who receives them.

Evidence24 items, each checked on 16 August 2026
All 24 evidence items are listed. The table scrolls and the header stays put; nothing is truncated.
Evidence, 24 rows, complete.
#Evidence, as recorded on 16 August 2026
1Fresh clone test, today: git clone C:/Users/danaf/Projects/cpp-cpm-engine clonetest && git checkout v2.9.40, then hashed the two pinned files. cpm-engine.js = 72c1081de1e6a1b4953f4bc11fc291df5978251c666b1f5c4f891fa0999f7ec7 (454460 bytes), python_reference/cpm.py = 27829ddab0a6440cbb3ea890bc21a71cdfa35168e6bb9473531e24556aee138f (84712 bytes). Both identical to the published pins in C:/Users/danaf/Projects/cpp-cpm-engine/release-evidence/v2.9.40/cpm-engine.js.sha256 and .../python_reference-cpm.py.sha256, and to the table in .../VERIFY_RELEASE.md.
2Same clean clone: npm run verify completed with "Verdict: PASS", checks_passed 925 / checks_total 925, fixtures_failed 0, and wrote a witness whose engine.sha256 and python_reference.sha256 equal the published values.
3C:/Users/danaf/Projects/cpp-cpm-engine/CHANGELOG.md:15 carries a v2.9.40 entry and line 53 a v2.9.39 entry; lines 32-33 state plainly that the prior packet published the previous release's hashes.
4Ran schedule-risk-analysis twice on the same bundled sample schedule with default settings. All five outputs differed by SHA-256. Narrative diff: "Worst Case (P100): 2026-03-09" versus "2026-03-08"; sensitivity ranks 3 and 4 swapped (Final MEP Connections / Paint Topcoat), correlations 0.3566 versus 0.3527.
5Searched the Monte Carlo summary JSON from both runs for the string 'seed': not present in either. C:/Users/danaf/.claude/skills/schedule-risk-analysis/scripts/risk_builder.py:1088-1091 still emits "Monte Carlo seed is recorded in the simulation summary JSON for byte-stable reproduction; default-seed runs are deterministic within Python+NumPy version."
6C:/Users/danaf/.claude/skills/schedule-risk-analysis/scripts/risk_builder.py:564-566 docstring: "When both are None, RNG is seeded from system entropy (output is non-reproducible)." Lines 1059-1063 filter the recorded outputs to .txt/.csv/.json, so the HTML dashboard and DOCX report are unhashed; the emitted record listed only three of five files.
7Risk dashboard HTML from that run carries section A-09 headed "Reproducibility Manifest" and a footer stamp "Self-contained &#183; Reproducible"; grep for 'sha256' in that file returned 0 occurrences.
8Live forensic-delay-analysis run today on the two bundled sample schedules. The emitted record shows real input hashes (60c0a2c2..., 29a1cc86...) and real output hashes, but "rerun_command": null, and data_date, activity_count, relationship_count, calendar_count and topology_hash all null for both inputs.
9Rendered flagship dashboard from that run: grep -oic sha256 returned 0 and grep -oEc '[0-9a-f]{64}' returned 0. The file carries "<span class=\"seal\">Reproducible · AACE 29R-03 / SCL Protocol 2nd ed.</span>" (render_forensic_dashboard.py:2845) and section A-10 subtitled "reproducibility manifest" (render_forensic_dashboard.py:2509).
10Cross-day test: patched the clock to 30-Sep and re-ran the same analysis. The TXT and all three CSVs stayed byte-identical to the 16-Aug run; the HTML and the DOCX both changed, and every output filename changed because the date is embedded in it.
11Same-day repeat run: TXT, three CSVs and the HTML matched; the DOCX differed on every run.
12Manifest coverage counted today: 18 skill directories call write_skill_manifest or its measured-mile equivalent. Ten deliverable-producing skills call neither: 3-week-lookahead, NOA, bess-monthly-report, delay-analysis, path-explorer, portfolio-dashboard, resource-histogram, schedule-update-package, [client-named monthly report skill, name withheld], workforce-plan.
13Skills that stamp the word Reproducible with no provenance record: C:/Users/danaf/.claude/skills/resource-histogram/scripts/histogram_builder.py:244, 283, 338 ('Reproducible . Forensic-grade') and render_histogram_dashboard.py:644 ('CPP-derived \u00b7 Reproducible'); C:/Users/danaf/.claude/skills/path-explorer/scripts/render_path_dashboard.py:782 (section A-05 desc='reproducibility manifest', contents at lines 772-781 are project, data date, project finish, trace direction, timestamp).
14rerun_command is passed by only 5 of the 17 wired skills: claim_provability.py:704, build_report.py:1696, cp_validator.py:2663, build_3way.py:381, generate_report.py:475. C:/Users/danaf/.claude/skills/_cpp_common/tests/test_skill_manifest_conventions.py:253 asserts m['reproduction']['rerun_command'] is None.
15C:/Users/danaf/.claude/skills/_cpp_common/scripts/skill_manifest.py:441-448 emits the rerun note unconditionally regardless of whether a command was supplied.
16Ran the library path that C:/Users/danaf/.claude/skills/critical-path-validator/SKILL.md:84-94 documents (validate_critical_path then generate_dashboard). It produced a 104757-byte HTML deliverable and no provenance file. The record is only built inside _cli() at cp_validator.py:2647.
17C:/Users/danaf/.claude/skills/counter-claim-analysis/scripts/counter_claim_builder.py:309-311 passes both inputs with path:''; skill_manifest.py:143 returns ('unavailable', 0) unless the path passes os.path.isfile. claims-preparation/scripts/claims_builder.py:279-282 passes the upstream result the same way.
18C:/Users/danaf/.claude/skills/_cpp_common/scripts/skill_manifest.py:458 signoff_sha256 is None and never filled; lines 187 and 385 record basenames only; line 290 hardcodes the excluded task types as string literals.
19Provenance failure is silent and non-blocking in conc_builder.py:529-534, accel_builder.py:664-669, risk_builder.py:1096-1104, collapsed_builder.py:853-856, tia_builder.py:1185-1188, iap_builder.py:559-564, claims_builder.py:316-322, counter_claim_builder.py:357-363: an {"error": ...} string is written in place of the record and the deliverable ships.
20C:/Users/danaf/.claude/skills/measured-mile-disruption/scripts/mm_manifest.py:94-152 declares the same cpp-skill-manifest/v1 schema but emits no engine block and no reproduction block, so those deliverables carry no engine version.
21C:/Users/danaf/.claude/skills/_cpp_common/scripts/verify_report.py:378-392, 486 requires a topology hash before it can check anything; that field came out null on the live run today. A search for readers of the sidecar found only test files.
22C:/Users/danaf/.claude/skills/_cpp_common/PROCEDURE.md:167-330 is a real, specific reissue and supersession procedure, with change classes tied to named engine releases.
23C:/Users/danaf/.claude/skills/_cpp_common/scripts/docx_shell.py:890-907 now raises if the Word footer does not carry the real engine version from the single source of truth, and engine_version.py:28 sets that to 2.9.40 with the import fallback fixed at the literal 'unknown'.
24Skill code is version controlled but the remotes are named cpp-internal-* (checked on forensic-delay-analysis, schedule-risk-analysis, _cpp_common, rfp-contract-baseline-comparison). The deliverable folder from the live run contained seven report files and no code.
What worksIncluding the parts that were wrong before and are right now

The engine half of this is now genuinely fixed, and the finding graded on 2026-08-12 is out of date on that point. That grade flagged a published verification packet that named the wrong file fingerprints, so a reader following the instructions got a mismatch. That is corrected. Today I made a clean copy of the engine repository, checked out the released version, and computed the fingerprints myself. Both matched the published values to the character, and the file sizes matched too. I then ran the project's own verification command on that clean copy. It ran 1134 tests and printed "Verdict: PASS". A buyer, or an opposing expert, can do the same thing and get the same answer.

The supporting record around that release is also honest now. The change log has entries for the last two releases. The accuracy figure is published as "925 of 989 defined comparisons executed, 64 skipped" rather than the older "925 of 925", which had read as perfect coverage when it was not. A written procedure now exists for withdrawing and reissuing a report after the engine changes, and it names the specific behaviours that moved.

On the report side, some real things work. When a schedule file is handed in as a file, its fingerprint is recorded truthfully. I confirmed this on a live run. Output fingerprints are taken after the files are written, so they describe the real documents. The plain-text and spreadsheet-style outputs of the main delay method are stable: I ran it twice today and then simulated a run six weeks later, and those four files came out byte for byte identical every time. One product does the whole job properly. The three-schedule comparison prints a fingerprint table on the face of the report, records every input path and the exact command to run again, and refuses to finish if the provenance file cannot be written.

What failsThe full list, not a selection

The reports still do not carry what a third party would need to reproduce them, and in places they say the opposite of the truth.

The worst case is the risk simulation product. I ran it twice today, same schedule file, same settings, minutes apart. All five outputs were different. The worst-case finish moved from 09-Mar to 08-Mar. The ranked list of the biggest risk drivers reordered, with two activities swapping places. No random seed appears anywhere in any output; I searched the summary file and there is none. The report page still carries a section headed "Reproducibility Manifest" and a stamp reading "Self-contained, Reproducible", and the provenance file still states that the seed is recorded for byte-stable reproduction and that default runs are deterministic. Both of those statements are false as of today.

The flagship delay report claims reproducibility on its face with nothing behind it. I searched the rendered file: zero fingerprints of any kind, and zero 64-character hash strings. It nonetheless carries a footer stamp reading "Reproducible" and a section subtitled "reproducibility manifest".

Ten products that generate client deliverables write no provenance record at all. Eighteen do. Several of the ten stamp the word "Reproducible" on the output anyway.

Reproduction detail is thin where it does exist. Twelve of the seventeen wired products record no command to run again; the live delay run I did today wrote "rerun_command: null" directly beside a sentence telling the reader to re-run the stated command. A unit test locks that null in place. The code the reports name lives in repositories titled "internal", and no code ships in the deliverable folder, so a recipient could not run the command even if it were named. Nothing that ships reads these provenance files back; the one checking tool in the suite needs a schedule fingerprint that comes out empty on every real run.

Other gaps I re-confirmed today. On a live run, every schedule-identifying field, including data date and activity count, came out empty. Inputs an analyst types in rather than uploads are recorded as "unavailable", which is the same marker used for a file that is missing or unreadable, so the reader cannot tell the two apart. The record is unsigned and does not fingerprint itself. Nothing ties the report to the version of the code that made it. If the provenance step fails, most products write an error string in its place and ship the report anyway. Only two stop. On a cross-day re-run the two documents a human actually reads, the web page and the Word file, both change, and the filenames change too, so nothing matches. One product declares the same record format while leaving out the engine block entirely, so its deliverables carry no engine version at all.

Limits of this gradeWhat this row does not establish

This row is about whether a report can be reproduced and checked. It says nothing about whether the underlying delay maths is correct, whether the methods are the right ones, or whether any conclusion in a report is sound. I ran two products end to end today, the main delay method and the risk simulation, plus the documented path of a third. For the remaining products I read the code rather than running them, so their behaviour is established by inspection, not by execution. The schedules I used are the small demo files bundled with the tool, not real project files; file size does not change the wiring gaps I found, but I did not test on a large real schedule. The engine verification I ran was a clone of the local copy of the repository on this machine, not a download from the public host, so it proves the tagged bytes and the published fingerprints agree, not that the public copy is identical to the local one. I did not attempt to verify the cryptographic signature record against the external transparency log. The engine version fingerprints are taken over the committed form of the files; a Windows copy made with automatic line-ending conversion turned on and the repository's own settings ignored would compute a different value, though a normal clean copy on this Windows machine reproduced the published values exactly. Nothing was edited, committed or published; all writes went to a temporary scratch folder.

Criterion 8

Method labelling honesty

The standard, stated for any product

Products borrow method numbering and thresholds from published industry standards. The citation has to be the right one for what the tool actually does, an in-house score must not be printed as though a standard defined it, and no figure may be presented as an industry benchmark without a source.

What to ask the vendor

  1. Is the cited method number the correct one for what this tool actually performs?
  2. Which parts of this score are your own invention rather than the standard's?
  3. Where does each industry benchmark figure come from?
  4. Does a blanket compliance claim cover tools that comply with nothing?

What a pass looks like

Correct method citations, in-house constructs labelled as in-house, every benchmark figure sourced, and compliance claimed only where it was earned.

What a fail looks like

A letter grade or percentage printed under a standard's name that the standard does not define, and unsourced statistics described as industry benchmarks.

Critical Path Partners, graded against it
Partially meets Unchanged since the 12 August grade

The product names the outside standards it borrows accurately, but it never tells you which parts of the score it invented itself, and every labelling defect found on 12 August is still in the files today.

Evidence22 items, each checked on 16 August 2026
All 22 evidence items are listed. The table scrolls and the header stays put; nothing is truncated.
Evidence, 22 rows, complete.
#Evidence, as recorded on 16 August 2026
1PASS: C:/Users/danaf/.claude/skills/schedule-health-review/scripts/generate_report.py:465-468 - skill_method_aace='DCMA 14-Point Assessment (schedule quality screen; not an AACE 29R-03 forensic MIP)', skill_method_id='dcma14_health_screen'
2PASS: C:/Users/danaf/.claude/skills/schedule-health-review - 'python -m pytest tests/test_manifest_sidecar_2026_08_11.py -q' -> '9 passed in 1.26s'; the disclaimer string is asserted at tests/test_manifest_sidecar_2026_08_11.py:57
3PASS: C:/Users/danaf/.claude/skills/schedule-health-review/template/Schedule_Health_Dashboard_v7_Obsidian.html:7789-7791 - 'This is a screening assessment, and its validation boundary is narrower than the engine's ... they are not a delay analysis or an entitlement finding'
4PASS: same template:6777 - criteria list ends 'C12 Critical Path Test', 'C13 CPLI', 'C14 BEI'
5PASS: C:/Users/danaf/.claude/skills/constructive-acceleration/SKILL.md:15 - 'It is not one of the AACE Method Implementation Protocols.'; C:/Users/danaf/.claude/skills/concurrency-apportionment/SKILL.md:3 - 'not a MIP itself'
6PASS (improved since 12 Aug): C:/Users/danaf/Projects/cpp-cpm-engine/CHANGELOG.md:55 - 'That 13 / 13 is fitted to the one capture: the engine was changed to match the cases it failed, and no held-out capture exists, so it is not an independent accuracy figure.'; same disclosure at DAUBERT.md:474 and docs/cross-exam-prep.md:47
7PASS (improved since 12 Aug): C:/Users/danaf/Projects/cpp-cpm-engine/DAUBERT.md:35 and :144 - '925 of 989 enumerated comparisons executed and bit-identical; 64 skipped rather than failed'
8CONTEXT: C:/Users/danaf/Projects/cpp-forensic-mcp - 'timeout 240 python -m pytest tests/ -q' -> '310 passed in 19.12s'
9FAIL: C:/Users/danaf/.claude/skills/_cpp_common/tests/test_no_fabricated_citations.py - 'python -m pytest tests/test_no_fabricated_citations.py -q' -> '1 passed in 8.05s'. Script loading FORBIDDEN and applying all patterns to the six live offending strings printed MISSED for forensic SKILL.md:277, collapsed SKILL.md:325, mcp README:77, methodology.py:373, template:16337 and the registered skill description; 'pattern count: 37'; SKIP_DIRS at :88 still contains '_deploy'
10FAIL: session skill listing 2026-08-16 - anthropic-skills:forensic-delay-analysis reads 'windows analysis (AACE MIP 3.7)' and 'AACE MIP 3.7-forward vs this skill's MIP 3.7-retrospective'
11FAIL: C:/Users/danaf/Downloads/Dana - Skills for Work/forensic-delay-analysis.zip (dated 26 Apr, unchanged) -> SKILL.md:3 '(AACE MIP 3.7)' and :290 'MIP 3.7-style observational/static/periodic'; local canonical C:/Users/danaf/.claude/skills/forensic-delay-analysis/SKILL.md:411 correctly reads 'MIP 3.3 - Observational / Dynamic / Contemporaneous As-Is'
12FAIL: C:/Users/danaf/.claude/skills/forensic-delay-analysis/SKILL.md:277 - 'near_critical_paths(...) - MCPM (AACE 29R-03 section 3.6)'
13FAIL: C:/Users/danaf/.claude/skills/collapsed-as-built/SKILL.md:325 - 'forensic-delay-analysis windows method, AACE 29R-03 section 3.6', four lines after MIP 3.3 at :321
14FAIL: C:/Users/danaf/.claude/skills/schedule-health-review/template/Schedule_Health_Dashboard_v7_Obsidian.html:16337 - 'Based on industry benchmarks, correcting these ${totalFailed} criteria typically requires ${totalFailed * 8} working days'; the 75/45/25 probabilities at :16338-16339; '14-point DCMA quality standards' at :16329 against 'Math.round(((13 - totalFailed) / 13) * 100)' at :15919. Same text at :16337 of cpp-forensic-mcp/_deploy/schedule-health-review/template/
15FAIL (reachability, stated precisely): same template - exportFailedCriteria appears only at :15882, its own definition; generateExecutiveFailureReport is called only at :15892 and defined at :15914. No caller exists, so the block is dead code that nonetheless ships inside every report file
16FAIL: same template:5901-5908 SCORE_RULES {statusGreen:85, statusYellow:65, gradeA:90, gradeB:75, gradeC:60, gradeD:45}; :7837 footer 'THRESHOLDS PER DCMA 14-POINT ASSESSMENT - QUALITY COMPLIANCE ANALYSIS' printed alongside 'GRADE ... % ... band'; :8100 'Standard / DCMA 14-Point Assessment'; SKILL.md:130 lists standard and grade in one provenance block. Greps of the template for 'fitted', 'CPP-defined', 'not defined by DCMA' and 'letter grade' return no provenance disclosure
17FAIL (new since 12 Aug): same template:7791 - 'the CPP CPM engine agrees with Primavera P6 23.12 on 13 of 13 independently scheduled test projects'; grep -i 'fitted' over that file returns no match. Identical text at :7791 of cpp-forensic-mcp/_deploy/schedule-health-review/template/
18FAIL: C:/Users/danaf/.claude/skills/schedule-risk-analysis/scripts/risk_drivers.py:15 'Run the forward pass; record project completion date' against :135 'completion_days[i] = float(durations.sum())'; :166 "'reference': 'AACE RP 57R-09 4.4 / Hulett 2011'"; signature at :67-68 accepts 'relationships' and the file never uses it after :75. Executed today with two independent 10-day activities: 'p50_days = 20.0', reference stamped, and the returned keys contain no caveat or limitation field
19FAIL: C:/Users/danaf/.claude/skills/_cpp_common/PROCEDURE.md:418 - 'Schedule-risk-analysis does not yet implement Hulett risk-driver Monte Carlo (AACE 57R-09 4.4) - known gap', which no longer matches the code above
20FAIL: C:/Users/danaf/Projects/cpp-forensic-mcp/cpp-forensic-mcp/server.py:53 and :78 and README.md:3 - "Critical Path Partners' AACE-compliant forensic-scheduling tools"; README.md:22 'DCMA-14, GAO/AACE compliance'; against C:/Users/danaf/.claude/skills/forensic-delay-analysis/SKILL.md:435 'Do not claim AACE 29R-03 compliance without filling in the methodology section's [FILL IN] marker'
21FAIL: C:/Users/danaf/Projects/cpp-forensic-mcp/cpp-forensic-mcp/README.md:77 - 'AACE 29R-03 windows analysis | yes (MIP 3.4-3.7)'
22FAIL: C:/Users/danaf/.claude/skills/forensic-delay-analysis/scripts/methodology.py:373 - 'Summarize how windows were chosen for MIP 3.7', in a function used only for MIP 3.3
What worksIncluding the parts that were wrong before and are right now

Where the product leans on an outside standard, it says so correctly and it has a test that stops the wording being quietly dropped. The schedule health report is labelled a "schedule quality screen; not an AACE 29R-03 forensic MIP" in the machine record that travels with every report, and a test pins that exact sentence. Page one of the report tells the reader the check is narrower than it looks: it is a screening assessment, it has not been compared against the contract or the project records, and it is not a delay finding. The fourteen checks really are the fourteen checks, and the last two are the two named ratio measures rather than invented substitutes. Skills that are not one of the numbered methods in the standard say so in plain words.

Two claims have genuinely improved since 12 August, and both improvements are about honesty. First, the engine's agreement with the commercial scheduling package is now described everywhere in the engine repository as fitted, not independent. The single test capture first scored 6 of 13. The engine was then changed to match the seven cases it failed. No second, blind capture has ever been run. The documents now say that plainly, including a prepared concession for cross-examination. Second, the engine-versus-reference comparison is now published as 925 of 989 defined checks actually run, with the 64 skipped checks named field by field, instead of the earlier flattering "925 of 925".

What failsThe full list, not a selection

Nothing on the 12 August failure list has been fixed. Every item below was re-checked today.

  1. The guard meant to catch wrong citations is a list of 37 exact phrases, and it catches none of the errors actually present. I ran all 37 patterns against the six live offending lines by hand. All six were missed. The guard still reports a clean pass, and it still skips the copy that ships to the server.
  2. One of the two registered copies of the forensic delay skill still carries the wrong method number. It calls windows analysis method 3.7, and describes it using the wording of a third method again. Windows analysis is method 3.3. The local copy is correct, so two versions with the same name disagree. The packaged copy on disk is unchanged since 26 April.
  3. Two more wrong method numbers sit in current documents. Near-critical-path detection is labelled section 3.6, and the windows method is called section 3.6 four lines after being correctly called method 3.3. Section 3.6 is a different method entirely.
  4. A made-up statistic still ships inside every generated health report file. The text says that, based on industry benchmarks, fixing each failed check takes eight working days, and that projects at a given compliance level have a 75, 45 or 25 percent chance of running more than 30 days late. There is no source. The numbers are typed into the file. The same block calls it a 14-point standard while dividing by 13. Stated fairly, this block is currently unreachable dead code, but it is inside every report file, visible in view-source, and one line of wiring away from printing.
  5. The A to F grade, the compliance percentage and the red, amber or green band are the vendor's own inventions, printed under the outside standard's name with no disclosure anywhere. The cut-offs are typed into the file. The footer prints "THRESHOLDS PER DCMA 14-POINT ASSESSMENT" in the same bar as the grade, the percentage and the band, and the provenance block lists the standard and the grade together. I searched the report and the skill documentation for any sentence saying the grade is the vendor's own construct. There is none. That standard defines checks and thresholds. It defines no letter grade, no percentage and no band.
  6. The report's own honesty paragraph now overstates one thing. It tells the reader the engine "agrees with Primavera P6 23.12 on 13 of 13 independently scheduled test projects". The word fitted does not appear anywhere in that file. The correction applied thoroughly across the engine documents did not reach the one document a client actually receives.
  7. A risk function stamps an industry citation on a number that is not a schedule calculation, and its own description says the opposite of what the code does. The description says it runs a forward pass and records the completion date. The code adds up every activity duration. I ran it on two activities of ten days each that run side by side: it returned a P50 of 20 days where the correct answer is 10, tagged with the citation, with no warning field in the result. It also accepts a list of logic links and never uses them. Inline comments and the skill documentation are explicit about the shortcut, and the shipped report path uses a different, logic-aware routine, so the harm is bounded. The internal procedure document still calls this method an unimplemented gap, which no longer matches the code.
  8. The server tells every connected client at start-up that these are "AACE-compliant forensic-scheduling tools", and the readme repeats it. The vendor's own written instruction says not to claim compliance with that standard without completing a methodology section. The blanket wording also covers the quality screen that the vendor's own record labels "not an AACE 29R-03 forensic MIP".
  9. The readme's comparison table sells windows analysis as "MIP 3.4-3.7". Windows analysis is 3.3, which that range excludes. A code comment in the same skill still says windows were chosen for method 3.7, in a function used only for 3.3.
Limits of this gradeWhat this row does not establish

This row is about labelling, not about whether the underlying calculations are right. It does not establish that the schedule maths is wrong, and it does not establish that it is right. I read source files and ran the tests named above. I did not render a health report in a browser, so the three findings inside that report template are proven from the file and from tracing which code can call which, not from a printed report. I could not confirm from disk which packaged copy the hosted skill catalogue serves today. The wrong-method wording in this session's own skill listing matches the packaged copy on disk exactly, which is why I treat it as live, but the definitive check is to open that skill in the browser. The other packaged copies in that folder are equally old and I compared only two of them against the current sources, so there may be further drift I have not counted. My reading of which number belongs to which method comes from general knowledge cross-checked against the vendor's own statements of the same numbering, not from the standards documents themselves, so the adjudication rests on the vendor's own files agreeing with me, which they do. One line I could not test either way: an internal procedure document lists the author as an active practitioner of a method I found no implementation of anywhere in the suite. That is a statement about a person rather than a tool label, and only he can speak to it.

Criterion 9

Completeness of what the report shows you

The standard, stated for any product

A register that quietly stops at the first handful of rows changes the finding, because the activity that decides the argument is as likely to be number sixty as number three. Any table, chart or register in a deliverable must either show every row it computed, or say on the face of it how many it is showing out of how many exist. Silence is the failure, not the cap.

What to ask the vendor

  1. Feed it more rows than any cap you would plausibly set: does the rendered count equal the stated total?
  2. Where a cap does exist, does the output name its own denominator on the page?
  3. Does the count hold on every branch of the report, including the well-evidenced path, not just the sparse one?
  4. Does anything automated scan your own code for row caps, and which file types does that scan not reach?

What a pass looks like

Rendered rows equal computed rows in every branch, every deliberate cap prints its own denominator, and an automated scan covers every tool that produces a client document with no unexplained exemptions.

What a fail looks like

A table that drops computed rows with no marker, prose above it describing rows that are not there, and a data file that disagrees with the page.

Critical Path Partners, graded against it
Partially meets Unchanged since the 12 August grade

The completeness guard is materially stronger than at the last review, and the worst defect it missed is fixed. One client-facing table still drops every computed row in one of its two branches, with no notice to the reader. Two producers driven end to end today counted correctly everywhere else. Still passes in part.

Evidence9 items, each checked on 16 August 2026
All 9 evidence items are listed. The table scrolls and the header stays put; nothing is truncated.
Evidence, 9 rows, complete.
#Evidence, as recorded on 16 August 2026
1claim_provability.py:375-384, _auto_weak no longer takes a limit; docstring reads "NOT capped." Driving the real function and the real renderer with 30 long-runners and no owner-event list: blow-out register 30 of 30, tier-table WEAK badges rendered = 30, long-runner codes in tier table = 30 of 30. The eight-row cap found at the last review is gone.
2claim_provability.py:578-587, the weak-item loop sits inside if not rows:. Same harness, one owner event supplied: "tier-table WEAK badges rendered : 0", "tier-table long-runner codes : 0 of 30", "disclosure markers in tier table: []". The returned data still carries 30. claim_provability.py:656 prints "long-runners the file cannot explain stay weak" two lines above that table.
3Schedule health report generated end to end today on a real 286-activity baseline/current pair. Five registers, each checked three ways: rendered rows = embedded CSV rows = stated label. 13/13, 6/6, 10/10, 134/134, 23/23. Labels read "13 activities, all listed" ... "134 activities, all listed". The criteria grid independently states "FAIL · 134" for that criterion.
4Same report generator on a 10,502-activity fixture: "All 13 scored DCMA criteria pass. No failed-activity registers are generated." Total table rows in the document: 6. Nothing silently emptied.
5_cpp_common/tests/test_no_data_truncation.py: 98 files scanned (was 76), 10 allowlist entries against a ceiling of 12, 4 written exclusions. python -m pytest _cpp_common/tests/test_no_data_truncation.py -q returns "17 passed in 1.61s" (was 9 tests).
6test_no_data_truncation.py:253-261, the scan pattern now matches a cap spelled with a variable. Fed to the shipped code today: for a in activities[:MAX_ROWS]: returns SCAN-HIT | row-cap. That shape was invisible at the last review.
7test_no_data_truncation.py:721-756 and 914-938 add a discovery walk plus a guard that fails when a producer is in neither ledger. Run today: 53 producers discovered, 0 unclassified. Excuses must carry a reason of at least 40 characters (test:940-947).
8test_no_data_truncation.py:409 still lists 'get' as a text producer and :454 still sets the date-width escape to {4,7,10}. Fed to the shipped classifier today: top = payload.get('blowouts')[:8] returns 'display', and for d in delay_drivers[:10]: returns 'display'. Rerunning all 98 files with both holes closed produced 0 new findings, so nothing is hiding behind them today.
9test_no_data_truncation.py:494-497 justifies the chart cap as "already capped upstream by cpm.py's top_n filter"; grep -rn top_n --include=cpm.py . returns nothing. Separately: 10 spreadsheet/CSV producers are in neither ledger and are not discovered, including a defense workbook, an acceleration workbook and a portfolio builder. 34 .js/.html files in the tree, 0 scanned.
What worksIncluding the parts that were wrong before and are right now

The tool-neutral test is simple. Feed a product more rows than any plausible cap, then count what it renders against the total it states, in every branch, and check whether each cap names its own denominator.

Producer one, the schedule health report, was run end to end today on a real 286-activity project pair. It produced five failed-activity registers. In all five, rendered rows equalled the rows in the embedded CSV and equalled the stated label: 13, 6, 10, 134 and 23. Each carried a label of the form "134 activities, all listed". The 134 matches the count the criteria grid states for that criterion. No cap language appears anywhere in the document. Run again on a 10,502-activity file, every criterion passed and the report says plainly that no registers were generated.

Producer two, the claim provability assessment, was driven through its real code with 30 long-runners. On its headline path, with no owner event list supplied, it rendered 30 of 30. The eight-row cap flagged at the last review is gone from the source, and the function now carries a written note explaining why long registers scroll instead of being cut.

The guard behind this also grew. It scans 98 files, up from 76. It runs 17 tests, up from 9, all passing. Caps written as a variable name rather than a number are now caught, which was the easiest way to hide one. A new discovery walk finds report producers by what they call, not by a hand-typed list, and the build fails if any producer lands in neither the scanned list nor the excused list. It found 53 producers and none escaped. Anything excused must carry a written reason, and the residual allowlist sits at 10 entries against a hard ceiling of 12.

Where caps exist by design, they generally state their own denominator: a chart says how many series it plotted out of how many exist, a workbook says "(+N)" when it clips, and the schedule viewer says "Showing first 200 of N activities".

What failsThe full list, not a selection

Same file, other branch. The claim provability causation-tier table has two paths. The one fixed since the last review is the sparse one. The better-evidenced path was not fixed. When you supply a list of owner events, which is what a well-documented claim looks like, every auto-detected long-runner is dropped from that table. Driven today with 30 long-runners and one owner event: 30 computed, 0 rendered, and no marker of any kind telling the reader something is missing. The paragraph printed directly above the table says long-runners the file cannot explain stay weak. None of them appear. The machine-readable output still carries all 30, so the page and the data file disagree.

The written justification for the one remaining chart cap is factually wrong. It says the data is already limited upstream by a filter called top_n. That name does not exist anywhere in the file it points at. The chart itself is honest about what it plotted. The audit note that waves it through is not, and that note is exactly what a reviewer would rely on.

The guard states a safety promise that it does not keep: that an unfamiliar pattern must never pass by resembling something safe. Two shapes pass. A slice of exactly 4, 7 or 10 items on an unrecognised name is waved through as a date. A collection reached through a dictionary lookup is waved through as text, even when the collection's own name is on the guard's list. Neither hides a real cap today. I reran all 98 files with both holes closed and got zero new findings.

Coverage is uneven. The new discovery walk only recognises web-page and Word producers. Ten spreadsheet and CSV producers sit in neither ledger and are not discovered either, so a row cap added to a client workbook would not fail anything. Thirty-four JavaScript and HTML files in the same tree are scanned by nothing, and the schedule viewer's code is outside the guard entirely.

Three slice shapes remain invisible to the scanner. I swept for them by hand and found nine matches, all benign on reading.

Limits of this gradeWhat this row does not establish

The 30-row long-runner population is synthetic, because the sample files on this machine produce zero blow-outs and the table cannot be reached from shipped fixtures. The rows use the exact shape the product's own code emits. The dropped rows and the absent notice are properties of the code, not of my input.

I swept the unscanned spreadsheet and CSV producers with the product's own pattern and classifier and found one match, which discloses its own overflow. So the coverage gaps above are missing protection, not proof of a second live defect.

I did not test the deployment mirror, which the guard excludes by rule, and I did not run the schedule viewer in a browser.

One thing I was told was fixed today did not hold: the server test suite ended at 309 passed and 1 failed, on a client-name check unrelated to this criterion.

Criterion 10

What the product does with your schedule and who can read it

The standard, stated for any product

A schedule file carries the project, the parties, the sequence and often the names of the people who built it. A product must state accurately whether that file leaves your machine, must not leak paths, usernames or client folder names back through errors, and must not describe as anonymous an output that still carries identifying fields. The statement in the interface has to match what the code does.

What to ask the vendor

  1. Does my schedule file leave my machine, and does every screen that says otherwise say it accurately?
  2. Raise a deliberate error: does the message hand back a full path, a username, or a folder named after my client?
  3. Exactly which fields does your anonymiser strip, and which identifying fields does it leave in?
  4. How long is uploaded data kept, what triggers the deletion, and what happens if that trigger never fires?

What a pass looks like

One accurate statement of what is sent, applied everywhere the question is answered; error text scrubbed on every path that reaches a wire; a published field list for any anonymiser; and a retention sweep that runs on a clock rather than on an event.

What a fail looks like

A no-upload promise printed beside an upload button, absolute paths returned in error messages and stored for the life of the record, and an anonymiser whose own header claims more than its field list delivers.

Critical Path Partners, graded against it
Does not meet Downgraded since the 12 August grade

Today's path-scrubber fix is real, and I confirmed it against nine raised exceptions. But it is not wired into the one path that actually sends a schedule off the machine, which I watched return a full absolute path to the wire. And the shipped viewer still states four times that schedule files are never uploaded while offering a button that uploads them.

Evidence9 items, each checked on 16 August 2026
All 9 evidence items are listed. The table scrolls and the header stays put; nothing is truncated.
Evidence, 9 rows, complete.
#Evidence, as recorded on 16 August 2026
1Nine real exceptions through the tool-surface scrubber, all clean. Example: open() on C:\Users\<user>\Downloads\<Two Word Folder>\update 3.xer returned "not_found: [Errno 2] No such file or directory: '[path]'". Prose kept: scrub_paths("window 2026/08/12 slipped 3/4 of a day, N/A and/or 24/7") returned that string unchanged, and "see http://example.com/docs for detail" was unchanged.
2Upload path leak, run live today. Forcing a real FileNotFoundError inside the dispatcher returned to the wire: "FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\danaf\\Downloads\\Acme Tunnel Job\\update 3.xer'". Source: cpp-forensic-mcp/cpp-forensic-mcp/lens/dispatcher.py:380, and the identical pattern at :471, :539, :598, :668, :733, plus lens/facade.py:269.
3That error text is stored and re-served. lens/jobs.py:35 writes it into the job row; lens/facade.py:307 returns job['errors'] on every status poll. No safe_error or scrub_paths import exists anywhere in cpp-forensic-mcp/cpp-forensic-mcp/lens/*.py (grep returned nothing).
4The test that polices raw interpolation does not cover the upload path. tests/test_no_exception_leakage.py:1314 test_no_raw_path_or_exception_interpolation_in_wire_strings lists eight claim-workbench parser files and no lens file.
5Lower-case trailing folders still leak. Real FileNotFoundError, scrubbed: 'C:\\Users\\danaf\\Downloads\\john smith' returned "not_found: ... '[path] smith'"; '/tmp/uploads/acme construction ltd' returned "... '[path] construction ltd'". Pinned as known residual at tests/test_no_exception_leakage.py:1002-1017.
6Anonymiser field gap. cpp-lens-viewer/src/mcp/anonymizer.js:46 lists TASK: ['task_name', 'task_memo'] only, with no create_user or update_user entry, and PROJECT has no add_by_name. The deployed bundle dist/lens-viewer.html contains zero occurrences of "create_user". The header at anonymizer.js:9-13 claims the output contains none of the original identity strings.
7Disclosure. grep on cpp-lens-viewer/dist/lens-viewer.html returns four hits, including "XER and XML schedules are parsed entirely in your browser and never uploaded" and "XER and XML files are never uploaded". grep -c -i privacy on the same file returns 0.
8Retention. lens/db.py:195 and lens/storage.py:57 both default ttl_days=365; serve_http.py:3819-3830 runs both sweeps once at app-build time only. lens/rate_limit.py:35-45 hash_ip is a plain unsalted SHA-256 of the address; grep for lens_rate_limit across lens/*.py shows INSERT, UPDATE and SELECT only, and no DELETE anywhere.
9Suite state. timeout 300 python -m pytest tests/ -q in cpp-forensic-mcp gave "1 failed, 309 passed in 22.00s". The failure is tests/test_bundle_no_client_names.py:107, "CLIENT-NAME LEAK 1 hit(s). Scrub before deploy", pointing at _cpp_common/scripts/engine_version.py:35, which git status shows as modified and uncommitted.
What worksIncluding the parts that were wrong before and are right now

The error scrubber on the tool surface is genuinely fixed, not fixed on paper. I raised nine real exceptions today (missing file, permission, key lookup, bad JSON, archive open, file copy) using Windows and POSIX paths with spaces, quotes and a surname folder. None of the nine returned a username, a surname or a client folder name. A quoted spaced path now redacts whole, which was the shape a real Python error most often produces. Ordinary forensic prose survives untouched: a date written 2026/08/12, a 3/4 fraction, and/or, 24/7, and a web address all came back unchanged.

The design is honest about what it did not solve. The regex only finds candidates; a second pass decides how far each one really runs. The cases still not covered are written into a table in the test file with a note saying that the test passing does not mean the input is safe, and the pattern is pinned byte for byte across all twelve copies in the tree by a drift test.

The input side has real guards. Caller-supplied paths are checked for null bytes and traversal, must resolve under the server temp directory, and system locations are denied outright even if reached through a link.

The browser side does what it says for the ordinary case. Loading a P6 XER or XML into the viewer stays on the machine, and the browser-side anonymiser does tokenise activity, WBS, resource, calendar and code names before any upload, with the token-to-name map never leaving the browser.

The retention purge script for the free intake form is conservative and unusually candid. It refuses to delete anything outside its own folder, never follows links, ages folders by the timestamp in the name rather than by file date, logs every deletion, and states in its own header that the automatic trigger only fires when a new submission arrives, so a scheduled job is what actually keeps the promise.

What failsThe full list, not a selection

The upload path does not use the scrubber. I ran it and read what came back: the browser receives the server's full absolute path verbatim, plus the raw Python error class name. Six sites in the dispatcher and one in the request handler build their error text by direct interpolation. The source-inspection test that polices exactly this habit covers only eight document-parser scripts and never looks at the upload path. That error string is then written into the job record and handed back on every status poll for the life of the record.

The remaining scrubber gap is not academic. A trailing folder whose name is all lower case is still emitted. I checked two shapes that a real matter produces: a lower-case personal name folder and a lower-case company folder. Both left the name on the wire.

Anonymised does not mean anonymous. The strip list covers names, descriptions and labels. It has no entry for the P6 user name recorded on every activity row, so the scheduler's login travels verbatim on every activity. Every activity ID, every internal identifier and every date and logic link also travel verbatim. The module's own header states that the output contains none of the original identity strings. That statement is not true.

Retention is a year by default, and the sweep runs only when the container starts. Nothing expires while the service is up. The rate-limit table is keyed on an unsalted digest of the caller's address and is never swept at all.

Disclosure is the decisive failure. The shipped viewer says "XER and XML files are never uploaded" and three near-identical variants, and the word privacy does not appear anywhere in the deployed bundle, so nothing links the policy that does describe the exception.

Finally, the suite does not exit at 310 passed. It is 309 passed and 1 failed, and the failure is the project's own client-name guard, tripped by an employer name left in a comment by today's uncommitted edit.

Limits of this gradeWhat this row does not establish

Five limits. I did not run a real upload end to end, because doing so would send a schedule to the server; the retention and stored-error findings come from reading the code path and the boot wiring, not from a running container's disk. Environment settings on the host could override the one-year default and the storage location, so 365 days is the code default rather than a confirmed live value. I could not confirm whether a scheduled purge job exists on the web host, so the intake retention promise rests on a cron I cannot see. My view of anonymiser coverage is based on the P6 exports available on this machine; other P6 versions emit further tables that the strip list does not name and that would travel verbatim. The scrubber verdict rests on nine exception shapes I chose, not on an exhaustive enumeration.

How the grading was done

Method, and what this page is not.

Method

Every line was checked on 16 August 2026 by reading the source files and, where the finding needed it, by running the code and recording what it printed. Where a figure is quoted it came from a command that was actually executed, not from documentation about the command. Checks were read-only. No file was edited, nothing was committed, and nothing was deployed as part of the grading.

One redaction is applied and it is the only one. An internal skill directory is named after a client engagement, and client names are not published here, so that one token reads as a withheld name wherever it appears. No finding, figure, file path, line number or command is changed anywhere on this page.

All work was done on one machine that happens to hold every repository involved. Several checks weaken on a machine holding only part of the setup, and that condition was not tested. Each criterion carries its own limits drawer naming what that row does not establish, and those limits are part of the grade rather than a footnote to it.

What this page is not

It is not a certification, it confers nothing on anyone, and it is not issued by a standards body. It is one practitioner's checklist, published so it can be argued with. It does not grade whether the underlying delay analysis conclusions are correct, which is a separate question that no amount of version discipline answers.

If a criterion here is wrong, or too easy, or missing, say so and it gets changed. The version of this page that survives contact with a hostile reader is worth more than the one that does not.