forge-workflow 0.0.2 → 0.0.3
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/.claude/commands/plan.md +93 -4
- package/.cline/workflows/dev.md +311 -0
- package/.cline/workflows/plan.md +475 -0
- package/.cline/workflows/premerge.md +176 -0
- package/.cline/workflows/research.md +39 -0
- package/.cline/workflows/review.md +439 -0
- package/.cline/workflows/rollback.md +718 -0
- package/.cline/workflows/ship.md +131 -0
- package/.cline/workflows/sonarcloud.md +146 -0
- package/.cline/workflows/status.md +74 -0
- package/.cline/workflows/validate.md +234 -0
- package/.cline/workflows/verify.md +218 -0
- package/.codex/config.toml +11 -0
- package/.codex/skills/dev/SKILL.md +314 -0
- package/.codex/skills/plan/SKILL.md +478 -0
- package/.codex/skills/premerge/SKILL.md +179 -0
- package/.codex/skills/research/SKILL.md +42 -0
- package/.codex/skills/review/SKILL.md +442 -0
- package/.codex/skills/rollback/SKILL.md +721 -0
- package/.codex/skills/ship/SKILL.md +134 -0
- package/.codex/skills/sonarcloud/SKILL.md +149 -0
- package/.codex/skills/status/SKILL.md +77 -0
- package/.codex/skills/validate/SKILL.md +237 -0
- package/.codex/skills/verify/SKILL.md +221 -0
- package/.cursor/commands/dev.md +311 -0
- package/.cursor/commands/plan.md +475 -0
- package/.cursor/commands/premerge.md +176 -0
- package/.cursor/commands/research.md +39 -0
- package/.cursor/commands/review.md +439 -0
- package/.cursor/commands/rollback.md +718 -0
- package/.cursor/commands/ship.md +131 -0
- package/.cursor/commands/sonarcloud.md +146 -0
- package/.cursor/commands/status.md +74 -0
- package/.cursor/commands/validate.md +234 -0
- package/.cursor/commands/verify.md +218 -0
- package/.cursor/rules/permissions-guidance.mdc +37 -0
- package/.github/prompts/dev.prompt.md +316 -0
- package/.github/prompts/plan.prompt.md +480 -0
- package/.github/prompts/premerge.prompt.md +181 -0
- package/.github/prompts/research.prompt.md +44 -0
- package/.github/prompts/review.prompt.md +444 -0
- package/.github/prompts/rollback.prompt.md +723 -0
- package/.github/prompts/ship.prompt.md +136 -0
- package/.github/prompts/sonarcloud.prompt.md +151 -0
- package/.github/prompts/status.prompt.md +79 -0
- package/.github/prompts/validate.prompt.md +239 -0
- package/.github/prompts/verify.prompt.md +223 -0
- package/.kilocode/workflows/dev.md +315 -0
- package/.kilocode/workflows/plan.md +479 -0
- package/.kilocode/workflows/premerge.md +180 -0
- package/.kilocode/workflows/research.md +43 -0
- package/.kilocode/workflows/review.md +443 -0
- package/.kilocode/workflows/rollback.md +722 -0
- package/.kilocode/workflows/ship.md +135 -0
- package/.kilocode/workflows/sonarcloud.md +150 -0
- package/.kilocode/workflows/status.md +78 -0
- package/.kilocode/workflows/validate.md +238 -0
- package/.kilocode/workflows/verify.md +222 -0
- package/.opencode/commands/dev.md +314 -0
- package/.opencode/commands/plan.md +478 -0
- package/.opencode/commands/premerge.md +179 -0
- package/.opencode/commands/research.md +42 -0
- package/.opencode/commands/review.md +442 -0
- package/.opencode/commands/rollback.md +721 -0
- package/.opencode/commands/ship.md +134 -0
- package/.opencode/commands/sonarcloud.md +149 -0
- package/.opencode/commands/status.md +77 -0
- package/.opencode/commands/validate.md +237 -0
- package/.opencode/commands/verify.md +221 -0
- package/.roo/commands/dev.md +315 -0
- package/.roo/commands/plan.md +479 -0
- package/.roo/commands/premerge.md +180 -0
- package/.roo/commands/research.md +43 -0
- package/.roo/commands/review.md +443 -0
- package/.roo/commands/rollback.md +722 -0
- package/.roo/commands/ship.md +135 -0
- package/.roo/commands/sonarcloud.md +150 -0
- package/.roo/commands/status.md +78 -0
- package/.roo/commands/validate.md +238 -0
- package/.roo/commands/verify.md +222 -0
- package/LICENSE +21 -21
- package/docs/ENHANCED_ONBOARDING.md +2 -2
- package/docs/TOOLCHAIN.md +15 -234
- package/install.sh +32 -36
- package/lib/commands/plan.js +11 -15
- package/lib/commands/recommend.js +2 -2
- package/lib/dep-guard/analyzer.js +294 -0
- package/lib/dep-guard/behavior-detector.js +98 -0
- package/lib/dep-guard/contract-detector.js +162 -0
- package/lib/dep-guard/import-detector.js +498 -0
- package/lib/dep-guard/path-utils.js +13 -0
- package/lib/dep-guard/rubric.js +120 -0
- package/lib/dep-guard/task-parser.js +318 -0
- package/lib/plugin-catalog.js +18 -28
- package/lib/workflow-profiles.js +5 -11
- package/package.json +15 -4
- package/skills/parallel-deep-research/SKILL.md +108 -0
- package/skills/parallel-deep-research/evals/README.md +27 -0
- package/skills/parallel-deep-research/evals/evals.json +62 -0
- package/skills/sonarcloud-analysis/SKILL.md +171 -0
- package/skills/sonarcloud-analysis/evals/README.md +27 -0
- package/skills/sonarcloud-analysis/evals/evals.json +50 -0
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -0
- package/docs/planning/PROGRESS.md +0 -396
- package/docs/plans/.gitkeep +0 -0
- package/docs/plans/2026-02-27-forge-test-suite-v2-decisions.md +0 -21
- package/docs/plans/2026-02-27-forge-test-suite-v2-design.md +0 -362
- package/docs/plans/2026-02-27-forge-test-suite-v2-tasks.md +0 -343
- package/docs/plans/2026-03-02-superpowers-gaps-decisions.md +0 -26
- package/docs/plans/2026-03-02-superpowers-gaps-design.md +0 -239
- package/docs/plans/2026-03-02-superpowers-gaps-tasks.md +0 -260
- package/docs/plans/2026-03-04-agent-command-parity-design.md +0 -163
- package/docs/plans/2026-03-04-verify-worktree-cleanup-decisions.md +0 -7
- package/docs/plans/2026-03-04-verify-worktree-cleanup-design.md +0 -165
- package/docs/plans/2026-03-05-forge-uto-decisions.md +0 -6
- package/docs/plans/2026-03-05-forge-uto-design.md +0 -116
- package/docs/plans/2026-03-05-forge-uto-tasks.md +0 -244
- package/docs/plans/2026-03-10-command-creator-and-eval-decisions.md +0 -52
- package/docs/plans/2026-03-10-command-creator-and-eval-design.md +0 -350
- package/docs/plans/2026-03-10-command-creator-and-eval-tasks.md +0 -426
- package/docs/plans/2026-03-10-stale-workflow-refs-decisions.md +0 -8
- package/docs/plans/2026-03-10-stale-workflow-refs-design.md +0 -80
- package/docs/plans/2026-03-10-stale-workflow-refs-tasks.md +0 -90
- package/docs/plans/2026-03-14-beads-plan-context-decisions.md +0 -9
- package/docs/plans/2026-03-14-beads-plan-context-design.md +0 -171
- package/docs/plans/2026-03-14-beads-plan-context-tasks.md +0 -160
- package/docs/plans/2026-03-14-skill-eval-loop-decisions.md +0 -33
- package/docs/plans/2026-03-14-skill-eval-loop-design.md +0 -118
- package/docs/plans/2026-03-14-skill-eval-loop-results.md +0 -78
- package/docs/plans/2026-03-14-skill-eval-loop-tasks.md +0 -160
- package/docs/plans/2026-03-15-agent-command-parity-v2-decisions.md +0 -11
- package/docs/plans/2026-03-15-agent-command-parity-v2-design.md +0 -145
- package/docs/plans/2026-03-15-agent-command-parity-v2-tasks.md +0 -211
- package/docs/research/TEMPLATE.md +0 -292
- package/docs/research/advanced-testing.md +0 -297
- package/docs/research/agent-permissions.md +0 -167
- package/docs/research/dependency-chain.md +0 -328
- package/docs/research/forge-workflow-v2.md +0 -550
- package/docs/research/plugin-architecture.md +0 -772
- package/docs/research/pr4-cli-automation.md +0 -326
- package/docs/research/premerge-verify-restructure.md +0 -205
- package/docs/research/skills-restructure.md +0 -508
- package/docs/research/sonarcloud-perfection-plan.md +0 -166
- package/docs/research/sonarcloud-quality-gate.md +0 -184
- package/docs/research/superpowers-integration.md +0 -403
- package/docs/research/superpowers.md +0 -319
- package/docs/research/test-environment.md +0 -519
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const { detectContractType, extractTaskContracts } = require('./task-parser.js');
|
|
2
|
+
const { normalizeRepoPath } = require('./path-utils.js');
|
|
3
|
+
|
|
4
|
+
function isIdentifier(name) {
|
|
5
|
+
return typeof name === 'string' && /^[A-Za-z_]\w*$/.test(name);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isCommandContractName(name) {
|
|
9
|
+
return typeof name === 'string' && /^[A-Za-z_][\w-]*$/.test(name);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseContractToken(token) {
|
|
13
|
+
if (typeof token !== 'string') {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const trimmed = token.trim();
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let annotation = '';
|
|
23
|
+
let contractTarget = trimmed;
|
|
24
|
+
const openParenIndex = trimmed.lastIndexOf('(');
|
|
25
|
+
if (openParenIndex !== -1 && trimmed.endsWith(')')) {
|
|
26
|
+
annotation = trimmed.slice(openParenIndex + 1, -1);
|
|
27
|
+
contractTarget = trimmed.slice(0, openParenIndex);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const separatorIndex = contractTarget.lastIndexOf(':');
|
|
31
|
+
if (separatorIndex <= 0 || separatorIndex === contractTarget.length - 1) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const file = normalizeRepoPath(contractTarget.slice(0, separatorIndex));
|
|
36
|
+
const symbol = contractTarget.slice(separatorIndex + 1);
|
|
37
|
+
const contractType = detectContractType(symbol, annotation);
|
|
38
|
+
|
|
39
|
+
const validSymbol = contractType === 'command-contract'
|
|
40
|
+
? isCommandContractName(symbol)
|
|
41
|
+
: isIdentifier(symbol);
|
|
42
|
+
if (!validSymbol) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
contract: `${file}:${symbol}(${contractType})`,
|
|
48
|
+
file,
|
|
49
|
+
symbol,
|
|
50
|
+
contractType,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseStoredContractsFromNotes(notes = '') {
|
|
55
|
+
if (!notes || typeof notes !== 'string') {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const contractLines = notes
|
|
60
|
+
.split(/\r?\n/)
|
|
61
|
+
.filter((line) => line.startsWith('contracts@') && line.includes(': '));
|
|
62
|
+
if (contractLines.length === 0) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
contractLines.sort((left, right) => left.localeCompare(right));
|
|
67
|
+
const latestLine = contractLines.at(-1);
|
|
68
|
+
const latestPayload = latestLine.slice(latestLine.indexOf(': ') + 2);
|
|
69
|
+
|
|
70
|
+
return latestPayload
|
|
71
|
+
.split(/\s+/)
|
|
72
|
+
.map((token) => token.trim())
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function mergeEvidence(evidence) {
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
return evidence.filter((item) => {
|
|
79
|
+
const key = `${item.contract}:${item.storedContract}:${item.targetIssueId}`;
|
|
80
|
+
if (seen.has(key)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
seen.add(key);
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function scoreContractDependencies(normalizedInput) {
|
|
89
|
+
const currentContracts = extractTaskContracts(normalizedInput.taskContext, {
|
|
90
|
+
repositoryRoot: normalizedInput.repositoryRoot,
|
|
91
|
+
})
|
|
92
|
+
.map(parseContractToken)
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
|
|
95
|
+
if (currentContracts.length === 0) {
|
|
96
|
+
return {
|
|
97
|
+
score: 0,
|
|
98
|
+
findings: [],
|
|
99
|
+
evidence: [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const findings = [];
|
|
104
|
+
const evidence = [];
|
|
105
|
+
|
|
106
|
+
for (const issue of normalizedInput.openIssues) {
|
|
107
|
+
const issueContracts = Array.from(new Set([
|
|
108
|
+
...issue.contracts,
|
|
109
|
+
...parseStoredContractsFromNotes(issue.notes),
|
|
110
|
+
]))
|
|
111
|
+
.map(parseContractToken)
|
|
112
|
+
.filter(Boolean);
|
|
113
|
+
|
|
114
|
+
const issueEvidence = [];
|
|
115
|
+
for (const currentContract of currentContracts) {
|
|
116
|
+
for (const storedContract of issueContracts) {
|
|
117
|
+
if (
|
|
118
|
+
currentContract.file !== storedContract.file
|
|
119
|
+
|| currentContract.symbol !== storedContract.symbol
|
|
120
|
+
|| currentContract.contractType !== storedContract.contractType
|
|
121
|
+
) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
issueEvidence.push({
|
|
126
|
+
type: 'contract',
|
|
127
|
+
sourceIssueId: normalizedInput.currentIssue.id,
|
|
128
|
+
targetIssueId: issue.id,
|
|
129
|
+
contract: currentContract.contract,
|
|
130
|
+
storedContract: storedContract.contract,
|
|
131
|
+
symbol: currentContract.symbol,
|
|
132
|
+
contractType: currentContract.contractType,
|
|
133
|
+
scoreContribution: 1,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (issueEvidence.length === 0) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const mergedEvidence = mergeEvidence(issueEvidence);
|
|
143
|
+
findings.push({
|
|
144
|
+
sourceIssueId: normalizedInput.currentIssue.id,
|
|
145
|
+
targetIssueId: issue.id,
|
|
146
|
+
score: mergedEvidence.length,
|
|
147
|
+
evidence: mergedEvidence,
|
|
148
|
+
});
|
|
149
|
+
evidence.push(...mergedEvidence);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
score: findings.reduce((total, finding) => total + finding.score, 0),
|
|
154
|
+
findings,
|
|
155
|
+
evidence: mergeEvidence(evidence),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
parseStoredContractsFromNotes,
|
|
161
|
+
scoreContractDependencies,
|
|
162
|
+
};
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { parse } = require('@babel/parser');
|
|
4
|
+
const { normalizeRepoPath } = require('./path-utils.js');
|
|
5
|
+
const { extractSymbols } = require('./task-parser.js');
|
|
6
|
+
|
|
7
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.cjs', '.mjs', '.jsx', '.ts', '.tsx']);
|
|
8
|
+
const DEFAULT_EXCLUDED_DIRECTORIES = new Set([
|
|
9
|
+
'.git',
|
|
10
|
+
'.worktrees',
|
|
11
|
+
'coverage',
|
|
12
|
+
'dist',
|
|
13
|
+
'build',
|
|
14
|
+
'docs',
|
|
15
|
+
'fixtures',
|
|
16
|
+
'node_modules',
|
|
17
|
+
'test',
|
|
18
|
+
'test-env',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function isIdentifierStart(character) {
|
|
22
|
+
return /[A-Za-z_]/.test(character);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isIdentifierPart(character) {
|
|
26
|
+
return /\w/.test(character);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listSourceFiles(rootDir) {
|
|
30
|
+
const results = [];
|
|
31
|
+
const queue = [rootDir];
|
|
32
|
+
|
|
33
|
+
while (queue.length > 0) {
|
|
34
|
+
const current = queue.pop();
|
|
35
|
+
let entries;
|
|
36
|
+
try {
|
|
37
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
38
|
+
} catch (_error) {
|
|
39
|
+
// Inaccessible directory (permissions, broken symlink) — skip silently
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (entry.isDirectory() && DEFAULT_EXCLUDED_DIRECTORIES.has(entry.name)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const absolutePath = path.join(current, entry.name);
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
queue.push(absolutePath);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
55
|
+
results.push(absolutePath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractTargetSymbols(taskContext) {
|
|
64
|
+
const symbols = new Set();
|
|
65
|
+
|
|
66
|
+
for (const task of taskContext.tasks) {
|
|
67
|
+
for (const symbol of extractSymbols(task.whatToImplement ?? '')) {
|
|
68
|
+
if (symbol && isIdentifierStart(symbol[0]) && Array.from(symbol).every(isIdentifierPart)) {
|
|
69
|
+
symbols.add(symbol);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return symbols;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractTargetFiles(taskContext) {
|
|
78
|
+
const files = new Set();
|
|
79
|
+
|
|
80
|
+
for (const task of taskContext.tasks) {
|
|
81
|
+
for (const file of task.files) {
|
|
82
|
+
files.add(normalizeRepoPath(file));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return files;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveImportSource(sourceFile, specifier, repositoryRoot) {
|
|
90
|
+
if (!specifier || typeof specifier !== 'string' || !specifier.startsWith('.')) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const basePath = path.resolve(path.dirname(sourceFile), specifier);
|
|
95
|
+
const candidates = [
|
|
96
|
+
basePath,
|
|
97
|
+
`${basePath}.js`,
|
|
98
|
+
`${basePath}.cjs`,
|
|
99
|
+
`${basePath}.mjs`,
|
|
100
|
+
`${basePath}.jsx`,
|
|
101
|
+
`${basePath}.ts`,
|
|
102
|
+
`${basePath}.tsx`,
|
|
103
|
+
path.join(basePath, 'index.js'),
|
|
104
|
+
path.join(basePath, 'index.cjs'),
|
|
105
|
+
path.join(basePath, 'index.mjs'),
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
for (const candidate of candidates) {
|
|
109
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
110
|
+
return normalizeRepoPath(path.relative(repositoryRoot, candidate));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isAstNode(value) {
|
|
118
|
+
return Boolean(value) && typeof value === 'object' && typeof value.type === 'string';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function walkNode(node, visitor) {
|
|
122
|
+
if (!isAstNode(node)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
visitor(node);
|
|
127
|
+
|
|
128
|
+
for (const value of Object.values(node)) {
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
for (const item of value) {
|
|
131
|
+
if (isAstNode(item)) {
|
|
132
|
+
walkNode(item, visitor);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (isAstNode(value)) {
|
|
139
|
+
walkNode(value, visitor);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectFileAnalysis(filePath, repositoryRoot) {
|
|
145
|
+
const relativeFile = normalizeRepoPath(path.relative(repositoryRoot, filePath));
|
|
146
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
147
|
+
const ast = parse(content, {
|
|
148
|
+
sourceType: 'unambiguous',
|
|
149
|
+
errorRecovery: true,
|
|
150
|
+
plugins: ['jsx', 'typescript'],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const directImports = new Map();
|
|
154
|
+
const defaultImports = new Map();
|
|
155
|
+
const namespaceImports = new Map();
|
|
156
|
+
const directCalls = new Set();
|
|
157
|
+
const memberCalls = [];
|
|
158
|
+
const defaultExportSymbols = new Set();
|
|
159
|
+
|
|
160
|
+
walkNode(ast, (node) => {
|
|
161
|
+
recordImportDeclaration(node, filePath, repositoryRoot, directImports, defaultImports, namespaceImports);
|
|
162
|
+
recordRequireDeclarator(node, filePath, repositoryRoot, directImports, defaultImports, namespaceImports);
|
|
163
|
+
recordCallExpression(node, directCalls, memberCalls);
|
|
164
|
+
recordDefaultExport(node, defaultExportSymbols);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
file: relativeFile,
|
|
169
|
+
directImports,
|
|
170
|
+
defaultImports,
|
|
171
|
+
namespaceImports,
|
|
172
|
+
directCalls,
|
|
173
|
+
memberCalls,
|
|
174
|
+
defaultExportSymbols,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function recordImportDeclaration(node, filePath, repositoryRoot, directImports, defaultImports, namespaceImports) {
|
|
179
|
+
if (node.type !== 'ImportDeclaration') {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const resolvedSource = resolveImportSource(filePath, node.source?.value, repositoryRoot);
|
|
184
|
+
if (!resolvedSource) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const specifier of node.specifiers ?? []) {
|
|
189
|
+
if (specifier.type === 'ImportSpecifier') {
|
|
190
|
+
directImports.set(specifier.local.name, {
|
|
191
|
+
sourceFile: resolvedSource,
|
|
192
|
+
importedName: specifier.imported.name,
|
|
193
|
+
});
|
|
194
|
+
} else if (specifier.type === 'ImportDefaultSpecifier' && specifier.local?.name) {
|
|
195
|
+
defaultImports.set(specifier.local.name, {
|
|
196
|
+
sourceFile: resolvedSource,
|
|
197
|
+
});
|
|
198
|
+
} else if (specifier.local?.name) {
|
|
199
|
+
namespaceImports.set(specifier.local.name, {
|
|
200
|
+
sourceFile: resolvedSource,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function recordRequireDeclarator(node, filePath, repositoryRoot, directImports, defaultImports, namespaceImports) {
|
|
207
|
+
if (
|
|
208
|
+
node.type !== 'VariableDeclarator'
|
|
209
|
+
|| node.init?.type !== 'CallExpression'
|
|
210
|
+
|| node.init.callee?.type !== 'Identifier'
|
|
211
|
+
|| node.init.callee.name !== 'require'
|
|
212
|
+
|| node.init.arguments?.[0]?.type !== 'StringLiteral'
|
|
213
|
+
) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const resolvedSource = resolveImportSource(
|
|
218
|
+
filePath,
|
|
219
|
+
node.init.arguments[0].value,
|
|
220
|
+
repositoryRoot,
|
|
221
|
+
);
|
|
222
|
+
if (!resolvedSource) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (node.id.type === 'ObjectPattern') {
|
|
227
|
+
for (const property of node.id.properties ?? []) {
|
|
228
|
+
if (property.type !== 'ObjectProperty' || property.value?.type !== 'Identifier') {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const importedName = property.key?.name ?? property.key?.value;
|
|
233
|
+
if (!importedName) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
directImports.set(property.value.name, {
|
|
238
|
+
sourceFile: resolvedSource,
|
|
239
|
+
importedName,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (node.id.type === 'Identifier') {
|
|
246
|
+
defaultImports.set(node.id.name, {
|
|
247
|
+
sourceFile: resolvedSource,
|
|
248
|
+
});
|
|
249
|
+
namespaceImports.set(node.id.name, {
|
|
250
|
+
sourceFile: resolvedSource,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function recordCallExpression(node, directCalls, memberCalls) {
|
|
256
|
+
if (node.type !== 'CallExpression') {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (node.callee?.type === 'Identifier') {
|
|
261
|
+
directCalls.add(node.callee.name);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
node.callee?.type === 'MemberExpression'
|
|
267
|
+
&& node.callee.object?.type === 'Identifier'
|
|
268
|
+
&& !node.callee.computed
|
|
269
|
+
&& node.callee.property?.type === 'Identifier'
|
|
270
|
+
) {
|
|
271
|
+
memberCalls.push({
|
|
272
|
+
objectName: node.callee.object.name,
|
|
273
|
+
propertyName: node.callee.property.name,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function recordDefaultExport(node, defaultExportSymbols) {
|
|
279
|
+
if (node.type === 'ExportDefaultDeclaration') {
|
|
280
|
+
if (node.declaration?.type === 'Identifier') {
|
|
281
|
+
defaultExportSymbols.add(node.declaration.name);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (
|
|
286
|
+
(node.declaration?.type === 'FunctionDeclaration' || node.declaration?.type === 'ClassDeclaration')
|
|
287
|
+
&& node.declaration.id?.name
|
|
288
|
+
) {
|
|
289
|
+
defaultExportSymbols.add(node.declaration.id.name);
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (
|
|
295
|
+
node.type === 'AssignmentExpression'
|
|
296
|
+
&& node.operator === '='
|
|
297
|
+
&& node.left?.type === 'MemberExpression'
|
|
298
|
+
&& node.left.object?.type === 'Identifier'
|
|
299
|
+
&& node.left.object.name === 'module'
|
|
300
|
+
&& !node.left.computed
|
|
301
|
+
&& node.left.property?.type === 'Identifier'
|
|
302
|
+
&& node.left.property.name === 'exports'
|
|
303
|
+
) {
|
|
304
|
+
if (node.right?.type === 'Identifier') {
|
|
305
|
+
defaultExportSymbols.add(node.right.name);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (
|
|
310
|
+
(node.right?.type === 'FunctionExpression' || node.right?.type === 'ClassExpression')
|
|
311
|
+
&& node.right.id?.name
|
|
312
|
+
) {
|
|
313
|
+
defaultExportSymbols.add(node.right.id.name);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function issueTouchesFile(issue, filePath) {
|
|
319
|
+
if (issue.files.includes(filePath)) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return issue.contracts.some((contract) => typeof contract === 'string' && contract.startsWith(`${filePath}:`));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function mergeEvidence(evidence) {
|
|
327
|
+
const seen = new Set();
|
|
328
|
+
return evidence.filter((item) => {
|
|
329
|
+
const key = `${item.type}:${item.consumerFile}:${item.targetFile}:${item.symbol}`;
|
|
330
|
+
if (seen.has(key)) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
seen.add(key);
|
|
334
|
+
return true;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function collectDirectImportEvidence(analysis, targetFiles, targetSymbols) {
|
|
339
|
+
const evidence = [];
|
|
340
|
+
for (const [localName, importMeta] of analysis.directImports.entries()) {
|
|
341
|
+
if (!targetFiles.has(importMeta.sourceFile) || !targetSymbols.has(importMeta.importedName)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
evidence.push({
|
|
346
|
+
type: 'import',
|
|
347
|
+
consumerFile: analysis.file,
|
|
348
|
+
sourceFile: analysis.file,
|
|
349
|
+
scoreContribution: 1,
|
|
350
|
+
targetFile: importMeta.sourceFile,
|
|
351
|
+
symbol: importMeta.importedName,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (analysis.directCalls.has(localName)) {
|
|
355
|
+
evidence.push({
|
|
356
|
+
type: 'call',
|
|
357
|
+
consumerFile: analysis.file,
|
|
358
|
+
sourceFile: analysis.file,
|
|
359
|
+
scoreContribution: 1,
|
|
360
|
+
targetFile: importMeta.sourceFile,
|
|
361
|
+
symbol: importMeta.importedName,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return evidence;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function collectDefaultImportEvidence(analysis, targetFiles, targetSymbols, defaultExportSymbolsByFile) {
|
|
369
|
+
const evidence = [];
|
|
370
|
+
for (const [localName, importMeta] of analysis.defaultImports.entries()) {
|
|
371
|
+
if (!targetFiles.has(importMeta.sourceFile)) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const defaultExportSymbols = defaultExportSymbolsByFile.get(importMeta.sourceFile) ?? new Set();
|
|
376
|
+
const matchedSymbol = targetSymbols.has(localName)
|
|
377
|
+
? localName
|
|
378
|
+
: Array.from(defaultExportSymbols).find((symbol) => targetSymbols.has(symbol));
|
|
379
|
+
|
|
380
|
+
if (!matchedSymbol) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
evidence.push({
|
|
385
|
+
type: 'import',
|
|
386
|
+
consumerFile: analysis.file,
|
|
387
|
+
sourceFile: analysis.file,
|
|
388
|
+
scoreContribution: 1,
|
|
389
|
+
targetFile: importMeta.sourceFile,
|
|
390
|
+
symbol: matchedSymbol,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (analysis.directCalls.has(localName)) {
|
|
394
|
+
evidence.push({
|
|
395
|
+
type: 'call',
|
|
396
|
+
consumerFile: analysis.file,
|
|
397
|
+
sourceFile: analysis.file,
|
|
398
|
+
scoreContribution: 1,
|
|
399
|
+
targetFile: importMeta.sourceFile,
|
|
400
|
+
symbol: matchedSymbol,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return evidence;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function collectMemberCallEvidence(analysis, targetFiles, targetSymbols) {
|
|
408
|
+
const evidence = [];
|
|
409
|
+
for (const memberCall of analysis.memberCalls) {
|
|
410
|
+
const namespaceImport = analysis.namespaceImports.get(memberCall.objectName);
|
|
411
|
+
if (!namespaceImport) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!targetFiles.has(namespaceImport.sourceFile) || !targetSymbols.has(memberCall.propertyName)) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
evidence.push({
|
|
420
|
+
type: 'call',
|
|
421
|
+
consumerFile: analysis.file,
|
|
422
|
+
sourceFile: analysis.file,
|
|
423
|
+
scoreContribution: 1,
|
|
424
|
+
targetFile: namespaceImport.sourceFile,
|
|
425
|
+
symbol: memberCall.propertyName,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
return evidence;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function scoreImportDependencies(normalizedInput) {
|
|
432
|
+
const targetSymbols = extractTargetSymbols(normalizedInput.taskContext);
|
|
433
|
+
const targetFiles = extractTargetFiles(normalizedInput.taskContext);
|
|
434
|
+
|
|
435
|
+
if (targetSymbols.size === 0 || targetFiles.size === 0) {
|
|
436
|
+
return {
|
|
437
|
+
score: 0,
|
|
438
|
+
findings: [],
|
|
439
|
+
evidence: [],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const findings = [];
|
|
444
|
+
const evidence = [];
|
|
445
|
+
const analyses = [];
|
|
446
|
+
|
|
447
|
+
for (const absoluteFile of listSourceFiles(normalizedInput.repositoryRoot)) {
|
|
448
|
+
let analysis;
|
|
449
|
+
try {
|
|
450
|
+
analysis = collectFileAnalysis(absoluteFile, normalizedInput.repositoryRoot);
|
|
451
|
+
} catch (_error) {
|
|
452
|
+
// Unparseable file (binary, syntax error) — skip silently
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
analyses.push(analysis);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const defaultExportSymbolsByFile = new Map(
|
|
459
|
+
analyses.map((analysis) => [analysis.file, analysis.defaultExportSymbols]),
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
for (const analysis of analyses) {
|
|
463
|
+
const fileEvidence = [
|
|
464
|
+
...collectDirectImportEvidence(analysis, targetFiles, targetSymbols),
|
|
465
|
+
...collectDefaultImportEvidence(analysis, targetFiles, targetSymbols, defaultExportSymbolsByFile),
|
|
466
|
+
...collectMemberCallEvidence(analysis, targetFiles, targetSymbols),
|
|
467
|
+
];
|
|
468
|
+
|
|
469
|
+
if (fileEvidence.length === 0) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const touchedIssues = normalizedInput.openIssues.filter((issue) => issueTouchesFile(issue, analysis.file));
|
|
474
|
+
for (const issue of touchedIssues) {
|
|
475
|
+
const issueEvidence = mergeEvidence(fileEvidence);
|
|
476
|
+
const score = issueEvidence.length;
|
|
477
|
+
|
|
478
|
+
findings.push({
|
|
479
|
+
sourceIssueId: normalizedInput.currentIssue.id,
|
|
480
|
+
targetIssueId: issue.id,
|
|
481
|
+
score,
|
|
482
|
+
evidence: issueEvidence,
|
|
483
|
+
});
|
|
484
|
+
evidence.push(...issueEvidence);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
score: findings.reduce((total, finding) => total + finding.score, 0),
|
|
490
|
+
findings,
|
|
491
|
+
evidence: mergeEvidence(evidence),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
module.exports = {
|
|
496
|
+
normalizeRepoPath,
|
|
497
|
+
scoreImportDependencies,
|
|
498
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
function toPosixPath(filePath) {
|
|
4
|
+
return filePath.replaceAll('\\', '/').split(path.sep).join('/');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function normalizeRepoPath(filePath) {
|
|
8
|
+
return toPosixPath(path.normalize(filePath));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
normalizeRepoPath,
|
|
13
|
+
};
|