@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.
Files changed (38) hide show
  1. package/dist/nax.js +808 -104
  2. package/package.json +1 -1
  3. package/src/cli/analyze.ts +145 -0
  4. package/src/cli/config.ts +9 -0
  5. package/src/config/defaults.ts +8 -0
  6. package/src/config/schema.ts +1 -0
  7. package/src/config/schemas.ts +10 -0
  8. package/src/config/types.ts +18 -0
  9. package/src/context/elements.ts +13 -0
  10. package/src/context/greenfield.ts +1 -1
  11. package/src/decompose/apply.ts +44 -0
  12. package/src/decompose/builder.ts +181 -0
  13. package/src/decompose/index.ts +8 -0
  14. package/src/decompose/sections/codebase.ts +26 -0
  15. package/src/decompose/sections/constraints.ts +32 -0
  16. package/src/decompose/sections/index.ts +4 -0
  17. package/src/decompose/sections/sibling-stories.ts +25 -0
  18. package/src/decompose/sections/target-story.ts +31 -0
  19. package/src/decompose/types.ts +55 -0
  20. package/src/decompose/validators/complexity.ts +45 -0
  21. package/src/decompose/validators/coverage.ts +134 -0
  22. package/src/decompose/validators/dependency.ts +91 -0
  23. package/src/decompose/validators/index.ts +35 -0
  24. package/src/decompose/validators/overlap.ts +128 -0
  25. package/src/execution/escalation/tier-escalation.ts +9 -2
  26. package/src/execution/sequential-executor.ts +4 -3
  27. package/src/interaction/index.ts +1 -0
  28. package/src/interaction/triggers.ts +21 -0
  29. package/src/interaction/types.ts +7 -0
  30. package/src/pipeline/stages/review.ts +6 -0
  31. package/src/pipeline/stages/routing.ts +89 -0
  32. package/src/pipeline/types.ts +2 -0
  33. package/src/plugins/types.ts +33 -0
  34. package/src/prd/index.ts +5 -1
  35. package/src/prd/types.ts +11 -1
  36. package/src/review/orchestrator.ts +1 -0
  37. package/src/review/types.ts +2 -0
  38. 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
  };
@@ -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
  /**
@@ -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 = "pending" | "in-progress" | "passed" | "failed" | "skipped" | "blocked" | "paused";
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
  }
@@ -72,6 +72,7 @@ export class ReviewOrchestrator {
72
72
  passed: result.passed,
73
73
  output: result.output,
74
74
  exitCode: result.exitCode,
75
+ findings: result.findings,
75
76
  });
76
77
  if (!result.passed) {
77
78
  builtIn.pluginReviewers = pluginResults;
@@ -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 */
@@ -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
  }