@nathapp/nax 0.32.2 → 0.33.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/dist/nax.js +808 -104
- package/package.json +1 -1
- package/src/cli/analyze.ts +145 -0
- package/src/cli/config.ts +9 -0
- package/src/config/defaults.ts +8 -0
- package/src/config/schema.ts +1 -0
- package/src/config/schemas.ts +10 -0
- package/src/config/types.ts +18 -0
- package/src/context/elements.ts +13 -0
- package/src/context/greenfield.ts +1 -1
- package/src/decompose/apply.ts +44 -0
- package/src/decompose/builder.ts +181 -0
- package/src/decompose/index.ts +8 -0
- package/src/decompose/sections/codebase.ts +26 -0
- package/src/decompose/sections/constraints.ts +32 -0
- package/src/decompose/sections/index.ts +4 -0
- package/src/decompose/sections/sibling-stories.ts +25 -0
- package/src/decompose/sections/target-story.ts +31 -0
- package/src/decompose/types.ts +55 -0
- package/src/decompose/validators/complexity.ts +45 -0
- package/src/decompose/validators/coverage.ts +134 -0
- package/src/decompose/validators/dependency.ts +91 -0
- package/src/decompose/validators/index.ts +35 -0
- package/src/decompose/validators/overlap.ts +128 -0
- package/src/execution/escalation/tier-escalation.ts +9 -2
- package/src/execution/sequential-executor.ts +4 -3
- package/src/interaction/index.ts +1 -0
- package/src/interaction/triggers.ts +21 -0
- package/src/interaction/types.ts +7 -0
- package/src/pipeline/stages/review.ts +6 -0
- package/src/pipeline/stages/routing.ts +89 -0
- package/src/pipeline/types.ts +2 -0
- package/src/plugins/types.ts +33 -0
- package/src/prd/index.ts +5 -1
- package/src/prd/types.ts +11 -1
- package/src/review/orchestrator.ts +1 -0
- package/src/review/types.ts +2 -0
- package/src/tdd/isolation.ts +1 -1
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
* RRP-003: contentHash staleness detection — if story.routing.contentHash is missing or
|
|
9
9
|
* does not match the current story content, treats cached routing as a miss and re-classifies.
|
|
10
10
|
*
|
|
11
|
+
* SD-004: Oversized story detection — after routing, checks if story exceeds
|
|
12
|
+
* config.decompose.maxAcceptanceCriteria with complex/expert complexity. Decomposes
|
|
13
|
+
* based on trigger mode (auto / confirm / disabled).
|
|
14
|
+
*
|
|
11
15
|
* @returns
|
|
12
16
|
* - `continue`: Routing determined, proceed to next stage
|
|
17
|
+
* - `skip`: Story was decomposed into substories; runner should pick up first substory
|
|
13
18
|
*
|
|
14
19
|
* @example
|
|
15
20
|
* ```ts
|
|
@@ -20,13 +25,42 @@
|
|
|
20
25
|
* ```
|
|
21
26
|
*/
|
|
22
27
|
|
|
28
|
+
import type { NaxConfig } from "../../config";
|
|
23
29
|
import { isGreenfieldStory } from "../../context/greenfield";
|
|
30
|
+
import { applyDecomposition } from "../../decompose/apply";
|
|
31
|
+
import { DecomposeBuilder } from "../../decompose/builder";
|
|
32
|
+
import type { DecomposeConfig as BuilderDecomposeConfig, DecomposeResult } from "../../decompose/types";
|
|
33
|
+
import { checkStoryOversized } from "../../interaction/triggers";
|
|
24
34
|
import { getLogger } from "../../logger";
|
|
25
35
|
import { savePRD } from "../../prd";
|
|
36
|
+
import type { PRD, UserStory } from "../../prd";
|
|
26
37
|
import { complexityToModelTier, computeStoryContentHash, routeStory } from "../../routing";
|
|
27
38
|
import { clearCache, routeBatch } from "../../routing/strategies/llm";
|
|
28
39
|
import type { PipelineContext, PipelineStage, RoutingResult, StageResult } from "../types";
|
|
29
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Run story decomposition using DecomposeBuilder.
|
|
43
|
+
* Used as the default implementation in _routingDeps.runDecompose.
|
|
44
|
+
* In production, replace with an LLM-backed adapter.
|
|
45
|
+
*/
|
|
46
|
+
async function runDecompose(story: UserStory, prd: PRD, config: NaxConfig, _workdir: string): Promise<DecomposeResult> {
|
|
47
|
+
const naxDecompose = config.decompose;
|
|
48
|
+
const builderConfig: BuilderDecomposeConfig = {
|
|
49
|
+
maxSubStories: naxDecompose?.maxSubstories ?? 5,
|
|
50
|
+
maxComplexity: naxDecompose?.maxSubstoryComplexity ?? "medium",
|
|
51
|
+
maxRetries: naxDecompose?.maxRetries ?? 2,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Stub adapter — replaced in tests via _routingDeps injection.
|
|
55
|
+
const adapter = {
|
|
56
|
+
async decompose(_prompt: string): Promise<string> {
|
|
57
|
+
throw new Error("[decompose] No LLM adapter configured for story decomposition");
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
|
|
62
|
+
}
|
|
63
|
+
|
|
30
64
|
export const routingStage: PipelineStage = {
|
|
31
65
|
name: "routing",
|
|
32
66
|
enabled: () => true,
|
|
@@ -116,6 +150,58 @@ export const routingStage: PipelineStage = {
|
|
|
116
150
|
logger.debug("routing", ctx.routing.reasoning);
|
|
117
151
|
}
|
|
118
152
|
|
|
153
|
+
// SD-004: Oversized story detection and decomposition
|
|
154
|
+
const decomposeConfig = ctx.config.decompose;
|
|
155
|
+
if (decomposeConfig) {
|
|
156
|
+
const acCount = ctx.story.acceptanceCriteria.length;
|
|
157
|
+
const complexity = ctx.routing.complexity;
|
|
158
|
+
const isOversized =
|
|
159
|
+
acCount > decomposeConfig.maxAcceptanceCriteria && (complexity === "complex" || complexity === "expert");
|
|
160
|
+
|
|
161
|
+
if (isOversized) {
|
|
162
|
+
if (decomposeConfig.trigger === "disabled") {
|
|
163
|
+
logger.warn(
|
|
164
|
+
"routing",
|
|
165
|
+
`Story ${ctx.story.id} is oversized (${acCount} ACs) but decompose is disabled — continuing with original`,
|
|
166
|
+
);
|
|
167
|
+
} else if (decomposeConfig.trigger === "auto") {
|
|
168
|
+
const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir);
|
|
169
|
+
if (result.validation.valid) {
|
|
170
|
+
_routingDeps.applyDecomposition(ctx.prd, result);
|
|
171
|
+
if (ctx.prdPath) {
|
|
172
|
+
await _routingDeps.savePRD(ctx.prd, ctx.prdPath);
|
|
173
|
+
}
|
|
174
|
+
logger.info("routing", `Story ${ctx.story.id} decomposed into ${result.subStories.length} substories`);
|
|
175
|
+
return { action: "skip", reason: `Decomposed into ${result.subStories.length} substories` };
|
|
176
|
+
}
|
|
177
|
+
logger.warn("routing", `Story ${ctx.story.id} decompose failed after retries — continuing with original`, {
|
|
178
|
+
errors: result.validation.errors,
|
|
179
|
+
});
|
|
180
|
+
} else if (decomposeConfig.trigger === "confirm") {
|
|
181
|
+
const action = await _routingDeps.checkStoryOversized(
|
|
182
|
+
{ featureName: ctx.prd.feature, storyId: ctx.story.id, criteriaCount: acCount },
|
|
183
|
+
ctx.config,
|
|
184
|
+
// biome-ignore lint/style/noNonNullAssertion: confirm mode is only reached when interaction chain is present in production; tests mock checkStoryOversized directly
|
|
185
|
+
ctx.interaction!,
|
|
186
|
+
);
|
|
187
|
+
if (action === "decompose") {
|
|
188
|
+
const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir);
|
|
189
|
+
if (result.validation.valid) {
|
|
190
|
+
_routingDeps.applyDecomposition(ctx.prd, result);
|
|
191
|
+
if (ctx.prdPath) {
|
|
192
|
+
await _routingDeps.savePRD(ctx.prd, ctx.prdPath);
|
|
193
|
+
}
|
|
194
|
+
logger.info("routing", `Story ${ctx.story.id} decomposed into ${result.subStories.length} substories`);
|
|
195
|
+
return { action: "skip", reason: `Decomposed into ${result.subStories.length} substories` };
|
|
196
|
+
}
|
|
197
|
+
logger.warn("routing", `Story ${ctx.story.id} decompose failed after retries — continuing with original`, {
|
|
198
|
+
errors: result.validation.errors,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
119
205
|
return { action: "continue" };
|
|
120
206
|
},
|
|
121
207
|
};
|
|
@@ -131,4 +217,7 @@ export const _routingDeps = {
|
|
|
131
217
|
clearCache,
|
|
132
218
|
savePRD,
|
|
133
219
|
computeStoryContentHash,
|
|
220
|
+
applyDecomposition,
|
|
221
|
+
runDecompose,
|
|
222
|
+
checkStoryOversized,
|
|
134
223
|
};
|
package/src/pipeline/types.ts
CHANGED
|
@@ -110,6 +110,8 @@ export interface PipelineContext {
|
|
|
110
110
|
tddFailureCategory?: FailureCategory;
|
|
111
111
|
/** Set to true when TDD full-suite gate already passed — verify stage skips to avoid redundant run (BUG-054) */
|
|
112
112
|
fullSuiteGatePassed?: boolean;
|
|
113
|
+
/** Structured review findings from plugin reviewers — passed to escalation for retry context */
|
|
114
|
+
reviewFindings?: import("../plugins/types").ReviewFinding[];
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/**
|
package/src/plugins/types.ts
CHANGED
|
@@ -119,6 +119,37 @@ export type { IPromptOptimizer } from "../optimizer/types";
|
|
|
119
119
|
// Review Extension
|
|
120
120
|
// ============================================================================
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* A single structured finding from a review check.
|
|
124
|
+
*
|
|
125
|
+
* Designed to be service-agnostic — works with Semgrep, ESLint, SonarQube,
|
|
126
|
+
* Snyk, CodeQL, and other SAST/DAST/linting tools.
|
|
127
|
+
*/
|
|
128
|
+
export interface ReviewFinding {
|
|
129
|
+
/** Rule or check ID (e.g., "detect-non-literal-regexp", "no-unused-vars") */
|
|
130
|
+
ruleId: string;
|
|
131
|
+
/** Severity level (tool-agnostic scale) */
|
|
132
|
+
severity: "critical" | "error" | "warning" | "info" | "low";
|
|
133
|
+
/** File path (relative to workdir) */
|
|
134
|
+
file: string;
|
|
135
|
+
/** Line number (1-indexed) */
|
|
136
|
+
line: number;
|
|
137
|
+
/** Column number (1-indexed, optional) */
|
|
138
|
+
column?: number;
|
|
139
|
+
/** End line number (optional, for multi-line findings) */
|
|
140
|
+
endLine?: number;
|
|
141
|
+
/** End column number (optional) */
|
|
142
|
+
endColumn?: number;
|
|
143
|
+
/** Human-readable message */
|
|
144
|
+
message: string;
|
|
145
|
+
/** Optional URL for rule documentation or details */
|
|
146
|
+
url?: string;
|
|
147
|
+
/** Source tool that produced this finding (e.g., "semgrep", "eslint", "snyk") */
|
|
148
|
+
source?: string;
|
|
149
|
+
/** Finding category (e.g., "security", "performance", "style", "bug") */
|
|
150
|
+
category?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
122
153
|
/**
|
|
123
154
|
* Result from a review check.
|
|
124
155
|
*/
|
|
@@ -129,6 +160,8 @@ export interface ReviewCheckResult {
|
|
|
129
160
|
output: string;
|
|
130
161
|
/** Exit code from the check process (if applicable) */
|
|
131
162
|
exitCode?: number;
|
|
163
|
+
/** Structured findings (optional — plugins can provide machine-readable results) */
|
|
164
|
+
findings?: ReviewFinding[];
|
|
132
165
|
}
|
|
133
166
|
|
|
134
167
|
/**
|
package/src/prd/index.ts
CHANGED
|
@@ -107,6 +107,7 @@ export function getNextStory(prd: PRD, currentStoryId?: string | null, maxRetrie
|
|
|
107
107
|
s.status !== "blocked" &&
|
|
108
108
|
s.status !== "failed" &&
|
|
109
109
|
s.status !== "paused" &&
|
|
110
|
+
s.status !== "decomposed" &&
|
|
110
111
|
s.dependencies.every((dep) => completedIds.has(dep)),
|
|
111
112
|
) ?? null
|
|
112
113
|
);
|
|
@@ -132,6 +133,7 @@ export function countStories(prd: PRD): {
|
|
|
132
133
|
skipped: number;
|
|
133
134
|
blocked: number;
|
|
134
135
|
paused: number;
|
|
136
|
+
decomposed: number;
|
|
135
137
|
} {
|
|
136
138
|
return {
|
|
137
139
|
total: prd.userStories.length,
|
|
@@ -144,11 +146,13 @@ export function countStories(prd: PRD): {
|
|
|
144
146
|
s.status !== "failed" &&
|
|
145
147
|
s.status !== "skipped" &&
|
|
146
148
|
s.status !== "blocked" &&
|
|
147
|
-
s.status !== "paused"
|
|
149
|
+
s.status !== "paused" &&
|
|
150
|
+
s.status !== "decomposed",
|
|
148
151
|
).length,
|
|
149
152
|
skipped: prd.userStories.filter((s) => s.status === "skipped").length,
|
|
150
153
|
blocked: prd.userStories.filter((s) => s.status === "blocked").length,
|
|
151
154
|
paused: prd.userStories.filter((s) => s.status === "paused").length,
|
|
155
|
+
decomposed: prd.userStories.filter((s) => s.status === "decomposed").length,
|
|
152
156
|
};
|
|
153
157
|
}
|
|
154
158
|
|
package/src/prd/types.ts
CHANGED
|
@@ -9,7 +9,15 @@ import type { ModelTier } from "../config";
|
|
|
9
9
|
import type { FailureCategory } from "../tdd/types";
|
|
10
10
|
|
|
11
11
|
/** User story status */
|
|
12
|
-
export type StoryStatus =
|
|
12
|
+
export type StoryStatus =
|
|
13
|
+
| "pending"
|
|
14
|
+
| "in-progress"
|
|
15
|
+
| "passed"
|
|
16
|
+
| "failed"
|
|
17
|
+
| "skipped"
|
|
18
|
+
| "blocked"
|
|
19
|
+
| "paused"
|
|
20
|
+
| "decomposed";
|
|
13
21
|
|
|
14
22
|
/** Verification stage where failure occurred */
|
|
15
23
|
export type VerificationStage = "verify" | "review" | "regression" | "rectification" | "agent-session" | "escalation";
|
|
@@ -38,6 +46,8 @@ export interface StructuredFailure {
|
|
|
38
46
|
summary: string;
|
|
39
47
|
/** Parsed test failures (if applicable) */
|
|
40
48
|
testFailures?: TestFailureContext[];
|
|
49
|
+
/** Structured review findings from plugin reviewers (e.g., semgrep, eslint) */
|
|
50
|
+
reviewFindings?: import("../plugins/types").ReviewFinding[];
|
|
41
51
|
/** ISO timestamp when failure was recorded */
|
|
42
52
|
timestamp: string;
|
|
43
53
|
}
|
package/src/review/types.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface PluginReviewerResult {
|
|
|
35
35
|
exitCode?: number;
|
|
36
36
|
/** Error message if reviewer threw an exception */
|
|
37
37
|
error?: string;
|
|
38
|
+
/** Structured findings from the reviewer (optional) */
|
|
39
|
+
findings?: import("../plugins/types").ReviewFinding[];
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/** Review phase result */
|
package/src/tdd/isolation.ts
CHANGED
|
@@ -55,7 +55,7 @@ function matchesAllowedPath(filePath: string, allowedPaths: string[]): boolean {
|
|
|
55
55
|
return allowedPaths.some((pattern) => {
|
|
56
56
|
// Simple glob matching: ** = any directory, * = any filename segment
|
|
57
57
|
const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
|
|
58
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
58
|
+
const regex = new RegExp(`^${regexPattern}$`); // nosemgrep: detect-non-literal-regexp — pattern from PRD scope config, not user input
|
|
59
59
|
return regex.test(filePath);
|
|
60
60
|
});
|
|
61
61
|
}
|