sequant 1.13.4 → 1.14.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/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -15
- package/dist/src/commands/run.js +16 -1
- package/dist/src/lib/cli-ui.d.ts +1 -1
- package/dist/src/lib/cli-ui.js +12 -14
- package/dist/src/lib/scope/analyzer.d.ts +91 -0
- package/dist/src/lib/scope/analyzer.js +310 -0
- package/dist/src/lib/scope/formatter.d.ts +52 -0
- package/dist/src/lib/scope/formatter.js +169 -0
- package/dist/src/lib/scope/index.d.ts +36 -0
- package/dist/src/lib/scope/index.js +73 -0
- package/dist/src/lib/scope/types.d.ts +198 -0
- package/dist/src/lib/scope/types.js +60 -0
- package/dist/src/lib/scope/verdict.d.ts +80 -0
- package/dist/src/lib/scope/verdict.js +173 -0
- package/dist/src/lib/settings.d.ts +33 -0
- package/dist/src/lib/settings.js +21 -0
- package/dist/src/lib/workflow/state-manager.d.ts +11 -0
- package/dist/src/lib/workflow/state-manager.js +26 -0
- package/dist/src/lib/workflow/state-schema.d.ts +74 -0
- package/dist/src/lib/workflow/state-schema.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,11 +9,12 @@ Solve GitHub issues with structured phases and quality gates — from issue to m
|
|
|
9
9
|
|
|
10
10
|
## Quick Start
|
|
11
11
|
|
|
12
|
-
### Install
|
|
12
|
+
### Install
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
# In your project directory
|
|
16
16
|
npm install sequant
|
|
17
|
+
npx sequant init # Install skills to your project
|
|
17
18
|
npx sequant doctor # Verify setup
|
|
18
19
|
```
|
|
19
20
|
|
|
@@ -22,18 +23,6 @@ To update:
|
|
|
22
23
|
npm update sequant
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
### Alternative: Install as Claude Code Plugin
|
|
26
|
-
|
|
27
|
-
> **Note:** Plugin updates via `/plugin update` may not work reliably. Use `npm update sequant` to get latest skills.
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
# Add the Sequant marketplace
|
|
31
|
-
/plugin marketplace add admarble/sequant
|
|
32
|
-
|
|
33
|
-
# Install the plugin
|
|
34
|
-
/plugin install sequant
|
|
35
|
-
```
|
|
36
|
-
|
|
37
26
|
### Start Using
|
|
38
27
|
|
|
39
28
|
Then in Claude Code:
|
|
@@ -238,7 +227,6 @@ See [Customization Guide](docs/guides/customization.md) for all options.
|
|
|
238
227
|
- [Run Command](docs/reference/run-command.md)
|
|
239
228
|
- [Git Workflows](docs/guides/git-workflows.md)
|
|
240
229
|
- [Customization](docs/guides/customization.md)
|
|
241
|
-
- [Plugin Updates & Versioning](docs/internal/plugin-updates.md)
|
|
242
230
|
- [Troubleshooting](docs/troubleshooting.md)
|
|
243
231
|
|
|
244
232
|
Stack guides: [Next.js](docs/stacks/nextjs.md) · [Rust](docs/stacks/rust.md) · [Python](docs/stacks/python.md) · [Go](docs/stacks/go.md)
|
|
@@ -249,7 +237,6 @@ Stack guides: [Next.js](docs/stacks/nextjs.md) · [Rust](docs/stacks/rust.md) ·
|
|
|
249
237
|
|
|
250
238
|
### Reporting Issues
|
|
251
239
|
|
|
252
|
-
- **Plugin issues:** [Plugin Feedback template](https://github.com/admarble/sequant/issues/new?template=plugin-feedback.yml)
|
|
253
240
|
- **Bug reports:** [Bug template](https://github.com/admarble/sequant/issues/new?template=bug.yml)
|
|
254
241
|
- **Feature requests:** [Feature template](https://github.com/admarble/sequant/issues/new?template=feature.yml)
|
|
255
242
|
- **Questions:** [GitHub Discussions](https://github.com/admarble/sequant/discussions)
|
package/dist/src/commands/run.js
CHANGED
|
@@ -605,6 +605,8 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
605
605
|
// Determine working directory and environment
|
|
606
606
|
const shouldUseWorktree = worktreePath && ISOLATED_PHASES.includes(phase);
|
|
607
607
|
const cwd = shouldUseWorktree ? worktreePath : process.cwd();
|
|
608
|
+
// Track stderr for error diagnostics (declared outside try for catch access)
|
|
609
|
+
let capturedStderr = "";
|
|
608
610
|
try {
|
|
609
611
|
// Check if shutdown is in progress
|
|
610
612
|
if (shutdownManager?.shuttingDown) {
|
|
@@ -667,6 +669,15 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
667
669
|
env,
|
|
668
670
|
// Pass MCP servers for headless mode (AC-2)
|
|
669
671
|
...(mcpServers ? { mcpServers } : {}),
|
|
672
|
+
// Capture stderr for debugging (helps diagnose early exit failures)
|
|
673
|
+
stderr: (data) => {
|
|
674
|
+
capturedStderr += data;
|
|
675
|
+
if (config.verbose) {
|
|
676
|
+
spinner?.pause();
|
|
677
|
+
process.stderr.write(chalk.red(data));
|
|
678
|
+
spinner?.resume();
|
|
679
|
+
}
|
|
680
|
+
},
|
|
670
681
|
},
|
|
671
682
|
});
|
|
672
683
|
// Stream and process messages
|
|
@@ -791,11 +802,15 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
791
802
|
error: `Timeout after ${config.phaseTimeout}s`,
|
|
792
803
|
};
|
|
793
804
|
}
|
|
805
|
+
// Include stderr in error message if available (helps diagnose early exit failures)
|
|
806
|
+
const stderrSuffix = capturedStderr
|
|
807
|
+
? `\nStderr: ${capturedStderr.slice(0, 500)}`
|
|
808
|
+
: "";
|
|
794
809
|
return {
|
|
795
810
|
phase,
|
|
796
811
|
success: false,
|
|
797
812
|
durationSeconds,
|
|
798
|
-
error,
|
|
813
|
+
error: error + stderrSuffix,
|
|
799
814
|
};
|
|
800
815
|
}
|
|
801
816
|
}
|
package/dist/src/lib/cli-ui.d.ts
CHANGED
package/dist/src/lib/cli-ui.js
CHANGED
|
@@ -9,7 +9,6 @@ import ora from "ora";
|
|
|
9
9
|
import boxen from "boxen";
|
|
10
10
|
// cli-table3 uses CommonJS `export =` syntax, default import works with esModuleInterop
|
|
11
11
|
import Table from "cli-table3";
|
|
12
|
-
import gradient from "gradient-string";
|
|
13
12
|
import { isCI, isStdoutTTY } from "./tty.js";
|
|
14
13
|
/**
|
|
15
14
|
* Global UI configuration state
|
|
@@ -109,27 +108,26 @@ function isLegacyWindows() {
|
|
|
109
108
|
// ============================================================================
|
|
110
109
|
/**
|
|
111
110
|
* Static ASCII logo for SEQUANT branding
|
|
112
|
-
*
|
|
111
|
+
* Clean, legible design inspired by the sequant logo
|
|
112
|
+
* Q has distinctive tail like the brand logo
|
|
113
113
|
*/
|
|
114
|
-
const ASCII_LOGO = `
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`.trimStart();
|
|
114
|
+
const ASCII_LOGO = ` ██████ ███████ ██████ ██ ██ █████ ███ ██ ████████
|
|
115
|
+
██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██
|
|
116
|
+
█████ █████ ██ ██ ██ ██ ███████ ██ ██ ██ ██
|
|
117
|
+
██ ██ ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
118
|
+
██████ ███████ ██████ ██████ ██ ██ ██ ████ ██
|
|
119
|
+
▀▀
|
|
120
|
+
`;
|
|
122
121
|
/**
|
|
123
|
-
* Get the ASCII logo with
|
|
122
|
+
* Get the ASCII logo with brand color
|
|
124
123
|
*/
|
|
125
124
|
export function logo() {
|
|
126
125
|
if (config.jsonMode || config.minimal)
|
|
127
126
|
return "";
|
|
128
127
|
if (config.noColor || !config.isTTY)
|
|
129
128
|
return ASCII_LOGO;
|
|
130
|
-
// Apply
|
|
131
|
-
|
|
132
|
-
return sequantGradient(ASCII_LOGO);
|
|
129
|
+
// Apply brand orange color
|
|
130
|
+
return chalk.hex("#FF8012")(ASCII_LOGO);
|
|
133
131
|
}
|
|
134
132
|
/**
|
|
135
133
|
* Get the banner (logo + tagline)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Detects bundled features and analyzes issue scope using heuristics:
|
|
5
|
+
* - AC clustering by functional area
|
|
6
|
+
* - Title verb detection (multiple verbs = multiple features)
|
|
7
|
+
* - Directory spread analysis
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { detectFeatures, analyzeScope } from './analyzer';
|
|
12
|
+
*
|
|
13
|
+
* const criteria = parseAcceptanceCriteria(issueBody);
|
|
14
|
+
* const detection = detectFeatures(criteria, issueTitle);
|
|
15
|
+
* console.log(`Feature count: ${detection.featureCount}`);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { AcceptanceCriterion } from "../workflow/state-schema.js";
|
|
19
|
+
import type { ACCluster, FeatureDetection, NonGoals, ScopeAssessmentConfig } from "./types.js";
|
|
20
|
+
/**
|
|
21
|
+
* Cluster AC items by functional area based on keywords
|
|
22
|
+
*
|
|
23
|
+
* @param criteria - Parsed acceptance criteria
|
|
24
|
+
* @returns Array of AC clusters
|
|
25
|
+
*/
|
|
26
|
+
export declare function clusterACByKeyword(criteria: AcceptanceCriterion[]): ACCluster[];
|
|
27
|
+
/**
|
|
28
|
+
* Detect action verbs in issue title
|
|
29
|
+
*
|
|
30
|
+
* @param title - Issue title
|
|
31
|
+
* @returns Array of detected verbs
|
|
32
|
+
*/
|
|
33
|
+
export declare function detectTitleVerbs(title: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Estimate directory spread from AC descriptions
|
|
36
|
+
*
|
|
37
|
+
* Looks for patterns that suggest different areas of the codebase:
|
|
38
|
+
* - File path mentions (src/, lib/, components/)
|
|
39
|
+
* - Module references
|
|
40
|
+
* - Component type mentions
|
|
41
|
+
*
|
|
42
|
+
* @param criteria - Parsed acceptance criteria
|
|
43
|
+
* @returns Estimated number of distinct directories
|
|
44
|
+
*/
|
|
45
|
+
export declare function estimateDirectorySpread(criteria: AcceptanceCriterion[]): {
|
|
46
|
+
spread: number;
|
|
47
|
+
directories: string[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Calculate feature count based on multiple signals
|
|
51
|
+
*
|
|
52
|
+
* Algorithm:
|
|
53
|
+
* 1. Count distinct AC clusters (if >1, suggests multiple features)
|
|
54
|
+
* 2. Count action verbs in title (multiple verbs = multiple features)
|
|
55
|
+
* 3. Consider directory spread (wide spread suggests complexity)
|
|
56
|
+
*
|
|
57
|
+
* @param clusters - AC clusters from keyword analysis
|
|
58
|
+
* @param titleVerbs - Detected verbs from title
|
|
59
|
+
* @param directorySpread - Number of directories touched
|
|
60
|
+
* @returns Estimated feature count
|
|
61
|
+
*/
|
|
62
|
+
export declare function calculateFeatureCount(clusters: ACCluster[], titleVerbs: string[], directorySpread: number): number;
|
|
63
|
+
/**
|
|
64
|
+
* Detect features in issue based on AC items and title
|
|
65
|
+
*
|
|
66
|
+
* @param criteria - Parsed acceptance criteria
|
|
67
|
+
* @param title - Issue title
|
|
68
|
+
* @returns Feature detection result
|
|
69
|
+
*/
|
|
70
|
+
export declare function detectFeatures(criteria: AcceptanceCriterion[], title: string): FeatureDetection;
|
|
71
|
+
/**
|
|
72
|
+
* Parse non-goals section from issue body
|
|
73
|
+
*
|
|
74
|
+
* Looks for a "Non-Goals" or "Out of Scope" section with checkbox items.
|
|
75
|
+
*
|
|
76
|
+
* @param issueBody - Full issue body markdown
|
|
77
|
+
* @returns Non-goals extraction result
|
|
78
|
+
*/
|
|
79
|
+
export declare function parseNonGoals(issueBody: string): NonGoals;
|
|
80
|
+
/**
|
|
81
|
+
* Check if an issue should skip scope assessment (trivial issue)
|
|
82
|
+
*
|
|
83
|
+
* @param acCount - Number of acceptance criteria
|
|
84
|
+
* @param directorySpread - Estimated directory spread
|
|
85
|
+
* @param config - Scope assessment configuration
|
|
86
|
+
* @returns Whether to skip assessment
|
|
87
|
+
*/
|
|
88
|
+
export declare function shouldSkipAssessment(acCount: number, directorySpread: number, config?: ScopeAssessmentConfig): {
|
|
89
|
+
skip: boolean;
|
|
90
|
+
reason?: string;
|
|
91
|
+
};
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Detects bundled features and analyzes issue scope using heuristics:
|
|
5
|
+
* - AC clustering by functional area
|
|
6
|
+
* - Title verb detection (multiple verbs = multiple features)
|
|
7
|
+
* - Directory spread analysis
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { detectFeatures, analyzeScope } from './analyzer';
|
|
12
|
+
*
|
|
13
|
+
* const criteria = parseAcceptanceCriteria(issueBody);
|
|
14
|
+
* const detection = detectFeatures(criteria, issueTitle);
|
|
15
|
+
* console.log(`Feature count: ${detection.featureCount}`);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { DEFAULT_SCOPE_CONFIG } from "./types.js";
|
|
19
|
+
/**
|
|
20
|
+
* Keywords for clustering AC items by functional area
|
|
21
|
+
*/
|
|
22
|
+
const CLUSTER_KEYWORDS = {
|
|
23
|
+
auth: [
|
|
24
|
+
"auth",
|
|
25
|
+
"login",
|
|
26
|
+
"logout",
|
|
27
|
+
"session",
|
|
28
|
+
"password",
|
|
29
|
+
"token",
|
|
30
|
+
"jwt",
|
|
31
|
+
"oauth",
|
|
32
|
+
"sso",
|
|
33
|
+
],
|
|
34
|
+
ui: [
|
|
35
|
+
"display",
|
|
36
|
+
"show",
|
|
37
|
+
"render",
|
|
38
|
+
"button",
|
|
39
|
+
"form",
|
|
40
|
+
"modal",
|
|
41
|
+
"page",
|
|
42
|
+
"component",
|
|
43
|
+
"ui",
|
|
44
|
+
"dashboard",
|
|
45
|
+
],
|
|
46
|
+
api: [
|
|
47
|
+
"api",
|
|
48
|
+
"endpoint",
|
|
49
|
+
"route",
|
|
50
|
+
"request",
|
|
51
|
+
"response",
|
|
52
|
+
"http",
|
|
53
|
+
"rest",
|
|
54
|
+
"graphql",
|
|
55
|
+
],
|
|
56
|
+
data: [
|
|
57
|
+
"database",
|
|
58
|
+
"db",
|
|
59
|
+
"query",
|
|
60
|
+
"store",
|
|
61
|
+
"save",
|
|
62
|
+
"persist",
|
|
63
|
+
"cache",
|
|
64
|
+
"storage",
|
|
65
|
+
],
|
|
66
|
+
perf: [
|
|
67
|
+
"performance",
|
|
68
|
+
"optimize",
|
|
69
|
+
"speed",
|
|
70
|
+
"fast",
|
|
71
|
+
"slow",
|
|
72
|
+
"latency",
|
|
73
|
+
"throughput",
|
|
74
|
+
],
|
|
75
|
+
config: ["config", "setting", "option", "preference", "environment", "env"],
|
|
76
|
+
test: ["test", "spec", "coverage", "mock", "stub", "fixture"],
|
|
77
|
+
docs: ["document", "readme", "guide", "tutorial", "example"],
|
|
78
|
+
error: ["error", "exception", "fail", "handle", "catch", "throw"],
|
|
79
|
+
validation: ["valid", "invalid", "check", "verify", "sanitize"],
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Action verbs that indicate distinct features when multiple are present
|
|
83
|
+
*/
|
|
84
|
+
const ACTION_VERBS = [
|
|
85
|
+
"add",
|
|
86
|
+
"create",
|
|
87
|
+
"implement",
|
|
88
|
+
"build",
|
|
89
|
+
"fix",
|
|
90
|
+
"update",
|
|
91
|
+
"refactor",
|
|
92
|
+
"remove",
|
|
93
|
+
"delete",
|
|
94
|
+
"improve",
|
|
95
|
+
"optimize",
|
|
96
|
+
"migrate",
|
|
97
|
+
"convert",
|
|
98
|
+
"integrate",
|
|
99
|
+
"enable",
|
|
100
|
+
"disable",
|
|
101
|
+
];
|
|
102
|
+
/**
|
|
103
|
+
* Cluster AC items by functional area based on keywords
|
|
104
|
+
*
|
|
105
|
+
* @param criteria - Parsed acceptance criteria
|
|
106
|
+
* @returns Array of AC clusters
|
|
107
|
+
*/
|
|
108
|
+
export function clusterACByKeyword(criteria) {
|
|
109
|
+
const clusters = new Map();
|
|
110
|
+
const unclassified = [];
|
|
111
|
+
for (const ac of criteria) {
|
|
112
|
+
const descLower = ac.description.toLowerCase();
|
|
113
|
+
let matched = false;
|
|
114
|
+
for (const [keyword, patterns] of Object.entries(CLUSTER_KEYWORDS)) {
|
|
115
|
+
if (patterns.some((p) => descLower.includes(p))) {
|
|
116
|
+
const existing = clusters.get(keyword) ?? [];
|
|
117
|
+
existing.push(ac.id);
|
|
118
|
+
clusters.set(keyword, existing);
|
|
119
|
+
matched = true;
|
|
120
|
+
break; // Only assign to first matching cluster
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!matched) {
|
|
124
|
+
unclassified.push(ac.id);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Add unclassified as a separate cluster if not empty
|
|
128
|
+
if (unclassified.length > 0) {
|
|
129
|
+
clusters.set("other", unclassified);
|
|
130
|
+
}
|
|
131
|
+
// Convert to array format
|
|
132
|
+
return Array.from(clusters.entries()).map(([keyword, acIds]) => ({
|
|
133
|
+
keyword,
|
|
134
|
+
acIds,
|
|
135
|
+
count: acIds.length,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Detect action verbs in issue title
|
|
140
|
+
*
|
|
141
|
+
* @param title - Issue title
|
|
142
|
+
* @returns Array of detected verbs
|
|
143
|
+
*/
|
|
144
|
+
export function detectTitleVerbs(title) {
|
|
145
|
+
const titleLower = title.toLowerCase();
|
|
146
|
+
const detected = [];
|
|
147
|
+
for (const verb of ACTION_VERBS) {
|
|
148
|
+
// Match word boundaries to avoid partial matches
|
|
149
|
+
const regex = new RegExp(`\\b${verb}\\b`, "i");
|
|
150
|
+
if (regex.test(titleLower)) {
|
|
151
|
+
detected.push(verb);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return detected;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Estimate directory spread from AC descriptions
|
|
158
|
+
*
|
|
159
|
+
* Looks for patterns that suggest different areas of the codebase:
|
|
160
|
+
* - File path mentions (src/, lib/, components/)
|
|
161
|
+
* - Module references
|
|
162
|
+
* - Component type mentions
|
|
163
|
+
*
|
|
164
|
+
* @param criteria - Parsed acceptance criteria
|
|
165
|
+
* @returns Estimated number of distinct directories
|
|
166
|
+
*/
|
|
167
|
+
export function estimateDirectorySpread(criteria) {
|
|
168
|
+
const directories = new Set();
|
|
169
|
+
// Common directory patterns
|
|
170
|
+
const dirPatterns = [
|
|
171
|
+
/\b(src|lib|utils|helpers)\b/i,
|
|
172
|
+
/\b(components|pages|views|layouts)\b/i,
|
|
173
|
+
/\b(api|routes|handlers|controllers)\b/i,
|
|
174
|
+
/\b(models|schemas|types|interfaces)\b/i,
|
|
175
|
+
/\b(tests?|__tests__|specs?)\b/i,
|
|
176
|
+
/\b(scripts|bin|cli)\b/i,
|
|
177
|
+
/\b(config|settings)\b/i,
|
|
178
|
+
/\b(docs|documentation)\b/i,
|
|
179
|
+
/\b(styles|css|scss)\b/i,
|
|
180
|
+
/\b(hooks|contexts?|providers?)\b/i,
|
|
181
|
+
/\b(services|queries|mutations)\b/i,
|
|
182
|
+
/\b(middleware|plugins?)\b/i,
|
|
183
|
+
];
|
|
184
|
+
for (const ac of criteria) {
|
|
185
|
+
const desc = ac.description;
|
|
186
|
+
for (const pattern of dirPatterns) {
|
|
187
|
+
const match = desc.match(pattern);
|
|
188
|
+
if (match) {
|
|
189
|
+
directories.add(match[1].toLowerCase());
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
spread: directories.size,
|
|
195
|
+
directories: Array.from(directories),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Calculate feature count based on multiple signals
|
|
200
|
+
*
|
|
201
|
+
* Algorithm:
|
|
202
|
+
* 1. Count distinct AC clusters (if >1, suggests multiple features)
|
|
203
|
+
* 2. Count action verbs in title (multiple verbs = multiple features)
|
|
204
|
+
* 3. Consider directory spread (wide spread suggests complexity)
|
|
205
|
+
*
|
|
206
|
+
* @param clusters - AC clusters from keyword analysis
|
|
207
|
+
* @param titleVerbs - Detected verbs from title
|
|
208
|
+
* @param directorySpread - Number of directories touched
|
|
209
|
+
* @returns Estimated feature count
|
|
210
|
+
*/
|
|
211
|
+
export function calculateFeatureCount(clusters, titleVerbs, directorySpread) {
|
|
212
|
+
// Base: number of distinct clusters with >1 AC items
|
|
213
|
+
const significantClusters = clusters.filter((c) => c.count >= 2).length;
|
|
214
|
+
// Additional signal: multiple verbs in title
|
|
215
|
+
const verbSignal = Math.max(0, titleVerbs.length - 1);
|
|
216
|
+
// Directory spread signal (normalized)
|
|
217
|
+
const dirSignal = directorySpread >= 4 ? 1 : 0;
|
|
218
|
+
// Combine signals with weights
|
|
219
|
+
// Clusters are primary signal, title and directories are secondary
|
|
220
|
+
const rawCount = significantClusters + verbSignal * 0.5 + dirSignal * 0.5;
|
|
221
|
+
// Minimum is 1 feature, round to nearest integer
|
|
222
|
+
return Math.max(1, Math.round(rawCount));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Detect features in issue based on AC items and title
|
|
226
|
+
*
|
|
227
|
+
* @param criteria - Parsed acceptance criteria
|
|
228
|
+
* @param title - Issue title
|
|
229
|
+
* @returns Feature detection result
|
|
230
|
+
*/
|
|
231
|
+
export function detectFeatures(criteria, title) {
|
|
232
|
+
const clusters = clusterACByKeyword(criteria);
|
|
233
|
+
const titleVerbs = detectTitleVerbs(title);
|
|
234
|
+
const { spread, directories } = estimateDirectorySpread(criteria);
|
|
235
|
+
const featureCount = calculateFeatureCount(clusters, titleVerbs, spread);
|
|
236
|
+
return {
|
|
237
|
+
featureCount,
|
|
238
|
+
clusters,
|
|
239
|
+
multipleVerbs: titleVerbs.length > 1,
|
|
240
|
+
titleVerbs,
|
|
241
|
+
directorySpread: spread,
|
|
242
|
+
directories,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Parse non-goals section from issue body
|
|
247
|
+
*
|
|
248
|
+
* Looks for a "Non-Goals" or "Out of Scope" section with checkbox items.
|
|
249
|
+
*
|
|
250
|
+
* @param issueBody - Full issue body markdown
|
|
251
|
+
* @returns Non-goals extraction result
|
|
252
|
+
*/
|
|
253
|
+
export function parseNonGoals(issueBody) {
|
|
254
|
+
const items = [];
|
|
255
|
+
// Find Non-Goals section (case-insensitive)
|
|
256
|
+
const sectionPattern = /##\s*(?:Non[- ]?Goals|Out\s+of\s+Scope|Scope\s+Boundaries)\s*\n([\s\S]*?)(?=\n##|\n---|$)/i;
|
|
257
|
+
const sectionMatch = issueBody.match(sectionPattern);
|
|
258
|
+
if (!sectionMatch) {
|
|
259
|
+
return {
|
|
260
|
+
items: [],
|
|
261
|
+
found: false,
|
|
262
|
+
warning: "Non-Goals section not found. Consider adding scope boundaries.",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const sectionContent = sectionMatch[1];
|
|
266
|
+
// Extract checkbox items or list items
|
|
267
|
+
const itemPattern = /^[-*]\s*(?:\[[ x]\]\s*)?(.+)$/gim;
|
|
268
|
+
let match;
|
|
269
|
+
while ((match = itemPattern.exec(sectionContent)) !== null) {
|
|
270
|
+
const item = match[1].trim();
|
|
271
|
+
if (item && !item.startsWith("#")) {
|
|
272
|
+
items.push(item);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (items.length === 0) {
|
|
276
|
+
return {
|
|
277
|
+
items: [],
|
|
278
|
+
found: true,
|
|
279
|
+
warning: "Non-Goals section is empty. Add explicit scope boundaries.",
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
items,
|
|
284
|
+
found: true,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Check if an issue should skip scope assessment (trivial issue)
|
|
289
|
+
*
|
|
290
|
+
* @param acCount - Number of acceptance criteria
|
|
291
|
+
* @param directorySpread - Estimated directory spread
|
|
292
|
+
* @param config - Scope assessment configuration
|
|
293
|
+
* @returns Whether to skip assessment
|
|
294
|
+
*/
|
|
295
|
+
export function shouldSkipAssessment(acCount, directorySpread, config = DEFAULT_SCOPE_CONFIG) {
|
|
296
|
+
if (!config.enabled) {
|
|
297
|
+
return { skip: true, reason: "Scope assessment disabled in config" };
|
|
298
|
+
}
|
|
299
|
+
if (!config.skipIfSimple) {
|
|
300
|
+
return { skip: false };
|
|
301
|
+
}
|
|
302
|
+
const { maxACItems, maxDirectories } = config.trivialThresholds;
|
|
303
|
+
if (acCount <= maxACItems && directorySpread <= maxDirectories) {
|
|
304
|
+
return {
|
|
305
|
+
skip: true,
|
|
306
|
+
reason: `Trivial issue (${acCount} AC items, ${directorySpread} directories)`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return { skip: false };
|
|
310
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope Assessment Formatter
|
|
3
|
+
*
|
|
4
|
+
* Formats scope assessment results as markdown for /spec output
|
|
5
|
+
* and GitHub issue comments.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { formatScopeAssessment } from './formatter';
|
|
10
|
+
*
|
|
11
|
+
* const assessment = performScopeAssessment(criteria, issueBody, title);
|
|
12
|
+
* console.log(formatScopeAssessment(assessment));
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import type { ScopeAssessment, NonGoals } from "./types.js";
|
|
16
|
+
/**
|
|
17
|
+
* Format non-goals section for output
|
|
18
|
+
*
|
|
19
|
+
* @param nonGoals - Non-goals parsing result
|
|
20
|
+
* @returns Formatted markdown string
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatNonGoals(nonGoals: NonGoals): string;
|
|
23
|
+
/**
|
|
24
|
+
* Format scope metrics table
|
|
25
|
+
*
|
|
26
|
+
* @param assessment - Complete scope assessment
|
|
27
|
+
* @returns Formatted markdown table
|
|
28
|
+
*/
|
|
29
|
+
export declare function formatMetricsTable(assessment: ScopeAssessment): string;
|
|
30
|
+
/**
|
|
31
|
+
* Format scope verdict section
|
|
32
|
+
*
|
|
33
|
+
* @param assessment - Complete scope assessment
|
|
34
|
+
* @returns Formatted markdown string
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatVerdict(assessment: ScopeAssessment): string;
|
|
37
|
+
/**
|
|
38
|
+
* Format complete scope assessment section
|
|
39
|
+
*
|
|
40
|
+
* @param assessment - Complete scope assessment
|
|
41
|
+
* @returns Formatted markdown string
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatScopeAssessment(assessment: ScopeAssessment): string;
|
|
44
|
+
/**
|
|
45
|
+
* Format condensed scope assessment for issue comments
|
|
46
|
+
*
|
|
47
|
+
* A shorter version suitable for GitHub issue comments.
|
|
48
|
+
*
|
|
49
|
+
* @param assessment - Complete scope assessment
|
|
50
|
+
* @returns Condensed markdown string
|
|
51
|
+
*/
|
|
52
|
+
export declare function formatCondensedAssessment(assessment: ScopeAssessment): string;
|