diff --git a/analytics.js b/analytics.js index 5d86500f..b32ca052 100644 --- a/analytics.js +++ b/analytics.js @@ -169,7 +169,12 @@ export function computeCpm(tasks, opts = {}) { else if (l.type === 'SF') c = slf - l.lag + d; else c = sls - l.lag; // FS return Math.min(m, c); - }, Infinity) + // Seed with projectDurationDays (not Infinity): standard CPM caps every + // activity's Late Finish at project completion, then tightens it with + // successor constraints. With Infinity, a node whose only successor links + // are SS/FF/SF could take an LF looser than the project end, giving a + // truly-critical activity false total float (and an empty critical path). + }, projectDurationDays) : projectDurationDays; lf.set(id, finish); ls.set(id, finish - d); diff --git a/package-lock.json b/package-lock.json index 21575e82..7abe7165 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "scopeweave", "version": "1.0.0", "dependencies": { - "@hono/node-server": "^1.19.14", + "@hono/node-server": "^2.0.12", "hono": "^4.12.27" }, "devDependencies": { @@ -17,12 +17,12 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" diff --git a/package.json b/package.json index 9ae8b292..2f90e2a3 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "fuzz": "node --test tests/fuzz/*.mjs" }, "dependencies": { - "@hono/node-server": "^1.19.14", + "@hono/node-server": "^2.0.12", "hono": "^4.12.27" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bffabf92..5d7b7de5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@hono/node-server': - specifier: ^1.19.14 - version: 1.19.14(hono@4.12.28) + specifier: ^2.0.12 + version: 2.0.12(hono@4.12.28) hono: specifier: ^4.12.27 version: 4.12.28 @@ -24,9 +24,9 @@ importers: packages: - '@hono/node-server@1.19.14': - resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} - engines: {node: '>=18.14.1'} + '@hono/node-server@2.0.12': + resolution: {integrity: sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==} + engines: {node: '>=20'} peerDependencies: hono: ^4 @@ -63,7 +63,7 @@ packages: snapshots: - '@hono/node-server@1.19.14(hono@4.12.28)': + '@hono/node-server@2.0.12(hono@4.12.28)': dependencies: hono: 4.12.28 diff --git a/tests/unit/dep-types.test.mjs b/tests/unit/dep-types.test.mjs index 8943fac9..cbc0ff9a 100644 --- a/tests/unit/dep-types.test.mjs +++ b/tests/unit/dep-types.test.mjs @@ -61,4 +61,22 @@ const T = (id, duration, predecessors = '') => ({ id, duration, predecessors }); assert.equal(r.cycleDetected, true); } +// backward pass caps Late Finish at the project duration. An SS long-pole with a +// short terminal follow-on: A (10d) alone drives the schedule end; B (2d) starts +// with A but finishes early. A must be critical with zero total float. Regression: +// seeding the successor reduce with Infinity left A's LF uncapped at 18 (> project +// duration 10), giving A a false slack of 8, marking it non-critical, and returning +// an EMPTY critical path for a schedule that plainly has one. +{ + const r = computeCpm([T('A', 10), T('B', 2, 'ASS')]); + assert.equal(r.projectDurationDays, 10, 'A(0-10) is the long pole'); + assert.equal(r.perTask.A.lf, 10, 'LF capped at project duration, not 18'); + assert.equal(r.perTask.A.slack, 0, 'sole long-pole activity has zero total float'); + assert.ok(r.perTask.A.critical, 'A is critical'); + assert.deepEqual(r.criticalPath, ['A'], 'critical path is A, not empty'); + // B keeps its real slack and stays non-critical. + assert.equal(r.perTask.B.slack, 8, 'B(0-2) has 8d float to the project end'); + assert.equal(r.perTask.B.critical, false); +} + console.log('✓ dependency-type (SS/FF/SF+lag) tests passed');