@lumenflow/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +119 -0
- package/dist/active-wu-detector.d.ts +33 -0
- package/dist/active-wu-detector.js +106 -0
- package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
- package/dist/adapters/filesystem-metrics.adapter.js +519 -0
- package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
- package/dist/adapters/terminal-renderer.adapter.js +337 -0
- package/dist/arg-parser.d.ts +63 -0
- package/dist/arg-parser.js +560 -0
- package/dist/backlog-editor.d.ts +98 -0
- package/dist/backlog-editor.js +179 -0
- package/dist/backlog-generator.d.ts +111 -0
- package/dist/backlog-generator.js +381 -0
- package/dist/backlog-parser.d.ts +45 -0
- package/dist/backlog-parser.js +102 -0
- package/dist/backlog-sync-validator.d.ts +78 -0
- package/dist/backlog-sync-validator.js +294 -0
- package/dist/branch-drift.d.ts +34 -0
- package/dist/branch-drift.js +51 -0
- package/dist/cleanup-install-config.d.ts +33 -0
- package/dist/cleanup-install-config.js +37 -0
- package/dist/cleanup-lock.d.ts +139 -0
- package/dist/cleanup-lock.js +313 -0
- package/dist/code-path-validator.d.ts +146 -0
- package/dist/code-path-validator.js +537 -0
- package/dist/code-paths-overlap.d.ts +55 -0
- package/dist/code-paths-overlap.js +245 -0
- package/dist/commands-logger.d.ts +77 -0
- package/dist/commands-logger.js +254 -0
- package/dist/commit-message-utils.d.ts +25 -0
- package/dist/commit-message-utils.js +41 -0
- package/dist/compliance-parser.d.ts +150 -0
- package/dist/compliance-parser.js +507 -0
- package/dist/constants/backlog-patterns.d.ts +20 -0
- package/dist/constants/backlog-patterns.js +23 -0
- package/dist/constants/dora-constants.d.ts +49 -0
- package/dist/constants/dora-constants.js +53 -0
- package/dist/constants/gate-constants.d.ts +15 -0
- package/dist/constants/gate-constants.js +15 -0
- package/dist/constants/linter-constants.d.ts +16 -0
- package/dist/constants/linter-constants.js +16 -0
- package/dist/constants/tokenizer-constants.d.ts +15 -0
- package/dist/constants/tokenizer-constants.js +15 -0
- package/dist/core/scope-checker.d.ts +97 -0
- package/dist/core/scope-checker.js +163 -0
- package/dist/core/tool-runner.d.ts +161 -0
- package/dist/core/tool-runner.js +393 -0
- package/dist/core/tool.constants.d.ts +105 -0
- package/dist/core/tool.constants.js +101 -0
- package/dist/core/tool.schemas.d.ts +226 -0
- package/dist/core/tool.schemas.js +226 -0
- package/dist/core/worktree-guard.d.ts +130 -0
- package/dist/core/worktree-guard.js +242 -0
- package/dist/coverage-gate.d.ts +108 -0
- package/dist/coverage-gate.js +196 -0
- package/dist/date-utils.d.ts +75 -0
- package/dist/date-utils.js +140 -0
- package/dist/dependency-graph.d.ts +142 -0
- package/dist/dependency-graph.js +550 -0
- package/dist/dependency-guard.d.ts +54 -0
- package/dist/dependency-guard.js +142 -0
- package/dist/dependency-validator.d.ts +105 -0
- package/dist/dependency-validator.js +154 -0
- package/dist/docs-path-validator.d.ts +36 -0
- package/dist/docs-path-validator.js +95 -0
- package/dist/domain/orchestration.constants.d.ts +99 -0
- package/dist/domain/orchestration.constants.js +97 -0
- package/dist/domain/orchestration.schemas.d.ts +280 -0
- package/dist/domain/orchestration.schemas.js +211 -0
- package/dist/domain/orchestration.types.d.ts +133 -0
- package/dist/domain/orchestration.types.js +12 -0
- package/dist/error-handler.d.ts +116 -0
- package/dist/error-handler.js +136 -0
- package/dist/file-classifiers.d.ts +62 -0
- package/dist/file-classifiers.js +108 -0
- package/dist/gates-agent-mode.d.ts +81 -0
- package/dist/gates-agent-mode.js +94 -0
- package/dist/generate-traceability.d.ts +107 -0
- package/dist/generate-traceability.js +411 -0
- package/dist/git-adapter.d.ts +395 -0
- package/dist/git-adapter.js +649 -0
- package/dist/git-staged-validator.d.ts +32 -0
- package/dist/git-staged-validator.js +48 -0
- package/dist/hardcoded-strings.d.ts +61 -0
- package/dist/hardcoded-strings.js +270 -0
- package/dist/incremental-lint.d.ts +78 -0
- package/dist/incremental-lint.js +129 -0
- package/dist/incremental-test.d.ts +39 -0
- package/dist/incremental-test.js +61 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +61 -0
- package/dist/invariants/check-automated-tests.d.ts +50 -0
- package/dist/invariants/check-automated-tests.js +166 -0
- package/dist/invariants-runner.d.ts +103 -0
- package/dist/invariants-runner.js +527 -0
- package/dist/lane-checker.d.ts +50 -0
- package/dist/lane-checker.js +319 -0
- package/dist/lane-inference.d.ts +39 -0
- package/dist/lane-inference.js +195 -0
- package/dist/lane-lock.d.ts +211 -0
- package/dist/lane-lock.js +474 -0
- package/dist/lane-validator.d.ts +48 -0
- package/dist/lane-validator.js +114 -0
- package/dist/logs-lib.d.ts +104 -0
- package/dist/logs-lib.js +207 -0
- package/dist/lumenflow-config-schema.d.ts +272 -0
- package/dist/lumenflow-config-schema.js +207 -0
- package/dist/lumenflow-config.d.ts +95 -0
- package/dist/lumenflow-config.js +236 -0
- package/dist/manual-test-validator.d.ts +80 -0
- package/dist/manual-test-validator.js +200 -0
- package/dist/merge-lock.d.ts +115 -0
- package/dist/merge-lock.js +251 -0
- package/dist/micro-worktree.d.ts +159 -0
- package/dist/micro-worktree.js +427 -0
- package/dist/migration-deployer.d.ts +69 -0
- package/dist/migration-deployer.js +151 -0
- package/dist/orchestration-advisory-loader.d.ts +28 -0
- package/dist/orchestration-advisory-loader.js +87 -0
- package/dist/orchestration-advisory.d.ts +58 -0
- package/dist/orchestration-advisory.js +94 -0
- package/dist/orchestration-di.d.ts +48 -0
- package/dist/orchestration-di.js +57 -0
- package/dist/orchestration-rules.d.ts +57 -0
- package/dist/orchestration-rules.js +201 -0
- package/dist/orphan-detector.d.ts +131 -0
- package/dist/orphan-detector.js +226 -0
- package/dist/path-classifiers.d.ts +57 -0
- package/dist/path-classifiers.js +93 -0
- package/dist/piped-command-detector.d.ts +34 -0
- package/dist/piped-command-detector.js +64 -0
- package/dist/ports/dashboard-renderer.port.d.ts +112 -0
- package/dist/ports/dashboard-renderer.port.js +25 -0
- package/dist/ports/metrics-collector.port.d.ts +132 -0
- package/dist/ports/metrics-collector.port.js +26 -0
- package/dist/process-detector.d.ts +84 -0
- package/dist/process-detector.js +172 -0
- package/dist/prompt-linter.d.ts +72 -0
- package/dist/prompt-linter.js +312 -0
- package/dist/prompt-monitor.d.ts +15 -0
- package/dist/prompt-monitor.js +205 -0
- package/dist/rebase-artifact-cleanup.d.ts +145 -0
- package/dist/rebase-artifact-cleanup.js +433 -0
- package/dist/retry-strategy.d.ts +189 -0
- package/dist/retry-strategy.js +283 -0
- package/dist/risk-detector.d.ts +108 -0
- package/dist/risk-detector.js +252 -0
- package/dist/rollback-utils.d.ts +76 -0
- package/dist/rollback-utils.js +104 -0
- package/dist/section-headings.d.ts +43 -0
- package/dist/section-headings.js +49 -0
- package/dist/spawn-escalation.d.ts +90 -0
- package/dist/spawn-escalation.js +253 -0
- package/dist/spawn-monitor.d.ts +229 -0
- package/dist/spawn-monitor.js +672 -0
- package/dist/spawn-recovery.d.ts +82 -0
- package/dist/spawn-recovery.js +298 -0
- package/dist/spawn-registry-schema.d.ts +98 -0
- package/dist/spawn-registry-schema.js +108 -0
- package/dist/spawn-registry-store.d.ts +146 -0
- package/dist/spawn-registry-store.js +273 -0
- package/dist/spawn-tree.d.ts +121 -0
- package/dist/spawn-tree.js +285 -0
- package/dist/stamp-status-validator.d.ts +84 -0
- package/dist/stamp-status-validator.js +134 -0
- package/dist/stamp-utils.d.ts +100 -0
- package/dist/stamp-utils.js +229 -0
- package/dist/state-machine.d.ts +26 -0
- package/dist/state-machine.js +83 -0
- package/dist/system-map-validator.d.ts +80 -0
- package/dist/system-map-validator.js +272 -0
- package/dist/telemetry.d.ts +80 -0
- package/dist/telemetry.js +213 -0
- package/dist/token-counter.d.ts +51 -0
- package/dist/token-counter.js +145 -0
- package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
- package/dist/usecases/get-dashboard-data.usecase.js +61 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
- package/dist/usecases/get-suggestions.usecase.js +153 -0
- package/dist/user-normalizer.d.ts +41 -0
- package/dist/user-normalizer.js +141 -0
- package/dist/validators/phi-constants.d.ts +97 -0
- package/dist/validators/phi-constants.js +152 -0
- package/dist/validators/phi-scanner.d.ts +58 -0
- package/dist/validators/phi-scanner.js +215 -0
- package/dist/worktree-ownership.d.ts +50 -0
- package/dist/worktree-ownership.js +74 -0
- package/dist/worktree-scanner.d.ts +103 -0
- package/dist/worktree-scanner.js +168 -0
- package/dist/worktree-symlink.d.ts +99 -0
- package/dist/worktree-symlink.js +359 -0
- package/dist/wu-backlog-updater.d.ts +17 -0
- package/dist/wu-backlog-updater.js +37 -0
- package/dist/wu-checkpoint.d.ts +124 -0
- package/dist/wu-checkpoint.js +233 -0
- package/dist/wu-claim-helpers.d.ts +26 -0
- package/dist/wu-claim-helpers.js +63 -0
- package/dist/wu-claim-resume.d.ts +106 -0
- package/dist/wu-claim-resume.js +276 -0
- package/dist/wu-consistency-checker.d.ts +95 -0
- package/dist/wu-consistency-checker.js +567 -0
- package/dist/wu-constants.d.ts +1275 -0
- package/dist/wu-constants.js +1382 -0
- package/dist/wu-create-validators.d.ts +42 -0
- package/dist/wu-create-validators.js +93 -0
- package/dist/wu-done-branch-only.d.ts +63 -0
- package/dist/wu-done-branch-only.js +191 -0
- package/dist/wu-done-messages.d.ts +119 -0
- package/dist/wu-done-messages.js +185 -0
- package/dist/wu-done-pr.d.ts +72 -0
- package/dist/wu-done-pr.js +174 -0
- package/dist/wu-done-retry-helpers.d.ts +85 -0
- package/dist/wu-done-retry-helpers.js +172 -0
- package/dist/wu-done-ui.d.ts +37 -0
- package/dist/wu-done-ui.js +69 -0
- package/dist/wu-done-validators.d.ts +411 -0
- package/dist/wu-done-validators.js +1229 -0
- package/dist/wu-done-worktree.d.ts +182 -0
- package/dist/wu-done-worktree.js +1097 -0
- package/dist/wu-helpers.d.ts +128 -0
- package/dist/wu-helpers.js +248 -0
- package/dist/wu-lint.d.ts +70 -0
- package/dist/wu-lint.js +234 -0
- package/dist/wu-paths.d.ts +171 -0
- package/dist/wu-paths.js +178 -0
- package/dist/wu-preflight-validators.d.ts +86 -0
- package/dist/wu-preflight-validators.js +251 -0
- package/dist/wu-recovery.d.ts +138 -0
- package/dist/wu-recovery.js +341 -0
- package/dist/wu-repair-core.d.ts +131 -0
- package/dist/wu-repair-core.js +669 -0
- package/dist/wu-schema-normalization.d.ts +17 -0
- package/dist/wu-schema-normalization.js +82 -0
- package/dist/wu-schema.d.ts +793 -0
- package/dist/wu-schema.js +881 -0
- package/dist/wu-spawn-helpers.d.ts +121 -0
- package/dist/wu-spawn-helpers.js +271 -0
- package/dist/wu-spawn.d.ts +158 -0
- package/dist/wu-spawn.js +1306 -0
- package/dist/wu-state-schema.d.ts +213 -0
- package/dist/wu-state-schema.js +156 -0
- package/dist/wu-state-store.d.ts +264 -0
- package/dist/wu-state-store.js +691 -0
- package/dist/wu-status-transition.d.ts +63 -0
- package/dist/wu-status-transition.js +382 -0
- package/dist/wu-status-updater.d.ts +25 -0
- package/dist/wu-status-updater.js +116 -0
- package/dist/wu-transaction-collectors.d.ts +116 -0
- package/dist/wu-transaction-collectors.js +272 -0
- package/dist/wu-transaction.d.ts +170 -0
- package/dist/wu-transaction.js +273 -0
- package/dist/wu-validation-constants.d.ts +60 -0
- package/dist/wu-validation-constants.js +66 -0
- package/dist/wu-validation.d.ts +118 -0
- package/dist/wu-validation.js +243 -0
- package/dist/wu-validator.d.ts +62 -0
- package/dist/wu-validator.js +325 -0
- package/dist/wu-yaml-fixer.d.ts +97 -0
- package/dist/wu-yaml-fixer.js +264 -0
- package/dist/wu-yaml.d.ts +86 -0
- package/dist/wu-yaml.js +222 -0
- package/package.json +114 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readWU } from './wu-yaml.js';
|
|
4
|
+
import { WU_PATHS } from './wu-paths.js';
|
|
5
|
+
import { STRING_LITERALS, WU_STATUS } from './wu-constants.js';
|
|
6
|
+
// Optional import from @lumenflow/initiatives - if not available, provide stub
|
|
7
|
+
let detectCycles;
|
|
8
|
+
try {
|
|
9
|
+
// Dynamic import for optional peer dependency
|
|
10
|
+
const module = await import('@lumenflow/initiatives');
|
|
11
|
+
detectCycles = module.detectCycles;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Fallback stub if @lumenflow/initiatives is not available
|
|
15
|
+
detectCycles = () => ({ hasCycle: false, cycles: [] });
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Dependency Graph Module (WU-1247, WU-1568)
|
|
19
|
+
*
|
|
20
|
+
* Provides graph building, visualization, and analysis for WU dependencies.
|
|
21
|
+
* Supports ASCII tree and Mermaid diagram output formats.
|
|
22
|
+
* Includes graph algorithms for operational insights: topological sort,
|
|
23
|
+
* critical path, impact scoring, and bottleneck detection.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* import { buildDependencyGraph, renderASCII, renderMermaid, topologicalSort, criticalPath, bottlenecks } from './lib/dependency-graph.js';
|
|
27
|
+
*
|
|
28
|
+
* const graph = buildDependencyGraph();
|
|
29
|
+
* console.log(renderASCII(graph, 'WU-1247'));
|
|
30
|
+
* console.log(renderMermaid(graph, { direction: 'TD' }));
|
|
31
|
+
*
|
|
32
|
+
* // Graph analysis (WU-1568)
|
|
33
|
+
* const sorted = topologicalSort(graph);
|
|
34
|
+
* const critical = criticalPath(graph);
|
|
35
|
+
* const topBottlenecks = bottlenecks(graph, 10);
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Build a dependency graph from all WU YAML files.
|
|
39
|
+
*
|
|
40
|
+
* @returns {Map<string, {id: string, title: string, status: string, blocks: string[], blockedBy: string[]}>}
|
|
41
|
+
*/
|
|
42
|
+
export function buildDependencyGraph() {
|
|
43
|
+
const wuDir = path.dirname(WU_PATHS.WU('dummy'));
|
|
44
|
+
const graph = new Map();
|
|
45
|
+
if (!existsSync(wuDir)) {
|
|
46
|
+
return graph;
|
|
47
|
+
}
|
|
48
|
+
const files = readdirSync(wuDir).filter((f) => f.endsWith('.yaml') && f.startsWith('WU-'));
|
|
49
|
+
for (const f of files) {
|
|
50
|
+
const filePath = path.join(wuDir, f);
|
|
51
|
+
const id = f.replace('.yaml', '');
|
|
52
|
+
try {
|
|
53
|
+
const doc = readWU(filePath, id);
|
|
54
|
+
graph.set(id, {
|
|
55
|
+
id,
|
|
56
|
+
title: doc.title || id,
|
|
57
|
+
status: doc.status || 'unknown',
|
|
58
|
+
blocks: Array.isArray(doc.blocks) ? doc.blocks : [],
|
|
59
|
+
blockedBy: Array.isArray(doc.blocked_by) ? doc.blocked_by : [],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Skip invalid files
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return graph;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get all dependencies (upstream: blocked_by) for a WU.
|
|
70
|
+
*
|
|
71
|
+
* @param {Map} graph - Dependency graph
|
|
72
|
+
* @param {string} wuId - WU ID to get dependencies for
|
|
73
|
+
* @param {number} [maxDepth=10] - Maximum traversal depth
|
|
74
|
+
* @returns {Array<{id: string, depth: number, path: string[]}>}
|
|
75
|
+
*/
|
|
76
|
+
export function getUpstreamDependencies(graph, wuId, maxDepth = 10) {
|
|
77
|
+
const visited = new Set();
|
|
78
|
+
const result = [];
|
|
79
|
+
function traverse(id, depth, pathSoFar) {
|
|
80
|
+
if (depth > maxDepth || visited.has(id))
|
|
81
|
+
return;
|
|
82
|
+
visited.add(id);
|
|
83
|
+
const node = graph.get(id);
|
|
84
|
+
if (!node)
|
|
85
|
+
return;
|
|
86
|
+
for (const dep of node.blockedBy) {
|
|
87
|
+
if (!visited.has(dep)) {
|
|
88
|
+
result.push({ id: dep, depth, path: [...pathSoFar, dep] });
|
|
89
|
+
traverse(dep, depth + 1, [...pathSoFar, dep]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
traverse(wuId, 1, [wuId]);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get all dependents (downstream: blocks) for a WU.
|
|
98
|
+
*
|
|
99
|
+
* @param {Map} graph - Dependency graph
|
|
100
|
+
* @param {string} wuId - WU ID to get dependents for
|
|
101
|
+
* @param {number} [maxDepth=10] - Maximum traversal depth
|
|
102
|
+
* @returns {Array<{id: string, depth: number, path: string[]}>}
|
|
103
|
+
*/
|
|
104
|
+
export function getDownstreamDependents(graph, wuId, maxDepth = 10) {
|
|
105
|
+
const visited = new Set();
|
|
106
|
+
const result = [];
|
|
107
|
+
function traverse(id, depth, pathSoFar) {
|
|
108
|
+
if (depth > maxDepth || visited.has(id))
|
|
109
|
+
return;
|
|
110
|
+
visited.add(id);
|
|
111
|
+
const node = graph.get(id);
|
|
112
|
+
if (!node)
|
|
113
|
+
return;
|
|
114
|
+
for (const dep of node.blocks) {
|
|
115
|
+
if (!visited.has(dep)) {
|
|
116
|
+
result.push({ id: dep, depth, path: [...pathSoFar, dep] });
|
|
117
|
+
traverse(dep, depth + 1, [...pathSoFar, dep]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
traverse(wuId, 1, [wuId]);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Render dependency graph as ASCII tree.
|
|
126
|
+
*
|
|
127
|
+
* @param {Map} graph - Dependency graph
|
|
128
|
+
* @param {string} rootId - Root WU ID
|
|
129
|
+
* @param {RenderASCIIOptions} [options] - Render options
|
|
130
|
+
* @returns {string} ASCII tree representation
|
|
131
|
+
*/
|
|
132
|
+
export function renderASCII(graph, rootId, options = {}) {
|
|
133
|
+
const { direction = 'both', depth: maxDepth = 3 } = options;
|
|
134
|
+
const lines = [];
|
|
135
|
+
const root = graph.get(rootId);
|
|
136
|
+
if (!root) {
|
|
137
|
+
return `WU not found: ${rootId}`;
|
|
138
|
+
}
|
|
139
|
+
// Header
|
|
140
|
+
lines.push(`${rootId}: ${truncate(root.title, 50)}`);
|
|
141
|
+
lines.push('');
|
|
142
|
+
// Upstream (blocked_by)
|
|
143
|
+
if (direction === 'up' || direction === 'both') {
|
|
144
|
+
const upstream = getUpstreamDependencies(graph, rootId, maxDepth);
|
|
145
|
+
if (upstream.length > 0) {
|
|
146
|
+
lines.push('Dependencies (blocked by):');
|
|
147
|
+
for (const dep of upstream) {
|
|
148
|
+
const node = graph.get(dep.id);
|
|
149
|
+
const status = node ? `[${node.status}]` : '[unknown]';
|
|
150
|
+
const title = node ? truncate(node.title, 40) : '';
|
|
151
|
+
const indent = ' '.repeat(dep.depth);
|
|
152
|
+
lines.push(`${indent}+-- ${dep.id}: ${title} ${status}`);
|
|
153
|
+
}
|
|
154
|
+
lines.push('');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Downstream (blocks)
|
|
158
|
+
if (direction === 'down' || direction === 'both') {
|
|
159
|
+
const downstream = getDownstreamDependents(graph, rootId, maxDepth);
|
|
160
|
+
if (downstream.length > 0) {
|
|
161
|
+
lines.push('Dependents (blocks):');
|
|
162
|
+
for (const dep of downstream) {
|
|
163
|
+
const node = graph.get(dep.id);
|
|
164
|
+
const status = node ? `[${node.status}]` : '[unknown]';
|
|
165
|
+
const title = node ? truncate(node.title, 40) : '';
|
|
166
|
+
const indent = ' '.repeat(dep.depth);
|
|
167
|
+
lines.push(`${indent}+-- ${dep.id}: ${title} ${status}`);
|
|
168
|
+
}
|
|
169
|
+
lines.push('');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Check for cycles
|
|
173
|
+
const cycleResult = detectCycles(graph);
|
|
174
|
+
if (cycleResult.hasCycle) {
|
|
175
|
+
lines.push('⚠️ Circular dependencies detected:');
|
|
176
|
+
for (const cycle of cycleResult.cycles) {
|
|
177
|
+
lines.push(` ${cycle.join(' → ')}`);
|
|
178
|
+
}
|
|
179
|
+
lines.push('');
|
|
180
|
+
}
|
|
181
|
+
return lines.join(STRING_LITERALS.NEWLINE);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Render dependency graph as Mermaid diagram.
|
|
185
|
+
*
|
|
186
|
+
* @param {Map} graph - Dependency graph
|
|
187
|
+
* @param {RenderMermaidOptions} [options] - Render options
|
|
188
|
+
* @returns {string} Mermaid diagram syntax
|
|
189
|
+
*/
|
|
190
|
+
export function renderMermaid(graph, options = {}) {
|
|
191
|
+
const { rootId, direction = 'TD', depth: maxDepth = 3 } = options;
|
|
192
|
+
const lines = [];
|
|
193
|
+
const nodes = new Set();
|
|
194
|
+
const edges = [];
|
|
195
|
+
// Collect nodes and edges
|
|
196
|
+
if (rootId) {
|
|
197
|
+
// Focus on specific WU
|
|
198
|
+
const upstream = getUpstreamDependencies(graph, rootId, maxDepth);
|
|
199
|
+
const downstream = getDownstreamDependents(graph, rootId, maxDepth);
|
|
200
|
+
nodes.add(rootId);
|
|
201
|
+
for (const dep of upstream) {
|
|
202
|
+
nodes.add(dep.id);
|
|
203
|
+
}
|
|
204
|
+
for (const dep of downstream) {
|
|
205
|
+
nodes.add(dep.id);
|
|
206
|
+
}
|
|
207
|
+
// Build edges for focused view
|
|
208
|
+
for (const nodeId of nodes) {
|
|
209
|
+
const node = graph.get(nodeId);
|
|
210
|
+
if (!node)
|
|
211
|
+
continue;
|
|
212
|
+
for (const blockedBy of node.blockedBy) {
|
|
213
|
+
if (nodes.has(blockedBy)) {
|
|
214
|
+
edges.push({ from: blockedBy, to: nodeId });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// Full graph
|
|
221
|
+
for (const [nodeId, node] of graph.entries()) {
|
|
222
|
+
nodes.add(nodeId);
|
|
223
|
+
for (const blockedBy of node.blockedBy) {
|
|
224
|
+
if (graph.has(blockedBy)) {
|
|
225
|
+
edges.push({ from: blockedBy, to: nodeId });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Generate Mermaid
|
|
231
|
+
lines.push(`flowchart ${direction}`);
|
|
232
|
+
// Node definitions with labels
|
|
233
|
+
for (const nodeId of nodes) {
|
|
234
|
+
const node = graph.get(nodeId);
|
|
235
|
+
if (node) {
|
|
236
|
+
const shortTitle = truncate(node.title, 30);
|
|
237
|
+
lines.push(` ${nodeId}["${nodeId}: ${shortTitle}<br/>${node.status}"]`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
lines.push('');
|
|
241
|
+
// Edges
|
|
242
|
+
for (const { from, to } of edges) {
|
|
243
|
+
lines.push(` ${from} --> ${to}`);
|
|
244
|
+
}
|
|
245
|
+
lines.push('');
|
|
246
|
+
// Status styling
|
|
247
|
+
lines.push(' classDef done fill:#86efac,stroke:#22c55e');
|
|
248
|
+
lines.push(' classDef in_progress fill:#93c5fd,stroke:#3b82f6');
|
|
249
|
+
lines.push(' classDef ready fill:#fde68a,stroke:#f59e0b');
|
|
250
|
+
lines.push(' classDef blocked fill:#fca5a5,stroke:#ef4444');
|
|
251
|
+
// Apply classes
|
|
252
|
+
const statusGroups = { done: [], in_progress: [], ready: [], blocked: [] };
|
|
253
|
+
for (const nodeId of nodes) {
|
|
254
|
+
const node = graph.get(nodeId);
|
|
255
|
+
if (node && statusGroups[node.status]) {
|
|
256
|
+
statusGroups[node.status].push(nodeId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const [status, nodeIds] of Object.entries(statusGroups)) {
|
|
260
|
+
if (nodeIds.length > 0) {
|
|
261
|
+
lines.push(` class ${nodeIds.join(',')} ${status}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return lines.join(STRING_LITERALS.NEWLINE);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Validate dependency graph for cycles and orphans.
|
|
268
|
+
*
|
|
269
|
+
* @param {Map} graph - Dependency graph
|
|
270
|
+
* @returns {{hasCycle: boolean, cycles: string[][], orphans: Array<{wuId: string, ref: string}>}}
|
|
271
|
+
*/
|
|
272
|
+
export function validateGraph(graph) {
|
|
273
|
+
const allIds = new Set(graph.keys());
|
|
274
|
+
const orphans = [];
|
|
275
|
+
// Check for orphan references
|
|
276
|
+
for (const [wuId, node] of graph.entries()) {
|
|
277
|
+
for (const ref of [...node.blocks, ...node.blockedBy]) {
|
|
278
|
+
if (!allIds.has(ref)) {
|
|
279
|
+
orphans.push({ wuId, ref });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Transform graph to snake_case for detectCycles compatibility
|
|
284
|
+
// (dependency-graph uses camelCase internally, initiative-validator uses snake_case)
|
|
285
|
+
const snakeCaseGraph = new Map();
|
|
286
|
+
for (const [id, node] of graph.entries()) {
|
|
287
|
+
snakeCaseGraph.set(id, {
|
|
288
|
+
id: node.id,
|
|
289
|
+
blocked_by: node.blockedBy || [],
|
|
290
|
+
blocks: node.blocks || [],
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// Check for cycles
|
|
294
|
+
const cycleResult = detectCycles(snakeCaseGraph);
|
|
295
|
+
return {
|
|
296
|
+
hasCycle: cycleResult.hasCycle,
|
|
297
|
+
cycles: cycleResult.cycles,
|
|
298
|
+
orphans,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
// =============================================================================
|
|
302
|
+
// Graph Analysis Functions (WU-1568)
|
|
303
|
+
// =============================================================================
|
|
304
|
+
/**
|
|
305
|
+
* Filter graph to active (non-done) WUs only.
|
|
306
|
+
*
|
|
307
|
+
* @param {Map} graph - Full dependency graph
|
|
308
|
+
* @returns {Map} Graph containing only non-done WUs
|
|
309
|
+
*/
|
|
310
|
+
function filterActiveGraph(graph) {
|
|
311
|
+
const activeGraph = new Map();
|
|
312
|
+
for (const [id, node] of graph.entries()) {
|
|
313
|
+
if (node.status !== WU_STATUS.DONE) {
|
|
314
|
+
activeGraph.set(id, node);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return activeGraph;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Check if a WU's dependencies are satisfied (done or not in active graph).
|
|
321
|
+
*
|
|
322
|
+
* @param {Map} activeGraph - Active subgraph
|
|
323
|
+
* @param {Map} fullGraph - Full graph (includes done WUs)
|
|
324
|
+
* @param {object} node - Node to check
|
|
325
|
+
* @returns {boolean} True if all dependencies are satisfied
|
|
326
|
+
*/
|
|
327
|
+
function areDependenciesSatisfied(activeGraph, fullGraph, node) {
|
|
328
|
+
for (const depId of node.blockedBy) {
|
|
329
|
+
// Dependency is satisfied if:
|
|
330
|
+
// 1. It doesn't exist in the graph (orphan reference, treat as satisfied)
|
|
331
|
+
// 2. It's done
|
|
332
|
+
// 3. It's not in the active graph (already filtered out)
|
|
333
|
+
const depNode = fullGraph.get(depId);
|
|
334
|
+
if (depNode && depNode.status !== WU_STATUS.DONE && activeGraph.has(depId)) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Perform topological sort on non-done WUs using Kahn's algorithm.
|
|
342
|
+
* Returns valid execution ordering where dependencies come before dependents.
|
|
343
|
+
* Handles cycles gracefully by returning partial ordering with warning.
|
|
344
|
+
*
|
|
345
|
+
* @param {Map} graph - Dependency graph
|
|
346
|
+
* @returns {string[]|{order: string[], warning: string, cycleNodes: string[]}} Sorted WU IDs or warning object
|
|
347
|
+
*/
|
|
348
|
+
export function topologicalSort(graph) {
|
|
349
|
+
const activeGraph = filterActiveGraph(graph);
|
|
350
|
+
if (activeGraph.size === 0) {
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
// Build in-degree map (count of unsatisfied dependencies)
|
|
354
|
+
const inDegree = new Map();
|
|
355
|
+
for (const [id, node] of activeGraph.entries()) {
|
|
356
|
+
// Count only dependencies that are in the active graph
|
|
357
|
+
let count = 0;
|
|
358
|
+
for (const depId of node.blockedBy) {
|
|
359
|
+
if (activeGraph.has(depId)) {
|
|
360
|
+
count++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
inDegree.set(id, count);
|
|
364
|
+
}
|
|
365
|
+
// Start with nodes that have no active dependencies
|
|
366
|
+
const queue = [];
|
|
367
|
+
for (const [id, degree] of inDegree.entries()) {
|
|
368
|
+
if (degree === 0) {
|
|
369
|
+
queue.push(id);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const sorted = [];
|
|
373
|
+
while (queue.length > 0) {
|
|
374
|
+
const current = queue.shift();
|
|
375
|
+
sorted.push(current);
|
|
376
|
+
const node = activeGraph.get(current);
|
|
377
|
+
if (!node)
|
|
378
|
+
continue;
|
|
379
|
+
// Decrease in-degree of dependents
|
|
380
|
+
for (const depId of node.blocks) {
|
|
381
|
+
if (!activeGraph.has(depId))
|
|
382
|
+
continue;
|
|
383
|
+
const newDegree = inDegree.get(depId) - 1;
|
|
384
|
+
inDegree.set(depId, newDegree);
|
|
385
|
+
if (newDegree === 0) {
|
|
386
|
+
queue.push(depId);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Check for cycles (not all nodes processed)
|
|
391
|
+
if (sorted.length < activeGraph.size) {
|
|
392
|
+
const cycleNodes = [];
|
|
393
|
+
for (const [id, degree] of inDegree.entries()) {
|
|
394
|
+
if (degree > 0) {
|
|
395
|
+
cycleNodes.push(id);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
order: sorted,
|
|
400
|
+
warning: 'Cycle detected: some WUs have circular dependencies',
|
|
401
|
+
cycleNodes,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return sorted;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Find the critical path (longest dependency chain) in the graph.
|
|
408
|
+
* Uses dynamic programming on the DAG to find the longest path.
|
|
409
|
+
* Excludes done WUs from the path.
|
|
410
|
+
*
|
|
411
|
+
* @param {Map} graph - Dependency graph
|
|
412
|
+
* @returns {{path: string[], length: number, warning?: string}} Critical path info
|
|
413
|
+
*/
|
|
414
|
+
export function criticalPath(graph) {
|
|
415
|
+
const activeGraph = filterActiveGraph(graph);
|
|
416
|
+
if (activeGraph.size === 0) {
|
|
417
|
+
return { path: [], length: 0 };
|
|
418
|
+
}
|
|
419
|
+
// First, get topological order
|
|
420
|
+
const topoResult = topologicalSort(graph);
|
|
421
|
+
// Handle cycle case
|
|
422
|
+
if (!Array.isArray(topoResult)) {
|
|
423
|
+
return {
|
|
424
|
+
path: [],
|
|
425
|
+
length: 0,
|
|
426
|
+
warning: topoResult.warning,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
// Distance and predecessor maps for longest path
|
|
430
|
+
const distance = new Map();
|
|
431
|
+
const predecessor = new Map();
|
|
432
|
+
// Initialise distances
|
|
433
|
+
for (const id of topoResult) {
|
|
434
|
+
distance.set(id, 1); // Each node has length 1
|
|
435
|
+
predecessor.set(id, null);
|
|
436
|
+
}
|
|
437
|
+
// Process in topological order
|
|
438
|
+
for (const current of topoResult) {
|
|
439
|
+
const node = activeGraph.get(current);
|
|
440
|
+
if (!node)
|
|
441
|
+
continue;
|
|
442
|
+
// Update distances for dependents
|
|
443
|
+
for (const depId of node.blocks) {
|
|
444
|
+
if (!activeGraph.has(depId))
|
|
445
|
+
continue;
|
|
446
|
+
const newDistance = distance.get(current) + 1;
|
|
447
|
+
if (newDistance > distance.get(depId)) {
|
|
448
|
+
distance.set(depId, newDistance);
|
|
449
|
+
predecessor.set(depId, current);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Find the node with maximum distance
|
|
454
|
+
let maxDistance = 0;
|
|
455
|
+
let endNode = null;
|
|
456
|
+
for (const [id, dist] of distance.entries()) {
|
|
457
|
+
if (dist > maxDistance) {
|
|
458
|
+
maxDistance = dist;
|
|
459
|
+
endNode = id;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Reconstruct path
|
|
463
|
+
const path = [];
|
|
464
|
+
let current = endNode;
|
|
465
|
+
while (current !== null) {
|
|
466
|
+
path.unshift(current);
|
|
467
|
+
current = predecessor.get(current);
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
path,
|
|
471
|
+
length: path.length,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Calculate the impact score for a WU.
|
|
476
|
+
* Impact score is the count of all downstream dependents (recursive).
|
|
477
|
+
* Excludes done WUs from the count.
|
|
478
|
+
*
|
|
479
|
+
* @param {Map} graph - Dependency graph
|
|
480
|
+
* @param {string} wuId - WU ID to score
|
|
481
|
+
* @returns {number} Count of downstream dependents
|
|
482
|
+
*/
|
|
483
|
+
export function impactScore(graph, wuId) {
|
|
484
|
+
const activeGraph = filterActiveGraph(graph);
|
|
485
|
+
if (!activeGraph.has(wuId)) {
|
|
486
|
+
return 0;
|
|
487
|
+
}
|
|
488
|
+
// BFS to count all downstream dependents
|
|
489
|
+
const visited = new Set();
|
|
490
|
+
const queue = [wuId];
|
|
491
|
+
visited.add(wuId);
|
|
492
|
+
while (queue.length > 0) {
|
|
493
|
+
const current = queue.shift();
|
|
494
|
+
const node = activeGraph.get(current);
|
|
495
|
+
if (!node)
|
|
496
|
+
continue;
|
|
497
|
+
for (const depId of node.blocks) {
|
|
498
|
+
if (!visited.has(depId) && activeGraph.has(depId)) {
|
|
499
|
+
visited.add(depId);
|
|
500
|
+
queue.push(depId);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Score is count minus 1 (exclude the starting node)
|
|
505
|
+
return visited.size - 1;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Find the top N bottleneck WUs by impact score.
|
|
509
|
+
* A bottleneck is a WU that blocks many other WUs.
|
|
510
|
+
* Excludes done WUs from results.
|
|
511
|
+
*
|
|
512
|
+
* @param {Map} graph - Dependency graph
|
|
513
|
+
* @param {number} limit - Maximum number of bottlenecks to return
|
|
514
|
+
* @returns {Array<{id: string, score: number, title?: string}>} Bottlenecks sorted by score descending
|
|
515
|
+
*/
|
|
516
|
+
export function bottlenecks(graph, limit) {
|
|
517
|
+
const activeGraph = filterActiveGraph(graph);
|
|
518
|
+
if (activeGraph.size === 0) {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
// Calculate impact score for each active WU
|
|
522
|
+
const scores = [];
|
|
523
|
+
for (const [id, node] of activeGraph.entries()) {
|
|
524
|
+
const score = impactScore(graph, id);
|
|
525
|
+
scores.push({
|
|
526
|
+
id,
|
|
527
|
+
score,
|
|
528
|
+
title: node.title,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
// Sort by score descending
|
|
532
|
+
scores.sort((a, b) => b.score - a.score);
|
|
533
|
+
// Return top N
|
|
534
|
+
return scores.slice(0, limit);
|
|
535
|
+
}
|
|
536
|
+
// =============================================================================
|
|
537
|
+
// Helper Functions
|
|
538
|
+
// =============================================================================
|
|
539
|
+
/**
|
|
540
|
+
* Truncate string with ellipsis.
|
|
541
|
+
*
|
|
542
|
+
* @param {string} str - String to truncate
|
|
543
|
+
* @param {number} maxLen - Maximum length
|
|
544
|
+
* @returns {string} Truncated string
|
|
545
|
+
*/
|
|
546
|
+
function truncate(str, maxLen) {
|
|
547
|
+
if (!str)
|
|
548
|
+
return '';
|
|
549
|
+
return str.length > maxLen ? `${str.substring(0, maxLen - 3)}...` : str;
|
|
550
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Guard (WU-1783)
|
|
3
|
+
*
|
|
4
|
+
* Detects dependency-mutating pnpm commands and provides blocking/guidance
|
|
5
|
+
* for worktree discipline enforcement.
|
|
6
|
+
*
|
|
7
|
+
* Used by:
|
|
8
|
+
* - pre-tool-use-hook.sh to block dependency mutations on main
|
|
9
|
+
* - deps:add and deps:remove wrapper commands
|
|
10
|
+
*
|
|
11
|
+
* @see {@link .claude/hooks/pre-tool-use-hook.sh} - PreToolUse hook
|
|
12
|
+
* @see {@link tools/deps-add.mjs} - Safe wrapper for pnpm add
|
|
13
|
+
* @see {@link tools/deps-remove.mjs} - Safe wrapper for pnpm remove
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* pnpm subcommands that mutate dependencies.
|
|
17
|
+
*
|
|
18
|
+
* These commands modify package.json, pnpm-lock.yaml, and/or node_modules.
|
|
19
|
+
* Running them on main checkout violates worktree isolation.
|
|
20
|
+
*
|
|
21
|
+
* Includes both full names and shorthand aliases:
|
|
22
|
+
* - add: Add packages to dependencies
|
|
23
|
+
* - install/i: Install packages from lockfile
|
|
24
|
+
* - remove/rm/uninstall: Remove packages from dependencies
|
|
25
|
+
* - update/up: Update packages to latest
|
|
26
|
+
*/
|
|
27
|
+
export declare const DEPENDENCY_MUTATING_COMMANDS: string[];
|
|
28
|
+
/**
|
|
29
|
+
* Check if a command is a dependency-mutating pnpm command.
|
|
30
|
+
*
|
|
31
|
+
* @param {string|null|undefined} command - Command string to check
|
|
32
|
+
* @returns {boolean} True if the command mutates dependencies
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* isDependencyMutatingCommand('pnpm add react'); // true
|
|
36
|
+
* isDependencyMutatingCommand('pnpm run test'); // false
|
|
37
|
+
* isDependencyMutatingCommand('npm install'); // false (not pnpm)
|
|
38
|
+
*/
|
|
39
|
+
export declare function isDependencyMutatingCommand(command: any): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Build a blocking message for dependency-mutating commands on main.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} command - The blocked command
|
|
44
|
+
* @returns {string} Formatted error message with guidance
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const message = buildDependencyBlockMessage('pnpm add react');
|
|
48
|
+
* // Returns multi-line message with guidance
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildDependencyBlockMessage(command: any): string;
|
|
51
|
+
/**
|
|
52
|
+
* Log prefix for dependency guard output
|
|
53
|
+
*/
|
|
54
|
+
export declare const DEPS_LOG_PREFIX = "[deps-guard]";
|