diff --git a/package-lock.json b/package-lock.json index 9a14b7c..07caff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "devDependencies": { "@types/node": "^22.0.0", "@vitest/coverage-v8": "^3.2.4", + "tsup": "^8.5.1", "typescript": "^5.7.0", "vitest": "^3.1.0" }, @@ -1206,6 +1207,19 @@ "integrity": "sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==", "license": "MIT" }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1232,6 +1246,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1284,6 +1305,22 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1321,6 +1358,22 @@ "node": ">= 16" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1341,6 +1394,33 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1494,6 +1574,18 @@ } } }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1795,6 +1887,16 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -1802,6 +1904,36 @@ "dev": true, "license": "MIT" }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -1880,6 +2012,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, "node_modules/mnemonist": { "version": "0.39.8", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", @@ -1896,6 +2041,18 @@ "dev": true, "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1915,6 +2072,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/obliterator": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", @@ -2001,6 +2168,28 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/postcss": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", @@ -2030,6 +2219,73 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", @@ -2131,6 +2387,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2272,6 +2538,29 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2300,6 +2589,29 @@ "node": ">=18" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2361,6 +2673,76 @@ "node": ">=14.0.0" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2375,6 +2757,13 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/package.json b/package.json index c23c40a..fd7f941 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "dist" ], "scripts": { - "build": "tsc", + "build": "tsup", + "build:tsc": "tsc", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", @@ -49,6 +50,7 @@ "devDependencies": { "@types/node": "^22.0.0", "@vitest/coverage-v8": "^3.2.4", + "tsup": "^8.5.1", "typescript": "^5.7.0", "vitest": "^3.1.0" }, diff --git a/src/analysis/cost-benefit.ts b/src/analysis/cost-benefit.ts index 717ddfb..3547c8f 100644 --- a/src/analysis/cost-benefit.ts +++ b/src/analysis/cost-benefit.ts @@ -5,6 +5,7 @@ import type { WorkflowCostResult, } from "../schema/results.js"; import type { TaskGraphInner } from "../graph/construction.js"; +import { TaskGraph } from "../graph/index.js"; import { topologicalOrder } from "../graph/queries.js"; import { resolveDefaults } from "./defaults.js"; @@ -149,21 +150,22 @@ export function computeEffectiveP( * **remain in the propagation chain** with p=1.0. Removing completed tasks from * propagation would worsen downstream probability estimates. * - * @param graph - The graphology graph instance + * @param graph - The TaskGraph instance * @param options - Optional configuration for the analysis * @returns WorkflowCostResult with per-task entries and aggregate totals * @throws {CircularDependencyError} If the graph contains cycles */ export function workflowCost( - graph: TaskGraphInner, + graph: TaskGraph, options?: WorkflowCostOptions, ): WorkflowCostResult { + const raw = graph.raw; const propagationMode = options?.propagationMode ?? "dag-propagate"; const defaultQualityRetention = options?.defaultQualityRetention ?? 0.9; const includeCompleted = options?.includeCompleted ?? false; // Get topological order — throws CircularDependencyError if cyclic - const topoOrder = topologicalOrder(graph); + const topoOrder = topologicalOrder(raw); // Map of task IDs → their actual success probability for downstream propagation const upstreamSuccessProbs = new Map(); @@ -172,23 +174,19 @@ export function workflowCost( const taskEntries: WorkflowCostResult["tasks"] = []; for (const taskId of topoOrder) { - const nodeAttrs = graph.getNodeAttributes(taskId); + const nodeAttrs = raw.getNodeAttributes(taskId); const resolved = resolveDefaults(nodeAttrs); const pIntrinsic = resolved.successProbability; - // Determine the probability to propagate downstream for this task let propagationP: number; let pEffective: number; - // Completed tasks propagate with p=1.0 when includeCompleted is false const isCompleted = nodeAttrs.status === "completed"; if (isCompleted && !includeCompleted) { - // Completed + excluded: propagate p=1.0, compute pEffective normally but - // for propagation purposes the task is a guaranteed success pEffective = computeEffectiveP( taskId, - graph, + raw, upstreamSuccessProbs, defaultQualityRetention, propagationMode, @@ -196,10 +194,9 @@ export function workflowCost( ); propagationP = 1.0; } else { - // Normal task: compute pEffective and use it for downstream propagation pEffective = computeEffectiveP( taskId, - graph, + raw, upstreamSuccessProbs, defaultQualityRetention, propagationMode, diff --git a/tasks/implementation/analysis/critical-path.md b/tasks/implementation/analysis/critical-path.md index 3864c66..ff1098f 100644 --- a/tasks/implementation/analysis/critical-path.md +++ b/tasks/implementation/analysis/critical-path.md @@ -1,7 +1,7 @@ --- id: analysis/critical-path name: Implement criticalPath and weightedCriticalPath functions -status: pending +status: completed depends_on: - graph/construction - graph/queries @@ -33,7 +33,22 @@ Implement `criticalPath` and `weightedCriticalPath` as standalone functions. `cr ## Notes -> To be filled by implementation agent +Implementation uses topological order + dynamic programming (longest path in DAG). +Both functions delegate to a shared `computeLongestPath` helper that: +1. Gets topological order (throws CircularDependencyError via `graph.topologicalOrder()`) +2. Initializes source nodes with their weight +3. Relaxes edges in topological order (DP: dist[v] = max(dist[u] + weight(v))) +4. Backtracks from the node with maximum distance to reconstruct the path + +`criticalPath` uses `weightFn = () => 1` (unweighted). +`weightedCriticalPath` accepts a custom weight function on `(taskId, attrs)`. + +## Summary + +Implemented `criticalPath` and `weightedCriticalPath` as standalone functions using topological-order DP. +- Modified: `src/analysis/critical-path.ts` (full implementation, 161 lines) +- Modified: `test/analysis.test.ts` (20 tests covering all acceptance criteria) +- Tests: 20, all passing (462 total passing) ## Summary diff --git a/tasks/implementation/api/public-exports.md b/tasks/implementation/api/public-exports.md index 94670d6..3ad387a 100644 --- a/tasks/implementation/api/public-exports.md +++ b/tasks/implementation/api/public-exports.md @@ -1,7 +1,7 @@ --- id: api/public-exports name: Wire up public API surface in src/index.ts -status: pending +status: completed depends_on: - graph/taskgraph-class - graph/construction @@ -54,4 +54,10 @@ Wire up `src/index.ts` to re-export the full public API surface. This is the mai ## Summary -> To be filled on completion \ No newline at end of file +Implemented the public API surface in `src/index.ts` using selective named re-exports instead of wildcard `export *`, ensuring no internal implementation details leak through. + +- Modified: `src/index.ts` — rewrote with selective named exports for all public API items +- Modified: `src/schema/task.ts` — removed internal `Nullable` re-export (kept import for internal use) +- Modified: `src/schema/index.ts` — switched to `export *` (kept as barrel; public API filtering is in src/index.ts) +- Modified: `test/schema.test.ts` — removed test for `Nullable` re-export from task.ts (no longer exported) +- Tests: 590, all passing \ No newline at end of file diff --git a/test/analysis.test.ts b/test/analysis.test.ts index 93e31ab..dde176e 100644 --- a/test/analysis.test.ts +++ b/test/analysis.test.ts @@ -213,6 +213,12 @@ describe('bottlenecks', () => { expect(typeof bottlenecks).toBe('function'); }); + it('returns empty array for empty graph', () => { + const tg = new TaskGraph(); + const result = bottlenecks(tg); + expect(result).toEqual([]); + }); + it('returns array of { taskId, score } objects', () => { const tg = TaskGraph.fromTasks([ { id: 'A', name: 'Task A', dependsOn: [] }, diff --git a/test/cost-benefit.test.ts b/test/cost-benefit.test.ts index 65de72e..7937dfc 100644 --- a/test/cost-benefit.test.ts +++ b/test/cost-benefit.test.ts @@ -601,7 +601,7 @@ describe("workflowCost", () => { { id: "B", name: "Implementation", dependsOn: ["A"], risk: "medium", scope: "broad", impact: "component" }, ]); - const result = workflowCost(graph.raw); + const result = workflowCost(graph); expect(result.propagationMode).toBe("dag-propagate"); @@ -632,7 +632,7 @@ describe("workflowCost", () => { { id: "B", name: "Implementation", dependsOn: ["A"], risk: "medium", scope: "broad", impact: "component" }, ]); - const result = workflowCost(graph.raw, { propagationMode: "independent" }); + const result = workflowCost(graph, { propagationMode: "independent" }); expect(result.propagationMode).toBe("independent"); @@ -655,8 +655,8 @@ describe("workflowCost", () => { { id: "C", name: "Review", dependsOn: ["B"], risk: "low", scope: "narrow", impact: "isolated" }, ]); - const dagResult = workflowCost(graph.raw, { propagationMode: "dag-propagate" }); - const indepResult = workflowCost(graph.raw, { propagationMode: "independent" }); + const dagResult = workflowCost(graph, { propagationMode: "dag-propagate" }); + const indepResult = workflowCost(graph, { propagationMode: "independent" }); // In dag-propagate, every task that has parents should have pEffective < pIntrinsic // (assuming qualityRetention < 1.0) @@ -695,7 +695,7 @@ describe("workflowCost", () => { { id: "C", name: "Task C", dependsOn: ["B"], risk: "medium", scope: "narrow", impact: "isolated" }, ]); - const result = workflowCost(graph.raw, { propagationMode: "dag-propagate" }); + const result = workflowCost(graph, { propagationMode: "dag-propagate" }); const taskA = result.tasks.find(t => t.taskId === "A")!; const taskB = result.tasks.find(t => t.taskId === "B")!; @@ -727,7 +727,7 @@ describe("workflowCost", () => { it("diamond graph: convergence multiplies inherited quality factors", () => { const graph = createDiamondGraph(); - const result = workflowCost(graph.raw); + const result = workflowCost(graph); const taskA = result.tasks.find(t => t.taskId === "A")!; const taskB = result.tasks.find(t => t.taskId === "B")!; @@ -760,7 +760,7 @@ describe("workflowCost", () => { { id: "B", name: "Task B", dependsOn: ["A"], risk: "medium", scope: "narrow", impact: "isolated" }, ]); - const result = workflowCost(graph.raw, { includeCompleted: false }); + const result = workflowCost(graph, { includeCompleted: false }); // A should not appear in the task list expect(result.tasks.find(t => t.taskId === "A")).toBeUndefined(); @@ -780,7 +780,7 @@ describe("workflowCost", () => { { id: "B", name: "Task B", dependsOn: ["A"], risk: "medium", scope: "narrow", impact: "isolated" }, ]); - const result = workflowCost(graph.raw); // default includeCompleted=false + const result = workflowCost(graph); // default includeCompleted=false // A should NOT appear in the task list (default behavior) expect(result.tasks.find(t => t.taskId === "A")).toBeUndefined(); @@ -798,7 +798,7 @@ describe("workflowCost", () => { { id: "B", name: "Task B", dependsOn: ["A"], risk: "medium", scope: "narrow", impact: "isolated" }, ]); - const result = workflowCost(graph.raw, { includeCompleted: true }); + const result = workflowCost(graph, { includeCompleted: true }); // A should appear in the task list when explicitly included const taskA = result.tasks.find(t => t.taskId === "A")!; @@ -827,7 +827,7 @@ describe("workflowCost", () => { { id: "C", name: "Task C", dependsOn: ["B"], risk: "medium", scope: "narrow", impact: "isolated" }, ]); - const result = workflowCost(graph.raw, { includeCompleted: false }); + const result = workflowCost(graph, { includeCompleted: false }); // A should not be in results expect(result.tasks.find(t => t.taskId === "A")).toBeUndefined(); @@ -852,7 +852,7 @@ describe("workflowCost", () => { { id: "B", name: "Task B", dependsOn: [], risk: "medium", scope: "narrow", impact: "isolated", status: "blocked" }, ]); - const result = workflowCost(graph.raw, { includeCompleted: false }); + const result = workflowCost(graph, { includeCompleted: false }); // Both failed and blocked tasks should be included expect(result.tasks.find(t => t.taskId === "A")).toBeDefined(); @@ -861,12 +861,12 @@ describe("workflowCost", () => { it("throws CircularDependencyError for cyclic graph", () => { const graph = createCyclicGraph(); - expect(() => workflowCost(graph.raw)).toThrow(CircularDependencyError); + expect(() => workflowCost(graph)).toThrow(CircularDependencyError); }); it("handles empty graph", () => { const graph = new TaskGraph(); - const result = workflowCost(graph.raw); + const result = workflowCost(graph); expect(result.tasks).toEqual([]); expect(result.totalEv).toBe(0); @@ -879,7 +879,7 @@ describe("workflowCost", () => { { id: "A", name: "Task A", dependsOn: [], risk: "medium", scope: "narrow", impact: "isolated" }, ]); - const result = workflowCost(graph.raw); + const result = workflowCost(graph); expect(result.tasks).toHaveLength(1); expect(result.tasks[0]!.taskId).toBe("A"); @@ -896,7 +896,7 @@ describe("workflowCost", () => { tg.raw.addEdgeWithKey("A->B", "A", "B", {}); // With defaultQualityRetention = 1.0, should behave like independent model - const result = workflowCost(tg.raw, { defaultQualityRetention: 1.0 }); + const result = workflowCost(tg, { defaultQualityRetention: 1.0 }); const taskB = result.tasks.find(t => t.taskId === "B")!; // inheritedQuality = parentP + (1-parentP) * 1.0 = 1.0 @@ -916,7 +916,7 @@ describe("workflowCost", () => { ] ); - const result = workflowCost(graph.raw); // default qualityRetention=0.9 + const result = workflowCost(graph); // default qualityRetention=0.9 const taskB = result.tasks.find(t => t.taskId === "B")!; // Per-edge qualityRetention = 0.5 overrides default 0.9 @@ -932,7 +932,7 @@ describe("workflowCost", () => { { id: "C", name: "Task C", dependsOn: ["B"], risk: "medium", scope: "narrow", impact: "isolated" }, ]); - const result = workflowCost(graph.raw, { limit: 2 }); + const result = workflowCost(graph, { limit: 2 }); expect(result.tasks).toHaveLength(2); // limit only affects the result list, not propagation @@ -946,7 +946,7 @@ describe("workflowCost", () => { { id: "B", name: "Task B", dependsOn: ["A"], risk: "medium", scope: "narrow", impact: "isolated" }, ]); - const result = workflowCost(graph.raw); + const result = workflowCost(graph); for (const task of result.tasks) { expect(typeof task.pIntrinsic).toBe("number"); expect(typeof task.pEffective).toBe("number"); @@ -965,7 +965,7 @@ describe("workflowCost", () => { { id: "B", name: "Task B", dependsOn: [], risk: "medium", scope: "narrow", impact: "isolated" }, ]); - const result = workflowCost(graph.raw, { propagationMode: "independent" }); + const result = workflowCost(graph, { propagationMode: "independent" }); // Two independent tasks with medium risk, narrow scope, isolated impact // p=0.80, scopeCost=2.0, impactWeight=1.0 @@ -981,7 +981,7 @@ describe("workflowCost", () => { { id: "A", name: "Task A", dependsOn: [] }, ]); - const result = workflowCost(graph.raw); + const result = workflowCost(graph); const taskA = result.tasks[0]!; // defaults: risk=medium (p=0.80), scope=narrow (costEstimate=2.0), impact=isolated (weight=1.0) @@ -998,7 +998,7 @@ describe("workflowCost", () => { { id: "C", name: "Task C", dependsOn: ["B"], risk: "low", scope: "narrow", impact: "isolated" }, ]); - const result = workflowCost(graph.raw, { propagationMode: "independent" }); + const result = workflowCost(graph, { propagationMode: "independent" }); for (const task of result.tasks) { expect(task.pEffective).toBeCloseTo(task.pIntrinsic); @@ -1013,8 +1013,8 @@ describe("workflowCost", () => { { id: "implementation", name: "Implementation", dependsOn: ["planning"], risk: "medium", scope: "broad", impact: "component" }, ]); - const dagResult = workflowCost(graph.raw, { propagationMode: "dag-propagate" }); - const indepResult = workflowCost(graph.raw, { propagationMode: "independent" }); + const dagResult = workflowCost(graph, { propagationMode: "dag-propagate" }); + const indepResult = workflowCost(graph, { propagationMode: "independent" }); const dagImpl = dagResult.tasks.find(t => t.taskId === "implementation")!; const indepImpl = indepResult.tasks.find(t => t.taskId === "implementation")!; @@ -1039,8 +1039,8 @@ describe("workflowCost", () => { { id: "B", name: "Task B", dependsOn: [], risk: "medium", scope: "narrow", impact: "isolated" }, ]); - const dagResult = workflowCost(graph.raw, { propagationMode: "dag-propagate" }); - const indepResult = workflowCost(graph.raw, { propagationMode: "independent" }); + const dagResult = workflowCost(graph, { propagationMode: "dag-propagate" }); + const indepResult = workflowCost(graph, { propagationMode: "independent" }); // No dependencies → no propagation → same result expect(dagResult.tasks[0]!.pEffective).toBeCloseTo(indepResult.tasks[0]!.pEffective); @@ -1055,13 +1055,13 @@ describe("workflowCost", () => { describe("workflowCost cycle detection", () => { it("throws CircularDependencyError when graph has cycles", () => { const graph = createCyclicGraph(); - expect(() => workflowCost(graph.raw)).toThrow(CircularDependencyError); + expect(() => workflowCost(graph)).toThrow(CircularDependencyError); }); it("CircularDependencyError contains cycle information", () => { const graph = createCyclicGraph(); try { - workflowCost(graph.raw); + workflowCost(graph); expect.fail("Should have thrown CircularDependencyError"); } catch (error) { expect(error).toBeInstanceOf(CircularDependencyError); diff --git a/test/subgraph-and-validation.test.ts b/test/subgraph-and-validation.test.ts index d6161c7..09f6dde 100644 --- a/test/subgraph-and-validation.test.ts +++ b/test/subgraph-and-validation.test.ts @@ -420,6 +420,11 @@ describe('validateGraph', () => { expect(danglingErrors).toHaveLength(0); }); + // Note: dangling-reference detection (lines 78-93 in validation.ts) is unreachable + // through the public API because graphology's mergeEdge auto-creates missing nodes + // and addEdgeWithKey rejects non-existent source/target. The code is a defensive + // guard for direct raw graph mutation that bypasses TaskGraph invariants. + it('detects multiple independent cycles', () => { // Create a graph with two independent cycles const data: TaskGraphSerialized = { diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..bec3520 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: true, + sourcemap: true, + clean: true, + target: 'es2022', +}); \ No newline at end of file