forge-workflow 0.0.2 → 0.0.4
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/dev.md +26 -0
- package/.claude/commands/plan.md +141 -9
- package/.claude/commands/premerge.md +0 -3
- package/.claude/commands/rollback.md +4 -4
- package/.claude/commands/ship.md +71 -41
- package/.claude/commands/status.md +9 -38
- package/.claude/commands/validate.md +47 -2
- package/.cline/workflows/dev.md +337 -0
- package/.cline/workflows/plan.md +518 -0
- package/.cline/workflows/premerge.md +173 -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 +161 -0
- package/.cline/workflows/sonarcloud.md +146 -0
- package/.cline/workflows/status.md +45 -0
- package/.cline/workflows/validate.md +279 -0
- package/.cline/workflows/verify.md +218 -0
- package/.codex/config.toml +11 -0
- package/.codex/skills/dev/SKILL.md +340 -0
- package/.codex/skills/plan/SKILL.md +521 -0
- package/.codex/skills/premerge/SKILL.md +176 -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 +164 -0
- package/.codex/skills/sonarcloud/SKILL.md +149 -0
- package/.codex/skills/status/SKILL.md +48 -0
- package/.codex/skills/validate/SKILL.md +282 -0
- package/.codex/skills/verify/SKILL.md +221 -0
- package/.cursor/commands/dev.md +337 -0
- package/.cursor/commands/plan.md +518 -0
- package/.cursor/commands/premerge.md +173 -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 +161 -0
- package/.cursor/commands/sonarcloud.md +146 -0
- package/.cursor/commands/status.md +45 -0
- package/.cursor/commands/validate.md +279 -0
- package/.cursor/commands/verify.md +218 -0
- package/.cursor/hooks/state/continual-learning-index.json +19 -0
- package/.cursor/hooks/state/continual-learning.json +8 -0
- package/.cursor/rules/permissions-guidance.mdc +37 -0
- package/.github/prompts/dev.prompt.md +342 -0
- package/.github/prompts/plan.prompt.md +523 -0
- package/.github/prompts/premerge.prompt.md +178 -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 +166 -0
- package/.github/prompts/sonarcloud.prompt.md +151 -0
- package/.github/prompts/status.prompt.md +50 -0
- package/.github/prompts/validate.prompt.md +284 -0
- package/.github/prompts/verify.prompt.md +223 -0
- package/.kilocode/workflows/dev.md +341 -0
- package/.kilocode/workflows/plan.md +522 -0
- package/.kilocode/workflows/premerge.md +177 -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 +165 -0
- package/.kilocode/workflows/sonarcloud.md +150 -0
- package/.kilocode/workflows/status.md +49 -0
- package/.kilocode/workflows/validate.md +283 -0
- package/.kilocode/workflows/verify.md +222 -0
- package/.opencode/commands/dev.md +340 -0
- package/.opencode/commands/plan.md +521 -0
- package/.opencode/commands/premerge.md +176 -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 +164 -0
- package/.opencode/commands/sonarcloud.md +149 -0
- package/.opencode/commands/status.md +48 -0
- package/.opencode/commands/validate.md +282 -0
- package/.opencode/commands/verify.md +221 -0
- package/.roo/commands/dev.md +341 -0
- package/.roo/commands/plan.md +522 -0
- package/.roo/commands/premerge.md +177 -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 +165 -0
- package/.roo/commands/sonarcloud.md +150 -0
- package/.roo/commands/status.md +49 -0
- package/.roo/commands/validate.md +283 -0
- package/.roo/commands/verify.md +222 -0
- package/AGENTS.md +7 -1
- package/CLAUDE.md +5 -4
- package/README.md +21 -19
- package/bin/{forge-validate.js → forge-preflight.js} +21 -15
- package/bin/forge.js +209 -138
- package/docs/AGENT_INSTALL_PROMPT.md +1 -1
- package/docs/BEADS_GITHUB_SYNC.md +251 -0
- package/docs/ENHANCED_ONBOARDING.md +8 -8
- package/docs/EXAMPLES.md +4 -4
- package/docs/GREPTILE_SETUP.md +1 -1
- package/docs/MANUAL_REVIEW_GUIDE.md +1 -1
- package/docs/ROADMAP.md +6 -6
- package/docs/SETUP.md +1 -2
- package/docs/TOOLCHAIN.md +15 -234
- package/docs/VALIDATION.md +11 -11
- package/install.sh +33 -39
- package/lib/agents-config.js +3 -3
- 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/detect-agent.js +191 -0
- package/lib/detect-worktree.js +47 -0
- package/lib/file-hash.js +26 -0
- package/lib/plugin-catalog.js +18 -28
- package/lib/setup-action-log.js +139 -0
- package/lib/setup-summary-renderer.js +106 -0
- package/lib/setup.js +75 -1
- package/lib/workflow-profiles.js +5 -11
- package/package.json +17 -7
- 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/WORKFLOW.md +0 -400
- 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
package/lib/commands/plan.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plan Command -
|
|
2
|
+
* Plan Command - Beads Integration
|
|
3
3
|
* Creates implementation plan after research is complete
|
|
4
4
|
*
|
|
5
5
|
* Security: Uses execFileSync instead of exec/execSync to prevent command injection
|
|
@@ -123,18 +123,18 @@ function readResearchDoc(featureSlug) {
|
|
|
123
123
|
* 3. Default to tactical if no clear signals
|
|
124
124
|
*
|
|
125
125
|
* @param {string} researchContent - Research document markdown content
|
|
126
|
-
* @returns {{type: 'tactical'|'strategic',
|
|
126
|
+
* @returns {{type: 'tactical'|'strategic', requiresDesignDoc: boolean, reason: string}} Scope analysis result
|
|
127
127
|
* @example
|
|
128
128
|
* const scope = detectScope(researchMarkdown);
|
|
129
129
|
* if (scope.type === 'strategic') {
|
|
130
|
-
* console.log('Requires
|
|
130
|
+
* console.log('Requires design doc:', scope.reason);
|
|
131
131
|
* }
|
|
132
132
|
*/
|
|
133
133
|
function detectScope(researchContent) {
|
|
134
134
|
if (!researchContent || typeof researchContent !== 'string') {
|
|
135
135
|
return {
|
|
136
136
|
type: 'tactical',
|
|
137
|
-
|
|
137
|
+
requiresDesignDoc: false,
|
|
138
138
|
reason: 'Invalid research content (defaulting to tactical)',
|
|
139
139
|
};
|
|
140
140
|
}
|
|
@@ -166,7 +166,7 @@ function detectScope(researchContent) {
|
|
|
166
166
|
const type = scopeMatch[1].toLowerCase();
|
|
167
167
|
return {
|
|
168
168
|
type,
|
|
169
|
-
|
|
169
|
+
requiresDesignDoc: type === 'strategic',
|
|
170
170
|
reason: `Explicit scope declaration: ${scopeMatch[1]}`,
|
|
171
171
|
};
|
|
172
172
|
}
|
|
@@ -190,7 +190,7 @@ function detectScope(researchContent) {
|
|
|
190
190
|
if (foundKeywords.length > 0) {
|
|
191
191
|
return {
|
|
192
192
|
type: 'strategic',
|
|
193
|
-
|
|
193
|
+
requiresDesignDoc: true,
|
|
194
194
|
reason: `Strategic keywords detected: ${foundKeywords.join(', ')}`,
|
|
195
195
|
};
|
|
196
196
|
}
|
|
@@ -198,7 +198,7 @@ function detectScope(researchContent) {
|
|
|
198
198
|
// Default to tactical (safe fallback)
|
|
199
199
|
return {
|
|
200
200
|
type: 'tactical',
|
|
201
|
-
|
|
201
|
+
requiresDesignDoc: false,
|
|
202
202
|
reason: 'No strategic indicators found (tactical by default)',
|
|
203
203
|
};
|
|
204
204
|
}
|
|
@@ -244,7 +244,7 @@ function createBeadsIssue(featureName, researchPath, scope) {
|
|
|
244
244
|
if (!slugValidation.valid) {
|
|
245
245
|
return { success: false, error: `Cannot generate valid slug from feature name: ${slugValidation.error}` };
|
|
246
246
|
}
|
|
247
|
-
description += `\n\
|
|
247
|
+
description += `\n\nDesign: docs/plans/${featureSlug}-design.md`;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// Execute bd create command using execFileSync for safety (OWASP A03)
|
|
@@ -585,12 +585,9 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
585
585
|
* Strategic workflow (architecture changes, >1 day):
|
|
586
586
|
* 1. Read research document
|
|
587
587
|
* 2. Detect scope (strategic)
|
|
588
|
-
* 3. Create Beads issue with
|
|
588
|
+
* 3. Create Beads issue with design doc link
|
|
589
589
|
* 4. Create feature branch
|
|
590
|
-
*
|
|
591
|
-
* 6. Commit and push proposal
|
|
592
|
-
* 7. Create proposal PR
|
|
593
|
-
* → Next: Wait for proposal approval
|
|
590
|
+
* → Next: Create design doc, then /dev command
|
|
594
591
|
*
|
|
595
592
|
* @param {string} featureName - Feature name (human-readable, e.g., "Payment Integration")
|
|
596
593
|
* @returns {Promise<{
|
|
@@ -598,8 +595,7 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
598
595
|
* scope?: 'tactical'|'strategic',
|
|
599
596
|
* beadsIssueId?: string,
|
|
600
597
|
* branchName?: string,
|
|
601
|
-
*
|
|
602
|
-
* proposalPR?: {url: string, number: number},
|
|
598
|
+
*
|
|
603
599
|
* summary?: string,
|
|
604
600
|
* nextCommand?: string,
|
|
605
601
|
* error?: string
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
const { scoreImportDependencies } = require('./import-detector.js');
|
|
2
|
+
const { parseTaskFile } = require('./task-parser.js');
|
|
3
|
+
const { scoreContractDependencies } = require('./contract-detector.js');
|
|
4
|
+
const { scoreBehavioralDependencies } = require('./behavior-detector.js');
|
|
5
|
+
const { WEIGHTS: RUBRIC_WEIGHTS, aggregateRubricScores, needsUserEscalation } = require('./rubric.js');
|
|
6
|
+
const { normalizeRepoPath } = require('./path-utils.js');
|
|
7
|
+
|
|
8
|
+
const CONFIDENCE_THRESHOLD = 0.7;
|
|
9
|
+
const DETECTOR_KEYS = Object.freeze([
|
|
10
|
+
'importCallChain',
|
|
11
|
+
'contractDependencies',
|
|
12
|
+
'behavioralDependencies',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function createZeroedDetectorMap() {
|
|
16
|
+
return DETECTOR_KEYS.reduce((result, key) => {
|
|
17
|
+
result[key] = 0;
|
|
18
|
+
return result;
|
|
19
|
+
}, {});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createEmptyDetectorEvidence() {
|
|
23
|
+
return DETECTOR_KEYS.reduce((result, key) => {
|
|
24
|
+
result[key] = [];
|
|
25
|
+
return result;
|
|
26
|
+
}, {});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeContractToken(contract) {
|
|
30
|
+
if (typeof contract !== 'string') {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const separatorIndex = contract.indexOf(':');
|
|
35
|
+
if (separatorIndex === -1) {
|
|
36
|
+
return contract;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return `${normalizeRepoPath(contract.slice(0, separatorIndex))}${contract.slice(separatorIndex)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeContractNotes(notes = '') {
|
|
43
|
+
return notes
|
|
44
|
+
.split(/\r?\n/)
|
|
45
|
+
.map((line) => {
|
|
46
|
+
const marker = ': ';
|
|
47
|
+
if (!line.startsWith('contracts@') || !line.includes(marker)) {
|
|
48
|
+
return line;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const separatorIndex = line.indexOf(marker);
|
|
52
|
+
const prefix = line.slice(0, separatorIndex + marker.length);
|
|
53
|
+
const payload = line.slice(separatorIndex + marker.length);
|
|
54
|
+
const normalizedPayload = payload
|
|
55
|
+
.split(/\s+/)
|
|
56
|
+
.map(normalizeContractToken)
|
|
57
|
+
.join(' ');
|
|
58
|
+
return `${prefix}${normalizedPayload}`;
|
|
59
|
+
})
|
|
60
|
+
.join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeIssue(issue, label) {
|
|
64
|
+
if (!issue || typeof issue !== 'object') {
|
|
65
|
+
throw new Error(`${label} is required`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!issue.id || typeof issue.id !== 'string') {
|
|
69
|
+
throw new Error(`${label} id is required`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
id: issue.id,
|
|
74
|
+
title: typeof issue.title === 'string' ? issue.title : '',
|
|
75
|
+
description: typeof issue.description === 'string' ? issue.description : '',
|
|
76
|
+
status: typeof issue.status === 'string' ? issue.status : '',
|
|
77
|
+
contracts: Array.isArray(issue.contracts)
|
|
78
|
+
? issue.contracts.map(normalizeContractToken).filter((contract) => typeof contract === 'string')
|
|
79
|
+
: [],
|
|
80
|
+
files: Array.isArray(issue.files)
|
|
81
|
+
? issue.files
|
|
82
|
+
.filter((file) => typeof file === 'string')
|
|
83
|
+
.map((file) => normalizeRepoPath(file))
|
|
84
|
+
: [],
|
|
85
|
+
notes: typeof issue.notes === 'string' ? normalizeContractNotes(issue.notes) : '',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizePhase3Input(input = {}) {
|
|
90
|
+
const currentIssue = normalizeIssue(input.currentIssue, 'Current issue');
|
|
91
|
+
const openIssues = Array.isArray(input.openIssues)
|
|
92
|
+
? input.openIssues.map((issue) => normalizeIssue(issue, 'Open issue'))
|
|
93
|
+
: [];
|
|
94
|
+
const taskContext = input.taskContext ?? parseTaskFile(input.taskFile);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
currentIssue,
|
|
98
|
+
openIssues,
|
|
99
|
+
taskContext,
|
|
100
|
+
repositoryRoot: typeof input.repositoryRoot === 'string' && input.repositoryRoot
|
|
101
|
+
? input.repositoryRoot
|
|
102
|
+
: process.cwd(),
|
|
103
|
+
thresholds: {
|
|
104
|
+
confidence: CONFIDENCE_THRESHOLD,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createScaffoldResult(normalizedInput) {
|
|
110
|
+
const detectorScores = createZeroedDetectorMap();
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
currentIssue: normalizedInput.currentIssue,
|
|
114
|
+
taskContext: normalizedInput.taskContext,
|
|
115
|
+
issues: [],
|
|
116
|
+
scores: {
|
|
117
|
+
...detectorScores,
|
|
118
|
+
rubric: 0,
|
|
119
|
+
},
|
|
120
|
+
rubric: {
|
|
121
|
+
score: 0,
|
|
122
|
+
threshold: normalizedInput.thresholds.confidence,
|
|
123
|
+
summary: 'No detector findings yet.',
|
|
124
|
+
weights: RUBRIC_WEIGHTS,
|
|
125
|
+
reasons: [],
|
|
126
|
+
},
|
|
127
|
+
detectorEvidence: createEmptyDetectorEvidence(),
|
|
128
|
+
detectorConflicts: [],
|
|
129
|
+
importCallChain: {
|
|
130
|
+
score: 0,
|
|
131
|
+
evidence: [],
|
|
132
|
+
findings: [],
|
|
133
|
+
},
|
|
134
|
+
contractDependencies: {
|
|
135
|
+
score: 0,
|
|
136
|
+
evidence: [],
|
|
137
|
+
findings: [],
|
|
138
|
+
},
|
|
139
|
+
behavioralDependencies: {
|
|
140
|
+
score: 0,
|
|
141
|
+
evidence: [],
|
|
142
|
+
findings: [],
|
|
143
|
+
uncertain: false,
|
|
144
|
+
},
|
|
145
|
+
confidence: {
|
|
146
|
+
score: 1,
|
|
147
|
+
threshold: normalizedInput.thresholds.confidence,
|
|
148
|
+
belowThreshold: false,
|
|
149
|
+
reasons: [],
|
|
150
|
+
},
|
|
151
|
+
proposals: [],
|
|
152
|
+
needsUserDecision: false,
|
|
153
|
+
metadata: {
|
|
154
|
+
analyzerVersion: 'phase3-v1-scaffold',
|
|
155
|
+
parser: '@babel/parser',
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function inferDetectorType(finding) {
|
|
161
|
+
if (finding.behavioralSignal) {
|
|
162
|
+
return 'behavioralDependencies';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const firstEvidenceType = finding.evidence?.[0]?.type;
|
|
166
|
+
if (firstEvidenceType === 'contract') {
|
|
167
|
+
return 'contractDependencies';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return 'importCallChain';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatDetectorList(detectorTypes) {
|
|
174
|
+
const labels = detectorTypes.map((type) => {
|
|
175
|
+
if (type === 'importCallChain') {
|
|
176
|
+
return 'import/call-chain evidence';
|
|
177
|
+
}
|
|
178
|
+
if (type === 'contractDependencies') {
|
|
179
|
+
return 'contract evidence';
|
|
180
|
+
}
|
|
181
|
+
return 'behavioral evidence';
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return labels.join(', ');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildDependencyProposals(normalizedInput, findings, rubricResult) {
|
|
188
|
+
if (findings.length === 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const groupedFindings = new Map();
|
|
193
|
+
for (const finding of findings) {
|
|
194
|
+
const entry = groupedFindings.get(finding.targetIssueId) ?? {
|
|
195
|
+
targetIssueId: finding.targetIssueId,
|
|
196
|
+
detectorTypes: new Set(),
|
|
197
|
+
hasBehavioralUncertainty: false,
|
|
198
|
+
};
|
|
199
|
+
entry.detectorTypes.add(inferDetectorType(finding));
|
|
200
|
+
entry.hasBehavioralUncertainty = entry.hasBehavioralUncertainty || Boolean(finding.uncertain);
|
|
201
|
+
groupedFindings.set(finding.targetIssueId, entry);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return Array.from(groupedFindings.values()).map((entry) => {
|
|
205
|
+
const detectorTypes = Array.from(entry.detectorTypes);
|
|
206
|
+
const reasons = [
|
|
207
|
+
`${entry.targetIssueId} appears to consume or rely on logic being changed in ${normalizedInput.currentIssue.id} through ${formatDetectorList(detectorTypes)}.`,
|
|
208
|
+
];
|
|
209
|
+
const cons = [
|
|
210
|
+
'Adds coordination overhead and may delay the dependent issue.',
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
if (entry.hasBehavioralUncertainty) {
|
|
214
|
+
reasons.push('Behavioral evidence is heuristic and should be confirmed before applying the dependency.');
|
|
215
|
+
cons.push('Behavioral evidence is heuristic and may be noisy until a human confirms the dependency.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (rubricResult.confidence.belowThreshold) {
|
|
219
|
+
reasons.push(`Overall confidence ${rubricResult.confidence.score.toFixed(2)} is below the review threshold.`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
action: 'add-dependency',
|
|
224
|
+
dependentIssueId: entry.targetIssueId,
|
|
225
|
+
dependsOnIssueId: normalizedInput.currentIssue.id,
|
|
226
|
+
detectorTypes,
|
|
227
|
+
requiresApproval: true,
|
|
228
|
+
confidence: rubricResult.confidence.score,
|
|
229
|
+
reasons,
|
|
230
|
+
pros: [
|
|
231
|
+
'Preserves issue independence claims by sequencing affected work explicitly.',
|
|
232
|
+
'Reduces merge-order surprises for downstream logic consumers.',
|
|
233
|
+
],
|
|
234
|
+
cons,
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function analyzePhase3Dependencies(input = {}) {
|
|
240
|
+
const normalizedInput = normalizePhase3Input(input);
|
|
241
|
+
const result = createScaffoldResult(normalizedInput);
|
|
242
|
+
const importResult = scoreImportDependencies(normalizedInput);
|
|
243
|
+
const contractResult = scoreContractDependencies(normalizedInput);
|
|
244
|
+
const behavioralResult = scoreBehavioralDependencies(normalizedInput);
|
|
245
|
+
const rubricResult = aggregateRubricScores({
|
|
246
|
+
importCallChain: importResult,
|
|
247
|
+
contractDependencies: contractResult,
|
|
248
|
+
behavioralDependencies: behavioralResult,
|
|
249
|
+
}, {
|
|
250
|
+
confidenceThreshold: normalizedInput.thresholds.confidence,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
result.issues = [
|
|
254
|
+
...importResult.findings,
|
|
255
|
+
...contractResult.findings,
|
|
256
|
+
...behavioralResult.findings,
|
|
257
|
+
];
|
|
258
|
+
result.scores.importCallChain = importResult.score;
|
|
259
|
+
result.scores.contractDependencies = contractResult.score;
|
|
260
|
+
result.scores.behavioralDependencies = behavioralResult.score;
|
|
261
|
+
result.scores.rubric = rubricResult.total;
|
|
262
|
+
result.rubric = rubricResult;
|
|
263
|
+
result.detectorEvidence.importCallChain = importResult.evidence;
|
|
264
|
+
result.detectorEvidence.contractDependencies = contractResult.evidence;
|
|
265
|
+
result.detectorEvidence.behavioralDependencies = behavioralResult.evidence;
|
|
266
|
+
result.detectorConflicts = rubricResult.detectorConflicts;
|
|
267
|
+
result.confidence = rubricResult.confidence;
|
|
268
|
+
result.needsUserDecision = needsUserEscalation(rubricResult);
|
|
269
|
+
result.proposals = buildDependencyProposals(normalizedInput, result.issues, rubricResult);
|
|
270
|
+
result.importCallChain = {
|
|
271
|
+
score: importResult.score,
|
|
272
|
+
evidence: importResult.evidence,
|
|
273
|
+
findings: importResult.findings,
|
|
274
|
+
};
|
|
275
|
+
result.contractDependencies = {
|
|
276
|
+
score: contractResult.score,
|
|
277
|
+
evidence: contractResult.evidence,
|
|
278
|
+
findings: contractResult.findings,
|
|
279
|
+
};
|
|
280
|
+
result.behavioralDependencies = {
|
|
281
|
+
score: behavioralResult.score,
|
|
282
|
+
evidence: behavioralResult.evidence,
|
|
283
|
+
findings: behavioralResult.findings,
|
|
284
|
+
uncertain: behavioralResult.uncertain,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
analyzePhase3Dependencies,
|
|
292
|
+
DETECTOR_KEYS,
|
|
293
|
+
normalizePhase3Input,
|
|
294
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const STOP_WORDS = new Set([
|
|
2
|
+
'the', 'a', 'an', 'and', 'or', 'is', 'in', 'to', 'for', 'of', 'with', 'on',
|
|
3
|
+
'at', 'by', 'from', 'add', 'fix', 'update', 'implement', 'create', 'remove',
|
|
4
|
+
'delete', 'make', 'use', 'get', 'set', 'run', 'test', 'check', 'this', 'that',
|
|
5
|
+
'it', 'be', 'as', 'not', 'no', 'but', 'if', 'do', 'we', 'they', 'are', 'was',
|
|
6
|
+
'were', 'been', 'have', 'has', 'had', 'will', 'would', 'could', 'should',
|
|
7
|
+
'may', 'can', 'each', 'every', 'both', 'also', 'into', 'than', 'then', 'when',
|
|
8
|
+
'where', 'which', 'what', 'how', 'why', 'who', 'its', 'new', 'first', 'last',
|
|
9
|
+
'same', 'other',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const BEHAVIOR_TERMS = new Set([
|
|
13
|
+
'approval', 'behavior', 'confidence', 'default', 'manual', 'ordering',
|
|
14
|
+
'output', 'policy', 'review', 'rules', 'rule', 'threshold', 'validation',
|
|
15
|
+
'workflow', 'state',
|
|
16
|
+
]);
|
|
17
|
+
const STRONG_SIGNAL_TERMS = new Set([
|
|
18
|
+
'approval',
|
|
19
|
+
'confidence',
|
|
20
|
+
'manual',
|
|
21
|
+
'policy',
|
|
22
|
+
'review',
|
|
23
|
+
'rule',
|
|
24
|
+
'rules',
|
|
25
|
+
'threshold',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function tokenize(text) {
|
|
29
|
+
return Array.from(new Set(
|
|
30
|
+
text
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.split(/[^a-z0-9]+/)
|
|
33
|
+
.filter((term) => term.length >= 3 && !STOP_WORDS.has(term)),
|
|
34
|
+
));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scoreBehavioralDependencies(normalizedInput) {
|
|
38
|
+
const taskText = normalizedInput.taskContext.tasks
|
|
39
|
+
.map((task) => task.whatToImplement)
|
|
40
|
+
.join(' ');
|
|
41
|
+
const taskTerms = tokenize(taskText).filter((term) => BEHAVIOR_TERMS.has(term));
|
|
42
|
+
|
|
43
|
+
if (taskTerms.length === 0) {
|
|
44
|
+
return {
|
|
45
|
+
score: 0,
|
|
46
|
+
findings: [],
|
|
47
|
+
evidence: [],
|
|
48
|
+
uncertain: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const findings = [];
|
|
53
|
+
const evidence = [];
|
|
54
|
+
|
|
55
|
+
for (const issue of normalizedInput.openIssues) {
|
|
56
|
+
const issueText = [issue.title, issue.description, issue.notes].filter(Boolean).join(' ');
|
|
57
|
+
const issueTerms = new Set(tokenize(issueText));
|
|
58
|
+
const sharedTerms = taskTerms.filter((term) => issueTerms.has(term));
|
|
59
|
+
|
|
60
|
+
if (sharedTerms.length < 2) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const strongSignalCount = sharedTerms.filter((term) => STRONG_SIGNAL_TERMS.has(term)).length;
|
|
65
|
+
const isUncertain = strongSignalCount < 2 || sharedTerms.length < 3;
|
|
66
|
+
const signal = sharedTerms.some((term) => ['rule', 'rules', 'policy', 'approval'].includes(term))
|
|
67
|
+
? 'rule-change-overlap'
|
|
68
|
+
: 'behavior-change-overlap';
|
|
69
|
+
const issueEvidence = sharedTerms.map((term) => ({
|
|
70
|
+
type: 'behavior',
|
|
71
|
+
targetIssueId: issue.id,
|
|
72
|
+
sharedTerms: sharedTerms,
|
|
73
|
+
term,
|
|
74
|
+
scoreContribution: 1,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
findings.push({
|
|
78
|
+
sourceIssueId: normalizedInput.currentIssue.id,
|
|
79
|
+
targetIssueId: issue.id,
|
|
80
|
+
score: sharedTerms.length,
|
|
81
|
+
behavioralSignal: signal,
|
|
82
|
+
uncertain: isUncertain,
|
|
83
|
+
evidence: issueEvidence,
|
|
84
|
+
});
|
|
85
|
+
evidence.push(...issueEvidence);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
score: findings.reduce((total, finding) => total + finding.score, 0),
|
|
90
|
+
findings,
|
|
91
|
+
evidence,
|
|
92
|
+
uncertain: findings.some((finding) => finding.uncertain),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
scoreBehavioralDependencies,
|
|
98
|
+
};
|
|
@@ -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
|
+
};
|