Skip to content

fix(cpm): cap Late Finish at project duration so SS/FF/SF long-poles stay critical - #381

Closed
seonghobae wants to merge 3 commits into
developfrom
claude/cwlab-pr-audit-governance-1hdcp5
Closed

fix(cpm): cap Late Finish at project duration so SS/FF/SF long-poles stay critical#381
seonghobae wants to merge 3 commits into
developfrom
claude/cwlab-pr-audit-governance-1hdcp5

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

Problem

The CPM backward pass in analytics.js (computeCpm) seeded the successor reduce with Infinity:

}, Infinity)   // ← a node WITH successors is never capped at projectDurationDays

Standard CPM (PMBOK float definitions; Kelley/Walker) initializes every activity's Late Finish to the project finish date, then tightens it with successor constraints:

LF = min(projectDuration, min over successors of the per-link constraint)

Seeding with Infinity drops the projectDuration cap, so a node that has successors takes its LF purely from those successors. In a pure FS network the successor bound is always ≤ project duration, so the missing cap is invisible — and every existing unit test uses FS chains (or FS-terminated chains). But an SS/FF/SF successor can impose a looser bound than the project end, letting a predecessor's LF exceed the project duration and giving a genuinely-critical activity false total float (and returning an empty critical path).

Reproduction

computeCpm([
  { id: 'A', duration: 10 },
  { id: 'B', duration: 2, predecessors: 'ASS' },  // B starts when A starts
]);

A (the sole 10-day long pole) drives the schedule end. Before the fix:

A: es=0 ef=10 ls=8 lf=18 slack=8 critical=false   ← lf=18 > project(10) is impossible
criticalPath: []                                   ← empty!

A planner would see 8 days of buffer on the one activity that actually determines the finish, and no critical path at all. After the fix:

A: es=0 ef=10 ls=0 lf=10 slack=0 critical=true
B: es=0 ef=2  ls=8 lf=10 slack=8 critical=false    ← B keeps its real 8d float
criticalPath: ["A"]

SS relationships (overlapping/parallel work) are among the most common non-FS links in schedule-control, so this is not an exotic edge case.

Fix

Seed the successor reduce with projectDurationDays instead of Infinity — the canonical robust backward-pass initialization (no LF can exceed project completion without extending the project). One-line change plus an explanatory comment.

Tests

Adds a regression test to tests/unit/dep-types.test.mjs (SS long-pole → A critical with zero float, criticalPath === ['A'], B keeps 8d float). The gap that let this ship: no prior test exercised a non-FS link whose successor constraint is looser than the project duration.

Verification

node tests/unit/dep-types.test.mjs   # new regression + existing SS/FF/SF assertions
node tests/unit/cpm.test.mjs         # classic AON CPM (unchanged: proj=13, path A,B,D,E, C slack 2)
npm run test:unit                    # full pure-math suite — 13 suites pass
npm run fuzz                          # fast-check property fuzz — 14 pass

All green; existing FS and SS-chain assertions are unchanged (the fix is a no-op for FS networks and for the existing A(2)→SS B(6)→C(1) case).

🤖 Generated with Claude Code

https://claude.ai/code/session_01HdCssGnNMhKHNu3TXFstWH


Generated by Claude Code

…stay critical

The CPM backward pass seeded the successor `reduce` with `Infinity`, so a node
with successors took its Late Finish purely from successor constraints and was
never bounded by the project duration. Standard CPM (PMBOK float definitions;
Kelley/Walker) initializes every activity's LF to the project finish, then
tightens it with successors: `LF = min(projectDuration, min over successors)`.

Under pure FS networks a successor bound is always <= project duration, so the
missing cap was invisible (every prior test used FS chains). But an SS/FF/SF
successor can impose a looser bound than the project end, letting a predecessor's
LF exceed the project duration and giving a genuinely-critical activity false
total float — and an empty critical path.

Reproduction: `computeCpm([{id:'A',duration:10},{id:'B',duration:2,predecessors:'ASS'}])`
reported A (the sole 10-day long pole) as lf=18 (> project 10), slack=8,
critical=false, criticalPath=[]. A planner would wrongly see 8 days of buffer on
the one activity that drives the finish. After the fix: A lf=10, slack=0,
critical=true, criticalPath=["A"]; B keeps its real 8d float.

Fix: seed the reduce with `projectDurationDays` instead of `Infinity`. Adds a
regression test to tests/unit/dep-types.test.mjs. Verified: full pure-math suite
(13 suites) + property fuzz (14) pass; existing FS and SS-chain assertions
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdCssGnNMhKHNu3TXFstWH
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@seonghobae, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cb7e299b-1987-4524-b9da-3bc97dfe4c81

📥 Commits

Reviewing files that changed from the base of the PR and between a756b7e and 5ff195b.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (3)
  • analytics.js
  • package.json
  • tests/unit/dep-types.test.mjs

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor Author

CI status: both red checks are pre-existing/base, not this diff

This PR changes only analytics.js (a one-line CPM backward-pass fix + comment) and tests/unit/dep-types.test.mjs. Its own change is fully verified — npm run test:unit (13 suites) and npm run fuzz (14 fast-check properties) both pass, and the existing FS/SS assertions are unchanged.

The two failing required checks are not caused by this diff:

  • trivy-fs — a base-branch dependency finding: GHSA-frvp-7c67-39w9 in @hono/node-server (MEDIUM, security-severity 5.9), reported against package-lock.json and pnpm-lock.yaml. The scan's own message is "Remediate each finding at the shared base branch so open PRs inherit the fix." This diff touches no dependency. It's already being remediated at base by fix(security): bump @hono/node-server to ^2.0.12 (GHSA-frvp-7c67-39w9) #379 ("bump @hono/node-server to ^2.0.12"), so per ONE SOURCE I'm not duplicating that lockfile bump here — this PR inherits the fix once fix(security): bump @hono/node-server to ^2.0.12 (GHSA-frvp-7c67-39w9) #379 lands on develop.
  • Semgrep (multi-language SAST) — the job produced only harden-runner runner-lockdown logs with no SARIF finding (empty output = tool errored on setup/egress, not a code hit). A one-line arithmetic change in a pure-math function is not a SAST pattern; Semgrep passes elsewhere in the org on real code.

No action fixable from this branch without duplicating #379 or churning on an infra hiccup. Flagging once here rather than re-reacting to each re-run; happy to revisit if either check produces a new, non-empty finding tied to this diff.


Generated by Claude Code

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head 8b5fa34454a616b0f720681cd4f333b3360f0cb8.

  • Head SHA: 8b5fa34454a616b0f720681cd4f333b3360f0cb8

  • Workflow run: 30577484110

  • Workflow attempt: 1

Coverage evidence

Coverage evidence job did not run or did not publish coverage evidence.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file: analytics.js"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file: analytics.js"]
  R1 --> V1["required checks"]
  Evidence --> S2["Test: dep-types.test.mjs"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test: dep-types.test.mjs"]
  R2 --> V2["targeted test run"]
Loading

@opencode-agent

opencode-agent Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

  • Head SHA: 5ff195bc54de8418ce56a26f8279f4e6253509b2
  • Workflow run: 30593549226
  • Workflow attempt: 1
  • Gate result: REQUEST_CHANGES (approval step)

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head 5ff195bc54de8418ce56a26f8279f4e6253509b2.

  • Head SHA: 5ff195bc54de8418ce56a26f8279f4e6253509b2

  • Workflow run: 30593549226

  • Workflow attempt: 1

Coverage evidence

Coverage evidence job did not run or did not publish coverage evidence.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (4 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (4 files)"]
  R1 --> V1["required checks"]
  Evidence --> S2["Test: dep-types.test.mjs"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test: dep-types.test.mjs"]
  R2 --> V2["targeted test run"]
Loading

claude added 2 commits July 30, 2026 23:50
…67-39w9)

@hono/node-server <2.0.5 has a moderate path-traversal in `serveStatic` on
Windows via an encoded backslash (%5C). The org Security Scan (trivy-fs/OSV)
flags it repo-wide. Patched in 2.0.5+; bump to ^2.0.12.

Scope of the "breaking" 1.x -> 2.x major is minimal here: server.mjs imports
only `serve` (not the vulnerable `serveStatic`), and `serve({ fetch, port },
info => ...)` is unchanged across the major. hono stays ^4.12.27 (deduped;
2.x supports hono 4). No runtime dependency added — the two-dep contract holds.

Verified: npm audit → 0 vulnerabilities; test:unit (13/13 files),
test:api (smoke + rate-limit) all pass — the real server boots and serves
correctly on 2.x. (test:e2e:cloud is browser-revision-blocked locally: the
sandbox ships Chromium build 1194 while @playwright/test 1.61.1 wants 1228;
CI runs that gate with the matching browser.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdCssGnNMhKHNu3TXFstWH
The repo carries two lockfiles (CI caches npm/package-lock.json, but a
pnpm-lock.yaml also exists). The previous commit updated only package-lock.json,
so trivy-fs — which scans every lockfile in the tree, not just the CI one —
still flagged @hono/node-server 1.19.14 (GHSA-frvp-7c67-39w9) via pnpm-lock.yaml.
Bring pnpm-lock.yaml in sync so both lockfiles pin the patched 2.0.12.

Minimal diff: only the @hono/node-server entries change; hono stays 4.12.28,
lockfileVersion unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdCssGnNMhKHNu3TXFstWH

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head 5ff195bc54de8418ce56a26f8279f4e6253509b2.

  • Head SHA: 5ff195bc54de8418ce56a26f8279f4e6253509b2

  • Workflow run: 30593549226

  • Workflow attempt: 1

Coverage evidence

Coverage evidence job did not run or did not publish coverage evidence.

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Changed file (4 files)"]
  S1 --> I1["repository behavior"]
  I1 --> R1["Review risk: Changed file (4 files)"]
  R1 --> V1["required checks"]
  Evidence --> S2["Test: dep-types.test.mjs"]
  S2 --> I2["regression suite"]
  I2 --> R2["Review risk: Test: dep-types.test.mjs"]
  R2 --> V2["targeted test run"]
Loading

This was referenced Jul 31, 2026
@seonghobae

Copy link
Copy Markdown
Contributor Author

Closing temporarily: Semgrep red + OpenCode CHANGES_REQUESTED, and it is competing with the security train for runner/review capacity.

CPM Late-Finish cap is a real correctness fix — please re-open a clean branch from green develop after #386/#387 land (no lockfile churn if possible).

@seonghobae seonghobae closed this Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants